From e8c45530143702163ebe31d23005ec92a12cf00f Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 23:08:14 +0000 Subject: [PATCH] feat: implement authentication pages with GSAP animations Add all auth pages under apps/www/src/app/auth/ following the design system and GSAP patterns from page.tsx: - Auth layout with animated background orbs and header/footer - Login page with form validation, loading/success/error states - Signup page with password strength meter and post-submit verification prompt - Forgot Password page with email submission and success feedback - Reset Password page with live requirements checklist - Email Verification page with resend cooldown timer - Shared AuthInput component (label, error, icon, password toggle) - Shared AuthCard component (styled container with decorative elements) All pages use useGSAP for entrance animations and state transition effects. Backend stubs are in place for future integration. Co-authored-by: Collins Ikechukwu <0xdevcollins@users.noreply.github.com> --- .../www/src/app/auth/forgot-password/page.tsx | 201 ++++++++++++ apps/www/src/app/auth/layout.tsx | 102 ++++++ apps/www/src/app/auth/login/page.tsx | 222 +++++++++++++ apps/www/src/app/auth/reset-password/page.tsx | 247 +++++++++++++++ apps/www/src/app/auth/signup/page.tsx | 294 ++++++++++++++++++ apps/www/src/app/auth/verify-email/page.tsx | 228 ++++++++++++++ apps/www/src/components/auth/AuthCard.tsx | 26 ++ apps/www/src/components/auth/AuthInput.tsx | 74 +++++ 8 files changed, 1394 insertions(+) create mode 100644 apps/www/src/app/auth/forgot-password/page.tsx create mode 100644 apps/www/src/app/auth/layout.tsx create mode 100644 apps/www/src/app/auth/login/page.tsx create mode 100644 apps/www/src/app/auth/reset-password/page.tsx create mode 100644 apps/www/src/app/auth/signup/page.tsx create mode 100644 apps/www/src/app/auth/verify-email/page.tsx create mode 100644 apps/www/src/components/auth/AuthCard.tsx create mode 100644 apps/www/src/components/auth/AuthInput.tsx diff --git a/apps/www/src/app/auth/forgot-password/page.tsx b/apps/www/src/app/auth/forgot-password/page.tsx new file mode 100644 index 0000000..c0171f7 --- /dev/null +++ b/apps/www/src/app/auth/forgot-password/page.tsx @@ -0,0 +1,201 @@ +"use client"; + +import { useRef, useState } from "react"; +import Link from "next/link"; +import gsap from "gsap"; +import { useGSAP } from "@gsap/react"; +import { Mail, ArrowRight, ArrowLeft, KeyRound } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { AuthCard } from "@/components/auth/AuthCard"; +import { AuthInput } from "@/components/auth/AuthInput"; + +gsap.registerPlugin(useGSAP); + +// Stub: replace with real auth logic +async function requestPasswordReset(_email: string): Promise { + await new Promise((resolve) => setTimeout(resolve, 1200)); +} + +export default function ForgotPasswordPage() { + const cardRef = useRef(null); + const [email, setEmail] = useState(""); + const [emailError, setEmailError] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [isSuccess, setIsSuccess] = useState(false); + const [serverError, setServerError] = useState(""); + + useGSAP( + () => { + const tl = gsap.timeline(); + tl.fromTo( + cardRef.current, + { opacity: 0, y: 32, scale: 0.97 }, + { opacity: 1, y: 0, scale: 1, duration: 0.8, ease: "expo.out" } + ); + tl.from( + ".auth-item", + { + opacity: 0, + y: 12, + stagger: 0.07, + duration: 0.5, + ease: "power2.out", + }, + "-=0.4" + ); + }, + { scope: cardRef } + ); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setServerError(""); + if (!email) { + setEmailError("Email is required"); + return; + } + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { + setEmailError("Enter a valid email address"); + gsap.fromTo( + ".auth-field-error", + { opacity: 0, x: -6 }, + { opacity: 1, x: 0, duration: 0.3, ease: "power2.out" } + ); + return; + } + setEmailError(""); + setIsLoading(true); + try { + await requestPasswordReset(email); + setIsSuccess(true); + gsap.fromTo( + ".auth-success", + { opacity: 0, scale: 0.9 }, + { opacity: 1, scale: 1, duration: 0.5, ease: "back.out(1.7)" } + ); + } catch { + setServerError("Something went wrong. Please try again."); + gsap.fromTo( + ".auth-server-error", + { opacity: 0, y: -8 }, + { opacity: 1, y: 0, duration: 0.4, ease: "power2.out" } + ); + } finally { + setIsLoading(false); + } + } + + return ( +
+ + {/* Back link */} + + + Back to sign in + + + {/* Header */} +
+
+ +
+
+

