From 36d4128e964922702110f2b58204b08a65e884e8 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Wed, 1 Oct 2025 15:47:21 +0200 Subject: [PATCH] feat(web): add user authentication button with login and profile popover --- web/src/components/user-button.tsx | 343 +++++++++++++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 web/src/components/user-button.tsx diff --git a/web/src/components/user-button.tsx b/web/src/components/user-button.tsx new file mode 100644 index 00000000..a6259441 --- /dev/null +++ b/web/src/components/user-button.tsx @@ -0,0 +1,343 @@ +"use client"; + +import { Github, LogOut, User, LayoutDashboard } from "lucide-react"; +import type React from "react"; +import { useState } from "react"; +import Link from "next/link"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Separator } from "@/components/ui/separator"; +import { pb } from "@/lib/pb"; + +interface UserData { + username: string; + email: string; + avatar?: string; +} + +interface UserButtonProps { + asChild?: boolean; + isLoggedIn?: boolean; + userData?: UserData; +} + +export function UserButton({ + asChild, + isLoggedIn = false, + userData, +}: UserButtonProps) { + return ( + + + + ); +} + +interface UserMenuProps { + userData: UserData; + onSignOut: () => void; +} + +export function UserMenu({ userData, onSignOut }: UserMenuProps) { + return ( +
+
+ + + + {userData.username.slice(0, 2).toUpperCase()} + + +
+

{userData.username}

+

+ {userData.email} +

+
+
+ + + + + + +
+ ); +} + +interface LoginPopupProps { + trigger?: React.ReactNode; + isLoggedIn?: boolean; + userData?: UserData; + onSignOut?: () => void; +} + +export function LoginPopup({ + trigger, + isLoggedIn = false, + userData, + onSignOut, +}: LoginPopupProps) { + const [open, setOpen] = useState(false); + 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 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 { + // For login, use email as the identifier + await pb.collection('users').authWithPassword(email, password); + } + + setOpen(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); + setEmail(""); + setUsername(""); + setPassword(""); + setConfirmPassword(""); + setError(""); + }; + + const handleSignOut = () => { + setOpen(false); + // Wait for dropdown close animation before updating parent state + setTimeout(() => { + onSignOut?.(); + }, 150); + }; + + return ( + + {trigger || } + + {isLoggedIn && userData ? ( + + ) : ( +
+
+

+ {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)} + required + /> + {isRegister && ( +

+ Used only to send you updates about your submissions +

+ )} +
+ + {isRegister && ( +
+ + setUsername(e.target.value)} + required + /> +

+ This will be displayed publicly with your submissions +

+
+ )} + +
+ + setPassword(e.target.value)} + required + /> +
+ + {isRegister && ( +
+ + setConfirmPassword(e.target.value)} + required + /> +
+ )} +
+ + +
+ )} +
+
+ ); +}