first commit
This commit is contained in:
		
							
								
								
									
										22
									
								
								components.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								components.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | { | ||||||
|  |   "$schema": "https://ui.shadcn.com/schema.json", | ||||||
|  |   "style": "new-york", | ||||||
|  |   "rsc": false, | ||||||
|  |   "tsx": true, | ||||||
|  |   "tailwind": { | ||||||
|  |     "config": "", | ||||||
|  |     "css": "src/styles/global.css", | ||||||
|  |     "baseColor": "neutral", | ||||||
|  |     "cssVariables": true, | ||||||
|  |     "prefix": "" | ||||||
|  |   }, | ||||||
|  |   "aliases": { | ||||||
|  |     "components": "@/components", | ||||||
|  |     "utils": "@/lib/utils", | ||||||
|  |     "ui": "@/components/ui", | ||||||
|  |     "lib": "@/lib", | ||||||
|  |     "hooks": "@/hooks" | ||||||
|  |   }, | ||||||
|  |   "iconLibrary": "lucide" | ||||||
|  | } | ||||||
|  |  | ||||||
							
								
								
									
										129
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										129
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -10,15 +10,24 @@ | |||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@astrojs/mdx": "^4.3.3", |         "@astrojs/mdx": "^4.3.3", | ||||||
|         "@astrojs/react": "^4.3.0", |         "@astrojs/react": "^4.3.0", | ||||||
|  |         "@radix-ui/react-label": "^2.1.7", | ||||||
|  |         "@radix-ui/react-slot": "^1.2.3", | ||||||
|         "@tailwindcss/vite": "^4.1.3", |         "@tailwindcss/vite": "^4.1.3", | ||||||
|         "@types/canvas-confetti": "^1.9.0", |         "@types/canvas-confetti": "^1.9.0", | ||||||
|         "@types/react": "^19.1.9", |         "@types/react": "^19.1.9", | ||||||
|         "@types/react-dom": "^19.1.7", |         "@types/react-dom": "^19.1.7", | ||||||
|         "astro": "^5.12.9", |         "astro": "^5.12.9", | ||||||
|         "canvas-confetti": "^1.9.3", |         "canvas-confetti": "^1.9.3", | ||||||
|  |         "class-variance-authority": "^0.7.1", | ||||||
|  |         "clsx": "^2.1.1", | ||||||
|  |         "lucide-react": "^0.539.0", | ||||||
|         "react": "^19.1.1", |         "react": "^19.1.1", | ||||||
|         "react-dom": "^19.1.1", |         "react-dom": "^19.1.1", | ||||||
|  |         "tailwind-merge": "^3.3.1", | ||||||
|         "tailwindcss": "^4.1.3" |         "tailwindcss": "^4.1.3" | ||||||
|  |       }, | ||||||
|  |       "devDependencies": { | ||||||
|  |         "tw-animate-css": "^1.3.6" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@ampproject/remapping": { |     "node_modules/@ampproject/remapping": { | ||||||
| @@ -1409,6 +1418,85 @@ | |||||||
|       "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", |       "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", | ||||||
|       "license": "MIT" |       "license": "MIT" | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/@radix-ui/react-compose-refs": { | ||||||
|  |       "version": "1.1.2", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", | ||||||
|  |       "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "peerDependencies": { | ||||||
|  |         "@types/react": "*", | ||||||
|  |         "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" | ||||||
|  |       }, | ||||||
|  |       "peerDependenciesMeta": { | ||||||
|  |         "@types/react": { | ||||||
|  |           "optional": true | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/@radix-ui/react-label": { | ||||||
|  |       "version": "2.1.7", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", | ||||||
|  |       "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "@radix-ui/react-primitive": "2.1.3" | ||||||
|  |       }, | ||||||
|  |       "peerDependencies": { | ||||||
|  |         "@types/react": "*", | ||||||
|  |         "@types/react-dom": "*", | ||||||
|  |         "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", | ||||||
|  |         "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" | ||||||
|  |       }, | ||||||
|  |       "peerDependenciesMeta": { | ||||||
|  |         "@types/react": { | ||||||
|  |           "optional": true | ||||||
|  |         }, | ||||||
|  |         "@types/react-dom": { | ||||||
|  |           "optional": true | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/@radix-ui/react-primitive": { | ||||||
|  |       "version": "2.1.3", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", | ||||||
|  |       "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "@radix-ui/react-slot": "1.2.3" | ||||||
|  |       }, | ||||||
|  |       "peerDependencies": { | ||||||
|  |         "@types/react": "*", | ||||||
|  |         "@types/react-dom": "*", | ||||||
|  |         "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", | ||||||
|  |         "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" | ||||||
|  |       }, | ||||||
|  |       "peerDependenciesMeta": { | ||||||
|  |         "@types/react": { | ||||||
|  |           "optional": true | ||||||
|  |         }, | ||||||
|  |         "@types/react-dom": { | ||||||
|  |           "optional": true | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/@radix-ui/react-slot": { | ||||||
|  |       "version": "1.2.3", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", | ||||||
|  |       "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "@radix-ui/react-compose-refs": "1.1.2" | ||||||
|  |       }, | ||||||
|  |       "peerDependencies": { | ||||||
|  |         "@types/react": "*", | ||||||
|  |         "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" | ||||||
|  |       }, | ||||||
|  |       "peerDependenciesMeta": { | ||||||
|  |         "@types/react": { | ||||||
|  |           "optional": true | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/@rolldown/pluginutils": { |     "node_modules/@rolldown/pluginutils": { | ||||||
|       "version": "1.0.0-beta.27", |       "version": "1.0.0-beta.27", | ||||||
|       "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", |       "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", | ||||||
| @@ -2803,6 +2891,18 @@ | |||||||
|         "node": ">=8" |         "node": ">=8" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/class-variance-authority": { | ||||||
|  |       "version": "0.7.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", | ||||||
|  |       "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", | ||||||
|  |       "license": "Apache-2.0", | ||||||
|  |       "dependencies": { | ||||||
|  |         "clsx": "^2.1.1" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "url": "https://polar.sh/cva" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/cli-boxes": { |     "node_modules/cli-boxes": { | ||||||
|       "version": "3.0.0", |       "version": "3.0.0", | ||||||
|       "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", |       "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", | ||||||
| @@ -4168,6 +4268,15 @@ | |||||||
|       "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", |       "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", | ||||||
|       "license": "ISC" |       "license": "ISC" | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/lucide-react": { | ||||||
|  |       "version": "0.539.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.539.0.tgz", | ||||||
|  |       "integrity": "sha512-VVISr+VF2krO91FeuCrm1rSOLACQUYVy7NQkzrOty52Y8TlTPcXcMdQFj9bYzBgXbWCiywlwSZ3Z8u6a+6bMlg==", | ||||||
|  |       "license": "ISC", | ||||||
|  |       "peerDependencies": { | ||||||
|  |         "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/magic-string": { |     "node_modules/magic-string": { | ||||||
|       "version": "0.30.17", |       "version": "0.30.17", | ||||||
|       "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", |       "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", | ||||||
| @@ -6215,6 +6324,16 @@ | |||||||
|         "inline-style-parser": "0.2.4" |         "inline-style-parser": "0.2.4" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/tailwind-merge": { | ||||||
|  |       "version": "3.3.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", | ||||||
|  |       "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "funding": { | ||||||
|  |         "type": "github", | ||||||
|  |         "url": "https://github.com/sponsors/dcastil" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/tailwindcss": { |     "node_modules/tailwindcss": { | ||||||
|       "version": "4.1.11", |       "version": "4.1.11", | ||||||
|       "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", |       "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", | ||||||
| @@ -6327,6 +6446,16 @@ | |||||||
|       "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", |       "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", | ||||||
|       "license": "0BSD" |       "license": "0BSD" | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/tw-animate-css": { | ||||||
|  |       "version": "1.3.6", | ||||||
|  |       "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.6.tgz", | ||||||
|  |       "integrity": "sha512-9dy0R9UsYEGmgf26L8UcHiLmSFTHa9+D7+dAt/G/sF5dCnPePZbfgDYinc7/UzAM7g/baVrmS6m9yEpU46d+LA==", | ||||||
|  |       "dev": true, | ||||||
|  |       "license": "MIT", | ||||||
|  |       "funding": { | ||||||
|  |         "url": "https://github.com/sponsors/Wombosvideo" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/type-fest": { |     "node_modules/type-fest": { | ||||||
|       "version": "4.41.0", |       "version": "4.41.0", | ||||||
|       "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", |       "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", | ||||||
|   | |||||||
| @@ -11,14 +11,23 @@ | |||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@astrojs/mdx": "^4.3.3", |     "@astrojs/mdx": "^4.3.3", | ||||||
|     "@astrojs/react": "^4.3.0", |     "@astrojs/react": "^4.3.0", | ||||||
|  |     "@radix-ui/react-label": "^2.1.7", | ||||||
|  |     "@radix-ui/react-slot": "^1.2.3", | ||||||
|     "@tailwindcss/vite": "^4.1.3", |     "@tailwindcss/vite": "^4.1.3", | ||||||
|     "@types/canvas-confetti": "^1.9.0", |     "@types/canvas-confetti": "^1.9.0", | ||||||
|     "@types/react": "^19.1.9", |     "@types/react": "^19.1.9", | ||||||
|     "@types/react-dom": "^19.1.7", |     "@types/react-dom": "^19.1.7", | ||||||
|     "astro": "^5.12.9", |     "astro": "^5.12.9", | ||||||
|     "canvas-confetti": "^1.9.3", |     "canvas-confetti": "^1.9.3", | ||||||
|  |     "class-variance-authority": "^0.7.1", | ||||||
|  |     "clsx": "^2.1.1", | ||||||
|  |     "lucide-react": "^0.539.0", | ||||||
|     "react": "^19.1.1", |     "react": "^19.1.1", | ||||||
|     "react-dom": "^19.1.1", |     "react-dom": "^19.1.1", | ||||||
|  |     "tailwind-merge": "^3.3.1", | ||||||
|     "tailwindcss": "^4.1.3" |     "tailwindcss": "^4.1.3" | ||||||
|  |   }, | ||||||
|  |   "devDependencies": { | ||||||
|  |     "tw-animate-css": "^1.3.6" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										135
									
								
								src/components/LoginClient.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								src/components/LoginClient.jsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,135 @@ | |||||||
|  | import { useState } from "react"; | ||||||
|  | // Import zod for validation | ||||||
|  | import { z } from "zod"; | ||||||
|  | import { | ||||||
|  |   Card, | ||||||
|  |   CardContent, | ||||||
|  |   CardDescription, | ||||||
|  |   CardHeader, | ||||||
|  |   CardTitle, | ||||||
|  | } from "@/components/ui/card"; | ||||||
|  | import { Input } from "@/components/ui/input"; | ||||||
|  | import { Label } from "@/components/ui/label"; | ||||||
|  | import { Button } from "@/components/ui/button"; | ||||||
|  |  | ||||||
|  | // Define the validation schema using zod | ||||||
|  | const loginSchema = z.object({ | ||||||
|  |   email: z.string().email({ message: "Invalid email address." }), | ||||||
|  |   password: z | ||||||
|  |     .string() | ||||||
|  |     .min(8, { message: "Password must be at least 8 characters." }), | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | // A simple toast-like component for messages. | ||||||
|  | const MessageBox = ({ message, type }) => { | ||||||
|  |   if (!message) return null; | ||||||
|  |   const color = type === "success" ? "text-emerald-500" : "text-rose-500"; | ||||||
|  |   return <div className={`mt-4 text-center ${color}`}>{message}</div>; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default function LoginClient() { | ||||||
|  |   const [email, setEmail] = useState(""); | ||||||
|  |   const [password, setPassword] = useState(""); | ||||||
|  |   const [message, setMessage] = useState(""); | ||||||
|  |   const [messageType, setMessageType] = useState(""); | ||||||
|  |   const [loading, setLoading] = useState(false); | ||||||
|  |   // State to hold validation errors | ||||||
|  |   const [errors, setErrors] = useState({}); | ||||||
|  |  | ||||||
|  |   const handleSubmit = async (event) => { | ||||||
|  |     event.preventDefault(); | ||||||
|  |     setMessage(""); | ||||||
|  |     setErrors({}); | ||||||
|  |  | ||||||
|  |     // Validate the form data against the schema | ||||||
|  |     const validationResult = loginSchema.safeParse({ email, password }); | ||||||
|  |  | ||||||
|  |     if (!validationResult.success) { | ||||||
|  |       // If validation fails, format and set the errors | ||||||
|  |       const newErrors = {}; | ||||||
|  |       validationResult.error.errors.forEach((err) => { | ||||||
|  |         newErrors[err.path[0]] = err.message; | ||||||
|  |       }); | ||||||
|  |       setErrors(newErrors); | ||||||
|  |       return; // Stop the function if validation fails | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     setLoading(true); | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       const response = await fetch("http://localhost:3000/api/login", { | ||||||
|  |         method: "POST", | ||||||
|  |         headers: { | ||||||
|  |           "Content-Type": "application/json", | ||||||
|  |         }, | ||||||
|  |         body: JSON.stringify({ email, password }), | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       const data = await response.json(); | ||||||
|  |  | ||||||
|  |       if (response.ok) { | ||||||
|  |         setMessage(data.message); | ||||||
|  |         setMessageType("success"); | ||||||
|  |       } else { | ||||||
|  |         setMessage(data.error); | ||||||
|  |         setMessageType("error"); | ||||||
|  |       } | ||||||
|  |     } catch (error) { | ||||||
|  |       setMessage("An error occurred. Please try again."); | ||||||
|  |       setMessageType("error"); | ||||||
|  |     } finally { | ||||||
|  |       setLoading(false); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Card className="w-[350px]"> | ||||||
|  |       <CardHeader> | ||||||
|  |         <CardTitle className="text-center text-3xl font-extrabold text-blue-500"> | ||||||
|  |           Sign In | ||||||
|  |         </CardTitle> | ||||||
|  |         <CardDescription className="text-center"> | ||||||
|  |           Enter your credentials to access your account. | ||||||
|  |         </CardDescription> | ||||||
|  |       </CardHeader> | ||||||
|  |       <CardContent> | ||||||
|  |         <form onSubmit={handleSubmit}> | ||||||
|  |           <div className="grid w-full items-center gap-4"> | ||||||
|  |             <div className="flex flex-col space-y-1.5"> | ||||||
|  |               <Label htmlFor="email">Email</Label> | ||||||
|  |               <Input | ||||||
|  |                 id="email" | ||||||
|  |                 type="email" | ||||||
|  |                 placeholder="you@example.com" | ||||||
|  |                 value={email} | ||||||
|  |                 onChange={(e) => setEmail(e.target.value)} | ||||||
|  |                 required | ||||||
|  |               /> | ||||||
|  |               {errors.email && ( | ||||||
|  |                 <p className="text-sm text-rose-500">{errors.email}</p> | ||||||
|  |               )} | ||||||
|  |             </div> | ||||||
|  |             <div className="flex flex-col space-y-1.5"> | ||||||
|  |               <Label htmlFor="password">Password</Label> | ||||||
|  |               <Input | ||||||
|  |                 id="password" | ||||||
|  |                 type="password" | ||||||
|  |                 placeholder="********" | ||||||
|  |                 value={password} | ||||||
|  |                 onChange={(e) => setPassword(e.target.value)} | ||||||
|  |                 required | ||||||
|  |               /> | ||||||
|  |               {errors.password && ( | ||||||
|  |                 <p className="text-sm text-rose-500">{errors.password}</p> | ||||||
|  |               )} | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |           <Button type="submit" className="mt-6 w-full" disabled={loading}> | ||||||
|  |             {loading ? "Loading..." : "Login"} | ||||||
|  |           </Button> | ||||||
|  |         </form> | ||||||
|  |         <MessageBox message={message} type={messageType} /> | ||||||
|  |       </CardContent> | ||||||
|  |     </Card> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										59
									
								
								src/components/ui/button.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/components/ui/button.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | |||||||
|  | import * as React from "react" | ||||||
|  | import { Slot } from "@radix-ui/react-slot" | ||||||
|  | import { cva, type VariantProps } from "class-variance-authority" | ||||||
|  |  | ||||||
|  | import { cn } from "@/lib/utils" | ||||||
|  |  | ||||||
|  | const buttonVariants = cva( | ||||||
|  |   "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", | ||||||
|  |   { | ||||||
|  |     variants: { | ||||||
|  |       variant: { | ||||||
|  |         default: | ||||||
|  |           "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", | ||||||
|  |         destructive: | ||||||
|  |           "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", | ||||||
|  |         outline: | ||||||
|  |           "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", | ||||||
|  |         secondary: | ||||||
|  |           "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", | ||||||
|  |         ghost: | ||||||
|  |           "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", | ||||||
|  |         link: "text-primary underline-offset-4 hover:underline", | ||||||
|  |       }, | ||||||
|  |       size: { | ||||||
|  |         default: "h-9 px-4 py-2 has-[>svg]:px-3", | ||||||
|  |         sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", | ||||||
|  |         lg: "h-10 rounded-md px-6 has-[>svg]:px-4", | ||||||
|  |         icon: "size-9", | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     defaultVariants: { | ||||||
|  |       variant: "default", | ||||||
|  |       size: "default", | ||||||
|  |     }, | ||||||
|  |   } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | function Button({ | ||||||
|  |   className, | ||||||
|  |   variant, | ||||||
|  |   size, | ||||||
|  |   asChild = false, | ||||||
|  |   ...props | ||||||
|  | }: React.ComponentProps<"button"> & | ||||||
|  |   VariantProps<typeof buttonVariants> & { | ||||||
|  |     asChild?: boolean | ||||||
|  |   }) { | ||||||
|  |   const Comp = asChild ? Slot : "button" | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Comp | ||||||
|  |       data-slot="button" | ||||||
|  |       className={cn(buttonVariants({ variant, size, className }))} | ||||||
|  |       {...props} | ||||||
|  |     /> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export { Button, buttonVariants } | ||||||
							
								
								
									
										92
									
								
								src/components/ui/card.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/components/ui/card.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | |||||||
|  | import * as React from "react" | ||||||
|  |  | ||||||
|  | import { cn } from "@/lib/utils" | ||||||
|  |  | ||||||
|  | function Card({ className, ...props }: React.ComponentProps<"div">) { | ||||||
|  |   return ( | ||||||
|  |     <div | ||||||
|  |       data-slot="card" | ||||||
|  |       className={cn( | ||||||
|  |         "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", | ||||||
|  |         className | ||||||
|  |       )} | ||||||
|  |       {...props} | ||||||
|  |     /> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function CardHeader({ className, ...props }: React.ComponentProps<"div">) { | ||||||
|  |   return ( | ||||||
|  |     <div | ||||||
|  |       data-slot="card-header" | ||||||
|  |       className={cn( | ||||||
|  |         "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", | ||||||
|  |         className | ||||||
|  |       )} | ||||||
|  |       {...props} | ||||||
|  |     /> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function CardTitle({ className, ...props }: React.ComponentProps<"div">) { | ||||||
|  |   return ( | ||||||
|  |     <div | ||||||
|  |       data-slot="card-title" | ||||||
|  |       className={cn("leading-none font-semibold", className)} | ||||||
|  |       {...props} | ||||||
|  |     /> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function CardDescription({ className, ...props }: React.ComponentProps<"div">) { | ||||||
|  |   return ( | ||||||
|  |     <div | ||||||
|  |       data-slot="card-description" | ||||||
|  |       className={cn("text-muted-foreground text-sm", className)} | ||||||
|  |       {...props} | ||||||
|  |     /> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function CardAction({ className, ...props }: React.ComponentProps<"div">) { | ||||||
|  |   return ( | ||||||
|  |     <div | ||||||
|  |       data-slot="card-action" | ||||||
|  |       className={cn( | ||||||
|  |         "col-start-2 row-span-2 row-start-1 self-start justify-self-end", | ||||||
|  |         className | ||||||
|  |       )} | ||||||
|  |       {...props} | ||||||
|  |     /> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function CardContent({ className, ...props }: React.ComponentProps<"div">) { | ||||||
|  |   return ( | ||||||
|  |     <div | ||||||
|  |       data-slot="card-content" | ||||||
|  |       className={cn("px-6", className)} | ||||||
|  |       {...props} | ||||||
|  |     /> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function CardFooter({ className, ...props }: React.ComponentProps<"div">) { | ||||||
|  |   return ( | ||||||
|  |     <div | ||||||
|  |       data-slot="card-footer" | ||||||
|  |       className={cn("flex items-center px-6 [.border-t]:pt-6", className)} | ||||||
|  |       {...props} | ||||||
|  |     /> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export { | ||||||
|  |   Card, | ||||||
|  |   CardHeader, | ||||||
|  |   CardFooter, | ||||||
|  |   CardTitle, | ||||||
|  |   CardAction, | ||||||
|  |   CardDescription, | ||||||
|  |   CardContent, | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								src/components/ui/input.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/components/ui/input.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | import * as React from "react" | ||||||
|  |  | ||||||
|  | import { cn } from "@/lib/utils" | ||||||
|  |  | ||||||
|  | function Input({ className, type, ...props }: React.ComponentProps<"input">) { | ||||||
|  |   return ( | ||||||
|  |     <input | ||||||
|  |       type={type} | ||||||
|  |       data-slot="input" | ||||||
|  |       className={cn( | ||||||
|  |         "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", | ||||||
|  |         "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", | ||||||
|  |         "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", | ||||||
|  |         className | ||||||
|  |       )} | ||||||
|  |       {...props} | ||||||
|  |     /> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export { Input } | ||||||
							
								
								
									
										22
									
								
								src/components/ui/label.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/components/ui/label.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | import * as React from "react" | ||||||
|  | import * as LabelPrimitive from "@radix-ui/react-label" | ||||||
|  |  | ||||||
|  | import { cn } from "@/lib/utils" | ||||||
|  |  | ||||||
|  | function Label({ | ||||||
|  |   className, | ||||||
|  |   ...props | ||||||
|  | }: React.ComponentProps<typeof LabelPrimitive.Root>) { | ||||||
|  |   return ( | ||||||
|  |     <LabelPrimitive.Root | ||||||
|  |       data-slot="label" | ||||||
|  |       className={cn( | ||||||
|  |         "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", | ||||||
|  |         className | ||||||
|  |       )} | ||||||
|  |       {...props} | ||||||
|  |     /> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export { Label } | ||||||
							
								
								
									
										29
									
								
								src/layouts/BaseLayout.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/layouts/BaseLayout.astro
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | --- | ||||||
|  | import '@/styles/global.css'; | ||||||
|  | // import Favicons from "~/components/Favicons.astro"; | ||||||
|  |  | ||||||
|  | const { metadata = {} } = Astro.props; | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | <!doctype html> | ||||||
|  | <html lang="en"> | ||||||
|  |   <head> | ||||||
|  |     <meta charset="UTF-8" /> | ||||||
|  |     <meta name="viewport" content="width=device-width" /> | ||||||
|  |     <!-- <Favicons /> --> | ||||||
|  |     <meta name="generator" content={Astro.generator} /> | ||||||
|  |     <title>{metadata.title}</title> | ||||||
|  |   </head> | ||||||
|  |   <body> | ||||||
|  |     <slot /> | ||||||
|  |   </body> | ||||||
|  | </html> | ||||||
|  |  | ||||||
|  | <style> | ||||||
|  |   html, | ||||||
|  |   body { | ||||||
|  |     margin: 0; | ||||||
|  |     width: 100%; | ||||||
|  |     height: 100%; | ||||||
|  |   } | ||||||
|  | </style> | ||||||
							
								
								
									
										17
									
								
								src/layouts/PageLayout.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/layouts/PageLayout.astro
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | --- | ||||||
|  | import BaseLayout from '@/layouts/BaseLayout.astro'; | ||||||
|  |  | ||||||
|  | const { metadata } = Astro.props; | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | <BaseLayout metadata={metadata}> | ||||||
|  |   <slot name="header"> | ||||||
|  |     <!-- <Header /> --> | ||||||
|  |   </slot> | ||||||
|  |   <main> | ||||||
|  |     <slot /> | ||||||
|  |   </main> | ||||||
|  |   <slot name="footer"> | ||||||
|  |     <!-- <Footer /> --> | ||||||
|  |   </slot> | ||||||
|  | </BaseLayout> | ||||||
							
								
								
									
										6
									
								
								src/lib/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/lib/utils.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | import { clsx, type ClassValue } from "clsx" | ||||||
|  | import { twMerge } from "tailwind-merge" | ||||||
|  |  | ||||||
|  | export function cn(...inputs: ClassValue[]) { | ||||||
|  |   return twMerge(clsx(inputs)) | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								src/pages/login.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/pages/login.astro
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | --- | ||||||
|  | import Layout from '@/layouts/PageLayout.astro'; | ||||||
|  | import LoginClient from '@/components/LoginClient'; | ||||||
|  |  | ||||||
|  | // The 'client:only' directive ensures the React component only runs in the browser. | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | <Layout title="Login Page"> | ||||||
|  | 	<main class="flex min-h-screen items-center justify-center p-4"> | ||||||
|  | 		<LoginClient client:only="react" /> | ||||||
|  | 	</main> | ||||||
|  | </Layout> | ||||||
| @@ -1 +1,120 @@ | |||||||
| @import "tailwindcss"; | @import "tailwindcss"; | ||||||
|  | @import "tw-animate-css"; | ||||||
|  |  | ||||||
|  | @custom-variant dark (&:is(.dark *)); | ||||||
|  |  | ||||||
|  | @theme inline { | ||||||
|  |   --radius-sm: calc(var(--radius) - 4px); | ||||||
|  |   --radius-md: calc(var(--radius) - 2px); | ||||||
|  |   --radius-lg: var(--radius); | ||||||
|  |   --radius-xl: calc(var(--radius) + 4px); | ||||||
|  |   --color-background: var(--background); | ||||||
|  |   --color-foreground: var(--foreground); | ||||||
|  |   --color-card: var(--card); | ||||||
|  |   --color-card-foreground: var(--card-foreground); | ||||||
|  |   --color-popover: var(--popover); | ||||||
|  |   --color-popover-foreground: var(--popover-foreground); | ||||||
|  |   --color-primary: var(--primary); | ||||||
|  |   --color-primary-foreground: var(--primary-foreground); | ||||||
|  |   --color-secondary: var(--secondary); | ||||||
|  |   --color-secondary-foreground: var(--secondary-foreground); | ||||||
|  |   --color-muted: var(--muted); | ||||||
|  |   --color-muted-foreground: var(--muted-foreground); | ||||||
|  |   --color-accent: var(--accent); | ||||||
|  |   --color-accent-foreground: var(--accent-foreground); | ||||||
|  |   --color-destructive: var(--destructive); | ||||||
|  |   --color-border: var(--border); | ||||||
|  |   --color-input: var(--input); | ||||||
|  |   --color-ring: var(--ring); | ||||||
|  |   --color-chart-1: var(--chart-1); | ||||||
|  |   --color-chart-2: var(--chart-2); | ||||||
|  |   --color-chart-3: var(--chart-3); | ||||||
|  |   --color-chart-4: var(--chart-4); | ||||||
|  |   --color-chart-5: var(--chart-5); | ||||||
|  |   --color-sidebar: var(--sidebar); | ||||||
|  |   --color-sidebar-foreground: var(--sidebar-foreground); | ||||||
|  |   --color-sidebar-primary: var(--sidebar-primary); | ||||||
|  |   --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); | ||||||
|  |   --color-sidebar-accent: var(--sidebar-accent); | ||||||
|  |   --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); | ||||||
|  |   --color-sidebar-border: var(--sidebar-border); | ||||||
|  |   --color-sidebar-ring: var(--sidebar-ring); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | :root { | ||||||
|  |   --radius: 0.625rem; | ||||||
|  |   --background: oklch(1 0 0); | ||||||
|  |   --foreground: oklch(0.145 0 0); | ||||||
|  |   --card: oklch(1 0 0); | ||||||
|  |   --card-foreground: oklch(0.145 0 0); | ||||||
|  |   --popover: oklch(1 0 0); | ||||||
|  |   --popover-foreground: oklch(0.145 0 0); | ||||||
|  |   --primary: oklch(0.205 0 0); | ||||||
|  |   --primary-foreground: oklch(0.985 0 0); | ||||||
|  |   --secondary: oklch(0.97 0 0); | ||||||
|  |   --secondary-foreground: oklch(0.205 0 0); | ||||||
|  |   --muted: oklch(0.97 0 0); | ||||||
|  |   --muted-foreground: oklch(0.556 0 0); | ||||||
|  |   --accent: oklch(0.97 0 0); | ||||||
|  |   --accent-foreground: oklch(0.205 0 0); | ||||||
|  |   --destructive: oklch(0.577 0.245 27.325); | ||||||
|  |   --border: oklch(0.922 0 0); | ||||||
|  |   --input: oklch(0.922 0 0); | ||||||
|  |   --ring: oklch(0.708 0 0); | ||||||
|  |   --chart-1: oklch(0.646 0.222 41.116); | ||||||
|  |   --chart-2: oklch(0.6 0.118 184.704); | ||||||
|  |   --chart-3: oklch(0.398 0.07 227.392); | ||||||
|  |   --chart-4: oklch(0.828 0.189 84.429); | ||||||
|  |   --chart-5: oklch(0.769 0.188 70.08); | ||||||
|  |   --sidebar: oklch(0.985 0 0); | ||||||
|  |   --sidebar-foreground: oklch(0.145 0 0); | ||||||
|  |   --sidebar-primary: oklch(0.205 0 0); | ||||||
|  |   --sidebar-primary-foreground: oklch(0.985 0 0); | ||||||
|  |   --sidebar-accent: oklch(0.97 0 0); | ||||||
|  |   --sidebar-accent-foreground: oklch(0.205 0 0); | ||||||
|  |   --sidebar-border: oklch(0.922 0 0); | ||||||
|  |   --sidebar-ring: oklch(0.708 0 0); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .dark { | ||||||
|  |   --background: oklch(0.145 0 0); | ||||||
|  |   --foreground: oklch(0.985 0 0); | ||||||
|  |   --card: oklch(0.205 0 0); | ||||||
|  |   --card-foreground: oklch(0.985 0 0); | ||||||
|  |   --popover: oklch(0.205 0 0); | ||||||
|  |   --popover-foreground: oklch(0.985 0 0); | ||||||
|  |   --primary: oklch(0.922 0 0); | ||||||
|  |   --primary-foreground: oklch(0.205 0 0); | ||||||
|  |   --secondary: oklch(0.269 0 0); | ||||||
|  |   --secondary-foreground: oklch(0.985 0 0); | ||||||
|  |   --muted: oklch(0.269 0 0); | ||||||
|  |   --muted-foreground: oklch(0.708 0 0); | ||||||
|  |   --accent: oklch(0.269 0 0); | ||||||
|  |   --accent-foreground: oklch(0.985 0 0); | ||||||
|  |   --destructive: oklch(0.704 0.191 22.216); | ||||||
|  |   --border: oklch(1 0 0 / 10%); | ||||||
|  |   --input: oklch(1 0 0 / 15%); | ||||||
|  |   --ring: oklch(0.556 0 0); | ||||||
|  |   --chart-1: oklch(0.488 0.243 264.376); | ||||||
|  |   --chart-2: oklch(0.696 0.17 162.48); | ||||||
|  |   --chart-3: oklch(0.769 0.188 70.08); | ||||||
|  |   --chart-4: oklch(0.627 0.265 303.9); | ||||||
|  |   --chart-5: oklch(0.645 0.246 16.439); | ||||||
|  |   --sidebar: oklch(0.205 0 0); | ||||||
|  |   --sidebar-foreground: oklch(0.985 0 0); | ||||||
|  |   --sidebar-primary: oklch(0.488 0.243 264.376); | ||||||
|  |   --sidebar-primary-foreground: oklch(0.985 0 0); | ||||||
|  |   --sidebar-accent: oklch(0.269 0 0); | ||||||
|  |   --sidebar-accent-foreground: oklch(0.985 0 0); | ||||||
|  |   --sidebar-border: oklch(1 0 0 / 10%); | ||||||
|  |   --sidebar-ring: oklch(0.556 0 0); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @layer base { | ||||||
|  |   * { | ||||||
|  |     @apply border-border outline-ring/50; | ||||||
|  |   } | ||||||
|  |   body { | ||||||
|  |     @apply bg-background text-foreground; | ||||||
|  |   } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,14 +1,14 @@ | |||||||
| { | { | ||||||
|   "extends": "astro/tsconfigs/strict", |   "extends": "astro/tsconfigs/strict", | ||||||
|   "include": [ |   "include": [".astro/types.d.ts", "**/*"], | ||||||
|     ".astro/types.d.ts", |   "exclude": ["dist"], | ||||||
|     "**/*" |  | ||||||
|   ], |  | ||||||
|   "exclude": [ |  | ||||||
|     "dist" |  | ||||||
|   ], |  | ||||||
|   "compilerOptions": { |   "compilerOptions": { | ||||||
|     "jsx": "react-jsx", |     "jsx": "react-jsx", | ||||||
|     "jsxImportSource": "react" |     "jsxImportSource": "react", | ||||||
|  |     "baseUrl": ".", | ||||||
|  |     "paths": { | ||||||
|  |       "@/*": ["./src/*"] | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Chang CL
					Chang CL