feat: add user profile page and replace nav welcome text with avatar dropdown

- Replace welcome text in navigation with clickable avatar dropdown
- Add dropdown menu showing user name, email, profile link, and logout
- Create comprehensive user profile page (/profile) with:
  - Profile overview with avatar, name, email, and verification status
  - Account information section with detailed user data
  - Account activity tracking with current session status
  - Quick actions section with placeholder buttons for future features
- Add missing UI components (Card, Badge, Separator) following shadcn/ui patterns
- Use proper design tokens and destructive variant for logout action
- All components pass lint and type checking

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Leon van Zyl
2025-08-27 08:12:09 +02:00
parent 9377f6eabb
commit eb71ad75b6
5 changed files with 422 additions and 22 deletions

222
src/app/profile/page.tsx Normal file
View File

@@ -0,0 +1,222 @@
"use client";
import { useSession } from "@/lib/auth-client";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { Mail, Calendar, User, Shield, ArrowLeft } from "lucide-react";
import { useRouter } from "next/navigation";
export default function ProfilePage() {
const { data: session, isPending } = useSession();
const router = useRouter();
if (isPending) {
return (
<div className="flex items-center justify-center min-h-screen">
<div>Loading...</div>
</div>
);
}
if (!session) {
router.push("/");
return null;
}
const user = session.user;
const createdDate = user.createdAt ? new Date(user.createdAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
}) : null;
return (
<div className="container max-w-4xl mx-auto py-8 px-4">
<div className="flex items-center gap-4 mb-8">
<Button
variant="ghost"
size="sm"
onClick={() => router.back()}
className="flex items-center gap-2"
>
<ArrowLeft className="h-4 w-4" />
Back
</Button>
<h1 className="text-3xl font-bold">Your Profile</h1>
</div>
<div className="grid gap-6">
{/* Profile Overview Card */}
<Card>
<CardHeader>
<div className="flex items-center space-x-4">
<Avatar className="h-20 w-20">
<AvatarImage
src={user.image || ""}
alt={user.name || "User"}
referrerPolicy="no-referrer"
/>
<AvatarFallback className="text-lg">
{(
user.name?.[0] ||
user.email?.[0] ||
"U"
).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="space-y-2">
<h2 className="text-2xl font-semibold">{user.name}</h2>
<div className="flex items-center gap-2 text-muted-foreground">
<Mail className="h-4 w-4" />
<span>{user.email}</span>
{user.emailVerified && (
<Badge variant="outline" className="text-green-600 border-green-600">
<Shield className="h-3 w-3 mr-1" />
Verified
</Badge>
)}
</div>
{createdDate && (
<div className="flex items-center gap-2 text-muted-foreground text-sm">
<Calendar className="h-4 w-4" />
<span>Member since {createdDate}</span>
</div>
)}
</div>
</div>
</CardHeader>
</Card>
{/* Account Information */}
<Card>
<CardHeader>
<CardTitle>Account Information</CardTitle>
<CardDescription>
Your account details and settings
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-sm font-medium text-muted-foreground">
Full Name
</label>
<div className="p-3 border rounded-md bg-muted/10">
{user.name || "Not provided"}
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-muted-foreground">
Email Address
</label>
<div className="p-3 border rounded-md bg-muted/10 flex items-center justify-between">
<span>{user.email}</span>
{user.emailVerified && (
<Badge variant="outline" className="text-green-600 border-green-600">
Verified
</Badge>
)}
</div>
</div>
</div>
<Separator />
<div className="space-y-4">
<h3 className="text-lg font-medium">Account Status</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-center justify-between p-4 border rounded-lg">
<div className="space-y-1">
<p className="font-medium">Email Verification</p>
<p className="text-sm text-muted-foreground">
Email address verification status
</p>
</div>
<Badge variant={user.emailVerified ? "default" : "secondary"}>
{user.emailVerified ? "Verified" : "Unverified"}
</Badge>
</div>
<div className="flex items-center justify-between p-4 border rounded-lg">
<div className="space-y-1">
<p className="font-medium">Account Type</p>
<p className="text-sm text-muted-foreground">
Your account access level
</p>
</div>
<Badge variant="outline">Standard</Badge>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Account Activity */}
<Card>
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
<CardDescription>
Your recent account activity and sessions
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center space-x-3">
<div className="h-2 w-2 bg-green-500 rounded-full"></div>
<div>
<p className="font-medium">Current Session</p>
<p className="text-sm text-muted-foreground">Active now</p>
</div>
</div>
<Badge variant="outline" className="text-green-600 border-green-600">
Active
</Badge>
</div>
</div>
</CardContent>
</Card>
{/* Quick Actions */}
<Card>
<CardHeader>
<CardTitle>Quick Actions</CardTitle>
<CardDescription>
Manage your account settings and preferences
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Button variant="outline" className="justify-start h-auto p-4" disabled>
<User className="h-4 w-4 mr-2" />
<div className="text-left">
<div className="font-medium">Edit Profile</div>
<div className="text-xs text-muted-foreground">Update your information</div>
</div>
</Button>
<Button variant="outline" className="justify-start h-auto p-4" disabled>
<Shield className="h-4 w-4 mr-2" />
<div className="text-left">
<div className="font-medium">Security Settings</div>
<div className="text-xs text-muted-foreground">Manage security options</div>
</div>
</Button>
<Button variant="outline" className="justify-start h-auto p-4" disabled>
<Mail className="h-4 w-4 mr-2" />
<div className="text-left">
<div className="font-medium">Email Preferences</div>
<div className="text-xs text-muted-foreground">Configure notifications</div>
</div>
</Button>
</div>
<p className="text-xs text-muted-foreground mt-4">
Additional profile management features coming soon.
</p>
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -1,12 +1,23 @@
"use client"; "use client";
import { useSession } from "@/lib/auth-client"; import { useSession, signOut } from "@/lib/auth-client";
import { SignInButton } from "./sign-in-button"; import { SignInButton } from "./sign-in-button";
import { SignOutButton } from "./sign-out-button";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { User, LogOut } from "lucide-react";
export function UserProfile() { export function UserProfile() {
const { data: session, isPending } = useSession(); const { data: session, isPending } = useSession();
const router = useRouter();
if (isPending) { if (isPending) {
return <div>Loading...</div>; return <div>Loading...</div>;
@@ -20,26 +31,54 @@ export function UserProfile() {
); );
} }
const handleSignOut = async () => {
await signOut();
router.replace("/");
router.refresh();
};
return ( return (
<div className="flex items-center gap-3"> <DropdownMenu>
<span className="text-sm text-muted-foreground"> <DropdownMenuTrigger asChild>
Welcome {session.user?.name} <Avatar className="size-8 cursor-pointer hover:opacity-80 transition-opacity">
</span> <AvatarImage
<Avatar className="size-8"> src={session.user?.image || ""}
<AvatarImage alt={session.user?.name || "User"}
src={session.user?.image || ""} referrerPolicy="no-referrer"
alt={session.user?.name || "User"} />
referrerPolicy="no-referrer" <AvatarFallback>
/> {(
<AvatarFallback> session.user?.name?.[0] ||
{( session.user?.email?.[0] ||
session.user?.name?.[0] || "U"
session.user?.email?.[0] || ).toUpperCase()}
"U" </AvatarFallback>
).toUpperCase()} </Avatar>
</AvatarFallback> </DropdownMenuTrigger>
</Avatar> <DropdownMenuContent align="end" className="w-56">
<SignOutButton /> <DropdownMenuLabel className="font-normal">
</div> <div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">
{session.user?.name}
</p>
<p className="text-xs leading-none text-muted-foreground">
{session.user?.email}
</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link href="/profile" className="flex items-center">
<User className="mr-2 h-4 w-4" />
Your Profile
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleSignOut} variant="destructive">
<LogOut className="mr-2 h-4 w-4" />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
); );
} }

View File

@@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@@ -0,0 +1,79 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "@/lib/utils"
interface SeparatorProps extends React.HTMLAttributes<HTMLDivElement> {
orientation?: "horizontal" | "vertical"
}
const Separator = React.forwardRef<HTMLDivElement, SeparatorProps>(
({ className, orientation = "horizontal", ...props }, ref) => (
<div
ref={ref}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = "Separator"
export { Separator }