diff --git a/.gitignore b/.gitignore index e32406c..26076f2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,11 +7,16 @@ **/.idea/ **/html/ **/.env -packages/client/public/static/fonts/* **/secrets.json **/.dev.vars -packages/client/public/sitemap.xml -packages/client/public/robots.txt wrangler.dev.jsonc /packages/client/public/static/fonts/ +/packages/client/public/robots.txt +/packages/client/public/sitemap.xml +/packages/client/public/assets/textures/bevy.png +/packages/client/public/assets/audio/flying.ogg +/packages/client/public/assets/textures/github.png +/packages/client/public/yachtpit.html +/packages/client/public/yachtpit.js +/packages/client/public/yachtpit_bg.wasm diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..df3cef0 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "crates/yachtpit"] + path = crates/yachtpit + url = https://github.com/seemueller-io/yachtpit.git diff --git a/bun.lock b/bun.lock index 7cd3872..c30c350 100644 --- a/bun.lock +++ b/bun.lock @@ -2,6 +2,9 @@ "lockfileVersion": 1, "workspaces": { "": { + "dependencies": { + "@chakra-ui/icons": "^2.2.4", + }, "devDependencies": { "@types/bun": "^1.2.17", "@typescript-eslint/eslint-plugin": "^8.35.0", @@ -32,6 +35,7 @@ "packages/client": { "name": "@open-gsio/client", "devDependencies": { + "@chakra-ui/icons": "^2.2.4", "@chakra-ui/react": "^2.10.6", "@cloudflare/workers-types": "^4.20241205.0", "@emotion/react": "^11.13.5", @@ -61,7 +65,6 @@ "mobx": "^6.13.5", "mobx-react-lite": "^4.0.7", "mobx-state-tree": "^6.0.1", - "moo": "^0.5.2", "qrcode.react": "^4.1.0", "react": "^19.1.0", "react-dom": "^19.1.0", @@ -69,10 +72,11 @@ "react-streaming": "^0.3.44", "react-textarea-autosize": "^8.5.5", "shiki": "^1.24.0", + "tslog": "^4.9.3", "typescript": "^5.7.2", "vike": "^0.4.235", "vite": "^7.0.0", - "vite-plugin-pwa": "^1.0.0", + "vite-plugin-pwa": "^1.0.1", "vitest": "^3.1.4", }, }, @@ -400,6 +404,8 @@ "@chakra-ui/hooks": ["@chakra-ui/hooks@2.4.4", "", { "dependencies": { "@chakra-ui/utils": "2.2.4", "@zag-js/element-size": "0.31.1", "copy-to-clipboard": "3.3.3", "framesync": "6.1.2" }, "peerDependencies": { "react": ">=18" } }, "sha512-+gMwLIkabtddIL/GICU7JmnYtvfONP+fNiTfdYLV9/I1eyCz8igKgLmFJOGM6F+BpUev6hh+/+DX5ezGQ9VTbQ=="], + "@chakra-ui/icons": ["@chakra-ui/icons@2.2.4", "", { "peerDependencies": { "@chakra-ui/react": ">=2.0.0", "react": ">=18" } }, "sha512-l5QdBgwrAg3Sc2BRqtNkJpfuLw/pWRDwwT58J6c4PqQT6wzXxyNa8Q0PForu1ltB5qEiFb1kxr/F/HO1EwNa6g=="], + "@chakra-ui/react": ["@chakra-ui/react@2.10.7", "", { "dependencies": { "@chakra-ui/hooks": "2.4.4", "@chakra-ui/styled-system": "2.12.2", "@chakra-ui/theme": "3.4.8", "@chakra-ui/utils": "2.2.4", "@popperjs/core": "^2.11.8", "@zag-js/focus-visible": "^0.31.1", "aria-hidden": "^1.2.3", "react-fast-compare": "3.2.2", "react-focus-lock": "^2.9.6", "react-remove-scroll": "^2.5.7" }, "peerDependencies": { "@emotion/react": ">=11", "@emotion/styled": ">=11", "framer-motion": ">=4.0.0", "react": ">=18", "react-dom": ">=18" } }, "sha512-GX1dCmnvrxxyZEofDX9GMAtRakZJKnUqFM9k8qhaycPaeyfkiTNNTjhPNX917hgVx1yhC3kcJOs5IeC7yW56/g=="], "@chakra-ui/styled-system": ["@chakra-ui/styled-system@2.12.2", "", { "dependencies": { "@chakra-ui/utils": "2.2.4", "csstype": "^3.1.2" } }, "sha512-BlQ7i3+GYC0S0c72B+paa0sYo+QeNSMfz6fwQRFsc8A5Aax9i9lSdRL+vwJVC+k6r/0HWfRwk016R2RD2ihEwQ=="], @@ -1768,6 +1774,8 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tslog": ["tslog@4.9.3", "", {}, "sha512-oDWuGVONxhVEBtschLf2cs/Jy8i7h1T+CpdkTNWQgdAF7DhRo2G8vMCgILKe7ojdEkLhICWgI1LYSSKaJsRgcw=="], + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], "type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], @@ -1844,7 +1852,7 @@ "vite-node": ["vite-node@3.1.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.0", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA=="], - "vite-plugin-pwa": ["vite-plugin-pwa@1.0.0", "", { "dependencies": { "debug": "^4.3.6", "pretty-bytes": "^6.1.1", "tinyglobby": "^0.2.10", "workbox-build": "^7.3.0", "workbox-window": "^7.3.0" }, "peerDependencies": { "@vite-pwa/assets-generator": "^1.0.0", "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "optionalPeers": ["@vite-pwa/assets-generator"] }, "sha512-X77jo0AOd5OcxmWj3WnVti8n7Kw2tBgV1c8MCXFclrSlDV23ePzv2eTDIALXI2Qo6nJ5pZJeZAuX0AawvRfoeA=="], + "vite-plugin-pwa": ["vite-plugin-pwa@1.0.1", "", { "dependencies": { "debug": "^4.3.6", "pretty-bytes": "^6.1.1", "tinyglobby": "^0.2.10", "workbox-build": "^7.3.0", "workbox-window": "^7.3.0" }, "peerDependencies": { "@vite-pwa/assets-generator": "^1.0.0", "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@vite-pwa/assets-generator"] }, "sha512-STyUomQbydj7vGamtgQYIJI0YsUZ3T4pJLGBQDQPhzMse6aGSncmEN21OV35PrFsmCvmtiH+Nu1JS1ke4RqBjQ=="], "vitest": ["vitest@3.1.4", "", { "dependencies": { "@vitest/expect": "3.1.4", "@vitest/mocker": "3.1.4", "@vitest/pretty-format": "^3.1.4", "@vitest/runner": "3.1.4", "@vitest/snapshot": "3.1.4", "@vitest/spy": "3.1.4", "@vitest/utils": "3.1.4", "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.13", "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", "vite-node": "3.1.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.1.4", "@vitest/ui": "3.1.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ=="], diff --git a/crates/yachtpit b/crates/yachtpit new file mode 160000 index 0000000..66b8a85 --- /dev/null +++ b/crates/yachtpit @@ -0,0 +1 @@ +Subproject commit 66b8a855b513c11511cbc3b59b96bca76e34c3a5 diff --git a/package.json b/package.json index ad68bbf..9cfca36 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ ], "scripts": { "clean": "packages/scripts/cleanup.sh", + "restore:submodules": "rm -rf crates/yachtpit && (git rm --cached crates/yachtpit) && git submodule add --force https://github.com/seemueller-io/yachtpit.git crates/yachtpit ", "test:all": "bun run --filter='*' tests", "client:dev": "(cd packages/client && bun run dev)", "server:dev": "bun build:client && (cd packages/server && bun run dev)", @@ -41,5 +42,8 @@ "peerDependencies": { "typescript": "^5.8.3" }, + "dependencies": { + "@chakra-ui/icons": "^2.2.4" + }, "packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39" } diff --git a/packages/client/package.json b/packages/client/package.json index 7f45c60..3c83dd5 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -8,7 +8,8 @@ "tests:coverage": "vitest run --coverage.enabled=true", "generate:sitemap": "bun ./scripts/generate_sitemap.js open-gsio.seemueller.workers.dev", "generate:robotstxt": "bun ./scripts/generate_robots_txt.js open-gsio.seemueller.workers.dev", - "generate:fonts": "cp -r ../../node_modules/katex/dist/fonts public/static" + "generate:fonts": "cp -r ../../node_modules/katex/dist/fonts public/static", + "generate:bevy:bundle": "bun scripts/generate-bevy-bundle.js" }, "exports": { "./server/index.ts": { @@ -17,19 +18,22 @@ } }, "devDependencies": { - "@open-gsio/env": "workspace:*", - "@open-gsio/scripts": "workspace:*", + "@chakra-ui/icons": "^2.2.4", "@chakra-ui/react": "^2.10.6", "@cloudflare/workers-types": "^4.20241205.0", "@emotion/react": "^11.13.5", "@emotion/styled": "^11.13.5", + "@open-gsio/env": "workspace:*", + "@open-gsio/scripts": "workspace:*", "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.5.2", + "@types/bun": "^1.2.17", "@types/marked": "^6.0.0", "@vitejs/plugin-react": "^4.3.4", "@vitest/coverage-v8": "^3.1.4", "@vitest/ui": "^3.1.4", + "bun": "^1.2.17", "chokidar": "^4.0.1", "framer-motion": "^11.13.1", "isomorphic-dompurify": "^2.19.0", @@ -44,7 +48,6 @@ "mobx": "^6.13.5", "mobx-react-lite": "^4.0.7", "mobx-state-tree": "^6.0.1", - "moo": "^0.5.2", "qrcode.react": "^4.1.0", "react": "^19.1.0", "react-dom": "^19.1.0", @@ -52,12 +55,11 @@ "react-streaming": "^0.3.44", "react-textarea-autosize": "^8.5.5", "shiki": "^1.24.0", + "tslog": "^4.9.3", "typescript": "^5.7.2", "vike": "^0.4.235", "vite": "^7.0.0", - "vite-plugin-pwa": "^1.0.0", - "vitest": "^3.1.4", - "bun": "^1.2.17", - "@types/bun": "^1.2.17" + "vite-plugin-pwa": "^1.0.1", + "vitest": "^3.1.4" } } diff --git a/packages/client/public/icon.ico b/packages/client/public/icon.ico new file mode 100644 index 0000000..8c7afbf Binary files /dev/null and b/packages/client/public/icon.ico differ diff --git a/packages/client/scripts/generate-bevy-bundle.js b/packages/client/scripts/generate-bevy-bundle.js new file mode 100644 index 0000000..414693f --- /dev/null +++ b/packages/client/scripts/generate-bevy-bundle.js @@ -0,0 +1,164 @@ +import { execSync } from 'node:child_process'; +import { + existsSync, + readdirSync, + readFileSync, + writeFileSync, + renameSync, + rmSync, + cpSync, +} from 'node:fs'; +import { resolve, dirname, join, basename } from 'node:path'; + +import { Logger } from 'tslog'; +const logger = new Logger({ + stdio: 'inherit', + prettyLogTimeZone: 'local', + type: 'pretty', + stylePrettyLogs: true, + prefix: ['\n'], + overwrite: true, +}); + +function main() { + bundleCrate(); + cleanup(); + logger.info('πŸŽ‰ yachtpit built successfully'); +} + +const getRepoRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim(); +const repoRoot = resolve(getRepoRoot); +const publicDir = resolve(repoRoot, 'packages/client/public'); +const indexHtml = resolve(publicDir, 'index.html'); + +function bundleCrate() { + // ───────────── Build yachtpit project ─────────────────────────────────── + logger.info('πŸ”¨ Building yachtpit...'); + + logger.info(`πŸ“ Repository root: ${repoRoot}`); + + // Check if submodules need to be initialized + const yachtpitPath = resolve(repoRoot, 'crates/yachtpit'); + logger.info(`πŸ“ Yachtpit path: ${yachtpitPath}`); + + if (!existsSync(yachtpitPath)) { + logger.info('πŸ“¦ Initializing submodules...'); + execSync('git submodule update --init --remote', { stdio: 'inherit' }); + } else { + logger.info(`βœ… Submodules already initialized at: ${yachtpitPath}`); + } + + // Build the yachtpit project + const buildCwd = resolve(repoRoot, 'crates/yachtpit'); + logger.info(`πŸ”¨ Building in directory: ${buildCwd}`); + + try { + execSync('trunk build --release', { + cwd: buildCwd, + }); + logger.info('βœ… Yachtpit built'); + } catch (error) { + console.error('❌ Failed to build yachtpit:', error.message); + process.exit(1); + } + + // ───────────── Copy assets to public directory ────────────────────────── + const yachtpitDistDir = join(yachtpitPath, 'dist'); + + logger.info(`πŸ“‹ Copying assets to public directory...`); + + // Remove existing yachtpit assets from public directory + const skipRemoveOldAssets = false; + + if (!skipRemoveOldAssets) { + const existingAssets = readdirSync(publicDir).filter( + file => file.startsWith('yachtpit') && (file.endsWith('.js') || file.endsWith('.wasm')), + ); + + existingAssets.forEach(asset => { + const assetPath = join(publicDir, asset); + rmSync(assetPath, { force: true }); + logger.info(`πŸ—‘οΈ Removed old asset: ${assetPath}`); + }); + } else { + logger.warn('SKIPPING REMOVING OLD ASSETS'); + } + + // Copy new assets from yachtpit/dist to public directory + if (existsSync(yachtpitDistDir)) { + logger.info(`πŸ“Located yachtpit build: ${yachtpitDistDir}`); + try { + cpSync(yachtpitDistDir, publicDir, { + recursive: true, + force: true, + }); + logger.info(`βœ… Assets copied from ${yachtpitDistDir} to ${publicDir}`); + } catch (error) { + console.error('❌ Failed to copy assets:', error.message); + process.exit(1); + } + } else { + console.error(`❌ Yachtpit dist directory not found at: ${yachtpitDistDir}`); + process.exit(1); + } + + // ───────────── locate targets ─────────────────────────────────────────── + const dstPath = join(publicDir, 'yachtpit.html'); + + // Regexes for the hashed filenames produced by most bundlers + const JS_RE = /^yachtpit-[\da-f]{16}\.js$/i; + const WASM_RE = /^yachtpit-[\da-f]{16}_bg\.wasm$/i; + + // Always perform renaming of bundle files + const files = readdirSync(publicDir); + + // helper that doesn't explode if the target file is already present + const safeRename = (from, to) => { + if (!existsSync(from)) return; + if (existsSync(to)) { + logger.info(`ℹ️ ${to} already exists – removing and replacing.`); + rmSync(to, { force: true }); + } + renameSync(from, to); + logger.info(`πŸ“ Renamed: ${basename(from)} β†’ ${basename(to)}`); + }; + + files.forEach(f => { + const fullPath = join(publicDir, f); + if (JS_RE.test(f)) safeRename(fullPath, join(publicDir, 'yachtpit.js')); + if (WASM_RE.test(f)) safeRename(fullPath, join(publicDir, 'yachtpit_bg.wasm')); + }); + + // ───────────── patch markup inside HTML ───────────────────────────────── + if (existsSync(indexHtml)) { + logger.info(`πŸ“ Patching HTML file: ${indexHtml}`); + let html = readFileSync(indexHtml, 'utf8'); + + html = html + .replace(/yachtpit-[\da-f]{16}\.js/gi, 'yachtpit.js') + .replace(/yachtpit-[\da-f]{16}_bg\.wasm/gi, 'yachtpit_bg.wasm'); + + writeFileSync(indexHtml, html, 'utf8'); + + // ───────────── rename HTML entrypoint ───────────────────────────────── + if (basename(indexHtml) !== 'yachtpit.html') { + logger.info(`πŸ“ Renaming HTML file: ${indexHtml} β†’ ${dstPath}`); + // Remove existing yachtpit.html if it exists + if (existsSync(dstPath)) { + rmSync(dstPath, { force: true }); + } + renameSync(indexHtml, dstPath); + } + } else { + logger.info(`⚠️ ${indexHtml} not found – skipping HTML processing.`); + } +} + +function cleanup() { + logger.info('Running cleanup...'); + rmSync(indexHtml, { force: true }); + const creditsDir = resolve(`${repoRoot}/packages/client/public`, 'credits'); + rmSync(creditsDir, { force: true, recursive: true }); +} + +main(); diff --git a/packages/client/src/components/landing-component/BevyScene.tsx b/packages/client/src/components/landing-component/BevyScene.tsx new file mode 100644 index 0000000..e92d178 --- /dev/null +++ b/packages/client/src/components/landing-component/BevyScene.tsx @@ -0,0 +1,45 @@ +import { Box } from '@chakra-ui/react'; +import { useEffect } from 'react'; + +export interface BevySceneProps { + speed?: number; // transition seconds + intensity?: number; + glow?: boolean; +} + +export const BevyScene: React.FC = ({ speed = 1, intensity = 1, glow = false }) => { + useEffect(() => { + const script = document.createElement('script'); + script.src = '/yachtpit.js'; + script.type = 'module'; + document.body.appendChild(script); + script.onload = loaded => { + console.log('loaded', loaded); + }; + }, []); + + return ( + + + + {/**/} + + ); +}; diff --git a/packages/client/src/components/landing-component/LandingComponent.tsx b/packages/client/src/components/landing-component/LandingComponent.tsx new file mode 100644 index 0000000..dc34a99 --- /dev/null +++ b/packages/client/src/components/landing-component/LandingComponent.tsx @@ -0,0 +1,102 @@ +import { Box } from '@chakra-ui/react'; +import React, { useState } from 'react'; + +import { BevyScene } from './BevyScene.tsx'; +import { MatrixRain } from './MatrixRain.tsx'; +import Particles from './Particles.tsx'; +import Tweakbox from './Tweakbox.tsx'; + +export const LandingComponent: React.FC = () => { + const [speed, setSpeed] = useState(0.2); + const [intensity, setIntensity] = useState(0.5); + const [particles, setParticles] = useState(false); + const [glow, setGlow] = useState(false); + const [matrixRain, setMatrixRain] = useState(false); + const [bevyScene, setBevyScene] = useState(true); + + return ( + + + + + {!particles && !matrixRain && } + {!particles && matrixRain && } + {particles && } + + ); +}; diff --git a/packages/client/src/components/landing-component/MatrixRain.tsx b/packages/client/src/components/landing-component/MatrixRain.tsx new file mode 100644 index 0000000..cbeff94 --- /dev/null +++ b/packages/client/src/components/landing-component/MatrixRain.tsx @@ -0,0 +1,117 @@ +import { useBreakpointValue, useTheme } from '@chakra-ui/react'; +import React, { useEffect, useRef, useMemo } from 'react'; + +const MATRIX_CHARS = + 'をむウエγ‚ͺγ‚«γ‚­γ‚―γ‚±γ‚³γ‚΅γ‚·γ‚Ήγ‚»γ‚½γ‚Ώγƒγƒ„γƒ†γƒˆγƒŠγƒ‹γƒŒγƒγƒŽγƒγƒ’γƒ•γƒ˜γƒ›0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + +interface MatrixRainProps { + speed?: number; + glow?: boolean; + intensity?: number; +} + +export const MatrixRain: React.FC = ({ + speed = 1, + glow = false, + intensity = 1, +}) => { + const fontSize = useBreakpointValue({ base: 14, md: 18, lg: 22 }) ?? 14; + const theme = useTheme(); + const canvasRef = useRef(null); + const animationRef = useRef(null); + const dropsRef = useRef([]); + const columnsRef = useRef(0); + + const colors = useMemo( + () => ({ + background: theme.colors.background.primary, + textAccent: theme.colors.text.accent, + }), + [theme.colors.background.primary, theme.colors.text.accent], + ); + + const colorsRef = useRef(colors); + colorsRef.current = colors; + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + const resize = () => { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + + const newColumns = Math.floor(canvas.width / fontSize); + if (newColumns !== columnsRef.current) { + columnsRef.current = newColumns; + const newDrops: number[] = []; + + for (let i = 0; i < newColumns; i++) { + if (i < dropsRef.current.length) { + newDrops[i] = dropsRef.current[i]; + } else { + newDrops[i] = Math.random() * (canvas.height / fontSize); + } + } + dropsRef.current = newDrops; + } + }; + + resize(); + window.addEventListener('resize', resize); + + if (dropsRef.current.length === 0) { + const columns = Math.floor(canvas.width / fontSize); + columnsRef.current = columns; + + for (let i = 0; i < columns; i++) { + dropsRef.current[i] = Math.random() * (canvas.height / fontSize); + } + } + + const draw = () => { + if (!ctx || !canvas) return; + + const currentColors = colorsRef.current; + + ctx.fillStyle = currentColors.background; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.font = `${fontSize}px monospace`; + + for (let i = 0; i < dropsRef.current.length; i++) { + const text = MATRIX_CHARS[Math.floor(Math.random() * MATRIX_CHARS.length)]; + const x = i * fontSize; + const y = dropsRef.current[i] * fontSize; + + ctx.fillStyle = currentColors.textAccent; + if (glow) { + ctx.shadowBlur = 10; + ctx.shadowColor = currentColors.textAccent; + } + ctx.fillText(text, x, y); + + if (y > canvas.height) { + dropsRef.current[i] = -Math.random() * 5; + } else { + dropsRef.current[i] += (0.1 + Math.random() * 0.5) * speed * intensity; + } + } + + animationRef.current = requestAnimationFrame(draw); + }; + + animationRef.current = requestAnimationFrame(draw); + + return () => { + window.removeEventListener('resize', resize); + if (animationRef.current) { + cancelAnimationFrame(animationRef.current); + } + }; + }, [fontSize, speed, glow, intensity]); + + return ; +}; diff --git a/packages/client/src/components/landing-component/Particles.tsx b/packages/client/src/components/landing-component/Particles.tsx new file mode 100644 index 0000000..619f5e0 --- /dev/null +++ b/packages/client/src/components/landing-component/Particles.tsx @@ -0,0 +1,161 @@ +import { Box, useTheme } from '@chakra-ui/react'; +import React, { useEffect, useRef } from 'react'; + +interface ParticlesProps { + speed: number; + intensity: number; + particles: boolean; + glow: boolean; +} + +interface Particle { + x: number; + y: number; + vx: number; + vy: number; + size: number; +} + +const Particles: React.FC = ({ speed, intensity, particles, glow }) => { + const canvasRef = useRef(null); + const particlesRef = useRef([]); + const animationFrameRef = useRef(undefined); + const theme = useTheme(); + + // Helper function to create a single particle with proper canvas dimensions + const createParticle = (canvas: HTMLCanvasElement): Particle => ({ + x: Math.random() * canvas.parentElement!.getBoundingClientRect().width, + y: Math.random() * canvas.parentElement!.getBoundingClientRect().height, + vx: (Math.random() - 0.5) * speed, + vy: (Math.random() - 0.5) * speed, + size: Math.random() * 3 + 1, + }); + + // Main animation effect + useEffect(() => { + if (!particles) { + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + animationFrameRef.current = undefined; + } + particlesRef.current = []; // Clear particles when disabled + return; + } + + const canvas = canvasRef.current; + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + const resizeCanvas = () => { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + + // Reposition existing particles that are outside new bounds + particlesRef.current.forEach(particle => { + if (particle.x > canvas.width) particle.x = Math.random() * canvas.width; + if (particle.y > canvas.height) particle.y = Math.random() * canvas.height; + }); + }; + + const ensureParticleCount = () => { + const targetCount = Math.floor(intensity * 100); + const currentCount = particlesRef.current.length; + + if (currentCount < targetCount) { + // Add new particles + const newParticles = Array.from({ length: targetCount - currentCount }, () => + createParticle(canvas), + ); + particlesRef.current = [...particlesRef.current, ...newParticles]; + } else if (currentCount > targetCount) { + // Remove excess particles + particlesRef.current = particlesRef.current.slice(0, targetCount); + } + }; + + const updateParticles = () => { + particlesRef.current.forEach(particle => { + particle.x += particle.vx; + particle.y += particle.vy; + + if (particle.x < 0) particle.x = canvas.width; + if (particle.x > canvas.width) particle.x = 0; + if (particle.y < 0) particle.y = canvas.height; + if (particle.y > canvas.height) particle.y = 0; + }); + }; + + const drawParticles = () => { + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = theme.colors.text.accent; + ctx.globalCompositeOperation = 'lighter'; + + if (glow) { + ctx.shadowBlur = 10; + ctx.shadowColor = 'white'; + } else { + ctx.shadowBlur = 0; + } + + particlesRef.current.forEach(particle => { + ctx.beginPath(); + ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2); + ctx.fill(); + }); + }; + + const animate = () => { + updateParticles(); + drawParticles(); + animationFrameRef.current = requestAnimationFrame(animate); + }; + + const handleResize = () => { + resizeCanvas(); + }; + + window.addEventListener('resize', handleResize); + resizeCanvas(); // Set canvas size first + ensureParticleCount(); // Then create particles with proper dimensions + animate(); + + return () => { + window.removeEventListener('resize', handleResize); + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + animationFrameRef.current = undefined; + } + }; + }, [particles, intensity, speed, glow, theme.colors.text.accent]); + + // Separate effect for speed changes - update existing particle velocities + useEffect(() => { + if (!particles) return; + + particlesRef.current.forEach(particle => { + const currentSpeed = Math.sqrt(particle.vx * particle.vx + particle.vy * particle.vy); + if (currentSpeed > 0) { + const normalizedVx = particle.vx / currentSpeed; + const normalizedVy = particle.vy / currentSpeed; + particle.vx = normalizedVx * speed; + particle.vy = normalizedVy * speed; + } else { + particle.vx = (Math.random() - 0.5) * speed; + particle.vy = (Math.random() - 0.5) * speed; + } + }); + }, [speed, particles]); + + return ( + + + + ); +}; + +export default Particles; diff --git a/packages/client/src/components/landing-component/Tweakbox.tsx b/packages/client/src/components/landing-component/Tweakbox.tsx new file mode 100644 index 0000000..ee6d4d5 --- /dev/null +++ b/packages/client/src/components/landing-component/Tweakbox.tsx @@ -0,0 +1,111 @@ +import { + Box, + Grid, + GridItem, + Heading, + Slider, + SliderTrack, + SliderFilledTrack, + SliderThumb, + Text, + Switch, + Collapse, + IconButton, +} from '@chakra-ui/react'; +import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react'; +import { observer } from 'mobx-react-lite'; +import React, { useState } from 'react'; + +interface SliderControl { + value: number; + onChange: (value: number) => void; + label: string; + min: number; + max: number; + step: number; + ariaLabel: string; +} + +interface SwitchControl { + value: boolean; + onChange: (enabled: boolean) => void; + label: string; + exclusive?: boolean; +} + +interface TweakboxProps { + sliders: { + speed: SliderControl; + intensity: SliderControl; + }; + switches: { + particles: SwitchControl; + glow: SwitchControl; + } & Record; +} + +const Tweakbox = observer(({ sliders, switches }: TweakboxProps) => { + const [isCollapsed, setIsCollapsed] = useState(false); + + return ( + + : } + onClick={() => setIsCollapsed(!isCollapsed)} + size="sm" + marginRight={2} + /> + + + + + + + {Object.keys(switches).map(key => { + return ( + + + {switches[key].label} + + switches[key].onChange(e.target.checked)} + /> + + ); + })} + {Object.entries(sliders).map(([key, slider]) => ( + + + {slider.label} + + + + + + + + + ))} + + + + + ); +}); + +export default Tweakbox; diff --git a/packages/client/src/layout/Hero.tsx b/packages/client/src/layout/Hero.tsx index 2128e7e..849af24 100644 --- a/packages/client/src/layout/Hero.tsx +++ b/packages/client/src/layout/Hero.tsx @@ -17,9 +17,9 @@ export default function Hero() { minWidth="90px" maxWidth={'220px'} color="text.accent" - as="h3" + // as="h3" letterSpacing={'tight'} - size="lg" + size="xl" > {Routes[normalizePath(pageContext.urlPathname)]?.heroLabel} diff --git a/packages/client/src/layout/Navigation.tsx b/packages/client/src/layout/Navigation.tsx index 43b4a2f..91b7d9f 100644 --- a/packages/client/src/layout/Navigation.tsx +++ b/packages/client/src/layout/Navigation.tsx @@ -1,4 +1,4 @@ -import { Box, Collapse, Grid, GridItem, useBreakpointValue } from '@chakra-ui/react'; +import { Box, Collapse, Grid, GridItem, useBreakpointValue, useTheme } from '@chakra-ui/react'; import { MenuIcon } from 'lucide-react'; import { observer } from 'mobx-react-lite'; import React, { useEffect } from 'react'; @@ -18,6 +18,8 @@ const Navigation = observer(({ children, routeRegistry }) => { const currentPath = pageContext.urlPathname || '/'; + const theme = useTheme(); + const getTopValue = () => { if (!isMobile) return undefined; if (currentPath === '/') return 12; @@ -53,9 +55,10 @@ const Navigation = observer(({ children, routeRegistry }) => { { switch (menuState.isOpen) { case true: diff --git a/packages/client/src/layout/theme/color-themes/OneDark.ts b/packages/client/src/layout/theme/color-themes/OneDark.ts index 37f02b6..6ffd135 100644 --- a/packages/client/src/layout/theme/color-themes/OneDark.ts +++ b/packages/client/src/layout/theme/color-themes/OneDark.ts @@ -15,8 +15,8 @@ export default { }, background: { - primary: 'linear-gradient(360deg, #15171C 100%, #353A47 100%)', - + // primary: 'linear-gradient(360deg, #15171C 100%, #353A47 100%)', + primary: '#15171C', secondary: '#1B1F26', tertiary: '#1E1E2E', }, diff --git a/packages/client/src/pages/+client.ts b/packages/client/src/pages/+client.ts index 3cebda6..9c8c27d 100644 --- a/packages/client/src/pages/+client.ts +++ b/packages/client/src/pages/+client.ts @@ -2,3 +2,20 @@ import UserOptionsStore from '../stores/UserOptionsStore'; UserOptionsStore.initialize(); + +try { + const isLocal = window.location.hostname.includes('localhost'); + if (!isLocal) { + navigator.serviceWorker.register('/service-worker.js'); + } else { + (async () => { + await navigator.serviceWorker.getRegistrations().then(registrations => { + registrations.map(r => { + r.unregister(); + }); + }); + })(); + } +} catch (e) { + // fail silent +} diff --git a/packages/client/src/pages/index/+Page.tsx b/packages/client/src/pages/index/+Page.tsx index e48e82a..0b9db0a 100644 --- a/packages/client/src/pages/index/+Page.tsx +++ b/packages/client/src/pages/index/+Page.tsx @@ -2,6 +2,7 @@ import { Stack } from '@chakra-ui/react'; import React, { useEffect } from 'react'; import Chat from '../../components/chat/Chat'; +import { LandingComponent } from '../../components/landing-component/LandingComponent.tsx'; import clientChatStore from '../../stores/ClientChatStore'; // renders "/" @@ -18,7 +19,8 @@ export default function IndexPage() { return ( - + + {/**/} ); } diff --git a/packages/client/vite.config.ts b/packages/client/vite.config.ts index 1ec1a8e..20dd840 100644 --- a/packages/client/vite.config.ts +++ b/packages/client/vite.config.ts @@ -17,6 +17,10 @@ const prebuildPlugin = () => ({ console.log('Generated robots.txt -> public/robots.txt'); child_process.execSync('bun run generate:fonts'); console.log('Copied fonts -> public/static/fonts'); + child_process.execSync('bun run generate:bevy:bundle', { + stdio: 'inherit', + }); + console.log('Bundled bevy app -> public/yachtpit.html'); } }, }); @@ -31,6 +35,26 @@ export default defineConfig(({ command }) => { prerender: true, disableAutoFullBuild: false, }), + VitePWA({ + registerType: 'autoUpdate', + injectRegister: null, + minify: true, + disable: false, + filename: 'service-worker.js', + devOptions: { + enabled: false, + }, + manifest: { + name: 'open-gsio', + short_name: 'open-gsio', + description: 'Assistant', + }, + workbox: { + globPatterns: ['**/*.{js,css,html,ico,png,svg,wasm}'], + navigateFallbackDenylist: [/^\/api\//], + maximumFileSizeToCacheInBytes: 25000000, + }, + }), // PWA plugin saves money on data transfer by caching assets on the client /* For safari, use this script in the console to unregister the service worker. @@ -41,21 +65,6 @@ export default defineConfig(({ command }) => { }) }) */ - // VitePWA({ - // registerType: 'autoUpdate', - // devOptions: { - // enabled: false, - // }, - // manifest: { - // name: "open-gsio", - // short_name: "open-gsio", - // description: "Assistant" - // }, - // workbox: { - // globPatterns: ['**/*.{js,css,html,ico,png,svg}'], - // navigateFallbackDenylist: [/^\/api\//], - // } - // }) ], server: { port: 3000, diff --git a/packages/cloudflare-workers/open-gsio/wrangler.jsonc b/packages/cloudflare-workers/open-gsio/wrangler.jsonc index fa9c5b3..2f2789e 100644 --- a/packages/cloudflare-workers/open-gsio/wrangler.jsonc +++ b/packages/cloudflare-workers/open-gsio/wrangler.jsonc @@ -20,9 +20,10 @@ { "binding": "KV_STORAGE", // $ npx wrangler kv namespace create open-gsio + // $ npx wrangler kv namespace create open-gsio "id": "placeholderId", // $ npx wrangler kv namespace create open-gsio --preview - "preview_id": "placeholderIdPreview" + "preview_id": "placeholderId" } ], "migrations": [ diff --git a/packages/scripts/cleanup.sh b/packages/scripts/cleanup.sh index 42b6fe3..8611c8f 100755 --- a/packages/scripts/cleanup.sh +++ b/packages/scripts/cleanup.sh @@ -15,7 +15,12 @@ find . -name ".wrangler" -type d -prune -exec rm -rf {} \; # Remove build directories find . -name "dist" -type d -prune -exec rm -rf {} \; -find . -name "build" -type d -prune -exec rm -rf {} \; + + +#----- +# crates/yachtpit uses a directory called build for staging assets so it can't be removed +#find . -name "build" -type d -prune -exec rm -rf {} \; +#----- find . -name "fonts" -type d -prune -exec rm -rf {} \;