Added stuff idek
This commit is contained in:
87
bun.lock
87
bun.lock
@@ -6,42 +6,45 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@convex-dev/auth": "^0.0.81",
|
"@convex-dev/auth": "^0.0.81",
|
||||||
"@hookform/resolvers": "^5.2.1",
|
"@hookform/resolvers": "^5.2.1",
|
||||||
|
"@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-label": "^2.1.7",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@sentry/nextjs": "^10.7.0",
|
"@sentry/nextjs": "^10.8.0",
|
||||||
"@t3-oss/env-nextjs": "^0.13.8",
|
"@t3-oss/env-nextjs": "^0.13.8",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"convex": "^1.26.0",
|
"convex": "^1.26.2",
|
||||||
"eslint-plugin-prettier": "^5.5.4",
|
"eslint-plugin-prettier": "^5.5.4",
|
||||||
"lucide-react": "^0.542.0",
|
"lucide-react": "^0.542.0",
|
||||||
"next": "15.2.3",
|
"next": "15.2.3",
|
||||||
"next-plausible": "^3.12.4",
|
"next-plausible": "^3.12.4",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.0.0",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.1.1",
|
||||||
"react-hook-form": "^7.62.0",
|
"react-hook-form": "^7.62.0",
|
||||||
"require-in-the-middle": "^7.5.2",
|
"require-in-the-middle": "^7.5.2",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"typescript-eslint": "^8.41.0",
|
"typescript-eslint": "^8.41.0",
|
||||||
"zod": "^4.1.5",
|
"zod": "^4.1.5",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4.1.12",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20.19.11",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19.1.12",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19.1.9",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.6.1",
|
||||||
"eslint": "^9",
|
"eslint": "^9.34.0",
|
||||||
"eslint-config-next": "15.2.3",
|
"eslint-config-next": "15.2.3",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.6.2",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4.1.12",
|
||||||
"tw-animate-css": "^1.3.7",
|
"tw-animate-css": "^1.3.7",
|
||||||
"typescript": "^5",
|
"typescript": "^5.9.2",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -296,6 +299,14 @@
|
|||||||
|
|
||||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.5", "", { "dependencies": { "@eslint/core": "^0.15.2", "levn": "^0.4.1" } }, "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w=="],
|
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.5", "", { "dependencies": { "@eslint/core": "^0.15.2", "levn": "^0.4.1" } }, "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w=="],
|
||||||
|
|
||||||
|
"@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=="],
|
||||||
|
|
||||||
"@hookform/resolvers": ["@hookform/resolvers@5.2.1", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-u0+6X58gkjMcxur1wRWokA7XsiiBJ6aK17aPZxhkoYiK5J+HcTx0Vhu9ovXe6H+dVpO6cjrn2FkJTryXEMlryQ=="],
|
"@hookform/resolvers": ["@hookform/resolvers@5.2.1", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-u0+6X58gkjMcxur1wRWokA7XsiiBJ6aK17aPZxhkoYiK5J+HcTx0Vhu9ovXe6H+dVpO6cjrn2FkJTryXEMlryQ=="],
|
||||||
|
|
||||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||||
@@ -528,6 +539,10 @@
|
|||||||
|
|
||||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
"@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-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-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=="],
|
||||||
@@ -536,10 +551,24 @@
|
|||||||
|
|
||||||
"@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-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-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-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-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-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=="],
|
||||||
@@ -558,8 +587,18 @@
|
|||||||
|
|
||||||
"@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-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-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/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
|
||||||
|
|
||||||
"@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@28.0.1", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA=="],
|
"@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@28.0.1", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA=="],
|
||||||
|
|
||||||
"@rollup/pluginutils": ["@rollup/pluginutils@5.2.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw=="],
|
"@rollup/pluginutils": ["@rollup/pluginutils@5.2.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw=="],
|
||||||
@@ -842,6 +881,8 @@
|
|||||||
|
|
||||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
"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=="],
|
||||||
|
|
||||||
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
|
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
|
||||||
|
|
||||||
"array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
|
"array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
|
||||||
@@ -980,6 +1021,8 @@
|
|||||||
|
|
||||||
"detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
|
"detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
|
||||||
|
|
||||||
|
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
||||||
|
|
||||||
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
|
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
|
||||||
|
|
||||||
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
|
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
|
||||||
@@ -1110,6 +1153,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-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-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||||
|
|
||||||
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
|
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
|
||||||
@@ -1470,6 +1515,12 @@
|
|||||||
|
|
||||||
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||||
|
|
||||||
|
"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-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-pkg": ["read-pkg@3.0.0", "", { "dependencies": { "load-json-file": "^4.0.0", "normalize-package-data": "^2.3.2", "path-type": "^3.0.0" } }, "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA=="],
|
"read-pkg": ["read-pkg@3.0.0", "", { "dependencies": { "load-json-file": "^4.0.0", "normalize-package-data": "^2.3.2", "path-type": "^3.0.0" } }, "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA=="],
|
||||||
|
|
||||||
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||||
@@ -1540,6 +1591,8 @@
|
|||||||
|
|
||||||
"simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="],
|
"simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="],
|
||||||
|
|
||||||
|
"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": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
@@ -1642,6 +1695,12 @@
|
|||||||
|
|
||||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
"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=="],
|
||||||
|
|
||||||
"uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
|
"uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
|
||||||
|
|
||||||
"validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="],
|
"validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="],
|
||||||
|
@@ -1,228 +1,22 @@
|
|||||||
import {
|
import { ConvexError } from "convex/values";
|
||||||
ConvexCredentials,
|
import { Password } from "@convex-dev/auth/providers/Password";
|
||||||
type ConvexCredentialsUserConfig,
|
import type { DataModel } from "./_generated/dataModel";
|
||||||
} from "@convex-dev/auth/providers/ConvexCredentials";
|
|
||||||
import {
|
|
||||||
type EmailConfig,
|
|
||||||
type GenericActionCtxWithAuthConfig,
|
|
||||||
type GenericDoc,
|
|
||||||
createAccount,
|
|
||||||
invalidateSessions,
|
|
||||||
modifyAccountCredentials,
|
|
||||||
retrieveAccount,
|
|
||||||
signInViaProvider,
|
|
||||||
} from "@convex-dev/auth/server";
|
|
||||||
import type {
|
|
||||||
DocumentByName,
|
|
||||||
GenericDataModel,
|
|
||||||
WithoutSystemFields,
|
|
||||||
} from "convex/server";
|
|
||||||
import type { Value } from "convex/values";
|
|
||||||
import { Scrypt } from "lucia";
|
|
||||||
|
|
||||||
export type PasswordConfig<DataModel extends GenericDataModel> = {
|
export default Password<DataModel>({
|
||||||
id?: string;
|
profile(params, ctx) {
|
||||||
/**
|
return {
|
||||||
* Perform checks on provided params and customize the user
|
email: params.email as string,
|
||||||
* information stored after sign up, including email normalization.
|
name: params.name as string,
|
||||||
*
|
};
|
||||||
* Called for every flow ("signUp", "signIn", "reset",
|
},
|
||||||
* "reset-verification" and "email-verification").
|
validatePasswordRequirements: (password: string) => {
|
||||||
*/
|
if (
|
||||||
profile?: (
|
password.length < 8 ||
|
||||||
/**
|
!/\d/.test(password) ||
|
||||||
* The values passed to the `signIn` function.
|
!/[a-z]/.test(password) ||
|
||||||
*/
|
!/[A-Z]/.test(password)
|
||||||
params: Record<string, Value | undefined>,
|
) {
|
||||||
/**
|
throw new ConvexError("Invalid password.");
|
||||||
* Convex ActionCtx in case you want to read from or write to
|
}
|
||||||
* the database.
|
},
|
||||||
*/
|
});
|
||||||
ctx: GenericActionCtxWithAuthConfig<DataModel>,
|
|
||||||
) => WithoutSystemFields<DocumentByName<DataModel, "users">> & {
|
|
||||||
email: string;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Performs custom validation on password provided during sign up or reset.
|
|
||||||
*
|
|
||||||
* Otherwise the default validation is used (password is not empty and
|
|
||||||
* at least 8 characters in length).
|
|
||||||
*
|
|
||||||
* If the provided password is invalid, implementations must throw an Error.
|
|
||||||
*
|
|
||||||
* @param password the password supplied during "signUp" or
|
|
||||||
* "reset-verification" flows.
|
|
||||||
*/
|
|
||||||
validatePasswordRequirements?: (password: string) => void;
|
|
||||||
/**
|
|
||||||
* Provide hashing and verification functions if you want to control
|
|
||||||
* how passwords are hashed.
|
|
||||||
*/
|
|
||||||
crypto?: ConvexCredentialsUserConfig["crypto"];
|
|
||||||
/**
|
|
||||||
* An Auth.js email provider used to require verification
|
|
||||||
* before password reset.
|
|
||||||
*/
|
|
||||||
reset?: EmailConfig | ((...args: any) => EmailConfig);
|
|
||||||
/**
|
|
||||||
* An Auth.js email provider used to require verification
|
|
||||||
* before sign up / sign in.
|
|
||||||
*/
|
|
||||||
verify?: EmailConfig | ((...args: any) => EmailConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Email and password authentication provider.
|
|
||||||
*
|
|
||||||
* Passwords are by default hashed using Scrypt from Lucia.
|
|
||||||
* You can customize the hashing via the `crypto` option.
|
|
||||||
*
|
|
||||||
* Email verification is not required unless you pass
|
|
||||||
* an email provider to the `verify` option.
|
|
||||||
*/
|
|
||||||
export function Password<DataModel extends GenericDataModel>(
|
|
||||||
config: PasswordConfig<DataModel> = {},
|
|
||||||
) {
|
|
||||||
const provider = config.id ?? "password";
|
|
||||||
return ConvexCredentials<DataModel>({
|
|
||||||
id: "password",
|
|
||||||
authorize: async (params, ctx) => {
|
|
||||||
const flow = params.flow as string;
|
|
||||||
const passwordToValidate =
|
|
||||||
flow === "signUp"
|
|
||||||
? (params.password as string)
|
|
||||||
: flow === "reset-verification"
|
|
||||||
? (params.newPassword as string)
|
|
||||||
: null;
|
|
||||||
if (passwordToValidate !== null) {
|
|
||||||
if (config.validatePasswordRequirements !== undefined) {
|
|
||||||
config.validatePasswordRequirements(passwordToValidate);
|
|
||||||
} else {
|
|
||||||
validateDefaultPasswordRequirements(passwordToValidate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const profile = config.profile?.(params, ctx) ?? defaultProfile(params);
|
|
||||||
const { email } = profile;
|
|
||||||
const secret = params.password as string;
|
|
||||||
let account: GenericDoc<DataModel, "authAccounts">;
|
|
||||||
let user: GenericDoc<DataModel, "users">;
|
|
||||||
if (flow === "signUp") {
|
|
||||||
if (secret === undefined) {
|
|
||||||
throw new Error("Missing `password` param for `signUp` flow");
|
|
||||||
}
|
|
||||||
const created = await createAccount(ctx, {
|
|
||||||
provider,
|
|
||||||
account: { id: email, secret },
|
|
||||||
profile: profile as any,
|
|
||||||
shouldLinkViaEmail: config.verify !== undefined,
|
|
||||||
shouldLinkViaPhone: false,
|
|
||||||
});
|
|
||||||
({ account, user } = created);
|
|
||||||
} else if (flow === "signIn") {
|
|
||||||
if (secret === undefined) {
|
|
||||||
throw new Error("Missing `password` param for `signIn` flow");
|
|
||||||
}
|
|
||||||
const retrieved = await retrieveAccount(ctx, {
|
|
||||||
provider,
|
|
||||||
account: { id: email, secret },
|
|
||||||
});
|
|
||||||
if (retrieved === null) {
|
|
||||||
throw new Error("Invalid credentials");
|
|
||||||
}
|
|
||||||
({ account, user } = retrieved);
|
|
||||||
// START: Optional, support password reset
|
|
||||||
} else if (flow === "reset") {
|
|
||||||
if (!config.reset) {
|
|
||||||
throw new Error(`Password reset is not enabled for ${provider}`);
|
|
||||||
}
|
|
||||||
const { account } = await retrieveAccount(ctx, {
|
|
||||||
provider,
|
|
||||||
account: { id: email },
|
|
||||||
});
|
|
||||||
return await signInViaProvider(ctx, config.reset, {
|
|
||||||
accountId: account._id,
|
|
||||||
params,
|
|
||||||
});
|
|
||||||
} else if (flow === "reset-verification") {
|
|
||||||
if (!config.reset) {
|
|
||||||
throw new Error(`Password reset is not enabled for ${provider}`);
|
|
||||||
}
|
|
||||||
if (params.newPassword === undefined) {
|
|
||||||
throw new Error(
|
|
||||||
"Missing `newPassword` param for `reset-verification` flow",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const result = await signInViaProvider(ctx, config.reset, { params });
|
|
||||||
if (result === null) {
|
|
||||||
throw new Error("Invalid code");
|
|
||||||
}
|
|
||||||
const { userId, sessionId } = result;
|
|
||||||
const secret = params.newPassword as string;
|
|
||||||
await modifyAccountCredentials(ctx, {
|
|
||||||
provider,
|
|
||||||
account: { id: email, secret },
|
|
||||||
});
|
|
||||||
await invalidateSessions(ctx, { userId, except: [sessionId] });
|
|
||||||
return { userId, sessionId };
|
|
||||||
// END
|
|
||||||
// START: Optional, email verification during sign in
|
|
||||||
} else if (flow === "email-verification") {
|
|
||||||
if (!config.verify) {
|
|
||||||
throw new Error(`Email verification is not enabled for ${provider}`);
|
|
||||||
}
|
|
||||||
const { account } = await retrieveAccount(ctx, {
|
|
||||||
provider,
|
|
||||||
account: { id: email },
|
|
||||||
});
|
|
||||||
return await signInViaProvider(ctx, config.verify, {
|
|
||||||
accountId: account._id,
|
|
||||||
params,
|
|
||||||
});
|
|
||||||
// END
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
"Missing `flow` param, it must be one of " +
|
|
||||||
'"signUp", "signIn", "reset", "reset-verification" or ' +
|
|
||||||
'"email-verification"!',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// START: Optional, email verification during sign in
|
|
||||||
if (config.verify && !account.emailVerified) {
|
|
||||||
return await signInViaProvider(ctx, config.verify, {
|
|
||||||
accountId: account._id,
|
|
||||||
params,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// END
|
|
||||||
return { userId: user._id };
|
|
||||||
},
|
|
||||||
crypto: {
|
|
||||||
async hashSecret(password: string) {
|
|
||||||
return await new Scrypt().hash(password);
|
|
||||||
},
|
|
||||||
async verifySecret(password: string, hash: string) {
|
|
||||||
return await new Scrypt().verify(hash, password);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
extraProviders: [config.reset, config.verify],
|
|
||||||
...config,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateDefaultPasswordRequirements(password: string) {
|
|
||||||
if (
|
|
||||||
password.length < 8 ||
|
|
||||||
!/\d/.test(password) ||
|
|
||||||
!/[a-z]/.test(password) ||
|
|
||||||
!/[A-Z]/.test(password)
|
|
||||||
) {
|
|
||||||
throw new Error("Invalid password.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function defaultProfile(params: Record<string, unknown>) {
|
|
||||||
return {
|
|
||||||
email: params.email as string,
|
|
||||||
name: params.name as string,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
@@ -1,6 +1,20 @@
|
|||||||
import { convexAuth } from '@convex-dev/auth/server';
|
import { convexAuth, getAuthUserId } from '@convex-dev/auth/server';
|
||||||
import { Password } from './CustomPassword';
|
import { query } from './_generated/server';
|
||||||
|
import Password from './CustomPassword';
|
||||||
|
|
||||||
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
|
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
|
||||||
providers: [Password],
|
providers: [Password],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getUser = query(async (ctx) => {
|
||||||
|
const userId = await getAuthUserId(ctx);
|
||||||
|
if (!userId) return null;
|
||||||
|
const user = await ctx.db.get(userId);
|
||||||
|
if (!user) return null;
|
||||||
|
return {
|
||||||
|
id: user._id,
|
||||||
|
email: user.email ?? null,
|
||||||
|
name: user.name ?? null,
|
||||||
|
image: user.image ?? null,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@@ -21,21 +21,6 @@ const nextConfig = withPlausibleProxy({
|
|||||||
bodySizeLimit: '10mb',
|
bodySizeLimit: '10mb',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
turbopack: {
|
|
||||||
rules: {
|
|
||||||
'*.svg': {
|
|
||||||
loaders: [
|
|
||||||
{
|
|
||||||
loader: '@svgr/webpack',
|
|
||||||
options: {
|
|
||||||
icon: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
as: '*.js',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const sentryConfig = {
|
const sentryConfig = {
|
||||||
|
@@ -20,6 +20,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@convex-dev/auth": "^0.0.81",
|
"@convex-dev/auth": "^0.0.81",
|
||||||
"@hookform/resolvers": "^5.2.1",
|
"@hookform/resolvers": "^5.2.1",
|
||||||
|
"@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-label": "^2.1.7",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
@@ -38,6 +40,7 @@
|
|||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-hook-form": "^7.62.0",
|
"react-hook-form": "^7.62.0",
|
||||||
"require-in-the-middle": "^7.5.2",
|
"require-in-the-middle": "^7.5.2",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"typescript-eslint": "^8.41.0",
|
"typescript-eslint": "^8.41.0",
|
||||||
"zod": "^4.1.5"
|
"zod": "^4.1.5"
|
||||||
|
1
public/icons/misc/gitea.svg
Normal file
1
public/icons/misc/gitea.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M414.4 376.5 200 379.6l-1.4-256.7 103.5-15.2 108.8-1.5z" style="fill:#fff"/><path d="M502.6 103.7c-3.3-3.3-7.8-3.3-7.8-3.3s-95.5 5.4-144.9 6.5c-10.8.2-21.6.5-32.3.6V203c-4.5-2.1-9-4.3-13.5-6.4 0-29.6-.1-88.9-.1-88.9-23.6.3-72.7-1.8-72.7-1.8s-115.2-5.8-127.7-6.9c-8-.5-18.3-1.7-31.8 1.2-7.1 1.5-27.3 6-43.8 21.9C-8.7 154.8.7 206.7 1.9 214.5c1.4 9.5 5.6 36 25.8 59 37.3 45.7 117.6 44.6 117.6 44.6s9.9 23.5 24.9 45.2c20.4 27 41.3 48 61.7 50.5 51.3 0 153.9-.1 153.9-.1s9.8.1 23-8.4c11.4-6.9 21.6-19.1 21.6-19.1s10.5-11.2 25.2-36.9c4.5-7.9 8.2-15.6 11.5-22.8 0 0 45-95.4 45-188.2-1-28-7.9-33-9.5-34.6M97.7 269.9c-21.1-6.9-30.1-15.2-30.1-15.2S52 243.8 44.2 222.3c-13.4-36-1.1-58-1.1-58s6.8-18.3 31.4-24.4c11.2-3 25.2-2.5 25.2-2.5s5.8 48.4 12.8 76.7c5.9 23.8 20.2 63.3 20.2 63.3s-21.3-2.6-35-7.5m289.4-4.5c-5.2 12.6-44.8 92.1-44.8 92.1s-5 11.8-16 12.5c-4.7.3-8.4-1-8.4-1s-.2-.1-4.3-1.7l-92-44.8s-8.9-4.6-10.4-12.7c-1.8-6.6 2.2-14.7 2.2-14.7l44.2-91.1s3.9-7.9 9.9-10.6c.5-.2 1.9-.8 3.7-1.2 6.6-1.7 14.7 2.3 14.7 2.3l18.4 8.9c-3.7 7.6-7.5 15.2-11.2 22.9-5.5-.1-10.5 2.9-13.1 7.7-2.8 5.1-2.2 11.5 1.5 16.1-6.6 13.8-13.3 27.5-19.9 41.1-6.7.1-12.5 4.7-14.1 11.2-1.5 6.5 1.6 13.3 7.4 16.3 6.3 3.3 14.3 1.5 18.5-4.4 4.2-5.8 3.5-13.8-1.5-18.8l19.5-40c1.2.1 3 .2 5-.4 3.3-.7 5.8-2.9 5.8-2.9 3.4 1.5 7 3.1 10.8 5 3.9 2 7.6 4 10.9 5.9.7.4 1.5.9 2.3 1.5 1.3 1.1 2.8 2.5 3.8 4.5 1.5 4.5-1.5 12.1-1.5 12.1-1.9 6.2-15 33.1-15 33.1-6.6-.2-12.5 4.1-14.4 10.2-2.1 6.6.9 14.1 7.2 17.3 6.4 3.3 14.2 1.4 18.3-4.3 4.1-5.5 3.7-13.3-.9-18.4l4.6-9.2c4.1-8.5 11-24.8 11-24.8.7-1.4 4.6-8.4 2.2-17.3-2-9.3-10.3-13.6-10.3-13.6-9.9-6.4-23.8-12.4-23.8-12.4s0-3.3-.9-5.8-2.3-4.2-3.2-5.1c3.6-7.6 7.4-15.1 11-22.6l61.8 29.9s10.3 4.6 12.5 13.2c1.5 6-.4 11.4-1.5 14" style="fill:#609926"/></svg>
|
After Width: | Height: | Size: 1.8 KiB |
63
public/icons/tv/enter.svg
Normal file
63
public/icons/tv/enter.svg
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="48"
|
||||||
|
height="48"
|
||||||
|
viewBox="0 0 12.7 12.7"
|
||||||
|
version="1.1"
|
||||||
|
id="svg513"
|
||||||
|
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
|
||||||
|
sodipodi:docname="Fullscreen.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview515"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="8.4359982"
|
||||||
|
inkscape:cx="69.108597"
|
||||||
|
inkscape:cy="37.458519"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1017"
|
||||||
|
inkscape:window-x="3832"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs510" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<path
|
||||||
|
style="fill:#b3b3b3;stroke-width:0.193912"
|
||||||
|
d="M 5.8615386,0 H 0 V 5.8615386 H 0.97692256 V 0.97692327 H 5.8615386 Z"
|
||||||
|
id="path396-6"
|
||||||
|
sodipodi:nodetypes="ccccccc" />
|
||||||
|
<path
|
||||||
|
style="fill:#b3b3b3;stroke-width:0.193912"
|
||||||
|
d="M 0,6.8384619 V 12.7 H 5.8615386 V 11.723076 H 0.97692256 V 6.8384619 Z"
|
||||||
|
id="path396-52"
|
||||||
|
sodipodi:nodetypes="ccccccc" />
|
||||||
|
<path
|
||||||
|
style="fill:#b3b3b3;stroke-width:0.193912"
|
||||||
|
d="M 6.8384613,12.7 H 12.7 V 6.8384619 H 11.723078 V 11.723076 H 6.8384613 Z"
|
||||||
|
id="path396-4"
|
||||||
|
sodipodi:nodetypes="ccccccc" />
|
||||||
|
<path
|
||||||
|
style="fill:#b3b3b3;stroke-width:0.193912"
|
||||||
|
d="M 12.7,5.8615386 V 0 H 6.8384613 V 0.97692327 H 11.723078 V 5.8615386 Z"
|
||||||
|
id="path396-0"
|
||||||
|
sodipodi:nodetypes="ccccccc" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
63
public/icons/tv/exit.svg
Normal file
63
public/icons/tv/exit.svg
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="48"
|
||||||
|
height="48"
|
||||||
|
viewBox="0 0 12.7 12.7"
|
||||||
|
version="1.1"
|
||||||
|
id="svg513"
|
||||||
|
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
|
||||||
|
sodipodi:docname="ExitFullscreen.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview515"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="8.4359982"
|
||||||
|
inkscape:cx="69.108597"
|
||||||
|
inkscape:cy="37.458519"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1017"
|
||||||
|
inkscape:window-x="3832"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs510" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<path
|
||||||
|
style="fill:#b3b3b3;stroke-width:0.193906"
|
||||||
|
d="M 0,5.8620045 H 5.8615381 L 5.8621526,0 H 4.8849607 L 4.8846152,4.8851478 H 0 Z"
|
||||||
|
id="path190"
|
||||||
|
sodipodi:nodetypes="ccccccc" />
|
||||||
|
<path
|
||||||
|
style="fill:#b3b3b3;stroke-width:0.193906"
|
||||||
|
d="M 6.8384615,8.6556325e-4 V 5.8620045 H 12.7 V 4.8851478 H 7.815384 V 8.6556325e-4 Z"
|
||||||
|
id="path396"
|
||||||
|
sodipodi:nodetypes="ccccccc" />
|
||||||
|
<path
|
||||||
|
style="fill:#b3b3b3;stroke-width:0.193906"
|
||||||
|
d="M 12.7,6.8388612 H 6.8384615 V 12.7 H 7.815384 V 7.8157173 H 12.7 Z"
|
||||||
|
id="path396-5"
|
||||||
|
sodipodi:nodetypes="ccccccc" />
|
||||||
|
<path
|
||||||
|
style="fill:#b3b3b3;stroke-width:0.193906"
|
||||||
|
d="M 5.8615381,12.7 V 6.8388612 H 0 V 7.8157173 H 4.8846152 V 12.7 Z"
|
||||||
|
id="path396-1"
|
||||||
|
sodipodi:nodetypes="ccccccc" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
14
src/app/(auth)/profile/layout.tsx
Normal file
14
src/app/(auth)/profile/layout.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const generateMetadata = (): Metadata => {
|
||||||
|
return {
|
||||||
|
title: 'Profile',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProfileLayout = ({
|
||||||
|
children,
|
||||||
|
}: Readonly<{ children: React.ReactNode }>) => {
|
||||||
|
return <div>{children}</div>;
|
||||||
|
};
|
||||||
|
export default ProfileLayout;
|
4
src/app/(auth)/profile/page.tsx
Normal file
4
src/app/(auth)/profile/page.tsx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
const Profile = () => {
|
||||||
|
return <div></div>;
|
||||||
|
};
|
||||||
|
export default Profile;
|
79
src/app/global-error.tsx
Normal file
79
src/app/global-error.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
import NextError from 'next/error';
|
||||||
|
import { Geist, Geist_Mono } from 'next/font/google';
|
||||||
|
import '@/styles/globals.css';
|
||||||
|
import {
|
||||||
|
ConvexClientProvider,
|
||||||
|
ThemeProvider,
|
||||||
|
TVModeProvider,
|
||||||
|
} from '@/components/providers'
|
||||||
|
import * as Sentry from '@sentry/nextjs';
|
||||||
|
import { generateMetadata } from '@/lib/metadata';
|
||||||
|
import PlausibleProvider from 'next-plausible';
|
||||||
|
import Header from '@/components/layout/header';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { Button, Toaster } from '@/components/ui';
|
||||||
|
|
||||||
|
const geistSans = Geist({
|
||||||
|
variable: '--font-geist-sans',
|
||||||
|
subsets: ['latin'],
|
||||||
|
});
|
||||||
|
const geistMono = Geist_Mono({
|
||||||
|
variable: '--font-geist-mono',
|
||||||
|
subsets: ['latin'],
|
||||||
|
});
|
||||||
|
const metadata: Metadata = generateMetadata();
|
||||||
|
metadata.title = `Error | Tech Tracker`;
|
||||||
|
export {metadata};
|
||||||
|
|
||||||
|
type GlobalErrorProps = {
|
||||||
|
error: Error & { digest?: string}
|
||||||
|
reset?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const GlobalError = ({error, reset = undefined }: GlobalErrorProps) => {
|
||||||
|
useEffect(() => {
|
||||||
|
Sentry.captureException(error);
|
||||||
|
}, [error])
|
||||||
|
return (
|
||||||
|
<ConvexClientProvider>
|
||||||
|
<PlausibleProvider
|
||||||
|
domain='techtracker.gbrown.org'
|
||||||
|
customDomain='https://plausible.gbrown.org'
|
||||||
|
>
|
||||||
|
<html
|
||||||
|
lang='en'
|
||||||
|
suppressHydrationWarning
|
||||||
|
>
|
||||||
|
<body
|
||||||
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
|
>
|
||||||
|
<ThemeProvider
|
||||||
|
attribute='class'
|
||||||
|
defaultTheme='system'
|
||||||
|
enableSystem
|
||||||
|
disableTransitionOnChange
|
||||||
|
>
|
||||||
|
<ConvexClientProvider>
|
||||||
|
<TVModeProvider>
|
||||||
|
<Header />
|
||||||
|
<main className='min-h-[90vh] flex flex-col items-center'>
|
||||||
|
<NextError statusCode={0} />
|
||||||
|
{reset !== undefined && (
|
||||||
|
<Button onClick={() => reset()}>Try Again</Button>
|
||||||
|
)}
|
||||||
|
<Toaster />
|
||||||
|
</main>
|
||||||
|
</TVModeProvider>
|
||||||
|
</ConvexClientProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
</PlausibleProvider>
|
||||||
|
</ConvexClientProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default GlobalError;
|
@@ -2,20 +2,24 @@ import type { Metadata } from 'next';
|
|||||||
import { Geist, Geist_Mono } from 'next/font/google';
|
import { Geist, Geist_Mono } from 'next/font/google';
|
||||||
import '@/styles/globals.css';
|
import '@/styles/globals.css';
|
||||||
import { ConvexAuthNextjsServerProvider } from '@convex-dev/auth/nextjs/server';
|
import { ConvexAuthNextjsServerProvider } from '@convex-dev/auth/nextjs/server';
|
||||||
import { ConvexClientProvider, ThemeProvider } from '@/components/providers';
|
import {
|
||||||
|
ConvexClientProvider,
|
||||||
|
ThemeProvider,
|
||||||
|
TVModeProvider,
|
||||||
|
} from '@/components/providers';
|
||||||
import PlausibleProvider from 'next-plausible';
|
import PlausibleProvider from 'next-plausible';
|
||||||
import { generateMetadata } from '@/lib/metadata';
|
import { generateMetadata } from '@/lib/metadata';
|
||||||
|
import { Toaster } from '@/components/ui';
|
||||||
|
import Header from '@/components/layout/header';
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: '--font-geist-sans',
|
variable: '--font-geist-sans',
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
const geistMono = Geist_Mono({
|
||||||
variable: '--font-geist-mono',
|
variable: '--font-geist-mono',
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = generateMetadata();
|
export const metadata: Metadata = generateMetadata();
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@@ -39,9 +43,14 @@ export default function RootLayout({
|
|||||||
enableSystem
|
enableSystem
|
||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
<ConvexClientProvider>{children}</ConvexClientProvider>
|
<ConvexClientProvider>
|
||||||
|
<TVModeProvider>
|
||||||
|
<Header />
|
||||||
|
{children}
|
||||||
|
<Toaster />
|
||||||
|
</TVModeProvider>
|
||||||
|
</ConvexClientProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
</PlausibleProvider>
|
</PlausibleProvider>
|
||||||
|
154
src/app/page.tsx
154
src/app/page.tsx
@@ -6,154 +6,10 @@ import Link from 'next/link';
|
|||||||
import { useAuthActions } from '@convex-dev/auth/react';
|
import { useAuthActions } from '@convex-dev/auth/react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
export default function Home() {
|
const Home = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<main>
|
||||||
<header className='sticky top-0 z-10 bg-background p-4 border-b-2 border-slate-200 dark:border-slate-800 flex flex-row justify-between items-center'>
|
</main>
|
||||||
Convex + Next.js + Convex Auth
|
|
||||||
<SignOutButton />
|
|
||||||
</header>
|
|
||||||
<main className='p-8 flex flex-col gap-8'>
|
|
||||||
<h1 className='text-4xl font-bold text-center'>
|
|
||||||
Convex + Next.js + Convex Auth
|
|
||||||
</h1>
|
|
||||||
<Content />
|
|
||||||
</main>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
export default Home;
|
||||||
function SignOutButton() {
|
|
||||||
const { isAuthenticated } = useConvexAuth();
|
|
||||||
const { signOut } = useAuthActions();
|
|
||||||
const router = useRouter();
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{isAuthenticated && (
|
|
||||||
<button
|
|
||||||
className='bg-slate-200 dark:bg-slate-800 text-foreground rounded-md px-2 py-1'
|
|
||||||
onClick={() =>
|
|
||||||
void signOut().then(() => {
|
|
||||||
router.push('/signin');
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Sign out
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Content() {
|
|
||||||
const { viewer, numbers } =
|
|
||||||
useQuery(api.myFunctions.listNumbers, {
|
|
||||||
count: 10,
|
|
||||||
}) ?? {};
|
|
||||||
const addNumber = useMutation(api.myFunctions.addNumber);
|
|
||||||
|
|
||||||
if (viewer === undefined || numbers === undefined) {
|
|
||||||
return (
|
|
||||||
<div className='mx-auto'>
|
|
||||||
<p>loading... (consider a loading skeleton)</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='flex flex-col gap-8 max-w-lg mx-auto'>
|
|
||||||
<p>Welcome {viewer ?? 'Anonymous'}!</p>
|
|
||||||
<p>
|
|
||||||
Click the button below and open this page in another window - this data
|
|
||||||
is persisted in the Convex cloud database!
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<button
|
|
||||||
className='bg-foreground text-background text-sm px-4 py-2 rounded-md'
|
|
||||||
onClick={() => {
|
|
||||||
void addNumber({ value: Math.floor(Math.random() * 10) });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Add a random number
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Numbers:{' '}
|
|
||||||
{numbers?.length === 0
|
|
||||||
? 'Click the button!'
|
|
||||||
: (numbers?.join(', ') ?? '...')}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Edit{' '}
|
|
||||||
<code className='text-sm font-bold font-mono bg-slate-200 dark:bg-slate-800 px-1 py-0.5 rounded-md'>
|
|
||||||
convex/myFunctions.ts
|
|
||||||
</code>{' '}
|
|
||||||
to change your backend
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Edit{' '}
|
|
||||||
<code className='text-sm font-bold font-mono bg-slate-200 dark:bg-slate-800 px-1 py-0.5 rounded-md'>
|
|
||||||
app/page.tsx
|
|
||||||
</code>{' '}
|
|
||||||
to change your frontend
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
See the{' '}
|
|
||||||
<Link href='/server' className='underline hover:no-underline'>
|
|
||||||
/server route
|
|
||||||
</Link>{' '}
|
|
||||||
for an example of loading data in a server component
|
|
||||||
</p>
|
|
||||||
<div className='flex flex-col'>
|
|
||||||
<p className='text-lg font-bold'>Useful resources:</p>
|
|
||||||
<div className='flex gap-2'>
|
|
||||||
<div className='flex flex-col gap-2 w-1/2'>
|
|
||||||
<ResourceCard
|
|
||||||
title='Convex docs'
|
|
||||||
description='Read comprehensive documentation for all Convex features.'
|
|
||||||
href='https://docs.convex.dev/home'
|
|
||||||
/>
|
|
||||||
<ResourceCard
|
|
||||||
title='Stack articles'
|
|
||||||
description='Learn about best practices, use cases, and more from a growing
|
|
||||||
collection of articles, videos, and walkthroughs.'
|
|
||||||
href='https://www.typescriptlang.org/docs/handbook/2/basic-types.html'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='flex flex-col gap-2 w-1/2'>
|
|
||||||
<ResourceCard
|
|
||||||
title='Templates'
|
|
||||||
description='Browse our collection of templates to get started quickly.'
|
|
||||||
href='https://www.convex.dev/templates'
|
|
||||||
/>
|
|
||||||
<ResourceCard
|
|
||||||
title='Discord'
|
|
||||||
description='Join our developer community to ask questions, trade tips & tricks,
|
|
||||||
and show off your projects.'
|
|
||||||
href='https://www.convex.dev/community'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ResourceCard({
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
href,
|
|
||||||
}: {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
href: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className='flex flex-col gap-2 bg-slate-200 dark:bg-slate-800 p-4 rounded-md h-28 overflow-auto'>
|
|
||||||
<a href={href} className='text-sm underline hover:no-underline'>
|
|
||||||
{title}
|
|
||||||
</a>
|
|
||||||
<p className='text-xs'>{description}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
@@ -1,31 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { type Preloaded, useMutation, usePreloadedQuery } from 'convex/react';
|
|
||||||
import { api } from '~/convex/_generated/api';
|
|
||||||
|
|
||||||
export default function Home({
|
|
||||||
preloaded,
|
|
||||||
}: {
|
|
||||||
preloaded: Preloaded<typeof api.myFunctions.listNumbers>;
|
|
||||||
}) {
|
|
||||||
const data = usePreloadedQuery(preloaded);
|
|
||||||
const addNumber = useMutation(api.myFunctions.addNumber);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className='flex flex-col gap-4 bg-slate-200 dark:bg-slate-800 p-4 rounded-md'>
|
|
||||||
<h2 className='text-xl font-bold'>Reactive client-loaded data</h2>
|
|
||||||
<code>
|
|
||||||
<pre>{JSON.stringify(data, null, 2)}</pre>
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className='bg-foreground text-background px-4 py-2 rounded-md mx-auto'
|
|
||||||
onClick={() => {
|
|
||||||
void addNumber({ value: Math.floor(Math.random() * 10) });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Add a random number
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -1,24 +0,0 @@
|
|||||||
import Home from './inner';
|
|
||||||
import { preloadQuery, preloadedQueryResult } from 'convex/nextjs';
|
|
||||||
import { api } from '~/convex/_generated/api';
|
|
||||||
|
|
||||||
export default async function ServerPage() {
|
|
||||||
const preloaded = await preloadQuery(api.myFunctions.listNumbers, {
|
|
||||||
count: 3,
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = preloadedQueryResult(preloaded);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className='p-8 flex flex-col gap-4 mx-auto max-w-2xl'>
|
|
||||||
<h1 className='text-4xl font-bold text-center'>Convex + Next.js</h1>
|
|
||||||
<div className='flex flex-col gap-4 bg-slate-200 dark:bg-slate-800 p-4 rounded-md'>
|
|
||||||
<h2 className='text-xl font-bold'>Non-reactive server-loaded data</h2>
|
|
||||||
<code>
|
|
||||||
<pre>{JSON.stringify(data, null, 2)}</pre>
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
<Home preloaded={preloaded} />
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
72
src/components/layout/header/controls/AvatarDropdown.tsx
Normal file
72
src/components/layout/header/controls/AvatarDropdown.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import {
|
||||||
|
BasedAvatar,
|
||||||
|
Button,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui';
|
||||||
|
import { useConvexAuth, useQuery } from 'convex/react';
|
||||||
|
import { useAuthActions } from '@convex-dev/auth/react';
|
||||||
|
import { api } from '~/convex/_generated/api';
|
||||||
|
|
||||||
|
export const AvatarDropdown = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { isLoading, isAuthenticated } = useConvexAuth();
|
||||||
|
const { signOut} = useAuthActions();
|
||||||
|
const user = useQuery(api.auth.getUser);
|
||||||
|
|
||||||
|
if (isLoading) return <BasedAvatar className='animate-pulse' />;
|
||||||
|
if (!isAuthenticated) return <div/>;
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
<BasedAvatar
|
||||||
|
src={user?.image}
|
||||||
|
fullName={user?.name}
|
||||||
|
className='lg:h-10 lg:w-10 my-auto'
|
||||||
|
fallbackProps={{ className:'text-xl font-semibold' }}
|
||||||
|
userIconProps={{ size: 32 }}
|
||||||
|
/>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
{(user?.name ?? user?.email) && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuLabel className='font-bold text-center'>
|
||||||
|
{user.name?.trim() ?? user.email?.trim()}
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Link
|
||||||
|
href='/profile'
|
||||||
|
className='w-full justify-center cursor-pointer'
|
||||||
|
>
|
||||||
|
Edit Profile
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator className='h-[2px]' />
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Button
|
||||||
|
onClick={() =>
|
||||||
|
void signOut().then(() => {
|
||||||
|
router.push('/signin');
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className='w-full justify-center cursor-pointer'
|
||||||
|
variant='ghost'
|
||||||
|
>
|
||||||
|
Sign Out
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
};
|
23
src/components/layout/header/controls/index.tsx
Normal file
23
src/components/layout/header/controls/index.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
'use client';
|
||||||
|
import {
|
||||||
|
ThemeToggle,
|
||||||
|
type ThemeToggleProps,
|
||||||
|
} from '@/components/providers';
|
||||||
|
import { AvatarDropdown } from './AvatarDropdown';
|
||||||
|
|
||||||
|
export const Controls = (themeToggleProps?: ThemeToggleProps) => {
|
||||||
|
return (
|
||||||
|
<div className='flex flex-row items-center'>
|
||||||
|
<ThemeToggle
|
||||||
|
size={1.2}
|
||||||
|
buttonProps={{
|
||||||
|
variant: 'secondary',
|
||||||
|
size: 'sm',
|
||||||
|
className: 'mr-4 py-5',
|
||||||
|
...themeToggleProps?.buttonProps,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AvatarDropdown />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
69
src/components/layout/header/index.tsx
Normal file
69
src/components/layout/header/index.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
'use client';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import {
|
||||||
|
useTVMode,
|
||||||
|
} from '@/components/providers';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { type ComponentProps } from 'react';
|
||||||
|
import { Controls } from './controls';
|
||||||
|
|
||||||
|
const Header = (headerProps: ComponentProps<'header'>) => {
|
||||||
|
const { tvMode } = useTVMode();
|
||||||
|
|
||||||
|
if (tvMode) {
|
||||||
|
return (
|
||||||
|
<div className='absolute top-10 right-37'>
|
||||||
|
<Controls />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header
|
||||||
|
{...headerProps}
|
||||||
|
className={cn(
|
||||||
|
'w-full min-h-[10vh] px-4 md:px-6 lg:px-20 my-8',
|
||||||
|
headerProps?.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
{/* Left spacer for perfect centering */}
|
||||||
|
<div className='flex flex-1 justify-start'>
|
||||||
|
<div className='sm:w-[120px] md:w-[160px]' />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Centered logo and title */}
|
||||||
|
<div className='flex-shrink-0'>
|
||||||
|
<Link
|
||||||
|
href='/'
|
||||||
|
scroll={false}
|
||||||
|
className='flex flex-row items-center justify-center px-4'
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src='/favicon.png'
|
||||||
|
alt='Tech Tracker Logo'
|
||||||
|
width={100}
|
||||||
|
height={100}
|
||||||
|
className='max-w-[40px] md:max-w-[120px]'
|
||||||
|
/>
|
||||||
|
<h1
|
||||||
|
className='title-text text-sm md:text-4xl lg:text-8xl
|
||||||
|
bg-gradient-to-r from-[#281A65] via-[#363354] to-accent-foreground
|
||||||
|
dark:from-[#bec8e6] dark:via-[#F0EEE4] dark:to-[#FFF8E7]
|
||||||
|
font-bold pl-2 md:pl-12 text-transparent bg-clip-text'
|
||||||
|
>
|
||||||
|
Tech Tracker
|
||||||
|
</h1>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right-aligned controls */}
|
||||||
|
<div className='flex-1 flex justify-end'>
|
||||||
|
<Controls />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Header;
|
166
src/components/providers/TVModeProvider.tsx
Normal file
166
src/components/providers/TVModeProvider.tsx
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
'use client';
|
||||||
|
import React, { createContext, useContext, useState } from 'react';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { Button } from '@/components/ui';
|
||||||
|
import { type ComponentProps } from 'react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
type TVModeContextProps = {
|
||||||
|
tvMode: boolean;
|
||||||
|
toggleTVMode: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TVToggleProps = {
|
||||||
|
buttonClassName?: ComponentProps<typeof Button>['className'];
|
||||||
|
buttonProps?: Omit<ComponentProps<typeof Button>, 'className'>;
|
||||||
|
size?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TVModeContext = createContext<TVModeContextProps | undefined>(undefined);
|
||||||
|
|
||||||
|
const TVModeProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const [tvMode, setTVMode] = useState(false);
|
||||||
|
const toggleTVMode = () => {
|
||||||
|
setTVMode((prev) => !prev);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<TVModeContext.Provider value={{ tvMode, toggleTVMode }}>
|
||||||
|
{children}
|
||||||
|
</TVModeContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useTVMode = () => {
|
||||||
|
const context = useContext(TVModeContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useTVMode must be used within a TVModeProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TV Icon Component with animations
|
||||||
|
const TVIcon = ({ tvMode, size = 25 }: { tvMode: boolean; size?: number }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="relative transition-all duration-300 ease-in-out"
|
||||||
|
style={{ width: size, height: size }}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
className="transition-all duration-300 ease-in-out"
|
||||||
|
>
|
||||||
|
{/* TV Screen */}
|
||||||
|
<rect
|
||||||
|
x="3"
|
||||||
|
y="6"
|
||||||
|
width="18"
|
||||||
|
height="12"
|
||||||
|
rx="2"
|
||||||
|
className={cn(
|
||||||
|
"stroke-current stroke-2 fill-none transition-all duration-300",
|
||||||
|
tvMode
|
||||||
|
? "stroke-blue-500 animate-pulse"
|
||||||
|
: "stroke-current"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* TV Stand */}
|
||||||
|
<path
|
||||||
|
d="M8 18h8M12 18v2"
|
||||||
|
className="stroke-current stroke-2 transition-all duration-300"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Corner arrows - animate based on mode */}
|
||||||
|
<g className={cn(
|
||||||
|
"transition-all duration-300 ease-in-out origin-center",
|
||||||
|
tvMode ? "scale-75 opacity-100" : "scale-100 opacity-70"
|
||||||
|
)}>
|
||||||
|
{tvMode ? (
|
||||||
|
// Exit fullscreen arrows (pointing inward)
|
||||||
|
<>
|
||||||
|
<path
|
||||||
|
d="M6 8l2 2M6 8h2M6 8v2"
|
||||||
|
className="stroke-current stroke-1.5 transition-all duration-300"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M18 8l-2 2M18 8h-2M18 8v2"
|
||||||
|
className="stroke-current stroke-1.5 transition-all duration-300"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M6 16l2-2M6 16h2M6 16v-2"
|
||||||
|
className="stroke-current stroke-1.5 transition-all duration-300"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M18 16l-2-2M18 16h-2M18 16v-2"
|
||||||
|
className="stroke-current stroke-1.5 transition-all duration-300"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
// Enter fullscreen arrows (pointing outward)
|
||||||
|
<>
|
||||||
|
<path
|
||||||
|
d="M8 6l-2 2M8 6v2M8 6h-2"
|
||||||
|
className="stroke-current stroke-1.5 transition-all duration-300"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M16 6l2 2M16 6v2M16 6h2"
|
||||||
|
className="stroke-current stroke-1.5 transition-all duration-300"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M8 18l-2-2M8 18v-2M8 18h-2"
|
||||||
|
className="stroke-current stroke-1.5 transition-all duration-300"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M16 18l2-2M16 18v-2M16 18h2"
|
||||||
|
className="stroke-current stroke-1.5 transition-all duration-300"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* Optional: Screen content indicator */}
|
||||||
|
<circle
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="1"
|
||||||
|
className={cn(
|
||||||
|
"transition-all duration-300",
|
||||||
|
tvMode
|
||||||
|
? "fill-blue-400 animate-ping"
|
||||||
|
: "fill-current opacity-30"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TVToggle = ({
|
||||||
|
buttonClassName,
|
||||||
|
buttonProps = {
|
||||||
|
variant: 'outline',
|
||||||
|
size: 'default',
|
||||||
|
},
|
||||||
|
size = 25,
|
||||||
|
}: TVToggleProps) => {
|
||||||
|
const { tvMode, toggleTVMode } = useTVMode();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
onClick={toggleTVMode}
|
||||||
|
className={cn(
|
||||||
|
'my-auto cursor-pointer transition-all duration-200 hover:scale-105 active:scale-95',
|
||||||
|
buttonClassName
|
||||||
|
)}
|
||||||
|
aria-label={tvMode ? 'Exit TV Mode' : 'Enter TV Mode'}
|
||||||
|
{...buttonProps}
|
||||||
|
>
|
||||||
|
<TVIcon tvMode={tvMode} size={size} />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { TVModeProvider, useTVMode, TVToggle };
|
@@ -1,2 +1,3 @@
|
|||||||
export { ConvexClientProvider } from './ConvexClientProvider';
|
export { ConvexClientProvider } from './ConvexClientProvider';
|
||||||
export { ThemeProvider, ThemeToggle, type ThemeToggleProps } from './ThemeProvider';
|
export { ThemeProvider, ThemeToggle, type ThemeToggleProps } from './ThemeProvider';
|
||||||
|
export { TVModeProvider, useTVMode, TVToggle } from './TVModeProvider';
|
||||||
|
53
src/components/ui/avatar.tsx
Normal file
53
src/components/ui/avatar.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
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 }
|
69
src/components/ui/based-avatar.tsx
Normal file
69
src/components/ui/based-avatar.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
'use client';
|
||||||
|
import * as AvatarPrimitive from '@radix-ui/react-avatar';
|
||||||
|
import { User } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { AvatarImage } from '@/components/ui/avatar';
|
||||||
|
import { type ComponentProps } from 'react';
|
||||||
|
|
||||||
|
type BasedAvatarProps = ComponentProps<typeof AvatarPrimitive.Root> & {
|
||||||
|
src?: string | null;
|
||||||
|
fullName?: string | null;
|
||||||
|
imageProps?: Omit<ComponentProps<typeof AvatarImage>, 'data-slot'>;
|
||||||
|
fallbackProps?: ComponentProps<typeof AvatarPrimitive.Fallback>;
|
||||||
|
userIconProps?: ComponentProps<typeof User>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BasedAvatar = ({
|
||||||
|
src = null,
|
||||||
|
fullName = null,
|
||||||
|
imageProps,
|
||||||
|
fallbackProps,
|
||||||
|
userIconProps = {
|
||||||
|
size: 32,
|
||||||
|
},
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: BasedAvatarProps) => {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Root
|
||||||
|
data-slot='avatar'
|
||||||
|
className={cn(
|
||||||
|
'cursor-pointer relative flex size-8 shrink-0 overflow-hidden rounded-full',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{src ? (
|
||||||
|
<AvatarImage
|
||||||
|
{...imageProps}
|
||||||
|
src={src}
|
||||||
|
className={imageProps?.className}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<AvatarPrimitive.Fallback
|
||||||
|
{...fallbackProps}
|
||||||
|
data-slot='avatar-fallback'
|
||||||
|
className={cn(
|
||||||
|
'bg-muted flex size-full items-center justify-center rounded-full',
|
||||||
|
fallbackProps?.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{fullName ? (
|
||||||
|
fullName
|
||||||
|
.split(' ')
|
||||||
|
.map((n) => n[0])
|
||||||
|
.join('')
|
||||||
|
.toUpperCase()
|
||||||
|
) : (
|
||||||
|
<User
|
||||||
|
{...userIconProps}
|
||||||
|
className={cn('', userIconProps?.className)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AvatarPrimitive.Fallback>
|
||||||
|
)}
|
||||||
|
</AvatarPrimitive.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { BasedAvatar };
|
257
src/components/ui/dropdown-menu.tsx
Normal file
257
src/components/ui/dropdown-menu.tsx
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
@@ -1,3 +1,5 @@
|
|||||||
|
export { Avatar, AvatarImage, AvatarFallback } from './avatar';
|
||||||
|
export { BasedAvatar } from './based-avatar';
|
||||||
export { Button, buttonVariants } from './button';
|
export { Button, buttonVariants } from './button';
|
||||||
export {
|
export {
|
||||||
Card,
|
Card,
|
||||||
@@ -8,6 +10,14 @@ export {
|
|||||||
CardDescription,
|
CardDescription,
|
||||||
CardContent,
|
CardContent,
|
||||||
} from './card';
|
} from './card';
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger
|
||||||
|
} from './dropdown-menu';
|
||||||
export {
|
export {
|
||||||
useFormField,
|
useFormField,
|
||||||
Form,
|
Form,
|
||||||
@@ -29,3 +39,4 @@ export {
|
|||||||
TabsTrigger,
|
TabsTrigger,
|
||||||
TabsContent
|
TabsContent
|
||||||
} from './tabs';
|
} from './tabs';
|
||||||
|
export { Toaster } from './sonner';
|
||||||
|
25
src/components/ui/sonner.tsx
Normal file
25
src/components/ui/sonner.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
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 }
|
Reference in New Issue
Block a user