+ Forgot password? +

+

+ We'll send a reset link +

+
+
+ + {serverError && ( +
+

{serverError}

+
+ )} + + {isSuccess ? ( +
+
+ +
+

+ Check your inbox +

+

+ If an account exists for{" "} + {email}, you'll receive + a password reset link shortly. +

+
+ + + Return to sign in + +
+
+ ) : ( +
+
+

+ Enter the email address associated with your account and + we'll send you a link to reset your password. +

+ } + value={email} + onChange={(e) => setEmail(e.target.value)} + error={emailError} + autoComplete="email" + aria-label="Email address" + /> +
+ +
+ +
+
+ )} +
+
+ ); +} diff --git a/apps/www/src/app/auth/layout.tsx b/apps/www/src/app/auth/layout.tsx new file mode 100644 index 0000000..023b7bf --- /dev/null +++ b/apps/www/src/app/auth/layout.tsx @@ -0,0 +1,102 @@ +"use client"; + +import Link from "next/link"; +import Image from "next/image"; +import { useRef } from "react"; +import gsap from "gsap"; +import { useGSAP } from "@gsap/react"; + +gsap.registerPlugin(useGSAP); + +export default function AuthLayout({ + children, +}: { + children: React.ReactNode; +}) { + const containerRef = useRef(null); + + useGSAP( + () => { + gsap.fromTo( + ".auth-bg-orb", + { opacity: 0, scale: 0.8 }, + { opacity: 1, scale: 1, duration: 2, ease: "power2.out", stagger: 0.3 } + ); + }, + { scope: containerRef } + ); + + return ( +
+ {/* Background decorative orbs */} +
+
+
+ + {/* Grid pattern overlay */} +
+ + {/* Header */} +
+ + Useroutr + + +
+ + {/* Main content */} +
+ {children} +
+ + {/* Footer */} +
+ + Privacy + +
+ + Terms + +
+ + © 2025 Useroutr Labs + +
+
+ ); +} diff --git a/apps/www/src/app/auth/login/page.tsx b/apps/www/src/app/auth/login/page.tsx new file mode 100644 index 0000000..e9bb09e --- /dev/null +++ b/apps/www/src/app/auth/login/page.tsx @@ -0,0 +1,222 @@ +"use client"; + +import { useRef, useState } from "react"; +import Link from "next/link"; +import gsap from "gsap"; +import { useGSAP } from "@gsap/react"; +import { Mail, Lock, ArrowRight, ShieldCheck } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { AuthCard } from "@/components/auth/AuthCard"; +import { AuthInput } from "@/components/auth/AuthInput"; + +gsap.registerPlugin(useGSAP); + +// Stub: replace with real auth logic +async function loginUser(_email: string, _password: string): Promise { + await new Promise((resolve) => setTimeout(resolve, 1200)); +} + +export default function LoginPage() { + const cardRef = useRef(null); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [errors, setErrors] = useState<{ email?: string; password?: string }>({}); + const [isLoading, setIsLoading] = useState(false); + const [isSuccess, setIsSuccess] = useState(false); + const [serverError, setServerError] = useState(""); + + useGSAP( + () => { + const tl = gsap.timeline(); + tl.fromTo( + cardRef.current, + { opacity: 0, y: 32, scale: 0.97 }, + { opacity: 1, y: 0, scale: 1, duration: 0.8, ease: "expo.out" } + ); + tl.from( + ".auth-item", + { + opacity: 0, + y: 12, + stagger: 0.07, + duration: 0.5, + ease: "power2.out", + }, + "-=0.4" + ); + }, + { scope: cardRef } + ); + + function validate() { + const errs: typeof errors = {}; + if (!email) errs.email = "Email is required"; + else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) + errs.email = "Enter a valid email address"; + if (!password) errs.password = "Password is required"; + else if (password.length < 8) errs.password = "Password must be at least 8 characters"; + return errs; + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setServerError(""); + const errs = validate(); + if (Object.keys(errs).length) { + setErrors(errs); + gsap.fromTo( + ".auth-field-error", + { opacity: 0, x: -6 }, + { opacity: 1, x: 0, duration: 0.3, stagger: 0.05, ease: "power2.out" } + ); + return; + } + setErrors({}); + setIsLoading(true); + try { + await loginUser(email, password); + setIsSuccess(true); + gsap.fromTo( + ".auth-success", + { opacity: 0, scale: 0.9 }, + { opacity: 1, scale: 1, duration: 0.5, ease: "back.out(1.7)" } + ); + } catch { + setServerError("Invalid email or password. Please try again."); + gsap.fromTo( + ".auth-server-error", + { opacity: 0, y: -8 }, + { opacity: 1, y: 0, duration: 0.4, ease: "power2.out" } + ); + } finally { + setIsLoading(false); + } + } + + return ( +
+ + {/* Header */} +
+
+ +
+
+

