More changes that I would want my example to have I think
23
bun.lock
@@ -5,15 +5,21 @@
|
|||||||
"name": "example",
|
"name": "example",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@convex-dev/auth": "^0.0.81",
|
"@convex-dev/auth": "^0.0.81",
|
||||||
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@sentry/nextjs": "^10.7.0",
|
"@sentry/nextjs": "^10.7.0",
|
||||||
"@t3-oss/env-nextjs": "^0.13.8",
|
"@t3-oss/env-nextjs": "^0.13.8",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"convex": "^1.26.0",
|
"convex": "^1.26.0",
|
||||||
"eslint-plugin-prettier": "^5.5.4",
|
"eslint-plugin-prettier": "^5.5.4",
|
||||||
|
"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",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"require-in-the-middle": "^7.5.2",
|
"require-in-the-middle": "^7.5.2",
|
||||||
|
"tailwind-merge": "^3.3.1",
|
||||||
"typescript-eslint": "^8.41.0",
|
"typescript-eslint": "^8.41.0",
|
||||||
"zod": "^4.1.5",
|
"zod": "^4.1.5",
|
||||||
},
|
},
|
||||||
@@ -29,6 +35,7 @@
|
|||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
|
"tw-animate-css": "^1.3.7",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -512,6 +519,10 @@
|
|||||||
|
|
||||||
"@prisma/instrumentation": ["@prisma/instrumentation@6.14.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0" }, "peerDependencies": { "@opentelemetry/api": "^1.8" } }, "sha512-Po/Hry5bAeunRDq0yAQueKookW3glpP+qjjvvyOfm6dI2KG5/Y6Bgg3ahyWd7B0u2E+Wf9xRk2rtdda7ySgK1A=="],
|
"@prisma/instrumentation": ["@prisma/instrumentation@6.14.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0" }, "peerDependencies": { "@opentelemetry/api": "^1.8" } }, "sha512-Po/Hry5bAeunRDq0yAQueKookW3glpP+qjjvvyOfm6dI2KG5/Y6Bgg3ahyWd7B0u2E+Wf9xRk2rtdda7ySgK1A=="],
|
||||||
|
|
||||||
|
"@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-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
"@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=="],
|
||||||
@@ -858,10 +869,14 @@
|
|||||||
|
|
||||||
"cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="],
|
"cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="],
|
||||||
|
|
||||||
|
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
|
||||||
|
|
||||||
"clear-module": ["clear-module@4.1.2", "", { "dependencies": { "parent-module": "^2.0.0", "resolve-from": "^5.0.0" } }, "sha512-LWAxzHqdHsAZlPlEyJ2Poz6AIs384mPeqLVCru2p0BrP9G/kVGuhNyZYClLO6cXlnuJjzC8xtsJIuMjKqLXoAw=="],
|
"clear-module": ["clear-module@4.1.2", "", { "dependencies": { "parent-module": "^2.0.0", "resolve-from": "^5.0.0" } }, "sha512-LWAxzHqdHsAZlPlEyJ2Poz6AIs384mPeqLVCru2p0BrP9G/kVGuhNyZYClLO6cXlnuJjzC8xtsJIuMjKqLXoAw=="],
|
||||||
|
|
||||||
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
||||||
|
|
||||||
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|
||||||
"color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
|
"color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
|
||||||
|
|
||||||
"color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
|
"color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
|
||||||
@@ -1254,6 +1269,8 @@
|
|||||||
|
|
||||||
"lucia": ["lucia@3.2.2", "", { "dependencies": { "@oslojs/crypto": "^1.0.1", "@oslojs/encoding": "^1.1.0" } }, "sha512-P1FlFBGCMPMXu+EGdVD9W4Mjm0DqsusmKgO7Xc33mI5X1bklmsQb0hfzPhXomQr9waWIBDsiOjvr1e6BTaUqpA=="],
|
"lucia": ["lucia@3.2.2", "", { "dependencies": { "@oslojs/crypto": "^1.0.1", "@oslojs/encoding": "^1.1.0" } }, "sha512-P1FlFBGCMPMXu+EGdVD9W4Mjm0DqsusmKgO7Xc33mI5X1bklmsQb0hfzPhXomQr9waWIBDsiOjvr1e6BTaUqpA=="],
|
||||||
|
|
||||||
|
"lucide-react": ["lucide-react@0.542.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw=="],
|
||||||
|
|
||||||
"magic-string": ["magic-string@0.30.18", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ=="],
|
"magic-string": ["magic-string@0.30.18", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ=="],
|
||||||
|
|
||||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||||
@@ -1300,6 +1317,8 @@
|
|||||||
|
|
||||||
"next-plausible": ["next-plausible@3.12.4", "", { "peerDependencies": { "next": "^11.1.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 ", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-cD3+ixJxf8yBYvsideTxqli3fvrB7R4BXcvsNJz8Sm2X1QN039WfiXjCyNWkub4h5++rRs6fHhchUMnOuJokcg=="],
|
"next-plausible": ["next-plausible@3.12.4", "", { "peerDependencies": { "next": "^11.1.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 ", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-cD3+ixJxf8yBYvsideTxqli3fvrB7R4BXcvsNJz8Sm2X1QN039WfiXjCyNWkub4h5++rRs6fHhchUMnOuJokcg=="],
|
||||||
|
|
||||||
|
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
|
||||||
|
|
||||||
"nice-try": ["nice-try@1.0.5", "", {}, "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="],
|
"nice-try": ["nice-try@1.0.5", "", {}, "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="],
|
||||||
|
|
||||||
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||||
@@ -1528,6 +1547,8 @@
|
|||||||
|
|
||||||
"synckit": ["synckit@0.11.11", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw=="],
|
"synckit": ["synckit@0.11.11", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw=="],
|
||||||
|
|
||||||
|
"tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="],
|
||||||
|
|
||||||
"tailwindcss": ["tailwindcss@4.1.12", "", {}, "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA=="],
|
"tailwindcss": ["tailwindcss@4.1.12", "", {}, "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA=="],
|
||||||
|
|
||||||
"tapable": ["tapable@2.2.3", "", {}, "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg=="],
|
"tapable": ["tapable@2.2.3", "", {}, "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg=="],
|
||||||
@@ -1550,6 +1571,8 @@
|
|||||||
|
|
||||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"tw-animate-css": ["tw-animate-css@1.3.7", "", {}, "sha512-lvLb3hTIpB5oGsk8JmLoAjeCHV58nKa2zHYn8yWOoG5JJusH3bhJlF2DLAZ/5NmJ+jyH3ssiAx/2KmbhavJy/A=="],
|
||||||
|
|
||||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||||
|
|
||||||
"type-fest": ["type-fest@0.7.1", "", {}, "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg=="],
|
"type-fest": ["type-fest@0.7.1", "", {}, "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg=="],
|
||||||
|
21
components.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "src/app/styles/globals.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
}
|
||||||
|
}
|
@@ -17,15 +17,21 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@convex-dev/auth": "^0.0.81",
|
"@convex-dev/auth": "^0.0.81",
|
||||||
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@sentry/nextjs": "^10.7.0",
|
"@sentry/nextjs": "^10.7.0",
|
||||||
"@t3-oss/env-nextjs": "^0.13.8",
|
"@t3-oss/env-nextjs": "^0.13.8",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"convex": "^1.26.0",
|
"convex": "^1.26.0",
|
||||||
"eslint-plugin-prettier": "^5.5.4",
|
"eslint-plugin-prettier": "^5.5.4",
|
||||||
|
"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",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"require-in-the-middle": "^7.5.2",
|
"require-in-the-middle": "^7.5.2",
|
||||||
|
"tailwind-merge": "^3.3.1",
|
||||||
"typescript-eslint": "^8.41.0",
|
"typescript-eslint": "^8.41.0",
|
||||||
"zod": "^4.1.5"
|
"zod": "^4.1.5"
|
||||||
},
|
},
|
||||||
@@ -41,6 +47,7 @@
|
|||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
|
"tw-animate-css": "^1.3.7",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
BIN
public/appicon/icon-144.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
public/appicon/icon-36.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
public/appicon/icon-48.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
public/appicon/icon-72.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
public/appicon/icon-96.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
public/appicon/icon.png
Normal file
After Width: | Height: | Size: 14 KiB |
@@ -1,17 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 367 370" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<g transform="matrix(1,0,0,1,-129.225,-127.948)">
|
|
||||||
<g id="Layer-1" serif:id="Layer 1" transform="matrix(4.16667,0,0,4.16667,0,0)">
|
|
||||||
<g transform="matrix(1,0,0,1,86.6099,107.074)">
|
|
||||||
<path d="M0,-6.544C13.098,-7.973 25.449,-14.834 32.255,-26.287C29.037,2.033 -2.48,19.936 -28.196,8.94C-30.569,7.925 -32.605,6.254 -34.008,4.088C-39.789,-4.83 -41.69,-16.18 -38.963,-26.48C-31.158,-13.247 -15.3,-5.131 0,-6.544" style="fill:rgb(245,176,26);fill-rule:nonzero;"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(1,0,0,1,47.1708,74.7779)">
|
|
||||||
<path d="M0,-2.489C-5.312,9.568 -5.545,23.695 0.971,35.316C-21.946,18.37 -21.692,-17.876 0.689,-34.65C2.754,-36.197 5.219,-37.124 7.797,-37.257C18.41,-37.805 29.19,-33.775 36.747,-26.264C21.384,-26.121 6.427,-16.446 0,-2.489" style="fill:rgb(141,37,118);fill-rule:nonzero;"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(1,0,0,1,91.325,66.4152)">
|
|
||||||
<path d="M0,-14.199C-7.749,-24.821 -19.884,-32.044 -33.173,-32.264C-7.482,-43.726 24.112,-25.143 27.557,2.322C27.877,4.876 27.458,7.469 26.305,9.769C21.503,19.345 12.602,26.776 2.203,29.527C9.838,15.64 8.889,-1.328 0,-14.199" style="fill:rgb(238,52,47);fill-rule:nonzero;"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.7 KiB |
BIN
public/favicon-16.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
public/favicon-32.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
public/favicon.png
Normal file
After Width: | Height: | Size: 11 KiB |
@@ -1,26 +0,0 @@
|
|||||||
@import "tailwindcss";
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--background: #ffffff;
|
|
||||||
--foreground: #171717;
|
|
||||||
}
|
|
||||||
|
|
||||||
@theme inline {
|
|
||||||
--color-background: var(--background);
|
|
||||||
--color-foreground: var(--foreground);
|
|
||||||
--font-sans: var(--font-geist-sans);
|
|
||||||
--font-mono: var(--font-geist-mono);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--background: #0a0a0a;
|
|
||||||
--foreground: #ededed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
color: var(--foreground);
|
|
||||||
background: var(--background);
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
}
|
|
@@ -1,8 +1,11 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { Geist, Geist_Mono } from 'next/font/google';
|
import { Geist, Geist_Mono } from 'next/font/google';
|
||||||
import './globals.css';
|
import '@/styles/globals.css';
|
||||||
import { ConvexAuthNextjsServerProvider } from '@convex-dev/auth/nextjs/server';
|
import { ConvexAuthNextjsServerProvider } from '@convex-dev/auth/nextjs/server';
|
||||||
import ConvexClientProvider from '@/components/ConvexClientProvider';
|
import { ConvexClientProvider, ThemeProvider } from '@/components/providers';
|
||||||
|
import PlausibleProvider from 'next-plausible';
|
||||||
|
import { generateMetadata } from '@/lib/metadata';
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: '--font-geist-sans',
|
variable: '--font-geist-sans',
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
@@ -13,13 +16,7 @@ const geistMono = Geist_Mono({
|
|||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = generateMetadata();
|
||||||
title: 'Create Next App',
|
|
||||||
description: 'Generated by create next app',
|
|
||||||
icons: {
|
|
||||||
icon: '/convex.svg',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
@@ -28,13 +25,26 @@ export default function RootLayout({
|
|||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<ConvexAuthNextjsServerProvider>
|
<ConvexAuthNextjsServerProvider>
|
||||||
|
<PlausibleProvider
|
||||||
|
domain='techtracker.gbrown.org'
|
||||||
|
customDomain='https://plausible.gbrown.org'
|
||||||
|
>
|
||||||
<html lang='en'>
|
<html lang='en'>
|
||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
|
>
|
||||||
|
<ThemeProvider
|
||||||
|
attribute='class'
|
||||||
|
defaultTheme='system'
|
||||||
|
enableSystem
|
||||||
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
<ConvexClientProvider>{children}</ConvexClientProvider>
|
<ConvexClientProvider>{children}</ConvexClientProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
</PlausibleProvider>
|
||||||
</ConvexAuthNextjsServerProvider>
|
</ConvexAuthNextjsServerProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Preloaded, useMutation, usePreloadedQuery } from 'convex/react';
|
import { type Preloaded, useMutation, usePreloadedQuery } from 'convex/react';
|
||||||
import { api } from '~/convex/_generated/api';
|
import { api } from '~/convex/_generated/api';
|
||||||
|
|
||||||
export default function Home({
|
export default function Home({
|
||||||
|
@@ -2,15 +2,15 @@
|
|||||||
|
|
||||||
import { ConvexAuthNextjsProvider } from '@convex-dev/auth/nextjs';
|
import { ConvexAuthNextjsProvider } from '@convex-dev/auth/nextjs';
|
||||||
import { ConvexReactClient } from 'convex/react';
|
import { ConvexReactClient } from 'convex/react';
|
||||||
import { ReactNode } from 'react';
|
import { type ReactNode } from 'react';
|
||||||
|
|
||||||
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
|
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
|
||||||
|
|
||||||
export default function ConvexClientProvider({
|
export const ConvexClientProvider = ({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}) {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<ConvexAuthNextjsProvider client={convex}>
|
<ConvexAuthNextjsProvider client={convex}>
|
||||||
{children}
|
{children}
|
69
src/components/providers/ThemeProvider.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
'use client';
|
||||||
|
import { useEffect, useState, type ComponentProps } from 'react';
|
||||||
|
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
||||||
|
import { Moon, Sun } from 'lucide-react';
|
||||||
|
import { useTheme } from 'next-themes';
|
||||||
|
import { Button } from '@/components/ui';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const ThemeProvider = ({
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: ComponentProps<typeof NextThemesProvider>) => {
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!mounted) return null;
|
||||||
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ThemeToggleProps = {
|
||||||
|
size?: number;
|
||||||
|
buttonProps?: Omit<ComponentProps<typeof Button>, 'onClick'>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ThemeToggle = ({ size = 1, buttonProps }: ThemeToggleProps) => {
|
||||||
|
const { setTheme, resolvedTheme } = useTheme();
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return (
|
||||||
|
<Button {...buttonProps}>
|
||||||
|
<span style={{ height: `${size}rem`, width: `${size}rem` }} />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
if (resolvedTheme === 'dark') setTheme('light');
|
||||||
|
else setTheme('dark');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
size='icon'
|
||||||
|
{...buttonProps}
|
||||||
|
onClick={toggleTheme}
|
||||||
|
className={cn('cursor-pointer', buttonProps?.className)}
|
||||||
|
>
|
||||||
|
<Sun
|
||||||
|
style={{ height: `${size}rem`, width: `${size}rem` }}
|
||||||
|
className='rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0'
|
||||||
|
/>
|
||||||
|
<Moon
|
||||||
|
style={{ height: `${size}rem`, width: `${size}rem` }}
|
||||||
|
className='absolute rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100'
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ThemeProvider, ThemeToggle, type ThemeToggleProps };
|
2
src/components/providers/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { ConvexClientProvider } from './ConvexClientProvider';
|
||||||
|
export { ThemeProvider, ThemeToggle, type ThemeToggleProps } from './ThemeProvider';
|
59
src/components/ui/button.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
|
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||||
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
|
icon: "size-9",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> &
|
||||||
|
VariantProps<typeof buttonVariants> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="button"
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
1
src/components/ui/index.tsx
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { Button, buttonVariants } from './button';
|
369
src/lib/metadata.ts
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
import * as Sentry from '@sentry/nextjs';
|
||||||
|
|
||||||
|
export const generateMetadata = (): Metadata => {
|
||||||
|
return {
|
||||||
|
title: {
|
||||||
|
template: '%s | Tech Tracker',
|
||||||
|
default: 'Tech Tracker',
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
'App used by COG IT employees to \
|
||||||
|
update their status throughout the day.',
|
||||||
|
applicationName: 'Tech Tracker',
|
||||||
|
keywords:
|
||||||
|
'Tech Tracker, City of Gulfport, Information Technology, T3 Template, ' +
|
||||||
|
'Next.js, Supabase, Tailwind, TypeScript, React, T3, Gib',
|
||||||
|
authors: [{ name: 'Gib', url: 'https://gbrown.org' }],
|
||||||
|
creator: 'Gib Brown',
|
||||||
|
publisher: 'Gib Brown',
|
||||||
|
formatDetection: {
|
||||||
|
email: false,
|
||||||
|
address: false,
|
||||||
|
telephone: false,
|
||||||
|
},
|
||||||
|
robots: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
nocache: false,
|
||||||
|
googleBot: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
noimageindex: false,
|
||||||
|
'max-video-preview': -1,
|
||||||
|
'max-image-preview': 'large',
|
||||||
|
'max-snippet': -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
icons: {
|
||||||
|
icon: [
|
||||||
|
{ url: '/favicon.ico', type: 'image/x-icon', sizes: 'any' },
|
||||||
|
{
|
||||||
|
url: '/favicon-16.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '16x16',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/favicon-32.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '32x32',
|
||||||
|
},
|
||||||
|
{ url: '/favicon.png', type: 'image/png', sizes: '96x96' },
|
||||||
|
{
|
||||||
|
url: '/favicon.ico',
|
||||||
|
type: 'image/x-icon',
|
||||||
|
sizes: 'any',
|
||||||
|
media: '(prefers-color-scheme: dark)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/favicon-16.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '16x16',
|
||||||
|
media: '(prefers-color-scheme: dark)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/favicon-32.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '32x32',
|
||||||
|
media: '(prefers-color-scheme: dark)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/favicon.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '96x96',
|
||||||
|
media: '(prefers-color-scheme: dark)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/appicon/icon-36.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '36x36',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/appicon/icon-48.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '48x48',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/appicon/icon-72.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '72x72',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/appicon/icon-96.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '96x96',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/appicon/icon-144.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '144x144',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/appicon/icon.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '192x192',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/appicon/icon-36.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '36x36',
|
||||||
|
media: '(prefers-color-scheme: dark)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/appicon/icon-48.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '48x48',
|
||||||
|
media: '(prefers-color-scheme: dark)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/appicon/icon-72.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '72x72',
|
||||||
|
media: '(prefers-color-scheme: dark)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/appicon/icon-96.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '96x96',
|
||||||
|
media: '(prefers-color-scheme: dark)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/appicon/icon-144.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '144x144',
|
||||||
|
media: '(prefers-color-scheme: dark)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/appicon/icon.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '192x192',
|
||||||
|
media: '(prefers-color-scheme: dark)',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
shortcut: [
|
||||||
|
{
|
||||||
|
url: '/appicon/icon-36.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '36x36',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/appicon/icon-48.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '48x48',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/appicon/icon-72.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '72x72',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/appicon/icon-96.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '96x96',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/appicon/icon-144.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '144x144',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/appicon/icon.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '192x192',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/appicon/icon-36.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '36x36',
|
||||||
|
media: '(prefers-color-scheme: dark)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/appicon/icon-48.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '48x48',
|
||||||
|
media: '(prefers-color-scheme: dark)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/appicon/icon-72.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '72x72',
|
||||||
|
media: '(prefers-color-scheme: dark)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/appicon/icon-96.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '96x96',
|
||||||
|
media: '(prefers-color-scheme: dark)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/appicon/icon-144.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '144x144',
|
||||||
|
media: '(prefers-color-scheme: dark)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: '/appicon/icon.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '192x192',
|
||||||
|
media: '(prefers-color-scheme: dark)',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
apple: [
|
||||||
|
{
|
||||||
|
url: 'appicon/icon-57.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '57x57',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'appicon/icon-60.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '60x60',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'appicon/icon-72.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '72x72',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'appicon/icon-76.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '76x76',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'appicon/icon-114.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '114x114',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'appicon/icon-120.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '120x120',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'appicon/icon-144.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '144x144',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'appicon/icon-152.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '152x152',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'appicon/icon-180.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '180x180',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'appicon/icon.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '192x192',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'appicon/icon-57.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '57x57',
|
||||||
|
media: '(prefers-color-scheme: dark)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'appicon/icon-60.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '60x60',
|
||||||
|
media: '(prefers-color-scheme: dark)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'appicon/icon-72.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '72x72',
|
||||||
|
media: '(prefers-color-scheme: dark)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'appicon/icon-76.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '76x76',
|
||||||
|
media: '(prefers-color-scheme: dark)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'appicon/icon-114.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '114x114',
|
||||||
|
media: '(prefers-color-scheme: dark)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'appicon/icon-120.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '120x120',
|
||||||
|
media: '(prefers-color-scheme: dark)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'appicon/icon-144.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '144x144',
|
||||||
|
media: '(prefers-color-scheme: dark)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'appicon/icon-152.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '152x152',
|
||||||
|
media: '(prefers-color-scheme: dark)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'appicon/icon-180.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '180x180',
|
||||||
|
media: '(prefers-color-scheme: dark)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'appicon/icon.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '192x192',
|
||||||
|
media: '(prefers-color-scheme: dark)',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
other: [
|
||||||
|
{
|
||||||
|
rel: 'apple-touch-icon-precomposed',
|
||||||
|
url: '/appicon/icon-precomposed.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '180x180',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
other: {
|
||||||
|
...Sentry.getTraceData(),
|
||||||
|
},
|
||||||
|
appleWebApp: {
|
||||||
|
title: 'Tech Tracker',
|
||||||
|
statusBarStyle: 'black-translucent',
|
||||||
|
startupImage: [
|
||||||
|
'/icons/apple/splash-768x1004.png',
|
||||||
|
{
|
||||||
|
url: '/icons/apple/splash-1536x2008.png',
|
||||||
|
media: '(device-width: 768px) and (device-height: 1024px)',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
verification: {
|
||||||
|
google: 'google',
|
||||||
|
yandex: 'yandex',
|
||||||
|
yahoo: 'yahoo',
|
||||||
|
},
|
||||||
|
category: 'technology',
|
||||||
|
/*
|
||||||
|
appLinks: {
|
||||||
|
ios: {
|
||||||
|
url: 'https://techtracker.gbrown.org/ios',
|
||||||
|
app_store_id: 'com.gbrown.techtracker',
|
||||||
|
},
|
||||||
|
android: {
|
||||||
|
package: 'https://techtracker.gbrown.org/android',
|
||||||
|
app_name: 'app_t3_template',
|
||||||
|
},
|
||||||
|
web: {
|
||||||
|
url: 'https://techtracker.gbrown.org',
|
||||||
|
should_fallback: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
};
|
||||||
|
};
|
201
src/lib/middleware/ban-suspicious-ips.ts
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
import { type NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
// In-memory stores for tracking IPs (use Redis in production)
|
||||||
|
const ipAttempts = new Map<string, { count: number; lastAttempt: number }>();
|
||||||
|
const ip404Attempts = new Map<string, { count: number; lastAttempt: number }>();
|
||||||
|
const bannedIPs = new Set<string>();
|
||||||
|
|
||||||
|
// Ban Arctic Wolf Explicitly
|
||||||
|
bannedIPs.add('::ffff:10.0.1.49');
|
||||||
|
|
||||||
|
// Suspicious patterns that indicate malicious activity
|
||||||
|
const MALICIOUS_PATTERNS = [
|
||||||
|
// Your existing patterns
|
||||||
|
/web-inf/i,
|
||||||
|
/\.jsp/i,
|
||||||
|
/\.php/i,
|
||||||
|
/puttest/i,
|
||||||
|
/WEB-INF/i,
|
||||||
|
/\.xml$/i,
|
||||||
|
/perl/i,
|
||||||
|
/xampp/i,
|
||||||
|
/phpwebgallery/i,
|
||||||
|
/FileManager/i,
|
||||||
|
/standalonemanager/i,
|
||||||
|
/h2console/i,
|
||||||
|
/WebAdmin/i,
|
||||||
|
/login_form\.php/i,
|
||||||
|
/%2e/i,
|
||||||
|
/%u002e/i,
|
||||||
|
/\.%00/i,
|
||||||
|
/\.\./,
|
||||||
|
/lcgi/i,
|
||||||
|
|
||||||
|
// New patterns from your logs
|
||||||
|
/\/appliance\//i,
|
||||||
|
/bomgar/i,
|
||||||
|
/netburner-logo/i,
|
||||||
|
/\/ui\/images\//i,
|
||||||
|
/logon_merge/i,
|
||||||
|
/logon_t\.gif/i,
|
||||||
|
/login_top\.gif/i,
|
||||||
|
/theme1\/images/i,
|
||||||
|
/\.well-known\/acme-challenge\/.*\.jpg$/i,
|
||||||
|
/\.well-known\/pki-validation\/.*\.jpg$/i,
|
||||||
|
|
||||||
|
// Path traversal and system file access patterns
|
||||||
|
/\/etc\/passwd/i,
|
||||||
|
/\/etc%2fpasswd/i,
|
||||||
|
/\/etc%5cpasswd/i,
|
||||||
|
/\/\/+etc/i,
|
||||||
|
/\\\\+.*etc/i,
|
||||||
|
/%2f%2f/i,
|
||||||
|
/%5c%5c/i,
|
||||||
|
/\/\/+/,
|
||||||
|
/\\\\+/,
|
||||||
|
/%00/i,
|
||||||
|
/%23/i,
|
||||||
|
|
||||||
|
// Encoded path traversal attempts
|
||||||
|
/%2e%2e/i,
|
||||||
|
/%252e/i,
|
||||||
|
/%c0%ae/i,
|
||||||
|
/%c1%9c/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Suspicious HTTP methods
|
||||||
|
const SUSPICIOUS_METHODS = ['TRACE', 'PUT', 'DELETE', 'PATCH'];
|
||||||
|
|
||||||
|
const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute
|
||||||
|
const MAX_ATTEMPTS = 10; // Max suspicious requests per window
|
||||||
|
const BAN_DURATION = 30 * 60 * 1000; // 30 minutes
|
||||||
|
|
||||||
|
// 404 rate limiting settings
|
||||||
|
const RATE_404_WINDOW = 2 * 60 * 1000; // 2 minutes
|
||||||
|
const MAX_404_ATTEMPTS = 10; // Max 404s before ban
|
||||||
|
|
||||||
|
const getClientIP = (request: NextRequest): string => {
|
||||||
|
const forwarded = request.headers.get('x-forwarded-for');
|
||||||
|
const realIP = request.headers.get('x-real-ip');
|
||||||
|
const cfConnectingIP = request.headers.get('cf-connecting-ip');
|
||||||
|
|
||||||
|
if (forwarded) return (forwarded.split(',')[0] ?? '').trim();
|
||||||
|
if (realIP) return realIP;
|
||||||
|
if (cfConnectingIP) return cfConnectingIP;
|
||||||
|
return request.headers.get('host') ?? 'unknown';
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPathSuspicious = (pathname: string): boolean => {
|
||||||
|
return MALICIOUS_PATTERNS.some((pattern) => pattern.test(pathname));
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMethodSuspicious = (method: string): boolean => {
|
||||||
|
return SUSPICIOUS_METHODS.includes(method);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateIPAttempts = (ip: string): boolean => {
|
||||||
|
const now = Date.now();
|
||||||
|
const attempts = ipAttempts.get(ip);
|
||||||
|
|
||||||
|
if (!attempts || now - attempts.lastAttempt > RATE_LIMIT_WINDOW) {
|
||||||
|
ipAttempts.set(ip, { count: 1, lastAttempt: now });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
attempts.count++;
|
||||||
|
attempts.lastAttempt = now;
|
||||||
|
|
||||||
|
if (attempts.count > MAX_ATTEMPTS) {
|
||||||
|
bannedIPs.add(ip);
|
||||||
|
ipAttempts.delete(ip);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
bannedIPs.delete(ip);
|
||||||
|
}, BAN_DURATION);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const update404Attempts = (ip: string): boolean => {
|
||||||
|
const now = Date.now();
|
||||||
|
const attempts = ip404Attempts.get(ip);
|
||||||
|
|
||||||
|
if (!attempts || now - attempts.lastAttempt > RATE_404_WINDOW) {
|
||||||
|
ip404Attempts.set(ip, { count: 1, lastAttempt: now });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
attempts.count++;
|
||||||
|
attempts.lastAttempt = now;
|
||||||
|
|
||||||
|
if (attempts.count > MAX_404_ATTEMPTS) {
|
||||||
|
bannedIPs.add(ip);
|
||||||
|
ip404Attempts.delete(ip);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`🔨 IP ${ip} banned for excessive 404 requests (${attempts.count} in ${RATE_404_WINDOW / 1000}s)`,
|
||||||
|
);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
bannedIPs.delete(ip);
|
||||||
|
}, BAN_DURATION);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const banSuspiciousIPs = (request: NextRequest): NextResponse | null => {
|
||||||
|
const { pathname } = request.nextUrl;
|
||||||
|
const method = request.method;
|
||||||
|
const ip = getClientIP(request);
|
||||||
|
|
||||||
|
// Check if IP is already banned
|
||||||
|
if (bannedIPs.has(ip)) {
|
||||||
|
return new NextResponse('Access denied.', { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSuspiciousPath = isPathSuspicious(pathname);
|
||||||
|
const isSuspiciousMethod = isMethodSuspicious(method);
|
||||||
|
|
||||||
|
// Handle suspicious activity
|
||||||
|
if (isSuspiciousPath || isSuspiciousMethod) {
|
||||||
|
const shouldBan = updateIPAttempts(ip);
|
||||||
|
|
||||||
|
if (shouldBan) {
|
||||||
|
console.log(`🔨 IP ${ip} has been banned for suspicious activity`);
|
||||||
|
return new NextResponse('Access denied - IP banned. Please fuck off.', {
|
||||||
|
status: 403,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new NextResponse('Not Found', { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call this function when you detect a 404 response
|
||||||
|
export const handle404Response = (
|
||||||
|
request: NextRequest,
|
||||||
|
): NextResponse | null => {
|
||||||
|
const ip = getClientIP(request);
|
||||||
|
|
||||||
|
if (bannedIPs.has(ip)) {
|
||||||
|
return new NextResponse('Access denied.', { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldBan = update404Attempts(ip);
|
||||||
|
|
||||||
|
if (shouldBan) {
|
||||||
|
return new NextResponse('Access denied - IP banned for excessive 404s.', {
|
||||||
|
status: 403,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
20
src/lib/utils.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ccn = ({
|
||||||
|
context,
|
||||||
|
className,
|
||||||
|
on = '',
|
||||||
|
off = '',
|
||||||
|
}: {
|
||||||
|
context: boolean;
|
||||||
|
className: string;
|
||||||
|
on: string;
|
||||||
|
off: string;
|
||||||
|
}) => {
|
||||||
|
return twMerge(className, context ? on : off);
|
||||||
|
};
|
167
src/styles/globals.css
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--font-sans: var(--font-sans), ui-sans-serif, system-ui, sans-serif,
|
||||||
|
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
|
||||||
|
--font-sans: var(--font-sans);
|
||||||
|
--font-mono: var(--font-mono);
|
||||||
|
--font-serif: var(--font-serif);
|
||||||
|
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
|
||||||
|
--shadow-2xs: var(--shadow-2xs);
|
||||||
|
--shadow-xs: var(--shadow-xs);
|
||||||
|
--shadow-sm: var(--shadow-sm);
|
||||||
|
--shadow: var(--shadow);
|
||||||
|
--shadow-md: var(--shadow-md);
|
||||||
|
--shadow-lg: var(--shadow-lg);
|
||||||
|
--shadow-xl: var(--shadow-xl);
|
||||||
|
--shadow-2xl: var(--shadow-2xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: oklch(0.9227 0.0011 17.1793);
|
||||||
|
--foreground: oklch(0.2840 0.0220 262.4967);
|
||||||
|
--card: oklch(0.9699 0.0013 106.4238);
|
||||||
|
--card-foreground: oklch(0.2840 0.0220 262.4967);
|
||||||
|
--popover: oklch(0.9699 0.0013 106.4238);
|
||||||
|
--popover-foreground: oklch(0.2840 0.0220 262.4967);
|
||||||
|
--primary: oklch(0.6378 0.1247 281.2150);
|
||||||
|
--primary-foreground: oklch(1.0000 0 0);
|
||||||
|
--secondary: oklch(0.8682 0.0026 48.7143);
|
||||||
|
--secondary-foreground: oklch(0.4507 0.0152 255.5845);
|
||||||
|
--muted: oklch(0.9227 0.0011 17.1793);
|
||||||
|
--muted-foreground: oklch(0.5551 0.0147 266.6154);
|
||||||
|
--accent: oklch(0.9409 0.0164 322.6966);
|
||||||
|
--accent-foreground: oklch(0.3774 0.0189 260.6754);
|
||||||
|
--destructive: oklch(0.6322 0.1310 21.4751);
|
||||||
|
--destructive-foreground: oklch(1.0000 0 0);
|
||||||
|
--border: oklch(0.8682 0.0026 48.7143);
|
||||||
|
--input: oklch(0.8682 0.0026 48.7143);
|
||||||
|
--ring: oklch(0.6378 0.1247 281.2150);
|
||||||
|
--chart-1: oklch(0.6378 0.1247 281.2150);
|
||||||
|
--chart-2: oklch(0.5608 0.1433 283.1275);
|
||||||
|
--chart-3: oklch(0.5008 0.1358 283.9499);
|
||||||
|
--chart-4: oklch(0.4372 0.1108 283.4322);
|
||||||
|
--chart-5: oklch(0.3928 0.0817 282.8932);
|
||||||
|
--sidebar: oklch(0.8682 0.0026 48.7143);
|
||||||
|
--sidebar-foreground: oklch(0.2840 0.0220 262.4967);
|
||||||
|
--sidebar-primary: oklch(0.6378 0.1247 281.2150);
|
||||||
|
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
||||||
|
--sidebar-accent: oklch(0.9409 0.0164 322.6966);
|
||||||
|
--sidebar-accent-foreground: oklch(0.3774 0.0189 260.6754);
|
||||||
|
--sidebar-border: oklch(0.8682 0.0026 48.7143);
|
||||||
|
--sidebar-ring: oklch(0.6378 0.1247 281.2150);
|
||||||
|
--font-sans: Inter, sans-serif;
|
||||||
|
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||||
|
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
--radius: 1.0rem;
|
||||||
|
--shadow-2xs: 2px 2px 10px 4px hsl(240 1.9608% 60% / 0.09);
|
||||||
|
--shadow-xs: 2px 2px 10px 4px hsl(240 1.9608% 60% / 0.09);
|
||||||
|
--shadow-sm: 2px 2px 10px 4px hsl(240 1.9608% 60% / 0.18), 2px 1px 2px 3px hsl(240 1.9608% 60% / 0.18);
|
||||||
|
--shadow: 2px 2px 10px 4px hsl(240 1.9608% 60% / 0.18), 2px 1px 2px 3px hsl(240 1.9608% 60% / 0.18);
|
||||||
|
--shadow-md: 2px 2px 10px 4px hsl(240 1.9608% 60% / 0.18), 2px 2px 4px 3px hsl(240 1.9608% 60% / 0.18);
|
||||||
|
--shadow-lg: 2px 2px 10px 4px hsl(240 1.9608% 60% / 0.18), 2px 4px 6px 3px hsl(240 1.9608% 60% / 0.18);
|
||||||
|
--shadow-xl: 2px 2px 10px 4px hsl(240 1.9608% 60% / 0.18), 2px 8px 10px 3px hsl(240 1.9608% 60% / 0.18);
|
||||||
|
--shadow-2xl: 2px 2px 10px 4px hsl(240 1.9608% 60% / 0.45);
|
||||||
|
--tracking-normal: 0em;
|
||||||
|
--spacing: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.2236 0.0049 67.5717);
|
||||||
|
--foreground: oklch(0.9301 0.0075 260.7315);
|
||||||
|
--card: oklch(0.2793 0.0057 56.1503);
|
||||||
|
--card-foreground: oklch(0.9301 0.0075 260.7315);
|
||||||
|
--popover: oklch(0.2793 0.0057 56.1503);
|
||||||
|
--popover-foreground: oklch(0.9301 0.0075 260.7315);
|
||||||
|
--primary: oklch(0.7223 0.0946 279.6746);
|
||||||
|
--primary-foreground: oklch(0.2236 0.0049 67.5717);
|
||||||
|
--secondary: oklch(0.3352 0.0055 56.2080);
|
||||||
|
--secondary-foreground: oklch(0.8726 0.0059 264.5296);
|
||||||
|
--muted: oklch(0.2793 0.0057 56.1503);
|
||||||
|
--muted-foreground: oklch(0.7176 0.0111 261.7826);
|
||||||
|
--accent: oklch(0.3889 0.0053 56.2463);
|
||||||
|
--accent-foreground: oklch(0.8726 0.0059 264.5296);
|
||||||
|
--destructive: oklch(0.6322 0.1310 21.4751);
|
||||||
|
--destructive-foreground: oklch(0.2236 0.0049 67.5717);
|
||||||
|
--border: oklch(0.3352 0.0055 56.2080);
|
||||||
|
--input: oklch(0.3352 0.0055 56.2080);
|
||||||
|
--ring: oklch(0.7223 0.0946 279.6746);
|
||||||
|
--chart-1: oklch(0.7223 0.0946 279.6746);
|
||||||
|
--chart-2: oklch(0.6378 0.1247 281.2150);
|
||||||
|
--chart-3: oklch(0.5608 0.1433 283.1275);
|
||||||
|
--chart-4: oklch(0.5008 0.1358 283.9499);
|
||||||
|
--chart-5: oklch(0.4372 0.1108 283.4322);
|
||||||
|
--sidebar: oklch(0.3352 0.0055 56.2080);
|
||||||
|
--sidebar-foreground: oklch(0.9301 0.0075 260.7315);
|
||||||
|
--sidebar-primary: oklch(0.7223 0.0946 279.6746);
|
||||||
|
--sidebar-primary-foreground: oklch(0.2236 0.0049 67.5717);
|
||||||
|
--sidebar-accent: oklch(0.3889 0.0053 56.2463);
|
||||||
|
--sidebar-accent-foreground: oklch(0.8726 0.0059 264.5296);
|
||||||
|
--sidebar-border: oklch(0.3352 0.0055 56.2080);
|
||||||
|
--sidebar-ring: oklch(0.7223 0.0946 279.6746);
|
||||||
|
--font-sans: Inter, sans-serif;
|
||||||
|
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||||
|
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
--radius: 1.0rem;
|
||||||
|
--shadow-2xs: 2px 2px 10px 4px hsl(0 0% 10.1961% / 0.09);
|
||||||
|
--shadow-xs: 2px 2px 10px 4px hsl(0 0% 10.1961% / 0.09);
|
||||||
|
--shadow-sm: 2px 2px 10px 4px hsl(0 0% 10.1961% / 0.18), 2px 1px 2px 3px hsl(0 0% 10.1961% / 0.18);
|
||||||
|
--shadow: 2px 2px 10px 4px hsl(0 0% 10.1961% / 0.18), 2px 1px 2px 3px hsl(0 0% 10.1961% / 0.18);
|
||||||
|
--shadow-md: 2px 2px 10px 4px hsl(0 0% 10.1961% / 0.18), 2px 2px 4px 3px hsl(0 0% 10.1961% / 0.18);
|
||||||
|
--shadow-lg: 2px 2px 10px 4px hsl(0 0% 10.1961% / 0.18), 2px 4px 6px 3px hsl(0 0% 10.1961% / 0.18);
|
||||||
|
--shadow-xl: 2px 2px 10px 4px hsl(0 0% 10.1961% / 0.18), 2px 8px 10px 3px hsl(0 0% 10.1961% / 0.18);
|
||||||
|
--shadow-2xl: 2px 2px 10px 4px hsl(0 0% 10.1961% / 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|