- Add Google OAuth sign-in alongside magic link authentication - Username onboarding: require username selection after first sign-in - Public profile pages at /u/[username] with social links (GitHub, X) - Settings page for managing username and social links - Viewer header: unified floating card with project name, @username, breadcrumb - Guest CTA on viewer page prompting sign-in - Show owner avatar + username on community project cards - Redesigned project cards (larger preview, avatar + stats row below) - Consistent navbar across hub and profile pages with GitHub link - Footer with links to GitHub repo and npm packages - Borderless design with natural shadows throughout - Apple corner smoothing (squircle) progressive enhancement - cursor:pointer globally on buttons and links - DB migrations: username, github_url, x_url columns on auth_users - Root-level db:generate/migrate/push/studio scripts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
90 lines
2.7 KiB
TypeScript
90 lines
2.7 KiB
TypeScript
'use client'
|
|
|
|
import Image from 'next/image'
|
|
import { useRouter } from 'next/navigation'
|
|
import { useAuth } from '../lib/auth/hooks'
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/primitives/dropdown-menu'
|
|
|
|
function getInitials(name: string): string {
|
|
return name
|
|
.split(' ')
|
|
.map((n) => n[0])
|
|
.join('')
|
|
.toUpperCase()
|
|
.slice(0, 2)
|
|
}
|
|
|
|
/**
|
|
* ProfileDropdown - User profile menu with avatar, settings, and sign out
|
|
*/
|
|
export function ProfileDropdown() {
|
|
const { user, signOut } = useAuth()
|
|
const router = useRouter()
|
|
|
|
const handleSignOut = async () => {
|
|
await signOut()
|
|
}
|
|
|
|
const initials = user?.name ? getInitials(user.name) : user?.email?.[0]?.toUpperCase() || 'U'
|
|
|
|
return (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<button
|
|
className="flex h-9 w-9 items-center justify-center overflow-hidden rounded-lg border border-border bg-background/95 font-medium text-sm shadow-lg backdrop-blur-md transition-colors hover:bg-accent hover:text-accent-foreground focus:outline-none"
|
|
type="button"
|
|
>
|
|
{user?.image ? (
|
|
<Image
|
|
src={user.image}
|
|
alt={user.name || 'Profile'}
|
|
width={36}
|
|
height={36}
|
|
className="h-full w-full object-cover"
|
|
/>
|
|
) : (
|
|
initials
|
|
)}
|
|
</button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-56">
|
|
<div className="flex items-center gap-3 px-2 py-2">
|
|
{user?.image ? (
|
|
<Image
|
|
src={user.image}
|
|
alt={user.name || 'Profile'}
|
|
width={32}
|
|
height={32}
|
|
className="h-8 w-8 rounded-full object-cover"
|
|
/>
|
|
) : (
|
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-muted font-medium text-xs">
|
|
{initials}
|
|
</div>
|
|
)}
|
|
<div className="min-w-0 flex-1">
|
|
{user?.name && <div className="truncate font-medium text-sm">{user.name}</div>}
|
|
{user?.email && (
|
|
<div className="truncate text-muted-foreground text-xs">{user.email}</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem className="cursor-pointer" onClick={() => router.push('/settings')}>
|
|
Settings
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem className="cursor-pointer" variant="destructive" onClick={handleSignOut}>
|
|
Sign out
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)
|
|
}
|