+ Welcome back +

+

+ Sign in to your account +

+
+
+ + {/* Server error */} + {serverError && ( +
+

{serverError}

+
+ )} + + {/* Success state */} + {isSuccess ? ( +
+
+ +
+

+ Signed in! +

+

+ Redirecting you to your dashboard… +

+
+ ) : ( +
+
+ } + value={email} + onChange={(e) => setEmail(e.target.value)} + error={errors.email} + autoComplete="email" + aria-label="Email address" + /> +
+
+ } + value={password} + onChange={(e) => setPassword(e.target.value)} + error={errors.password} + autoComplete="current-password" + aria-label="Password" + /> +
+ + Forgot password? + +
+
+ +
+ +
+
+ )} + + {/* Divider */} + {!isSuccess && ( + <> +
+
+ + or + +
+
+ +

+ Don't have an account?{" "} + + Create one + +

+ + )} + +
+ ); +} diff --git a/apps/www/src/app/auth/reset-password/page.tsx b/apps/www/src/app/auth/reset-password/page.tsx new file mode 100644 index 0000000..6141c7b --- /dev/null +++ b/apps/www/src/app/auth/reset-password/page.tsx @@ -0,0 +1,247 @@ +"use client"; + +import { useRef, useState } from "react"; +import Link from "next/link"; +import gsap from "gsap"; +import { useGSAP } from "@gsap/react"; +import { Lock, ArrowRight, ShieldCheck, CheckCircle } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { AuthCard } from "@/components/auth/AuthCard"; +import { AuthInput } from "@/components/auth/AuthInput"; + +gsap.registerPlugin(useGSAP); + +// Stub: replace with real auth logic (token would come from URL params) +async function resetPassword(_password: string, _token: string): Promise { + await new Promise((resolve) => setTimeout(resolve, 1200)); +} + +const requirements = [ + { label: "At least 8 characters", test: (p: string) => p.length >= 8 }, + { label: "One uppercase letter", test: (p: string) => /[A-Z]/.test(p) }, + { label: "One number", test: (p: string) => /[0-9]/.test(p) }, + { label: "One special character", test: (p: string) => /[^A-Za-z0-9]/.test(p) }, +]; + +export default function ResetPasswordPage() { + const cardRef = useRef(null); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [errors, setErrors] = useState<{ + password?: string; + confirmPassword?: string; + }>({}); + const [isLoading, setIsLoading] = useState(false); + const [isSuccess, setIsSuccess] = useState(false); + const [serverError, setServerError] = useState(""); + + // Stub: in production, extract token from URL search params + const token = "stub-reset-token"; + + useGSAP( + () => { + const tl = gsap.timeline(); + tl.fromTo( + cardRef.current, + { opacity: 0, y: 32, scale: 0.97 }, + { opacity: 1, y: 0, scale: 1, duration: 0.8, ease: "expo.out" } + ); + tl.from( + ".auth-item", + { + opacity: 0, + y: 12, + stagger: 0.07, + duration: 0.5, + ease: "power2.out", + }, + "-=0.4" + ); + }, + { scope: cardRef } + ); + + function validate() { + const errs: typeof errors = {}; + if (!password) errs.password = "Password is required"; + else if (password.length < 8) + errs.password = "Password must be at least 8 characters"; + if (!confirmPassword) errs.confirmPassword = "Please confirm your password"; + else if (confirmPassword !== password) + errs.confirmPassword = "Passwords do not match"; + return errs; + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setServerError(""); + const errs = validate(); + if (Object.keys(errs).length) { + setErrors(errs); + gsap.fromTo( + ".auth-field-error", + { opacity: 0, x: -6 }, + { opacity: 1, x: 0, duration: 0.3, stagger: 0.05, ease: "power2.out" } + ); + return; + } + setErrors({}); + setIsLoading(true); + try { + await resetPassword(password, token); + setIsSuccess(true); + gsap.fromTo( + ".auth-success", + { opacity: 0, scale: 0.9 }, + { opacity: 1, scale: 1, duration: 0.5, ease: "back.out(1.7)" } + ); + } catch { + setServerError("Reset link is invalid or expired. Please request a new one."); + gsap.fromTo( + ".auth-server-error", + { opacity: 0, y: -8 }, + { opacity: 1, y: 0, duration: 0.4, ease: "power2.out" } + ); + } finally { + setIsLoading(false); + } + } + + return ( +
+ + {/* Header */} +
+
+ +
+
+

