diff --git a/concord-client/.gitignore b/concord-client/.gitignore index 4108b33..1c6a71f 100644 --- a/concord-client/.gitignore +++ b/concord-client/.gitignore @@ -10,7 +10,9 @@ lerna-debug.log* node_modules dist dist-ssr +dist-electron *.local +release/ # Editor directories and files .vscode/* diff --git a/concord-client/bun.lock b/concord-client/bun.lock index c80df7f..5e19909 100644 --- a/concord-client/bun.lock +++ b/concord-client/bun.lock @@ -4,8 +4,26 @@ "": { "name": "concord-client", "dependencies": { + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tooltip": "^1.2.8", + "@tailwindcss/vite": "^4.1.13", + "@tanstack/react-query": "^5.90.2", + "@tanstack/react-query-devtools": "^5.90.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.544.0", + "next-themes": "^0.4.6", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-router": "^7.9.3", + "sonner": "^2.0.7", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^4.1.13", + "zustand": "^5.0.8", }, "devDependencies": { "@types/react": "^18.2.64", @@ -18,6 +36,7 @@ "eslint": "^8.57.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", + "tw-animate-css": "^1.4.0", "typescript": "^5.2.2", "vite": "^5.1.6", "vite-plugin-electron": "^0.28.6", @@ -132,6 +151,14 @@ "@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="], + "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.6", "", { "dependencies": { "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], + "@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.13.0", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw=="], "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], @@ -140,6 +167,8 @@ "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -162,6 +191,72 @@ "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], + + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], + + "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="], + + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], + + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + + "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], + + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + + "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="], + + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], + + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], + + "@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="], + + "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="], + + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], + + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], + + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="], + + "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="], + + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="], + + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], + + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], + + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], + + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], + + "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.0", "", { "dependencies": { "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA=="], + + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], + + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], + + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="], + + "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.2", "", { "os": "android", "cpu": "arm" }, "sha512-o3pcKzJgSGt4d74lSZ+OCnHwkKBeAbFDmbEm5gg70eA8VkyCuC/zV9TwBnmw6VjDlRdF4Pshfb+WE9E6XY1PoQ=="], @@ -212,6 +307,44 @@ "@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="], + "@tailwindcss/node": ["@tailwindcss/node@4.1.13", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.5.1", "lightningcss": "1.30.1", "magic-string": "^0.30.18", "source-map-js": "^1.2.1", "tailwindcss": "4.1.13" } }, "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.13", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.13", "@tailwindcss/oxide-darwin-arm64": "4.1.13", "@tailwindcss/oxide-darwin-x64": "4.1.13", "@tailwindcss/oxide-freebsd-x64": "4.1.13", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.13", "@tailwindcss/oxide-linux-arm64-musl": "4.1.13", "@tailwindcss/oxide-linux-x64-gnu": "4.1.13", "@tailwindcss/oxide-linux-x64-musl": "4.1.13", "@tailwindcss/oxide-wasm32-wasi": "4.1.13", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.13", "@tailwindcss/oxide-win32-x64-msvc": "4.1.13" } }, "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.13", "", { "os": "android", "cpu": "arm64" }, "sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.13", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.13", "", { "os": "linux", "cpu": "arm" }, "sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.13", "", { "os": "linux", "cpu": "x64" }, "sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.13", "", { "os": "linux", "cpu": "x64" }, "sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.13", "", { "dependencies": { "@emnapi/core": "^1.4.5", "@emnapi/runtime": "^1.4.5", "@emnapi/wasi-threads": "^1.0.4", "@napi-rs/wasm-runtime": "^0.2.12", "@tybys/wasm-util": "^0.10.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.13", "", { "os": "win32", "cpu": "x64" }, "sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw=="], + + "@tailwindcss/vite": ["@tailwindcss/vite@4.1.13", "", { "dependencies": { "@tailwindcss/node": "4.1.13", "@tailwindcss/oxide": "4.1.13", "tailwindcss": "4.1.13" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-0PmqLQ010N58SbMTJ7BVJ4I2xopiQn/5i6nlb4JmxzQf8zcS5+m2Cv6tqh+sfDwtIdjoEnOvwsGQ1hkUi8QEHQ=="], + + "@tanstack/query-core": ["@tanstack/query-core@5.90.2", "", {}, "sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ=="], + + "@tanstack/query-devtools": ["@tanstack/query-devtools@5.90.1", "", {}, "sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ=="], + + "@tanstack/react-query": ["@tanstack/react-query@5.90.2", "", { "dependencies": { "@tanstack/query-core": "5.90.2" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw=="], + + "@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.90.2", "", { "dependencies": { "@tanstack/query-devtools": "5.90.1" }, "peerDependencies": { "@tanstack/react-query": "^5.90.2", "react": "^18 || ^19" } }, "sha512-vAXJzZuBXtCQtrY3F/yUNJCV4obT/A/n81kb3+YqLbro5Z2+phdAbceO+deU3ywPw8B42oyJlp4FhO0SoivDFQ=="], + "@tootallnate/once": ["@tootallnate/once@2.0.0", "", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -298,6 +431,8 @@ "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], + "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], "assert-plus": ["assert-plus@1.0.0", "", {}, "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="], @@ -362,12 +497,16 @@ "ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + "cli-truncate": ["cli-truncate@2.1.0", "", { "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" } }, "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg=="], "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], "clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="], + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], @@ -386,6 +525,8 @@ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], + "core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], "crc": ["crc@3.8.0", "", { "dependencies": { "buffer": "^5.1.0" } }, "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ=="], @@ -412,8 +553,12 @@ "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "detect-libc": ["detect-libc@2.1.1", "", {}, "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw=="], + "detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="], + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + "dir-compare": ["dir-compare@3.3.0", "", { "dependencies": { "buffer-equal": "^1.0.0", "minimatch": "^3.0.4" } }, "sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg=="], "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], @@ -448,6 +593,8 @@ "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="], + "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], "err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="], @@ -538,6 +685,8 @@ "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], @@ -620,6 +769,8 @@ "jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="], + "jiti": ["jiti@2.6.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], @@ -646,6 +797,28 @@ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="], + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], @@ -668,6 +841,10 @@ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "lucide-react": ["lucide-react@0.544.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw=="], + + "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="], + "matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], @@ -700,6 +877,8 @@ "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], + "node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="], "node-releases": ["node-releases@2.0.21", "", {}, "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw=="], @@ -766,6 +945,14 @@ "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], + "react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="], + + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + + "react-router": ["react-router@7.9.3", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-4o2iWCFIwhI/eYAIL43+cjORXYn/aRQPgtFRRZb3VzoyQ5Uej0Bmqj7437L97N9NJW4wnicSwLOLS+yCXfAPgg=="], + + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + "read-config-file": ["read-config-file@6.3.2", "", { "dependencies": { "config-file-ts": "^0.2.4", "dotenv": "^9.0.2", "dotenv-expand": "^5.1.0", "js-yaml": "^4.1.0", "json5": "^2.2.0", "lazy-val": "^1.0.4" } }, "sha512-M80lpCjnE6Wt6zb98DoW8WHR09nzMSpu8XHtPkiTHrJ5Az9CybfeQhTJ8D7saeBHpGhLPIVyA8lcL6ZmdKwY6Q=="], "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], @@ -808,6 +995,8 @@ "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], + "set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="], + "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=="], @@ -822,6 +1011,8 @@ "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], + "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -848,6 +1039,12 @@ "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="], + + "tailwindcss": ["tailwindcss@4.1.13", "", {}, "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w=="], + + "tapable": ["tapable@2.2.3", "", {}, "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg=="], + "tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], @@ -866,6 +1063,10 @@ "ts-api-utils": ["ts-api-utils@1.4.3", "", { "peerDependencies": { "typescript": ">=4.2.0" } }, "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], + "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=="], @@ -880,6 +1081,12 @@ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + + "use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="], + "utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], @@ -918,6 +1125,8 @@ "zip-stream": ["zip-stream@4.1.1", "", { "dependencies": { "archiver-utils": "^3.0.4", "compress-commons": "^4.1.2", "readable-stream": "^3.6.0" } }, "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ=="], + "zustand": ["zustand@5.0.8", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw=="], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -940,8 +1149,24 @@ "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + "@isaacs/fs-minipass/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + "@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + "@tailwindcss/oxide/tar": ["tar@7.5.1", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" }, "bundled": true }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "app-builder-lib/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], @@ -994,6 +1219,14 @@ "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "@tailwindcss/oxide/tar/chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + + "@tailwindcss/oxide/tar/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "@tailwindcss/oxide/tar/minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + + "@tailwindcss/oxide/tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "app-builder-lib/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], diff --git a/concord-client/components.json b/concord-client/components.json new file mode 100644 index 0000000..2b0833f --- /dev/null +++ b/concord-client/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/concord-client/dist-electron/main.js b/concord-client/dist-electron/main.js deleted file mode 100644 index 6e55b39..0000000 --- a/concord-client/dist-electron/main.js +++ /dev/null @@ -1,45 +0,0 @@ -import { app, BrowserWindow } from "electron"; -import { createRequire } from "node:module"; -import { fileURLToPath } from "node:url"; -import path from "node:path"; -createRequire(import.meta.url); -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -process.env.APP_ROOT = path.join(__dirname, ".."); -const VITE_DEV_SERVER_URL = process.env["VITE_DEV_SERVER_URL"]; -const MAIN_DIST = path.join(process.env.APP_ROOT, "dist-electron"); -const RENDERER_DIST = path.join(process.env.APP_ROOT, "dist"); -process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT, "public") : RENDERER_DIST; -let win; -function createWindow() { - win = new BrowserWindow({ - icon: path.join(process.env.VITE_PUBLIC, "electron-vite.svg"), - webPreferences: { - preload: path.join(__dirname, "preload.mjs") - } - }); - win.webContents.on("did-finish-load", () => { - win == null ? void 0 : win.webContents.send("main-process-message", (/* @__PURE__ */ new Date()).toLocaleString()); - }); - if (VITE_DEV_SERVER_URL) { - win.loadURL(VITE_DEV_SERVER_URL); - } else { - win.loadFile(path.join(RENDERER_DIST, "index.html")); - } -} -app.on("window-all-closed", () => { - if (process.platform !== "darwin") { - app.quit(); - win = null; - } -}); -app.on("activate", () => { - if (BrowserWindow.getAllWindows().length === 0) { - createWindow(); - } -}); -app.whenReady().then(createWindow); -export { - MAIN_DIST, - RENDERER_DIST, - VITE_DEV_SERVER_URL -}; diff --git a/concord-client/dist-electron/preload.mjs b/concord-client/dist-electron/preload.mjs deleted file mode 100644 index d421640..0000000 --- a/concord-client/dist-electron/preload.mjs +++ /dev/null @@ -1,22 +0,0 @@ -"use strict"; -const electron = require("electron"); -electron.contextBridge.exposeInMainWorld("ipcRenderer", { - on(...args) { - const [channel, listener] = args; - return electron.ipcRenderer.on(channel, (event, ...args2) => listener(event, ...args2)); - }, - off(...args) { - const [channel, ...omit] = args; - return electron.ipcRenderer.off(channel, ...omit); - }, - send(...args) { - const [channel, ...omit] = args; - return electron.ipcRenderer.send(channel, ...omit); - }, - invoke(...args) { - const [channel, ...omit] = args; - return electron.ipcRenderer.invoke(channel, ...omit); - } - // You can expose other APTs you need here. - // ... -}); diff --git a/concord-client/electron/main.ts b/concord-client/electron/main.ts index be9bbf4..dd45b52 100644 --- a/concord-client/electron/main.ts +++ b/concord-client/electron/main.ts @@ -1,68 +1,70 @@ -import { app, BrowserWindow } from 'electron' -import { createRequire } from 'node:module' -import { fileURLToPath } from 'node:url' -import path from 'node:path' - -const require = createRequire(import.meta.url) -const __dirname = path.dirname(fileURLToPath(import.meta.url)) - -// The built directory structure -// -// ├─┬─┬ dist -// │ │ └── index.html -// │ │ -// │ ├─┬ dist-electron -// │ │ ├── main.js -// │ │ └── preload.mjs -// │ -process.env.APP_ROOT = path.join(__dirname, '..') - -// 🚧 Use ['ENV_NAME'] avoid vite:define plugin - Vite@2.x -export const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL'] -export const MAIN_DIST = path.join(process.env.APP_ROOT, 'dist-electron') -export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist') - -process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT, 'public') : RENDERER_DIST - -let win: BrowserWindow | null - -function createWindow() { - win = new BrowserWindow({ - icon: path.join(process.env.VITE_PUBLIC, 'electron-vite.svg'), - webPreferences: { - preload: path.join(__dirname, 'preload.mjs'), - }, - }) - - // Test active push message to Renderer-process. - win.webContents.on('did-finish-load', () => { - win?.webContents.send('main-process-message', (new Date).toLocaleString()) - }) - - if (VITE_DEV_SERVER_URL) { - win.loadURL(VITE_DEV_SERVER_URL) - } else { - // win.loadFile('dist/index.html') - win.loadFile(path.join(RENDERER_DIST, 'index.html')) - } -} - -// Quit when all windows are closed, except on macOS. There, it's common -// for applications and their menu bar to stay active until the user quits -// explicitly with Cmd + Q. -app.on('window-all-closed', () => { - if (process.platform !== 'darwin') { - app.quit() - win = null - } -}) - -app.on('activate', () => { - // On OS X it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - if (BrowserWindow.getAllWindows().length === 0) { - createWindow() - } -}) - -app.whenReady().then(createWindow) +import { app, BrowserWindow } from "electron"; +// import { createRequire } from 'node:module' +import { fileURLToPath } from "node:url"; +import path from "node:path"; + +// const require = createRequire(import.meta.url) +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +// The built directory structure +// +// ├─┬─┬ dist +// │ │ └── index.html +// │ │ +// │ ├─┬ dist-electron +// │ │ ├── main.js +// │ │ └── preload.mjs +// │ +process.env.APP_ROOT = path.join(__dirname, ".."); + +// 🚧 Use ['ENV_NAME'] avoid vite:define plugin - Vite@2.x +export const VITE_DEV_SERVER_URL = process.env["VITE_DEV_SERVER_URL"]; +export const MAIN_DIST = path.join(process.env.APP_ROOT, "dist-electron"); +export const RENDERER_DIST = path.join(process.env.APP_ROOT, "dist"); + +process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL + ? path.join(process.env.APP_ROOT, "public") + : RENDERER_DIST; + +let win: BrowserWindow | null; + +function createWindow() { + win = new BrowserWindow({ + icon: path.join(process.env.VITE_PUBLIC, "electron-vite.svg"), + webPreferences: { + preload: path.join(__dirname, "preload.mjs"), + }, + }); + + // Test active push message to Renderer-process. + win.webContents.on("did-finish-load", () => { + win?.webContents.send("main-process-message", new Date().toLocaleString()); + }); + + if (VITE_DEV_SERVER_URL) { + win.loadURL(VITE_DEV_SERVER_URL); + } else { + // win.loadFile('dist/index.html') + win.loadFile(path.join(RENDERER_DIST, "index.html")); + } +} + +// Quit when all windows are closed, except on macOS. There, it's common +// for applications and their menu bar to stay active until the user quits +// explicitly with Cmd + Q. +app.on("window-all-closed", () => { + if (process.platform !== "darwin") { + app.quit(); + win = null; + } +}); + +app.on("activate", () => { + // On OS X it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } +}); + +app.whenReady().then(createWindow); diff --git a/concord-client/package.json b/concord-client/package.json index 68a0b9c..e894a49 100644 --- a/concord-client/package.json +++ b/concord-client/package.json @@ -4,14 +4,32 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", - "build": "tsc && vite build && electron-builder", + "dev": "bunx --bun vite --open", + "build": "tsc && bunx --bun vite build && electron-builder", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview" + "preview": "bunx --bun vite preview" }, "dependencies": { + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tooltip": "^1.2.8", + "@tailwindcss/vite": "^4.1.13", + "@tanstack/react-query": "^5.90.2", + "@tanstack/react-query-devtools": "^5.90.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.544.0", + "next-themes": "^0.4.6", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router": "^7.9.3", + "sonner": "^2.0.7", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^4.1.13", + "zustand": "^5.0.8" }, "devDependencies": { "@types/react": "^18.2.64", @@ -19,13 +37,14 @@ "@typescript-eslint/eslint-plugin": "^7.1.1", "@typescript-eslint/parser": "^7.1.1", "@vitejs/plugin-react": "^4.2.1", + "electron": "^30.0.1", + "electron-builder": "^24.13.3", "eslint": "^8.57.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", + "tw-animate-css": "^1.4.0", "typescript": "^5.2.2", "vite": "^5.1.6", - "electron": "^30.0.1", - "electron-builder": "^24.13.3", "vite-plugin-electron": "^0.28.6", "vite-plugin-electron-renderer": "^0.14.5" }, diff --git a/concord-client/src/App.css b/concord-client/src/App.css deleted file mode 100644 index fe59efc..0000000 --- a/concord-client/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/concord-client/src/App.tsx b/concord-client/src/App.tsx index 6c65102..60a5520 100644 --- a/concord-client/src/App.tsx +++ b/concord-client/src/App.tsx @@ -1,35 +1,111 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/electron-vite.animate.svg' -import './App.css' - -function App() { - const [count, setCount] = useState(0) - - return ( - <> -
- - Vite logo - - - React logo - -
-

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

