Compare commits
13 Commits
308f5c8380
...
6ab2946e8a
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ab2946e8a | |||
| cde44ee2d7 | |||
| 655517a27c | |||
| 9a836fc866 | |||
| 275e83a6c0 | |||
| d8d0039c44 | |||
| 6e6e9b0699 | |||
| 044e4fbb07 | |||
| 2d0da9dbeb | |||
| d8e55e85a1 | |||
| e0ff037c06 | |||
| 46a99775a0 | |||
| d50d77538b |
13
bun.lock
13
bun.lock
@@ -13,6 +13,7 @@
|
|||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.1",
|
||||||
"drizzle-orm": "^0.44.4",
|
"drizzle-orm": "^0.44.4",
|
||||||
"ical.js": "^2.2.1",
|
"ical.js": "^2.2.1",
|
||||||
@@ -25,7 +26,9 @@
|
|||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"postgres": "^3.4.7",
|
"postgres": "^3.4.7",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
|
"react-day-picker": "^9.9.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -55,6 +58,8 @@
|
|||||||
|
|
||||||
"@auth/drizzle-adapter": ["@auth/drizzle-adapter@1.10.0", "", { "dependencies": { "@auth/core": "0.40.0" } }, "sha512-3MKsdAINTfvV4QKev8PFMNG93HJEUHh9sggDXnmUmriFogRf8qLvgqnPsTlfUyWcLwTzzrrYjeu8CGM+4IxHwQ=="],
|
"@auth/drizzle-adapter": ["@auth/drizzle-adapter@1.10.0", "", { "dependencies": { "@auth/core": "0.40.0" } }, "sha512-3MKsdAINTfvV4QKev8PFMNG93HJEUHh9sggDXnmUmriFogRf8qLvgqnPsTlfUyWcLwTzzrrYjeu8CGM+4IxHwQ=="],
|
||||||
|
|
||||||
|
"@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="],
|
||||||
|
|
||||||
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
|
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
|
||||||
|
|
||||||
"@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" } }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="],
|
"@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" } }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="],
|
||||||
@@ -507,6 +512,10 @@
|
|||||||
|
|
||||||
"data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="],
|
"data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="],
|
||||||
|
|
||||||
|
"date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
|
||||||
|
|
||||||
|
"date-fns-jalali": ["date-fns-jalali@4.1.0-0", "", {}, "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg=="],
|
||||||
|
|
||||||
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||||
|
|
||||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||||
@@ -897,6 +906,8 @@
|
|||||||
|
|
||||||
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
|
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
|
||||||
|
|
||||||
|
"react-day-picker": ["react-day-picker@9.9.0", "", { "dependencies": { "@date-fns/tz": "^1.4.1", "date-fns": "^4.1.0", "date-fns-jalali": "^4.1.0-0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-NtkJbuX6cl/VaGNb3sVVhmMA6LSMnL5G3xNL+61IyoZj0mUZFWTg4hmj7PHjIQ8MXN9dHWhUHFoJWG6y60DKSg=="],
|
||||||
|
|
||||||
"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.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
|
||||||
|
|
||||||
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||||
@@ -953,6 +964,8 @@
|
|||||||
|
|
||||||
"simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="],
|
"simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="],
|
||||||
|
|
||||||
|
"sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
|
||||||
|
|
||||||
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.1",
|
||||||
"drizzle-orm": "^0.44.4",
|
"drizzle-orm": "^0.44.4",
|
||||||
"ical.js": "^2.2.1",
|
"ical.js": "^2.2.1",
|
||||||
@@ -30,7 +31,9 @@
|
|||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"postgres": "^3.4.7",
|
"postgres": "^3.4.7",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
|
"react-day-picker": "^9.9.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.3.1"
|
"tailwind-merge": "^3.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -3,113 +3,153 @@
|
|||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: oklch(0.9232 0.0026 48.7171);
|
||||||
|
--foreground: oklch(0.2795 0.0368 260.0310);
|
||||||
|
--card: oklch(0.9699 0.0013 106.4238);
|
||||||
|
--card-foreground: oklch(0.2795 0.0368 260.0310);
|
||||||
|
--popover: oklch(0.9699 0.0013 106.4238);
|
||||||
|
--popover-foreground: oklch(0.2795 0.0368 260.0310);
|
||||||
|
--primary: oklch(0.5854 0.2041 277.1173);
|
||||||
|
--primary-foreground: oklch(1.0000 0 0);
|
||||||
|
--secondary: oklch(0.8687 0.0043 56.3660);
|
||||||
|
--secondary-foreground: oklch(0.4461 0.0263 256.8018);
|
||||||
|
--muted: oklch(0.9232 0.0026 48.7171);
|
||||||
|
--muted-foreground: oklch(0.5510 0.0234 264.3637);
|
||||||
|
--accent: oklch(0.9376 0.0260 321.9388);
|
||||||
|
--accent-foreground: oklch(0.3729 0.0306 259.7328);
|
||||||
|
--destructive: oklch(0.6368 0.2078 25.3313);
|
||||||
|
--destructive-foreground: oklch(1.0000 0 0);
|
||||||
|
--border: oklch(0.8687 0.0043 56.3660);
|
||||||
|
--input: oklch(0.8687 0.0043 56.3660);
|
||||||
|
--ring: oklch(0.5854 0.2041 277.1173);
|
||||||
|
--chart-1: oklch(0.5854 0.2041 277.1173);
|
||||||
|
--chart-2: oklch(0.5106 0.2301 276.9656);
|
||||||
|
--chart-3: oklch(0.4568 0.2146 277.0229);
|
||||||
|
--chart-4: oklch(0.3984 0.1773 277.3662);
|
||||||
|
--chart-5: oklch(0.3588 0.1354 278.6973);
|
||||||
|
--sidebar: oklch(0.8687 0.0043 56.3660);
|
||||||
|
--sidebar-foreground: oklch(0.2795 0.0368 260.0310);
|
||||||
|
--sidebar-primary: oklch(0.5854 0.2041 277.1173);
|
||||||
|
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
||||||
|
--sidebar-accent: oklch(0.9376 0.0260 321.9388);
|
||||||
|
--sidebar-accent-foreground: oklch(0.3729 0.0306 259.7328);
|
||||||
|
--sidebar-border: oklch(0.8687 0.0043 56.3660);
|
||||||
|
--sidebar-ring: oklch(0.5854 0.2041 277.1173);
|
||||||
|
--font-sans: Plus Jakarta Sans, sans-serif;
|
||||||
|
--font-serif: Lora, serif;
|
||||||
|
--font-mono: Roboto Mono, monospace;
|
||||||
|
--radius: 1.25rem;
|
||||||
|
--shadow-2xs: 2px 2px 10px 4px hsl(240 4% 60% / 0.09);
|
||||||
|
--shadow-xs: 2px 2px 10px 4px hsl(240 4% 60% / 0.09);
|
||||||
|
--shadow-sm: 2px 2px 10px 4px hsl(240 4% 60% / 0.18), 2px 1px 2px 3px hsl(240 4% 60% / 0.18);
|
||||||
|
--shadow: 2px 2px 10px 4px hsl(240 4% 60% / 0.18), 2px 1px 2px 3px hsl(240 4% 60% / 0.18);
|
||||||
|
--shadow-md: 2px 2px 10px 4px hsl(240 4% 60% / 0.18), 2px 2px 4px 3px hsl(240 4% 60% / 0.18);
|
||||||
|
--shadow-lg: 2px 2px 10px 4px hsl(240 4% 60% / 0.18), 2px 4px 6px 3px hsl(240 4% 60% / 0.18);
|
||||||
|
--shadow-xl: 2px 2px 10px 4px hsl(240 4% 60% / 0.18), 2px 8px 10px 3px hsl(240 4% 60% / 0.18);
|
||||||
|
--shadow-2xl: 2px 2px 10px 4px hsl(240 4% 60% / 0.45);
|
||||||
|
--tracking-normal: 0em;
|
||||||
|
--spacing: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.2244 0.0074 67.4370);
|
||||||
|
--foreground: oklch(0.9288 0.0126 255.5078);
|
||||||
|
--card: oklch(0.2801 0.0080 59.3379);
|
||||||
|
--card-foreground: oklch(0.9288 0.0126 255.5078);
|
||||||
|
--popover: oklch(0.2801 0.0080 59.3379);
|
||||||
|
--popover-foreground: oklch(0.9288 0.0126 255.5078);
|
||||||
|
--primary: oklch(0.5994 0.1568 47.5224);
|
||||||
|
--primary-foreground: oklch(0.2244 0.0074 67.4370);
|
||||||
|
--secondary: oklch(0.3359 0.0077 59.4197);
|
||||||
|
--secondary-foreground: oklch(0.8717 0.0093 258.3382);
|
||||||
|
--muted: oklch(0.2801 0.0080 59.3379);
|
||||||
|
--muted-foreground: oklch(0.7137 0.0192 261.3246);
|
||||||
|
--accent: oklch(0.3896 0.0074 59.4734);
|
||||||
|
--accent-foreground: oklch(0.8717 0.0093 258.3382);
|
||||||
|
--destructive: oklch(0.6368 0.2078 25.3313);
|
||||||
|
--destructive-foreground: oklch(0.2244 0.0074 67.4370);
|
||||||
|
--border: oklch(0.3359 0.0077 59.4197);
|
||||||
|
--input: oklch(0.3359 0.0077 59.4197);
|
||||||
|
--ring: oklch(0.6801 0.1583 276.9349);
|
||||||
|
--chart-1: oklch(0.6801 0.1583 276.9349);
|
||||||
|
--chart-2: oklch(0.5854 0.2041 277.1173);
|
||||||
|
--chart-3: oklch(0.5106 0.2301 276.9656);
|
||||||
|
--chart-4: oklch(0.4568 0.2146 277.0229);
|
||||||
|
--chart-5: oklch(0.3984 0.1773 277.3662);
|
||||||
|
--sidebar: oklch(0.3359 0.0077 59.4197);
|
||||||
|
--sidebar-foreground: oklch(0.9288 0.0126 255.5078);
|
||||||
|
--sidebar-primary: oklch(0.6801 0.1583 276.9349);
|
||||||
|
--sidebar-primary-foreground: oklch(0.2244 0.0074 67.4370);
|
||||||
|
--sidebar-accent: oklch(0.3896 0.0074 59.4734);
|
||||||
|
--sidebar-accent-foreground: oklch(0.8717 0.0093 258.3382);
|
||||||
|
--sidebar-border: oklch(0.3359 0.0077 59.4197);
|
||||||
|
--sidebar-ring: oklch(0.6801 0.1583 276.9349);
|
||||||
|
--font-sans: Plus Jakarta Sans, sans-serif;
|
||||||
|
--font-serif: Lora, serif;
|
||||||
|
--font-mono: Roboto Mono, monospace;
|
||||||
|
--radius: 1.25rem;
|
||||||
|
--shadow-2xs: 2px 2px 10px 4px hsl(0 0% 0% / 0.09);
|
||||||
|
--shadow-xs: 2px 2px 10px 4px hsl(0 0% 0% / 0.09);
|
||||||
|
--shadow-sm: 2px 2px 10px 4px hsl(0 0% 0% / 0.18), 2px 1px 2px 3px hsl(0 0% 0% / 0.18);
|
||||||
|
--shadow: 2px 2px 10px 4px hsl(0 0% 0% / 0.18), 2px 1px 2px 3px hsl(0 0% 0% / 0.18);
|
||||||
|
--shadow-md: 2px 2px 10px 4px hsl(0 0% 0% / 0.18), 2px 2px 4px 3px hsl(0 0% 0% / 0.18);
|
||||||
|
--shadow-lg: 2px 2px 10px 4px hsl(0 0% 0% / 0.18), 2px 4px 6px 3px hsl(0 0% 0% / 0.18);
|
||||||
|
--shadow-xl: 2px 2px 10px 4px hsl(0 0% 0% / 0.18), 2px 8px 10px 3px hsl(0 0% 0% / 0.18);
|
||||||
|
--shadow-2xl: 2px 2px 10px 4px hsl(0 0% 0% / 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--font-sans: var(--font-geist-sans);
|
|
||||||
--font-mono: var(--font-geist-mono);
|
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
||||||
--color-sidebar-accent: var(--sidebar-accent);
|
|
||||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
||||||
--color-sidebar-primary: var(--sidebar-primary);
|
|
||||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
||||||
--color-sidebar: var(--sidebar);
|
|
||||||
--color-chart-5: var(--chart-5);
|
|
||||||
--color-chart-4: var(--chart-4);
|
|
||||||
--color-chart-3: var(--chart-3);
|
|
||||||
--color-chart-2: var(--chart-2);
|
|
||||||
--color-chart-1: var(--chart-1);
|
|
||||||
--color-ring: var(--ring);
|
|
||||||
--color-input: var(--input);
|
|
||||||
--color-border: var(--border);
|
|
||||||
--color-destructive: var(--destructive);
|
|
||||||
--color-accent-foreground: var(--accent-foreground);
|
|
||||||
--color-accent: var(--accent);
|
|
||||||
--color-muted-foreground: var(--muted-foreground);
|
|
||||||
--color-muted: var(--muted);
|
|
||||||
--color-secondary-foreground: var(--secondary-foreground);
|
|
||||||
--color-secondary: var(--secondary);
|
|
||||||
--color-primary-foreground: var(--primary-foreground);
|
|
||||||
--color-primary: var(--primary);
|
|
||||||
--color-popover-foreground: var(--popover-foreground);
|
|
||||||
--color-popover: var(--popover);
|
|
||||||
--color-card-foreground: var(--card-foreground);
|
|
||||||
--color-card: var(--card);
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
|
||||||
|
--font-sans: var(--font-sans);
|
||||||
|
--font-mono: var(--font-mono);
|
||||||
|
--font-serif: var(--font-serif);
|
||||||
|
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
--radius-lg: var(--radius);
|
--radius-lg: var(--radius);
|
||||||
--radius-xl: calc(var(--radius) + 4px);
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
--shadow-2xs: var(--shadow-2xs);
|
||||||
--radius: 0.65rem;
|
--shadow-xs: var(--shadow-xs);
|
||||||
--background: oklch(1 0 0);
|
--shadow-sm: var(--shadow-sm);
|
||||||
--foreground: oklch(0.141 0.005 285.823);
|
--shadow: var(--shadow);
|
||||||
--card: oklch(1 0 0);
|
--shadow-md: var(--shadow-md);
|
||||||
--card-foreground: oklch(0.141 0.005 285.823);
|
--shadow-lg: var(--shadow-lg);
|
||||||
--popover: oklch(1 0 0);
|
--shadow-xl: var(--shadow-xl);
|
||||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
--shadow-2xl: var(--shadow-2xl);
|
||||||
--primary: oklch(0.705 0.213 47.604);
|
|
||||||
--primary-foreground: oklch(0.98 0.016 73.684);
|
|
||||||
--secondary: oklch(0.967 0.001 286.375);
|
|
||||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
|
||||||
--muted: oklch(0.967 0.001 286.375);
|
|
||||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
|
||||||
--accent: oklch(0.967 0.001 286.375);
|
|
||||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
|
||||||
--border: oklch(0.92 0.004 286.32);
|
|
||||||
--input: oklch(0.92 0.004 286.32);
|
|
||||||
--ring: oklch(0.705 0.213 47.604);
|
|
||||||
--chart-1: oklch(0.646 0.222 41.116);
|
|
||||||
--chart-2: oklch(0.6 0.118 184.704);
|
|
||||||
--chart-3: oklch(0.398 0.07 227.392);
|
|
||||||
--chart-4: oklch(0.828 0.189 84.429);
|
|
||||||
--chart-5: oklch(0.769 0.188 70.08);
|
|
||||||
--sidebar: oklch(0.985 0 0);
|
|
||||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
|
||||||
--sidebar-primary: oklch(0.705 0.213 47.604);
|
|
||||||
--sidebar-primary-foreground: oklch(0.98 0.016 73.684);
|
|
||||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
|
||||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
|
||||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
|
||||||
--sidebar-ring: oklch(0.705 0.213 47.604);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
--background: oklch(0.141 0.005 285.823);
|
|
||||||
--foreground: oklch(0.985 0 0);
|
|
||||||
--card: oklch(0.21 0.006 285.885);
|
|
||||||
--card-foreground: oklch(0.985 0 0);
|
|
||||||
--popover: oklch(0.21 0.006 285.885);
|
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
|
||||||
--primary: oklch(0.646 0.222 41.116);
|
|
||||||
--primary-foreground: oklch(0.98 0.016 73.684);
|
|
||||||
--secondary: oklch(0.274 0.006 286.033);
|
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
|
||||||
--muted: oklch(0.274 0.006 286.033);
|
|
||||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
|
||||||
--accent: oklch(0.274 0.006 286.033);
|
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
|
||||||
--border: oklch(1 0 0 / 10%);
|
|
||||||
--input: oklch(1 0 0 / 15%);
|
|
||||||
--ring: oklch(0.646 0.222 41.116);
|
|
||||||
--chart-1: oklch(0.488 0.243 264.376);
|
|
||||||
--chart-2: oklch(0.696 0.17 162.48);
|
|
||||||
--chart-3: oklch(0.769 0.188 70.08);
|
|
||||||
--chart-4: oklch(0.627 0.265 303.9);
|
|
||||||
--chart-5: oklch(0.645 0.246 16.439);
|
|
||||||
--sidebar: oklch(0.21 0.006 285.885);
|
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-primary: oklch(0.646 0.222 41.116);
|
|
||||||
--sidebar-primary-foreground: oklch(0.98 0.016 73.684);
|
|
||||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
|
||||||
--sidebar-ring: oklch(0.646 0.222 41.116);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist } from "next/font/google";
|
import { Geist, Magra } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { ThemeProvider } from "next-themes";
|
import { ThemeProvider } from "next-themes";
|
||||||
import { ModeToggle } from "@/components/mode-toggle";
|
import { ModeToggle } from "@/components/mode-toggle";
|
||||||
import SignIn from "@/components/sign-in";
|
import SignIn from "@/components/sign-in";
|
||||||
import AuthSessionProvider from "@/components/SessionProvider";
|
import AuthSessionProvider from "@/components/SessionProvider";
|
||||||
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
|
|
||||||
const geist = Geist({ subsets: ['latin', 'cyrillic'], variable: "--font-geist-sans" })
|
const geist = Geist({ subsets: ['latin', 'cyrillic'], variable: "--font-geist-sans" })
|
||||||
|
|
||||||
|
const magra = Magra({ subsets: ["latin"], weight: "400", variable: "--font-cascadia-code" })
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Local iCal',
|
title: 'Local iCal',
|
||||||
description: 'Local iCal editor for calendar events',
|
description: 'Local iCal editor for calendar events',
|
||||||
@@ -32,9 +35,11 @@ export default function RootLayout({
|
|||||||
enableSystem
|
enableSystem
|
||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
<header className="dark:text-white text-gray-900 px-4 py-3 font-bold shadow flex justify-between items-center-safe">
|
<header className="dark:text-white text-gray-900 px-4 py-3 font-bold flex justify-between items-center-safe">
|
||||||
<Link href={"/"}>
|
<Link href={"/"}>
|
||||||
{metadata.title as string || "iCal PWA"}
|
<p className={`${magra.variable}`}>
|
||||||
|
{metadata.title as string || "iCal PWA"}
|
||||||
|
</p>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
<SignIn />
|
<SignIn />
|
||||||
@@ -42,6 +47,7 @@ export default function RootLayout({
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main className="flex-1 p-4">{children}</main>
|
<main className="flex-1 p-4">{children}</main>
|
||||||
|
<Toaster closeButton richColors />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</AuthSessionProvider>
|
</AuthSessionProvider>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
304
src/app/page.tsx
304
src/app/page.tsx
@@ -5,7 +5,10 @@ import { nanoid } from 'nanoid'
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Card } from '@/components/ui/card'
|
import { Card, CardHeader, CardContent } from '@/components/ui/card'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { LucideMapPin, Clock, MoreHorizontal, Calendar1Icon } from 'lucide-react'
|
||||||
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, DropdownMenuItem } from '@/components/ui/dropdown-menu'
|
||||||
import { RecurrencePicker } from '@/components/recurrence-picker'
|
import { RecurrencePicker } from '@/components/recurrence-picker'
|
||||||
import { IcsFilePicker } from '@/components/ics-file-picker'
|
import { IcsFilePicker } from '@/components/ics-file-picker'
|
||||||
|
|
||||||
@@ -14,6 +17,112 @@ import { parseICS, generateICS } from '@/lib/ical'
|
|||||||
import type { CalendarEvent } from '@/lib/types'
|
import type { CalendarEvent } from '@/lib/types'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
|
// Individual event card component
|
||||||
|
const EventCard = ({ event, onEdit, onDelete }: { event: CalendarEvent, onEdit: (event: CalendarEvent) => void, onDelete: (eventId: string) => void }) => {
|
||||||
|
const formatDateTime = (dateStr: string, allDay: boolean | undefined) => {
|
||||||
|
return allDay
|
||||||
|
? new Date(dateStr).toLocaleDateString()
|
||||||
|
: new Date(dateStr).toLocaleString()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEdit = () => {
|
||||||
|
onEdit({
|
||||||
|
id: event.id,
|
||||||
|
title: event.title,
|
||||||
|
description: event.description || '',
|
||||||
|
location: event.location || '',
|
||||||
|
url: event.url || '',
|
||||||
|
start: event.start,
|
||||||
|
end: event.end || '',
|
||||||
|
allDay: event.allDay || false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="space-y-1 flex-1">
|
||||||
|
<h3 className="font-semibold leading-none tracking-tight">
|
||||||
|
{event.title}
|
||||||
|
</h3>
|
||||||
|
{event.recurrenceRule && (
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
Repeats: {event.recurrenceRule}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{event.description && (
|
||||||
|
<p className="text-sm text-muted-foreground mt-2 break-words">
|
||||||
|
{event.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Open menu</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem onClick={handleEdit}>
|
||||||
|
Edit event
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => onDelete(event.id)}
|
||||||
|
className="text-destructive"
|
||||||
|
>
|
||||||
|
Delete event
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="pt-0">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center text-sm text-muted-foreground">
|
||||||
|
<Clock className="mr-2 h-4 w-4" />
|
||||||
|
{formatDateTime(event.start, event.allDay)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{event.location && (
|
||||||
|
<div className="flex items-center text-sm text-muted-foreground">
|
||||||
|
<LucideMapPin className="mr-2 h-4 w-4" />
|
||||||
|
{event.location}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const EventsList = ({ events, onEdit, onDelete }: { events: CalendarEvent[], onEdit: (event: CalendarEvent) => void, onDelete: (eventId: string) => void }) => {
|
||||||
|
if (events.length === 0) {
|
||||||
|
|
||||||
|
return (<div className="flex flex-col items-center justify-center py-8 text-center">
|
||||||
|
<Calendar1Icon className='h-12 w-12 text-muted-foreground mb-4' />
|
||||||
|
<h3 className="text-lg font-medium text-muted-foreground">No events yet</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">Create your first event to get started</p>
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{events.map(event => (
|
||||||
|
<EventCard
|
||||||
|
key={event.id}
|
||||||
|
event={event}
|
||||||
|
onEdit={onEdit}
|
||||||
|
onDelete={onDelete}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const [events, setEvents] = useState<CalendarEvent[]>([])
|
const [events, setEvents] = useState<CalendarEvent[]>([])
|
||||||
@@ -128,7 +237,7 @@ export default function HomePage() {
|
|||||||
if (file.name.endsWith('.ics')) {
|
if (file.name.endsWith('.ics')) {
|
||||||
handleImport(file)
|
handleImport(file)
|
||||||
} else {
|
} else {
|
||||||
alert('Please drop an .ics file')
|
toast.warning('Please drop an .ics file')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,60 +246,88 @@ export default function HomePage() {
|
|||||||
const handleAiCreate = async () => {
|
const handleAiCreate = async () => {
|
||||||
if (!aiPrompt.trim()) return
|
if (!aiPrompt.trim()) return
|
||||||
setAiLoading(true)
|
setAiLoading(true)
|
||||||
try {
|
|
||||||
const res = await fetch('/api/ai-event', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ prompt: aiPrompt })
|
|
||||||
})
|
|
||||||
|
|
||||||
if (res.status === 401) {
|
const promise = (): Promise<{ message: string }> => new Promise(async (resolve, reject) => {
|
||||||
alert('Please sign in to use AI features.')
|
try {
|
||||||
setAiLoading(false)
|
const res = await fetch('/api/ai-event', {
|
||||||
return
|
method: 'POST',
|
||||||
}
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ prompt: aiPrompt })
|
||||||
|
})
|
||||||
|
|
||||||
const data = await res.json()
|
if (res.status === 401) {
|
||||||
|
setAiLoading(false)
|
||||||
if (Array.isArray(data) && data.length > 0) {
|
reject({
|
||||||
if (data.length === 1) {
|
message: 'Please sign in to use AI features.'
|
||||||
// Prefill dialog directly (same as before)
|
})
|
||||||
const ev = data[0]
|
return
|
||||||
setTitle(ev.title || '')
|
|
||||||
setDescription(ev.description || '')
|
|
||||||
setLocation(ev.location || '')
|
|
||||||
setUrl(ev.url || '')
|
|
||||||
setStart(ev.start || '')
|
|
||||||
setEnd(ev.end || '')
|
|
||||||
setAllDay(ev.allDay || false)
|
|
||||||
setEditingId(null)
|
|
||||||
setDialogOpen(true)
|
|
||||||
setRecurrenceRule(ev.recurrenceRule || undefined)
|
|
||||||
} else {
|
|
||||||
// Save them all directly to DB
|
|
||||||
for (const ev of data) {
|
|
||||||
const newEvent = {
|
|
||||||
id: nanoid(),
|
|
||||||
...ev,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
lastModified: new Date().toISOString(),
|
|
||||||
}
|
|
||||||
await addEvent(newEvent)
|
|
||||||
}
|
|
||||||
const stored = await getAllEvents()
|
|
||||||
setEvents(stored)
|
|
||||||
setSummary(`Added ${data.length} AI-generated events.`)
|
|
||||||
setSummaryUpdated(new Date().toLocaleString())
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
alert('AI did not return event data.')
|
const data = await res.json()
|
||||||
|
|
||||||
|
if (Array.isArray(data) && data.length > 0) {
|
||||||
|
if (data.length === 1) {
|
||||||
|
// Prefill dialog directly (same as before)
|
||||||
|
const ev = data[0]
|
||||||
|
setTitle(ev.title || '')
|
||||||
|
setDescription(ev.description || '')
|
||||||
|
setLocation(ev.location || '')
|
||||||
|
setUrl(ev.url || '')
|
||||||
|
setStart(ev.start || '')
|
||||||
|
setEnd(ev.end || '')
|
||||||
|
setAllDay(ev.allDay || false)
|
||||||
|
setEditingId(null)
|
||||||
|
setAiPrompt("")
|
||||||
|
setDialogOpen(true)
|
||||||
|
setRecurrenceRule(ev.recurrenceRule || undefined)
|
||||||
|
resolve({
|
||||||
|
message: 'Event has been created!'
|
||||||
|
})
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Save them all directly to DB
|
||||||
|
for (const ev of data) {
|
||||||
|
const newEvent = {
|
||||||
|
id: nanoid(),
|
||||||
|
...ev,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
lastModified: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
await addEvent(newEvent)
|
||||||
|
}
|
||||||
|
const stored = await getAllEvents()
|
||||||
|
setEvents(stored)
|
||||||
|
setAiPrompt("")
|
||||||
|
setSummary(`Added ${data.length} AI-generated events.`)
|
||||||
|
setSummaryUpdated(new Date().toLocaleString())
|
||||||
|
resolve({
|
||||||
|
message: 'Event has been created!'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reject({
|
||||||
|
message: 'AI did not return event data.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
reject({
|
||||||
|
message: 'Error from AI service.'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} catch (err) {
|
})
|
||||||
console.error(err)
|
|
||||||
alert('Error from AI service.')
|
toast.promise(promise, {
|
||||||
} finally {
|
loading: "Generating event...",
|
||||||
setAiLoading(false)
|
success: ({ message }) => {
|
||||||
}
|
return message
|
||||||
|
},
|
||||||
|
error: ({ message }) => {
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
setAiLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AI Summarize Events
|
// AI Summarize Events
|
||||||
@@ -231,16 +368,18 @@ export default function HomePage() {
|
|||||||
{/* AI Toolbar */}
|
{/* AI Toolbar */}
|
||||||
{status === "loading" ? <div className='mb-4 p-4 text-center animate-pulse bg-muted'>Loading...</div> : <div>
|
{status === "loading" ? <div className='mb-4 p-4 text-center animate-pulse bg-muted'>Loading...</div> : <div>
|
||||||
{session?.user ? (
|
{session?.user ? (
|
||||||
<div className="flex flex-row gap-4 mb-4 items-start">
|
<div className="flex flex-col sm:flex-row gap-4 mb-4 items-start">
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
<Textarea
|
<Textarea
|
||||||
className="wrap-anywhere min-h-12"
|
className="wrap-anywhere field-sizing-content resize-none w-full min-h-[2.5rem] max-h-64 overflow-y-auto sm:overflow-y-visible px-3 py-2 scroll-p-8 placeholder:italic"
|
||||||
|
// Band-aid for scrollbar clipping out of the box
|
||||||
|
style={{ clipPath: "inset(0 round 1rem)" }}
|
||||||
placeholder='Describe event for AI to create'
|
placeholder='Describe event for AI to create'
|
||||||
value={aiPrompt}
|
value={aiPrompt}
|
||||||
onChange={e => setAiPrompt(e.target.value)}
|
onChange={e => setAiPrompt(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-row gap-2 pt-1.5'>
|
<div className='flex flex-row gap-2 pt-1'>
|
||||||
<Button onClick={handleAiCreate} disabled={aiLoading}>
|
<Button onClick={handleAiCreate} disabled={aiLoading}>
|
||||||
{aiLoading ? 'Thinking...' : 'AI Create'}
|
{aiLoading ? 'Thinking...' : 'AI Create'}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -249,7 +388,7 @@ export default function HomePage() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="mb-4 p-4 border border-dashed rounded-lg text-center">
|
<div className="mb-4 p-4 border border-dashed rounded-lg text-center">
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
Sign in to unlock AI natural language event creation
|
Sign in to unlock natural language event creation powered by AI
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -290,41 +429,24 @@ export default function HomePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Event List */}
|
{/* Event List */}
|
||||||
{events.length === 0 && <p className="text-gray-500 italic">No events yet</p>}
|
{events.length === 0 && (
|
||||||
<ul className="space-y-2">
|
<>
|
||||||
{events.map(ev => (
|
</>)}
|
||||||
<li key={ev.id} className="p-3 border rounded flex justify-between items-start">
|
<EventsList
|
||||||
<div>
|
events={events}
|
||||||
<div className="font-semibold">{ev.title}</div>
|
onEdit={(eventData) => {
|
||||||
{ev.recurrenceRule && (
|
setTitle(eventData.title)
|
||||||
<div className="text-xs text-blue-600 mt-1">
|
setDescription(eventData.description || "")
|
||||||
Repeats: {ev.recurrenceRule}
|
setLocation(eventData.location || "")
|
||||||
</div>
|
setUrl(eventData.url || "")
|
||||||
)}
|
setStart(eventData.start)
|
||||||
<div className="text-sm text-gray-500">
|
setEnd(eventData.end || "")
|
||||||
{ev.allDay ? ev.start.split('T')[0] : new Date(ev.start).toLocaleString()}
|
setAllDay(eventData.allDay || false)
|
||||||
{ev.location && <span> @ {ev.location}</span>}
|
setEditingId(eventData.id)
|
||||||
</div>
|
setDialogOpen(true)
|
||||||
{ev.description && <div className="text-sm mt-1 wrap-anywhere">{ev.description}</div>}
|
}}
|
||||||
</div>
|
onDelete={handleDelete}
|
||||||
<div className="flex gap-2">
|
/>
|
||||||
<Button size="sm" onClick={() => {
|
|
||||||
setTitle(ev.title)
|
|
||||||
setDescription(ev.description || '')
|
|
||||||
setLocation(ev.location || '')
|
|
||||||
setUrl(ev.url || '')
|
|
||||||
setStart(ev.start)
|
|
||||||
setEnd(ev.end || '')
|
|
||||||
setAllDay(ev.allDay || false)
|
|
||||||
setEditingId(ev.id)
|
|
||||||
setDialogOpen(true)
|
|
||||||
}}>Edit</Button>
|
|
||||||
<Button variant="secondary" size="sm" onClick={() => handleDelete(ev.id)}>Delete</Button>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
|
|
||||||
{/* Add/Edit Dialog */}
|
{/* Add/Edit Dialog */}
|
||||||
<Dialog open={dialogOpen} onOpenChange={val => { if (!val) resetForm(); setDialogOpen(val) }}>
|
<Dialog open={dialogOpen} onOpenChange={val => { if (!val) resetForm(); setDialogOpen(val) }}>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Moon, Sun } from "lucide-react"
|
import { Moon, Sun, Monitor } from "lucide-react"
|
||||||
import { useTheme } from "next-themes"
|
import { useTheme } from "next-themes"
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
@@ -12,15 +12,51 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/components/ui/dropdown-menu"
|
||||||
|
|
||||||
|
type ThemeIconProps = {
|
||||||
|
theme?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeIcon = ({ theme }: ThemeIconProps) => {
|
||||||
|
|
||||||
|
const [mounted, setMounted] = React.useState(false)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setMounted(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (theme) {
|
||||||
|
case "light":
|
||||||
|
return (
|
||||||
|
<Sun className="absolute h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
|
||||||
|
)
|
||||||
|
case "dark":
|
||||||
|
return (
|
||||||
|
<Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
|
||||||
|
)
|
||||||
|
case "system":
|
||||||
|
return (
|
||||||
|
<Monitor className="absolute h-[1.2rem] w-[1.2rem] scale-100" />
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return (<>
|
||||||
|
<Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
|
||||||
|
<Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
|
||||||
|
</>)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function ModeToggle() {
|
export function ModeToggle() {
|
||||||
const { setTheme } = useTheme()
|
const { setTheme, theme } = useTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="outline" size="icon">
|
<Button variant="outline" size="icon">
|
||||||
<Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
|
<ThemeIcon theme={theme} />
|
||||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
|
|
||||||
<span className="sr-only">Toggle theme</span>
|
<span className="sr-only">Toggle theme</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|||||||
46
src/components/ui/badge.tsx
Normal file
46
src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const 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",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||||
|
secondary:
|
||||||
|
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||||
|
destructive:
|
||||||
|
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Badge({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span"> &
|
||||||
|
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||||
|
const Comp = asChild ? Slot : "span"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="badge"
|
||||||
|
className={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
||||||
@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
"active:scale-[.95] inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium duration-100 transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
213
src/components/ui/calendar.tsx
Normal file
213
src/components/ui/calendar.tsx
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import {
|
||||||
|
ChevronDownIcon,
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
} from "lucide-react"
|
||||||
|
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button, buttonVariants } from "@/components/ui/button"
|
||||||
|
|
||||||
|
function Calendar({
|
||||||
|
className,
|
||||||
|
classNames,
|
||||||
|
showOutsideDays = true,
|
||||||
|
captionLayout = "label",
|
||||||
|
buttonVariant = "ghost",
|
||||||
|
formatters,
|
||||||
|
components,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DayPicker> & {
|
||||||
|
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
|
||||||
|
}) {
|
||||||
|
const defaultClassNames = getDefaultClassNames()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DayPicker
|
||||||
|
showOutsideDays={showOutsideDays}
|
||||||
|
className={cn(
|
||||||
|
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
|
||||||
|
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
||||||
|
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
captionLayout={captionLayout}
|
||||||
|
formatters={{
|
||||||
|
formatMonthDropdown: (date) =>
|
||||||
|
date.toLocaleString("default", { month: "short" }),
|
||||||
|
...formatters,
|
||||||
|
}}
|
||||||
|
classNames={{
|
||||||
|
root: cn("w-fit", defaultClassNames.root),
|
||||||
|
months: cn(
|
||||||
|
"flex gap-4 flex-col md:flex-row relative",
|
||||||
|
defaultClassNames.months
|
||||||
|
),
|
||||||
|
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
|
||||||
|
nav: cn(
|
||||||
|
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
|
||||||
|
defaultClassNames.nav
|
||||||
|
),
|
||||||
|
button_previous: cn(
|
||||||
|
buttonVariants({ variant: buttonVariant }),
|
||||||
|
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||||
|
defaultClassNames.button_previous
|
||||||
|
),
|
||||||
|
button_next: cn(
|
||||||
|
buttonVariants({ variant: buttonVariant }),
|
||||||
|
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||||
|
defaultClassNames.button_next
|
||||||
|
),
|
||||||
|
month_caption: cn(
|
||||||
|
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
|
||||||
|
defaultClassNames.month_caption
|
||||||
|
),
|
||||||
|
dropdowns: cn(
|
||||||
|
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
|
||||||
|
defaultClassNames.dropdowns
|
||||||
|
),
|
||||||
|
dropdown_root: cn(
|
||||||
|
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
|
||||||
|
defaultClassNames.dropdown_root
|
||||||
|
),
|
||||||
|
dropdown: cn(
|
||||||
|
"absolute bg-popover inset-0 opacity-0",
|
||||||
|
defaultClassNames.dropdown
|
||||||
|
),
|
||||||
|
caption_label: cn(
|
||||||
|
"select-none font-medium",
|
||||||
|
captionLayout === "label"
|
||||||
|
? "text-sm"
|
||||||
|
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
|
||||||
|
defaultClassNames.caption_label
|
||||||
|
),
|
||||||
|
table: "w-full border-collapse",
|
||||||
|
weekdays: cn("flex", defaultClassNames.weekdays),
|
||||||
|
weekday: cn(
|
||||||
|
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
|
||||||
|
defaultClassNames.weekday
|
||||||
|
),
|
||||||
|
week: cn("flex w-full mt-2", defaultClassNames.week),
|
||||||
|
week_number_header: cn(
|
||||||
|
"select-none w-(--cell-size)",
|
||||||
|
defaultClassNames.week_number_header
|
||||||
|
),
|
||||||
|
week_number: cn(
|
||||||
|
"text-[0.8rem] select-none text-muted-foreground",
|
||||||
|
defaultClassNames.week_number
|
||||||
|
),
|
||||||
|
day: cn(
|
||||||
|
"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
|
||||||
|
defaultClassNames.day
|
||||||
|
),
|
||||||
|
range_start: cn(
|
||||||
|
"rounded-l-md bg-accent",
|
||||||
|
defaultClassNames.range_start
|
||||||
|
),
|
||||||
|
range_middle: cn("rounded-none", defaultClassNames.range_middle),
|
||||||
|
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
|
||||||
|
today: cn(
|
||||||
|
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
|
||||||
|
defaultClassNames.today
|
||||||
|
),
|
||||||
|
outside: cn(
|
||||||
|
"text-muted-foreground aria-selected:text-muted-foreground",
|
||||||
|
defaultClassNames.outside
|
||||||
|
),
|
||||||
|
disabled: cn(
|
||||||
|
"text-muted-foreground opacity-50",
|
||||||
|
defaultClassNames.disabled
|
||||||
|
),
|
||||||
|
hidden: cn("invisible", defaultClassNames.hidden),
|
||||||
|
...classNames,
|
||||||
|
}}
|
||||||
|
components={{
|
||||||
|
Root: ({ className, rootRef, ...props }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="calendar"
|
||||||
|
ref={rootRef}
|
||||||
|
className={cn(className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
Chevron: ({ className, orientation, ...props }) => {
|
||||||
|
if (orientation === "left") {
|
||||||
|
return (
|
||||||
|
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orientation === "right") {
|
||||||
|
return (
|
||||||
|
<ChevronRightIcon
|
||||||
|
className={cn("size-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChevronDownIcon className={cn("size-4", className)} {...props} />
|
||||||
|
)
|
||||||
|
},
|
||||||
|
DayButton: CalendarDayButton,
|
||||||
|
WeekNumber: ({ children, ...props }) => {
|
||||||
|
return (
|
||||||
|
<td {...props}>
|
||||||
|
<div className="flex size-(--cell-size) items-center justify-center text-center">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
...components,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CalendarDayButton({
|
||||||
|
className,
|
||||||
|
day,
|
||||||
|
modifiers,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DayButton>) {
|
||||||
|
const defaultClassNames = getDefaultClassNames()
|
||||||
|
|
||||||
|
const ref = React.useRef<HTMLButtonElement>(null)
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (modifiers.focused) ref.current?.focus()
|
||||||
|
}, [modifiers.focused])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
ref={ref}
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
data-day={day.date.toLocaleDateString()}
|
||||||
|
data-selected-single={
|
||||||
|
modifiers.selected &&
|
||||||
|
!modifiers.range_start &&
|
||||||
|
!modifiers.range_end &&
|
||||||
|
!modifiers.range_middle
|
||||||
|
}
|
||||||
|
data-range-start={modifiers.range_start}
|
||||||
|
data-range-end={modifiers.range_end}
|
||||||
|
data-range-middle={modifiers.range_middle}
|
||||||
|
className={cn(
|
||||||
|
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
|
||||||
|
defaultClassNames.day,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Calendar, CalendarDayButton }
|
||||||
25
src/components/ui/sonner.tsx
Normal file
25
src/components/ui/sonner.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useTheme } from "next-themes"
|
||||||
|
import { Toaster as Sonner, ToasterProps } from "sonner"
|
||||||
|
|
||||||
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
|
const { theme = "system" } = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sonner
|
||||||
|
theme={theme as ToasterProps["theme"]}
|
||||||
|
className="toaster group"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--normal-bg": "var(--popover)",
|
||||||
|
"--normal-text": "var(--popover-foreground)",
|
||||||
|
"--normal-border": "var(--border)",
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Toaster }
|
||||||
Reference in New Issue
Block a user