Theme overhaul

This commit is contained in:
2025-07-31 18:37:33 -04:00
parent a1616b161d
commit 8a2565adad
79 changed files with 2722 additions and 3917 deletions

289
bun.lock
View File

@@ -4,7 +4,7 @@
"": {
"name": "beenvoice",
"dependencies": {
"@auth/drizzle-adapter": "^1.7.2",
"@auth/drizzle-adapter": "^1.10.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
@@ -24,7 +24,7 @@
"@radix-ui/react-tabs": "^1.1.12",
"@react-pdf/renderer": "^4.3.0",
"@t3-oss/env-nextjs": "^0.12.0",
"@tanstack/react-query": "^5.69.0",
"@tanstack/react-query": "^5.84.0",
"@tanstack/react-table": "^8.21.3",
"@tiptap/extension-color": "^3.0.7",
"@tiptap/extension-list-item": "^3.0.7",
@@ -32,9 +32,9 @@
"@tiptap/extension-text-style": "^3.0.7",
"@tiptap/react": "^3.0.7",
"@tiptap/starter-kit": "^3.0.7",
"@trpc/client": "^11.0.0",
"@trpc/react-query": "^11.0.0",
"@trpc/server": "^11.0.0",
"@trpc/client": "^11.4.3",
"@trpc/react-query": "^11.4.3",
"@trpc/server": "^11.4.3",
"@types/bcryptjs": "^2.4.6",
"@types/file-saver": "^2.0.7",
"@types/pg": "^8.15.5",
@@ -46,45 +46,48 @@
"date-fns": "^4.1.0",
"drizzle-orm": "^0.44.4",
"file-saver": "^2.0.5",
"framer-motion": "^12.23.12",
"lucide": "^0.525.0",
"lucide-react": "^0.525.0",
"next": "^15.4.4",
"next": "^15.4.5",
"next-auth": "5.0.0-beta.25",
"next-themes": "^0.4.6",
"pg": "^8.16.3",
"react": "^19.0.0",
"react-day-picker": "^9.8.0",
"react-dom": "^19.0.0",
"react": "^19.1.1",
"react-day-picker": "^9.8.1",
"react-dom": "^19.1.1",
"react-dropzone": "^14.3.8",
"recharts": "^3.1.0",
"resend": "^4.7.0",
"server-only": "^0.0.1",
"sonner": "^2.0.6",
"superjson": "^2.2.1",
"superjson": "^2.2.2",
"tailwind-merge": "^3.3.1",
"turso": "^0.1.0",
"vercel": "^44.6.4",
"zod": "^3.24.2",
"vercel": "^44.6.5",
"zod": "^3.25.76",
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@tailwindcss/postcss": "^4.0.15",
"@tailwindcss/postcss": "^4.1.11",
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^20.14.10",
"@types/node": "^20.19.9",
"@types/raf": "^3.4.3",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@types/react": "^19.1.9",
"@types/react-dom": "^19.1.7",
"better-sqlite3": "^12.2.0",
"drizzle-kit": "^0.30.5",
"eslint": "^9.23.0",
"eslint-config-next": "^15.2.3",
"drizzle-kit": "^0.30.6",
"eslint": "^9.32.0",
"eslint-config-next": "^15.4.5",
"eslint-plugin-drizzle": "^0.2.3",
"postcss": "^8.5.3",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"tailwindcss": "^4.0.15",
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.6.14",
"tailwindcss": "^4.1.11",
"tailwindcss-animate": "^1.0.7",
"tw-animate-css": "^1.3.5",
"typescript": "^5.8.2",
"typescript-eslint": "^8.27.0",
"tw-animate-css": "^1.3.6",
"typescript": "^5.9.2",
"typescript-eslint": "^8.38.0",
},
},
},
@@ -194,15 +197,15 @@
"@eslint/config-helpers": ["@eslint/config-helpers@0.3.0", "", {}, "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw=="],
"@eslint/core": ["@eslint/core@0.14.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg=="],
"@eslint/core": ["@eslint/core@0.15.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA=="],
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
"@eslint/js": ["@eslint/js@9.30.1", "", {}, "sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg=="],
"@eslint/js": ["@eslint/js@9.32.0", "", {}, "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg=="],
"@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.3", "", { "dependencies": { "@eslint/core": "^0.15.1", "levn": "^0.4.1" } }, "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag=="],
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.4", "", { "dependencies": { "@eslint/core": "^0.15.1", "levn": "^0.4.1" } }, "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw=="],
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
@@ -282,25 +285,25 @@
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" } }, "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA=="],
"@next/env": ["@next/env@15.4.4", "", {}, "sha512-SJKOOkULKENyHSYXE5+KiFU6itcIb6wSBjgM92meK0HVKpo94dNOLZVdLLuS7/BxImROkGoPsjR4EnuDucqiiA=="],
"@next/env": ["@next/env@15.4.5", "", {}, "sha512-ruM+q2SCOVCepUiERoxOmZY9ZVoecR3gcXNwCYZRvQQWRjhOiPJGmQ2fAiLR6YKWXcSAh7G79KEFxN3rwhs4LQ=="],
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.3.5", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-BZwWPGfp9po/rAnJcwUBaM+yT/+yTWIkWdyDwc74G9jcfTrNrmsHe+hXHljV066YNdVs8cxROxX5IgMQGX190w=="],
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.4.5", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-YhbrlbEt0m4jJnXHMY/cCUDBAWgd5SaTa5mJjzOt82QwflAFfW/h3+COp2TfVSzhmscIZ5sg2WXt3MLziqCSCw=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.4.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eVG55dnGwfUuG+TtnUCt+mEJ+8TGgul6nHEvdb8HEH7dmJIFYOCApAaFrIrxwtEq2Cdf+0m5sG1Np8cNpw9EAw=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.4.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-84dAN4fkfdC7nX6udDLz9GzQlMUwEMKD7zsseXrl7FTeIItF8vpk1lhLEnsotiiDt+QFu3O1FVWnqwcRD2U3KA=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.4.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-zqG+/8apsu49CltEj4NAmCGZvHcZbOOOsNoTVeIXphYWIbE4l6A/vuQHyqll0flU2o3dmYCXsBW5FmbrGDgljQ=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.4.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-CL6mfGsKuFSyQjx36p2ftwMNSb8PQog8y0HO/ONLdQqDql7x3aJb/wB+LA651r4we2pp/Ck+qoRVUeZZEvSurA=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.4.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-LRD4l2lq4R+2QCHBQVC0wjxxkLlALGJCwigaJ5FSRSqnje+MRKHljQNZgDCaKUZQzO/TXxlmUdkZP/X3KNGZaw=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-1hTVd9n6jpM/thnDc5kYHD1OjjWYpUJrJxY4DlEacT7L5SEOXIifIdTye6SQNNn8JDZrcN+n8AWOmeJ8u3KlvQ=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.4.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-LsGUCTvuZ0690fFWerA4lnQvjkYg9gHo12A3wiPUR4kCxbx/d+SlwmonuTH2SWZI+RVGA9VL3N0S03WTYv6bYg=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-4W+D/nw3RpIwGrqpFi7greZ0hjrCaioGErI7XHgkcTeWdZd146NNu1s4HnaHonLeNTguKnL2Urqvj28UJj6Gqw=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.4.4", "", { "os": "linux", "cpu": "x64" }, "sha512-aOy5yNRpLL3wNiJVkFYl6w22hdREERNjvegE6vvtix8LHRdsTHhWTpgvcYdCK7AIDCQW5ATmzr9XkPHvSoAnvg=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-N6Mgdxe/Cn2K1yMHge6pclffkxzbSGOydXVKYOjYqQXZYjLCfN/CuFkaYDeDHY2VBwSHyM2fUjYBiQCIlxIKDA=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.4.4", "", { "os": "linux", "cpu": "x64" }, "sha512-FL7OAn4UkR8hKQRGBmlHiHinzOb07tsfARdGh7v0Z0jEJ3sz8/7L5bR23ble9E6DZMabSStqlATHlSxv1fuzAg=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-YZ3bNDrS8v5KiqgWE0xZQgtXgCTUacgFtnEgI4ccotAASwSvcMPDLua7BWLuTfucoRv6mPidXkITJLd8IdJplQ=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.4.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-eEdNW/TXwjYhOulQh0pffTMMItWVwKCQpbziSBmgBNFZIIRn2GTXrhrewevs8wP8KXWYMx8Z+mNU0X+AfvtrRg=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.4.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-9Wr4t9GkZmMNcTVvSloFtjzbH4vtT4a8+UHqDoVnxA5QyfWe6c5flTH1BIWPGNWSUlofc8dVJAE7j84FQgskvQ=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.4.4", "", { "os": "win32", "cpu": "x64" }, "sha512-SE5pYNbn/xZKMy1RE3pAs+4xD32OI4rY6mzJa4XUkp/ItZY+OMjIgilskmErt8ls/fVJ+Ihopi2QIeW6O3TrMw=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.4.5", "", { "os": "win32", "cpu": "x64" }, "sha512-voWk7XtGvlsP+w8VBz7lqp8Y+dYw/MTI4KeS0gTVtfdhdJ5QwhXLmNrndFOin/MDoCvUaLWMkYKATaCoUkt2/A=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
@@ -426,6 +429,8 @@
"@react-pdf/types": ["@react-pdf/types@2.9.0", "", { "dependencies": { "@react-pdf/font": "^4.0.2", "@react-pdf/primitives": "^4.1.1", "@react-pdf/stylesheet": "^6.1.0" } }, "sha512-ckj80vZLlvl9oYrQ4tovEaqKWP3O06Eb1D48/jQWbdwz1Yh7Y9v1cEmwlP8ET+a1Whp8xfdM0xduMexkuPANCQ=="],
"@reduxjs/toolkit": ["@reduxjs/toolkit@2.8.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^10.0.3", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A=="],
"@remirror/core-constants": ["@remirror/core-constants@3.0.0", "", {}, "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg=="],
"@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=="],
@@ -438,6 +443,10 @@
"@sinclair/typebox": ["@sinclair/typebox@0.25.24", "", {}, "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ=="],
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
"@t3-oss/env-core": ["@t3-oss/env-core@0.12.0", "", { "peerDependencies": { "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0" }, "optionalPeers": ["typescript", "valibot", "zod"] }, "sha512-lOPj8d9nJJTt81mMuN9GMk8x5veOt7q9m11OSnCBJhwp1QrL/qR+M8Y467ULBSm9SunosryWNbmQQbgoiMgcdw=="],
@@ -474,9 +483,9 @@
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.11", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "postcss": "^8.4.41", "tailwindcss": "4.1.11" } }, "sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA=="],
"@tanstack/query-core": ["@tanstack/query-core@5.82.0", "", {}, "sha512-JrjoVuaajBQtnoWSg8iaPHaT4mW73lK2t+exxHNOSMqy0+13eKLqJgTKXKImLejQIfdAHQ6Un0njEhOvUtOd5w=="],
"@tanstack/query-core": ["@tanstack/query-core@5.83.1", "", {}, "sha512-OG69LQgT7jSp+5pPuCfzltq/+7l2xoweggjme9vlbCPa/d7D7zaqv5vN/S82SzSYZ4EDLTxNO1PWrv49RAS64Q=="],
"@tanstack/react-query": ["@tanstack/react-query@5.82.0", "", { "dependencies": { "@tanstack/query-core": "5.82.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-mnk8/ofKEthFeMdhV1dV8YXRf+9HqvXAcciXkoo755d/ocfWq7N/Y9jGOzS3h7ZW9dDGwSIhs3/HANWUBsyqYg=="],
"@tanstack/react-query": ["@tanstack/react-query@5.84.0", "", { "dependencies": { "@tanstack/query-core": "5.83.1" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-iPycFGLq5lltDE16Jf13Nx7SOvtfoopfOH/+Ahbdd+z4QqOfYu/SOkY86AVYVcKjneuqPxTm8e85lSGhwe0cog=="],
"@tanstack/react-table": ["@tanstack/react-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="],
@@ -570,6 +579,24 @@
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
"@types/d3-array": ["@types/d3-array@3.2.1", "", {}, "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="],
"@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
"@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="],
"@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="],
"@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="],
"@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="],
"@types/d3-shape": ["@types/d3-shape@3.1.7", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg=="],
"@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
"@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/file-saver": ["@types/file-saver@2.0.7", "", {}, "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A=="],
@@ -584,15 +611,15 @@
"@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="],
"@types/node": ["@types/node@20.19.6", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-uYssdp9z5zH5GQ0L4zEJ2ZuavYsJwkozjiUzCRfGtaaQcyjAMJ34aP8idv61QlqTozu6kudyr6JMq9Chf09dfA=="],
"@types/node": ["@types/node@20.19.9", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw=="],
"@types/pg": ["@types/pg@8.15.5", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ=="],
"@types/raf": ["@types/raf@3.4.3", "", {}, "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw=="],
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
"@types/react": ["@types/react@19.1.9", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA=="],
"@types/react-dom": ["@types/react-dom@19.1.6", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw=="],
"@types/react-dom": ["@types/react-dom@19.1.7", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw=="],
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
@@ -600,19 +627,19 @@
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.36.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.36.0", "@typescript-eslint/types": "8.36.0", "@typescript-eslint/typescript-estree": "8.36.0", "@typescript-eslint/visitor-keys": "8.36.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.36.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.36.0", "@typescript-eslint/types": "^8.36.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.38.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.38.0", "@typescript-eslint/types": "^8.38.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.36.0", "", { "dependencies": { "@typescript-eslint/types": "8.36.0", "@typescript-eslint/visitor-keys": "8.36.0" } }, "sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.36.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.38.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.36.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.36.0", "@typescript-eslint/utils": "8.36.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-5aaGYG8cVDd6cxfk/ynpYzxBRZJk7w/ymto6uiyUFtdCozQIsQWh7M28/6r57Fwkbweng8qAzoMCPwSJfWlmsg=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.36.0", "", {}, "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.36.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.36.0", "@typescript-eslint/tsconfig-utils": "8.36.0", "@typescript-eslint/types": "8.36.0", "@typescript-eslint/visitor-keys": "8.36.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.38.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.38.0", "@typescript-eslint/tsconfig-utils": "8.38.0", "@typescript-eslint/types": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.36.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.36.0", "@typescript-eslint/types": "8.36.0", "@typescript-eslint/typescript-estree": "8.36.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.38.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/types": "8.38.0", "@typescript-eslint/typescript-estree": "8.38.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.36.0", "", { "dependencies": { "@typescript-eslint/types": "8.36.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA=="],
@@ -670,11 +697,11 @@
"@vercel/go": ["@vercel/go@3.2.2", "", {}, "sha512-yYIU3OXkuoxzr47vf+HZnVGujzkVW/e5hrUK0jzCzzmjgo4+5ECrXDSA1bTB6qmWEy6HVsSovnKoHkwDqNIN6Q=="],
"@vercel/hono": ["@vercel/hono@0.0.4", "", { "dependencies": { "@vercel/node": "5.3.7", "@vercel/static-config": "3.1.1", "ts-morph": "12.0.0" } }, "sha512-seH+eaHEooC8EWVKGRtPtFMrl7n2dvO1OJ3YCt1Fou6tRBfi4spErGs2sdC4raogAWO645BK2bepH5Fo/wJFZw=="],
"@vercel/hono": ["@vercel/hono@0.0.5", "", { "dependencies": { "@vercel/node": "5.3.7", "@vercel/static-config": "3.1.1", "ts-morph": "12.0.0" } }, "sha512-y+bmKJq9OcSMwtGbvpzXCIrwPrM1t3XkDQMo9KamVtDF9qK3V8NteX09wBeEZzObovFkfUwhneOth1HUVhqC0w=="],
"@vercel/hydrogen": ["@vercel/hydrogen@1.2.3", "", { "dependencies": { "@vercel/static-config": "3.1.1", "ts-morph": "12.0.0" } }, "sha512-sU0HWHabNxlTdKQWPMDqwr+3MqnhXc4Tvnl4i9cbdEYn5061SEKhdfpl2RhuvPByQJ8sZL8soVXFHIjFBJAR4w=="],
"@vercel/next": ["@vercel/next@4.10.8", "", { "dependencies": { "@vercel/nft": "0.29.2" } }, "sha512-2con6J6uj4IAs+3SU55ZjH6flskDpi6zCRH4XmfnR0SUrsGiy6hmDbZAvfJnSHKxLFQpmP0+cn5XC+hoUAUQLg=="],
"@vercel/next": ["@vercel/next@4.10.9", "", { "dependencies": { "@vercel/nft": "0.29.2" } }, "sha512-QtXJaOl2GyDYac3Bzgt6YqYgBb90KG5fLC4D2dC9KAxq2NNBt7y8yEakaWYrIyXCw0bhyX/TjcYf7VneIrHVpg=="],
"@vercel/nft": ["@vercel/nft@0.29.2", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^10.4.5", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-A/Si4mrTkQqJ6EXJKv5EYCDQ3NL6nJXxG8VGXePsaiQigsomHYQC9xSpX8qGk7AEZk4b1ssbYIqJ0ISQQ7bfcA=="],
@@ -846,6 +873,28 @@
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
"d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
"d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="],
"d3-format": ["d3-format@3.1.0", "", {}, "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="],
"d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
"d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
"d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
"d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="],
"d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
"d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
"d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
"damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="],
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
@@ -862,6 +911,8 @@
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
"deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
@@ -932,6 +983,8 @@
"es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="],
"es-toolkit": ["es-toolkit@1.39.8", "", {}, "sha512-A8QO9TfF+rltS8BXpdu8OS+rpGgEdnRhqIVxO/ZmNvnXBYgOdSsxukT55ELyP94gZIntWJ+Li9QRrT2u1Kitpg=="],
"esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="],
"esbuild-android-64": ["esbuild-android-64@0.14.47", "", { "os": "android", "cpu": "x64" }, "sha512-R13Bd9+tqLVFndncMHssZrPWe6/0Kpv2/dt4aA69soX4PRxlzsVpCvoJeFE8sOEoeVEiBkI0myjlkDodXlHa0g=="],
@@ -978,9 +1031,9 @@
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@9.30.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.30.1", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ=="],
"eslint": ["eslint@9.32.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.32.0", "@eslint/plugin-kit": "^0.3.4", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg=="],
"eslint-config-next": ["eslint-config-next@15.3.5", "", { "dependencies": { "@next/eslint-plugin-next": "15.3.5", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^5.0.0" }, "peerDependencies": { "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-oQdvnIgP68wh2RlR3MdQpvaJ94R6qEFl+lnu8ZKxPj5fsAHrSF/HlAOZcsimLw3DT6bnEQIUdbZC2Ab6sWyptg=="],
"eslint-config-next": ["eslint-config-next@15.4.5", "", { "dependencies": { "@next/eslint-plugin-next": "15.4.5", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^5.0.0" }, "peerDependencies": { "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-IMijiXaZ43qFB+Gcpnb374ipTKD8JIyVNR+6VsifFQ/LHyx+A9wgcgSIhCX5PYSjwOoSYD5LtNHKlM5uc23eww=="],
"eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="],
@@ -1016,6 +1069,8 @@
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
"eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="],
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
"events-intercept": ["events-intercept@2.0.0", "", {}, "sha512-blk1va0zol9QOrdZt0rFXo5KMkNPVSp92Eju/Qz8THwKWKRKeE0T8Br/1aW6+Edkyq9xHYgYxn2QtOnUKPUp+Q=="],
@@ -1058,6 +1113,8 @@
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
"framer-motion": ["framer-motion@12.23.12", "", { "dependencies": { "motion-dom": "^12.23.12", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg=="],
"fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
"fs-extra": ["fs-extra@11.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw=="],
@@ -1134,6 +1191,8 @@
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"immer": ["immer@10.1.1", "", {}, "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
@@ -1144,6 +1203,8 @@
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
"is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
"is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="],
@@ -1322,6 +1383,10 @@
"mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
"motion-dom": ["motion-dom@12.23.12", "", { "dependencies": { "motion-utils": "^12.23.6" } }, "sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw=="],
"motion-utils": ["motion-utils@12.23.6", "", {}, "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ=="],
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
@@ -1334,10 +1399,12 @@
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"next": ["next@15.4.4", "", { "dependencies": { "@next/env": "15.4.4", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.4", "@next/swc-darwin-x64": "15.4.4", "@next/swc-linux-arm64-gnu": "15.4.4", "@next/swc-linux-arm64-musl": "15.4.4", "@next/swc-linux-x64-gnu": "15.4.4", "@next/swc-linux-x64-musl": "15.4.4", "@next/swc-win32-arm64-msvc": "15.4.4", "@next/swc-win32-x64-msvc": "15.4.4", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-kNcubvJjOL9yUOfwtZF3HfDhuhp+kVD+FM2A6Tyua1eI/xfmY4r/8ZS913MMz+oWKDlbps/dQOWdDricuIkXLw=="],
"next": ["next@15.4.5", "", { "dependencies": { "@next/env": "15.4.5", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.5", "@next/swc-darwin-x64": "15.4.5", "@next/swc-linux-arm64-gnu": "15.4.5", "@next/swc-linux-arm64-musl": "15.4.5", "@next/swc-linux-x64-gnu": "15.4.5", "@next/swc-linux-x64-musl": "15.4.5", "@next/swc-win32-arm64-msvc": "15.4.5", "@next/swc-win32-x64-msvc": "15.4.5", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-nJ4v+IO9CPmbmcvsPebIoX3Q+S7f6Fu08/dEWu0Ttfa+wVwQRh9epcmsyCPjmL2b8MxC+CkBR97jgDhUUztI3g=="],
"next-auth": ["next-auth@5.0.0-beta.25", "", { "dependencies": { "@auth/core": "0.37.2" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "next": "^14.0.0-0 || ^15.0.0-0", "nodemailer": "^6.6.5", "react": "^18.2.0 || ^19.0.0-0" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-2dJJw1sHQl2qxCrRk+KTQbeH+izFbGFPuJj5eGgBZFYyiYYtvlrBeUw1E/OJJxTRjuxbSYGnCTkUIRsIIW0bog=="],
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
"node-abi": ["node-abi@3.75.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg=="],
"node-fetch": ["node-fetch@2.6.7", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="],
@@ -1518,11 +1585,11 @@
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
"react": ["react@19.1.1", "", {}, "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="],
"react-day-picker": ["react-day-picker@9.8.0", "", { "dependencies": { "@date-fns/tz": "1.2.0", "date-fns": "4.1.0", "date-fns-jalali": "4.1.0-0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-E0yhhg7R+pdgbl/2toTb0xBhsEAtmAx1l7qjIWYfcxOy8w4rTSVfbtBoSzVVhPwKP/5E9iL38LivzoE3AQDhCQ=="],
"react-day-picker": ["react-day-picker@9.8.1", "", { "dependencies": { "@date-fns/tz": "^1.2.0", "date-fns": "^4.1.0", "date-fns-jalali": "^4.1.0-0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-kMcLrp3PfN/asVJayVv82IjF3iLOOxuH5TNFWezX6lS/T8iVRFPTETpHl3TUSTH99IDMZLubdNPJr++rQctkEw=="],
"react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
"react-dom": ["react-dom@19.1.1", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.1" } }, "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw=="],
"react-dropzone": ["react-dropzone@14.3.8", "", { "dependencies": { "attr-accept": "^2.2.4", "file-selector": "^2.1.0", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.8 || 18.0.0" } }, "sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug=="],
@@ -1530,6 +1597,8 @@
"react-promise-suspense": ["react-promise-suspense@0.3.4", "", { "dependencies": { "fast-deep-equal": "^2.0.1" } }, "sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ=="],
"react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="],
"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=="],
@@ -1540,12 +1609,20 @@
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"recharts": ["recharts@3.1.0", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-NqAqQcGBmLrfDs2mHX/bz8jJCQtG2FeXfE0GqpZmIuXIjkpIwj8sd9ad0WyvKiBKPd8ZgNG0hL85c8sFDwascw=="],
"redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="],
"redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="],
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="],
"resend": ["resend@4.7.0", "", { "dependencies": { "@react-email/render": "1.1.2" } }, "sha512-30IbXGBUbmDweQH2IlO53XOXX7ndjYV9xFZ8IEBiWqefqQ/qmTsgrX0Ab6MUnmobJXbpdReVv+iXGRQPubQL5Q=="],
"resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
@@ -1692,6 +1769,8 @@
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
"tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
@@ -1720,7 +1799,7 @@
"turso": ["turso@0.1.0", "", {}, "sha512-gM6zaKIQA35QoujeknL1oNFjgduei9sgVRrKc9XRSG1P+VAUAQzfKJGlLQqcnsHFh+a1kqRV16gI/mCvyEJS1Q=="],
"tw-animate-css": ["tw-animate-css@1.3.5", "", {}, "sha512-t3u+0YNoloIhj1mMXs779P6MO9q3p3mvGn4k1n3nJPqJw/glZcuijG2qTSN4z4mgNRfW5ZC3aXJFLwDtiipZXA=="],
"tw-animate-css": ["tw-animate-css@1.3.6", "", {}, "sha512-9dy0R9UsYEGmgf26L8UcHiLmSFTHa9+D7+dAt/G/sF5dCnPePZbfgDYinc7/UzAM7g/baVrmS6m9yEpU46d+LA=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
@@ -1732,9 +1811,9 @@
"typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
"typescript-eslint": ["typescript-eslint@8.36.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.36.0", "@typescript-eslint/parser": "8.36.0", "@typescript-eslint/utils": "8.36.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-fTCqxthY+h9QbEgSIBfL9iV6CvKDFuoxg6bHPNpJ9HIUzS+jy2lCEyCmGyZRWEBSaykqcDPf1SJ+BfCI8DRopA=="],
"typescript-eslint": ["typescript-eslint@8.38.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.38.0", "@typescript-eslint/parser": "8.38.0", "@typescript-eslint/typescript-estree": "8.38.0", "@typescript-eslint/utils": "8.38.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-FsZlrYK6bPDGoLeZRuvx2v6qrM03I0U0SnfCLPs/XCCPCFD80xU9Pg09H/K+XFa68uJuZo7l/Xhs+eDRg2l3hg=="],
"uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="],
@@ -1768,7 +1847,9 @@
"v8-compile-cache-lib": ["v8-compile-cache-lib@3.0.1", "", {}, "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="],
"vercel": ["vercel@44.6.4", "", { "dependencies": { "@vercel/blob": "1.0.2", "@vercel/build-utils": "11.0.0", "@vercel/fun": "1.1.6", "@vercel/go": "3.2.2", "@vercel/hono": "0.0.4", "@vercel/hydrogen": "1.2.3", "@vercel/next": "4.10.8", "@vercel/node": "5.3.7", "@vercel/python": "5.0.0", "@vercel/redwood": "2.3.4", "@vercel/remix-builder": "5.4.10", "@vercel/ruby": "2.2.1", "@vercel/static-build": "2.7.17", "chokidar": "4.0.0", "jose": "5.9.6" }, "bin": { "vc": "dist/vc.js", "vercel": "dist/vc.js" } }, "sha512-BfZelATWvXMhjta0LgaUU+HTB3YOk1a3iCuRkJwPDRB8SIKjCgIJSRNA3lzW/sDswpVGb3ZxWo4HLp9TYERM1w=="],
"vercel": ["vercel@44.6.5", "", { "dependencies": { "@vercel/blob": "1.0.2", "@vercel/build-utils": "11.0.0", "@vercel/fun": "1.1.6", "@vercel/go": "3.2.2", "@vercel/hono": "0.0.5", "@vercel/hydrogen": "1.2.3", "@vercel/next": "4.10.9", "@vercel/node": "5.3.7", "@vercel/python": "5.0.0", "@vercel/redwood": "2.3.4", "@vercel/remix-builder": "5.4.10", "@vercel/ruby": "2.2.1", "@vercel/static-build": "2.7.17", "chokidar": "4.0.0", "jose": "5.9.6" }, "bin": { "vc": "dist/vc.js", "vercel": "dist/vc.js" } }, "sha512-a5fJkYbWEVUgzxPU9depZlWZir/8PAPo4pHCzykQ6NAubNSWmYZE0rZBYrw+jSAhzGJKdbS0ATVbti1MF/e/xA=="],
"victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="],
"vite-compatible-readable-stream": ["vite-compatible-readable-stream@3.6.1", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ=="],
@@ -1828,8 +1909,6 @@
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.15.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA=="],
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
"@mapbox/node-pre-gyp/node-fetch": ["node-fetch@2.6.9", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg=="],
@@ -1854,12 +1933,34 @@
"@ts-morph/common/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
"@types/better-sqlite3/@types/node": ["@types/node@20.19.6", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-uYssdp9z5zH5GQ0L4zEJ2ZuavYsJwkozjiUzCRfGtaaQcyjAMJ34aP8idv61QlqTozu6kudyr6JMq9Chf09dfA=="],
"@types/pg/@types/node": ["@types/node@20.19.6", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-uYssdp9z5zH5GQ0L4zEJ2ZuavYsJwkozjiUzCRfGtaaQcyjAMJ34aP8idv61QlqTozu6kudyr6JMq9Chf09dfA=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.36.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.36.0", "@typescript-eslint/types": "8.36.0", "@typescript-eslint/typescript-estree": "8.36.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g=="],
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"@typescript-eslint/parser/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.36.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.36.0", "@typescript-eslint/tsconfig-utils": "8.36.0", "@typescript-eslint/types": "8.36.0", "@typescript-eslint/visitor-keys": "8.36.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg=="],
"@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.38.0", "", {}, "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw=="],
"@typescript-eslint/type-utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.36.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.36.0", "@typescript-eslint/tsconfig-utils": "8.36.0", "@typescript-eslint/types": "8.36.0", "@typescript-eslint/visitor-keys": "8.36.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg=="],
"@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.36.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.36.0", "@typescript-eslint/types": "8.36.0", "@typescript-eslint/typescript-estree": "8.36.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g=="],
"@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.38.0", "", {}, "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw=="],
"@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g=="],
"@typescript-eslint/typescript-estree/fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0" } }, "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ=="],
"@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.38.0", "", {}, "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw=="],
"@vercel/fun/debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="],
"@vercel/fun/ms": ["ms@2.1.1", "", {}, "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="],
@@ -1948,6 +2049,10 @@
"ts-node/arg": ["arg@4.1.3", "", {}, "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="],
"typescript-eslint/@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.38.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/type-utils": "8.38.0", "@typescript-eslint/utils": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.38.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA=="],
"typescript-eslint/@typescript-eslint/parser": ["@typescript-eslint/parser@8.38.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/types": "8.38.0", "@typescript-eslint/typescript-estree": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ=="],
"unicode-properties/base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"unicode-trie/pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="],
@@ -2004,10 +2109,30 @@
"@ts-morph/common/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.36.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.36.0", "@typescript-eslint/tsconfig-utils": "8.36.0", "@typescript-eslint/types": "8.36.0", "@typescript-eslint/visitor-keys": "8.36.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg=="],
"@typescript-eslint/parser/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.36.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.36.0", "@typescript-eslint/types": "^8.36.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g=="],
"@typescript-eslint/parser/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.36.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA=="],
"@typescript-eslint/parser/@typescript-eslint/typescript-estree/fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
"@typescript-eslint/parser/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.36.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.36.0", "@typescript-eslint/types": "^8.36.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g=="],
"@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.36.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA=="],
"@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
"@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"@typescript-eslint/typescript-estree/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g=="],
"@vercel/fun/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="],
"@vercel/fun/tar/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
@@ -2040,10 +2165,50 @@
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0" } }, "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ=="],
"typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "@typescript-eslint/typescript-estree": "8.38.0", "@typescript-eslint/utils": "8.38.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg=="],
"typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g=="],
"typescript-eslint/@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"typescript-eslint/@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0" } }, "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ=="],
"typescript-eslint/@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.38.0", "", {}, "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw=="],
"typescript-eslint/@typescript-eslint/parser/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g=="],
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.36.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.36.0", "@typescript-eslint/types": "^8.36.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.36.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"@typescript-eslint/parser/@typescript-eslint/typescript-estree/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"@typescript-eslint/parser/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"@vercel/fun/tar/minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.38.0", "", {}, "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw=="],
"typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.38.0", "", {}, "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw=="],
"typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.38.0", "", {}, "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
}
}

View File

@@ -28,7 +28,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@auth/drizzle-adapter": "^1.7.2",
"@auth/drizzle-adapter": "^1.10.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
@@ -48,7 +48,7 @@
"@radix-ui/react-tabs": "^1.1.12",
"@react-pdf/renderer": "^4.3.0",
"@t3-oss/env-nextjs": "^0.12.0",
"@tanstack/react-query": "^5.69.0",
"@tanstack/react-query": "^5.84.0",
"@tanstack/react-table": "^8.21.3",
"@tiptap/extension-color": "^3.0.7",
"@tiptap/extension-list-item": "^3.0.7",
@@ -56,9 +56,9 @@
"@tiptap/extension-text-style": "^3.0.7",
"@tiptap/react": "^3.0.7",
"@tiptap/starter-kit": "^3.0.7",
"@trpc/client": "^11.0.0",
"@trpc/react-query": "^11.0.0",
"@trpc/server": "^11.0.0",
"@trpc/client": "^11.4.3",
"@trpc/react-query": "^11.4.3",
"@trpc/server": "^11.4.3",
"@types/bcryptjs": "^2.4.6",
"@types/file-saver": "^2.0.7",
"@types/pg": "^8.15.5",
@@ -70,45 +70,48 @@
"date-fns": "^4.1.0",
"drizzle-orm": "^0.44.4",
"file-saver": "^2.0.5",
"framer-motion": "^12.23.12",
"lucide": "^0.525.0",
"lucide-react": "^0.525.0",
"next": "^15.4.4",
"next": "^15.4.5",
"next-auth": "5.0.0-beta.25",
"next-themes": "^0.4.6",
"pg": "^8.16.3",
"react": "^19.0.0",
"react-day-picker": "^9.8.0",
"react-dom": "^19.0.0",
"react": "^19.1.1",
"react-day-picker": "^9.8.1",
"react-dom": "^19.1.1",
"react-dropzone": "^14.3.8",
"recharts": "^3.1.0",
"resend": "^4.7.0",
"server-only": "^0.0.1",
"sonner": "^2.0.6",
"superjson": "^2.2.1",
"superjson": "^2.2.2",
"tailwind-merge": "^3.3.1",
"turso": "^0.1.0",
"vercel": "^44.6.4",
"zod": "^3.24.2"
"vercel": "^44.6.5",
"zod": "^3.25.76"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@tailwindcss/postcss": "^4.0.15",
"@tailwindcss/postcss": "^4.1.11",
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^20.14.10",
"@types/node": "^20.19.9",
"@types/raf": "^3.4.3",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@types/react": "^19.1.9",
"@types/react-dom": "^19.1.7",
"better-sqlite3": "^12.2.0",
"drizzle-kit": "^0.30.5",
"eslint": "^9.23.0",
"eslint-config-next": "^15.2.3",
"drizzle-kit": "^0.30.6",
"eslint": "^9.32.0",
"eslint-config-next": "^15.4.5",
"eslint-plugin-drizzle": "^0.2.3",
"postcss": "^8.5.3",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"tailwindcss": "^4.0.15",
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.6.14",
"tailwindcss": "^4.1.11",
"tailwindcss-animate": "^1.0.7",
"tw-animate-css": "^1.3.5",
"typescript": "^5.8.2",
"typescript-eslint": "^8.27.0"
"tw-animate-css": "^1.3.6",
"typescript": "^5.9.2",
"typescript-eslint": "^8.38.0"
},
"ct3aMetadata": {
"initVersion": "7.39.3"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 56 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -1,20 +1,19 @@
"use client";
import Link from "next/link";
import { useState, Suspense } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { useRouter } from "next/navigation";
import { Card, CardContent } from "~/components/ui/card";
import { Input } from "~/components/ui/input";
import { Button } from "~/components/ui/button";
import { Label } from "~/components/ui/label";
import { toast } from "sonner";
import { Logo } from "~/components/branding/logo";
import { User, Mail, Lock, ArrowRight } from "lucide-react";
import { LegalModal } from "~/components/ui/legal-modal";
import { Mail, Lock, ArrowRight, User, Clock, Rocket, Zap } from "lucide-react";
function RegisterForm() {
const router = useRouter();
const searchParams = useSearchParams();
const callbackUrl = searchParams.get("callbackUrl") ?? "/dashboard";
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [email, setEmail] = useState("");
@@ -24,177 +23,238 @@ function RegisterForm() {
async function handleRegister(e: React.FormEvent) {
e.preventDefault();
setLoading(true);
const res = await fetch("/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
firstName,
lastName,
name: `${firstName} ${lastName}`,
email,
password,
}),
});
setLoading(false);
if (res.ok) {
toast.success("Account created successfully! Please sign in.");
const signInUrl =
callbackUrl !== "/dashboard"
? `/auth/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
: "/auth/signin";
router.push(signInUrl);
router.push("/auth/signin");
} else {
const error = await res.text();
toast.error(error || "Failed to create account");
const data = (await res.json()) as { error?: string };
toast.error(data.error ?? "Registration failed");
}
}
return (
<div className="floating-orbs flex min-h-screen items-center justify-center p-4">
<div className="w-full max-w-md space-y-8">
{/* Logo and Welcome */}
<div className="space-y-4 text-center">
<Logo size="lg" className="mx-auto" />
<div>
<h1 className="text-foreground text-2xl font-bold">
Join beenvoice
</h1>
<p className="text-muted-foreground mt-2">
Create your account to get started
</p>
</div>
</div>
<div className="bg-background flex min-h-screen items-center justify-center">
<Card className="mx-auto h-screen w-full overflow-hidden border-0 shadow-none md:h-auto md:max-w-6xl md:border md:shadow-lg">
<CardContent className="grid h-full p-0 md:grid-cols-2">
{/* Hero Section - Hidden on mobile */}
<div className="bg-muted relative hidden md:flex md:flex-col md:justify-center md:p-12">
<div className="space-y-8">
<div className="space-y-4">
<div className="flex items-center space-x-2">
<Logo size="xl" />
</div>
<div className="space-y-3">
<h1 className="text-3xl font-bold lg:text-4xl">
Start your
<span className="text-primary"> invoicing journey</span>
</h1>
<p className="text-muted-foreground text-lg">
Join thousands of freelancers and small businesses who trust
beenvoice to manage their invoicing and get paid faster.
</p>
</div>
</div>
{/* Registration Form */}
<Card className="card-primary">
<CardHeader className="space-y-1">
<CardTitle className="text-center text-xl">
Create Account
</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleRegister} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="firstName">First Name</Label>
<div className="relative">
<User className="form-icon-left" />
<Input
id="firstName"
type="text"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
required
autoFocus
className="form-input-with-icon"
placeholder="First name"
/>
<div className="grid gap-4">
<div className="flex items-start space-x-4">
<div className="bg-primary/10 rounded-lg p-2">
<Rocket className="text-primary h-5 w-5" />
</div>
<div className="space-y-1">
<h3 className="font-semibold">Quick Setup</h3>
<p className="text-muted-foreground text-sm">
Get started in minutes with our intuitive setup wizard
</p>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="lastName">Last Name</Label>
<div className="relative">
<User className="form-icon-left" />
<Input
id="lastName"
type="text"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
required
className="form-input-with-icon"
placeholder="Last name"
/>
<div className="flex items-start space-x-4">
<div className="bg-primary/10 rounded-lg p-2">
<Zap className="text-primary h-5 w-5" />
</div>
<div className="space-y-1">
<h3 className="font-semibold">Fast Payments</h3>
<p className="text-muted-foreground text-sm">
Professional invoices that get you paid 3x faster
</p>
</div>
</div>
<div className="flex items-start space-x-4">
<div className="bg-primary/10 rounded-lg p-2">
<Clock className="text-primary h-5 w-5" />
</div>
<div className="space-y-1">
<h3 className="font-semibold">Time Tracking</h3>
<p className="text-muted-foreground text-sm">
Track time and convert it to accurate invoices instantly
</p>
</div>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<div className="relative">
<Mail className="form-icon-left" />
<Input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="form-input-with-icon"
placeholder="Enter your email"
/>
</div>
</div>
</div>
{/* Sign Up Form */}
<div className="flex flex-col justify-center p-6 md:p-12">
<div className="mx-auto w-full max-w-sm space-y-6">
{/* Mobile Logo */}
<div className="flex justify-center md:hidden">
<Logo size="lg" />
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<div className="relative">
<Lock className="form-icon-left" />
<Input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
minLength={6}
className="form-input-with-icon"
placeholder="Create a password"
/>
</div>
<p className="text-muted-foreground text-xs">
Must be at least 6 characters
<div className="space-y-2 text-center md:text-left">
<h1 className="text-2xl font-bold">Create your account</h1>
<p className="text-muted-foreground">
Supercharge your invoicing today
</p>
</div>
<Button type="submit" className="w-full" disabled={loading}>
{loading ? (
"Creating account..."
) : (
<>
Create Account
<ArrowRight className="ml-2 h-4 w-4" />
</>
)}
</Button>
</form>
<div className="mt-6 text-center text-sm">
<span className="text-muted-foreground">
Already have an account?{" "}
</span>
<Link href="/auth/signin" className="nav-link-brand">
Sign in here
</Link>
</div>
</CardContent>
</Card>
{/* Features */}
<div className="space-y-4 text-center">
<p className="welcome-description">Start invoicing like a pro</p>
<div className="welcome-feature-list">
<span> Free to start</span>
<span> No credit card</span>
<span> Cancel anytime</span>
<form onSubmit={handleRegister} className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="firstName">First Name</Label>
<div className="relative">
<User className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 z-10 h-4 w-4 -translate-y-1/2" />
<Input
id="firstName"
type="text"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
required
autoFocus
className="h-11 pl-10"
placeholder="John"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="lastName">Last Name</Label>
<div className="relative">
<User className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 z-10 h-4 w-4 -translate-y-1/2" />
<Input
id="lastName"
type="text"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
required
className="h-11 pl-10"
placeholder="Doe"
/>
</div>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email Address</Label>
<div className="relative">
<Mail className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 z-10 h-4 w-4 -translate-y-1/2" />
<Input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="h-11 pl-10"
placeholder="john@example.com"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<div className="relative">
<Lock className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 z-10 h-4 w-4 -translate-y-1/2" />
<Input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="h-11 pl-10"
placeholder="Create a strong password"
/>
</div>
<p className="text-muted-foreground text-xs">
Must be at least 8 characters long
</p>
</div>
<Button
type="submit"
className="h-11 w-full"
disabled={loading}
>
{loading ? (
<div className="flex items-center space-x-2">
<div className="border-primary-foreground/30 border-t-primary-foreground h-4 w-4 animate-spin rounded-full border-2"></div>
<span>Creating account...</span>
</div>
) : (
<div className="flex items-center space-x-2">
<span>Create Account</span>
<ArrowRight className="h-4 w-4" />
</div>
)}
</Button>
</form>
<div className="text-center text-sm">
Already have an account?{" "}
<a
href="/auth/signin"
className="text-primary font-medium hover:underline"
>
Sign in
</a>
</div>
<div className="text-muted-foreground text-center text-xs leading-relaxed">
By creating an account, you agree to our{" "}
<LegalModal
type="terms"
trigger={
<span className="text-primary inline cursor-pointer hover:underline">
Terms of Service
</span>
}
/>{" "}
and{" "}
<LegalModal
type="privacy"
trigger={
<span className="text-primary inline cursor-pointer hover:underline">
Privacy Policy
</span>
}
/>
.
</div>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
);
}
export default function RegisterPage() {
return (
<Suspense
fallback={
<div className="floating-orbs flex min-h-screen items-center justify-center p-4">
<div className="w-full max-w-md space-y-8">
<div className="space-y-4 text-center">
<Logo size="lg" className="mx-auto" />
<div>
<h1 className="text-foreground text-2xl font-bold">
Join beenvoice
</h1>
<p className="text-muted-foreground mt-2">Loading...</p>
</div>
</div>
</div>
</div>
}
>
<Suspense fallback={<div>Loading...</div>}>
<RegisterForm />
</Suspense>
);

View File

@@ -1,16 +1,23 @@
"use client";
import Link from "next/link";
import { useState, Suspense } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { signIn } from "next-auth/react";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Card, CardContent } from "~/components/ui/card";
import { Input } from "~/components/ui/input";
import { Button } from "~/components/ui/button";
import { Label } from "~/components/ui/label";
import { toast } from "sonner";
import { Logo } from "~/components/branding/logo";
import { Mail, Lock, ArrowRight } from "lucide-react";
import { LegalModal } from "~/components/ui/legal-modal";
import {
Mail,
Lock,
ArrowRight,
Users,
FileText,
TrendingUp,
} from "lucide-react";
function SignInForm() {
const router = useRouter();
@@ -42,114 +49,184 @@ function SignInForm() {
}
return (
<div className="floating-orbs flex min-h-screen items-center justify-center p-4">
<div className="w-full max-w-md space-y-8">
{/* Logo and Welcome */}
<div className="space-y-4 text-center">
<Logo size="lg" className="mx-auto" />
<div>
<h1 className="text-foreground text-2xl font-bold">Welcome back</h1>
<p className="text-muted-foreground mt-2">
Sign in to your beenvoice account
</p>
</div>
</div>
<div className="bg-background flex min-h-screen items-center justify-center">
<Card className="mx-auto h-screen w-full overflow-hidden border-0 shadow-none md:h-auto md:max-w-6xl md:border md:shadow-lg">
<CardContent className="grid h-full p-0 md:grid-cols-2">
{/* Hero Section - Hidden on mobile */}
<div className="bg-muted relative hidden md:flex md:flex-col md:justify-center md:p-12">
<div className="space-y-8">
<div className="space-y-4">
<Logo size="xl" />
<div className="space-y-3">
<h1 className="text-3xl font-bold lg:text-4xl">
Welcome back to your
<span className="text-primary"> invoicing workspace</span>
</h1>
<p className="text-muted-foreground text-lg">
Continue managing your clients and creating professional
invoices that get you paid faster.
</p>
</div>
</div>
{/* Sign In Form */}
<Card className="card-primary">
<CardHeader className="space-y-1">
<CardTitle className="text-center text-xl">Sign In</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSignIn} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<div className="relative">
<Mail className="form-icon-left" />
<Input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
autoFocus
className="form-input-with-icon"
placeholder="Enter your email"
/>
<div className="grid gap-4">
<div className="flex items-start space-x-4">
<div className="bg-primary/10 rounded-lg p-2">
<Users className="text-primary h-5 w-5" />
</div>
<div className="space-y-1">
<h3 className="font-semibold">Client Management</h3>
<p className="text-muted-foreground text-sm">
Organize and track all your clients in one place
</p>
</div>
</div>
<div className="flex items-start space-x-4">
<div className="bg-primary/10 rounded-lg p-2">
<FileText className="text-primary h-5 w-5" />
</div>
<div className="space-y-1">
<h3 className="font-semibold">Professional Invoices</h3>
<p className="text-muted-foreground text-sm">
Beautiful templates that get you paid faster
</p>
</div>
</div>
<div className="flex items-start space-x-4">
<div className="bg-primary/10 rounded-lg p-2">
<TrendingUp className="text-primary h-5 w-5" />
</div>
<div className="space-y-1">
<h3 className="font-semibold">Payment Tracking</h3>
<p className="text-muted-foreground text-sm">
Monitor your income with real-time insights
</p>
</div>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<div className="relative">
<Lock className="form-icon-left" />
<Input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="form-input-with-icon"
placeholder="Enter your password"
/>
</div>
</div>
<Button type="submit" className="w-full" disabled={loading}>
{loading ? (
"Signing in..."
) : (
<>
Sign In
<ArrowRight className="ml-2 h-4 w-4" />
</>
)}
</Button>
</form>
<div className="mt-6 text-center text-sm">
<span className="text-muted-foreground">
Don&apos;t have an account?{" "}
</span>
<Link href="/auth/register" className="nav-link-brand">
Create one now
</Link>
</div>
</CardContent>
</Card>
{/* Features */}
<div className="space-y-4 text-center">
<p className="welcome-description">
Simple invoicing for freelancers and small businesses
</p>
<div className="welcome-feature-list">
<span> Easy client management</span>
<span> Professional invoices</span>
<span> Payment tracking</span>
</div>
</div>
</div>
{/* Sign In Form */}
<div className="flex flex-col justify-center p-6 md:p-12">
<div className="mx-auto w-full max-w-sm space-y-6">
{/* Mobile Logo */}
<div className="flex justify-center md:hidden">
<Logo size="lg" />
</div>
<div className="space-y-2 text-center md:text-left">
<h1 className="text-2xl font-bold">Sign In</h1>
<p className="text-muted-foreground">
Enter your credentials to access your account
</p>
</div>
<form onSubmit={handleSignIn} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email Address</Label>
<div className="relative">
<Mail className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 z-10 h-4 w-4 -translate-y-1/2" />
<Input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
autoFocus
className="h-11 pl-10"
placeholder="m@example.com"
/>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="password">Password</Label>
<a
href="/auth/forgot-password"
className="text-primary text-sm hover:underline"
>
Forgot password?
</a>
</div>
<div className="relative">
<Lock className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 z-10 h-4 w-4 -translate-y-1/2" />
<Input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="h-11 pl-10"
placeholder="Enter your password"
/>
</div>
</div>
<Button
type="submit"
className="h-11 w-full"
disabled={loading}
>
{loading ? (
<div className="flex items-center space-x-2">
<div className="border-primary-foreground/30 border-t-primary-foreground h-4 w-4 animate-spin rounded-full border-2"></div>
<span>Signing in...</span>
</div>
) : (
<div className="flex items-center space-x-2">
<span>Sign In</span>
<ArrowRight className="h-4 w-4" />
</div>
)}
</Button>
</form>
<div className="text-center text-sm">
Don&apos;t have an account?{" "}
<a
href="/auth/register"
className="text-primary font-medium hover:underline"
>
Sign up
</a>
</div>
<div className="text-muted-foreground text-center text-xs leading-relaxed">
By signing in, you agree to our{" "}
<LegalModal
type="terms"
trigger={
<span className="text-primary inline cursor-pointer hover:underline">
Terms of Service
</span>
}
/>{" "}
and{" "}
<LegalModal
type="privacy"
trigger={
<span className="text-primary inline cursor-pointer hover:underline">
Privacy Policy
</span>
}
/>
.
</div>
</div>
</div>
</CardContent>
</Card>
</div>
);
}
export default function SignInPage() {
return (
<Suspense
fallback={
<div className="floating-orbs flex min-h-screen items-center justify-center p-4">
<div className="w-full max-w-md space-y-8">
<div className="space-y-4 text-center">
<Logo size="lg" className="mx-auto" />
<div>
<h1 className="text-foreground text-2xl font-bold">
Welcome back
</h1>
<p className="text-muted-foreground mt-2">Loading...</p>
</div>
</div>
</div>
</div>
}
>
<Suspense fallback={<div>Loading...</div>}>
<SignInForm />
</Suspense>
);

View File

@@ -229,7 +229,7 @@ export function StatusManager({
{/* Overdue Warning */}
{isOverdue && (
<div className="flex items-center gap-2 rounded-lg bg-red-50 p-3 text-red-800 dark:bg-red-900/20 dark:text-red-300">
<div className="bg-destructive/10 text-destructive flex items-center gap-2 p-3">
<AlertCircle className="h-4 w-4" />
<span className="text-sm font-medium">
{daysPastDue} day{daysPastDue !== 1 ? "s" : ""} overdue
@@ -325,7 +325,7 @@ export function StatusManager({
{/* No Email Warning */}
{!clientEmail && effectiveStatus !== "paid" && (
<div className="rounded-lg bg-amber-50 p-3 text-amber-800 dark:bg-amber-900/20 dark:text-amber-300">
<div className="bg-muted text-muted-foreground p-3">
<div className="flex items-center gap-2">
<AlertCircle className="h-4 w-4" />
<span className="text-sm font-medium">

View File

@@ -55,7 +55,7 @@ export default async function BusinessDetailPage({
<span>Back to Businesses</span>
</Link>
</Button>
<Button asChild className="btn-brand-primary shadow-md">
<Button asChild variant="default" className="shadow-md">
<Link href={`/dashboard/businesses/${business.id}/edit`}>
<Edit className="mr-2 h-4 w-4" />
<span>Edit Business</span>
@@ -66,11 +66,11 @@ export default async function BusinessDetailPage({
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
{/* Business Information Card */}
<div className="lg:col-span-2">
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<div className="bg-blue-subtle rounded-lg p-2">
<Building className="text-icon-blue h-5 w-5" />
<div className="bg-primary/10 p-2">
<Building className="text-primary h-5 w-5" />
</div>
<span>Business Information</span>
</CardTitle>
@@ -84,8 +84,8 @@ export default async function BusinessDetailPage({
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{business.email && (
<div className="flex items-center space-x-3">
<div className="bg-green-subtle rounded-lg p-2">
<Mail className="text-icon-green h-4 w-4" />
<div className="bg-primary/10 p-2">
<Mail className="text-primary h-4 w-4" />
</div>
<div>
<p className="text-muted-foreground text-sm font-medium">
@@ -100,8 +100,8 @@ export default async function BusinessDetailPage({
{business.phone && (
<div className="flex items-center space-x-3">
<div className="bg-green-subtle rounded-lg p-2">
<Phone className="text-icon-green h-4 w-4" />
<div className="bg-primary/10 p-2">
<Phone className="text-primary h-4 w-4" />
</div>
<div>
<p className="text-muted-foreground text-sm font-medium">
@@ -116,8 +116,8 @@ export default async function BusinessDetailPage({
{business.website && (
<div className="flex items-center space-x-3">
<div className="bg-green-subtle rounded-lg p-2">
<Globe className="text-icon-green h-4 w-4" />
<div className="bg-primary/10 p-2">
<Globe className="text-primary h-4 w-4" />
</div>
<div>
<p className="text-muted-foreground text-sm font-medium">
@@ -137,8 +137,8 @@ export default async function BusinessDetailPage({
{business.taxId && (
<div className="flex items-center space-x-3">
<div className="bg-green-subtle rounded-lg p-2">
<Hash className="text-icon-green h-4 w-4" />
<div className="bg-primary/10 p-2">
<Hash className="text-primary h-4 w-4" />
</div>
<div>
<p className="text-muted-foreground text-sm font-medium">
@@ -162,8 +162,8 @@ export default async function BusinessDetailPage({
Business Address
</h3>
<div className="flex items-start space-x-3">
<div className="bg-green-subtle rounded-lg p-2">
<MapPin className="text-icon-green h-4 w-4" />
<div className="bg-primary/10 p-2">
<MapPin className="text-primary h-4 w-4" />
</div>
<div className="space-y-1 text-sm">
{business.addressLine1 && (
@@ -205,8 +205,8 @@ export default async function BusinessDetailPage({
<h3 className="mb-4 text-lg font-semibold">Business Details</h3>
<div className="space-y-4">
<div className="flex items-center space-x-3">
<div className="bg-green-subtle rounded-lg p-2">
<Calendar className="text-icon-green h-4 w-4" />
<div className="bg-primary/10 p-2">
<Calendar className="text-primary h-4 w-4" />
</div>
<div>
<p className="text-muted-foreground text-sm font-medium">
@@ -221,8 +221,8 @@ export default async function BusinessDetailPage({
{/* Default Business Badge */}
{business.isDefault && (
<div className="flex items-center space-x-3">
<div className="bg-green-subtle rounded-lg p-2">
<Building className="text-icon-green h-4 w-4" />
<div className="bg-primary/10 p-2">
<Building className="text-primary h-4 w-4" />
</div>
<div>
<p className="text-muted-foreground text-sm font-medium">
@@ -230,7 +230,7 @@ export default async function BusinessDetailPage({
</p>
<Badge
variant="default"
className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
className="bg-primary/10 text-primary"
>
Default Business
</Badge>
@@ -245,11 +245,11 @@ export default async function BusinessDetailPage({
{/* Settings & Actions Card */}
<div className="space-y-6">
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<div className="bg-blue-subtle rounded-lg p-2">
<Building className="text-icon-blue h-5 w-5" />
<div className="bg-primary/10 p-2">
<Building className="text-primary h-5 w-5" />
</div>
<span>Quick Actions</span>
</CardTitle>
@@ -281,7 +281,7 @@ export default async function BusinessDetailPage({
</Card>
{/* Information Card */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="text-lg">About This Business</CardTitle>
</CardHeader>
@@ -292,7 +292,7 @@ export default async function BusinessDetailPage({
represents your company information to clients.
</p>
{business.isDefault && (
<p className="text-green-600 dark:text-green-400">
<p className="text-primary">
This is your default business and will be automatically
selected when creating new invoices.
</p>

View File

@@ -91,8 +91,8 @@ export function BusinessesDataTable({ businesses }: BusinessesDataTableProps) {
const business = row.original;
return (
<div className="flex items-center gap-3">
<div className="bg-blue-subtle hidden rounded-lg p-2 sm:flex">
<Building className="text-icon-blue h-4 w-4" />
<div className="bg-primary/10 hidden p-2 sm:flex">
<Building className="text-primary h-4 w-4" />
</div>
<div className="min-w-0">
<p className="truncate font-medium">{business.name}</p>

View File

@@ -22,7 +22,7 @@ export default async function BusinessesPage() {
description="Manage your businesses and their information"
variant="gradient"
>
<Button asChild className="btn-brand-primary shadow-md">
<Button asChild variant="default" className="shadow-md">
<Link href="/dashboard/businesses/new">
<Plus className="mr-2 h-5 w-5" />
<span>Add Business</span>

View File

@@ -69,7 +69,7 @@ export default async function ClientDetailPage({
<span>Back to Clients</span>
</Link>
</Button>
<Button asChild className="btn-brand-primary shadow-md">
<Button asChild variant="default" className="shadow-md">
<Link href={`/dashboard/clients/${client.id}/edit`}>
<Edit className="mr-2 h-4 w-4" />
<span>Edit Client</span>
@@ -80,11 +80,11 @@ export default async function ClientDetailPage({
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
{/* Client Information Card */}
<div className="lg:col-span-2">
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<div className="bg-blue-subtle rounded-lg p-2">
<Building className="text-icon-blue h-5 w-5" />
<div className="bg-primary/10 p-2">
<Building className="text-primary h-5 w-5" />
</div>
<span>Contact Information</span>
</CardTitle>
@@ -94,8 +94,8 @@ export default async function ClientDetailPage({
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{client.email && (
<div className="flex items-center space-x-3">
<div className="bg-green-subtle rounded-lg p-2">
<Mail className="text-icon-green h-4 w-4" />
<div className="bg-primary/10 p-2">
<Mail className="text-primary h-4 w-4" />
</div>
<div>
<p className="text-muted-foreground text-sm font-medium">
@@ -108,8 +108,8 @@ export default async function ClientDetailPage({
{client.phone && (
<div className="flex items-center space-x-3">
<div className="bg-green-subtle rounded-lg p-2">
<Phone className="text-icon-green h-4 w-4" />
<div className="bg-primary/10 p-2">
<Phone className="text-primary h-4 w-4" />
</div>
<div>
<p className="text-muted-foreground text-sm font-medium">
@@ -126,8 +126,8 @@ export default async function ClientDetailPage({
<div>
<h3 className="mb-4 text-lg font-semibold">Client Address</h3>
<div className="flex items-start space-x-3">
<div className="bg-green-subtle rounded-lg p-2">
<MapPin className="text-icon-green h-4 w-4" />
<div className="bg-primary/10 p-2">
<MapPin className="text-primary h-4 w-4" />
</div>
<div className="space-y-1 text-sm">
{client.addressLine1 && (
@@ -155,8 +155,8 @@ export default async function ClientDetailPage({
<div>
<h3 className="mb-4 text-lg font-semibold">Client Details</h3>
<div className="flex items-center space-x-3">
<div className="bg-green-subtle rounded-lg p-2">
<Calendar className="text-icon-green h-4 w-4" />
<div className="bg-primary/10 p-2">
<Calendar className="text-primary h-4 w-4" />
</div>
<div>
<p className="text-muted-foreground text-sm font-medium">
@@ -174,11 +174,11 @@ export default async function ClientDetailPage({
{/* Stats Card */}
<div className="space-y-6">
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<div className="bg-blue-subtle rounded-lg p-2">
<DollarSign className="text-icon-blue h-5 w-5" />
<div className="bg-primary/10 p-2">
<DollarSign className="text-primary h-5 w-5" />
</div>
<span>Invoice Summary</span>
</CardTitle>
@@ -213,8 +213,8 @@ export default async function ClientDetailPage({
<Card className="">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<div className="bg-blue-subtle rounded-lg p-2">
<DollarSign className="text-icon-blue h-5 w-5" />
<div className="bg-primary/10 p-2">
<DollarSign className="text-primary h-5 w-5" />
</div>
<span>Recent Invoices</span>
</CardTitle>
@@ -224,7 +224,7 @@ export default async function ClientDetailPage({
{client.invoices.slice(0, 3).map((invoice) => (
<div
key={invoice.id}
className="card-secondary flex items-center justify-between rounded-lg border p-3 transition-colors hover:bg-gray-200/70 dark:hover:bg-gray-700/60"
className="card-secondary hover:bg-muted/50 flex items-center justify-between border p-3 transition-colors"
>
<div>
<p className="text-foreground font-medium">

View File

@@ -90,7 +90,7 @@ export function ClientsDataTable({
const client = row.original;
return (
<div className="flex items-center gap-3">
<div className="bg-status-info-muted hidden rounded-lg p-2 sm:flex">
<div className="bg-status-info-muted hidden p-2 sm:flex">
<UserPlus className="text-status-info h-4 w-4" />
</div>
<div className="min-w-0">

View File

@@ -13,7 +13,7 @@ export default async function ClientsPage() {
description="Manage your clients and their information."
variant="gradient"
>
<Button asChild className="btn-brand-primary shadow-md">
<Button asChild variant="default" className="shadow-md">
<Link href="/dashboard/clients/new">
<Plus className="mr-2 h-5 w-5" />
<span>Add Client</span>

View File

@@ -22,7 +22,7 @@ export function InvoiceDetailsSkeleton() {
{/* Left Column */}
<div className="space-y-6 lg:col-span-2">
{/* Invoice Header Skeleton */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardContent className="p-4 sm:p-6">
<div className="space-y-4">
<div className="flex items-start justify-between gap-6">
@@ -48,7 +48,7 @@ export function InvoiceDetailsSkeleton() {
{/* Client & Business Info */}
<div className="grid gap-4 sm:grid-cols-2">
{Array.from({ length: 2 }).map((_, i) => (
<Card key={i} className="card-primary">
<Card key={i} className="bg-card border-border border">
<CardHeader className="pb-3">
<div className="flex items-center gap-2">
<Skeleton className="bg-muted/30 h-4 w-4 sm:h-5 sm:w-5" />
@@ -60,7 +60,7 @@ export function InvoiceDetailsSkeleton() {
<div className="space-y-3">
{Array.from({ length: 3 }).map((_, j) => (
<div key={j} className="flex items-center gap-3">
<Skeleton className="bg-muted/30 h-8 w-8 rounded-lg" />
<Skeleton className="bg-muted/30 h-8 w-8 " />
<Skeleton className="bg-muted/30 h-4 w-28" />
</div>
))}
@@ -71,7 +71,7 @@ export function InvoiceDetailsSkeleton() {
</div>
{/* Invoice Items Skeleton */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<div className="flex items-center gap-2">
<Skeleton className="bg-muted/30 h-4 w-4 sm:h-5 sm:w-5" />
@@ -80,7 +80,7 @@ export function InvoiceDetailsSkeleton() {
</CardHeader>
<CardContent className="space-y-4">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="space-y-3 rounded-lg border p-4">
<div key={i} className="space-y-3 border p-4">
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 flex-1">
<Skeleton className="bg-muted/30 mb-2 h-4 w-full sm:h-5 sm:w-3/4" />
@@ -98,7 +98,7 @@ export function InvoiceDetailsSkeleton() {
))}
{/* Totals */}
<div className="bg-muted/30 rounded-lg p-4">
<div className="bg-muted/30 p-4">
<div className="space-y-3">
<div className="flex justify-between">
<Skeleton className="bg-muted/30 h-4 w-16" />
@@ -119,7 +119,7 @@ export function InvoiceDetailsSkeleton() {
</Card>
{/* Notes */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<Skeleton className="bg-muted/30 h-6 w-16" />
</CardHeader>
@@ -135,7 +135,7 @@ export function InvoiceDetailsSkeleton() {
{/* Right Column - Actions */}
<div className="space-y-6">
<Card className="card-primary sticky top-6">
<Card className="bg-card border-border border sticky top-6">
<CardHeader>
<div className="flex items-center gap-2">
<Skeleton className="bg-muted/30 h-5 w-5" />

View File

@@ -66,7 +66,7 @@ const columns: ColumnDef<InvoiceItem>[] = [
accessorKey: "amount",
header: "Amount",
cell: ({ row }) => (
<div className="text-icon-emerald text-right font-medium">
<div className="text-primary text-right font-medium">
{formatCurrency(row.getValue("amount"))}
</div>
),

View File

@@ -1,12 +1,511 @@
"use client";
import { useParams } from "next/navigation";
import InvoiceForm from "~/components/forms/invoice-form";
import { DollarSign, Edit, Loader2, Trash2 } from "lucide-react";
import Link from "next/link";
import { notFound, useParams, useRouter } from "next/navigation";
import { useState } from "react";
import { toast } from "sonner";
import { StatusBadge, type StatusType } from "~/components/data/status-badge";
import { PageHeader } from "~/components/layout/page-header";
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
import { Separator } from "~/components/ui/separator";
import {
getEffectiveInvoiceStatus,
isInvoiceOverdue,
} from "~/lib/invoice-status";
import { api } from "~/trpc/react";
import type { StoredInvoiceStatus } from "~/types/invoice";
import { InvoiceDetailsSkeleton } from "./_components/invoice-details-skeleton";
import { PDFDownloadButton } from "./_components/pdf-download-button";
import { EnhancedSendInvoiceButton } from "~/components/forms/enhanced-send-invoice-button";
export default function InvoiceFormPage() {
import {
AlertTriangle,
Building,
Check,
FileText,
Mail,
MapPin,
Phone,
User,
} from "lucide-react";
function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
const router = useRouter();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const { data: invoice, isLoading } = api.invoices.getById.useQuery({
id: invoiceId,
});
const utils = api.useUtils();
const deleteInvoice = api.invoices.delete.useMutation({
onSuccess: () => {
toast.success("Invoice deleted successfully");
router.push("/dashboard/invoices");
},
onError: (error) => {
toast.error(error.message ?? "Failed to delete invoice");
},
});
const updateStatus = api.invoices.updateStatus.useMutation({
onSuccess: (data) => {
toast.success(data.message);
void utils.invoices.getById.invalidate({ id: invoiceId });
},
onError: (error) => {
toast.error(error.message ?? "Failed to update invoice status");
},
});
const handleDelete = () => {
setDeleteDialogOpen(true);
};
const handleMarkAsPaid = () => {
updateStatus.mutate({
id: invoiceId,
status: "paid" as StoredInvoiceStatus,
});
};
const confirmDelete = () => {
deleteInvoice.mutate({ id: invoiceId });
};
if (isLoading) {
return <InvoiceDetailsSkeleton />;
}
if (!invoice) {
notFound();
}
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "short",
day: "numeric",
}).format(new Date(date));
};
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount);
};
const subtotal = invoice.items.reduce((sum, item) => sum + item.amount, 0);
const taxAmount = (subtotal * invoice.taxRate) / 100;
const total = subtotal + taxAmount;
const effectiveStatus = getEffectiveInvoiceStatus(
invoice.status as StoredInvoiceStatus,
invoice.dueDate,
);
const isOverdue = isInvoiceOverdue(
invoice.status as StoredInvoiceStatus,
invoice.dueDate,
);
const getStatusType = (): StatusType => {
return effectiveStatus as StatusType;
};
return (
<div className="space-y-6 pb-24">
<PageHeader
title="Invoice Details"
description="View and manage invoice information"
variant="gradient"
>
<PDFDownloadButton invoiceId={invoice.id} variant="outline" />
<Button asChild variant="default">
<Link href={`/dashboard/invoices/${invoice.id}`}>
<Edit className="h-5 w-5" />
<span>Edit</span>
</Link>
</Button>
</PageHeader>
{/* Content */}
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
{/* Left Column */}
<div className="space-y-6 lg:col-span-2">
{/* Invoice Header */}
<Card>
<CardContent className="p-4 sm:p-6">
<div className="space-y-4">
<div className="flex items-start justify-between gap-6">
<div className="min-w-0 flex-1 space-y-2">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
<h2 className="text-foreground truncate text-2xl font-bold">
{invoice.invoiceNumber}
</h2>
<StatusBadge status={getStatusType()} />
</div>
<div className="text-muted-foreground space-y-1 text-sm sm:space-y-0">
<div className="sm:inline">
Issued {formatDate(invoice.issueDate)}
</div>
<div className="sm:inline sm:before:content-['_•_']">
Due {formatDate(invoice.dueDate)}
</div>
</div>
</div>
<div className="flex-shrink-0 text-right">
<p className="text-muted-foreground text-sm">
Total Amount
</p>
<p className="text-primary text-3xl font-bold">
{formatCurrency(total)}
</p>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Overdue Alert */}
{isOverdue && (
<Card className="border-destructive/20 bg-destructive/5">
<CardContent className="p-4">
<div className="text-destructive flex items-center gap-3">
<AlertTriangle className="h-5 w-5 flex-shrink-0" />
<div>
<p className="font-medium">Invoice Overdue</p>
<p className="text-sm">
{Math.ceil(
(new Date().getTime() -
new Date(invoice.dueDate).getTime()) /
(1000 * 60 * 60 * 24),
)}{" "}
days past due date
</p>
</div>
</div>
</CardContent>
</Card>
)}
{/* Client & Business Info */}
<div className="grid gap-4 sm:grid-cols-2">
{/* Client Information */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2">
<User className="h-5 w-5" />
Bill To
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<h3 className="text-foreground text-xl font-semibold">
{invoice.client.name}
</h3>
</div>
<div className="space-y-3">
{invoice.client.email && (
<div className="flex items-center gap-3">
<div className="bg-primary/10 p-2">
<Mail className="text-primary h-4 w-4" />
</div>
<span className="text-sm break-all">
{invoice.client.email}
</span>
</div>
)}
{invoice.client.phone && (
<div className="flex items-center gap-3">
<div className="bg-primary/10 p-2">
<Phone className="text-primary h-4 w-4" />
</div>
<span className="text-sm">{invoice.client.phone}</span>
</div>
)}
{(invoice.client.addressLine1 ?? invoice.client.city) && (
<div className="flex items-start gap-3">
<div className="bg-primary/10 p-2">
<MapPin className="text-primary h-4 w-4" />
</div>
<div className="space-y-1 text-sm">
{invoice.client.addressLine1 && (
<div>{invoice.client.addressLine1}</div>
)}
{invoice.client.addressLine2 && (
<div>{invoice.client.addressLine2}</div>
)}
{(invoice.client.city ??
invoice.client.state ??
invoice.client.postalCode) && (
<div>
{[
invoice.client.city,
invoice.client.state,
invoice.client.postalCode,
]
.filter(Boolean)
.join(", ")}
</div>
)}
{invoice.client.country && (
<div>{invoice.client.country}</div>
)}
</div>
</div>
)}
</div>
</CardContent>
</Card>
{/* Business Information */}
{invoice.business && (
<Card>
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2">
<Building className="h-5 w-5" />
From
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<h3 className="text-foreground text-xl font-semibold">
{invoice.business.name}
</h3>
</div>
<div className="space-y-3">
{invoice.business.email && (
<div className="flex items-center gap-3">
<div className="bg-primary/10 p-2">
<Mail className="text-primary h-4 w-4" />
</div>
<span className="text-sm break-all">
{invoice.business.email}
</span>
</div>
)}
{invoice.business.phone && (
<div className="flex items-center gap-3">
<div className="bg-primary/10 p-2">
<Phone className="text-primary h-4 w-4" />
</div>
<span className="text-sm">
{invoice.business.phone}
</span>
</div>
)}
</div>
</CardContent>
</Card>
)}
</div>
{/* Invoice Items */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileText className="h-5 w-5" />
Invoice Items
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{invoice.items.map((item) => (
<Card key={item.id} className="card-secondary">
<CardContent className="py-2">
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 flex-1">
<p className="text-foreground mb-2 text-base font-medium">
{item.description}
</p>
<div className="text-muted-foreground text-sm">
<span className="inline whitespace-nowrap">
{formatDate(item.date).replace(/ /g, "\u00A0")}
</span>
<span className="inline whitespace-nowrap before:mx-2 before:content-['_|_']">
{item.hours.toString().replace(/ /g, "\u00A0")}
&nbsp;hours
</span>
<span className="inline whitespace-nowrap before:mx-2 before:content-['_|_']">
@&nbsp;${item.rate}/hr
</span>
</div>
</div>
<div className="flex-shrink-0 text-right">
<p className="text-primary text-lg font-semibold">
{formatCurrency(item.amount)}
</p>
</div>
</div>
</CardContent>
</Card>
))}
{/* Totals */}
<div className="bg-muted/30 p-4">
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-muted-foreground">Subtotal:</span>
<span className="font-medium">
{formatCurrency(subtotal)}
</span>
</div>
{invoice.taxRate > 0 && (
<div className="flex justify-between">
<span className="text-muted-foreground">
Tax ({invoice.taxRate}%):
</span>
<span className="font-medium">
{formatCurrency(taxAmount)}
</span>
</div>
)}
<Separator />
<div className="flex justify-between text-lg font-bold">
<span>Total:</span>
<span className="text-primary">
{formatCurrency(total)}
</span>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Notes */}
{invoice.notes && (
<Card>
<CardHeader>
<CardTitle>Notes</CardTitle>
</CardHeader>
<CardContent>
<p className="text-foreground whitespace-pre-wrap">
{invoice.notes}
</p>
</CardContent>
</Card>
)}
</div>
{/* Right Column - Actions */}
<div className="space-y-6">
<Card className="sticky top-6">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Check className="h-5 w-5" />
Actions
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<Button asChild variant="outline" className="w-full">
<Link href={`/dashboard/invoices/${invoice.id}/edit`}>
<Edit className="mr-2 h-4 w-4" />
Edit Invoice
</Link>
</Button>
{invoice.items && invoice.client && (
<PDFDownloadButton invoiceId={invoice.id} className="w-full" />
)}
{/* Send Invoice Button - Show for draft, sent, and overdue */}
{effectiveStatus === "draft" && (
<EnhancedSendInvoiceButton
invoiceId={invoice.id}
className="w-full"
/>
)}
{(effectiveStatus === "sent" ||
effectiveStatus === "overdue") && (
<EnhancedSendInvoiceButton
invoiceId={invoice.id}
className="w-full"
showResend={true}
/>
)}
{/* Manual Status Updates */}
{(effectiveStatus === "sent" ||
effectiveStatus === "overdue") && (
<Button
onClick={handleMarkAsPaid}
disabled={updateStatus.isPending}
className="bg-primary text-primary-foreground hover:bg-primary/90 w-full"
>
{updateStatus.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<DollarSign className="mr-2 h-4 w-4" />
)}
Mark as Paid
</Button>
)}
<Button
variant="outline"
onClick={handleDelete}
disabled={deleteInvoice.isPending}
className="text-destructive hover:bg-destructive/10 w-full"
>
<Trash2 className="mr-2 h-4 w-4" />
Delete Invoice
</Button>
</CardContent>
</Card>
</div>
</div>
{/* Delete Confirmation Dialog */}
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete Invoice</DialogTitle>
<DialogDescription>
Are you sure you want to delete invoice{" "}
<strong>{invoice.invoiceNumber}</strong>? This action cannot be
undone and will permanently remove the invoice and all its data.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="outline"
onClick={() => setDeleteDialogOpen(false)}
disabled={deleteInvoice.isPending}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={confirmDelete}
disabled={deleteInvoice.isPending}
>
{deleteInvoice.isPending ? "Deleting..." : "Delete Invoice"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
export default function InvoiceViewPage() {
const params = useParams();
const id = params.id as string;
// Pass the actual id, let the form component handle the logic
return <InvoiceForm invoiceId={id} />;
return <InvoiceViewContent invoiceId={id} />;
}

View File

@@ -44,10 +44,10 @@ function SendEmailPageSkeleton() {
/>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
<div className="space-y-6 lg:col-span-2">
<div className="bg-muted h-96 animate-pulse rounded-lg" />
<div className="bg-muted h-96 animate-pulse" />
</div>
<div className="space-y-6">
<div className="bg-muted h-64 animate-pulse rounded-lg" />
<div className="bg-muted h-64 animate-pulse" />
</div>
</div>
</div>
@@ -91,7 +91,7 @@ export default function SendEmailPage() {
});
// Navigate back to invoice view
router.push(`/dashboard/invoices/${invoiceId}/view`);
router.push(`/dashboard/invoices/${invoiceId}`);
// Refresh invoice data
void utils.invoices.getById.invalidate({ id: invoiceId });
@@ -275,7 +275,7 @@ export default function SendEmailPage() {
>
<Button
variant="outline"
onClick={() => router.push(`/dashboard/invoices/${invoiceId}/view`)}
onClick={() => router.push(`/dashboard/invoices/${invoiceId}`)}
>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Invoice
@@ -334,9 +334,9 @@ export default function SendEmailPage() {
onBccEmailChange={setBccEmail}
/>
) : (
<div className="bg-muted flex h-[400px] items-center justify-center rounded-md border">
<div className="bg-muted flex h-[400px] items-center justify-center border">
<div className="text-center">
<div className="border-primary mx-auto mb-2 h-4 w-4 animate-spin rounded-full border-2 border-t-transparent"></div>
<div className="border-primary mx-auto mb-2 h-4 w-4 animate-spin border-2 border-t-transparent"></div>
<p className="text-muted-foreground text-sm">
Initializing email content...
</p>
@@ -382,7 +382,7 @@ export default function SendEmailPage() {
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<FileText className="h-5 w-5 text-green-600" />
<FileText className="text-primary h-5 w-5" />
Invoice #{invoice.invoiceNumber}
</CardTitle>
</CardHeader>
@@ -506,14 +506,12 @@ export default function SendEmailPage() {
<FloatingActionBar
leftContent={
<div className="flex items-center space-x-3">
<div className="rounded-lg bg-green-100 p-2 dark:bg-green-900/30">
<Send className="h-5 w-5 text-green-600 dark:text-green-400" />
<div className="bg-primary/10 p-2">
<Send className="text-primary h-5 w-5" />
</div>
<div>
<p className="font-medium text-gray-900 dark:text-gray-100">
Send Invoice
</p>
<p className="text-sm text-gray-600 dark:text-gray-300">
<p className="text-foreground font-medium">Send Invoice</p>
<p className="text-muted-foreground text-sm">
Email invoice to {invoice.client?.name ?? "client"}
</p>
</div>
@@ -523,7 +521,7 @@ export default function SendEmailPage() {
<Button
variant="outline"
size="sm"
onClick={() => router.push(`/dashboard/invoices/${invoiceId}/view`)}
onClick={() => router.push(`/dashboard/invoices/${invoiceId}`)}
>
Cancel
</Button>
@@ -531,7 +529,7 @@ export default function SendEmailPage() {
<Button
onClick={handleSendEmail}
disabled={!canSend || isSending}
className="bg-gradient-to-r from-emerald-600 to-teal-600 shadow-md transition-colors duration-200 hover:from-emerald-700 hover:to-teal-700"
variant="default"
size="sm"
>
{isSending ? (
@@ -570,7 +568,7 @@ export default function SendEmailPage() {
)}
.
{retryCount > 0 && (
<div className="mt-2 text-sm text-yellow-600">
<div className="text-muted-foreground mt-2 text-sm">
Retry attempt {retryCount} of 2
</div>
)}
@@ -583,10 +581,7 @@ export default function SendEmailPage() {
>
Cancel
</Button>
<Button
onClick={confirmSendEmail}
className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700"
>
<Button onClick={confirmSendEmail} variant="default">
<Send className="mr-2 h-4 w-4" />
Send Email
</Button>

View File

@@ -1,511 +0,0 @@
"use client";
import { DollarSign, Edit, Loader2, Trash2 } from "lucide-react";
import Link from "next/link";
import { notFound, useParams, useRouter } from "next/navigation";
import { useState } from "react";
import { toast } from "sonner";
import { StatusBadge, type StatusType } from "~/components/data/status-badge";
import { PageHeader } from "~/components/layout/page-header";
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
import { Separator } from "~/components/ui/separator";
import {
getEffectiveInvoiceStatus,
isInvoiceOverdue,
} from "~/lib/invoice-status";
import { api } from "~/trpc/react";
import type { StoredInvoiceStatus } from "~/types/invoice";
import { InvoiceDetailsSkeleton } from "../_components/invoice-details-skeleton";
import { PDFDownloadButton } from "../_components/pdf-download-button";
import { EnhancedSendInvoiceButton } from "~/components/forms/enhanced-send-invoice-button";
import {
AlertTriangle,
Building,
Check,
FileText,
Mail,
MapPin,
Phone,
User,
} from "lucide-react";
function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
const router = useRouter();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const { data: invoice, isLoading } = api.invoices.getById.useQuery({
id: invoiceId,
});
const utils = api.useUtils();
const deleteInvoice = api.invoices.delete.useMutation({
onSuccess: () => {
toast.success("Invoice deleted successfully");
router.push("/dashboard/invoices");
},
onError: (error) => {
toast.error(error.message ?? "Failed to delete invoice");
},
});
const updateStatus = api.invoices.updateStatus.useMutation({
onSuccess: (data) => {
toast.success(data.message);
void utils.invoices.getById.invalidate({ id: invoiceId });
},
onError: (error) => {
toast.error(error.message ?? "Failed to update invoice status");
},
});
const handleDelete = () => {
setDeleteDialogOpen(true);
};
const handleMarkAsPaid = () => {
updateStatus.mutate({
id: invoiceId,
status: "paid" as StoredInvoiceStatus,
});
};
const confirmDelete = () => {
deleteInvoice.mutate({ id: invoiceId });
};
if (isLoading) {
return <InvoiceDetailsSkeleton />;
}
if (!invoice) {
notFound();
}
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "short",
day: "numeric",
}).format(new Date(date));
};
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount);
};
const subtotal = invoice.items.reduce((sum, item) => sum + item.amount, 0);
const taxAmount = (subtotal * invoice.taxRate) / 100;
const total = subtotal + taxAmount;
const effectiveStatus = getEffectiveInvoiceStatus(
invoice.status as StoredInvoiceStatus,
invoice.dueDate,
);
const isOverdue = isInvoiceOverdue(
invoice.status as StoredInvoiceStatus,
invoice.dueDate,
);
const getStatusType = (): StatusType => {
return effectiveStatus as StatusType;
};
return (
<div className="space-y-6 pb-24">
<PageHeader
title="Invoice Details"
description="View and manage invoice information"
variant="gradient"
>
<PDFDownloadButton invoiceId={invoice.id} variant="outline" />
<Button asChild variant="default">
<Link href={`/dashboard/invoices/${invoice.id}`}>
<Edit className="h-5 w-5" />
<span>Edit</span>
</Link>
</Button>
</PageHeader>
{/* Content */}
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
{/* Left Column */}
<div className="space-y-6 lg:col-span-2">
{/* Invoice Header */}
<Card className="card-primary">
<CardContent className="p-4 sm:p-6">
<div className="space-y-4">
<div className="flex items-start justify-between gap-6">
<div className="min-w-0 flex-1 space-y-2">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
<h2 className="text-foreground truncate text-2xl font-bold">
{invoice.invoiceNumber}
</h2>
<StatusBadge status={getStatusType()} />
</div>
<div className="text-muted-foreground space-y-1 text-sm sm:space-y-0">
<div className="sm:inline">
Issued {formatDate(invoice.issueDate)}
</div>
<div className="sm:inline sm:before:content-['_•_']">
Due {formatDate(invoice.dueDate)}
</div>
</div>
</div>
<div className="flex-shrink-0 text-right">
<p className="text-muted-foreground text-sm">
Total Amount
</p>
<p className="text-primary text-3xl font-bold">
{formatCurrency(total)}
</p>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Overdue Alert */}
{isOverdue && (
<Card className="border-destructive/20 bg-destructive/5 card-secondary">
<CardContent className="p-4">
<div className="text-destructive flex items-center gap-3">
<AlertTriangle className="h-5 w-5 flex-shrink-0" />
<div>
<p className="font-medium">Invoice Overdue</p>
<p className="text-sm">
{Math.ceil(
(new Date().getTime() -
new Date(invoice.dueDate).getTime()) /
(1000 * 60 * 60 * 24),
)}{" "}
days past due date
</p>
</div>
</div>
</CardContent>
</Card>
)}
{/* Client & Business Info */}
<div className="grid gap-4 sm:grid-cols-2">
{/* Client Information */}
<Card className="card-primary">
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2">
<User className="h-5 w-5" />
Bill To
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<h3 className="text-foreground text-xl font-semibold">
{invoice.client.name}
</h3>
</div>
<div className="space-y-3">
{invoice.client.email && (
<div className="flex items-center gap-3">
<div className="bg-primary/10 rounded-lg p-2">
<Mail className="text-primary h-4 w-4" />
</div>
<span className="text-sm break-all">
{invoice.client.email}
</span>
</div>
)}
{invoice.client.phone && (
<div className="flex items-center gap-3">
<div className="bg-primary/10 rounded-lg p-2">
<Phone className="text-primary h-4 w-4" />
</div>
<span className="text-sm">{invoice.client.phone}</span>
</div>
)}
{(invoice.client.addressLine1 ?? invoice.client.city) && (
<div className="flex items-start gap-3">
<div className="bg-primary/10 rounded-lg p-2">
<MapPin className="text-primary h-4 w-4" />
</div>
<div className="space-y-1 text-sm">
{invoice.client.addressLine1 && (
<div>{invoice.client.addressLine1}</div>
)}
{invoice.client.addressLine2 && (
<div>{invoice.client.addressLine2}</div>
)}
{(invoice.client.city ??
invoice.client.state ??
invoice.client.postalCode) && (
<div>
{[
invoice.client.city,
invoice.client.state,
invoice.client.postalCode,
]
.filter(Boolean)
.join(", ")}
</div>
)}
{invoice.client.country && (
<div>{invoice.client.country}</div>
)}
</div>
</div>
)}
</div>
</CardContent>
</Card>
{/* Business Information */}
{invoice.business && (
<Card className="card-primary">
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2">
<Building className="h-5 w-5" />
From
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<h3 className="text-foreground text-xl font-semibold">
{invoice.business.name}
</h3>
</div>
<div className="space-y-3">
{invoice.business.email && (
<div className="flex items-center gap-3">
<div className="bg-primary/10 rounded-lg p-2">
<Mail className="text-primary h-4 w-4" />
</div>
<span className="text-sm break-all">
{invoice.business.email}
</span>
</div>
)}
{invoice.business.phone && (
<div className="flex items-center gap-3">
<div className="bg-primary/10 rounded-lg p-2">
<Phone className="text-primary h-4 w-4" />
</div>
<span className="text-sm">
{invoice.business.phone}
</span>
</div>
)}
</div>
</CardContent>
</Card>
)}
</div>
{/* Invoice Items */}
<Card className="card-primary">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileText className="h-5 w-5" />
Invoice Items
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{invoice.items.map((item) => (
<Card key={item.id} className="card-secondary">
<CardContent className="py-2">
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 flex-1">
<p className="text-foreground mb-2 text-base font-medium">
{item.description}
</p>
<div className="text-muted-foreground text-sm">
<span className="inline whitespace-nowrap">
{formatDate(item.date).replace(/ /g, "\u00A0")}
</span>
<span className="inline whitespace-nowrap before:mx-2 before:content-['_|_']">
{item.hours.toString().replace(/ /g, "\u00A0")}
&nbsp;hours
</span>
<span className="inline whitespace-nowrap before:mx-2 before:content-['_|_']">
@&nbsp;${item.rate}/hr
</span>
</div>
</div>
<div className="flex-shrink-0 text-right">
<p className="text-primary text-lg font-semibold">
{formatCurrency(item.amount)}
</p>
</div>
</div>
</CardContent>
</Card>
))}
{/* Totals */}
<div className="bg-muted/30 rounded-lg p-4">
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-muted-foreground">Subtotal:</span>
<span className="font-medium">
{formatCurrency(subtotal)}
</span>
</div>
{invoice.taxRate > 0 && (
<div className="flex justify-between">
<span className="text-muted-foreground">
Tax ({invoice.taxRate}%):
</span>
<span className="font-medium">
{formatCurrency(taxAmount)}
</span>
</div>
)}
<Separator />
<div className="flex justify-between text-lg font-bold">
<span>Total:</span>
<span className="text-primary">
{formatCurrency(total)}
</span>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Notes */}
{invoice.notes && (
<Card className="card-primary">
<CardHeader>
<CardTitle>Notes</CardTitle>
</CardHeader>
<CardContent>
<p className="text-foreground whitespace-pre-wrap">
{invoice.notes}
</p>
</CardContent>
</Card>
)}
</div>
{/* Right Column - Actions */}
<div className="space-y-6">
<Card className="card-primary sticky top-6">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Check className="h-5 w-5" />
Actions
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<Button asChild variant="outline" className="w-full">
<Link href={`/dashboard/invoices/${invoice.id}`}>
<Edit className="mr-2 h-4 w-4" />
Edit Invoice
</Link>
</Button>
{invoice.items && invoice.client && (
<PDFDownloadButton invoiceId={invoice.id} className="w-full" />
)}
{/* Send Invoice Button - Show for draft, sent, and overdue */}
{effectiveStatus === "draft" && (
<EnhancedSendInvoiceButton
invoiceId={invoice.id}
className="w-full"
/>
)}
{(effectiveStatus === "sent" ||
effectiveStatus === "overdue") && (
<EnhancedSendInvoiceButton
invoiceId={invoice.id}
className="w-full"
showResend={true}
/>
)}
{/* Manual Status Updates */}
{(effectiveStatus === "sent" ||
effectiveStatus === "overdue") && (
<Button
onClick={handleMarkAsPaid}
disabled={updateStatus.isPending}
className="w-full bg-green-600 text-white hover:bg-green-700"
>
{updateStatus.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<DollarSign className="mr-2 h-4 w-4" />
)}
Mark as Paid
</Button>
)}
<Button
variant="outline"
onClick={handleDelete}
disabled={deleteInvoice.isPending}
className="w-full text-red-700 hover:bg-red-50"
>
<Trash2 className="mr-2 h-4 w-4" />
Delete Invoice
</Button>
</CardContent>
</Card>
</div>
</div>
{/* Delete Confirmation Dialog */}
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete Invoice</DialogTitle>
<DialogDescription>
Are you sure you want to delete invoice{" "}
<strong>{invoice.invoiceNumber}</strong>? This action cannot be
undone and will permanently remove the invoice and all its data.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="outline"
onClick={() => setDeleteDialogOpen(false)}
disabled={deleteInvoice.isPending}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={confirmDelete}
disabled={deleteInvoice.isPending}
>
{deleteInvoice.isPending ? "Deleting..." : "Delete Invoice"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
export default function InvoiceViewPage() {
const params = useParams();
const id = params.id as string;
return <InvoiceViewContent invoiceId={id} />;
}

View File

@@ -107,7 +107,7 @@ export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
});
const handleRowClick = (invoice: Invoice) => {
router.push(`/dashboard/invoices/${invoice.id}/view`);
router.push(`/dashboard/invoices/${invoice.id}`);
};
const handleDelete = (invoice: Invoice) => {
@@ -206,7 +206,7 @@ export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
const invoice = row.original;
return (
<div className="flex items-center justify-end gap-1">
<Link href={`/dashboard/invoices/${invoice.id}/view`}>
<Link href={`/dashboard/invoices/${invoice.id}`}>
<Button
variant="ghost"
size="sm"
@@ -216,7 +216,7 @@ export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
<Eye className="h-3.5 w-3.5" />
</Button>
</Link>
<Link href={`/dashboard/invoices/${invoice.id}`}>
<Link href={`/dashboard/invoices/${invoice.id}/edit`}>
<Button
variant="ghost"
size="sm"
@@ -229,7 +229,7 @@ export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) {
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 text-red-600 hover:text-red-700"
className="text-destructive hover:text-destructive/80 h-8 w-8 p-0"
onClick={(e) => {
e.stopPropagation();
handleDelete(invoice);

View File

@@ -21,16 +21,16 @@ function FormatInstructions() {
return (
<div className="grid gap-6 lg:grid-cols-2">
{/* Required Format */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="card-title-info">
<FileText className="text-icon-blue h-5 w-5" />
<CardTitle className="text-foreground flex items-center gap-2">
<FileText className="text-primary h-5 w-5" />
Required CSV Format
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="bg-muted-subtle rounded-lg p-4">
<p className="text-secondary font-mono text-sm">
<div className="bg-muted/50 p-4">
<p className="text-muted-foreground font-mono text-sm">
DATE,DESCRIPTION,HOURS,RATE,AMOUNT
</p>
</div>
@@ -49,7 +49,7 @@ function FormatInstructions() {
},
].map((col) => (
<div key={col.field} className="flex items-start gap-3">
<Badge className="badge-outline text-xs">{col.field}</Badge>
<Badge className="border text-xs">{col.field}</Badge>
<span className="text-muted-foreground text-sm">
{col.desc}
</span>
@@ -72,10 +72,10 @@ function FormatInstructions() {
</Card>
{/* Sample Data & Download */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="card-title-secondary">
<Download className="text-icon-green h-5 w-5" />
<CardTitle className="text-foreground flex items-center gap-2">
<Download className="text-primary h-5 w-5" />
Sample Template
</CardTitle>
</CardHeader>
@@ -85,9 +85,9 @@ function FormatInstructions() {
for importing time entries.
</p>
<div className="bg-green-subtle rounded-lg p-4">
<div className="bg-primary/10 p-4">
<div className="flex items-start gap-3">
<Info className="text-icon-green mt-0.5 h-5 w-5" />
<Info className="text-primary mt-0.5 h-5 w-5" />
<div>
<p className="text-success text-sm font-medium">Pro Tip</p>
<p className="text-success text-sm">
@@ -100,7 +100,7 @@ function FormatInstructions() {
<div className="space-y-2">
<h4 className="text-sm font-semibold">Sample Row:</h4>
<div className="bg-muted-subtle rounded-lg p-3">
<div className="bg-muted/50 p-3">
<p className="text-muted font-mono text-xs break-all">
1/15/24,&quot;Web development work&quot;,8,75.00,600.00
</p>
@@ -109,7 +109,7 @@ function FormatInstructions() {
<div className="space-y-2">
<h4 className="text-sm font-semibold">Sample Filename:</h4>
<div className="bg-muted-subtle rounded-lg p-3">
<div className="bg-muted/50 p-3">
<p className="text-muted font-mono text-xs">2024-01-15.csv</p>
</div>
</div>
@@ -122,10 +122,10 @@ function FormatInstructions() {
// Important Notes Section
function ImportantNotes() {
return (
<Card className="card-primary border-l-4 border-l-amber-500">
<Card className="bg-card border-border border border-l-4 border-l-amber-500">
<CardHeader>
<CardTitle className="card-title-warning">
<AlertCircle className="text-icon-amber h-5 w-5" />
<CardTitle className="text-destructive flex items-center gap-2">
<AlertCircle className="text-primary h-5 w-5" />
Important Notes
</CardTitle>
</CardHeader>
@@ -158,18 +158,18 @@ function ImportantNotes() {
// File Format Help Section
function FileFormatHelp() {
return (
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="card-title-info">
<FileSpreadsheet className="text-icon-blue h-5 w-5" />
<CardTitle className="text-foreground flex items-center gap-2">
<FileSpreadsheet className="text-primary h-5 w-5" />
Supported File Formats
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 md:grid-cols-3">
<div className="grid gap-6 md:grid-cols-3">
<div className="space-y-2 text-center">
<div className="mx-auto w-fit rounded-full bg-blue-50 p-3 dark:bg-blue-900/20">
<FileSpreadsheet className="h-6 w-6 text-blue-600" />
<div className="bg-accent mx-auto w-fit p-3">
<FileSpreadsheet className="text-foreground-foreground h-6 w-6" />
</div>
<h4 className="font-semibold">CSV Files</h4>
<p className="text-muted-foreground text-sm">
@@ -178,8 +178,8 @@ function FileFormatHelp() {
</p>
</div>
<div className="space-y-2 text-center">
<div className="mx-auto w-fit rounded-full bg-green-50 p-3 dark:bg-green-900/20">
<Upload className="h-6 w-6 text-green-600" />
<div className="bg-primary/10 mx-auto w-fit p-3">
<Upload className="text-primary h-6 w-6" />
</div>
<h4 className="font-semibold">Max Size</h4>
<p className="text-muted-foreground text-sm">
@@ -187,8 +187,8 @@ function FileFormatHelp() {
</p>
</div>
<div className="space-y-2 text-center">
<div className="mx-auto w-fit rounded-full bg-purple-50 p-3 dark:bg-purple-900/20">
<CheckCircle className="h-6 w-6 text-purple-600" />
<div className="bg-secondary mx-auto w-fit p-3">
<CheckCircle className="text-muted-foreground-foreground h-6 w-6" />
</div>
<h4 className="font-semibold">Validation</h4>
<p className="text-muted-foreground text-sm">

View File

@@ -28,7 +28,7 @@ export default async function InvoicesPage() {
<span>Import CSV</span>
</Link>
</Button>
<Button asChild className="btn-brand-primary shadow-md">
<Button asChild variant="default" className="shadow-md">
<Link href="/dashboard/invoices/new">
<Plus className="mr-2 h-5 w-5" />
<span>Create Invoice</span>

View File

@@ -8,19 +8,19 @@ export default function DashboardLayout({
children: React.ReactNode;
}) {
return (
<div className="floating-orbs relative min-h-screen">
<div className="bg-dashboard relative min-h-screen">
<Navbar />
<Sidebar />
{/* Mobile layout - no left margin */}
<main className="relative z-10 min-h-screen pt-20 md:hidden">
<div className="px-4 pt-4 pb-6 sm:px-6">
<main className="relative z-10 min-h-screen pt-16 md:hidden">
<div className="bg-background px-4 pt-4 pb-6 sm:px-6">
<DashboardBreadcrumbs />
{children}
</div>
</main>
{/* Desktop layout - with sidebar margin */}
<main className="relative z-10 hidden min-h-screen pt-20 md:ml-[276px] md:block">
<div className="px-6 pt-6 pb-6">
<main className="relative z-10 hidden min-h-screen pt-16 md:ml-64 md:block">
<div className="bg-background px-6 pt-6 pb-6">
<DashboardBreadcrumbs />
{children}
</div>

View File

@@ -9,6 +9,8 @@ import {
Eye,
FileText,
Plus,
TrendingDown,
TrendingUp,
Users,
} from "lucide-react";
import Link from "next/link";
@@ -21,23 +23,23 @@ import { getEffectiveInvoiceStatus } from "~/lib/invoice-status";
import { auth } from "~/server/auth";
import { HydrateClient, api } from "~/trpc/server";
import type { StoredInvoiceStatus } from "~/types/invoice";
import { RevenueChart } from "~/app/dashboard/_components/revenue-chart";
import { InvoiceStatusChart } from "~/app/dashboard/_components/invoice-status-chart";
import { MonthlyMetricsChart } from "~/app/dashboard/_components/monthly-metrics-chart";
// Modern gradient background component
// Hero section with clean mono design
function DashboardHero({ firstName }: { firstName: string }) {
return (
<Card className="relative mb-8 overflow-hidden border-0 p-8 shadow-sm transition-shadow hover:shadow-md">
<div className="absolute inset-0" />
<div className="relative z-10">
<h1 className="mb-2 text-3xl font-bold">Welcome back, {firstName}!</h1>
<p className="text-lg">Ready to manage your invoicing business</p>
</div>
<div className="absolute -top-8 -right-8 h-32 w-32 rounded-full bg-white/10" />
<div className="absolute -right-4 -bottom-4 h-24 w-24 rounded-full bg-white/5" />
</Card>
<div className="mb-8">
<h1 className="mb-2 text-3xl font-bold">Welcome back, {firstName}!</h1>
<p className="text-muted-foreground text-lg">
Here&apos;s what&apos;s happening with your business today
</p>
</div>
);
}
// Enhanced stats cards with better visual hierarchy
// Enhanced stats cards with better visuals
async function DashboardStats() {
const [clients, invoices] = await Promise.all([
api.clients.getAll(),
@@ -45,8 +47,48 @@ async function DashboardStats() {
]);
const totalClients = clients.length;
const totalInvoices = invoices.length;
const totalRevenue = invoices
const paidInvoices = invoices.filter(
(invoice) =>
getEffectiveInvoiceStatus(
invoice.status as StoredInvoiceStatus,
invoice.dueDate,
) === "paid",
);
const totalRevenue = paidInvoices.reduce(
(sum, invoice) => sum + invoice.totalAmount,
0,
);
const pendingInvoices = invoices.filter((invoice) => {
const effectiveStatus = getEffectiveInvoiceStatus(
invoice.status as StoredInvoiceStatus,
invoice.dueDate,
);
return effectiveStatus === "sent" || effectiveStatus === "overdue";
});
const pendingAmount = pendingInvoices.reduce(
(sum, invoice) => sum + invoice.totalAmount,
0,
);
const overdueInvoices = invoices.filter(
(invoice) =>
getEffectiveInvoiceStatus(
invoice.status as StoredInvoiceStatus,
invoice.dueDate,
) === "overdue",
);
// Calculate month-over-month trends
const now = new Date();
const currentMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
// Current month data
const currentMonthInvoices = invoices.filter(
(invoice) => new Date(invoice.issueDate) >= currentMonth,
);
const currentMonthRevenue = currentMonthInvoices
.filter(
(invoice) =>
getEffectiveInvoiceStatus(
@@ -55,79 +97,134 @@ async function DashboardStats() {
) === "paid",
)
.reduce((sum, invoice) => sum + invoice.totalAmount, 0);
const pendingAmount = invoices
.filter((invoice) => {
const effectiveStatus = getEffectiveInvoiceStatus(
// Last month data
const lastMonthInvoices = invoices.filter((invoice) => {
const date = new Date(invoice.issueDate);
return date >= lastMonth && date < currentMonth;
});
const lastMonthRevenue = lastMonthInvoices
.filter(
(invoice) =>
getEffectiveInvoiceStatus(
invoice.status as StoredInvoiceStatus,
invoice.dueDate,
) === "paid",
)
.reduce((sum, invoice) => sum + invoice.totalAmount, 0);
// Previous month data for clients
const prevMonthClients = clients.filter(
(client) => new Date(client.createdAt) < currentMonth,
).length;
// Calculate trends
const revenueChange =
lastMonthRevenue > 0
? ((currentMonthRevenue - lastMonthRevenue) / lastMonthRevenue) * 100
: currentMonthRevenue > 0
? 100
: 0;
const pendingChange =
lastMonthInvoices.length > 0
? ((pendingInvoices.length -
lastMonthInvoices.filter((invoice) => {
const status = getEffectiveInvoiceStatus(
invoice.status as StoredInvoiceStatus,
invoice.dueDate,
);
return status === "sent" || status === "overdue";
}).length) /
lastMonthInvoices.length) *
100
: pendingInvoices.length > 0
? 100
: 0;
const clientChange = totalClients - prevMonthClients;
const lastMonthOverdue = lastMonthInvoices.filter(
(invoice) =>
getEffectiveInvoiceStatus(
invoice.status as StoredInvoiceStatus,
invoice.dueDate,
);
return effectiveStatus === "sent" || effectiveStatus === "overdue";
})
.reduce((sum, invoice) => sum + invoice.totalAmount, 0);
) === "overdue",
).length;
const overdueChange = overdueInvoices.length - lastMonthOverdue;
const formatTrend = (value: number, isCount = false) => {
if (isCount) {
return value > 0 ? `+${value}` : value.toString();
}
return value > 0 ? `+${value.toFixed(1)}%` : `${value.toFixed(1)}%`;
};
const stats = [
{
title: "Total Revenue",
value: `$${totalRevenue.toLocaleString("en-US", { minimumFractionDigits: 2 })}`,
change: "+12.5%",
change: formatTrend(revenueChange),
trend: revenueChange >= 0 ? ("up" as const) : ("down" as const),
icon: DollarSign,
color: "",
bgColor: "bg-green-50",
changeColor: "",
description: `From ${paidInvoices.length} paid invoices`,
},
{
title: "Pending Amount",
value: `$${pendingAmount.toLocaleString("en-US", { minimumFractionDigits: 2 })}`,
change: "+8.2%",
change: formatTrend(pendingChange),
trend: pendingChange >= 0 ? ("up" as const) : ("down" as const),
icon: Clock,
color: "",
bgColor: "bg-amber-50",
changeColor: "",
description: `${pendingInvoices.length} invoices awaiting payment`,
},
{
title: "Active Clients",
value: totalClients.toString(),
change: "+3",
change: formatTrend(clientChange, true),
trend: clientChange >= 0 ? ("up" as const) : ("down" as const),
icon: Users,
color: "",
bgColor: "bg-blue-50",
changeColor: "",
description: "Total registered clients",
},
{
title: "Total Invoices",
value: totalInvoices.toString(),
change: "+15",
icon: FileText,
color: "",
bgColor: "bg-purple-50",
changeColor: "",
title: "Overdue Invoices",
value: overdueInvoices.length.toString(),
change: formatTrend(overdueChange, true),
trend: overdueChange <= 0 ? ("up" as const) : ("down" as const),
icon: TrendingDown,
description: "Invoices past due date",
},
];
return (
<div className="mb-8 grid grid-cols-2 gap-3 sm:gap-6 lg:grid-cols-4">
<div className="mb-8 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
{stats.map((stat) => {
const Icon = stat.icon;
const TrendIcon = stat.trend === "up" ? TrendingUp : TrendingDown;
const isPositive = stat.trend === "up";
return (
<Card
key={stat.title}
className="border-0 shadow-sm transition-shadow hover:shadow-md"
>
<CardContent className="p-3 sm:p-4 lg:p-6">
<div className="mb-2 flex items-center justify-between sm:mb-3 lg:mb-4">
<div className={`rounded-lg p-1.5 sm:p-2 ${stat.bgColor}`}>
<Icon className="h-3 w-3 text-gray-700 sm:h-4 sm:w-4 lg:h-5 lg:w-5 dark:text-gray-800" />
<Card key={stat.title}>
<CardContent className="p-6">
<div className="flex items-center justify-between space-y-0 pb-2">
<div className="flex items-center space-x-2">
<Icon className="text-muted-foreground h-5 w-5" />
<p className="text-muted-foreground text-sm font-medium">
{stat.title}
</p>
</div>
<div
className={`flex items-center space-x-1 text-xs ${
isPositive ? "text-green-600" : "text-red-600"
}`}
>
<TrendIcon className="h-3 w-3" />
<span>{stat.change}</span>
</div>
<span className="text-xs font-medium text-teal-600 dark:text-teal-400">
{stat.change}
</span>
</div>
<div>
<p className="mb-1 text-base font-bold text-gray-900 sm:text-xl lg:text-2xl dark:text-gray-100">
{stat.value}
</p>
<p className="text-xs text-gray-600 lg:text-sm dark:text-gray-300">
{stat.title}
<div className="space-y-1">
<p className="text-2xl font-bold">{stat.value}</p>
<p className="text-muted-foreground text-xs">
{stat.description}
</p>
</div>
</CardContent>
@@ -138,64 +235,111 @@ async function DashboardStats() {
);
}
// Quick Actions with better visual design
// Charts section
async function ChartsSection() {
const invoices = await api.invoices.getAll();
return (
<div className="mb-8 grid grid-cols-1 gap-6 lg:grid-cols-2">
{/* Revenue Trend Chart */}
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<BarChart3 className="h-5 w-5" />
Revenue Over Time
</CardTitle>
</CardHeader>
<CardContent>
<RevenueChart invoices={invoices} />
</CardContent>
</Card>
{/* Invoice Status Breakdown */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Activity className="h-5 w-5" />
Invoice Status
</CardTitle>
</CardHeader>
<CardContent>
<InvoiceStatusChart invoices={invoices} />
</CardContent>
</Card>
{/* Monthly Metrics */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Calendar className="h-5 w-5" />
Monthly Metrics
</CardTitle>
</CardHeader>
<CardContent>
<MonthlyMetricsChart invoices={invoices} />
</CardContent>
</Card>
</div>
);
}
// Enhanced Quick Actions
function QuickActions() {
const actions = [
{
title: "Create Invoice",
description: "Start a new invoice",
description: "Start a new invoice for a client",
href: "/dashboard/invoices/new",
icon: FileText,
primary: true,
featured: true,
},
{
title: "Add Client",
description: "Add a new client",
description: "Register a new client",
href: "/dashboard/clients/new",
icon: Users,
primary: false,
featured: false,
},
{
title: "View Reports",
description: "Business analytics",
href: "/dashboard/reports",
title: "View All Invoices",
description: "Manage your invoice pipeline",
href: "/dashboard/invoices",
icon: BarChart3,
primary: false,
featured: false,
},
];
return (
<Card className="border-0 shadow-sm">
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-lg">
<Plus className="h-5 w-5 text-teal-600 dark:text-teal-400" />
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Plus className="h-5 w-5" />
Quick Actions
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<CardContent className="space-y-3">
{actions.map((action) => {
const Icon = action.icon;
return (
<Button
key={action.title}
asChild
variant={action.primary ? "default" : "outline"}
className={`h-12 w-full justify-start px-3 ${
action.primary
? "bg-teal-600 text-white hover:bg-teal-700"
: "border-gray-200 hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-800"
variant="outline"
className={`h-auto w-full justify-start p-4 ${
action.featured
? "border-foreground/20 bg-muted/50 hover:bg-muted"
: "hover:bg-muted/50"
}`}
>
<Link href={action.href}>
<div className="flex items-center gap-3">
<Icon
className={`h-4 w-4 ${action.primary ? "text-white" : "text-gray-600 dark:text-gray-300"}`}
/>
<span
className={`font-medium ${action.primary ? "text-white" : "text-gray-900 dark:text-gray-100"}`}
>
{action.title}
</span>
<div className="flex items-center space-x-3">
<Icon className="h-5 w-5 flex-shrink-0" />
<div className="text-left">
<p className="font-semibold">{action.title}</p>
<p className="text-muted-foreground text-sm">
{action.description}
</p>
</div>
</div>
</Link>
</Button>
@@ -206,7 +350,7 @@ function QuickActions() {
);
}
// Current work in progress
// Current work section with enhanced design
async function CurrentWork() {
const invoices = await api.invoices.getAll();
const draftInvoices = invoices.filter(
@@ -220,20 +364,21 @@ async function CurrentWork() {
if (!currentInvoice) {
return (
<Card className="border-0 shadow-sm">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<Activity className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<CardTitle className="flex items-center gap-2">
<Activity className="h-5 w-5" />
Current Work
</CardTitle>
</CardHeader>
<CardContent>
<div className="py-8 text-center">
<FileText className="mx-auto mb-4 h-12 w-12 text-gray-300 dark:text-gray-600" />
<p className="mb-4 text-gray-600 dark:text-gray-300">
No draft invoices found
<FileText className="text-muted-foreground mx-auto mb-4 h-12 w-12" />
<h3 className="mb-2 text-lg font-semibold">No active drafts</h3>
<p className="text-muted-foreground mb-4">
Create a new invoice to get started
</p>
<Button asChild className="bg-teal-600 hover:bg-teal-700">
<Button asChild variant="outline" className="border-foreground/20">
<Link href="/dashboard/invoices/new">
<Plus className="mr-2 h-4 w-4" />
Create Invoice
@@ -249,49 +394,41 @@ async function CurrentWork() {
currentInvoice.items?.reduce((sum, item) => sum + item.hours, 0) ?? 0;
return (
<Card className="border-0 shadow-sm">
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="flex items-center gap-2 text-lg">
<Activity className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="flex items-center gap-2">
<Activity className="h-5 w-5" />
Current Work
</CardTitle>
<Badge variant="secondary">In Progress</Badge>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<p className="text-lg font-semibold">
<div className="space-y-2">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">
#{currentInvoice.invoiceNumber}
</p>
<p className="text-gray-600 dark:text-gray-300">
{currentInvoice.client?.name}
</p>
</div>
<div className="text-right">
<p className="text-2xl font-bold text-teal-600 dark:text-teal-400">
</h3>
<span className="text-primary text-2xl font-bold">
${currentInvoice.totalAmount.toFixed(2)}
</p>
<p className="text-sm text-gray-600 dark:text-gray-300">
{totalHours.toFixed(1)} hours
</p>
</span>
</div>
<div className="text-muted-foreground flex items-center justify-between text-sm">
<span>{currentInvoice.client?.name}</span>
<span>{totalHours.toFixed(1)} hours logged</span>
</div>
</div>
<div className="flex gap-2">
<Button asChild variant="outline" size="sm" className="flex-1">
<Link href={`/dashboard/invoices/${currentInvoice.id}`}>
<Eye className="mr-2 h-3 w-3" />
<Eye className="mr-2 h-4 w-4" />
View
</Link>
</Button>
<Button
asChild
size="sm"
className="flex-1 bg-teal-600 hover:bg-teal-700"
>
<Link href={`/dashboard/invoices/${currentInvoice.id}`}>
<Edit className="mr-2 h-3 w-3" />
<Button asChild size="sm" className="flex-1">
<Link href={`/dashboard/invoices/${currentInvoice.id}/edit`}>
<Edit className="mr-2 h-4 w-4" />
Continue
</Link>
</Button>
@@ -302,7 +439,7 @@ async function CurrentWork() {
);
}
// Recent activity with enhanced design
// Enhanced recent activity
async function RecentActivity() {
const invoices = await api.invoices.getAll();
const recentInvoices = invoices
@@ -315,21 +452,21 @@ async function RecentActivity() {
const getStatusColor = (status: string) => {
switch (status) {
case "paid":
return "bg-green-50 border-green-200";
return "bg-green-50 border-green-200 text-green-700";
case "sent":
return "bg-blue-50 border-blue-200";
return "bg-blue-50 border-blue-200 text-blue-700";
case "overdue":
return "bg-red-50 border-red-200";
return "bg-red-50 border-red-200 text-red-700";
default:
return "bg-gray-50 border-gray-200";
return "bg-gray-50 border-gray-200 text-gray-700";
}
};
return (
<Card className="border-0 shadow-sm">
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="flex items-center gap-2 text-lg">
<Calendar className="h-5 w-5 text-purple-600 dark:text-purple-400" />
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="flex items-center gap-2">
<Calendar className="h-5 w-5" />
Recent Activity
</CardTitle>
<Button variant="ghost" size="sm" asChild>
@@ -342,11 +479,12 @@ async function RecentActivity() {
<CardContent>
{recentInvoices.length === 0 ? (
<div className="py-8 text-center">
<FileText className="mx-auto mb-4 h-12 w-12 text-gray-300 dark:text-gray-600" />
<p className="mb-4 text-gray-600 dark:text-gray-300">
No invoices yet
<FileText className="text-muted-foreground mx-auto mb-4 h-12 w-12" />
<h3 className="mb-2 text-lg font-semibold">No invoices yet</h3>
<p className="text-muted-foreground mb-4">
Create your first invoice to get started
</p>
<Button asChild className="bg-teal-600 hover:bg-teal-700">
<Button asChild variant="outline" className="border-foreground/20">
<Link href="/dashboard/invoices/new">
<Plus className="mr-2 h-4 w-4" />
Create Your First Invoice
@@ -361,39 +499,28 @@ async function RecentActivity() {
href={`/dashboard/invoices/${invoice.id}`}
className="block"
>
<Card className="card-secondary transition-colors hover:bg-gray-200/70 dark:hover:bg-gray-700/60">
<CardContent className="p-4">
<div className="space-y-3">
<div className="flex items-center gap-3">
<div className="rounded-lg bg-gray-100 p-2 dark:bg-gray-700">
<FileText className="h-4 w-4 text-gray-600 dark:text-gray-300" />
</div>
<div className="min-w-0 flex-1">
<p className="font-medium text-gray-900 dark:text-gray-100">
#{invoice.invoiceNumber}
</p>
<p className="text-sm text-gray-600 dark:text-gray-300">
{invoice.client?.name} {" "}
{new Date(invoice.issueDate).toLocaleDateString()}
</p>
</div>
<div className="rounded-lg p-1 transition-colors hover:bg-gray-300/50 dark:hover:bg-gray-600/50">
<Eye className="h-4 w-4 text-gray-600 dark:text-gray-300" />
</div>
</div>
<div className="flex items-center justify-between">
<Badge
className={`border ${getStatusColor(invoice.status)}`}
>
{invoice.status}
</Badge>
<p className="font-semibold text-gray-900 dark:text-gray-100">
${invoice.totalAmount.toFixed(2)}
</p>
</div>
<div className="hover:bg-muted/50 flex items-center justify-between rounded-lg border p-3 transition-colors">
<div className="flex items-center space-x-3">
<div className="bg-muted rounded-lg p-2">
<FileText className="text-muted-foreground h-4 w-4" />
</div>
</CardContent>
</Card>
<div className="min-w-0 flex-1">
<p className="font-medium">#{invoice.invoiceNumber}</p>
<p className="text-muted-foreground text-sm">
{invoice.client?.name} {" "}
{new Date(invoice.issueDate).toLocaleDateString()}
</p>
</div>
</div>
<div className="flex items-center space-x-3">
<Badge className={getStatusColor(invoice.status)}>
{invoice.status}
</Badge>
<span className="font-semibold">
${invoice.totalAmount.toFixed(2)}
</span>
</div>
</div>
</Link>
))}
</div>
@@ -406,16 +533,16 @@ async function RecentActivity() {
// Loading skeletons
function StatsSkeleton() {
return (
<div className="mb-8 grid grid-cols-2 gap-3 sm:gap-6 lg:grid-cols-4">
<div className="mb-8 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
{Array.from({ length: 4 }).map((_, i) => (
<Card key={i} className="border-0 shadow-sm">
<CardContent className="p-3 sm:p-4 lg:p-6">
<div className="mb-2 flex items-center justify-between sm:mb-3 lg:mb-4">
<Skeleton className="h-6 w-6 rounded-lg sm:h-8 sm:w-8 lg:h-9 lg:w-9" />
<Skeleton className="h-3 w-8 sm:h-4 sm:w-12" />
<Card key={i}>
<CardContent className="p-6">
<div className="flex items-center justify-between space-y-0 pb-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-12" />
</div>
<Skeleton className="mb-1 h-5 w-16 sm:mb-2 sm:h-6 sm:w-20 lg:h-8" />
<Skeleton className="h-3 w-20 sm:h-4 sm:w-24" />
<Skeleton className="mb-2 h-8 w-20" />
<Skeleton className="h-3 w-32" />
</CardContent>
</Card>
))}
@@ -423,9 +550,40 @@ function StatsSkeleton() {
);
}
function ChartsSkeleton() {
return (
<div className="mb-8 grid grid-cols-1 gap-6 lg:grid-cols-2">
<Card className="lg:col-span-2">
<CardHeader>
<Skeleton className="h-6 w-40" />
</CardHeader>
<CardContent>
<Skeleton className="h-64 w-full" />
</CardContent>
</Card>
<Card>
<CardHeader>
<Skeleton className="h-6 w-32" />
</CardHeader>
<CardContent>
<Skeleton className="h-64 w-full" />
</CardContent>
</Card>
<Card>
<CardHeader>
<Skeleton className="h-6 w-36" />
</CardHeader>
<CardContent>
<Skeleton className="h-64 w-full" />
</CardContent>
</Card>
</div>
);
}
function CardSkeleton() {
return (
<Card className="border-0 shadow-sm">
<Card>
<CardHeader>
<Skeleton className="h-6 w-32" />
</CardHeader>
@@ -454,21 +612,28 @@ export default async function DashboardPage() {
</Suspense>
</HydrateClient>
<div className="grid grid-cols-1 gap-8 lg:grid-cols-2">
<HydrateClient>
<Suspense fallback={<CardSkeleton />}>
<CurrentWork />
</Suspense>
</HydrateClient>
<QuickActions />
</div>
<HydrateClient>
<Suspense fallback={<CardSkeleton />}>
<RecentActivity />
<Suspense fallback={<ChartsSkeleton />}>
<ChartsSection />
</Suspense>
</HydrateClient>
<div className="grid grid-cols-1 gap-8 lg:grid-cols-2">
<div className="space-y-8">
<HydrateClient>
<Suspense fallback={<CardSkeleton />}>
<CurrentWork />
</Suspense>
</HydrateClient>
<QuickActions />
</div>
<HydrateClient>
<Suspense fallback={<CardSkeleton />}>
<RecentActivity />
</Suspense>
</HydrateClient>
</div>
</div>
);
}

View File

@@ -267,22 +267,22 @@ export function SettingsContent() {
label: "Clients",
value: dataStats?.clients ?? 0,
icon: Users,
color: "text-blue-600",
bgColor: "bg-blue-50 dark:bg-blue-900/20",
color: "text-primary",
bgColor: "bg-primary/10",
},
{
label: "Businesses",
value: dataStats?.businesses ?? 0,
icon: Building,
color: "text-purple-600",
bgColor: "bg-purple-50 dark:bg-purple-900/20",
color: "text-muted-foreground",
bgColor: "bg-muted",
},
{
label: "Invoices",
value: dataStats?.invoices ?? 0,
icon: FileText,
color: "text-emerald-600",
bgColor: "bg-emerald-50 dark:bg-emerald-900/20",
color: "text-primary",
bgColor: "bg-accent",
},
];
@@ -291,10 +291,10 @@ export function SettingsContent() {
{/* Profile & Account Overview */}
<div className="grid gap-6 lg:grid-cols-2">
{/* Profile Section */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="card-title-secondary">
<User className="text-icon-blue h-5 w-5" />
<CardTitle className="text-foreground flex items-center gap-2">
<User className="text-primary h-5 w-5" />
Profile Information
</CardTitle>
<CardDescription>
@@ -327,7 +327,7 @@ export function SettingsContent() {
<Button
type="submit"
disabled={updateProfileMutation.isPending}
className="btn-brand-primary"
variant="default"
>
{updateProfileMutation.isPending
? "Updating..."
@@ -338,10 +338,10 @@ export function SettingsContent() {
</Card>
{/* Data Overview */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="card-title-info">
<Database className="text-icon-blue h-5 w-5" />
<CardTitle className="text-foreground flex items-center gap-2">
<Database className="text-primary h-5 w-5" />
Account Data
</CardTitle>
<CardDescription>
@@ -355,10 +355,10 @@ export function SettingsContent() {
return (
<div
key={item.label}
className="bg-card flex items-center justify-between rounded-lg border p-4 transition-shadow hover:shadow-sm"
className="bg-card flex items-center justify-between border p-4 transition-shadow hover:shadow-sm"
>
<div className="flex items-center gap-3">
<div className={`rounded-lg p-2 ${item.bgColor}`}>
<div className={` p-2 ${item.bgColor}`}>
<Icon className={`h-4 w-4 ${item.color}`} />
</div>
<span className="font-medium">{item.label}</span>
@@ -378,10 +378,10 @@ export function SettingsContent() {
</div>
{/* Security Settings */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="card-title-secondary">
<Key className="text-icon-amber h-5 w-5" />
<CardTitle className="text-foreground flex items-center gap-2">
<Key className="text-primary h-5 w-5" />
Security Settings
</CardTitle>
<CardDescription>
@@ -474,7 +474,7 @@ export function SettingsContent() {
<Button
type="submit"
disabled={changePasswordMutation.isPending}
className="btn-brand-primary"
variant="default"
>
{changePasswordMutation.isPending
? "Changing Password..."
@@ -485,9 +485,9 @@ export function SettingsContent() {
</Card>
{/* Data Management */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="card-title-secondary">
<CardTitle className="text-foreground flex items-center gap-2">
<Shield className="text-icon-indigo h-5 w-5" />
Data Management
</CardTitle>
@@ -604,7 +604,7 @@ export function SettingsContent() {
disabled={
!importData.trim() || importDataMutation.isPending
}
className="btn-brand-primary"
variant="default"
>
{importDataMutation.isPending
? "Importing..."
@@ -617,7 +617,7 @@ export function SettingsContent() {
</div>
{/* Backup Information */}
<div className="border-border bg-muted/20 rounded-lg border p-4">
<div className="border-border bg-muted/20 border p-4">
<h4 className="font-medium">Backup Information</h4>
<ul className="text-muted-foreground mt-2 space-y-1 text-sm">
<li> Regular backups protect your important business data</li>
@@ -634,9 +634,9 @@ export function SettingsContent() {
</Card>
{/* Danger Zone */}
<Card className="card-primary border-l-4 border-l-red-500">
<Card className="bg-card border-border border border-l-4 border-l-red-500">
<CardHeader>
<CardTitle className="card-title-warning">
<CardTitle className="text-destructive flex items-center gap-2">
<AlertTriangle className="text-icon-red h-5 w-5" />
Danger Zone
</CardTitle>
@@ -646,8 +646,8 @@ export function SettingsContent() {
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="rounded-lg border border-red-200 bg-red-50 p-4 dark:border-red-800 dark:bg-red-900/20">
<h4 className="font-medium text-red-600 dark:text-red-400">
<div className="bg-destructive/10 border-destructive/20 border p-4">
<h4 className="text-destructive font-medium">
Delete All Account Data
</h4>
<p className="text-muted-foreground mt-2 text-sm">
@@ -672,7 +672,7 @@ export function SettingsContent() {
This action cannot be undone. This will permanently delete
all your:
</div>
<ul className="border-border bg-muted/50 list-inside list-disc space-y-1 rounded-lg border p-3 text-sm">
<ul className="border-border bg-muted/50 list-inside list-disc space-y-1 border p-3 text-sm">
<li>Client information and contact details</li>
<li>Business profiles and settings</li>
<li>Invoices and invoice line items</li>
@@ -703,7 +703,7 @@ export function SettingsContent() {
deleteConfirmText !== "delete all my data" ||
deleteDataMutation.isPending
}
className="bg-red-600 hover:bg-red-700"
className="bg-destructive hover:bg-destructive/90"
>
{deleteDataMutation.isPending
? "Deleting..."

View File

@@ -2,10 +2,10 @@ import "~/styles/globals.css";
import { Analytics } from "@vercel/analytics/next";
import { type Metadata } from "next";
import { Geist, Azeret_Mono } from "next/font/google";
import { Geist_Mono } from "next/font/google";
import { TRPCReactProvider } from "~/trpc/react";
import { Toaster } from "~/components/ui/toaster";
import { Toaster } from "~/components/ui/sonner";
export const metadata: Metadata = {
title: "beenvoice - Invoicing Made Simple",
@@ -14,14 +14,9 @@ export const metadata: Metadata = {
icons: [{ rel: "icon", url: "/favicon.ico" }],
};
const geist = Geist({
const geistMono = Geist_Mono({
subsets: ["latin"],
variable: "--font-geist-sans",
});
const azeretMono = Azeret_Mono({
subsets: ["latin"],
variable: "--font-azeret-mono",
variable: "--font-geist-mono",
display: "swap",
});
@@ -29,9 +24,9 @@ export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en" className={`${geist.variable} ${azeretMono.variable}`}>
<html lang="en" className={geistMono.variable}>
<Analytics />
<body className="relative min-h-screen overflow-x-hidden font-sans antialiased">
<body className="bg-background text-foreground relative min-h-screen overflow-x-hidden font-sans antialiased">
<TRPCReactProvider>{children}</TRPCReactProvider>
<Toaster />
</body>

View File

@@ -9,30 +9,32 @@ import {
Check,
Zap,
Shield,
Sparkles,
BarChart3,
Clock,
Rocket,
Heart,
ChevronRight,
Stars,
} from "lucide-react";
export default function HomePage() {
return (
<div className="bg-page-gradient min-h-screen">
<div className="bg-background min-h-screen">
<AuthRedirect />
{/* Navigation */}
<nav className="nav-sticky">
<nav className="bg-background border-border sticky top-0 z-50 border-b">
<div className="container mx-auto px-4">
<div className="flex h-14 items-center justify-between sm:h-16">
<Logo />
<div className="hidden items-center space-x-6 md:flex">
<a href="#features" className="nav-link">
<a
href="#features"
className="text-muted-foreground hover:text-foreground text-sm font-medium transition-colors"
>
Features
</a>
<a href="#pricing" className="nav-link">
<a
href="#pricing"
className="text-muted-foreground hover:text-foreground text-sm font-medium transition-colors"
>
Pricing
</a>
</div>
@@ -41,14 +43,14 @@ export default function HomePage() {
<Button
variant="ghost"
size="sm"
className="text-slate-700 hover:text-slate-900 dark:text-slate-200 dark:hover:text-white"
className="text-muted-foreground hover:text-foreground"
>
<span className="hidden sm:inline">Sign In</span>
<span className="sm:hidden">Sign In</span>
</Button>
</Link>
<Link href="/auth/register">
<Button size="sm" className="btn-brand-primary">
<Button size="sm" variant="default">
<span className="hidden sm:inline">Get Started</span>
<span className="sm:hidden">Start</span>
</Button>
@@ -59,96 +61,59 @@ export default function HomePage() {
</nav>
{/* Hero Section */}
<section className="bg-hero-gradient relative overflow-hidden px-4 pt-12 pb-16 sm:pt-20">
{/* Background decoration */}
<div className="hero-overlay"></div>
<div className="hero-orb-1 animate-glow-pulse animate-pulse"></div>
<div className="hero-orb-2 animate-rainbow animate-bounce"></div>
<div className="hero-orb-3 animate-glow-pulse animate-pulse"></div>
{/* Particle Effects */}
<div className="particles-container">
<div className="particle"></div>
<div className="particle"></div>
<div className="particle"></div>
<div className="particle"></div>
<div className="particle"></div>
<div className="particle"></div>
<div className="particle"></div>
<div className="particle"></div>
<div className="particle"></div>
</div>
{/* Floating icons */}
<div className="animate-float-slow hover:animate-wiggle absolute top-20 left-10">
<Stars className="h-6 w-6 cursor-pointer text-white/20 transition-colors hover:text-white/40" />
</div>
<div className="animate-float-delayed hover:animate-wiggle absolute top-32 right-16">
<Zap className="h-8 w-8 cursor-pointer text-emerald-300/30 transition-colors hover:text-emerald-300/60" />
</div>
<div className="animate-float hover:animate-wiggle absolute bottom-32 left-20">
<Heart className="h-5 w-5 cursor-pointer text-pink-300/25 transition-colors hover:text-pink-300/50" />
</div>
<section className="bg-background relative overflow-hidden px-4 pt-12 pb-16 sm:pt-20">
<div className="relative container mx-auto text-center">
<div className="mx-auto max-w-4xl">
<Badge className="badge-brand hover:animate-wiggle mb-4 animate-pulse cursor-pointer sm:mb-6">
<Sparkles className="hover:animate-rainbow mr-1 h-3 w-3 animate-spin" />
<Badge className="bg-primary/10 text-primary border-primary/20 mb-4 border sm:mb-6">
<Zap className="mr-1 h-3 w-3" />
Free Forever
</Badge>
<h1 className="animate-fade-in-up animate-glitch mb-4 text-4xl font-bold tracking-tight text-white sm:mb-6 sm:text-6xl lg:text-7xl">
<h1 className="text-foreground mb-4 text-4xl font-bold tracking-tight sm:mb-6 sm:text-6xl lg:text-7xl">
Simple Invoicing for
<span className="animate-text-shimmer neon-glow block bg-gradient-to-r from-emerald-50 via-white to-emerald-50 bg-[length:200%_100%] bg-clip-text text-transparent">
Freelancers
</span>
<span className="text-primary block">Freelancers</span>
</h1>
<p className="animate-fade-in-up animation-delay-300 mx-auto mb-6 max-w-2xl text-lg leading-relaxed text-emerald-50/90 sm:mb-8 sm:text-xl">
<p className="text-muted-foreground mx-auto mb-6 max-w-2xl text-lg leading-relaxed sm:mb-8 sm:text-xl">
Create professional invoices, manage clients, and track payments.
Built for freelancers and small businesses
<span className="animate-pulse font-semibold text-white">
<span className="text-foreground font-semibold">
completely free
</span>
.
</p>
<div className="btn-group animate-fade-in-up animation-delay-500">
<div className="flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
<Link href="/auth/register">
<Button
size="lg"
className="btn-brand-secondary btn-flashy group w-full transform px-6 py-3 text-base font-semibold shadow-xl transition-all duration-300 hover:scale-105 hover:shadow-2xl sm:w-auto sm:px-8 sm:py-4 sm:text-lg"
variant="default"
className="group w-full px-6 py-3 text-base font-semibold sm:w-auto sm:px-8 sm:py-4 sm:text-lg"
>
Get Started
<ArrowRight className="ml-2 h-4 w-4 transition-all group-hover:translate-x-1 group-hover:scale-110 group-hover:rotate-12 sm:h-5 sm:w-5" />
<ArrowRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1 sm:h-5 sm:w-5" />
</Button>
</Link>
<Link href="#features">
<Button
variant="outline"
size="lg"
className="btn-flashy group w-full transform border-white/30 px-6 py-3 text-base text-white transition-all duration-300 hover:scale-105 hover:bg-white/10 hover:shadow-xl sm:w-auto sm:px-8 sm:py-4 sm:text-lg"
className="group w-full px-6 py-3 text-base sm:w-auto sm:px-8 sm:py-4 sm:text-lg"
>
Learn More
<ChevronRight className="ml-2 h-4 w-4 transition-all group-hover:translate-x-1 group-hover:scale-110 group-hover:rotate-12 sm:h-5 sm:w-5" />
<ChevronRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1 sm:h-5 sm:w-5" />
</Button>
</Link>
</div>
<div className="animate-fade-in-up animation-delay-700 mt-8 flex flex-col items-center justify-center gap-2 text-sm text-emerald-50/80 sm:mt-12 sm:flex-row sm:gap-6">
<div className="text-muted-foreground mt-8 flex flex-col items-center justify-center gap-2 text-sm sm:mt-12 sm:flex-row sm:gap-6">
{[
"No credit card required",
"Setup in 2 minutes",
"Free forever",
].map((text, i) => (
<div
key={i}
className="animate-fade-in-up flex items-center gap-2"
style={{ animationDelay: `${800 + i * 100}ms` }}
>
<Check
className="h-4 w-4 animate-bounce text-emerald-100"
style={{ animationDelay: `${1000 + i * 150}ms` }}
/>
<div key={i} className="flex items-center gap-2">
<Check className="text-primary h-4 w-4" />
<span className="text-center">{text}</span>
</div>
))}
@@ -160,110 +125,125 @@ export default function HomePage() {
{/* Features Section */}
<section
id="features"
className="bg-features-gradient relative overflow-hidden py-16 sm:py-24"
className="bg-muted/20 relative overflow-hidden py-16 sm:py-24"
>
{/* Floating background elements */}
<div className="floating-decoration-1"></div>
<div className="floating-decoration-2"></div>
<div className="relative container mx-auto px-4">
<div className="mb-12 text-center sm:mb-16">
<Badge className="badge-features mb-4">
<Badge className="bg-primary/10 text-primary border-primary/20 mb-4 border">
<Zap className="mr-1 h-3 w-3" />
Features
</Badge>
<h2 className="mb-4 text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl lg:text-5xl dark:text-slate-100">
<h2 className="text-foreground mb-4 text-3xl font-bold tracking-tight sm:text-4xl lg:text-5xl">
Everything you need to
<span className="text-brand-gradient block">get paid</span>
<span className="text-primary block">get paid</span>
</h2>
<p className="mx-auto max-w-2xl text-lg text-slate-600 sm:text-xl dark:text-slate-300">
<p className="text-muted-foreground mx-auto max-w-2xl text-lg sm:text-xl">
Simple, powerful features for freelancers and small businesses.
</p>
</div>
<div className="grid gap-6 sm:gap-8 md:grid-cols-2 lg:grid-cols-3">
{/* Feature 1 */}
<Card className="card-floating interactive-card group animate-fade-in-up animate-glow-pulse transform transition-all duration-500 hover:scale-105 hover:shadow-2xl">
<Card className="bg-card border-border hover:border-primary/20 border transition-all">
<CardContent className="p-6 sm:p-8">
<div className="icon-bg-brand hover:animate-wiggle mb-4 animate-bounce">
<Rocket className="h-6 w-6 transition-all group-hover:scale-125 group-hover:rotate-12" />
<div className="bg-primary/10 text-primary mb-4 inline-flex p-3">
<Rocket className="h-6 w-6" />
</div>
<h3 className="mb-3 text-xl font-bold text-slate-900 dark:text-slate-100">
<h3 className="text-foreground mb-3 text-xl font-bold">
Quick Setup
</h3>
<p className="mb-4 text-slate-600 dark:text-slate-300">
<p className="text-muted-foreground mb-4">
Start creating invoices immediately. No complicated setup
required.
</p>
<ul className="feature-list">
<li className="feature-item">
<Check className="feature-check" />
Simple client management
<ul className="space-y-2">
<li className="flex items-center gap-2">
<Check className="text-primary h-4 w-4" />
<span className="text-muted-foreground text-sm">
Simple client management
</span>
</li>
<li className="feature-item">
<Check className="feature-check" />
Professional templates
<li className="flex items-center gap-2">
<Check className="text-primary h-4 w-4" />
<span className="text-muted-foreground text-sm">
Professional templates
</span>
</li>
<li className="feature-item">
<Check className="feature-check" />
Easy invoice sending
<li className="flex items-center gap-2">
<Check className="text-primary h-4 w-4" />
<span className="text-muted-foreground text-sm">
Easy invoice sending
</span>
</li>
</ul>
</CardContent>
</Card>
{/* Feature 2 */}
<Card className="card-floating interactive-card group animate-fade-in-up animation-delay-300 transform transition-all duration-500 hover:scale-105 hover:shadow-2xl">
<Card className="bg-card border-border hover:border-primary/20 border transition-all">
<CardContent className="p-6 sm:p-8">
<div className="icon-bg-blue mb-4 animate-pulse">
<BarChart3 className="h-6 w-6 transition-transform group-hover:scale-110" />
<div className="bg-primary/10 text-primary mb-4 inline-flex p-3">
<BarChart3 className="h-6 w-6" />
</div>
<h3 className="mb-3 text-xl font-bold text-slate-900 dark:text-slate-100">
<h3 className="text-foreground mb-3 text-xl font-bold">
Payment Tracking
</h3>
<p className="mb-4 text-slate-600 dark:text-slate-300">
<p className="text-muted-foreground mb-4">
Keep track of invoice status and monitor payments.
</p>
<ul className="feature-list">
<li className="feature-item">
<Check className="feature-check" />
Invoice status tracking
<ul className="space-y-2">
<li className="flex items-center gap-2">
<Check className="text-primary h-4 w-4" />
<span className="text-muted-foreground text-sm">
Invoice status tracking
</span>
</li>
<li className="feature-item">
<Check className="feature-check" />
Payment history
<li className="flex items-center gap-2">
<Check className="text-primary h-4 w-4" />
<span className="text-muted-foreground text-sm">
Payment history
</span>
</li>
<li className="feature-item">
<Check className="feature-check" />
Overdue notifications
<li className="flex items-center gap-2">
<Check className="text-primary h-4 w-4" />
<span className="text-muted-foreground text-sm">
Overdue notifications
</span>
</li>
</ul>
</CardContent>
</Card>
{/* Feature 3 */}
<Card className="card-floating interactive-card group animate-fade-in-up animation-delay-500 transform transition-all duration-500 hover:scale-105 hover:shadow-2xl">
<Card className="bg-card border-border hover:border-primary/20 border transition-all">
<CardContent className="p-6 sm:p-8">
<div className="icon-bg-purple animate-float mb-4">
<Shield className="h-6 w-6 transition-all group-hover:scale-110 group-hover:rotate-12" />
<div className="bg-primary/10 text-primary mb-4 inline-flex p-3">
<Shield className="h-6 w-6" />
</div>
<h3 className="mb-3 text-xl font-bold text-slate-900 dark:text-slate-100">
<h3 className="text-foreground mb-3 text-xl font-bold">
Professional Features
</h3>
<p className="mb-4 text-slate-600 dark:text-slate-300">
<p className="text-muted-foreground mb-4">
Professional features to help you get paid on time.
</p>
<ul className="feature-list">
<li className="feature-item">
<Check className="feature-check" />
PDF generation
<ul className="space-y-2">
<li className="flex items-center gap-2">
<Check className="text-primary h-4 w-4" />
<span className="text-muted-foreground text-sm">
PDF generation
</span>
</li>
<li className="feature-item">
<Check className="feature-check" />
Custom tax rates
<li className="flex items-center gap-2">
<Check className="text-primary h-4 w-4" />
<span className="text-muted-foreground text-sm">
Custom tax rates
</span>
</li>
<li className="feature-item">
<Check className="feature-check" />
Professional numbering
<li className="flex items-center gap-2">
<Check className="text-primary h-4 w-4" />
<span className="text-muted-foreground text-sm">
Professional numbering
</span>
</li>
</ul>
</CardContent>
@@ -275,223 +255,110 @@ export default function HomePage() {
{/* Pricing Section */}
<section
id="pricing"
className="bg-features-gradient relative overflow-hidden py-16 sm:py-24"
className="bg-background relative overflow-hidden py-16 sm:py-24"
>
{/* Floating background elements */}
<div className="floating-decoration-1"></div>
<div className="floating-decoration-2"></div>
<div className="relative container mx-auto px-4">
<div className="mb-12 text-center sm:mb-16">
<h2 className="mb-4 text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl lg:text-5xl dark:text-slate-100">
<h2 className="text-foreground mb-4 text-3xl font-bold tracking-tight sm:text-4xl lg:text-5xl">
Simple pricing
</h2>
<p className="mx-auto max-w-2xl text-lg text-slate-600 sm:text-xl dark:text-slate-300">
<p className="text-muted-foreground mx-auto max-w-2xl text-lg sm:text-xl">
Start free, stay free. No hidden fees or limits.
</p>
</div>
<div className="mx-auto max-w-md">
<Card className="animate-fade-in-up animate-glow-pulse interactive-card hover:shadow-3xl relative transform border-2 border-emerald-500 bg-white/90 shadow-2xl backdrop-blur-sm transition-all duration-500 hover:scale-105 dark:border-emerald-400 dark:bg-slate-800/90">
<div className="absolute -top-4 left-1/2 -translate-x-1/2">
<Badge className="badge-success animate-pulse px-6 py-1">
Forever Free
</Badge>
<Card className="bg-card border-primary border-2">
<div className="bg-primary/10 text-primary border-primary/20 mx-auto -mt-3 w-fit border px-6 py-1 text-sm font-medium">
Forever Free
</div>
<CardContent className="p-6 text-center sm:p-8">
<div className="mb-6">
<div className="animate-text-shimmer mb-2 bg-gradient-to-r from-emerald-500 via-teal-500 to-emerald-500 bg-[length:200%_100%] bg-clip-text text-5xl font-bold text-transparent sm:text-6xl">
<CardContent className="p-6 sm:p-8">
<div className="mb-6 text-center">
<div className="text-foreground mb-2 text-4xl font-bold sm:text-5xl">
$0
</div>
<div className="text-slate-600 dark:text-slate-400">
per month, forever
</div>
<p className="text-muted-foreground">
Forever. No credit card required.
</p>
</div>
<div className="mb-6 space-y-3 text-left sm:mb-8 sm:space-y-4">
<ul className="mb-8 space-y-3">
{[
"Unlimited invoices",
"Unlimited clients",
"Professional templates",
"PDF export",
"Client management",
"PDF generation",
"Payment tracking",
"Multi-business support",
"Line item details",
"Free forever",
"Professional templates",
"Custom tax rates",
"Email support",
].map((feature, i) => (
<div
key={i}
className="animate-fade-in-up flex items-center gap-3"
style={{ animationDelay: `${i * 100}ms` }}
>
<Check
className="h-5 w-5 flex-shrink-0 animate-bounce text-emerald-500"
style={{ animationDelay: `${200 + i * 100}ms` }}
/>
<span className="text-slate-700 dark:text-slate-300">
{feature}
</span>
</div>
<li key={i} className="flex items-center gap-3">
<Check className="text-primary h-5 w-5" />
<span className="text-foreground">{feature}</span>
</li>
))}
</div>
</ul>
<Link href="/auth/register">
<Button
variant="brand"
className="btn-flashy animate-magnetic w-full transform py-3 text-base font-semibold transition-all duration-300 hover:scale-105 hover:shadow-lg sm:text-lg"
>
Get Started
<Link href="/auth/register" className="block">
<Button className="w-full" size="lg">
Get Started Free
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</Link>
<p className="mt-4 text-sm text-slate-600 dark:text-slate-400">
No credit card required
</p>
</CardContent>
</Card>
</div>
</div>
</section>
{/* Why Choose */}
<section className="bg-features-gradient relative overflow-hidden py-16 sm:py-24">
{/* Floating background elements */}
<div className="floating-decoration-1"></div>
<div className="floating-decoration-2"></div>
<div className="relative container mx-auto px-4">
<div className="mb-12 text-center sm:mb-16">
<h2 className="mb-4 text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl lg:text-5xl dark:text-slate-100">
Why choose
<span className="animate-text-shimmer neon-glow animate-glitch block bg-gradient-to-r from-teal-500 via-emerald-600 to-teal-500 bg-[length:200%_100%] bg-clip-text text-transparent">
BeenVoice
</span>
</h2>
</div>
<div className="grid gap-6 sm:gap-8 md:grid-cols-3">
<div className="text-center">
<div className="icon-bg-emerald mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl text-white shadow-lg">
<Zap className="h-6 w-6" />
</div>
<h3 className="mb-3 text-xl font-bold text-slate-900 dark:text-slate-100">
Quick & Simple
</h3>
<p className="text-slate-600 dark:text-slate-300">
No learning curve. Start creating invoices in minutes.
</p>
</div>
<div className="text-center">
<div className="icon-bg-blue mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl text-white shadow-lg">
<Shield className="h-6 w-6" />
</div>
<h3 className="mb-3 text-xl font-bold text-slate-900 dark:text-slate-100">
Always Free
</h3>
<p className="text-slate-600 dark:text-slate-300">
No hidden fees, no premium tiers. All features are free.
</p>
</div>
<div className="text-center">
<div className="icon-bg-purple mb-4">
<Clock className="h-6 w-6" />
</div>
<h3 className="mb-3 text-xl font-bold text-slate-900 dark:text-slate-100">
Save Time
</h3>
<p className="text-slate-600 dark:text-slate-300">
Focus on your work, not paperwork. Automated calculations and
formatting.
</p>
</div>
</div>
</div>
</section>
{/* CTA Section */}
<section className="bg-hero-gradient relative overflow-hidden py-16 sm:py-24">
<div className="hero-overlay"></div>
<div className="hero-orb-1"></div>
<div className="hero-orb-2"></div>
<div className="hero-orb-3"></div>
<section className="bg-primary relative overflow-hidden py-16 sm:py-24">
<div className="relative container mx-auto px-4 text-center">
<div className="mx-auto max-w-3xl">
<h2 className="mb-4 text-3xl font-bold text-white sm:mb-6 sm:text-4xl lg:text-5xl">
Ready to get started?
</h2>
<p className="mb-6 text-lg text-emerald-50/90 sm:mb-8 sm:text-xl">
Join thousands of freelancers already using BeenVoice. Start
todaycompletely free.
</p>
<div className="flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
<Link href="/auth/register">
<Button
size="lg"
variant="secondary"
className="btn-brand-secondary btn-flashy group animate-glow-pulse w-full transform px-6 py-3 text-base font-semibold shadow-xl transition-all duration-300 hover:scale-105 hover:shadow-2xl sm:w-auto sm:px-8 sm:py-4 sm:text-lg"
>
Start Free Today
<Rocket className="ml-2 h-4 w-4 animate-bounce transition-all group-hover:translate-x-1 group-hover:scale-125 group-hover:rotate-45 sm:h-5 sm:w-5" />
</Button>
</Link>
</div>
<div className="mt-6 flex flex-col items-center justify-center gap-3 text-emerald-50/80 sm:mt-8 sm:flex-row sm:gap-6">
<div className="flex items-center gap-2">
<Heart className="h-4 w-4" />
Free forever
</div>
<div className="flex items-center gap-2">
<Shield className="h-4 w-4" />
Secure & private
</div>
<div className="flex items-center gap-2">
<Clock className="h-4 w-4" />
2-minute setup
</div>
</div>
<h2 className="text-primary-foreground mb-4 text-3xl font-bold tracking-tight sm:text-4xl lg:text-5xl">
Ready to get started?
</h2>
<p className="text-primary-foreground/80 mx-auto mb-8 max-w-2xl text-lg sm:text-xl">
Join thousands of freelancers who trust beenvoice for their
invoicing needs.
</p>
<div className="flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
<Link href="/auth/register">
<Button
variant="secondary"
size="lg"
className="group w-full px-6 py-3 text-base font-semibold sm:w-auto sm:px-8 sm:py-4 sm:text-lg"
>
Start Free Today
<ArrowRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1 sm:h-5 sm:w-5" />
</Button>
</Link>
</div>
</div>
</section>
{/* Footer */}
<footer className="bg-features-gradient border-t py-8 sm:py-12 dark:border-slate-700">
<footer className="bg-muted border-border border-t py-8">
<div className="container mx-auto px-4">
<div className="text-center">
<Logo className="mx-auto mb-4" />
<p className="mb-4 text-sm text-slate-600 sm:mb-6 sm:text-base dark:text-slate-400">
Simple invoicing for freelancers. Free, forever.
</p>
<div className="flex flex-wrap items-center justify-center gap-4 text-sm text-slate-600 sm:gap-6 dark:text-slate-400">
<div className="flex flex-col items-center justify-between gap-4 sm:flex-row">
<div className="flex items-center gap-2">
<Logo size="sm" />
<span className="text-muted-foreground text-sm">
© 2024 beenvoice. Built for freelancers.
</span>
</div>
<div className="flex items-center gap-6">
<Link
href="/auth/signin"
className="transition-colors hover:text-emerald-600 dark:hover:text-emerald-400"
className="text-muted-foreground hover:text-foreground text-sm transition-colors"
>
Sign In
</Link>
<Link
href="/auth/register"
className="transition-colors hover:text-emerald-600 dark:hover:text-emerald-400"
className="text-muted-foreground hover:text-foreground text-sm transition-colors"
>
Register
Get Started
</Link>
<a
href="#features"
className="transition-colors hover:text-emerald-600 dark:hover:text-emerald-400"
>
Features
</a>
<a
href="#pricing"
className="transition-colors hover:text-emerald-600 dark:hover:text-emerald-400"
>
Pricing
</a>
</div>
<div className="mt-6 border-t border-slate-200 pt-6 sm:mt-8 sm:pt-8 dark:border-slate-700">
<p className="text-sm text-slate-600 sm:text-base dark:text-slate-400">
&copy; 2025 Sean O&apos;Connor.
</p>
</div>
</div>
</div>

View File

@@ -64,7 +64,7 @@ export function AddressAutocomplete({
onBlur={() => setTimeout(() => setShowSuggestions(false), 150)}
/>
{showSuggestions && suggestions.length > 0 && (
<Card className="card-primary absolute z-10 mt-1 max-h-60 w-full overflow-auto">
<Card className="bg-card border-border border absolute z-10 mt-1 max-h-60 w-full overflow-auto">
<ul>
{suggestions.map((s) => (
<li

View File

@@ -1,26 +1,72 @@
import Image from "next/image";
"use client";
import { motion } from "framer-motion";
import { cn } from "~/lib/utils";
interface LogoProps {
className?: string;
size?: "sm" | "md" | "lg";
size?: "sm" | "md" | "lg" | "xl";
animated?: boolean;
}
export function Logo({ className, size = "md" }: LogoProps) {
export function Logo({ className, size = "md", animated = true }: LogoProps) {
const sizeClasses = {
sm: { width: 120, height: 32 },
md: { width: 160, height: 42 },
lg: { width: 240, height: 64 },
sm: "text-sm",
md: "text-lg",
lg: "text-2xl",
xl: "text-4xl",
};
const { width, height } = sizeClasses[size];
const LogoContent = () => (
<div className={cn("flex items-center", sizeClasses[size], className)}>
<span className="text-primary font-bold tracking-tight">$</span>
<span className="inline-block w-2"></span>
<span className="text-foreground font-bold tracking-tight">been</span>
<span className="text-foreground/70 font-bold tracking-tight">voice</span>
</div>
);
if (!animated) {
return <LogoContent />;
}
return (
<Image
src="/beenvoice-logo.svg"
alt="beenvoice logo"
width={width}
height={height}
className={className}
priority
/>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.1, ease: "easeOut" }}
className={cn("flex items-center", sizeClasses[size], className)}
>
<motion.span
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.02, duration: 0.05, ease: "easeOut" }}
className="text-primary font-bold tracking-tight"
>
$
</motion.span>
<motion.span
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.03, duration: 0.05, ease: "easeOut" }}
className="inline-block w-2"
></motion.span>
<motion.span
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.04, duration: 0.05, ease: "easeOut" }}
className="text-foreground font-bold tracking-tight"
>
been
</motion.span>
<motion.span
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.06, duration: 0.05, ease: "easeOut" }}
className="text-foreground/70 font-bold tracking-tight"
>
voice
</motion.span>
</motion.div>
);
}

View File

@@ -438,10 +438,10 @@ export function CSVImportPage() {
return (
<div className="space-y-6">
{/* Global Client Selection */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="card-title-secondary">
<Users className="text-icon-blue h-5 w-5" />
<CardTitle className="text-foreground flex items-center gap-2">
<Users className="text-primary h-5 w-5" />
Default Client
</CardTitle>
</CardHeader>
@@ -460,7 +460,7 @@ export function CSVImportPage() {
applyGlobalClient(newClientId);
}
}}
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-12 w-full rounded-md border px-3 py-2 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus:ring-1 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-12 w-full border px-3 py-2 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus:ring-1 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
disabled={loadingClients}
>
<option value="">No default client (select individually)</option>
@@ -479,10 +479,10 @@ export function CSVImportPage() {
</Card>
{/* File Upload Area */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="card-title-secondary">
<Upload className="text-icon-emerald h-5 w-5" />
<CardTitle className="text-foreground flex items-center gap-2">
<Upload className="text-primary h-5 w-5" />
Upload CSV Files
</CardTitle>
</CardHeader>
@@ -498,49 +498,45 @@ export function CSVImportPage() {
{/* Summary Card */}
{totalFiles > 0 && (
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="card-title-secondary">
<FileText className="text-icon-emerald h-5 w-5" />
<CardTitle className="text-foreground flex items-center gap-2">
<FileText className="text-primary h-5 w-5" />
Import Summary
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4 rounded-lg bg-green-50/50 p-4 md:grid-cols-4 dark:bg-green-900/10">
<div className="bg-primary/10 grid grid-cols-2 gap-4 p-4 md:grid-cols-4">
<div className="text-center">
<div className="text-2xl font-bold text-green-600">
<div className="text-primary text-2xl font-bold">
{totalFiles}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
Files
</div>
<div className="text-muted-foreground text-sm">Files</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-green-600">
<div className="text-primary text-2xl font-bold">
{totalItems}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
<div className="text-muted-foreground text-sm">
Total Items
</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-green-600">
<div className="text-primary text-2xl font-bold">
{totalAmount.toLocaleString("en-US", {
style: "currency",
currency: "USD",
})}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
<div className="text-muted-foreground text-sm">
Total Amount
</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-green-600">
<div className="text-primary text-2xl font-bold">
{readyFiles}/{totalFiles}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
Ready
</div>
<div className="text-muted-foreground text-sm">Ready</div>
</div>
</div>
</CardContent>
@@ -551,9 +547,9 @@ export function CSVImportPage() {
{/* File List */}
{files.length > 0 && (
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="card-title-secondary">
<CardTitle className="text-foreground flex items-center gap-2">
Uploaded Files
</CardTitle>
</CardHeader>
@@ -562,11 +558,11 @@ export function CSVImportPage() {
{files.map((fileData, index) => (
<div
key={index}
className="border-border bg-card rounded-lg border p-4"
className="border-border bg-card border p-4"
>
<div className="mb-4 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<FileText className="text-icon-emerald h-5 w-5" />
<FileText className="text-primary h-5 w-5" />
<div>
<h3 className="text-foreground truncate font-medium">
{fileData.file.name}
@@ -593,7 +589,7 @@ export function CSVImportPage() {
variant="outline"
size="sm"
onClick={() => removeFile(index)}
className="text-red-600 hover:text-red-700"
className="text-destructive hover:text-destructive/80"
>
<Trash2 className="mr-1 h-4 w-4" />
Remove
@@ -623,7 +619,7 @@ export function CSVImportPage() {
onChange={(e) =>
updateFileData(index, { clientId: e.target.value })
}
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-9 w-full rounded-md border px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus:ring-1 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-9 w-full border px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus:ring-1 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
disabled={loadingClients}
>
<option value="">Select Client</option>
@@ -666,20 +662,20 @@ export function CSVImportPage() {
{/* Error Display */}
{fileData.errors.length > 0 && (
<div className="mt-4 rounded-lg border border-red-200 bg-red-50 p-3 dark:border-red-800 dark:bg-red-900/20">
<div className="border-destructive/20 bg-destructive/10 mt-4 border p-3">
<div className="mb-2 flex items-center gap-2">
<AlertCircle className="h-4 w-4 text-red-600" />
<span className="text-sm font-medium text-red-800 dark:text-red-200">
<AlertCircle className="text-destructive h-4 w-4" />
<span className="text-destructive text-sm font-medium">
Issues Found
</span>
</div>
<ul className="space-y-1 text-sm text-red-700 dark:text-red-300">
<ul className="text-destructive space-y-1 text-sm">
{fileData.errors.map((error, errorIndex) => (
<li
key={errorIndex}
className="flex items-start gap-2"
>
<span className="text-red-600"></span>
<span className="text-destructive"></span>
<span>{error}</span>
</li>
))}
@@ -735,10 +731,10 @@ export function CSVImportPage() {
{/* Batch Actions */}
{files.length > 0 && (
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="card-title-secondary">
<DollarSign className="text-icon-green h-5 w-5" />
<CardTitle className="text-foreground flex items-center gap-2">
<DollarSign className="text-primary h-5 w-5" />
Create Invoices
</CardTitle>
</CardHeader>
@@ -762,7 +758,7 @@ export function CSVImportPage() {
<Button
onClick={processBatch}
disabled={readyFiles === 0 || isProcessing}
className="btn-brand-primary"
variant="default"
>
{isProcessing
? "Processing..."
@@ -776,10 +772,10 @@ export function CSVImportPage() {
{/* Preview Modal */}
<Dialog open={previewModalOpen} onOpenChange={setPreviewModalOpen}>
<DialogContent className="card-primary flex max-h-[90vh] max-w-4xl flex-col">
<DialogContent className="bg-card border-border border flex max-h-[90vh] max-w-4xl flex-col">
<DialogHeader className="flex-shrink-0">
<DialogTitle className="flex items-center gap-2 text-xl font-bold text-gray-800">
<FileText className="h-5 w-5 text-emerald-600" />
<DialogTitle className="text-foreground flex items-center gap-2 text-xl font-bold">
<FileText className="text-primary h-5 w-5" />
{selectedFileIndex !== null &&
files[selectedFileIndex]?.file.name}
</DialogTitle>
@@ -792,14 +788,14 @@ export function CSVImportPage() {
<div className="flex min-h-0 flex-1 flex-col space-y-4">
<div className="grid flex-shrink-0 grid-cols-1 gap-4 md:grid-cols-3">
<div className="flex items-center gap-2">
<FileText className="h-4 w-4 text-emerald-600" />
<span className="text-sm text-gray-600">
<FileText className="text-primary h-4 w-4" />
<span className="text-muted-foreground text-sm">
{files[selectedFileIndex].parsedItems.length} items
</span>
</div>
<div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-emerald-600" />
<span className="text-sm text-gray-600">
<Clock className="text-primary h-4 w-4" />
<span className="text-muted-foreground text-sm">
{files[selectedFileIndex].parsedItems
.reduce((sum, item) => sum + item.hours, 0)
.toFixed(1)}{" "}
@@ -807,8 +803,8 @@ export function CSVImportPage() {
</span>
</div>
<div className="flex items-center gap-2">
<DollarSign className="h-4 w-4 text-emerald-600" />
<span className="text-sm font-medium text-gray-600">
<DollarSign className="text-primary h-4 w-4" />
<span className="text-muted-foreground text-sm font-medium">
{files[selectedFileIndex].parsedItems
.reduce((sum, item) => sum + item.amount, 0)
.toLocaleString("en-US", {

View File

@@ -73,7 +73,7 @@ export function ClientList() {
return (
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: 3 }, (_, i: number) => (
<Card key={i} className="card-primary">
<Card key={i} className="bg-card border-border border">
<CardHeader>
<div className="h-4 animate-pulse rounded bg-gray-200" />
</CardHeader>
@@ -91,9 +91,9 @@ export function ClientList() {
if (!clients || clients.length === 0) {
return (
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader className="text-center">
<CardTitle className="text-brand-gradient text-2xl font-bold">
<CardTitle className="text-primary text-2xl font-bold">
No Clients Yet
</CardTitle>
<CardDescription className="text-lg">
@@ -142,20 +142,20 @@ export function ClientList() {
{filteredClients.map((client) => (
<Card
key={client.id}
className="group card-primary transition-all duration-300 hover:shadow-lg"
className="group bg-card border-border border transition-all duration-300 hover:shadow-lg"
>
<CardHeader>
<CardTitle className="flex items-center justify-between text-lg">
<span className="text-accent group-hover:text-icon-emerald font-semibold transition-colors">
<span className="text-foreground group-hover:text-primary font-semibold transition-colors">
{client.name}
</span>
<div className="flex space-x-1 opacity-0 transition-opacity group-hover:opacity-100">
<Link href={`/clients/${client.id}`}>
<Link href={`/dashboard/clients/${client.id}`}>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<Eye className="h-4 w-4" />
</Button>
</Link>
<Link href={`/clients/${client.id}/edit`}>
<Link href={`/dashboard/clients/${client.id}/edit`}>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<Edit className="h-4 w-4" />
</Button>
@@ -173,25 +173,25 @@ export function ClientList() {
</CardHeader>
<CardContent className="space-y-3">
{client.email && (
<div className="text-secondary flex items-center text-sm">
<div className="bg-brand-muted mr-3 rounded p-1.5">
<Mail className="text-icon-emerald h-3 w-3" />
<div className="text-muted-foreground flex items-center text-sm">
<div className="bg-muted mr-3 rounded p-1.5">
<Mail className="text-muted-foreground h-3 w-3" />
</div>
{client.email}
</div>
)}
{client.phone && (
<div className="text-secondary flex items-center text-sm">
<div className="bg-brand-muted-blue mr-3 rounded p-1.5">
<Phone className="text-icon-blue h-3 w-3" />
<div className="text-muted-foreground flex items-center text-sm">
<div className="bg-muted mr-3 rounded p-1.5">
<Phone className="text-muted-foreground h-3 w-3" />
</div>
{client.phone}
</div>
)}
{(client.addressLine1 ?? client.city ?? client.state) && (
<div className="text-secondary flex items-start text-sm">
<div className="bg-brand-muted-teal mt-0.5 mr-3 flex-shrink-0 rounded p-1.5">
<MapPin className="text-icon-teal h-3 w-3" />
<div className="text-muted-foreground flex items-start text-sm">
<div className="bg-muted mt-0.5 mr-3 flex-shrink-0 rounded p-1.5">
<MapPin className="text-muted-foreground h-3 w-3" />
</div>
<div className="min-w-0">
{client.addressLine1 && <div>{client.addressLine1}</div>}
@@ -213,12 +213,12 @@ export function ClientList() {
</div>
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent className="card-primary">
<DialogContent className="bg-card border-border border">
<DialogHeader>
<DialogTitle className="text-accent text-xl font-bold">
<DialogTitle className="text-foreground text-xl font-bold">
Delete Client
</DialogTitle>
<DialogDescription className="text-secondary">
<DialogDescription className="text-muted-foreground">
Are you sure you want to delete this client? This action cannot be
undone.
</DialogDescription>
@@ -227,14 +227,14 @@ export function ClientList() {
<Button
variant="outline"
onClick={() => setDeleteDialogOpen(false)}
className="text-secondary"
className="text-muted-foreground"
>
Cancel
</Button>
<Button
variant="destructive"
onClick={confirmDelete}
className="bg-red-600 hover:bg-red-700"
className="bg-destructive hover:bg-destructive/90"
>
Delete
</Button>

View File

@@ -28,10 +28,10 @@ export function CurrentOpenInvoiceCard() {
if (isLoading) {
return (
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader className="pb-3">
<CardTitle className="card-title-secondary">
<FileText className="text-icon-emerald h-5 w-5" />
<CardTitle className="text-foreground flex items-center gap-2">
<FileText className="text-primary h-5 w-5" />
Current Open Invoice
</CardTitle>
</CardHeader>
@@ -49,10 +49,10 @@ export function CurrentOpenInvoiceCard() {
if (!currentInvoice) {
return (
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader className="pb-3">
<CardTitle className="card-title-secondary">
<FileText className="text-icon-emerald h-5 w-5" />
<CardTitle className="text-foreground flex items-center gap-2">
<FileText className="text-primary h-5 w-5" />
Current Open Invoice
</CardTitle>
</CardHeader>
@@ -80,10 +80,10 @@ export function CurrentOpenInvoiceCard() {
const totalAmount = currentInvoice.totalAmount;
return (
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader className="pb-3">
<CardTitle className="card-title-secondary">
<FileText className="text-icon-emerald h-5 w-5" />
<CardTitle className="text-foreground flex items-center gap-2">
<FileText className="text-primary h-5 w-5" />
Current Open Invoice
</CardTitle>
</CardHeader>
@@ -91,13 +91,13 @@ export function CurrentOpenInvoiceCard() {
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Badge className="badge-secondary text-xs">
<Badge className="bg-secondary text-secondary-foreground text-xs">
{currentInvoice.invoiceNumber}
</Badge>
<Badge className="badge-outline text-xs">Draft</Badge>
<Badge className="border text-xs">Draft</Badge>
</div>
<div className="text-right">
<p className="text-icon-emerald text-sm font-medium">
<p className="text-primary text-sm font-medium">
{formatCurrency(totalAmount)}
</p>
</div>

View File

@@ -215,12 +215,12 @@ export function DataTable<TData, TValue>({
{/* Filter Bar Card */}
{(showSearch || filterableColumns.length > 0 || showColumnVisibility) && (
<Card className="card-primary py-2">
<Card className="bg-card border-border border py-2">
<CardContent className="px-3 py-0">
<div className="flex items-center gap-2">
{showSearch && (
<div className="relative min-w-0 flex-1">
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
<Search className="text-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
<Input
placeholder={searchPlaceholder}
value={globalFilter ?? ""}
@@ -244,7 +244,7 @@ export function DataTable<TData, TValue>({
>
<SelectTrigger className="h-9 w-9 p-0 sm:w-[180px] sm:px-3 [&>svg]:hidden sm:[&>svg]:inline-flex">
<div className="flex w-full items-center justify-center">
<Filter className="h-4 w-4 sm:hidden" />
<Filter className="text-foreground h-4 w-4 sm:hidden" />
<span className="hidden sm:inline">
<SelectValue placeholder={column.title} />
</span>
@@ -272,7 +272,7 @@ export function DataTable<TData, TValue>({
>
<X className="h-4 w-4 sm:hidden" />
<span className="hidden sm:flex sm:items-center">
<Filter className="mr-2 h-3.5 w-3.5" />
<Filter className="text-foreground mr-2 h-3.5 w-3.5" />
Clear filters
</span>
</Button>
@@ -315,7 +315,7 @@ export function DataTable<TData, TValue>({
)}
{/* Table Content Card */}
<Card className="card-primary overflow-hidden p-0">
<Card className="bg-card border-border overflow-hidden border p-0">
<div className="w-full overflow-x-auto">
<Table>
<TableHeader>
@@ -400,7 +400,7 @@ export function DataTable<TData, TValue>({
{/* Pagination Bar Card */}
{showPagination && (
<Card className="card-primary py-2">
<Card className="bg-card border-border border py-2">
<CardContent className="px-3 py-0">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
@@ -561,17 +561,17 @@ export function DataTableSkeleton({
return (
<div className="space-y-4">
{/* Filter bar skeleton */}
<Card className="card-primary py-2">
<Card className="bg-card border-border border py-2">
<CardContent className="px-3 py-0">
<div className="flex items-center gap-2">
<div className="bg-muted/30 h-9 w-full flex-1 animate-pulse rounded-md sm:max-w-sm"></div>
<div className="bg-muted/30 h-9 w-24 animate-pulse rounded-md"></div>
<div className="bg-muted/30 h-9 w-full flex-1 animate-pulse sm:max-w-sm"></div>
<div className="bg-muted/30 h-9 w-24 animate-pulse"></div>
</div>
</CardContent>
</Card>
{/* Table skeleton */}
<Card className="card-primary overflow-hidden p-0">
<Card className="bg-card border-border overflow-hidden border p-0">
<div className="w-full overflow-x-auto">
<Table>
<TableHeader>
@@ -632,7 +632,7 @@ export function DataTableSkeleton({
</Card>
{/* Pagination skeleton */}
<Card className="card-primary py-2">
<Card className="bg-card border-border border py-2">
<CardContent className="px-3 py-0">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">

View File

@@ -87,7 +87,7 @@ function SortableItem({
<div
ref={setNodeRef}
style={style}
className={`card-secondary rounded-lg transition-colors ${
className={`card-secondary transition-colors ${
isDragging ? "opacity-50 shadow-lg" : ""
}`}
>
@@ -154,7 +154,7 @@ function SortableItem({
{/* Amount */}
<div className="col-span-1">
<div className="bg-muted/30 flex h-9 items-center rounded-md border px-3 font-medium text-emerald-600">
<div className="bg-muted/30 text-primary flex h-9 items-center border px-3 font-medium">
${item.amount.toFixed(2)}
</div>
</div>
@@ -166,7 +166,7 @@ function SortableItem({
onClick={() => onRemove(index)}
variant="ghost"
size="sm"
className="h-9 w-9 p-0 text-red-600 hover:bg-red-50 hover:text-red-700"
className="text-destructive hover:bg-destructive/10 hover:text-destructive/80 h-9 w-9 p-0"
>
<Trash2 className="h-4 w-4" />
</Button>
@@ -206,7 +206,7 @@ function SortableItem({
onClick={() => onRemove(index)}
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-red-600 hover:bg-red-50 hover:text-red-700"
className="text-destructive hover:bg-destructive/10 hover:text-destructive/80 h-6 w-6 p-0"
>
<Trash2 className="h-3 w-3" />
</Button>
@@ -266,10 +266,10 @@ function SortableItem({
</div>
{/* Amount */}
<div className="bg-muted/20 rounded-md border p-3">
<div className="bg-muted/20 border p-3">
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-sm">Total Amount:</span>
<span className="font-mono text-lg font-bold text-emerald-600">
<span className="text-primary font-mono text-lg font-bold">
${item.amount.toFixed(2)}
</span>
</div>
@@ -362,7 +362,7 @@ export function EditableInvoiceItems({
{items.map((item, _index) => (
<div
key={item.id}
className="card-secondary animate-pulse rounded-lg p-4"
className="card-secondary animate-pulse p-4"
>
{/* Desktop Skeleton */}
<div className="hidden grid-cols-12 gap-3 md:grid">

View File

@@ -150,12 +150,12 @@ export function InvoiceList() {
<CardTitle className="flex items-center justify-between">
<span className="truncate">{invoice.invoiceNumber}</span>
<div className="flex space-x-1">
<Link href={`/dashboard/invoices/${invoice.id}/view`}>
<Link href={`/dashboard/invoices/${invoice.id}`}>
<Button variant="ghost" size="sm">
<Eye className="h-4 w-4" />
</Button>
</Link>
<Link href={`/dashboard/invoices/${invoice.id}`}>
<Link href={`/dashboard/invoices/${invoice.id}/edit`}>
<Button variant="ghost" size="sm">
<Edit className="h-4 w-4" />
</Button>
@@ -171,7 +171,7 @@ export function InvoiceList() {
</CardTitle>
<div className="flex items-center justify-between">
<StatusBadge status={invoice.status as StatusType} />
<span className="text-icon-green text-lg font-bold">
<span className="text-primary text-lg font-bold">
{formatCurrency(invoice.totalAmount)}
</span>
</div>

View File

@@ -148,7 +148,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
return (
<div className="py-12 text-center">
<FileText className="text-muted mx-auto mb-4 h-12 w-12" />
<h3 className="text-accent mb-2 text-lg font-medium">
<h3 className="text-foreground mb-2 text-lg font-medium">
Invoice not found
</h3>
<p className="text-muted mb-4">
@@ -169,9 +169,9 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
<div className="space-y-6">
{/* Status Alert */}
{isOverdue && (
<Card className="border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-900/20">
<Card className="border-destructive/20 bg-destructive/10">
<CardContent className="p-4">
<div className="text-error flex items-center gap-2">
<div className="text-destructive flex items-center gap-2">
<AlertCircle className="h-5 w-5" />
<span className="font-medium">This invoice is overdue</span>
</div>
@@ -183,13 +183,13 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
{/* Main Content */}
<div className="space-y-6 lg:col-span-2">
{/* Invoice Header Card */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardContent>
<div className="flex items-start justify-between">
<div className="space-y-4">
<div className="flex items-center gap-3">
<div className="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/30">
<FileText className="h-6 w-6 text-emerald-600 dark:text-emerald-400" />
<div className="bg-primary/10 p-2">
<FileText className="text-primary h-6 w-6" />
</div>
<div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
@@ -228,7 +228,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
>
<StatusIcon className="mr-1 h-3 w-3" />
</StatusBadge>
<div className="text-3xl font-bold text-emerald-600 dark:text-emerald-400">
<div className="text-primary text-3xl font-bold">
{formatCurrency(invoice.totalAmount)}
</div>
<Button
@@ -239,7 +239,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
>
{isExportingPDF ? (
<>
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
<div className="mr-2 h-4 w-4 animate-spin border-2 border-white border-t-transparent" />
Generating PDF...
</>
) : (
@@ -255,9 +255,9 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
</Card>
{/* Client Information */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-emerald-700 dark:text-emerald-400">
<CardTitle className="text-primary flex items-center gap-2">
<User className="h-5 w-5" />
Bill To
</CardTitle>
@@ -318,31 +318,31 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
</Card>
{/* Invoice Items */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-emerald-700 dark:text-emerald-400">
<CardTitle className="text-primary flex items-center gap-2">
<Clock className="h-5 w-5" />
Invoice Items
</CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
<div className="border-border overflow-hidden border">
<table className="w-full">
<thead className="bg-gray-50 dark:bg-gray-700">
<thead className="bg-muted">
<tr>
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-300">
<th className="text-muted-foreground px-4 py-3 text-left text-sm font-semibold">
Date
</th>
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-300">
<th className="text-muted-foreground px-4 py-3 text-left text-sm font-semibold">
Description
</th>
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-700 dark:text-gray-300">
<th className="text-muted-foreground px-4 py-3 text-right text-sm font-semibold">
Hours
</th>
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-700 dark:text-gray-300">
<th className="text-muted-foreground px-4 py-3 text-right text-sm font-semibold">
Rate
</th>
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-700 dark:text-gray-300">
<th className="text-muted-foreground px-4 py-3 text-right text-sm font-semibold">
Amount
</th>
</tr>
@@ -351,21 +351,21 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
{invoice.items?.map((item, index) => (
<tr
key={item.id || index}
className="border-t border-gray-100 hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-700"
className="border-border hover:bg-muted/50 border-t"
>
<td className="px-4 py-3 text-sm text-gray-900 dark:text-gray-300">
<td className="text-foreground px-4 py-3 text-sm">
{formatDate(item.date)}
</td>
<td className="px-4 py-3 text-sm text-gray-900 dark:text-gray-300">
<td className="text-foreground px-4 py-3 text-sm">
{item.description}
</td>
<td className="px-4 py-3 text-right text-sm text-gray-900 dark:text-gray-300">
<td className="text-foreground px-4 py-3 text-right text-sm">
{item.hours}
</td>
<td className="px-4 py-3 text-right text-sm text-gray-900 dark:text-gray-300">
<td className="text-foreground px-4 py-3 text-right text-sm">
{formatCurrency(item.rate)}
</td>
<td className="px-4 py-3 text-right text-sm font-medium text-gray-900 dark:text-gray-300">
<td className="text-foreground px-4 py-3 text-right text-sm font-medium">
{formatCurrency(item.amount)}
</td>
</tr>
@@ -378,11 +378,9 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
{/* Notes */}
{invoice.notes && (
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="text-emerald-700 dark:text-emerald-400">
Notes
</CardTitle>
<CardTitle className="text-primary">Notes</CardTitle>
</CardHeader>
<CardContent>
<p className="whitespace-pre-wrap text-gray-700 dark:text-gray-300">
@@ -396,18 +394,16 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
{/* Sidebar */}
<div className="space-y-6">
{/* Status Actions */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="text-emerald-700 dark:text-emerald-400">
Status Actions
</CardTitle>
<CardTitle className="text-primary">Status Actions</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{invoice.status === "draft" && (
<Button
onClick={() => handleStatusUpdate("sent")}
disabled={updateStatus.isPending}
className="w-full bg-blue-600 text-white hover:bg-blue-700"
className="bg-primary text-primary-foreground hover:bg-primary/90 w-full"
>
<Send className="mr-2 h-4 w-4" />
Mark as Sent
@@ -418,7 +414,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
<Button
onClick={() => handleStatusUpdate("paid")}
disabled={updateStatus.isPending}
className="w-full bg-green-600 text-white hover:bg-green-700"
className="bg-primary text-primary-foreground hover:bg-primary/90 w-full"
>
<DollarSign className="mr-2 h-4 w-4" />
Mark as Paid
@@ -429,7 +425,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
<Button
onClick={() => handleStatusUpdate("paid")}
disabled={updateStatus.isPending}
className="w-full bg-green-600 text-white hover:bg-green-700"
className="bg-primary text-primary-foreground hover:bg-primary/90 w-full"
>
<DollarSign className="mr-2 h-4 w-4" />
Mark as Paid
@@ -438,21 +434,17 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
{invoice.status === "paid" && (
<div className="py-4 text-center">
<DollarSign className="mx-auto mb-2 h-8 w-8 text-green-600 dark:text-green-400" />
<p className="font-medium text-green-600 dark:text-green-400">
Invoice Paid
</p>
<DollarSign className="text-primary mx-auto mb-2 h-8 w-8" />
<p className="text-primary font-medium">Invoice Paid</p>
</div>
)}
</CardContent>
</Card>
{/* Invoice Summary */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<CardTitle className="text-emerald-700 dark:text-emerald-400">
Summary
</CardTitle>
<CardTitle className="text-primary">Summary</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3">
@@ -471,14 +463,14 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
<Separator />
<div className="flex justify-between text-lg font-bold">
<span className="dark:text-white">Total</span>
<span className="text-emerald-600 dark:text-emerald-400">
<span className="text-primary">
{formatCurrency(invoice.totalAmount)}
</span>
</div>
</div>
<div className="border-t border-gray-200 pt-4 text-center dark:border-gray-700">
<p className="text-sm text-gray-500 dark:text-gray-400">
<div className="border-border border-t pt-4 text-center">
<p className="text-muted-foreground text-sm">
{invoice.items?.length ?? 0} item
{invoice.items?.length !== 1 ? "s" : ""}
</p>
@@ -487,17 +479,15 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
</Card>
{/* Danger Zone */}
<Card className="card-primary border-red-200 dark:border-red-800">
<Card className="bg-card border-border border border-destructive/20">
<CardHeader>
<CardTitle className="text-red-700 dark:text-red-400">
Danger Zone
</CardTitle>
<CardTitle className="text-destructive">Danger Zone</CardTitle>
</CardHeader>
<CardContent>
<Button
onClick={handleDelete}
variant="outline"
className="w-full border-red-200 text-red-700 hover:bg-red-50 dark:border-red-800 dark:text-red-400 dark:hover:bg-red-900/20"
className="border-destructive/20 text-destructive hover:bg-destructive/10 w-full"
>
<Trash2 className="mr-2 h-4 w-4" />
Delete Invoice
@@ -509,7 +499,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
{/* Delete Confirmation Dialog */}
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent className="card-primary">
<DialogContent className="bg-card border-border border">
<DialogHeader>
<DialogTitle className="text-xl font-bold text-gray-800 dark:text-white">
Delete Invoice
@@ -524,7 +514,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
<Button
variant="outline"
onClick={() => setDeleteDialogOpen(false)}
className="border-gray-300 text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"
className="border-border text-muted-foreground hover:bg-muted"
>
Cancel
</Button>
@@ -532,7 +522,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
variant="destructive"
onClick={confirmDelete}
disabled={deleteInvoice.isPending}
className="bg-red-600 hover:bg-red-700"
className="bg-destructive hover:bg-destructive/90"
>
{deleteInvoice.isPending ? "Deleting..." : "Delete Invoice"}
</Button>

View File

@@ -22,8 +22,8 @@ const variantStyles = {
background: "bg-muted/50",
},
success: {
icon: "text-status-success",
background: "bg-status-success-muted",
icon: "text-primary",
background: "bg-primary/10",
},
warning: {
icon: "text-status-warning",
@@ -67,9 +67,7 @@ export function StatsCard({
<span
className={cn(
"text-sm font-medium",
trend.isPositive
? "text-status-success"
: "text-status-error",
trend.isPositive ? "text-primary" : "text-destructive",
)}
>
{trend.isPositive ? "+" : ""}
@@ -82,7 +80,7 @@ export function StatsCard({
)}
</div>
{Icon && (
<div className={cn("rounded-full p-3", styles.background)}>
<div className={cn(" p-3", styles.background)}>
<Icon className={cn("h-6 w-6", styles.icon)} />
</div>
)}
@@ -94,7 +92,7 @@ export function StatsCard({
export function StatsCardSkeleton() {
return (
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardContent className="p-6">
<div className="animate-pulse">
<div className="bg-muted mb-2 h-4 w-1/2 rounded"></div>

View File

@@ -19,19 +19,15 @@ interface StatusBadgeProps
}
const statusClassMap: Record<StatusType, string> = {
draft:
"border-slate-400 bg-slate-100/90 text-slate-800 shadow-md dark:border-slate-600 dark:bg-slate-700/90 dark:text-slate-200",
sent: "border-blue-400 bg-blue-100/90 text-blue-800 shadow-md dark:border-blue-600 dark:bg-blue-700/90 dark:text-blue-200",
paid: "border-green-400 bg-green-100/90 text-green-800 shadow-md dark:border-green-600 dark:bg-green-700/90 dark:text-green-200",
overdue:
"border-red-400 bg-red-100/90 text-red-800 shadow-md dark:border-red-600 dark:bg-red-700/90 dark:text-red-200",
success:
"border-green-400 bg-green-100/90 text-green-800 shadow-md dark:border-green-600 dark:bg-green-700/90 dark:text-green-200",
draft: "border-muted-foreground/40 bg-muted text-muted-foreground shadow-sm",
sent: "border-primary/40 bg-primary/10 text-primary shadow-sm",
paid: "border-primary/40 bg-primary/10 text-primary shadow-sm",
overdue: "border-destructive/40 bg-destructive/10 text-destructive shadow-sm",
success: "border-primary/40 bg-primary/10 text-primary shadow-sm",
warning:
"border-yellow-400 bg-yellow-100/90 text-yellow-800 shadow-md dark:border-yellow-600 dark:bg-yellow-700/90 dark:text-yellow-200",
error:
"border-red-400 bg-red-100/90 text-red-800 shadow-md dark:border-red-600 dark:bg-red-700/90 dark:text-red-200",
info: "border-blue-400 bg-blue-100/90 text-blue-800 shadow-md dark:border-blue-600 dark:bg-blue-700/90 dark:text-blue-200",
"border-muted-foreground/40 bg-muted text-muted-foreground shadow-sm",
error: "border-destructive/40 bg-destructive/10 text-destructive shadow-sm",
info: "border-primary/40 bg-primary/10 text-primary shadow-sm",
};
const statusLabelMap: Record<StatusType, string> = {

View File

@@ -414,7 +414,8 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
type="submit"
form="business-form"
disabled={isSubmitting}
className="btn-brand-primary shadow-md"
variant="default"
className="shadow-md"
>
{isSubmitting ? (
<>
@@ -438,11 +439,11 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
{/* Main Form Container - styled like data table */}
<div className="space-y-4">
{/* Basic Information */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<div className="flex items-center gap-3">
<div className="bg-brand-muted flex h-10 w-10 items-center justify-center rounded-lg">
<Building className="text-brand-light h-5 w-5" />
<div className="bg-muted flex h-10 w-10 items-center justify-center ">
<Building className="text-muted-foreground h-5 w-5" />
</div>
<div>
<CardTitle>Basic Information</CardTitle>
@@ -565,12 +566,12 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
</Card>
{/* Address */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<div className="flex items-center gap-3">
<div className="bg-brand-muted flex h-10 w-10 items-center justify-center rounded-lg">
<div className="bg-muted flex h-10 w-10 items-center justify-center ">
<svg
className="text-brand-light h-5 w-5"
className="text-muted-foreground h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@@ -613,11 +614,11 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
</Card>
{/* Email Configuration */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<div className="flex items-center gap-3">
<div className="bg-brand-muted flex h-10 w-10 items-center justify-center rounded-lg">
<Mail className="text-brand-light h-5 w-5" />
<div className="bg-muted flex h-10 w-10 items-center justify-center ">
<Mail className="text-muted-foreground h-5 w-5" />
</div>
<div>
<CardTitle>Email Configuration</CardTitle>
@@ -631,7 +632,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
<CardContent className="space-y-6">
{/* Current Status */}
{mode === "edit" && (
<div className="flex items-center justify-between rounded-lg bg-gray-50 p-4">
<div className="flex items-center justify-between bg-gray-50 p-4">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">
Current Status:
@@ -639,7 +640,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
{emailConfig?.hasApiKey && emailConfig?.resendDomain ? (
<Badge
variant="default"
className="bg-green-100 text-green-800"
className="bg-primary/10 text-primary"
>
<Key className="mr-1 h-3 w-3" />
Custom Configuration Active
@@ -670,7 +671,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
href="https://resend.com"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
className="text-primary hover:underline"
>
resend.com
</a>
@@ -709,7 +710,9 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
? "Enter new API key to update"
: "re_..."
}
className={errors.resendApiKey ? "border-red-500" : ""}
className={
errors.resendApiKey ? "border-destructive" : ""
}
/>
<Button
type="button"
@@ -726,7 +729,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
</Button>
</div>
{errors.resendApiKey && (
<p className="text-sm text-red-600">
<p className="text-destructive text-sm">
{errors.resendApiKey}
</p>
)}
@@ -749,10 +752,12 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
handleInputChange("resendDomain", e.target.value)
}
placeholder="yourdomain.com"
className={errors.resendDomain ? "border-red-500" : ""}
className={
errors.resendDomain ? "border-destructive" : ""
}
/>
{errors.resendDomain && (
<p className="text-sm text-red-600">
<p className="text-destructive text-sm">
{errors.resendDomain}
</p>
)}
@@ -779,10 +784,12 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
handleInputChange("emailFromName", e.target.value)
}
placeholder={formData.name || "Your Business Name"}
className={errors.emailFromName ? "border-red-500" : ""}
className={
errors.emailFromName ? "border-destructive" : ""
}
/>
{errors.emailFromName && (
<p className="text-sm text-red-600">
<p className="text-destructive text-sm">
{errors.emailFromName}
</p>
)}
@@ -796,11 +803,11 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
</Card>
{/* Settings */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<div className="flex items-center gap-3">
<div className="bg-brand-muted flex h-10 w-10 items-center justify-center rounded-lg">
<Star className="text-brand-light h-5 w-5" />
<div className="bg-muted flex h-10 w-10 items-center justify-center ">
<Star className="text-muted-foreground h-5 w-5" />
</div>
<div>
<CardTitle>Settings</CardTitle>
@@ -811,7 +818,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="bg-brand-muted border-border/40 flex items-center justify-between rounded-xl border p-4">
<div className="bg-muted border-border/40 flex items-center justify-between border p-4">
<div className="space-y-0.5">
<Label
htmlFor="isDefault"
@@ -841,8 +848,8 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
<FloatingActionBar
leftContent={
<div className="flex items-center space-x-3">
<div className="rounded-lg bg-blue-100 p-2 dark:bg-blue-900/30">
<FileText className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<div className="bg-primary/10 p-2">
<FileText className="text-primary h-5 w-5" />
</div>
<div>
<p className="font-medium text-gray-900 dark:text-gray-100">
@@ -873,7 +880,8 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
<Button
onClick={handleSubmit}
disabled={isSubmitting || !isDirty}
className="btn-brand-primary shadow-md"
variant="default"
className="shadow-md"
size="sm"
>
{isSubmitting ? (

View File

@@ -262,7 +262,8 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
type="submit"
form="client-form"
disabled={isSubmitting}
className="btn-brand-primary shadow-md"
variant="default"
className="shadow-md"
>
{isSubmitting ? (
<>
@@ -286,11 +287,11 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
{/* Main Form Container - styled like data table */}
<div className="space-y-4">
{/* Basic Information */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-r from-emerald-600/10 to-teal-600/10">
<UserPlus className="h-5 w-5 text-emerald-700 dark:text-emerald-400" />
<div className="bg-primary/10 flex h-10 w-10 items-center justify-center ">
<UserPlus className="text-primary h-5 w-5" />
</div>
<div>
<CardTitle>Basic Information</CardTitle>
@@ -367,12 +368,12 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
</Card>
{/* Address */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-r from-emerald-600/10 to-teal-600/10">
<div className="bg-primary/10 flex h-10 w-10 items-center justify-center ">
<svg
className="h-5 w-5 text-emerald-700 dark:text-emerald-400"
className="text-primary h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@@ -415,11 +416,11 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
</Card>
{/* Billing Information */}
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardHeader>
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-r from-emerald-600/10 to-teal-600/10">
<DollarSign className="h-5 w-5 text-emerald-700 dark:text-emerald-400" />
<div className="bg-primary/10 flex h-10 w-10 items-center justify-center ">
<DollarSign className="text-primary h-5 w-5" />
</div>
<div>
<CardTitle>Billing Information</CardTitle>
@@ -463,8 +464,8 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
<FloatingActionBar
leftContent={
<div className="flex items-center space-x-3">
<div className="rounded-lg bg-blue-100 p-2 dark:bg-blue-900/30">
<FileText className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<div className="bg-primary/10 p-2">
<FileText className="text-primary h-5 w-5" />
</div>
<div>
<p className="font-medium text-gray-900 dark:text-gray-100">
@@ -495,7 +496,7 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
<Button
onClick={handleSubmit}
disabled={isSubmitting || !isDirty}
className="bg-gradient-to-r from-emerald-600 to-teal-600 shadow-md transition-all duration-200 hover:from-emerald-700 hover:to-teal-700 hover:shadow-lg"
variant="default"
size="sm"
>
{isSubmitting ? (

View File

@@ -101,7 +101,7 @@ export function EmailComposer({
editorProps: {
attributes: {
class:
"prose prose-sm sm:prose lg:prose-lg xl:prose-2xl mx-auto focus:outline-none min-h-[120px] p-4 border rounded-md bg-background",
"prose prose-sm sm:prose lg:prose-lg xl:prose-2xl mx-auto focus:outline-none min-h-[120px] p-4 border bg-background",
},
},
});
@@ -133,9 +133,9 @@ export function EmailComposer({
if (!editor) {
return (
<div className="bg-muted flex h-[200px] items-center justify-center rounded-md border">
<div className="bg-muted flex h-[200px] items-center justify-center border">
<div className="text-center">
<div className="border-primary mx-auto mb-2 h-4 w-4 animate-spin rounded-full border-2 border-t-transparent"></div>
<div className="border-primary mx-auto mb-2 h-4 w-4 animate-spin border-2 border-t-transparent"></div>
<p className="text-muted-foreground text-sm">Loading editor...</p>
</div>
</div>
@@ -145,7 +145,7 @@ export function EmailComposer({
return (
<div className={className}>
{/* Email Headers */}
<div className="bg-muted/20 space-y-4 rounded-lg border p-4">
<div className="bg-muted/20 space-y-4 border p-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="from-email" className="text-sm font-medium">
@@ -231,7 +231,7 @@ export function EmailComposer({
</div>
{/* Editor Toolbar */}
<div className="bg-muted/20 flex flex-wrap items-center gap-1 rounded-lg border p-2">
<div className="bg-muted/20 flex flex-wrap items-center gap-1 border p-2">
<MenuButton
onClick={() => editor.chain().focus().toggleBold().run()}
isActive={editor.isActive("bold")}

View File

@@ -95,7 +95,7 @@ export function EmailPreview({
return (
<div className={className}>
{/* Email Headers */}
<div className="bg-muted/20 mb-4 space-y-3 rounded-lg p-4">
<div className="bg-muted/20 mb-4 space-y-3 p-4">
<div className="grid grid-cols-1 gap-3 text-sm md:grid-cols-3">
<div>
<span className="text-muted-foreground block text-xs font-medium">
@@ -142,7 +142,7 @@ export function EmailPreview({
{/* Email Content */}
{emailTemplate ? (
<div className="rounded-lg border bg-gray-50 p-1 shadow-sm">
<div className=" border bg-gray-50 p-1 shadow-sm">
<iframe
srcDoc={emailTemplate.html}
className="h-[700px] w-full rounded border-0"

View File

@@ -34,9 +34,9 @@ function FilePreview({
const getStatusIcon = () => {
switch (status) {
case "success":
return <CheckCircle className="h-4 w-4 text-green-600" />;
return <CheckCircle className="text-primary h-4 w-4" />;
case "error":
return <AlertCircle className="h-4 w-4 text-red-600" />;
return <AlertCircle className="text-destructive h-4 w-4" />;
default:
return <FileText className="h-4 w-4 text-gray-400" />;
}
@@ -45,9 +45,9 @@ function FilePreview({
const getStatusColor = () => {
switch (status) {
case "success":
return "border-green-200 bg-green-50";
return "border-primary/20 bg-primary/10";
case "error":
return "border-red-200 bg-red-50";
return "border-destructive/20 bg-destructive/10";
default:
return "border-gray-200 bg-gray-50";
}
@@ -56,20 +56,20 @@ function FilePreview({
return (
<div
className={cn(
"flex items-center justify-between rounded-lg border p-3",
"flex items-center justify-between border p-3",
getStatusColor(),
)}
>
<div className="flex items-center gap-3">
{getStatusIcon()}
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-gray-900">
<p className="text-foreground truncate text-sm font-medium">
{file.name}
</p>
<p className="text-xs text-gray-500">
<p className="text-muted-foreground text-xs">
{(file.size / 1024 / 1024).toFixed(2)} MB
</p>
{error && <p className="mt-1 text-xs text-red-600">{error}</p>}
{error && <p className="text-destructive mt-1 text-xs">{error}</p>}
</div>
</div>
<Button
@@ -152,28 +152,28 @@ export function FileUpload({
<div
{...getRootProps()}
className={cn(
"cursor-pointer rounded-lg border-2 border-dashed p-8 text-center transition-colors",
"hover:border-emerald-400 hover:bg-emerald-50/50",
isDragActive && "border-emerald-400 bg-emerald-50/50",
isDragReject && "border-red-400 bg-red-50/50",
"cursor-pointer border-2 border-dashed p-8 text-center transition-colors",
"hover:border-primary/40 hover:bg-primary/10",
isDragActive && "border-primary/40 bg-primary/10",
isDragReject && "border-destructive/40 bg-destructive/10",
disabled && "cursor-not-allowed opacity-50",
"bg-white/80 backdrop-blur-sm",
"bg-background/80 backdrop-blur-sm",
)}
>
<input {...getInputProps()} />
<div className="flex flex-col items-center gap-4">
<div
className={cn(
"rounded-full p-3 transition-colors",
isDragActive ? "bg-emerald-100" : "bg-gray-100",
isDragReject && "bg-red-100",
" p-3 transition-colors",
isDragActive ? "bg-primary/10" : "bg-muted",
isDragReject && "bg-destructive/10",
)}
>
<Upload
className={cn(
"h-6 w-6 transition-colors",
isDragActive ? "text-emerald-600" : "text-gray-400",
isDragReject && "text-red-600",
isDragActive ? "text-primary" : "text-muted-foreground",
isDragReject && "text-destructive",
)}
/>
</div>
@@ -181,8 +181,8 @@ export function FileUpload({
<p
className={cn(
"text-lg font-medium transition-colors",
isDragActive ? "text-emerald-600" : "text-gray-900",
isDragReject && "text-red-600",
isDragActive ? "text-primary" : "text-foreground",
isDragReject && "text-destructive",
)}
>
{isDragActive
@@ -222,17 +222,17 @@ export function FileUpload({
{/* Error Summary */}
{Object.keys(errors).length > 0 && (
<div className="rounded-lg border border-red-200 bg-red-50 p-3">
<div className="border-destructive/20 bg-destructive/10 border p-3">
<div className="mb-2 flex items-center gap-2">
<AlertCircle className="h-4 w-4 text-red-600" />
<span className="text-sm font-medium text-red-800">
<AlertCircle className="text-destructive h-4 w-4" />
<span className="text-destructive text-sm font-medium">
Upload Errors
</span>
</div>
<ul className="space-y-1 text-sm text-red-700">
<ul className="text-destructive space-y-1 text-sm">
{Object.entries(errors).map(([fileName, error]) => (
<li key={fileName} className="flex items-start gap-2">
<span className="text-red-600"></span>
<span className="text-destructive"></span>
<span>
<strong>{fileName}:</strong> {error}
</span>

View File

@@ -76,10 +76,10 @@ function InvoiceFormSkeleton() {
/>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
<div className="space-y-6 lg:col-span-2">
<div className="bg-muted h-96 animate-pulse rounded-lg" />
<div className="bg-muted h-96 animate-pulse" />
</div>
<div className="space-y-6">
<div className="bg-muted h-64 animate-pulse rounded-lg" />
<div className="bg-muted h-64 animate-pulse" />
</div>
</div>
</div>
@@ -306,7 +306,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
onSuccess: (invoice) => {
toast.success("Invoice created successfully");
void utils.invoices.getAll.invalidate();
router.push(`/dashboard/invoices/${invoice.id}/view`);
router.push(`/dashboard/invoices/${invoice.id}`);
},
onError: (error) => {
toast.error(error.message || "Failed to create invoice");
@@ -319,7 +319,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
await utils.invoices.getAll.invalidate();
// The update mutation returns { success: true }, so we use the current invoiceId
if (invoiceId && invoiceId !== "new") {
router.push(`/dashboard/invoices/${invoiceId}/view`);
router.push(`/dashboard/invoices/${invoiceId}`);
} else {
router.push("/dashboard/invoices");
}
@@ -462,17 +462,13 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
variant="outline"
onClick={handleDelete}
disabled={loading || deleteInvoice.isPending}
className="text-red-700 shadow-sm hover:bg-red-50"
className="text-destructive hover:bg-destructive/10 shadow-sm"
>
<Trash2 className="h-4 w-4 sm:mr-2" />
<span className="hidden sm:inline">Delete Invoice</span>
</Button>
)}
<Button
onClick={handleSubmit}
disabled={loading}
className="bg-gradient-to-r from-emerald-600 to-teal-600 shadow-md transition-all duration-200 hover:from-emerald-700 hover:to-teal-700 hover:shadow-lg"
>
<Button onClick={handleSubmit} disabled={loading} variant="default">
{loading ? (
<>
<Clock className="h-4 w-4 animate-spin sm:mr-2" />
@@ -499,7 +495,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
</TabsList>
<TabsContent value="invoice-details">
<Card className="card-primary">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileText className="h-5 w-5" />
@@ -657,14 +653,14 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
</TabsContent>
<TabsContent value="invoice-items">
<Card className="card-primary">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<DollarSign className="h-5 w-5" />
Invoice Items
</CardTitle>
</CardHeader>
<CardContent className="px-3 py-0">
<CardContent className="px-3 pb-4">
<InvoiceLineItems
items={formData.items}
onAddItem={addItem}
@@ -681,7 +677,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
</div>
<div className="space-y-6">
<Card className="card-primary sticky top-6">
<Card className="sticky top-6">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Check className="h-5 w-5" />
@@ -747,8 +743,8 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
<FloatingActionBar
leftContent={
<div className="flex items-center space-x-3">
<div className="rounded-lg bg-green-100 p-2 dark:bg-green-900/30">
<FileText className="h-5 w-5 text-green-600 dark:text-green-400" />
<div className="bg-primary/10 p-2">
<FileText className="text-primary h-5 w-5" />
</div>
<div>
<p className="font-medium text-gray-900 dark:text-gray-100">
@@ -769,7 +765,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
size="sm"
onClick={handleDelete}
disabled={loading || deleteInvoice.isPending}
className="text-red-700 hover:bg-red-50"
className="text-destructive hover:bg-destructive/10"
>
<Trash2 className="h-4 w-4 sm:mr-2" />
<span className="hidden sm:inline">Delete</span>
@@ -778,7 +774,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
<Button
onClick={handleSubmit}
disabled={loading}
className="bg-gradient-to-r from-emerald-600 to-teal-600 shadow-md transition-all duration-200 hover:from-emerald-700 hover:to-teal-700 hover:shadow-lg"
variant="default"
size="sm"
>
{loading ? (

View File

@@ -25,6 +25,7 @@ import {
Trash2,
} from "lucide-react";
import * as React from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Button } from "~/components/ui/button";
import { DatePicker } from "~/components/ui/date-picker";
import { Input } from "~/components/ui/input";
@@ -114,11 +115,16 @@ function SortableLineItem({
};
return (
<div
<motion.div
ref={setNodeRef}
style={style}
layout
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2, ease: "easeOut" }}
className={cn(
"card-secondary hidden rounded-lg p-4 md:block",
"bg-card border-border hidden border p-4 md:block",
isDragging && "opacity-50",
)}
>
@@ -227,7 +233,7 @@ function SortableLineItem({
size="sm"
onClick={() => onRemove(index)}
className={cn(
"text-muted-foreground h-8 w-8 p-0 transition-colors hover:text-red-500",
"text-muted-foreground hover:text-destructive h-8 w-8 p-0 transition-colors",
!canRemove && "cursor-not-allowed opacity-50",
)}
disabled={!canRemove}
@@ -238,7 +244,7 @@ function SortableLineItem({
</div>
</div>
</div>
</div>
</motion.div>
);
}
@@ -254,8 +260,15 @@ function MobileLineItem({
isLast,
}: LineItemRowProps) {
return (
<div className="card-secondary space-y-3 rounded-lg md:hidden">
<div className="space-y-3 p-4">
<motion.div
layout
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2, ease: "easeOut" }}
className="bg-card border-border space-y-3 border md:hidden"
>
<div className="bg-secondary space-y-3 p-4">
{/* Description */}
<div className="space-y-1">
<Label className="text-muted-foreground text-xs">Description</Label>
@@ -344,7 +357,7 @@ function MobileLineItem({
size="sm"
onClick={() => onRemove(index)}
className={cn(
"text-muted-foreground h-8 w-8 p-0 transition-colors hover:text-red-500",
"text-muted-foreground hover:text-destructive h-8 w-8 p-0 transition-colors",
!canRemove && "cursor-not-allowed opacity-50",
)}
disabled={!canRemove}
@@ -367,7 +380,7 @@ function MobileLineItem({
</span>
</div>
</div>
</div>
</motion.div>
);
}
@@ -414,51 +427,57 @@ export function InvoiceLineItems({
items={items.map((item) => item.id)}
strategy={verticalListSortingStrategy}
>
<div className="space-y-2">
{items.map((item, index) => (
<React.Fragment key={item.id}>
{/* Desktop/Tablet Card with Drag and Drop */}
<SortableLineItem
item={item}
index={index}
canRemove={canRemoveItems}
onRemove={onRemoveItem}
onUpdate={onUpdateItem}
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
isFirst={index === 0}
isLast={index === items.length - 1}
/>
<AnimatePresence>
<div className="space-y-2">
{items.map((item, index) => (
<React.Fragment key={item.id}>
{/* Desktop/Tablet Card with Drag and Drop */}
<SortableLineItem
item={item}
index={index}
canRemove={canRemoveItems}
onRemove={onRemoveItem}
onUpdate={onUpdateItem}
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
isFirst={index === 0}
isLast={index === items.length - 1}
/>
{/* Mobile Card */}
<MobileLineItem
item={item}
index={index}
canRemove={canRemoveItems}
onRemove={onRemoveItem}
onUpdate={onUpdateItem}
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
isFirst={index === 0}
isLast={index === items.length - 1}
/>
</React.Fragment>
))}
</div>
{/* Mobile Card */}
<MobileLineItem
item={item}
index={index}
canRemove={canRemoveItems}
onRemove={onRemoveItem}
onUpdate={onUpdateItem}
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
isFirst={index === 0}
isLast={index === items.length - 1}
/>
</React.Fragment>
))}
</div>
</AnimatePresence>
</SortableContext>
</DndContext>
{/* Add Item Button */}
<div className="px-3 pt-3">
<Button
type="button"
variant="outline"
onClick={onAddItem}
className="w-full"
>
<Plus className="mr-2 h-4 w-4" />
Add Another Item
</Button>
<div className="border-t pt-6">
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
<Button
type="button"
variant="outline"
onClick={onAddItem}
className="w-full"
>
<Plus className="mr-2 h-4 w-4" />
Add Line Item
</Button>
</motion.div>
</div>
</div>
</div>
);

View File

@@ -242,7 +242,7 @@ export function SendEmailDialog({
<DialogContent className="flex max-h-[90vh] max-w-4xl flex-col overflow-hidden">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Mail className="h-5 w-5 text-green-600" />
<Mail className="text-primary h-5 w-5" />
Send Invoice Email
</DialogTitle>
<DialogDescription>
@@ -419,7 +419,7 @@ export function SendEmailDialog({
<Button
onClick={handleSendEmail}
disabled={!canSend || isSending}
className="bg-green-600 hover:bg-green-700"
className="bg-primary hover:bg-primary/90"
>
{isSending ? (
<>

View File

@@ -50,7 +50,7 @@ export function FloatingActionBar({
// Base positioning - always at bottom
"fixed right-0 left-0 z-50",
// Safe area and sidebar adjustments
"pb-safe-area-inset-bottom md:left-[276px]",
"pb-safe-area-inset-bottom md:left-64",
// Conditional centering based on dock state
isDocked ? "flex justify-center" : "",
// Dynamic bottom positioning
@@ -65,7 +65,7 @@ export function FloatingActionBar({
isDocked ? "mx-auto mb-0 px-4" : "mb-4 px-4",
)}
>
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardContent className="flex items-center justify-between p-4">
{/* Left content */}
{leftContent && (

View File

@@ -15,60 +15,59 @@ export function Navbar() {
// const { data: currentInvoice } = api.invoices.getCurrentOpen.useQuery();
return (
<header className="fixed top-2 right-2 left-2 z-30 md:top-3 md:right-3 md:left-3">
<div className="bg-background/60 border-border/40 relative rounded-2xl border shadow-lg backdrop-blur-xl backdrop-saturate-150">
<div className="flex h-14 items-center justify-between px-4 md:h-16 md:px-8">
<div className="flex items-center gap-4 md:gap-6">
<SidebarTrigger
isOpen={isMobileNavOpen}
onToggle={() => setIsMobileNavOpen(!isMobileNavOpen)}
/>
<Link href="/dashboard" className="flex items-center gap-2">
<Logo size="md" />
</Link>
</div>
<div className="flex items-center gap-2 md:gap-4">
{status === "loading" ? (
<>
<Skeleton className="bg-muted/20 hidden h-5 w-20 sm:inline" />
<Skeleton className="bg-muted/20 h-8 w-16" />
</>
) : session?.user ? (
<>
<span className="text-muted-foreground hidden text-xs font-medium sm:inline md:text-sm">
{session.user.name ?? session.user.email}
</span>
<header className="bg-navbar border-navbar-border text-navbar-foreground fixed top-0 right-0 left-0 z-30 border-b">
<div className="flex h-14 items-center justify-between px-4 md:h-16 md:px-8">
<div className="flex items-center gap-4 md:gap-6">
<SidebarTrigger
isOpen={isMobileNavOpen}
onToggle={() => setIsMobileNavOpen(!isMobileNavOpen)}
/>
<Link href="/dashboard" className="flex items-center gap-2">
<Logo size="md" />
</Link>
</div>
<div className="flex items-center gap-2 md:gap-4">
{status === "loading" ? (
<>
<Skeleton className="bg-muted/20 hidden h-5 w-20 sm:inline" />
<Skeleton className="bg-muted/20 h-8 w-16" />
</>
) : session?.user ? (
<>
<span className="text-muted-foreground hidden text-xs font-medium sm:inline md:text-sm">
{session.user.name ?? session.user.email}
</span>
<Button
variant="outline"
size="sm"
onClick={() => signOut({ callbackUrl: "/" })}
className="text-xs md:text-sm"
>
Sign Out
</Button>
</>
) : (
<>
<Link href="/auth/signin">
<Button
variant="outline"
variant="ghost"
size="sm"
onClick={() => signOut({ callbackUrl: "/" })}
className="border-border/40 hover:bg-accent/50 text-xs md:text-sm"
className="text-xs md:text-sm"
>
Sign Out
Sign In
</Button>
</>
) : (
<>
<Link href="/auth/signin">
<Button
variant="ghost"
size="sm"
className="hover:bg-accent/50 text-xs md:text-sm"
>
Sign In
</Button>
</Link>
<Link href="/auth/register">
<Button
size="sm"
className="bg-gradient-to-r from-emerald-600 to-teal-600 text-xs font-medium text-white shadow-md transition-all duration-200 hover:from-emerald-700 hover:to-teal-700 hover:shadow-lg md:text-sm"
>
Register
</Button>
</Link>
</>
)}
</div>
</Link>
<Link href="/auth/register">
<Button
size="sm"
variant="default"
className="text-xs font-medium md:text-sm"
>
Register
</Button>
</Link>
</>
)}
</div>
</div>
</header>

View File

@@ -132,7 +132,7 @@ export function EmptyState({
return (
<div className={cn("py-12 text-center", className)}>
{icon && (
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-muted/50">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center bg-muted/50">
{icon}
</div>
)}

View File

@@ -35,10 +35,9 @@ const variantStyles = {
hoverBackground: "group-hover:bg-status-warning-muted/70",
},
purple: {
icon: "text-purple-600",
background: "bg-purple-100 dark:bg-purple-900/30",
hoverBackground:
"group-hover:bg-purple-200 dark:group-hover:bg-purple-900/50",
icon: "text-primary",
background: "bg-secondary",
hoverBackground: "group-hover:bg-secondary/80",
},
};
@@ -57,7 +56,7 @@ export function QuickActionCard({
<CardContent className="p-6 text-center">
<div
className={cn(
"mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-full transition-colors",
"mx-auto mb-3 flex h-12 w-12 items-center justify-center transition-colors",
styles.background,
styles.hoverBackground,
)}
@@ -99,10 +98,10 @@ export function QuickActionCard({
export function QuickActionCardSkeleton() {
return (
<Card className="card-primary">
<Card className="bg-card border-border border">
<CardContent className="p-6">
<div className="animate-pulse">
<div className="bg-muted mx-auto mb-3 h-12 w-12 rounded-full"></div>
<div className="bg-muted mx-auto mb-3 h-12 w-12 "></div>
<div className="bg-muted mx-auto mb-2 h-4 w-2/3 rounded"></div>
<div className="bg-muted mx-auto h-3 w-1/2 rounded"></div>
</div>

View File

@@ -2,23 +2,25 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useSession } from "next-auth/react";
import { useSession, signOut } from "next-auth/react";
import { Skeleton } from "~/components/ui/skeleton";
import { Button } from "~/components/ui/button";
import { LogOut, User } from "lucide-react";
import { navigationConfig } from "~/lib/navigation";
export function Sidebar() {
const pathname = usePathname();
const { status } = useSession();
const { data: session, status } = useSession();
return (
<aside className="border-border/40 bg-background/60 fixed top-[5.75rem] bottom-3 left-3 z-20 hidden w-64 flex-col justify-between rounded-2xl border p-6 shadow-lg backdrop-blur-xl backdrop-saturate-150 md:flex">
<aside className="bg-sidebar border-sidebar-border text-sidebar-foreground fixed top-[4rem] bottom-0 left-0 z-20 hidden w-64 flex-col justify-between border-r p-6 md:flex">
<nav className="flex flex-col">
{navigationConfig.map((section, sectionIndex) => (
<div key={section.title} className={sectionIndex > 0 ? "mt-6" : ""}>
{sectionIndex > 0 && (
<div className="border-border/40 my-4 border-t" />
)}
<div className="text-muted-foreground mb-3 text-xs font-semibold tracking-wider uppercase">
<div className="text-sidebar-foreground/60 mb-3 text-xs font-semibold tracking-wider uppercase">
{section.title}
</div>
<div className="flex flex-col gap-0.5">
@@ -27,10 +29,10 @@ export function Sidebar() {
{Array.from({ length: section.links.length }).map((_, i) => (
<div
key={i}
className="flex items-center gap-3 rounded-lg px-3 py-2.5"
className="flex items-center gap-3 px-3 py-2.5"
>
<Skeleton className="bg-muted/20 h-4 w-4" />
<Skeleton className="bg-muted/20 h-4 w-20" />
<Skeleton className="bg-sidebar-accent/20 h-4 w-4" />
<Skeleton className="bg-sidebar-accent/20 h-4 w-20" />
</div>
))}
</>
@@ -42,10 +44,10 @@ export function Sidebar() {
key={link.href}
href={link.href}
aria-current={pathname === link.href ? "page" : undefined}
className={`flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200 ${
className={`flex items-center gap-3 px-3 py-2.5 text-sm font-medium transition-colors ${
pathname === link.href
? "bg-gradient-to-r from-emerald-600/10 to-teal-600/10 text-emerald-700 shadow-sm dark:from-emerald-500/20 dark:to-teal-500/20 dark:text-emerald-400"
: "text-foreground hover:bg-accent/50 hover:text-accent-foreground"
? "bg-sidebar-primary text-sidebar-primary-foreground"
: "text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
}`}
>
<Icon className="h-4 w-4" />
@@ -58,6 +60,48 @@ export function Sidebar() {
</div>
))}
</nav>
{/* User Section */}
<div className="border-sidebar-border border-t pt-4">
{status === "loading" ? (
<div className="space-y-3">
<Skeleton className="bg-sidebar-accent/20 h-8 w-full" />
<Skeleton className="bg-sidebar-accent/20 h-8 w-full" />
<div className="flex items-center gap-3 p-3">
<Skeleton className="bg-sidebar-accent/20 h-8 w-8 rounded-full" />
<div className="flex-1">
<Skeleton className="bg-sidebar-accent/20 mb-1 h-4 w-24" />
<Skeleton className="bg-sidebar-accent/20 h-3 w-32" />
</div>
</div>
</div>
) : session?.user ? (
<div className="space-y-3">
<Button
variant="ghost"
size="sm"
onClick={() => signOut()}
className="text-sidebar-foreground/60 hover:text-sidebar-foreground hover:bg-sidebar-accent w-full justify-start px-3"
>
<LogOut className="mr-2 h-4 w-4" />
Sign Out
</Button>
<div className="flex items-center gap-3 px-3 pt-2">
<div className="bg-sidebar-accent flex h-8 w-8 items-center justify-center rounded-full">
<User className="h-4 w-4" />
</div>
<div className="min-w-0 flex-1">
<p className="text-sidebar-foreground truncate text-sm font-medium">
{session.user.name ?? "User"}
</p>
<p className="text-sidebar-foreground/60 truncate text-xs">
{session.user.email}
</p>
</div>
</div>
</div>
) : null}
</div>
</aside>
);
}

View File

@@ -7,8 +7,8 @@ import { cn } from "~/lib/utils";
const navigation = [
{ name: "Dashboard", href: "/dashboard" },
{ name: "Clients", href: "/clients" },
{ name: "Invoices", href: "/invoices" },
{ name: "Clients", href: "/dashboard/clients" },
{ name: "Invoices", href: "/dashboard/invoices" },
];
export function Navigation() {
@@ -24,7 +24,7 @@ export function Navigation() {
"transition-colors",
pathname === item.href
? "bg-primary text-primary-foreground"
: "hover:bg-muted"
: "hover:bg-muted",
)}
>
{item.name}

View File

@@ -24,14 +24,14 @@ export function SidebarTrigger({ isOpen, onToggle }: SidebarTriggerProps) {
size="icon"
aria-label="Toggle navigation"
onClick={onToggle}
className="bg-card/80 h-8 w-8 shadow-lg backdrop-blur-sm md:hidden"
className="h-8 w-8 md:hidden"
>
{isOpen ? <X className="h-4 w-4" /> : <MenuIcon className="h-4 w-4" />}
</Button>
{/* Mobile dropdown navigation */}
{isOpen && (
<div className="bg-background/95 border-border/40 absolute top-full right-0 left-0 z-40 mt-2 rounded-2xl border shadow-2xl backdrop-blur-xl md:hidden">
<div className="bg-background border-border absolute top-full right-0 left-0 z-40 mt-1 border-t">
{/* Navigation content */}
<nav className="flex flex-col p-4">
{navigationConfig.map((section, sectionIndex) => (
@@ -52,7 +52,7 @@ export function SidebarTrigger({ isOpen, onToggle }: SidebarTriggerProps) {
(_, i) => (
<div
key={i}
className="flex items-center gap-3 rounded-lg px-3 py-2.5"
className="flex items-center gap-3 px-3 py-2.5"
>
<Skeleton className="bg-muted/20 h-4 w-4" />
<Skeleton className="bg-muted/20 h-4 w-20" />
@@ -70,10 +70,10 @@ export function SidebarTrigger({ isOpen, onToggle }: SidebarTriggerProps) {
aria-current={
pathname === link.href ? "page" : undefined
}
className={`flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200 ${
className={`flex items-center gap-3 px-3 py-2.5 text-sm font-medium transition-colors ${
pathname === link.href
? "bg-gradient-to-r from-emerald-600/10 to-teal-600/10 text-emerald-700 shadow-sm dark:from-emerald-500/20 dark:to-teal-500/20 dark:text-emerald-400"
: "text-foreground hover:bg-accent/50 hover:text-accent-foreground"
? "bg-primary/10 text-primary"
: "text-foreground hover:bg-muted"
}`}
onClick={onToggle}
>

View File

@@ -54,7 +54,7 @@ function AlertDialogContent({
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}

View File

@@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "~/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
"relative w-full border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {

View File

@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "~/lib/utils";
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-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 transition-[color,box-shadow] overflow-hidden",
"inline-flex items-center justify-center border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-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 transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
@@ -23,13 +23,11 @@ const badgeVariants = cva(
info: "border-transparent bg-status-info [a&]:hover:opacity-90",
// Outlined variants for status badges
"outline-draft":
"border-gray-400 text-gray-600 dark:border-gray-500 dark:text-gray-300 bg-transparent",
"outline-sent":
"border-blue-400 text-blue-600 dark:border-blue-500 dark:text-blue-300 bg-transparent",
"outline-paid":
"border-green-400 text-green-600 dark:border-green-500 dark:text-green-300 bg-transparent",
"border-muted-foreground/40 text-muted-foreground bg-transparent",
"outline-sent": "border-primary/40 text-primary bg-transparent",
"outline-paid": "border-primary/40 text-primary bg-transparent",
"outline-overdue":
"border-red-400 text-red-600 dark:border-red-500 dark:text-red-300 bg-transparent",
"border-destructive/40 text-destructive bg-transparent",
},
},
defaultVariants: {

View File

@@ -5,28 +5,28 @@ 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-colors duration-150 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",
"inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-colors duration-150 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",
brand:
"bg-brand-gradient text-white shadow-lg hover:bg-brand-gradient hover:shadow-xl font-medium",
"bg-primary text-primary-foreground shadow-lg hover:bg-primary/90 hover:shadow-xl font-medium",
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 border-border/40 bg-background/60 shadow-sm backdrop-blur-sm hover:bg-accent/50 hover:text-accent-foreground hover:border-border/60 transition-colors duration-150",
"border border-border/40 bg-background/60 shadow-sm backdrop-blur-sm hover:bg-accent/50 hover:text-foreground-foreground hover:border-border/60 transition-colors duration-150",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
"bg-secondary text-muted-foreground-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
"hover:bg-accent hover:text-foreground-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",
sm: "h-8 gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 px-6 has-[>svg]:px-4",
icon: "size-9",
},
},

View File

@@ -100,7 +100,7 @@ function Calendar({
defaultClassNames.dropdowns,
),
dropdown_root: cn(
"relative has-focus:border-ring border border-input shadow-sm has-focus:ring-ring/50 has-focus:ring-2 rounded-md h-8",
"relative has-focus:border-ring border border-input shadow-sm has-focus:ring-ring/50 has-focus:ring-2 h-8",
defaultClassNames.dropdown_root,
),
dropdown: cn(
@@ -275,7 +275,7 @@ function CalendarDayButton({
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"hover:bg-accent hover:text-accent-foreground flex aspect-square size-auto h-8 w-full min-w-8 items-center justify-center rounded-md border-0 text-sm leading-none font-normal shadow-none",
"hover:bg-accent hover:text-foreground-foreground flex aspect-square size-auto h-8 w-full min-w-8 items-center justify-center border-0 text-sm leading-none font-normal shadow-none",
modifiers.selected && "bg-primary text-primary-foreground",
modifiers.today && !modifiers.selected && "bg-accent font-semibold",
className,

View File

@@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="card"
className={cn(
"bg-background/60 text-card-foreground border-border/40 flex flex-col gap-2 rounded-2xl border py-2 shadow-lg backdrop-blur-xl backdrop-saturate-150",
"bg-card text-card-foreground border-border/40 flex flex-col border shadow-lg backdrop-blur-xl backdrop-saturate-150",
className,
)}
{...props}
@@ -20,7 +20,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 p-3 px-5 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-5 pt-4 pb-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className,
)}
{...props}
@@ -65,7 +65,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-5 pb-3", className)}
className={cn("px-5 pb-4", className)}
{...props}
/>
);

View File

@@ -60,7 +60,7 @@ function DialogContent({
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}

View File

@@ -42,7 +42,7 @@ function DropdownMenuContent({
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-0 shadow-md",
"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 border-0 shadow-md",
className,
)}
{...props}
@@ -74,7 +74,7 @@ function DropdownMenuItem({
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",
"focus:bg-accent focus:text-foreground-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 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}
@@ -92,7 +92,7 @@ function DropdownMenuCheckboxItem({
<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",
"focus:bg-accent focus:text-foreground-foreground relative flex cursor-default items-center gap-2 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}
@@ -128,7 +128,7 @@ function DropdownMenuRadioItem({
<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",
"focus:bg-accent focus:text-foreground-foreground relative flex cursor-default items-center gap-2 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}
@@ -211,7 +211,7 @@ function DropdownMenuSubTrigger({
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",
"focus:bg-accent focus:text-foreground-foreground data-[state=open]:bg-accent data-[state=open]:text-foreground-foreground flex cursor-default items-center px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
className,
)}
{...props}
@@ -230,7 +230,7 @@ function DropdownMenuSubContent({
<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-0 shadow-lg",
"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 border-0 shadow-lg",
className,
)}
{...props}

View File

@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground bg-background/50 text-foreground border-border/40 flex h-10 w-full min-w-0 rounded-md border px-3 py-2 text-sm shadow-sm backdrop-blur-sm transition-all duration-200 outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground bg-background/50 text-foreground border-border/40 flex h-10 w-full min-w-0 border px-3 py-2 text-sm shadow-sm backdrop-blur-sm transition-all duration-200 outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
"focus-visible:border-ring focus-visible:bg-background/80 focus-visible:ring-ring/20 focus-visible:ring-[3px]",
"hover:border-border/60 hover:bg-background/60",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",

View File

@@ -59,7 +59,7 @@ function NavigationMenuItem({
}
const navigationMenuTriggerStyle = cva(
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
"group inline-flex h-9 w-max items-center justify-center bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-foreground-foreground focus:bg-accent focus:text-foreground-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-foreground-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
)
function NavigationMenuTrigger({
@@ -91,7 +91,7 @@ function NavigationMenuContent({
data-slot="navigation-menu-content"
className={cn(
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu: group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
className
)}
{...props}
@@ -112,7 +112,7 @@ function NavigationMenuViewport({
<NavigationMenuPrimitive.Viewport
data-slot="navigation-menu-viewport"
className={cn(
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
className
)}
{...props}
@@ -129,7 +129,7 @@ function NavigationMenuLink({
<NavigationMenuPrimitive.Link
data-slot="navigation-menu-link"
className={cn(
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-foreground-foreground hover:bg-accent hover:text-foreground-foreground focus:bg-accent focus:text-foreground-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}

View File

@@ -76,7 +76,7 @@ export function NumberInput({
return (
<div
className={cn(
"bg-background flex h-9 items-center justify-center rounded-md text-sm shadow-none",
"bg-background flex h-9 items-center justify-center text-sm shadow-none",
widthClass,
disabled && "cursor-not-allowed opacity-50",
className,

View File

@@ -30,7 +30,7 @@ function PopoverContent({
align={align}
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 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
"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 w-72 origin-(--radix-popover-content-transform-origin) border p-4 shadow-md outline-hidden",
className
)}
{...props}

View File

@@ -14,7 +14,7 @@ function Progress({
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
"bg-primary/20 relative h-2 w-full overflow-hidden ",
className,
)}
{...props}

View File

@@ -42,7 +42,7 @@ function SelectTrigger({
data-slot="select-trigger"
data-size={size}
className={cn(
"data-[placeholder]:text-muted-foreground border-input bg-background text-foreground focus-visible:border-ring focus-visible:ring-ring/50 flex h-10 w-full items-center justify-between gap-2 rounded-md border px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
"data-[placeholder]:text-muted-foreground border-input bg-background text-foreground focus-visible:border-ring focus-visible:ring-ring/50 flex h-10 w-full items-center justify-between gap-2 border px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
@@ -66,7 +66,7 @@ function SelectContent({
<SelectPrimitive.Content
data-slot="select-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 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border-0 shadow-md",
"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 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto border-0 shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
@@ -112,7 +112,7 @@ function SelectItem({
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 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 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
"focus:bg-accent focus:text-foreground-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 py-1.5 pr-8 pl-2 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 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className,
)}
{...props}
@@ -210,7 +210,7 @@ function SelectContentWithSearch({
<SelectPrimitive.Content
data-slot="select-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 relative z-50 max-h-96 min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-hidden rounded-md border-0 shadow-md",
"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 relative z-50 max-h-96 min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-hidden border-0 shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
@@ -235,7 +235,7 @@ function SelectContentWithSearch({
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<input
ref={searchInputRef}
className="placeholder:text-muted-foreground text-foreground flex h-8 w-full rounded-md border-0 bg-transparent py-2 text-sm outline-none focus:ring-0 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
className="placeholder:text-muted-foreground text-foreground flex h-8 w-full border-0 bg-transparent py-2 text-sm outline-none focus:ring-0 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
placeholder={searchPlaceholder}
value={searchValue}
onChange={(e) => onSearchChange(e.target.value)}

View File

@@ -6,7 +6,7 @@ function Skeleton({
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("bg-muted animate-pulse rounded-md", className)}
className={cn("bg-muted animate-pulse ", className)}
{...props}
/>
);
@@ -19,10 +19,10 @@ export function DashboardStatsSkeleton() {
{Array.from({ length: 4 }).map((_, i) => (
<div
key={i}
className="rounded-lg border border-gray-100 bg-white p-6 shadow-sm"
className=" border border-gray-100 bg-white p-6 shadow-sm"
>
<div className="mb-4 flex items-center justify-between">
<Skeleton className="h-9 w-9 rounded-lg" />
<Skeleton className="h-9 w-9 " />
<Skeleton className="h-4 w-12" />
</div>
<div>
@@ -41,7 +41,7 @@ export function DashboardCardsSkeleton() {
{Array.from({ length: 2 }).map((_, i) => (
<div
key={i}
className="rounded-lg border border-gray-100 bg-white p-6 shadow-sm"
className=" border border-gray-100 bg-white p-6 shadow-sm"
>
<div className="mb-6 flex items-center justify-between">
<div className="flex items-center gap-2">
@@ -69,7 +69,7 @@ export function DashboardCardsSkeleton() {
export function DashboardActivitySkeleton() {
return (
<div className="rounded-lg border border-gray-100 bg-white p-6 shadow-sm">
<div className=" border border-gray-100 bg-white p-6 shadow-sm">
<div className="mb-6 flex items-center justify-between">
<div className="flex items-center gap-2">
<Skeleton className="h-5 w-5 rounded" />
@@ -81,17 +81,17 @@ export function DashboardActivitySkeleton() {
{Array.from({ length: 5 }).map((_, i) => (
<div
key={i}
className="flex items-center justify-between rounded-lg border border-gray-100 p-4"
className="flex items-center justify-between border border-gray-100 p-4"
>
<div className="flex items-center gap-3">
<Skeleton className="h-8 w-8 rounded-lg" />
<Skeleton className="h-8 w-8 " />
<div className="space-y-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-3 w-32" />
</div>
</div>
<div className="flex items-center gap-3">
<Skeleton className="h-6 w-16 rounded-full" />
<Skeleton className="h-6 w-16 " />
<Skeleton className="h-4 w-16" />
<Skeleton className="h-8 w-8 rounded" />
</div>
@@ -115,14 +115,14 @@ export function DashboardHeroSkeleton() {
export function QuickActionsSkeleton() {
return (
<div className="rounded-lg border border-gray-100 bg-white p-6 shadow-sm">
<div className=" border border-gray-100 bg-white p-6 shadow-sm">
<div className="mb-4 flex items-center gap-2">
<Skeleton className="h-5 w-5 rounded" />
<Skeleton className="h-6 w-32" />
</div>
<div className="space-y-3">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="rounded-lg border border-gray-200 p-4">
<div key={i} className=" border border-gray-200 p-4">
<div className="flex items-center gap-3">
<Skeleton className="h-5 w-5" />
<div className="space-y-2">

View File

@@ -13,7 +13,7 @@ function Switch({
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 inline-flex h-5 w-9 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-emerald-600",
"peer data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 data-[state=checked]:bg-primary inline-flex h-5 w-9 shrink-0 items-center border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
@@ -21,7 +21,7 @@ function Switch({
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-background pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=checked]:bg-white data-[state=unchecked]:translate-x-0",
"bg-background pointer-events-none block size-4 ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=checked]:bg-white data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitive.Root>

View File

@@ -26,7 +26,7 @@ function TabsList({
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center p-[3px]",
className
)}
{...props}
@@ -42,7 +42,7 @@ function TabsTrigger({
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}

View File

@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive bg-background text-foreground flex field-sizing-content min-h-16 w-full resize-y rounded-md border px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive bg-background text-foreground flex field-sizing-content min-h-16 w-full resize-y border px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}

View File

@@ -1,8 +0,0 @@
// Copied from shadcn/ui documentation
import * as React from "react"
import { Toaster as Sonner } from "sonner"
export function Toaster() {
return <Sonner richColors position="top-center" />
}

View File

@@ -1 +1,2 @@
export { generateInvoiceEmailTemplate } from "./invoice-email";
export { generatePasswordResetEmailTemplate } from "./password-reset-email";

View File

@@ -81,17 +81,17 @@ export const statusConfig = {
},
sent: {
label: "Sent",
color: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300",
color: "bg-primary/10 text-primary",
description: "Invoice sent to client",
},
paid: {
label: "Paid",
color: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300",
color: "bg-primary/10 text-primary",
description: "Payment received",
},
overdue: {
label: "Overdue",
color: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300",
color: "bg-destructive/10 text-destructive",
description: "Payment is overdue",
},
} as const;

View File

@@ -22,6 +22,8 @@ export const users = createTable("user", (d) => ({
password: d.varchar({ length: 255 }),
emailVerified: d.timestamp().default(sql`CURRENT_TIMESTAMP`),
image: d.varchar({ length: 255 }),
resetToken: d.varchar({ length: 255 }),
resetTokenExpiry: d.timestamp(),
}));
export const usersRelations = relations(users, ({ many }) => ({

File diff suppressed because it is too large Load Diff

View File

@@ -14,72 +14,14 @@ export default {
xs: "475px",
},
fontFamily: {
sans: [
"var(--font-geist-sans)",
"ui-sans-serif",
"system-ui",
"sans-serif",
],
sans: ["var(--font-geist-mono)", "monospace"],
mono: ["var(--font-geist-mono)", "monospace"],
serif: ["var(--font-geist-mono)", "monospace"],
},
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
brand: {
primary: "hsl(var(--brand-primary))",
"primary-hover": "hsl(var(--brand-primary-hover))",
secondary: "hsl(var(--brand-secondary))",
"secondary-hover": "hsl(var(--brand-secondary-hover))",
},
status: {
success: "hsl(var(--status-success))",
"success-foreground": "hsl(var(--status-success-foreground))",
"success-muted": "hsl(var(--status-success-muted))",
warning: "hsl(var(--status-warning))",
"warning-foreground": "hsl(var(--status-warning-foreground))",
"warning-muted": "hsl(var(--status-warning-muted))",
error: "hsl(var(--status-error))",
"error-foreground": "hsl(var(--status-error-foreground))",
"error-muted": "hsl(var(--status-error-muted))",
info: "hsl(var(--status-info))",
"info-foreground": "hsl(var(--status-info-foreground))",
"info-muted": "hsl(var(--status-info-muted))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
navbar: "var(--navbar)",
"navbar-foreground": "var(--navbar-foreground)",
"navbar-border": "var(--navbar-border)",
},
keyframes: {
"accordion-down": {