- - ) -} - -export default App +import React, { useEffect } from "react"; +import { BrowserRouter as Router, Routes, Route, Navigate } from "react-router"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { Toaster } from "@/components/ui/sonner"; + +import AppLayout from "@/components/layout/AppLayout"; +import LoginPage from "@/pages/LoginPage"; +import ChatPage from "@/pages/ChatPage"; +import SettingsPage from "@/pages/SettingsPage"; +import NotFoundPage from "@/pages/NotFoundPage"; + +import { useAuthStore } from "@/stores/authStore"; +import { useUiStore } from "@/stores/uiStore"; +import ErrorBoundary from "@/components/common/ErrorBoundary"; + +// Create a client +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000 * 60 * 5, // 5 minutes + refetchOnWindowFocus: false, + retry: (failureCount, error: any) => { + if (error?.status === 401) return false; + return failureCount < 3; + }, + }, + }, +}); + +// Protected Route wrapper +const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + // const { isAuthenticated } = useAuthStore(); + + // if (!isAuthenticated) { + // return ; + // } + + return <>{children}; +}; + +function App() { + const { theme } = useUiStore(); + + // Apply theme to document + useEffect(() => { + document.documentElement.classList.toggle("dark", theme === "dark"); + }, [theme]); + + return ( + + + +
+ + {/* Auth routes */} + } /> + + {/* Protected routes with layout */} + + + // + } + > + {/* Default redirect to channels */} + } + /> + + {/* Chat routes */} + } + /> + } /> + } /> + } + /> + + {/* Settings */} + } /> + } /> + + + {/* 404 */} + } /> + +
+
+ + {/* Dev tools - only in development */} + {/*process.env.NODE_ENV === "development" && */} + + {/* Toast notifications */} + +
+
+ ); +} + +export default App; diff --git a/concord-client/src/README.md b/concord-client/src/README.md new file mode 100644 index 0000000..87f2d27 --- /dev/null +++ b/concord-client/src/README.md @@ -0,0 +1,13 @@ +# TODO + +- Messages + - Channels should include messages + - sample data + - message components +- User + - Set up fake user with auth to: + - Confirm userpanel is ok + - test login flow +- Add server ui +- Add channel ui +- Role based for above ^ diff --git a/concord-client/src/components/channel/ChannelList.tsx b/concord-client/src/components/channel/ChannelList.tsx new file mode 100644 index 0000000..1a861a5 --- /dev/null +++ b/concord-client/src/components/channel/ChannelList.tsx @@ -0,0 +1,183 @@ +import React, { useState } from "react"; +import { useNavigate, useParams } from "react-router"; +import { + Hash, + Volume2, + ChevronDown, + ChevronRight, + Plus, + Edit, +} from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { CategoryWithChannels } from "@/types/api"; +import { Channel } from "@/types/database"; + +interface ChannelItemProps { + channel: Channel; + isActive: boolean; + onClick: () => void; +} + +const ChannelItem: React.FC = ({ + channel, + isActive, + onClick, +}) => { + const Icon = channel.type === "voice" ? Volume2 : Hash; + + return ( + + ); +}; + +interface CategoryHeaderProps { + category: CategoryWithChannels; + isExpanded: boolean; + onToggle: () => void; +} + +const CategoryHeader: React.FC = ({ + category, + isExpanded, + onToggle, +}) => { + return ( + + + + + + + + + Create Channel + + + + + Edit Category + + + Delete Category + + + + ); +}; + +interface ChannelListProps { + categories: CategoryWithChannels[]; +} + +const ChannelList: React.FC = ({ categories }) => { + const navigate = useNavigate(); + const { instanceId, channelId } = useParams(); + + // Track expanded categories + const [expandedCategories, setExpandedCategories] = useState>( + new Set(categories.map((cat) => cat.id)), // Start with all expanded + ); + + const handleChannelClick = (channel: Channel) => { + navigate(`/channels/${instanceId}/${channel.id}`); + }; + + const toggleCategory = (categoryId: string) => { + setExpandedCategories((prev) => { + const newSet = new Set(prev); + if (newSet.has(categoryId)) { + newSet.delete(categoryId); + } else { + newSet.add(categoryId); + } + return newSet; + }); + }; + + if (!categories || categories.length === 0) { + return ( +
+ No channels available +
+ ); + } + + return ( +
+ {categories + .sort((a, b) => a.position - b.position) + .map((category) => { + const isExpanded = expandedCategories.has(category.id); + + return ( +
+ {/* Category Header */} + toggleCategory(category.id)} + /> + + {/* Channels */} + {isExpanded && ( +
+ {category.channels + .sort((a, b) => a.position - b.position) + .map((channel) => ( + handleChannelClick(channel)} + /> + ))} +
+ )} +
+ ); + })} +
+ ); +}; + +export default ChannelList; diff --git a/concord-client/src/components/common/Avatar.tsx b/concord-client/src/components/common/Avatar.tsx new file mode 100644 index 0000000..69e893c --- /dev/null +++ b/concord-client/src/components/common/Avatar.tsx @@ -0,0 +1,137 @@ +import React from "react"; +import { + Avatar as ShadcnAvatar, + AvatarFallback, + AvatarImage, +} from "@/components/ui/avatar"; +import { User } from "@/types/database"; +import { cn } from "@/lib/utils"; + +interface AvatarProps { + user: Pick; + size?: "xs" | "sm" | "md" | "lg" | "xl"; + showStatus?: boolean; + className?: string; + onClick?: () => void; +} + +const sizeClasses = { + xs: "h-6 w-6", + sm: "h-8 w-8", + md: "h-10 w-10", + lg: "h-12 w-12", + xl: "h-16 w-16", +}; + +const statusSizes = { + xs: "w-2 h-2", + sm: "w-3 h-3", + md: "w-3 h-3", + lg: "w-4 h-4", + xl: "w-5 h-5", +}; + +const statusPositions = { + xs: "-bottom-0.5 -right-0.5", + sm: "-bottom-0.5 -right-0.5", + md: "-bottom-0.5 -right-0.5", + lg: "-bottom-1 -right-1", + xl: "-bottom-1 -right-1", +}; + +const Avatar: React.FC = ({ + user, + size = "md", + showStatus = false, + className, + onClick, +}) => { + const getStatusColor = (status: string) => { + switch (status) { + case "online": + return "bg-green-500"; + case "away": + return "bg-yellow-500"; + case "busy": + return "bg-red-500"; + case "offline": + default: + return "bg-gray-500"; + } + }; + + const getUserInitials = (username: string, nickname?: string) => { + const name = nickname || username; + return name + .split(" ") + .map((word) => word[0]) + .join("") + .toUpperCase() + .slice(0, 2); + }; + + const getFallbackColor = (userId: string) => { + // Generate a consistent color based on user ID + const colors = [ + "bg-red-500", + "bg-blue-500", + "bg-green-500", + "bg-yellow-500", + "bg-purple-500", + "bg-pink-500", + "bg-indigo-500", + "bg-teal-500", + ]; + + const hash = userId.split("").reduce((a, b) => { + a = (a << 5) - a + b.charCodeAt(0); + return a & a; + }, 0); + + return colors[Math.abs(hash) % colors.length]; + }; + + return ( +
+ + + + {getUserInitials(user.username, user.nickname)} + + + + {showStatus && ( +
+ )} +
+ ); +}; + +export default Avatar; diff --git a/concord-client/src/components/common/ErrorBoundary.tsx b/concord-client/src/components/common/ErrorBoundary.tsx new file mode 100644 index 0000000..311c35e --- /dev/null +++ b/concord-client/src/components/common/ErrorBoundary.tsx @@ -0,0 +1,119 @@ +import { Component, ErrorInfo, ReactNode } from "react"; +import { AlertTriangle, RotateCcw } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; + +interface Props { + children: ReactNode; + fallback?: ReactNode; +} + +interface State { + hasError: boolean; + error?: Error; + errorInfo?: ErrorInfo; +} + +class ErrorBoundary extends Component { + public state: State = { + hasError: false, + }; + + public static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + public componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error("ErrorBoundary caught an error:", error, errorInfo); + + this.setState({ + error, + errorInfo, + }); + } + + private handleReload = () => { + window.location.reload(); + }; + + private handleReset = () => { + this.setState({ hasError: false, error: undefined, errorInfo: undefined }); + }; + + public render() { + if (this.state.hasError) { + // Custom fallback UI + if (this.props.fallback) { + return this.props.fallback; + } + + return ( +
+
+ + + + Something went wrong + + + The application encountered an unexpected error. This might be a + temporary issue. + + + +
+ + + +
+ + {/* Error details in development */} + {process.env.NODE_ENV === "development" && this.state.error && ( +
+ + Error Details (Development) + +
+
+ Error: {this.state.error.message} +
+
+ Stack: +
+                      {this.state.error.stack}
+                    
+
+ {this.state.errorInfo && ( +
+ Component Stack: +
+                        {this.state.errorInfo.componentStack}
+                      
+
+ )} +
+
+ )} +
+
+ ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/concord-client/src/components/common/LoadingSpinner.tsx b/concord-client/src/components/common/LoadingSpinner.tsx new file mode 100644 index 0000000..df06b08 --- /dev/null +++ b/concord-client/src/components/common/LoadingSpinner.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import { cn } from "@/lib/utils"; + +interface LoadingSpinnerProps { + size?: "xs" | "sm" | "md" | "lg" | "xl"; + className?: string; + color?: "white" | "blue" | "gray"; +} + +const sizeClasses = { + xs: "h-3 w-3", + sm: "h-4 w-4", + md: "h-6 w-6", + lg: "h-8 w-8", + xl: "h-12 w-12", +}; + +const colorClasses = { + white: "border-white", + blue: "border-blue-500", + gray: "border-gray-400", +}; + +const LoadingSpinner: React.FC = ({ + size = "md", + className, + color = "white", +}) => { + return ( +
+ ); +}; + +export default LoadingSpinner; diff --git a/concord-client/src/components/layout/AppLayout.tsx b/concord-client/src/components/layout/AppLayout.tsx new file mode 100644 index 0000000..ef15859 --- /dev/null +++ b/concord-client/src/components/layout/AppLayout.tsx @@ -0,0 +1,90 @@ +import React from "react"; +import { Outlet } from "react-router"; +import { useAuthStore } from "@/stores/authStore"; +import { useUiStore } from "@/stores/uiStore"; + +import ServerSidebar from "@/components/layout/ServerSidebar"; +import ChannelSidebar from "@/components/layout/ChannelSidebar"; +import UserPanel from "@/components/layout/UserPanel"; +import MemberList from "@/components/layout/MemberList"; +import LoadingSpinner from "@/components/common/LoadingSpinner"; + +const AppLayout: React.FC = () => { + const { user, isLoading } = useAuthStore(); + const { showMemberList, sidebarCollapsed, isMobile } = useUiStore(); + + if (isLoading) { + return ( +
+ +
+ ); + } + + // if (!user) { + // return ( + //
+ //
Authentication required
+ //
+ // ); + // } + + return ( +
+ {/* Server List Sidebar - Always visible on desktop, overlay on mobile */} +
+ +
+ + {/* Channel Sidebar - Collapsible */} +
+
+ + +
+
+ + {/* Main Content Area */} +
+ +
+ + {/* Member List - Conditionally shown */} + {showMemberList && !isMobile && ( +
+ +
+ )} + + {/* Mobile overlay for sidebars */} + {isMobile && !sidebarCollapsed && ( +
useUiStore.getState().toggleSidebar()} + /> + )} +
+ ); +}; + +export default AppLayout; diff --git a/concord-client/src/components/layout/ChannelSidebar.tsx b/concord-client/src/components/layout/ChannelSidebar.tsx new file mode 100644 index 0000000..19363d0 --- /dev/null +++ b/concord-client/src/components/layout/ChannelSidebar.tsx @@ -0,0 +1,147 @@ +import React from "react"; +import { useParams } from "react-router"; +import { ChevronDown, Plus, Users, X } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { useInstanceDetails } from "@/hooks/useServers"; +import { useChannels } from "@/hooks/useChannel"; +import { useUiStore } from "@/stores/uiStore"; +import { useResponsive } from "@/hooks/useResponsive"; +import ChannelList from "@/components/channel/ChannelList"; + +const ChannelSidebar: React.FC = () => { + const { instanceId } = useParams(); + const { data: instance, isLoading: instanceLoading } = + useInstanceDetails(instanceId); + const { data: categories, isLoading: channelsLoading } = + useChannels(instanceId); + const { + toggleMemberList, + showMemberList, + toggleSidebar, + openCreateChannel, + openServerSettings, + } = useUiStore(); + const { isMobile, isDesktop } = useResponsive(); + + // Handle Direct Messages view + if (!instanceId || instanceId === "@me") { + return ( +
+ {/* DM Header */} +
+
+ {isMobile && ( + + )} +

Direct Messages

+
+ +
+ + {/* DM List */} + +
+
+ No direct messages yet +
+
+
+
+ ); + } + + if (instanceLoading || channelsLoading) { + return ( +
+
+
+ ); + } + + if (!instance) { + return ( +
+
Server not found
+
+ ); + } + + return ( +
+ {/* Server Header */} +
+
+ {isMobile && ( + + )} + +
+
+ + {/* Channel Categories and Channels */} + +
+ {categories && categories.length > 0 ? ( + + ) : ( +
+ No channels yet +
+ )} +
+
+ + {/* Bottom Actions */} +
+
+ + + {isDesktop && ( + + )} +
+
+
+ ); +}; + +export default ChannelSidebar; diff --git a/concord-client/src/components/layout/MemberList.tsx b/concord-client/src/components/layout/MemberList.tsx new file mode 100644 index 0000000..547fd41 --- /dev/null +++ b/concord-client/src/components/layout/MemberList.tsx @@ -0,0 +1,176 @@ +import React from "react"; +import { useParams } from "react-router"; +import { Crown, Shield } from "lucide-react"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { useInstanceMembers } from "@/hooks/useServers"; +import { UserWithRoles } from "@/types/api"; + +interface MemberItemProps { + member: UserWithRoles; + isOwner?: boolean; +} + +const MemberItem: React.FC = ({ member, isOwner = false }) => { + const getStatusColor = (status: string) => { + switch (status) { + case "online": + return "bg-green-500"; + case "away": + return "bg-yellow-500"; + case "busy": + return "bg-red-500"; + default: + return "bg-gray-500"; + } + }; + + const getHighestRole = (roles: any[]) => { + if (!roles || roles.length === 0) return null; + // Sort by position (higher position = higher role) + return roles.sort((a, b) => b.position - a.position)[0]; + }; + + const highestRole = getHighestRole(member.roles); + + return ( +
+
+ + + + {member.username.slice(0, 2).toUpperCase()} + + + {/* Status indicator */} +
+
+ +
+
+ {isOwner && ( + + )} + {!isOwner && highestRole && ( + + )} + + {member.nickname || member.username} + +
+ {member.bio && ( +
{member.bio}
+ )} +
+
+ ); +}; + +const MemberList: React.FC = () => { + const { instanceId } = useParams(); + const { members, isLoading } = useInstanceMembers(instanceId); + + if (!instanceId || instanceId === "@me") { + return null; + } + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (!members || members.length === 0) { + return ( +
+
No members
+
+ ); + } + + // Group members by role + const groupedMembers = members.reduce( + (acc, member) => { + const highestRole = + member.roles?.length > 0 + ? member.roles.sort((a, b) => b.position - a.position)[0] + : null; + + const roleName = highestRole?.name || "Members"; + + if (!acc[roleName]) { + acc[roleName] = []; + } + acc[roleName].push(member); + return acc; + }, + {} as Record, + ); + + // Sort role groups by highest role position + const sortedRoleGroups = Object.entries(groupedMembers).sort( + ([roleNameA, membersA], [roleNameB, membersB]) => { + const roleA = membersA[0]?.roles?.find((r) => r.name === roleNameA); + const roleB = membersB[0]?.roles?.find((r) => r.name === roleNameB); + + if (!roleA && !roleB) return 0; + if (!roleA) return 1; + if (!roleB) return -1; + + return roleB.position - roleA.position; + }, + ); + + return ( +
+ {/* Header */} +
+

+ Members — {members.length} +

+
+ + {/* Member List */} + +
+ {sortedRoleGroups.map(([roleName, roleMembers]) => ( +
+ {/* Role Header */} +
+

+ {roleName} — {roleMembers.length} +

+
+ + {/* Role Members */} +
+ {roleMembers + .sort((a, b) => a.username.localeCompare(b.username)) + .map((member) => ( + + ))} +
+
+ ))} +
+
+
+ ); +}; + +export default MemberList; diff --git a/concord-client/src/components/layout/ServerList.tsx b/concord-client/src/components/layout/ServerList.tsx new file mode 100644 index 0000000..e69de29 diff --git a/concord-client/src/components/layout/ServerSidebar.tsx b/concord-client/src/components/layout/ServerSidebar.tsx new file mode 100644 index 0000000..f69e970 --- /dev/null +++ b/concord-client/src/components/layout/ServerSidebar.tsx @@ -0,0 +1,128 @@ +import React from "react"; +import { useNavigate, useParams } from "react-router"; +import { Plus, Home, Menu } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { useServers } from "@/hooks/useServers"; +import { useUiStore } from "@/stores/uiStore"; +import { useResponsive } from "@/hooks/useResponsive"; +import ServerIcon from "@/components/server/ServerIcon"; + +const ServerSidebar: React.FC = () => { + const navigate = useNavigate(); + const { instanceId } = useParams(); + const { data: servers, isLoading } = useServers(); + const { openCreateServer, toggleSidebar, setActiveInstance } = useUiStore(); + const { isMobile } = useResponsive(); + + const handleServerClick = (serverId: string) => { + setActiveInstance(serverId); + navigate(`/channels/${serverId}`); + }; + + const handleHomeClick = () => { + setActiveInstance(null); + navigate("/channels/@me"); + }; + + const handleMenuToggle = () => { + toggleSidebar(); + }; + + return ( + +
+ {/* Mobile menu toggle */} + {isMobile && ( + + + + + +

Toggle Menu

+
+
+ )} + + {/* Home/DM Button */} + + + + + +

Direct Messages

+
+
+ + {/* Separator */} +
+ + {/* Server List */} +
+ {isLoading ? ( +
+
+
+ ) : ( + servers?.map((server) => ( + + +
+ handleServerClick(server.id)} + /> +
+
+ +

{server.name}

+
+
+ )) + )} +
+ + {/* Add Server Button */} + + + + + +

Add a Server

+
+
+
+ + ); +}; + +export default ServerSidebar; diff --git a/concord-client/src/components/layout/UserPanel.tsx b/concord-client/src/components/layout/UserPanel.tsx new file mode 100644 index 0000000..7ddd2bc --- /dev/null +++ b/concord-client/src/components/layout/UserPanel.tsx @@ -0,0 +1,198 @@ +import React, { useState } from "react"; +import { Settings, Mic, MicOff, Headphones } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +import { useAuthStore } from "@/stores/authStore"; +import { useUiStore } from "@/stores/uiStore"; + +const UserPanel: React.FC = () => { + const { user, logout } = useAuthStore(); + const { openUserSettings } = useUiStore(); + + // Voice/Audio states (for future implementation) + const [isMuted, setIsMuted] = useState(false); + const [isDeafened, setIsDeafened] = useState(false); + + if (!user) return null; + + const getStatusColor = (status: string) => { + switch (status) { + case "online": + return "bg-green-500"; + case "away": + return "bg-yellow-500"; + case "busy": + return "bg-red-500"; + default: + return "bg-gray-500"; + } + }; + + const handleStatusChange = (newStatus: string) => { + // TODO: Implement status change + console.log("Status change to:", newStatus); + }; + + const handleMuteToggle = () => { + setIsMuted(!isMuted); + // TODO: Implement actual mute functionality + }; + + const handleDeafenToggle = () => { + setIsDeafened(!isDeafened); + if (!isDeafened) { + setIsMuted(true); // Deafening also mutes + } + // TODO: Implement actual deafen functionality + }; + + return ( +
+ {/* User Info */} + + + + + + + handleStatusChange("online")}> +
+
+ Online +
+ + handleStatusChange("away")}> +
+
+ Away +
+ + handleStatusChange("busy")}> +
+
+ Do Not Disturb +
+ + handleStatusChange("offline")}> +
+
+ Invisible +
+ + + + + + + User Settings + + + + + + Log Out + + + + + {/* Voice Controls */} +
+ + {/* Mute/Unmute */} + + + + + +

