Spaces:
Paused
Paused
| import { useState, useEffect, useRef } from "react"; | |
| import { Form, useActionData, useNavigation } from "react-router"; | |
| import { Button } from "~/components/ui/button"; | |
| import { Input } from "~/components/ui/input"; | |
| import { Label } from "~/components/ui/label"; | |
| import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card"; | |
| import { LogIn, UserPlus, ChevronDown } from "lucide-react"; | |
| export function AuthDropdown() { | |
| const [isOpen, setIsOpen] = useState(false); | |
| const [activeTab, setActiveTab] = useState<"login" | "register">("login"); | |
| const dropdownRef = useRef<HTMLDivElement>(null); | |
| useEffect(() => { | |
| function handleClickOutside(event: MouseEvent) { | |
| if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { | |
| setIsOpen(false); | |
| } | |
| } | |
| function handleEscape(event: KeyboardEvent) { | |
| if (event.key === "Escape") { | |
| setIsOpen(false); | |
| } | |
| } | |
| if (isOpen) { | |
| document.addEventListener("mousedown", handleClickOutside); | |
| document.addEventListener("keydown", handleEscape); | |
| } | |
| return () => { | |
| document.removeEventListener("mousedown", handleClickOutside); | |
| document.removeEventListener("keydown", handleEscape); | |
| }; | |
| }, [isOpen]); | |
| return ( | |
| <div className="relative" ref={dropdownRef}> | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={() => setIsOpen(!isOpen)} | |
| className="gap-1 shadow-glow-hover" | |
| data-auth-dropdown | |
| > | |
| <LogIn className="h-4 w-4" /> | |
| <span>Account</span> | |
| <ChevronDown className={`h-3 w-3 transition-transform ${isOpen ? "rotate-180" : ""}`} /> | |
| </Button> | |
| {isOpen && ( | |
| <Card className="absolute right-0 mt-2 w-80 shadow-lg glow-card animate-scaleIn z-50"> | |
| <CardHeader className="pb-3"> | |
| {/* Tab Switcher */} | |
| <div className="flex gap-2 mb-3"> | |
| <button | |
| onClick={() => setActiveTab("login")} | |
| className={`flex-1 flex items-center justify-center gap-2 py-1.5 px-3 rounded-md text-sm transition-all ${ | |
| activeTab === "login" | |
| ? "bg-primary text-primary-foreground" | |
| : "bg-muted text-muted-foreground hover:bg-muted/80" | |
| }`} | |
| > | |
| <LogIn className="h-3.5 w-3.5" /> | |
| <span className="font-medium">Login</span> | |
| </button> | |
| <button | |
| onClick={() => setActiveTab("register")} | |
| className={`flex-1 flex items-center justify-center gap-2 py-1.5 px-3 rounded-md text-sm transition-all ${ | |
| activeTab === "register" | |
| ? "bg-primary text-primary-foreground" | |
| : "bg-muted text-muted-foreground hover:bg-muted/80" | |
| }`} | |
| > | |
| <UserPlus className="h-3.5 w-3.5" /> | |
| <span className="font-medium">Register</span> | |
| </button> | |
| </div> | |
| <CardTitle className="text-lg"> | |
| {activeTab === "login" ? "Welcome back" : "Create account"} | |
| </CardTitle> | |
| <CardDescription className="text-xs"> | |
| {activeTab === "login" | |
| ? "Enter your credentials" | |
| : "Fill in your details"} | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent className="pb-4"> | |
| {activeTab === "login" ? ( | |
| <LoginForm onSuccess={() => setIsOpen(false)} /> | |
| ) : ( | |
| <RegisterForm onSuccess={() => setIsOpen(false)} /> | |
| )} | |
| </CardContent> | |
| </Card> | |
| )} | |
| </div> | |
| ); | |
| } | |
| function LoginForm({ onSuccess }: { onSuccess: () => void }) { | |
| const actionData = useActionData(); | |
| const navigation = useNavigation(); | |
| const isSubmitting = navigation.state === "submitting"; | |
| return ( | |
| <Form method="post" action="/login" className="space-y-3"> | |
| <div className="space-y-1.5"> | |
| <Label htmlFor="login-username" className="text-xs">Username</Label> | |
| <Input | |
| id="login-username" | |
| name="username" | |
| type="text" | |
| placeholder="username" | |
| required | |
| minLength={3} | |
| disabled={isSubmitting} | |
| className="h-9 text-sm" | |
| /> | |
| </div> | |
| <div className="space-y-1.5"> | |
| <Label htmlFor="login-password" className="text-xs">Password</Label> | |
| <Input | |
| id="login-password" | |
| name="password" | |
| type="password" | |
| required | |
| minLength={6} | |
| disabled={isSubmitting} | |
| className="h-9 text-sm" | |
| /> | |
| </div> | |
| {actionData?.error && ( | |
| <p className="text-xs text-red-500 dark:text-red-400">{actionData.error}</p> | |
| )} | |
| <Button | |
| type="submit" | |
| className="w-full h-9 text-sm shadow-glow-hover" | |
| disabled={isSubmitting} | |
| > | |
| {isSubmitting ? "Signing in..." : "Sign in"} | |
| </Button> | |
| </Form> | |
| ); | |
| } | |
| function RegisterForm({ onSuccess }: { onSuccess: () => void }) { | |
| const actionData = useActionData(); | |
| const navigation = useNavigation(); | |
| const isSubmitting = navigation.state === "submitting"; | |
| return ( | |
| <Form method="post" action="/register" className="space-y-3"> | |
| <div className="space-y-1.5"> | |
| <Label htmlFor="register-username" className="text-xs">Username</Label> | |
| <Input | |
| id="register-username" | |
| name="username" | |
| type="text" | |
| placeholder="username" | |
| required | |
| minLength={3} | |
| disabled={isSubmitting} | |
| className="h-9 text-sm" | |
| /> | |
| </div> | |
| <div className="space-y-1.5"> | |
| <Label htmlFor="register-password" className="text-xs">Password</Label> | |
| <Input | |
| id="register-password" | |
| name="password" | |
| type="password" | |
| required | |
| minLength={6} | |
| disabled={isSubmitting} | |
| className="h-9 text-sm" | |
| /> | |
| </div> | |
| {actionData?.error && ( | |
| <p className="text-xs text-red-500 dark:text-red-400">{actionData.error}</p> | |
| )} | |
| <Button | |
| type="submit" | |
| className="w-full h-9 text-sm shadow-glow-hover" | |
| disabled={isSubmitting} | |
| > | |
| {isSubmitting ? "Creating account..." : "Create account"} | |
| </Button> | |
| </Form> | |
| ); | |
| } | |