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:
222
src/app/profile/page.tsx
Normal file
222
src/app/profile/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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,12 +31,16 @@ 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>
|
|
||||||
<Avatar className="size-8">
|
|
||||||
<AvatarImage
|
<AvatarImage
|
||||||
src={session.user?.image || ""}
|
src={session.user?.image || ""}
|
||||||
alt={session.user?.name || "User"}
|
alt={session.user?.name || "User"}
|
||||||
@@ -39,7 +54,31 @@ export function UserProfile() {
|
|||||||
).toUpperCase()}
|
).toUpperCase()}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<SignOutButton />
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-56">
|
||||||
|
<DropdownMenuLabel className="font-normal">
|
||||||
|
<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>
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
36
src/components/ui/badge.tsx
Normal file
36
src/components/ui/badge.tsx
Normal 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 }
|
||||||
79
src/components/ui/card.tsx
Normal file
79
src/components/ui/card.tsx
Normal 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 }
|
||||||
24
src/components/ui/separator.tsx
Normal file
24
src/components/ui/separator.tsx
Normal 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 }
|
||||||
Reference in New Issue
Block a user