{isMuted ? "Unmute" : "Mute"}

+
+
+ + {/* Deafen/Undeafen */} + + + + + +

{isDeafened ? "Undeafen" : "Deafen"}

+
+
+ + {/* Settings */} + + + + + +

User Settings

+
+
+
+
+
+ ); +}; + +export default UserPanel; diff --git a/concord-client/src/components/mode-toggle.tsx b/concord-client/src/components/mode-toggle.tsx new file mode 100644 index 0000000..199d09f --- /dev/null +++ b/concord-client/src/components/mode-toggle.tsx @@ -0,0 +1,37 @@ +import { Moon, Sun } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { useTheme } from "@/components/theme-provider"; + +export function ModeToggle() { + const { setTheme } = useTheme(); + + return ( + + + + + + setTheme("light")}> + Light + + setTheme("dark")}> + Dark + + setTheme("system")}> + System + + + + ); +} diff --git a/concord-client/src/components/server/ServerIcon.tsx b/concord-client/src/components/server/ServerIcon.tsx new file mode 100644 index 0000000..1012be5 --- /dev/null +++ b/concord-client/src/components/server/ServerIcon.tsx @@ -0,0 +1,60 @@ +import React from "react"; +import { Button } from "@/components/ui/button"; +import { Instance } from "@/types/database"; + +interface ServerIconProps { + server: Instance; + isActive: boolean; + onClick: () => void; +} + +const ServerIcon: React.FC = ({ + server, + isActive, + onClick, +}) => { + const getServerInitials = (name: string) => { + return name + .split(" ") + .map((word) => word[0]) + .join("") + .toUpperCase() + .slice(0, 2); + }; + + return ( +
+ {/* Active indicator */} +
+ + +
+ ); +}; + +export default ServerIcon; diff --git a/concord-client/src/components/theme-provider.tsx b/concord-client/src/components/theme-provider.tsx new file mode 100644 index 0000000..e18440d --- /dev/null +++ b/concord-client/src/components/theme-provider.tsx @@ -0,0 +1,73 @@ +import { createContext, useContext, useEffect, useState } from "react"; + +type Theme = "dark" | "light" | "system"; + +type ThemeProviderProps = { + children: React.ReactNode; + defaultTheme?: Theme; + storageKey?: string; +}; + +type ThemeProviderState = { + theme: Theme; + setTheme: (theme: Theme) => void; +}; + +const initialState: ThemeProviderState = { + theme: "system", + setTheme: () => null, +}; + +const ThemeProviderContext = createContext(initialState); + +export function ThemeProvider({ + children, + defaultTheme = "system", + storageKey = "vite-ui-theme", + ...props +}: ThemeProviderProps) { + const [theme, setTheme] = useState( + () => (localStorage.getItem(storageKey) as Theme) || defaultTheme, + ); + + useEffect(() => { + const root = window.document.documentElement; + + root.classList.remove("light", "dark"); + + if (theme === "system") { + const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") + .matches + ? "dark" + : "light"; + + root.classList.add(systemTheme); + return; + } + + root.classList.add(theme); + }, [theme]); + + const value = { + theme, + setTheme: (theme: Theme) => { + localStorage.setItem(storageKey, theme); + setTheme(theme); + }, + }; + + return ( + + {children} + + ); +} + +export const useTheme = () => { + const context = useContext(ThemeProviderContext); + + if (context === undefined) + throw new Error("useTheme must be used within a ThemeProvider"); + + return context; +}; diff --git a/concord-client/src/components/ui/alert.tsx b/concord-client/src/components/ui/alert.tsx new file mode 100644 index 0000000..1421354 --- /dev/null +++ b/concord-client/src/components/ui/alert.tsx @@ -0,0 +1,66 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription } diff --git a/concord-client/src/components/ui/avatar.tsx b/concord-client/src/components/ui/avatar.tsx new file mode 100644 index 0000000..b7224f0 --- /dev/null +++ b/concord-client/src/components/ui/avatar.tsx @@ -0,0 +1,51 @@ +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/concord-client/src/components/ui/button.tsx b/concord-client/src/components/ui/button.tsx new file mode 100644 index 0000000..d96719c --- /dev/null +++ b/concord-client/src/components/ui/button.tsx @@ -0,0 +1,58 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/concord-client/src/components/ui/card.tsx b/concord-client/src/components/ui/card.tsx new file mode 100644 index 0000000..d05bbc6 --- /dev/null +++ b/concord-client/src/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/concord-client/src/components/ui/dropdown-menu.tsx b/concord-client/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..0d6741b --- /dev/null +++ b/concord-client/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,255 @@ +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean + variant?: "default" | "destructive" +}) { + return ( + + ) +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +} diff --git a/concord-client/src/components/ui/input.tsx b/concord-client/src/components/ui/input.tsx new file mode 100644 index 0000000..8916905 --- /dev/null +++ b/concord-client/src/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ) +} + +export { Input } diff --git a/concord-client/src/components/ui/label.tsx b/concord-client/src/components/ui/label.tsx new file mode 100644 index 0000000..ef7133a --- /dev/null +++ b/concord-client/src/components/ui/label.tsx @@ -0,0 +1,22 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" + +import { cn } from "@/lib/utils" + +function Label({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Label } diff --git a/concord-client/src/components/ui/scroll-area.tsx b/concord-client/src/components/ui/scroll-area.tsx new file mode 100644 index 0000000..9376f59 --- /dev/null +++ b/concord-client/src/components/ui/scroll-area.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" + +import { cn } from "@/lib/utils" + +function ScrollArea({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + + ) +} + +function ScrollBar({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { ScrollArea, ScrollBar } diff --git a/concord-client/src/components/ui/sonner.tsx b/concord-client/src/components/ui/sonner.tsx new file mode 100644 index 0000000..cd62aff --- /dev/null +++ b/concord-client/src/components/ui/sonner.tsx @@ -0,0 +1,23 @@ +import { useTheme } from "next-themes" +import { Toaster as Sonner, ToasterProps } from "sonner" + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme() + + return ( + + ) +} + +export { Toaster } diff --git a/concord-client/src/components/ui/tooltip.tsx b/concord-client/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..715bf76 --- /dev/null +++ b/concord-client/src/components/ui/tooltip.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import * as TooltipPrimitive from "@radix-ui/react-tooltip" + +import { cn } from "@/lib/utils" + +function TooltipProvider({ + delayDuration = 0, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function Tooltip({ + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function TooltipTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function TooltipContent({ + className, + sideOffset = 0, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + ) +} + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/concord-client/src/hooks/useChannel.ts b/concord-client/src/hooks/useChannel.ts new file mode 100644 index 0000000..cb0d201 --- /dev/null +++ b/concord-client/src/hooks/useChannel.ts @@ -0,0 +1,72 @@ +// src/hooks/useChannels.ts +import { useQuery } from "@tanstack/react-query"; +import { CategoryWithChannels } from "@/types/api"; + +// Placeholder hook for channels by instance +export const useChannels = (instanceId?: string) => { + return useQuery({ + queryKey: ["channels", instanceId], + queryFn: async (): Promise => { + if (!instanceId || instanceId === "@me") return []; + + // TODO: Replace with actual API call + return [ + { + id: "1", + name: "Text Channels", + instanceId: instanceId, + position: 0, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + channels: [ + { + id: "1", + name: "general", + type: "text", + categoryId: "1", + instanceId: instanceId, + position: 0, + topic: "General discussion", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + { + id: "2", + name: "random", + type: "text", + categoryId: "1", + instanceId: instanceId, + position: 1, + topic: "Random chat", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + ], + }, + { + id: "2", + name: "Voice Channels", + instanceId: instanceId, + position: 1, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + channels: [ + { + id: "3", + name: "General", + type: "voice", + categoryId: "2", + instanceId: instanceId, + position: 0, + topic: "", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + ], + }, + ]; + }, + enabled: !!instanceId && instanceId !== "@me", + staleTime: 1000 * 60 * 5, // 5 minutes + }); +}; diff --git a/concord-client/src/hooks/useResponsive.ts b/concord-client/src/hooks/useResponsive.ts new file mode 100644 index 0000000..56b995f --- /dev/null +++ b/concord-client/src/hooks/useResponsive.ts @@ -0,0 +1,32 @@ +import { useEffect } from "react"; +import { useUiStore } from "@/stores/uiStore"; + +export const useResponsive = () => { + const { screenWidth, isMobile, setScreenWidth, updateIsMobile } = + useUiStore(); + + useEffect(() => { + const handleResize = () => { + const width = window.innerWidth; + setScreenWidth(width); + updateIsMobile(); + }; + + // Set initial values + handleResize(); + + // Add event listener + window.addEventListener("resize", handleResize); + + // Cleanup + return () => window.removeEventListener("resize", handleResize); + }, [setScreenWidth, updateIsMobile]); + + return { + screenWidth, + isMobile, + isTablet: screenWidth >= 768 && screenWidth < 1024, + isDesktop: screenWidth >= 1024, + isLargeDesktop: screenWidth >= 1280, + }; +}; diff --git a/concord-client/src/hooks/useServers.ts b/concord-client/src/hooks/useServers.ts new file mode 100644 index 0000000..932c612 --- /dev/null +++ b/concord-client/src/hooks/useServers.ts @@ -0,0 +1,78 @@ +import { useQuery } from "@tanstack/react-query"; +import { Instance, InstanceWithDetails, UserWithRoles } from "@/types/api"; + +// Placeholder hook for servers/instances +export const useServers = () => { + return useQuery({ + queryKey: ["servers"], + queryFn: async (): Promise => { + // TODO: Replace with actual API call + return [ + { + id: "1", + name: "My Server", + icon: null, + description: "A cool server", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + ]; + }, + staleTime: 1000 * 60 * 5, // 5 minutes + }); +}; + +// Placeholder hook for instance details +export const useInstanceDetails = (instanceId?: string) => { + return useQuery({ + queryKey: ["instance", instanceId], + queryFn: async (): Promise => { + if (!instanceId || instanceId === "@me") return null; + + // TODO: Replace with actual API call + return { + id: instanceId, + name: "My Server", + icon: null, + description: "A cool server", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + categories: [], + memberCount: 1, + roles: [], + }; + }, + enabled: !!instanceId && instanceId !== "@me", + staleTime: 1000 * 60 * 5, + }); +}; + +// Placeholder hook for instance members +export const useInstanceMembers = (instanceId?: string) => { + return useQuery({ + queryKey: ["instance", instanceId, "members"], + queryFn: async (): Promise => { + if (!instanceId || instanceId === "@me") return []; + + // TODO: Replace with actual API call + return [ + { + id: "1", + username: "testuser", + nickname: "Test User", + bio: "Just testing", + picture: null, + banner: null, + hashPassword: "", + algorithms: "{}", + status: "online", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + roles: [], + }, + ]; + }, + enabled: !!instanceId && instanceId !== "@me", + staleTime: 1000 * 60 * 2, // 2 minutes (members change more frequently) + }); +}; diff --git a/concord-client/src/index.css b/concord-client/src/index.css index 00a7a3c..f4c1e9b 100644 --- a/concord-client/src/index.css +++ b/concord-client/src/index.css @@ -1,68 +1,120 @@ -:root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/concord-client/src/lib/utils.ts b/concord-client/src/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/concord-client/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/concord-client/src/pages/ChatPage.tsx b/concord-client/src/pages/ChatPage.tsx new file mode 100644 index 0000000..26e2341 --- /dev/null +++ b/concord-client/src/pages/ChatPage.tsx @@ -0,0 +1,192 @@ +import React from "react"; +import { useParams } from "react-router"; +import { Hash, Volume2, Users, HelpCircle, Inbox, Pin } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { useInstanceDetails } from "@/hooks/useServers"; +import { useChannels } from "@/hooks/useChannel"; +import { useUiStore } from "@/stores/uiStore"; + +const ChatPage: React.FC = () => { + const { instanceId, channelId } = useParams(); + const { instance } = useInstanceDetails(instanceId); + const { categories } = useChannels(instanceId); + const { toggleMemberList, showMemberList } = useUiStore(); + + // Find current channel + const currentChannel = categories + ?.flatMap((cat) => cat.channels) + ?.find((ch) => ch.id === channelId); + + // Handle Direct Messages view + // if (!instanceId || instanceId === '@me') { + // return ( + //
+ // {/* DM Header */} + //
+ //
+ // + // Direct Messages + //
+ //
+ // + //
+ //
+ + // {/* DM Content */} + //
+ //
+ //
+ // + //
+ //

No Direct Messages

+ //

+ // When someone sends you a direct message, it will show up here. + //

+ //
+ //
+ //
+ // ); + // } + + if (!currentChannel && channelId) { + return ( +
+
+

Channel not found

+

+ The channel you're looking for doesn't exist or you don't have + access to it. +

+
+
+ ); + } + + // Default channel view (when just /channels/instanceId) + if (!channelId && instance) { + const firstChannel = categories?.[0]?.channels?.[0]; + return ( +
+
+
+ +
+

+ Welcome to {instance.name}! +

+

+ {firstChannel + ? `Select a channel from the sidebar to start chatting, or head to #${firstChannel.name} to get started.` + : "This server doesn't have any channels yet. Create one to get started!"} +

+
+
+ ); + } + + const ChannelIcon = currentChannel?.type === "voice" ? Volume2 : Hash; + + return ( +
+ {/* Channel Header */} +
+
+ + + {currentChannel?.name} + + {currentChannel?.topic && ( + <> +
+ + {currentChannel.topic} + + + )} +
+ +
+ + +
+ +
+ + +
+
+ + {/* Chat Content */} +
+ {/* Messages Area */} +
+
+ {/* Welcome Message */} +
+
+
+ +
+
+

+ Welcome to #{currentChannel?.name}! +

+

+ This is the start of the #{currentChannel?.name} channel. +

+
+
+ {currentChannel?.topic && ( +
+ Topic: {currentChannel.topic} +
+ )} +
+ + {/* Placeholder messages */} +
+

No messages yet. Start the conversation!

+
+
+
+ + {/* Message Input */} +
+
+ +
+
+ {/* Emoji picker, file upload, etc. would go here */} +
Press Enter to send
+
+
+
+
+
+
+ ); +}; + +export default ChatPage; diff --git a/concord-client/src/pages/LoginPage.tsx b/concord-client/src/pages/LoginPage.tsx new file mode 100644 index 0000000..2251c84 --- /dev/null +++ b/concord-client/src/pages/LoginPage.tsx @@ -0,0 +1,109 @@ +import React, { useState } from "react"; +import { Navigate } from "react-router"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { useAuthStore } from "@/stores/authStore"; + +const LoginPage: React.FC = () => { + const { isAuthenticated, setAuth } = useAuthStore(); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + // Redirect if already authenticated + if (isAuthenticated) { + return ; + } + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + + try { + // TODO: Replace with actual login API call + setTimeout(() => { + setAuth( + { + id: "1", + username, + nickname: username, + bio: "Test user", + picture: null, + banner: null, + hashPassword: "", + algorithms: "{}", + status: "online", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + "fake-token", + "fake-refresh-token", + ); + setIsLoading(false); + }, 1000); + } catch (error) { + console.error("Login failed:", error); + setIsLoading(false); + } + }; + + return ( +
+ + + + Welcome back! + + + We're so excited to see you again! + + + +
+
+ + setUsername(e.target.value)} + className="bg-gray-700 border-gray-600 text-white" + placeholder="Enter your username" + required + /> +
+
+ + setPassword(e.target.value)} + className="bg-gray-700 border-gray-600 text-white" + placeholder="Enter your password" + required + /> +
+ +
+
+
+
+ ); +}; + +export default LoginPage; diff --git a/concord-client/src/pages/NotFoundPage.tsx b/concord-client/src/pages/NotFoundPage.tsx new file mode 100644 index 0000000..1cda54d --- /dev/null +++ b/concord-client/src/pages/NotFoundPage.tsx @@ -0,0 +1,12 @@ +const NotFoundPage: React.FC = () => { + return ( +
+
+

404

+

Page not found

+
+
+ ); +}; + +export default NotFoundPage; diff --git a/concord-client/src/pages/SettingsPage.tsx b/concord-client/src/pages/SettingsPage.tsx new file mode 100644 index 0000000..f437c26 --- /dev/null +++ b/concord-client/src/pages/SettingsPage.tsx @@ -0,0 +1,12 @@ +const SettingsPage: React.FC = () => { + return ( +
+
+

Settings

+

User settings will go here

+
+
+ ); +}; + +export default SettingsPage; diff --git a/concord-client/src/stores/authStore.ts b/concord-client/src/stores/authStore.ts new file mode 100644 index 0000000..0280591 --- /dev/null +++ b/concord-client/src/stores/authStore.ts @@ -0,0 +1,69 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; +import { User } from "@/types/database"; + +interface AuthState { + user: User | null; + token: string | null; + refreshToken: string | null; + isAuthenticated: boolean; + isLoading: boolean; + + // Actions + setAuth: (user: User, token: string, refreshToken: string) => void; + updateUser: (user: Partial) => void; + logout: () => void; + setLoading: (loading: boolean) => void; +} + +export const useAuthStore = create()( + persist( + (set, get) => ({ + user: null, + token: null, + refreshToken: null, + isAuthenticated: false, + isLoading: false, + + setAuth: (user, token, refreshToken) => + set({ + user, + token, + refreshToken, + isAuthenticated: true, + isLoading: false, + }), + + updateUser: (userData) => + set((state) => ({ + user: state.user ? { ...state.user, ...userData } : null, + })), + + logout: () => + set({ + user: null, + token: null, + refreshToken: null, + isAuthenticated: false, + isLoading: false, + }), + + setLoading: (isLoading) => set({ isLoading }), + }), + { + name: "concord-auth-store", + // Persist auth data + partialize: (state) => ({ + user: state.user, + token: state.token, + refreshToken: state.refreshToken, + isAuthenticated: state.isAuthenticated, + }), + }, + ), +); + +export const useCurrentUser = () => useAuthStore((state) => state.user); +export const useIsAuthenticated = () => + useAuthStore((state) => state.isAuthenticated); +export const useAuthToken = () => useAuthStore((state) => state.token); diff --git a/concord-client/src/stores/uiStore.ts b/concord-client/src/stores/uiStore.ts new file mode 100644 index 0000000..aac2b36 --- /dev/null +++ b/concord-client/src/stores/uiStore.ts @@ -0,0 +1,138 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; + +interface UiState { + // Sidebar states + showMemberList: boolean; + sidebarCollapsed: boolean; + + // Responsive + isMobile: boolean; + screenWidth: number; + + // Theme + theme: "dark" | "light"; + + // Modal states + showUserSettings: boolean; + showServerSettings: boolean; + showCreateChannel: boolean; + showCreateServer: boolean; + showInviteModal: boolean; + + // Chat states + // isTyping: boolean; + // typingUsers: string[]; + + // Navigation + activeChannelId: string | null; + activeInstanceId: string | null; + + // Actions + toggleMemberList: () => void; + toggleSidebar: () => void; + setTheme: (theme: "dark" | "light") => void; + setScreenWidth: (width: number) => void; + updateIsMobile: () => void; + + // Modal actions + openUserSettings: () => void; + closeUserSettings: () => void; + openServerSettings: () => void; + closeServerSettings: () => void; + openCreateChannel: () => void; + closeCreateChannel: () => void; + openCreateServer: () => void; + closeCreateServer: () => void; + openInviteModal: () => void; + closeInviteModal: () => void; + + // Chat actions + // setTyping: (isTyping: boolean) => void; + // addTypingUser: (userId: string) => void; + // removeTypingUser: (userId: string) => void; + // clearTypingUsers: () => void; + + // Navigation actions + setActiveChannel: (channelId: string | null) => void; + setActiveInstance: (instanceId: string | null) => void; +} + +export const useUiStore = create()( + persist( + (set, get) => ({ + // Initial state + showMemberList: true, + sidebarCollapsed: false, + isMobile: typeof window !== "undefined" ? window.innerWidth < 768 : false, + screenWidth: typeof window !== "undefined" ? window.innerWidth : 1024, + theme: "dark", + showUserSettings: false, + showServerSettings: false, + showCreateChannel: false, + showCreateServer: false, + showInviteModal: false, + isTyping: false, + typingUsers: [], + activeChannelId: null, + activeInstanceId: null, + + // Sidebar actions + toggleMemberList: () => + set((state) => ({ showMemberList: !state.showMemberList })), + toggleSidebar: () => + set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })), + setTheme: (theme) => set({ theme }), + setScreenWidth: (screenWidth) => set({ screenWidth }), + updateIsMobile: () => + set((state) => ({ + isMobile: state.screenWidth < 768, + // Auto-collapse sidebar on mobile + sidebarCollapsed: + state.screenWidth < 768 ? true : state.sidebarCollapsed, + // Hide member list on small screens + showMemberList: + state.screenWidth < 1024 ? false : state.showMemberList, + })), + + // Modal actions + openUserSettings: () => set({ showUserSettings: true }), + closeUserSettings: () => set({ showUserSettings: false }), + openServerSettings: () => set({ showServerSettings: true }), + closeServerSettings: () => set({ showServerSettings: false }), + openCreateChannel: () => set({ showCreateChannel: true }), + closeCreateChannel: () => set({ showCreateChannel: false }), + openCreateServer: () => set({ showCreateServer: true }), + closeCreateServer: () => set({ showCreateServer: false }), + openInviteModal: () => set({ showInviteModal: true }), + closeInviteModal: () => set({ showInviteModal: false }), + + // Chat actions + // setTyping: (isTyping) => set({ isTyping }), + // addTypingUser: (userId) => + // set((state) => ({ + // typingUsers: state.typingUsers.includes(userId) + // ? state.typingUsers + // : [...state.typingUsers, userId], + // })), + // removeTypingUser: (userId) => + // set((state) => ({ + // typingUsers: state.typingUsers.filter((id) => id !== userId), + // })), + // clearTypingUsers: () => set({ typingUsers: [] }), + + // Navigation actions + setActiveChannel: (channelId) => set({ activeChannelId: channelId }), + setActiveInstance: (instanceId) => set({ activeInstanceId: instanceId }), + }), + { + name: "concord-ui-store", + // Only persist UI preferences, not temporary states + partialize: (state) => ({ + showMemberList: state.showMemberList, + sidebarCollapsed: state.sidebarCollapsed, + theme: state.theme, + }), + }, + ), +); diff --git a/concord-client/src/types/api.ts b/concord-client/src/types/api.ts new file mode 100644 index 0000000..068114f --- /dev/null +++ b/concord-client/src/types/api.ts @@ -0,0 +1,91 @@ +import { Instance, Category, Channel, User, Role, Message } from "./database"; + +// API Response wrappers +export interface ApiResponse { + data: T; + success: boolean; + message?: string; +} + +export interface PaginatedResponse { + data: T[]; + pagination: { + page: number; + limit: number; + total: number; + totalPages: number; + hasNext: boolean; + hasPrev: boolean; + }; +} + +// Extended types with relations for frontend use +export interface ChannelWithCategory extends Channel { + category: Category; +} + +export interface CategoryWithChannels extends Category { + channels: Channel[]; +} + +export interface InstanceWithDetails extends Instance { + categories: CategoryWithChannels[]; + memberCount: number; + roles: Role[]; +} + +export interface MessageWithUser extends Message { + user: User; +} + +export interface UserWithRoles extends User { + roles: Role[]; +} + +// Request types +export interface CreateInstanceRequest { + name: string; + description?: string; + icon?: string; +} + +export interface CreateCategoryRequest { + name: string; + instanceId: string; + position?: number; +} + +export interface CreateChannelRequest { + name: string; + type: "text" | "voice"; + categoryId: string; + topic?: string; + position?: number; +} + +export interface SendMessageRequest { + content: string; + channelId: string; + user: User; +} + +export interface UpdateMessageRequest { + content: string; +} + +export interface LoginRequest { + username: string; + password: string; +} + +export interface RegisterRequest { + username: string; + password: string; + email?: string; +} + +export interface AuthResponse { + user: User; + token: string; + refreshToken: string; +} diff --git a/concord-client/src/types/database.ts b/concord-client/src/types/database.ts new file mode 100644 index 0000000..f69fcd0 --- /dev/null +++ b/concord-client/src/types/database.ts @@ -0,0 +1,93 @@ +export interface Instance { + id: string; + name: string; + icon?: string; + description?: string; + createdAt: string; + updatedAt: string; +} + +export interface Category { + id: string; + name: string; + instanceId: string; + position: number; + createdAt: string; + updatedAt: string; +} + +export interface Channel { + id: string; + name: string; + type: "text" | "voice"; + categoryId: string; + instanceId: string; + position: number; + topic?: string; + createdAt: string; + updatedAt: string; +} + +export interface User { + id: string; + username: string; + nickname?: string; + bio?: string; + picture?: string; + banner?: string; + hashPassword: string; // Won't be sent to client + admin: boolean; + status: "online" | "away" | "busy" | "offline"; + createdAt: string; + updatedAt: string; +} + +export interface Role { + id: string; + name: string; + color?: string; + permissions: string; // JSON string of permissions + instanceId: string; + position: number; + createdAt: string; + updatedAt: string; +} + +export interface Message { + id: string; + content: string; + channelId: string; + userId: string; + edited: boolean; + createdAt: string; + updatedAt: string; + // Relations + user?: User; + channel?: Channel; +} + +// Direct messages +// export interface DirectMessage { +// id: string; +// content: string; +// senderId: string; +// receiverId: string; +// edited: boolean; +// createdAt: string; +// updatedAt: string; +// // Relations +// sender?: User; +// receiver?: User; +// } + +export interface UserRole { + userId: string; + roleId: string; + instanceId: string; +} + +export interface UserInstance { + userId: string; + instanceId: string; + joinedAt: string; +} diff --git a/concord-client/tsconfig.json b/concord-client/tsconfig.json index fd9b1c3..017ff67 100644 --- a/concord-client/tsconfig.json +++ b/concord-client/tsconfig.json @@ -1,25 +1,29 @@ -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src", "electron"], - "references": [{ "path": "./tsconfig.node.json" }] -} +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + }, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + }, + "include": ["src", "electron"], + "references": [{ "path": "./tsconfig.node.json" }], +} diff --git a/concord-client/tsconfig.node.json b/concord-client/tsconfig.node.json index b850582..efd3582 100644 --- a/concord-client/tsconfig.node.json +++ b/concord-client/tsconfig.node.json @@ -1,11 +1,15 @@ -{ - "compilerOptions": { - "composite": true, - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true, - "strict": true - }, - "include": ["vite.config.ts"] -} +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["vite.config.ts"] +} diff --git a/concord-client/vite.config.ts b/concord-client/vite.config.ts index 544a4b4..754f636 100644 --- a/concord-client/vite.config.ts +++ b/concord-client/vite.config.ts @@ -1,29 +1,37 @@ -import { defineConfig } from 'vite' -import path from 'node:path' -import electron from 'vite-plugin-electron/simple' -import react from '@vitejs/plugin-react' - -// https://vitejs.dev/config/ -export default defineConfig({ +import { defineConfig } from "vite"; +import path from "path"; +import electron from "vite-plugin-electron/simple"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; + +// https://vitejs.dev/config/ +export default defineConfig({ plugins: [ react(), electron({ main: { // Shortcut of `build.lib.entry`. - entry: 'electron/main.ts', + entry: "electron/main.ts", }, preload: { // Shortcut of `build.rollupOptions.input`. // Preload scripts may contain Web assets, so use the `build.rollupOptions.input` instead `build.lib.entry`. - input: path.join(__dirname, 'electron/preload.ts'), + input: path.join(__dirname, "electron/preload.ts"), }, // Ployfill the Electron and Node.js API for Renderer process. // If you want use Node.js in Renderer process, the `nodeIntegration` needs to be enabled in the Main process. // See 👉 https://github.com/electron-vite/vite-plugin-electron-renderer - renderer: process.env.NODE_ENV === 'test' - // https://github.com/electron-vite/vite-plugin-electron-renderer/issues/78#issuecomment-2053600808 - ? undefined - : {}, + renderer: + process.env.NODE_ENV === "test" + ? // https://github.com/electron-vite/vite-plugin-electron-renderer/issues/78#issuecomment-2053600808 + undefined + : {}, }), + tailwindcss(), ], -}) + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +});