diff --git a/.gitignore b/.gitignore index e32406c..ad8cf16 100644 --- a/.gitignore +++ b/.gitignore @@ -7,11 +7,20 @@ **/.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/yachtpit.html +/packages/client/public/yachtpit.js +/packages/client/public/yachtpit_bg.wasm +/packages/client/public/assets/ +/packages/client/public/apple-touch-icon-180x180.png +/packages/client/public/icon.ico +/packages/client/public/maskable-icon-512x512.png +/packages/client/public/pwa-64x64.png +/packages/client/public/pwa-192x192.png +/packages/client/public/pwa-512x512.png 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 56aa0a7..225c662 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", @@ -43,6 +47,7 @@ "@testing-library/user-event": "^14.5.2", "@types/bun": "^1.2.17", "@types/marked": "^6.0.0", + "@vite-pwa/assets-generator": "^1.0.0", "@vitejs/plugin-react": "^4.3.4", "@vitest/coverage-v8": "^3.1.4", "@vitest/ui": "^3.1.4", @@ -61,18 +66,19 @@ "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", "react-icons": "^5.4.0", "react-streaming": "^0.4.2", "react-textarea-autosize": "^8.5.5", + "react-use-pwa-install": "^1.0.3", "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", }, }, @@ -396,10 +402,14 @@ "@brillout/vite-plugin-server-entry": ["@brillout/vite-plugin-server-entry@0.7.9", "", { "dependencies": { "@brillout/import": "^0.2.6", "@brillout/picocolors": "^1.0.26" } }, "sha512-mJrUakPTj8Zf3Pm4beKmHowfozHOLS/deMqheYYIlqK8FSR4Hd3vMeFQBL/rxLLd+svIlW/j2K3M2SVPDmvX7A=="], + "@canvas/image-data": ["@canvas/image-data@1.0.0", "", {}, "sha512-BxOqI5LgsIQP1odU5KMwV9yoijleOPzHL18/YvNqF9KFSGF2K/DLlYAbDQsWqd/1nbaFuSkYD/191dpMtNh4vw=="], + "@chakra-ui/anatomy": ["@chakra-ui/anatomy@2.3.6", "", {}, "sha512-TjmjyQouIZzha/l8JxdBZN1pKZTj7sLpJ0YkFnQFyqHcbfWggW9jKWzY1E0VBnhtFz/xF3KC6UAVuZVSJx+y0g=="], "@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=="], @@ -648,6 +658,8 @@ "@popperjs/core": ["@popperjs/core@2.11.8", "", {}, "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="], + "@quansync/fs": ["@quansync/fs@0.1.3", "", { "dependencies": { "quansync": "^0.2.10" } }, "sha512-G0OnZbMWEs5LhDyqy2UL17vGhSVHkQIfVojMtEWVenvj0V5S84VBgy86kJIuNsGDp2p7sTKlpSIpBUWdC35OKg=="], + "@rollup/plugin-babel": ["@rollup/plugin-babel@5.3.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.10.4", "@rollup/pluginutils": "^3.1.0" }, "peerDependencies": { "@babel/core": "^7.0.0", "@types/babel__core": "^7.1.9", "rollup": "^1.20.0||^2.0.0" }, "optionalPeers": ["@types/babel__core"] }, "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q=="], "@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@15.3.1", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA=="], @@ -786,6 +798,8 @@ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + "@vite-pwa/assets-generator": ["@vite-pwa/assets-generator@1.0.0", "", { "dependencies": { "cac": "^6.7.14", "colorette": "^2.0.20", "consola": "^3.4.2", "sharp": "^0.33.5", "sharp-ico": "^0.1.5", "unconfig": "^7.3.1" }, "bin": { "pwa-assets-generator": "bin/pwa-assets-generator.mjs" } }, "sha512-tWRF/tsqGkND5+dDVnJz7DzQkIRjtTRRYvA3y6l4FwTwK47OK72p1X7ResSz6T7PimIZMuFd+arsB8NRIG+Sww=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@4.3.4", "", { "dependencies": { "@babel/core": "^7.26.0", "@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.14.2" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug=="], "@vitest/coverage-v8": ["@vitest/coverage-v8@3.1.4", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^1.0.2", "debug": "^4.4.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.1.7", "magic-string": "^0.30.17", "magicast": "^0.3.5", "std-env": "^3.9.0", "test-exclude": "^7.0.1", "tinyrainbow": "^2.0.0" }, "peerDependencies": { "@vitest/browser": "3.1.4", "vitest": "3.1.4" }, "optionalPeers": ["@vitest/browser"] }, "sha512-G4p6OtioySL+hPV7Y6JHlhpsODbJzt1ndwHAFkyk6vVjpK03PFsKnauZIzcd0PrK4zAbc5lc+jeZ+eNGiMA+iw=="], @@ -920,6 +934,8 @@ "color2k": ["color2k@2.0.3", "", {}, "sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog=="], + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], @@ -930,6 +946,8 @@ "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + "convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], "cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="], @@ -964,6 +982,10 @@ "decimal.js": ["decimal.js@10.5.0", "", {}, "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw=="], + "decode-bmp": ["decode-bmp@0.2.1", "", { "dependencies": { "@canvas/image-data": "^1.0.0", "to-data-view": "^1.1.0" } }, "sha512-NiOaGe+GN0KJqi2STf24hfMkFitDUaIoUU3eKvP/wAbLe8o6FuW5n/x7MHPR0HKvBokp6MQY/j7w8lewEeVCIA=="], + + "decode-ico": ["decode-ico@0.4.1", "", { "dependencies": { "@canvas/image-data": "^1.0.0", "decode-bmp": "^0.2.0", "to-data-view": "^1.1.0" } }, "sha512-69NZfbKIzux1vBOd31al3XnMnH+2mqDhEgLdpygErm4d60N+UwA5Sq5WFjmEDQzumgB9fElojGwWG0vybVfFmA=="], + "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], "deep-equal": ["deep-equal@2.2.3", "", { "dependencies": { "array-buffer-byte-length": "^1.0.0", "call-bind": "^1.0.5", "es-get-iterator": "^1.1.3", "get-intrinsic": "^1.2.2", "is-arguments": "^1.1.1", "is-array-buffer": "^3.0.2", "is-date-object": "^1.0.5", "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", "isarray": "^2.0.5", "object-is": "^1.1.5", "object-keys": "^1.1.1", "object.assign": "^4.1.4", "regexp.prototype.flags": "^1.5.1", "side-channel": "^1.0.4", "which-boxed-primitive": "^1.0.2", "which-collection": "^1.0.1", "which-typed-array": "^1.1.13" } }, "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA=="], @@ -1190,6 +1212,8 @@ "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "ico-endec": ["ico-endec@0.1.6", "", {}, "sha512-ZdLU38ZoED3g1j3iEyzcQj+wAkY2xfWNkymszfJPoxucIUhK7NayQ+/C4Kv0nDFMIsbtbEHldv3V8PU494/ueQ=="], + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "idb": ["idb@7.1.1", "", {}, "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ=="], @@ -1298,6 +1322,8 @@ "jake": ["jake@10.9.2", "", { "dependencies": { "async": "^3.2.3", "chalk": "^4.0.2", "filelist": "^1.0.4", "minimatch": "^3.1.2" }, "bin": { "jake": "bin/cli.js" } }, "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA=="], + "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], + "js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], @@ -1538,8 +1564,12 @@ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "pwa-install-handler": ["pwa-install-handler@2.6.2", "", {}, "sha512-9hMpqWNxGZx4ZoBe9k9gHkdZC/d/mvMJLA08FCVVMxOhwHBNuQVzb0DwH8ffEaqFvqu7GaotcvYgGNT1yVWduQ=="], + "qrcode.react": ["qrcode.react@4.2.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA=="], + "quansync": ["quansync@0.2.10", "", {}, "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A=="], + "querystringify": ["querystringify@2.2.0", "", {}, "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], @@ -1572,6 +1602,8 @@ "react-textarea-autosize": ["react-textarea-autosize@8.5.9", "", { "dependencies": { "@babel/runtime": "^7.20.13", "use-composed-ref": "^1.3.0", "use-latest": "^1.2.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A=="], + "react-use-pwa-install": ["react-use-pwa-install@1.0.3", "", { "dependencies": { "pwa-install-handler": "^2.6.2" }, "peerDependencies": { "react": "18 || 19" } }, "sha512-poF5teATOCblAchP61+Hx/FIQJtSkjGFcZsJjiyXmG9SfmJWkj8M890lXKlu6QPg/bmG6GE3d+KP3aEO9ehgDw=="], + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], @@ -1642,6 +1674,8 @@ "sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], + "sharp-ico": ["sharp-ico@0.1.5", "", { "dependencies": { "decode-ico": "*", "ico-endec": "*", "sharp": "*" } }, "sha512-a3jODQl82NPp1d5OYb0wY+oFaPk7AvyxipIowCHk7pBsZCWgbe0yAkU2OOXdoH0ENyANhyOQbs9xkAiRHcF02Q=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], @@ -1750,6 +1784,8 @@ "tldts-core": ["tldts-core@6.1.85", "", {}, "sha512-DTjUVvxckL1fIoPSb3KE7ISNtkWSawZdpfxGxwiIrZoO6EbHVDXXUIlIuWympPaeS+BLGyggozX/HTMsRAdsoA=="], + "to-data-view": ["to-data-view@1.1.0", "", {}, "sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "toggle-selection": ["toggle-selection@1.0.6", "", {}, "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="], @@ -1768,6 +1804,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=="], @@ -1786,6 +1824,8 @@ "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], + "unconfig": ["unconfig@7.3.2", "", { "dependencies": { "@quansync/fs": "^0.1.1", "defu": "^6.1.4", "jiti": "^2.4.2", "quansync": "^0.2.8" } }, "sha512-nqG5NNL2wFVGZ0NA/aCFw0oJ2pxSf1lwg4Z5ill8wd7K4KX/rQbHlwbh+bjctXL5Ly1xtzHenHGOK0b+lG6JVg=="], + "undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], @@ -1844,7 +1884,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..1abe004 --- /dev/null +++ b/crates/yachtpit @@ -0,0 +1 @@ +Subproject commit 1abe0047fc20aafe6c8b4393cd4ebb98450c8d2f 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 8a0be2b..83c33ce 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -8,7 +8,9 @@ "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", + "generate:pwa:assets": "test ! -f public/pwa-64x64.png && pwa-assets-generator --preset minimal-2023 public/logo.png || echo 'PWA assets already exist'" }, "exports": { "./server/index.ts": { @@ -17,19 +19,23 @@ } }, "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", + "@vite-pwa/assets-generator": "^1.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,20 +50,19 @@ "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", "react-icons": "^5.4.0", "react-streaming": "^0.4.2", "react-textarea-autosize": "^8.5.5", + "react-use-pwa-install": "^1.0.3", "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/android-chrome-192x192.png b/packages/client/public/android-chrome-192x192.png deleted file mode 100644 index 1cc9518..0000000 Binary files a/packages/client/public/android-chrome-192x192.png and /dev/null differ diff --git a/packages/client/public/android-chrome-512x512.png b/packages/client/public/android-chrome-512x512.png deleted file mode 100644 index 3d12b3a..0000000 Binary files a/packages/client/public/android-chrome-512x512.png and /dev/null differ diff --git a/packages/client/public/apple-touch-icon.png b/packages/client/public/apple-touch-icon.png deleted file mode 100644 index 156b7a7..0000000 Binary files a/packages/client/public/apple-touch-icon.png and /dev/null differ diff --git a/packages/client/public/code-tokenizer-md.jpg b/packages/client/public/code-tokenizer-md.jpg deleted file mode 100644 index 029e96f..0000000 Binary files a/packages/client/public/code-tokenizer-md.jpg and /dev/null differ diff --git a/packages/client/public/favicon-16x16.png b/packages/client/public/favicon-16x16.png deleted file mode 100644 index 50af778..0000000 Binary files a/packages/client/public/favicon-16x16.png and /dev/null differ diff --git a/packages/client/public/favicon-32x32.png b/packages/client/public/favicon-32x32.png deleted file mode 100644 index 4b1dff8..0000000 Binary files a/packages/client/public/favicon-32x32.png and /dev/null differ diff --git a/packages/client/public/favicon.ico b/packages/client/public/favicon.ico index b707823..e825279 100644 Binary files a/packages/client/public/favicon.ico and b/packages/client/public/favicon.ico differ diff --git a/packages/client/public/general-problem-solver.png b/packages/client/public/general-problem-solver.png deleted file mode 100644 index 5f68cf9..0000000 Binary files a/packages/client/public/general-problem-solver.png and /dev/null differ diff --git a/packages/client/public/logo.png b/packages/client/public/logo.png new file mode 100644 index 0000000..3d82ad2 Binary files /dev/null and b/packages/client/public/logo.png differ diff --git a/packages/client/public/me.png b/packages/client/public/me.png deleted file mode 100644 index 908b8cb..0000000 Binary files a/packages/client/public/me.png and /dev/null differ diff --git a/packages/client/public/reactive-state-machine-4.png b/packages/client/public/reactive-state-machine-4.png deleted file mode 100644 index 623e46f..0000000 Binary files a/packages/client/public/reactive-state-machine-4.png and /dev/null differ diff --git a/packages/client/public/reactive_state_machine_5.png b/packages/client/public/reactive_state_machine_5.png deleted file mode 100644 index d6ab9e8..0000000 Binary files a/packages/client/public/reactive_state_machine_5.png and /dev/null differ diff --git a/packages/client/public/rehoboam.png b/packages/client/public/rehoboam.png deleted file mode 100644 index e84d5b7..0000000 Binary files a/packages/client/public/rehoboam.png and /dev/null differ diff --git a/packages/client/public/site.webmanifest b/packages/client/public/site.webmanifest deleted file mode 100644 index f68a551..0000000 --- a/packages/client/public/site.webmanifest +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "", - "short_name": "", - "icons": [ - { - "src": "/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "theme_color": "#fffff0", - "background_color": "#000000", - "display": "standalone" -} diff --git a/packages/client/scripts/generate-bevy-bundle.js b/packages/client/scripts/generate-bevy-bundle.js new file mode 100644 index 0000000..a830085 --- /dev/null +++ b/packages/client/scripts/generate-bevy-bundle.js @@ -0,0 +1,186 @@ +import { execSync } from 'node:child_process'; +import { + existsSync, + readdirSync, + readFileSync, + writeFileSync, + renameSync, + rmSync, + cpSync, + statSync, +} 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/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(buildCwd, '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.`); + } + optimizeWasmSize(); +} + +function optimizeWasmSize() { + logger.info('πŸ”¨ Checking WASM size...'); + + const wasmPath = resolve(publicDir, 'yachtpit_bg.wasm'); + const fileSize = statSync(wasmPath).size; + const sizeInMb = fileSize / (1024 * 1024); + + if (sizeInMb > 30) { + logger.info(`WASM size is ${sizeInMb.toFixed(2)}MB, optimizing...`); + execSync(`wasm-opt -Oz -o ${wasmPath} ${wasmPath}`, { + encoding: 'utf-8', + }); + logger.info(`βœ… WASM size optimized`); + } else { + logger.info( + `⏩ Skipping WASM optimization, size (${sizeInMb.toFixed(2)}MB) is under 30MB threshold`, + ); + } +} + +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/InstallButton.tsx b/packages/client/src/components/InstallButton.tsx new file mode 100644 index 0000000..9a1ae26 --- /dev/null +++ b/packages/client/src/components/InstallButton.tsx @@ -0,0 +1,34 @@ +import { IconButton } from '@chakra-ui/react'; +import { HardDriveDownload } from 'lucide-react'; +import React from 'react'; +import { usePWAInstall } from 'react-use-pwa-install'; + +import { toolbarButtonZIndex } from './toolbar/Toolbar.tsx'; + +function InstallButton() { + const install = usePWAInstall(); + + // ; + return ( + } + size="md" + bg="transparent" + stroke="text.accent" + color="text.accent" + onClick={() => install} + _hover={{ + bg: 'transparent', + svg: { + stroke: 'accent.secondary', + transition: 'stroke 0.3s ease-in-out', + }, + }} + zIndex={toolbarButtonZIndex} + /> + ); +} + +export default InstallButton; 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..d94d37f --- /dev/null +++ b/packages/client/src/components/landing-component/BevyScene.tsx @@ -0,0 +1,50 @@ +import { Box } from '@chakra-ui/react'; +import React, { memo, useEffect, useMemo } from 'react'; + +export interface BevySceneProps { + speed?: number; + intensity?: number; // 0-1 when visible + glow?: boolean; + visible?: boolean; // NEW β€” defaults to true +} + +const BevySceneInner: React.FC = ({ + speed = 1, + intensity = 1, + glow = false, + visible, +}) => { + /* initialise once */ + useEffect(() => { + let dispose: (() => void) | void; + (async () => { + const { default: init } = await import(/* webpackIgnore: true */ '/public/yachtpit.js'); + dispose = await init(); // zero-arg, uses #yachtpit-canvas + })(); + return () => { + if (typeof dispose === 'function') dispose(); + }; + }, []); + + /* memoised styles */ + const wrapperStyles = useMemo( + () => ({ + position: 'absolute' as const, + inset: 0, + zIndex: 0, + opacity: visible ? Math.min(Math.max(intensity, 0), 1) : 0, + filter: glow ? 'blur(1px)' : 'none', + transition: `opacity ${speed}s ease-in-out`, + display: visible ? 'block' : 'none', // optional: reclaim hit-testing entirely + }), + [visible, intensity, glow, speed], + ); + + return ( + + + + ); +}; + +export const BevyScene = memo(BevySceneInner); 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..7d8b833 --- /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 ( + + + + + + + + + ); +}; 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..c7a5a55 --- /dev/null +++ b/packages/client/src/components/landing-component/MatrixRain.tsx @@ -0,0 +1,124 @@ +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; + visible?: boolean; +} + +export const MatrixRain: React.FC = ({ + speed = 1, + glow = false, + intensity = 1, + visible, +}) => { + 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, visible]); + + 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..83ec36d --- /dev/null +++ b/packages/client/src/components/landing-component/Particles.tsx @@ -0,0 +1,162 @@ +import { Box, useTheme } from '@chakra-ui/react'; +import React, { useEffect, useRef } from 'react'; + +interface ParticlesProps { + speed: number; + intensity: number; + particles: boolean; + glow: boolean; + visible?: boolean; +} + +interface Particle { + x: number; + y: number; + vx: number; + vy: number; + size: number; +} + +const Particles: React.FC = ({ speed, intensity, glow, visible }) => { + 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 (!visible) { + 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; + } + }; + }, [visible, intensity, speed, glow, theme.colors.text.accent]); + + // Separate effect for speed changes - update existing particle velocities + useEffect(() => { + if (!visible) 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, visible]); + + 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/components/toolbar/Toolbar.tsx b/packages/client/src/components/toolbar/Toolbar.tsx index 1f7de88..f9b8367 100644 --- a/packages/client/src/components/toolbar/Toolbar.tsx +++ b/packages/client/src/components/toolbar/Toolbar.tsx @@ -2,6 +2,7 @@ import { Flex } from '@chakra-ui/react'; import React from 'react'; import BuiltWithButton from '../BuiltWithButton'; +import InstallButton from '../InstallButton.tsx'; import GithubButton from './GithubButton'; import SupportThisSiteButton from './SupportThisSiteButton'; @@ -17,6 +18,7 @@ function ToolBar({ isMobile }) { alignItems={isMobile ? 'flex-start' : 'flex-end'} pb={4} > + 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/NavItem.tsx b/packages/client/src/layout/NavItem.tsx index 073e675..189208a 100644 --- a/packages/client/src/layout/NavItem.tsx +++ b/packages/client/src/layout/NavItem.tsx @@ -5,7 +5,7 @@ function NavItem({ path, children, color, onClick, as, cursor }) { return ( 1 ? path : '/'} mb={2} cursor={cursor} // ml={5} 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..b2256f2 100644 --- a/packages/client/src/pages/index/+Page.tsx +++ b/packages/client/src/pages/index/+Page.tsx @@ -1,7 +1,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 +18,8 @@ export default function IndexPage() { return ( - + + {/**/} ); } diff --git a/packages/client/src/renderer/+onRenderHtml.tsx b/packages/client/src/renderer/+onRenderHtml.tsx index 2dea64c..074f200 100644 --- a/packages/client/src/renderer/+onRenderHtml.tsx +++ b/packages/client/src/renderer/+onRenderHtml.tsx @@ -28,10 +28,9 @@ const onRenderHtml: OnRenderHtmlAsync = async (pageContext): ReturnType open-gsio - - - - + + + diff --git a/packages/client/vite.config.ts b/packages/client/vite.config.ts index 1ec1a8e..dffaee6 100644 --- a/packages/client/vite.config.ts +++ b/packages/client/vite.config.ts @@ -7,20 +7,36 @@ import { VitePWA } from 'vite-plugin-pwa'; // eslint-disable-next-line import/no-unresolved import { configDefaults } from 'vitest/config'; +import { getColorThemes } from './src/layout/theme/color-themes'; + const prebuildPlugin = () => ({ name: 'prebuild', config(config, { command }) { if (command === 'build') { + console.log('Generate PWA Assets -> public/'); + child_process.execSync('bun generate:pwa:assets'); + console.log('Generated Sitemap -> public/sitemap.xml'); child_process.execSync('bun generate:sitemap'); console.log('Generated Sitemap -> public/sitemap.xml'); child_process.execSync('bun run generate:robotstxt'); 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'); } }, }); +// eslint-disable-next-line @typescript-eslint/no-require-imports +// const PROJECT_SOURCES_HASH = sha512Dir('./src'); +// +// console.log({ PROJECT_SOURCES_HASH }); + +const buildId = crypto.randomUUID(); + export default defineConfig(({ command }) => { return { mode: 'production', @@ -31,6 +47,62 @@ export default defineConfig(({ command }) => { prerender: true, disableAutoFullBuild: false, }), + VitePWA({ + registerType: 'autoUpdate', + injectRegister: null, + minify: true, + disable: false, + filename: 'service-worker.js', + devOptions: { + enabled: false, + navigateFallback: 'index.html', + suppressWarnings: true, + type: 'module', + }, + manifest: { + name: `open-gsio`, + short_name: 'open-gsio', + display: 'standalone', + description: `open-gsio client`, + theme_color: getColorThemes().at(0)?.colors.text.accent, + background_color: getColorThemes().at(0)?.colors.background.primary, + scope: '/', + start_url: '/', + icons: [ + { + src: 'pwa-64x64.png', + sizes: '64x64', + type: 'image/png', + }, + { + src: 'pwa-192x192.png', + sizes: '192x192', + type: 'image/png', + }, + { + src: 'pwa-512x512.png', + sizes: '512x512', + type: 'image/png', + purpose: 'any', + }, + { + src: 'maskable-icon-512x512.png', + sizes: '512x512', + type: 'image/png', + purpose: 'maskable', + }, + ], + }, + + workbox: { + globPatterns: ['**/*.{js,css,html,ico,png,svg,wasm}'], + navigateFallbackDenylist: [/^\/api\//], + maximumFileSizeToCacheInBytes: 25000000, + cacheId: buildId, + cleanupOutdatedCaches: true, + clientsClaim: true, + }, + }), // 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,22 +113,15 @@ 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\//], - // } - // }) ], + workbox: { + globPatterns: ['**/*.{js,css,html,ico,png,svg,wasm}'], + navigateFallbackDenylist: [/^\/api\//], + maximumFileSizeToCacheInBytes: 25000000, + cacheId: buildId, + cleanupOutdatedCaches: true, + clientsClaim: true, + }, server: { port: 3000, proxy: { diff --git a/packages/cloudflare-workers/open-gsio/main.ts b/packages/cloudflare-workers/open-gsio/main.ts index ed13b2b..3d0ed4e 100644 --- a/packages/cloudflare-workers/open-gsio/main.ts +++ b/packages/cloudflare-workers/open-gsio/main.ts @@ -1,6 +1,7 @@ import { ServerCoordinator } from '@open-gsio/coordinators'; import Router from '@open-gsio/router'; +import { error } from 'itty-router'; export { ServerCoordinator }; -export default Router.Router(); +export default Router.Router().catch(error); 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/router/src/router.ts b/packages/router/src/router.ts index 62fbd35..f334386 100644 --- a/packages/router/src/router.ts +++ b/packages/router/src/router.ts @@ -52,13 +52,15 @@ export function createRouter() { // }) .get('/api/metrics*', async (r, e, c) => { - const { metricsService } = createRequestContext(e, c); - return metricsService.handleMetricsRequest(r); + return new Response('ok'); + // const { metricsService } = createRequestContext(e, c); + // return metricsService.handleMetricsRequest(r); }) .post('/api/metrics*', async (r, e, c) => { - const { metricsService } = createRequestContext(e, c); - return metricsService.handleMetricsRequest(r); + return new Response('ok'); + // const { metricsService } = createRequestContext(e, c); + // return metricsService.handleMetricsRequest(r); }) // renders the app 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 {} \; diff --git a/packages/server/src/server/server.ts b/packages/server/src/server/server.ts index fd2bfcf..74d401c 100644 --- a/packages/server/src/server/server.ts +++ b/packages/server/src/server/server.ts @@ -4,6 +4,7 @@ import ServerCoordinator from '@open-gsio/coordinators/src/ServerCoordinatorBun. import Router from '@open-gsio/router'; import { config } from 'dotenv'; import type { RequestLike } from 'itty-router'; +import { error } from 'itty-router'; import { BunSqliteKVNamespace } from '../storage/BunSqliteKVNamespace.ts'; @@ -49,8 +50,7 @@ export default { reject(new Error('Request timeout after 5s')); }, 5000), ); - - return await Promise.race([router.fetch(request, env, ctx), timeout]); + return await Promise.race([router.fetch(request, env, ctx).catch(error), timeout]); } catch (e) { console.error('Error handling request:', e); return new Response('Server Error', { status: 500 });