+ Reset password +

+

+ Choose a new password +

+
+
+ + {serverError && ( +
+

{serverError}

+ + Request new link → + +
+ )} + + {isSuccess ? ( +
+
+ +
+

+ Password updated! +

+

+ Your password has been successfully reset. +

+ + + +
+ ) : ( +
+
+ } + value={password} + onChange={(e) => setPassword(e.target.value)} + error={errors.password} + autoComplete="new-password" + aria-label="New password" + /> +
+ + {/* Password requirements checklist */} + {password && ( +
+ {requirements.map((req) => { + const met = req.test(password); + return ( +
+ + + {req.label} + +
+ ); + })} +
+ )} + +
+ } + value={confirmPassword} + onChange={(e) => setConfirmPassword(e.target.value)} + error={errors.confirmPassword} + autoComplete="new-password" + aria-label="Confirm new password" + /> +
+ +
+ +
+
+ )} +
+
+ ); +} diff --git a/apps/www/src/app/auth/signup/page.tsx b/apps/www/src/app/auth/signup/page.tsx new file mode 100644 index 0000000..637ca16 --- /dev/null +++ b/apps/www/src/app/auth/signup/page.tsx @@ -0,0 +1,294 @@ +"use client"; + +import { useRef, useState } from "react"; +import Link from "next/link"; +import gsap from "gsap"; +import { useGSAP } from "@gsap/react"; +import { Mail, Lock, User, ArrowRight, UserPlus } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { AuthCard } from "@/components/auth/AuthCard"; +import { AuthInput } from "@/components/auth/AuthInput"; + +gsap.registerPlugin(useGSAP); + +// Stub: replace with real auth logic +async function registerUser( + _name: string, + _email: string, + _password: string +): Promise { + await new Promise((resolve) => setTimeout(resolve, 1400)); +} + +export default function SignupPage() { + const cardRef = useRef(null); + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [errors, setErrors] = useState<{ + name?: string; + email?: string; + password?: string; + confirmPassword?: string; + }>({}); + const [isLoading, setIsLoading] = useState(false); + const [isSuccess, setIsSuccess] = useState(false); + const [serverError, setServerError] = useState(""); + + useGSAP( + () => { + const tl = gsap.timeline(); + tl.fromTo( + cardRef.current, + { opacity: 0, y: 32, scale: 0.97 }, + { opacity: 1, y: 0, scale: 1, duration: 0.8, ease: "expo.out" } + ); + tl.from( + ".auth-item", + { + opacity: 0, + y: 12, + stagger: 0.07, + duration: 0.5, + ease: "power2.out", + }, + "-=0.4" + ); + }, + { scope: cardRef } + ); + + function getPasswordStrength(pw: string) { + if (!pw) return null; + let score = 0; + if (pw.length >= 8) score++; + if (/[A-Z]/.test(pw)) score++; + if (/[0-9]/.test(pw)) score++; + if (/[^A-Za-z0-9]/.test(pw)) score++; + if (score <= 1) return { label: "Weak", color: "bg-red", width: "w-1/4" }; + if (score === 2) return { label: "Fair", color: "bg-amber", width: "w-2/4" }; + if (score === 3) return { label: "Good", color: "bg-teal", width: "w-3/4" }; + return { label: "Strong", color: "bg-green", width: "w-full" }; + } + + function validate() { + const errs: typeof errors = {}; + if (!name.trim()) errs.name = "Name is required"; + if (!email) errs.email = "Email is required"; + else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) + errs.email = "Enter a valid email address"; + if (!password) errs.password = "Password is required"; + else if (password.length < 8) + errs.password = "Password must be at least 8 characters"; + if (!confirmPassword) errs.confirmPassword = "Please confirm your password"; + else if (confirmPassword !== password) + errs.confirmPassword = "Passwords do not match"; + return errs; + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setServerError(""); + const errs = validate(); + if (Object.keys(errs).length) { + setErrors(errs); + gsap.fromTo( + ".auth-field-error", + { opacity: 0, x: -6 }, + { opacity: 1, x: 0, duration: 0.3, stagger: 0.05, ease: "power2.out" } + ); + return; + } + setErrors({}); + setIsLoading(true); + try { + await registerUser(name, email, password); + setIsSuccess(true); + gsap.fromTo( + ".auth-success", + { opacity: 0, scale: 0.9 }, + { opacity: 1, scale: 1, duration: 0.5, ease: "back.out(1.7)" } + ); + } catch { + setServerError("Registration failed. Please try again."); + gsap.fromTo( + ".auth-server-error", + { opacity: 0, y: -8 }, + { opacity: 1, y: 0, duration: 0.4, ease: "power2.out" } + ); + } finally { + setIsLoading(false); + } + } + + const strength = getPasswordStrength(password); + + return ( +
+ + {/* Header */} +
+
+ +
+
+

