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 ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}