Merge pull request #4 from k-puig/feature/client-setup
feature/client setup
This commit is contained in:
2
concord-client/.gitignore
vendored
2
concord-client/.gitignore
vendored
@@ -10,7 +10,9 @@ lerna-debug.log*
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
dist-electron
|
||||
*.local
|
||||
release/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
|
||||
@@ -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=="],
|
||||
|
||||
22
concord-client/components.json
Normal file
22
concord-client/components.json
Normal file
@@ -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": {}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
@@ -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.
|
||||
// ...
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
import { app, BrowserWindow } from 'electron'
|
||||
import { createRequire } from 'node:module'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'node:path'
|
||||
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))
|
||||
// const require = createRequire(import.meta.url)
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// The built directory structure
|
||||
//
|
||||
@@ -15,54 +15,56 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
// │ │ ├── main.js
|
||||
// │ │ └── preload.mjs
|
||||
// │
|
||||
process.env.APP_ROOT = path.join(__dirname, '..')
|
||||
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')
|
||||
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
|
||||
process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL
|
||||
? path.join(process.env.APP_ROOT, "public")
|
||||
: RENDERER_DIST;
|
||||
|
||||
let win: BrowserWindow | null
|
||||
let win: BrowserWindow | null;
|
||||
|
||||
function createWindow() {
|
||||
win = new BrowserWindow({
|
||||
icon: path.join(process.env.VITE_PUBLIC, 'electron-vite.svg'),
|
||||
icon: path.join(process.env.VITE_PUBLIC, "electron-vite.svg"),
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.mjs'),
|
||||
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())
|
||||
})
|
||||
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)
|
||||
win.loadURL(VITE_DEV_SERVER_URL);
|
||||
} else {
|
||||
// win.loadFile('dist/index.html')
|
||||
win.loadFile(path.join(RENDERER_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("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
win = null;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
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()
|
||||
createWindow();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
app.whenReady().then(createWindow)
|
||||
app.whenReady().then(createWindow);
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,35 +1,111 @@
|
||||
import { useState } from 'react'
|
||||
import reactLogo from './assets/react.svg'
|
||||
import viteLogo from '/electron-vite.animate.svg'
|
||||
import './App.css'
|
||||
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 <Navigate to="/login" replace />;
|
||||
// }
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
function App() {
|
||||
const [count, setCount] = useState(0)
|
||||
const { theme } = useUiStore();
|
||||
|
||||
// Apply theme to document
|
||||
useEffect(() => {
|
||||
document.documentElement.classList.toggle("dark", theme === "dark");
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<a href="https://electron-vite.github.io" target="_blank">
|
||||
<img src={viteLogo} className="logo" alt="Vite logo" />
|
||||
</a>
|
||||
<a href="https://react.dev" target="_blank">
|
||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
||||
</a>
|
||||
</div>
|
||||
<h1>Vite + React</h1>
|
||||
<div className="card">
|
||||
<button onClick={() => setCount((count) => count + 1)}>
|
||||
count is {count}
|
||||
</button>
|
||||
<p>
|
||||
Edit <code>src/App.tsx</code> and save to test HMR
|
||||
</p>
|
||||
</div>
|
||||
<p className="read-the-docs">
|
||||
Click on the Vite and React logos to learn more
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
<ErrorBoundary>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Router>
|
||||
<div
|
||||
className={`h-screen w-screen overflow-hidden ${theme === "dark" ? "bg-gray-900 text-white" : "bg-white text-gray-900"}`}
|
||||
>
|
||||
<Routes>
|
||||
{/* Auth routes */}
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
|
||||
{/* Protected routes with layout */}
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
// <ProtectedRoute>
|
||||
<AppLayout />
|
||||
// </ProtectedRoute>
|
||||
}
|
||||
>
|
||||
{/* Default redirect to channels */}
|
||||
<Route
|
||||
index
|
||||
element={<Navigate to="/channels/@me" replace />}
|
||||
/>
|
||||
|
||||
{/* Chat routes */}
|
||||
<Route
|
||||
path="channels"
|
||||
element={<Navigate to="/channels/@me" replace />}
|
||||
/>
|
||||
<Route path="channels/@me" element={<ChatPage />} />
|
||||
<Route path="channels/:instanceId" element={<ChatPage />} />
|
||||
<Route
|
||||
path="channels/:instanceId/:channelId"
|
||||
element={<ChatPage />}
|
||||
/>
|
||||
|
||||
{/* Settings */}
|
||||
<Route path="settings" element={<SettingsPage />} />
|
||||
<Route path="settings/:section" element={<SettingsPage />} />
|
||||
</Route>
|
||||
|
||||
{/* 404 */}
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</Router>
|
||||
|
||||
{/* Dev tools - only in development */}
|
||||
{/*process.env.NODE_ENV === "development" && <ReactQueryDevtools />*/}
|
||||
|
||||
{/* Toast notifications */}
|
||||
<Toaster />
|
||||
</QueryClientProvider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
export default App
|
||||
export default App;
|
||||
|
||||
13
concord-client/src/README.md
Normal file
13
concord-client/src/README.md
Normal file
@@ -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 ^
|
||||
183
concord-client/src/components/channel/ChannelList.tsx
Normal file
183
concord-client/src/components/channel/ChannelList.tsx
Normal file
@@ -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<ChannelItemProps> = ({
|
||||
channel,
|
||||
isActive,
|
||||
onClick,
|
||||
}) => {
|
||||
const Icon = channel.type === "voice" ? Volume2 : Hash;
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={`w-full justify-start px-2 py-1 h-8 text-left font-medium ${
|
||||
isActive
|
||||
? "bg-gray-600 text-white"
|
||||
: "text-gray-300 hover:bg-gray-700 hover:text-gray-100"
|
||||
}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon size={16} className="mr-2 flex-shrink-0" />
|
||||
<span className="truncate">{channel.name}</span>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
interface CategoryHeaderProps {
|
||||
category: CategoryWithChannels;
|
||||
isExpanded: boolean;
|
||||
onToggle: () => void;
|
||||
}
|
||||
|
||||
const CategoryHeader: React.FC<CategoryHeaderProps> = ({
|
||||
category,
|
||||
isExpanded,
|
||||
onToggle,
|
||||
}) => {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-between px-1 py-1 h-6 text-xs font-semibold text-gray-400 uppercase tracking-wide hover:text-gray-300 group"
|
||||
onClick={(e) => {
|
||||
// Only toggle if not right-clicking (which opens dropdown)
|
||||
if (e.button === 0) {
|
||||
onToggle();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
{isExpanded ? (
|
||||
<ChevronDown size={12} className="mr-1" />
|
||||
) : (
|
||||
<ChevronRight size={12} className="mr-1" />
|
||||
)}
|
||||
<span className="truncate">{category.name}</span>
|
||||
</div>
|
||||
<Plus
|
||||
size={12}
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem>
|
||||
<Plus size={14} className="mr-2" />
|
||||
Create Channel
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<Edit size={14} className="mr-2" />
|
||||
Edit Category
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="text-red-400 focus:text-red-400">
|
||||
Delete Category
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
interface ChannelListProps {
|
||||
categories: CategoryWithChannels[];
|
||||
}
|
||||
|
||||
const ChannelList: React.FC<ChannelListProps> = ({ categories }) => {
|
||||
const navigate = useNavigate();
|
||||
const { instanceId, channelId } = useParams();
|
||||
|
||||
// Track expanded categories
|
||||
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(
|
||||
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 (
|
||||
<div className="text-sm text-gray-400 px-2 py-4 text-center">
|
||||
No channels available
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{categories
|
||||
.sort((a, b) => a.position - b.position)
|
||||
.map((category) => {
|
||||
const isExpanded = expandedCategories.has(category.id);
|
||||
|
||||
return (
|
||||
<div key={category.id} className="space-y-0.5">
|
||||
{/* Category Header */}
|
||||
<CategoryHeader
|
||||
category={category}
|
||||
isExpanded={isExpanded}
|
||||
onToggle={() => toggleCategory(category.id)}
|
||||
/>
|
||||
|
||||
{/* Channels */}
|
||||
{isExpanded && (
|
||||
<div className="ml-2 space-y-0.5">
|
||||
{category.channels
|
||||
.sort((a, b) => a.position - b.position)
|
||||
.map((channel) => (
|
||||
<ChannelItem
|
||||
key={channel.id}
|
||||
channel={channel}
|
||||
isActive={channelId === channel.id}
|
||||
onClick={() => handleChannelClick(channel)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelList;
|
||||
137
concord-client/src/components/common/Avatar.tsx
Normal file
137
concord-client/src/components/common/Avatar.tsx
Normal file
@@ -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<User, "id" | "username" | "nickname" | "picture" | "status">;
|
||||
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<AvatarProps> = ({
|
||||
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 (
|
||||
<div className="relative inline-block">
|
||||
<ShadcnAvatar
|
||||
className={cn(
|
||||
sizeClasses[size],
|
||||
onClick && "cursor-pointer hover:opacity-80 transition-opacity",
|
||||
className,
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<AvatarImage
|
||||
src={user.picture || undefined}
|
||||
alt={user.nickname || user.username}
|
||||
/>
|
||||
<AvatarFallback
|
||||
className={cn(
|
||||
"text-white font-medium",
|
||||
getFallbackColor(user.id),
|
||||
size === "xs" && "text-xs",
|
||||
size === "sm" && "text-xs",
|
||||
size === "md" && "text-sm",
|
||||
size === "lg" && "text-base",
|
||||
size === "xl" && "text-lg",
|
||||
)}
|
||||
>
|
||||
{getUserInitials(user.username, user.nickname)}
|
||||
</AvatarFallback>
|
||||
</ShadcnAvatar>
|
||||
|
||||
{showStatus && (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute rounded-full border-2 border-gray-800",
|
||||
statusSizes[size],
|
||||
statusPositions[size],
|
||||
getStatusColor(user.status),
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Avatar;
|
||||
119
concord-client/src/components/common/ErrorBoundary.tsx
Normal file
119
concord-client/src/components/common/ErrorBoundary.tsx
Normal file
@@ -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<Props, State> {
|
||||
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 (
|
||||
<div className="min-h-screen bg-gray-900 flex items-center justify-center p-4">
|
||||
<div className="max-w-md w-full space-y-4">
|
||||
<Alert className="border-red-500 bg-red-950/50">
|
||||
<AlertTriangle className="h-4 w-4 text-red-500" />
|
||||
<AlertTitle className="text-red-400">
|
||||
Something went wrong
|
||||
</AlertTitle>
|
||||
<AlertDescription className="text-gray-300">
|
||||
The application encountered an unexpected error. This might be a
|
||||
temporary issue.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
onClick={this.handleReset}
|
||||
className="w-full"
|
||||
variant="outline"
|
||||
>
|
||||
<RotateCcw size={16} className="mr-2" />
|
||||
Try Again
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={this.handleReload}
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
>
|
||||
Reload Application
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Error details in development */}
|
||||
{process.env.NODE_ENV === "development" && this.state.error && (
|
||||
<details className="mt-4 p-3 bg-gray-800 rounded-lg text-sm">
|
||||
<summary className="cursor-pointer text-red-400 font-medium mb-2">
|
||||
Error Details (Development)
|
||||
</summary>
|
||||
<div className="space-y-2 text-gray-300">
|
||||
<div>
|
||||
<strong>Error:</strong> {this.state.error.message}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Stack:</strong>
|
||||
<pre className="mt-1 text-xs overflow-auto text-gray-400">
|
||||
{this.state.error.stack}
|
||||
</pre>
|
||||
</div>
|
||||
{this.state.errorInfo && (
|
||||
<div>
|
||||
<strong>Component Stack:</strong>
|
||||
<pre className="mt-1 text-xs overflow-auto text-gray-400">
|
||||
{this.state.errorInfo.componentStack}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
41
concord-client/src/components/common/LoadingSpinner.tsx
Normal file
41
concord-client/src/components/common/LoadingSpinner.tsx
Normal file
@@ -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<LoadingSpinnerProps> = ({
|
||||
size = "md",
|
||||
className,
|
||||
color = "white",
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"animate-spin rounded-full border-2 border-transparent border-t-current",
|
||||
sizeClasses[size],
|
||||
colorClasses[color],
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingSpinner;
|
||||
90
concord-client/src/components/layout/AppLayout.tsx
Normal file
90
concord-client/src/components/layout/AppLayout.tsx
Normal file
@@ -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 (
|
||||
<div className="h-screen w-screen flex items-center justify-center bg-gray-900">
|
||||
<LoadingSpinner size="lg" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// if (!user) {
|
||||
// return (
|
||||
// <div className="h-screen w-screen flex items-center justify-center bg-gray-900">
|
||||
// <div className="text-red-400">Authentication required</div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden bg-gray-900 text-white">
|
||||
{/* Server List Sidebar - Always visible on desktop, overlay on mobile */}
|
||||
<div
|
||||
className={`${
|
||||
isMobile
|
||||
? "fixed left-0 top-0 z-50 h-full w-[72px] transform transition-transform duration-200 ease-in-out"
|
||||
: "relative w-[72px]"
|
||||
} bg-gray-900 flex-shrink-0`}
|
||||
>
|
||||
<ServerSidebar />
|
||||
</div>
|
||||
|
||||
{/* Channel Sidebar - Collapsible */}
|
||||
<div
|
||||
// className={`${
|
||||
// sidebarCollapsed
|
||||
// ? isMobile
|
||||
// ? "hidden"
|
||||
// : "w-0 overflow-hidden"
|
||||
// : isMobile
|
||||
// ? "fixed left-[72px] top-0 z-40 h-full w-60"
|
||||
// : "w-60"
|
||||
// } bg-gray-800 flex flex-col flex-shrink-0 transition-all duration-200 ease-in-out`}
|
||||
>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<ChannelSidebar />
|
||||
<UserPanel />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<div
|
||||
className={`flex-1 flex flex-col min-w-0 ${
|
||||
isMobile && !sidebarCollapsed ? "ml-60" : ""
|
||||
} transition-all duration-200 ease-in-out`}
|
||||
>
|
||||
<Outlet />
|
||||
</div>
|
||||
|
||||
{/* Member List - Conditionally shown */}
|
||||
{showMemberList && !isMobile && (
|
||||
<div className="w-60 bg-gray-800 flex-shrink-0 border-l border-gray-700">
|
||||
<MemberList />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile overlay for sidebars */}
|
||||
{isMobile && !sidebarCollapsed && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 z-30"
|
||||
onClick={() => useUiStore.getState().toggleSidebar()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppLayout;
|
||||
147
concord-client/src/components/layout/ChannelSidebar.tsx
Normal file
147
concord-client/src/components/layout/ChannelSidebar.tsx
Normal file
@@ -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 (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* DM Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-700">
|
||||
<div className="flex items-center space-x-2">
|
||||
{isMobile && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
onClick={toggleSidebar}
|
||||
>
|
||||
<X size={16} />
|
||||
</Button>
|
||||
)}
|
||||
<h2 className="font-semibold text-white">Direct Messages</h2>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6">
|
||||
<Plus size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* DM List */}
|
||||
<ScrollArea className="flex-1 px-2">
|
||||
<div className="py-2 space-y-1">
|
||||
<div className="text-sm text-gray-400 px-2 py-1">
|
||||
No direct messages yet
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (instanceLoading || channelsLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!instance) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-gray-400">Server not found</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Server Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-700 shadow-sm">
|
||||
<div className="flex items-center space-x-2 flex-1 min-w-0">
|
||||
{isMobile && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 flex-shrink-0"
|
||||
onClick={toggleSidebar}
|
||||
>
|
||||
<X size={16} />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="flex items-center justify-between w-full px-2 py-1 h-auto font-semibold text-white hover:bg-gray-700"
|
||||
onClick={openServerSettings}
|
||||
>
|
||||
<span className="truncate">{instance.name}</span>
|
||||
<ChevronDown size={18} className="flex-shrink-0 ml-1" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Channel Categories and Channels */}
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="p-2">
|
||||
{categories && categories.length > 0 ? (
|
||||
<ChannelList categories={categories} />
|
||||
) : (
|
||||
<div className="text-sm text-gray-400 px-2 py-4 text-center">
|
||||
No channels yet
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Bottom Actions */}
|
||||
<div className="px-2 py-2 border-t border-gray-700">
|
||||
<div className="flex items-center justify-between">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-gray-400 hover:text-white"
|
||||
onClick={openCreateChannel}
|
||||
>
|
||||
<Plus size={16} className="mr-1" />
|
||||
Add Channel
|
||||
</Button>
|
||||
|
||||
{isDesktop && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={`h-8 w-8 ${showMemberList ? "text-white" : "text-gray-400 hover:text-white"}`}
|
||||
onClick={toggleMemberList}
|
||||
>
|
||||
<Users size={16} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelSidebar;
|
||||
176
concord-client/src/components/layout/MemberList.tsx
Normal file
176
concord-client/src/components/layout/MemberList.tsx
Normal file
@@ -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<MemberItemProps> = ({ 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 (
|
||||
<div className="flex items-center px-2 py-1 mx-2 rounded hover:bg-gray-700/50 cursor-pointer group">
|
||||
<div className="relative">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={member.picture} alt={member.username} />
|
||||
<AvatarFallback className="text-xs">
|
||||
{member.username.slice(0, 2).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
{/* Status indicator */}
|
||||
<div
|
||||
className={`absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full border-2 border-gray-800 ${getStatusColor(member.status)}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="ml-3 flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1">
|
||||
{isOwner && (
|
||||
<Crown size={12} className="text-yellow-500 flex-shrink-0" />
|
||||
)}
|
||||
{!isOwner && highestRole && (
|
||||
<Shield
|
||||
size={12}
|
||||
className="flex-shrink-0"
|
||||
style={{ color: highestRole.color || "#ffffff" }}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
className="text-sm font-medium truncate"
|
||||
style={{ color: highestRole?.color || "#ffffff" }}
|
||||
>
|
||||
{member.nickname || member.username}
|
||||
</span>
|
||||
</div>
|
||||
{member.bio && (
|
||||
<div className="text-xs text-gray-400 truncate">{member.bio}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MemberList: React.FC = () => {
|
||||
const { instanceId } = useParams();
|
||||
const { members, isLoading } = useInstanceMembers(instanceId);
|
||||
|
||||
if (!instanceId || instanceId === "@me") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-white"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!members || members.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-gray-400 text-sm">No members</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 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<string, UserWithRoles[]>,
|
||||
);
|
||||
|
||||
// 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 (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<div className="px-4 py-3 border-b border-gray-700">
|
||||
<h3 className="text-sm font-semibold text-gray-300 uppercase tracking-wide">
|
||||
Members — {members.length}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* Member List */}
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="py-2">
|
||||
{sortedRoleGroups.map(([roleName, roleMembers]) => (
|
||||
<div key={roleName} className="mb-4">
|
||||
{/* Role Header */}
|
||||
<div className="px-4 py-1">
|
||||
<h4 className="text-xs font-semibold text-gray-400 uppercase tracking-wide">
|
||||
{roleName} — {roleMembers.length}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
{/* Role Members */}
|
||||
<div className="space-y-1">
|
||||
{roleMembers
|
||||
.sort((a, b) => a.username.localeCompare(b.username))
|
||||
.map((member) => (
|
||||
<MemberItem
|
||||
key={member.id}
|
||||
member={member}
|
||||
isOwner={false}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemberList;
|
||||
0
concord-client/src/components/layout/ServerList.tsx
Normal file
0
concord-client/src/components/layout/ServerList.tsx
Normal file
128
concord-client/src/components/layout/ServerSidebar.tsx
Normal file
128
concord-client/src/components/layout/ServerSidebar.tsx
Normal file
@@ -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 (
|
||||
<TooltipProvider>
|
||||
<div className="flex flex-col items-center py-3 space-y-2 h-full bg-gray-900 border-r border-gray-800">
|
||||
{/* Mobile menu toggle */}
|
||||
{isMobile && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="w-12 h-12 rounded-2xl hover:rounded-xl bg-gray-700 hover:bg-gray-600 text-white transition-all duration-200"
|
||||
onClick={handleMenuToggle}
|
||||
>
|
||||
<Menu size={24} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>Toggle Menu</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Home/DM Button */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={
|
||||
!instanceId || instanceId === "@me" ? "default" : "ghost"
|
||||
}
|
||||
size="icon"
|
||||
className="w-12 h-12 rounded-2xl hover:rounded-xl transition-all duration-200"
|
||||
onClick={handleHomeClick}
|
||||
>
|
||||
<Home size={24} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>Direct Messages</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{/* Separator */}
|
||||
<div className="w-8 h-0.5 bg-gray-600 rounded-full" />
|
||||
|
||||
{/* Server List */}
|
||||
<div className="flex-1 flex flex-col space-y-2 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-600">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-white"></div>
|
||||
</div>
|
||||
) : (
|
||||
servers?.map((server) => (
|
||||
<Tooltip key={server.id}>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<ServerIcon
|
||||
server={server}
|
||||
isActive={instanceId === server.id}
|
||||
onClick={() => handleServerClick(server.id)}
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>{server.name}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Add Server Button */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="w-12 h-12 rounded-2xl hover:rounded-xl bg-gray-700 hover:bg-green-600 text-green-500 hover:text-white transition-all duration-200"
|
||||
onClick={openCreateServer}
|
||||
>
|
||||
<Plus size={24} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>Add a Server</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerSidebar;
|
||||
198
concord-client/src/components/layout/UserPanel.tsx
Normal file
198
concord-client/src/components/layout/UserPanel.tsx
Normal file
@@ -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 (
|
||||
<div className="flex items-center justify-between px-2 py-2 bg-gray-900 border-t border-gray-700">
|
||||
{/* User Info */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="flex items-center space-x-2 p-1 h-auto hover:bg-gray-700"
|
||||
>
|
||||
<div className="relative">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={user.picture} alt={user.username} />
|
||||
<AvatarFallback className="text-xs bg-blue-600">
|
||||
{user.username.slice(0, 2).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
{/* Status indicator */}
|
||||
<div
|
||||
className={`absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full border-2 border-gray-900 ${getStatusColor(user.status)}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0 text-left">
|
||||
<div className="text-sm font-medium text-white truncate">
|
||||
{user.nickname || user.username}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 truncate capitalize">
|
||||
{user.status}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="start" className="w-48">
|
||||
<DropdownMenuItem onClick={() => handleStatusChange("online")}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-3 h-3 rounded-full bg-green-500" />
|
||||
<span>Online</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleStatusChange("away")}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-3 h-3 rounded-full bg-yellow-500" />
|
||||
<span>Away</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleStatusChange("busy")}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-3 h-3 rounded-full bg-red-500" />
|
||||
<span>Do Not Disturb</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleStatusChange("offline")}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-3 h-3 rounded-full bg-gray-500" />
|
||||
<span>Invisible</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem onClick={openUserSettings}>
|
||||
<Settings size={16} className="mr-2" />
|
||||
User Settings
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={logout}
|
||||
className="text-red-400 focus:text-red-400"
|
||||
>
|
||||
Log Out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Voice Controls */}
|
||||
<div className="flex items-center space-x-1">
|
||||
<TooltipProvider>
|
||||
{/* Mute/Unmute */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={`h-8 w-8 ${isMuted ? "text-red-400 hover:text-red-300" : "text-gray-400 hover:text-white"}`}
|
||||
onClick={handleMuteToggle}
|
||||
>
|
||||
{isMuted ? <MicOff size={18} /> : <Mic size={18} />}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{isMuted ? "Unmute" : "Mute"}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{/* Deafen/Undeafen */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={`h-8 w-8 ${isDeafened ? "text-red-400 hover:text-red-300" : "text-gray-400 hover:text-white"}`}
|
||||
onClick={handleDeafenToggle}
|
||||
>
|
||||
<Headphones size={18} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{isDeafened ? "Undeafen" : "Deafen"}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{/* Settings */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-gray-400 hover:text-white"
|
||||
onClick={openUserSettings}
|
||||
>
|
||||
<Settings size={18} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>User Settings</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserPanel;
|
||||
37
concord-client/src/components/mode-toggle.tsx
Normal file
37
concord-client/src/components/mode-toggle.tsx
Normal file
@@ -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 (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon">
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
|
||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||
Light
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||
Dark
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||
System
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
60
concord-client/src/components/server/ServerIcon.tsx
Normal file
60
concord-client/src/components/server/ServerIcon.tsx
Normal file
@@ -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<ServerIconProps> = ({
|
||||
server,
|
||||
isActive,
|
||||
onClick,
|
||||
}) => {
|
||||
const getServerInitials = (name: string) => {
|
||||
return name
|
||||
.split(" ")
|
||||
.map((word) => word[0])
|
||||
.join("")
|
||||
.toUpperCase()
|
||||
.slice(0, 2);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative group">
|
||||
{/* Active indicator */}
|
||||
<div
|
||||
className={`absolute left-0 top-1/2 transform -translate-y-1/2 w-1 bg-white rounded-r transition-all duration-200 ${
|
||||
isActive ? "h-10" : "h-2 group-hover:h-5"
|
||||
}`}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={`w-12 h-12 ml-3 transition-all duration-200 ${
|
||||
isActive
|
||||
? "rounded-xl bg-blue-600 hover:bg-blue-500 text-white"
|
||||
: "rounded-2xl hover:rounded-xl bg-gray-700 hover:bg-blue-600 text-gray-300 hover:text-white"
|
||||
}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{server.icon ? (
|
||||
<img
|
||||
src={server.icon}
|
||||
alt={server.name}
|
||||
className="w-full h-full object-cover rounded-inherit"
|
||||
/>
|
||||
) : (
|
||||
<span className="font-semibold text-sm">
|
||||
{getServerInitials(server.name)}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerIcon;
|
||||
73
concord-client/src/components/theme-provider.tsx
Normal file
73
concord-client/src/components/theme-provider.tsx
Normal file
@@ -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<ThemeProviderState>(initialState);
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = "system",
|
||||
storageKey = "vite-ui-theme",
|
||||
...props
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setTheme] = useState<Theme>(
|
||||
() => (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 (
|
||||
<ThemeProviderContext.Provider {...props} value={value}>
|
||||
{children}
|
||||
</ThemeProviderContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeProviderContext);
|
||||
|
||||
if (context === undefined)
|
||||
throw new Error("useTheme must be used within a ThemeProvider");
|
||||
|
||||
return context;
|
||||
};
|
||||
66
concord-client/src/components/ui/alert.tsx
Normal file
66
concord-client/src/components/ui/alert.tsx
Normal file
@@ -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<typeof alertVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert"
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-title"
|
||||
className={cn(
|
||||
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-description"
|
||||
className={cn(
|
||||
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
||||
51
concord-client/src/components/ui/avatar.tsx
Normal file
51
concord-client/src/components/ui/avatar.tsx
Normal file
@@ -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<typeof AvatarPrimitive.Root>) {
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
data-slot="avatar"
|
||||
className={cn(
|
||||
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarImage({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot="avatar-image"
|
||||
className={cn("aspect-square size-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||
return (
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
58
concord-client/src/components/ui/button.tsx
Normal file
58
concord-client/src/components/ui/button.tsx
Normal file
@@ -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<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
92
concord-client/src/components/ui/card.tsx
Normal file
92
concord-client/src/components/ui/card.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
255
concord-client/src/components/ui/dropdown-menu.tsx
Normal file
255
concord-client/src/components/ui/dropdown-menu.tsx
Normal file
@@ -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<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
className,
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
}
|
||||
21
concord-client/src/components/ui/input.tsx
Normal file
21
concord-client/src/components/ui/input.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
||||
22
concord-client/src/components/ui/label.tsx
Normal file
22
concord-client/src/components/ui/label.tsx
Normal file
@@ -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<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Label }
|
||||
56
concord-client/src/components/ui/scroll-area.tsx
Normal file
56
concord-client/src/components/ui/scroll-area.tsx
Normal file
@@ -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<typeof ScrollAreaPrimitive.Root>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
data-slot="scroll-area"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function ScrollBar({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none p-px transition-colors select-none",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="bg-border relative flex-1 rounded-full"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
)
|
||||
}
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
23
concord-client/src/components/ui/sonner.tsx
Normal file
23
concord-client/src/components/ui/sonner.tsx
Normal file
@@ -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 (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toaster }
|
||||
59
concord-client/src/components/ui/tooltip.tsx
Normal file
59
concord-client/src/components/ui/tooltip.tsx
Normal file
@@ -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<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function TooltipTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
72
concord-client/src/hooks/useChannel.ts
Normal file
72
concord-client/src/hooks/useChannel.ts
Normal file
@@ -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<CategoryWithChannels[]> => {
|
||||
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
|
||||
});
|
||||
};
|
||||
32
concord-client/src/hooks/useResponsive.ts
Normal file
32
concord-client/src/hooks/useResponsive.ts
Normal file
@@ -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,
|
||||
};
|
||||
};
|
||||
78
concord-client/src/hooks/useServers.ts
Normal file
78
concord-client/src/hooks/useServers.ts
Normal file
@@ -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<Instance[]> => {
|
||||
// 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<InstanceWithDetails | null> => {
|
||||
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<UserWithRoles[]> => {
|
||||
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)
|
||||
});
|
||||
};
|
||||
@@ -1,68 +1,120 @@
|
||||
@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 {
|
||||
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;
|
||||
--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);
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
.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);
|
||||
}
|
||||
|
||||
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;
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
6
concord-client/src/lib/utils.ts
Normal file
6
concord-client/src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
192
concord-client/src/pages/ChatPage.tsx
Normal file
192
concord-client/src/pages/ChatPage.tsx
Normal file
@@ -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 (
|
||||
// <div className="flex flex-col h-full">
|
||||
// {/* DM Header */}
|
||||
// <div className="flex items-center justify-between px-4 py-3 border-b border-gray-700 bg-gray-800">
|
||||
// <div className="flex items-center space-x-2">
|
||||
// <Inbox size={20} className="text-gray-400" />
|
||||
// <span className="font-semibold text-white">Direct Messages</span>
|
||||
// </div>
|
||||
// <div className="flex items-center space-x-2">
|
||||
// <Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
// <HelpCircle size={16} />
|
||||
// </Button>
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
// {/* DM Content */}
|
||||
// <div className="flex-1 flex items-center justify-center">
|
||||
// <div className="text-center text-gray-400 max-w-md">
|
||||
// <div className="w-16 h-16 mx-auto mb-4 bg-gray-700 rounded-full flex items-center justify-center">
|
||||
// <Inbox size={24} />
|
||||
// </div>
|
||||
// <h2 className="text-xl font-semibold mb-2 text-white">No Direct Messages</h2>
|
||||
// <p className="text-sm">
|
||||
// When someone sends you a direct message, it will show up here.
|
||||
// </p>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
if (!currentChannel && channelId) {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="text-center text-gray-400">
|
||||
<h2 className="text-xl font-semibold mb-2">Channel not found</h2>
|
||||
<p>
|
||||
The channel you're looking for doesn't exist or you don't have
|
||||
access to it.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Default channel view (when just /channels/instanceId)
|
||||
if (!channelId && instance) {
|
||||
const firstChannel = categories?.[0]?.channels?.[0];
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="text-center text-gray-400 max-w-md">
|
||||
<div className="w-16 h-16 mx-auto mb-4 bg-gray-700 rounded-full flex items-center justify-center">
|
||||
<Hash size={24} />
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold mb-2 text-white">
|
||||
Welcome to {instance.name}!
|
||||
</h2>
|
||||
<p className="text-sm mb-4">
|
||||
{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!"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ChannelIcon = currentChannel?.type === "voice" ? Volume2 : Hash;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Channel Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-700 bg-gray-800">
|
||||
<div className="flex items-center space-x-2">
|
||||
<ChannelIcon size={20} className="text-gray-400" />
|
||||
<span className="font-semibold text-white">
|
||||
{currentChannel?.name}
|
||||
</span>
|
||||
{currentChannel?.topic && (
|
||||
<>
|
||||
<div className="w-px h-4 bg-gray-600" />
|
||||
<span className="text-sm text-gray-400 truncate max-w-xs">
|
||||
{currentChannel.topic}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<Pin size={16} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={`h-8 w-8 ${showMemberList ? "text-white bg-gray-700" : "text-gray-400"}`}
|
||||
onClick={toggleMemberList}
|
||||
>
|
||||
<Users size={16} />
|
||||
</Button>
|
||||
<div className="w-40">
|
||||
<Input
|
||||
placeholder="Search"
|
||||
className="h-8 bg-gray-700 border-none text-sm"
|
||||
/>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<Inbox size={16} />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<HelpCircle size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chat Content */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
{/* Messages Area */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="p-4">
|
||||
{/* Welcome Message */}
|
||||
<div className="flex flex-col space-y-2 mb-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-16 h-16 bg-blue-600 rounded-full flex items-center justify-center">
|
||||
<ChannelIcon size={24} className="text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold text-white">
|
||||
Welcome to #{currentChannel?.name}!
|
||||
</h3>
|
||||
<p className="text-gray-400">
|
||||
This is the start of the #{currentChannel?.name} channel.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{currentChannel?.topic && (
|
||||
<div className="text-gray-400 bg-gray-800 p-3 rounded border-l-4 border-gray-600">
|
||||
<strong>Topic:</strong> {currentChannel.topic}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Placeholder messages */}
|
||||
<div className="space-y-4 text-gray-400 text-center">
|
||||
<p>No messages yet. Start the conversation!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Message Input */}
|
||||
<div className="p-4 bg-gray-800">
|
||||
<div className="relative">
|
||||
<Input
|
||||
placeholder={`Message #${currentChannel?.name || "channel"}`}
|
||||
className="w-full bg-gray-700 border-none text-white placeholder-gray-400 pr-12"
|
||||
/>
|
||||
<div className="absolute right-3 top-1/2 transform -translate-y-1/2">
|
||||
<div className="flex items-center space-x-1">
|
||||
{/* Emoji picker, file upload, etc. would go here */}
|
||||
<div className="text-gray-400 text-sm">Press Enter to send</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatPage;
|
||||
109
concord-client/src/pages/LoginPage.tsx
Normal file
109
concord-client/src/pages/LoginPage.tsx
Normal file
@@ -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 <Navigate to="/channels/@me" replace />;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="min-h-screen bg-gray-900 flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md bg-gray-800 border-gray-700">
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-2xl font-bold text-white">
|
||||
Welcome back!
|
||||
</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
We're so excited to see you again!
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleLogin} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="username" className="text-gray-300">
|
||||
Username
|
||||
</Label>
|
||||
<Input
|
||||
id="username"
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="bg-gray-700 border-gray-600 text-white"
|
||||
placeholder="Enter your username"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password" className="text-gray-300">
|
||||
Password
|
||||
</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="bg-gray-700 border-gray-600 text-white"
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? "Logging in..." : "Log In"}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
12
concord-client/src/pages/NotFoundPage.tsx
Normal file
12
concord-client/src/pages/NotFoundPage.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
const NotFoundPage: React.FC = () => {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 flex items-center justify-center">
|
||||
<div className="text-center text-gray-400">
|
||||
<h1 className="text-4xl font-bold mb-4">404</h1>
|
||||
<p className="text-xl">Page not found</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotFoundPage;
|
||||
12
concord-client/src/pages/SettingsPage.tsx
Normal file
12
concord-client/src/pages/SettingsPage.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
const SettingsPage: React.FC = () => {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="text-center text-gray-400">
|
||||
<h2 className="text-xl font-semibold mb-2">Settings</h2>
|
||||
<p>User settings will go here</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsPage;
|
||||
69
concord-client/src/stores/authStore.ts
Normal file
69
concord-client/src/stores/authStore.ts
Normal file
@@ -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<User>) => void;
|
||||
logout: () => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
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);
|
||||
138
concord-client/src/stores/uiStore.ts
Normal file
138
concord-client/src/stores/uiStore.ts
Normal file
@@ -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<UiState>()(
|
||||
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,
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
91
concord-client/src/types/api.ts
Normal file
91
concord-client/src/types/api.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { Instance, Category, Channel, User, Role, Message } from "./database";
|
||||
|
||||
// API Response wrappers
|
||||
export interface ApiResponse<T> {
|
||||
data: T;
|
||||
success: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
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;
|
||||
}
|
||||
93
concord-client/src/types/database.ts
Normal file
93
concord-client/src/types/database.ts
Normal file
@@ -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;
|
||||
}
|
||||
@@ -5,6 +5,10 @@
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
},
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
@@ -18,8 +22,8 @@
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
},
|
||||
"include": ["src", "electron"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
"references": [{ "path": "./tsconfig.node.json" }],
|
||||
}
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import path from 'node:path'
|
||||
import electron from 'vite-plugin-electron/simple'
|
||||
import react from '@vitejs/plugin-react'
|
||||
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({
|
||||
@@ -10,20 +11,27 @@ export default defineConfig({
|
||||
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"),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user