+ Create account +

+

+ Get started for free +

+
+
+ + {serverError && ( +
+

{serverError}

+
+ )} + + {isSuccess ? ( +
+
+ +
+

+ Check your inbox +

+

+ We sent a verification link to{" "} + {email}. Click it to + activate your account. +

+ + Continue to verification → + +
+ ) : ( +
+
+ } + value={name} + onChange={(e) => setName(e.target.value)} + error={errors.name} + autoComplete="name" + aria-label="Full name" + /> +
+
+ } + value={email} + onChange={(e) => setEmail(e.target.value)} + error={errors.email} + autoComplete="email" + aria-label="Email address" + /> +
+
+ } + value={password} + onChange={(e) => setPassword(e.target.value)} + error={errors.password} + autoComplete="new-password" + aria-label="Password" + /> + {strength && !errors.password && ( +
+
+
+
+

+ Strength:{" "} + {strength.label} +

+
+ )} +
+
+ } + value={confirmPassword} + onChange={(e) => setConfirmPassword(e.target.value)} + error={errors.confirmPassword} + autoComplete="new-password" + aria-label="Confirm password" + /> +
+ +
+ +

+ By signing up, you agree to our{" "} + + Terms of Service + +

+
+ + )} + + {!isSuccess && ( + <> +
+
+ + or + +
+
+

+ Already have an account?{" "} + + Sign in + +

+ + )} + +
+ ); +} diff --git a/apps/www/src/app/auth/verify-email/page.tsx b/apps/www/src/app/auth/verify-email/page.tsx new file mode 100644 index 0000000..0934f0a --- /dev/null +++ b/apps/www/src/app/auth/verify-email/page.tsx @@ -0,0 +1,228 @@ +"use client"; + +import { useRef, useState, useEffect } from "react"; +import Link from "next/link"; +import gsap from "gsap"; +import { useGSAP } from "@gsap/react"; +import { Mail, ArrowRight, RefreshCw, CheckCircle } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { AuthCard } from "@/components/auth/AuthCard"; + +gsap.registerPlugin(useGSAP); + +// Stub: replace with real auth logic +async function resendVerificationEmail(_email: string): Promise { + await new Promise((resolve) => setTimeout(resolve, 1000)); +} + +const RESEND_COOLDOWN = 60; + +export default function VerifyEmailPage() { + const cardRef = useRef(null); + const [isResending, setIsResending] = useState(false); + const [resendSuccess, setResendSuccess] = useState(false); + const [cooldown, setCooldown] = useState(0); + const [isVerified, setIsVerified] = useState(false); + + // Stub: in production, get email from session/query params + const email = "you@company.com"; + + useGSAP( + () => { + const tl = gsap.timeline(); + tl.fromTo( + cardRef.current, + { opacity: 0, y: 32, scale: 0.97 }, + { opacity: 1, y: 0, scale: 1, duration: 0.8, ease: "expo.out" } + ); + tl.from( + ".auth-item", + { + opacity: 0, + y: 12, + stagger: 0.07, + duration: 0.5, + ease: "power2.out", + }, + "-=0.4" + ); + + // Animate the mail icon + gsap.to(".mail-icon", { + y: -6, + duration: 2, + ease: "sine.inOut", + repeat: -1, + yoyo: true, + }); + }, + { scope: cardRef } + ); + + useEffect(() => { + if (cooldown <= 0) return; + const timer = setInterval(() => { + setCooldown((prev) => { + if (prev <= 1) { + clearInterval(timer); + return 0; + } + return prev - 1; + }); + }, 1000); + return () => clearInterval(timer); + }, [cooldown]); + + async function handleResend() { + if (cooldown > 0 || isResending) return; + setIsResending(true); + setResendSuccess(false); + try { + await resendVerificationEmail(email); + setResendSuccess(true); + setCooldown(RESEND_COOLDOWN); + gsap.fromTo( + ".resend-success", + { opacity: 0, y: -6 }, + { opacity: 1, y: 0, duration: 0.4, ease: "power2.out" } + ); + } finally { + setIsResending(false); + } + } + + // Simulate verification success (stub) + function handleVerifyDemo() { + setIsVerified(true); + gsap.fromTo( + ".auth-success", + { opacity: 0, scale: 0.9 }, + { opacity: 1, scale: 1, duration: 0.5, ease: "back.out(1.7)" } + ); + } + + return ( +
+ + {isVerified ? ( +
+
+ +
+

+ Email verified! +

+

+ Your account is now active. +

+ + + +
+ ) : ( + <> + {/* Mail icon */} +
+
+ +
+
+ + {/* Header */} +
+

+ Verify your email +

+

+ We've sent a verification link to{" "} + {email}. + Click the link in the email to activate your account. +

+
+ + {/* Resend success */} + {resendSuccess && ( +
+

+ Verification email sent! +

+
+ )} + + {/* Actions */} +
+ + + {/* Demo button to simulate verification */} + +
+ + {/* Help text */} +
+

+ Can't find the email? Check your spam folder or{" "} + + . +

+

+ Wrong email?{" "} + + Go back to sign up + +

+
+ + )} +
+
+ ); +} diff --git a/apps/www/src/components/auth/AuthCard.tsx b/apps/www/src/components/auth/AuthCard.tsx new file mode 100644 index 0000000..d91ae8b --- /dev/null +++ b/apps/www/src/components/auth/AuthCard.tsx @@ -0,0 +1,26 @@ +"use client"; + +import * as React from "react"; +import { cn } from "@/lib/utils"; + +interface AuthCardProps { + children: React.ReactNode; + className?: string; +} + +export function AuthCard({ children, className }: AuthCardProps) { + return ( +
+ {/* Top shine line */} +
+ {/* Subtle inner gradient */} +
+ {children} +
+ ); +} diff --git a/apps/www/src/components/auth/AuthInput.tsx b/apps/www/src/components/auth/AuthInput.tsx new file mode 100644 index 0000000..e6750e2 --- /dev/null +++ b/apps/www/src/components/auth/AuthInput.tsx @@ -0,0 +1,74 @@ +"use client"; + +import * as React from "react"; +import { cn } from "@/lib/utils"; +import { Eye, EyeOff } from "lucide-react"; + +interface AuthInputProps extends React.InputHTMLAttributes { + label?: string; + error?: string; + icon?: React.ReactNode; + hint?: string; +} + +export const AuthInput = React.forwardRef( + ({ label, error, icon, hint, className, type, ...props }, ref) => { + const [showPassword, setShowPassword] = React.useState(false); + const isPassword = type === "password"; + const inputType = isPassword ? (showPassword ? "text" : "password") : type; + + return ( +
+ {label && ( + + )} +
+ {icon && ( +
+ {icon} +
+ )} + + {isPassword && ( + + )} +
+ {error && ( +

+ + {error} +

+ )} + {hint && !error && ( +

+ {hint} +

+ )} +
+ ); + } +); +AuthInput.displayName = "AuthInput";