From 63507a767a69fedfccb97baabed0b170e3138b9d Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Wed, 1 Oct 2025 18:23:15 +0200 Subject: [PATCH] feat(web): add login and registration modal component Add comprehensive LoginModal component with toggle between login and registration modes. Includes form validation, error handling, loading states, and placeholder for GitHub OAuth (coming soon). Provides accessible form with proper ARIA attributes and keyboard navigation --- web/src/components/login-modal.tsx | 266 +++++++++++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 web/src/components/login-modal.tsx diff --git a/web/src/components/login-modal.tsx b/web/src/components/login-modal.tsx new file mode 100644 index 00000000..caee4fd5 --- /dev/null +++ b/web/src/components/login-modal.tsx @@ -0,0 +1,266 @@ +"use client" + +import { Github } from "lucide-react" +import type React from "react" +import { useRef, useState } from "react" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Separator } from "@/components/ui/separator" +import { pb } from "@/lib/pb" + +interface LoginModalProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +export function LoginModal({ open, onOpenChange }: LoginModalProps) { + const [isRegister, setIsRegister] = useState(false) + const [email, setEmail] = useState("") + const [username, setUsername] = useState("") + const [password, setPassword] = useState("") + const [confirmPassword, setConfirmPassword] = useState("") + const [error, setError] = useState("") + const [isLoading, setIsLoading] = useState(false) + const emailRef = useRef(null) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setError("") + setIsLoading(true) + + try { + if (isRegister) { + if (password !== confirmPassword) { + setError("Passwords do not match") + setIsLoading(false) + return + } + + if (!username.trim()) { + setError("Username is required") + setIsLoading(false) + return + } + + if (!email.trim()) { + setError("Email is required") + setIsLoading(false) + return + } + + await pb.collection("users").create({ + username: username.trim(), + email: email.trim(), + password, + passwordConfirm: confirmPassword, + }) + + await pb.collection("users").authWithPassword(email, password) + } else { + await pb.collection("users").authWithPassword(email, password) + } + + onOpenChange(false) + setEmail("") + setUsername("") + setPassword("") + setConfirmPassword("") + } catch (err: any) { + console.error("Auth error:", err) + setError(err?.message || "Authentication failed. Please try again.") + } finally { + setIsLoading(false) + } + } + + const toggleMode = () => { + setIsRegister(!isRegister) + emailRef.current?.focus() + setEmail("") + setUsername("") + setPassword("") + setConfirmPassword("") + setError("") + } + + return ( + + + + + {isRegister ? "Create account" : "Sign in"} + + + {isRegister + ? "Enter your details to create an account" + : "Enter your credentials to continue"} + + + +
+ {error && ( +
+ + + + {error} +
+ )} + + + +
+ + + or + +
+ +
+
+ + setEmail(e.target.value)} + aria-invalid={error ? "true" : "false"} + className="h-11 text-base" + required + /> + {isRegister && ( +

+ Used only to send you updates about your submissions +

+ )} +
+ + {isRegister && ( +
+ + setUsername(e.target.value)} + aria-invalid={error && !username.trim() ? "true" : "false"} + className="h-11 text-base" + required + /> +

+ This will be displayed publicly with your submissions +

+
+ )} + +
+ + setPassword(e.target.value)} + aria-invalid={error ? "true" : "false"} + className="h-11 text-base" + required + /> +
+ + {isRegister && ( +
+ + setConfirmPassword(e.target.value)} + aria-invalid={error && password !== confirmPassword ? "true" : "false"} + className="h-11 text-base" + required + /> +
+ )} +
+ + +
+
+
+ ) +} +