better profile settle

This commit is contained in:
songtianlun 2025-08-03 10:15:45 +08:00
parent 5a7aedf11b
commit d77be176c1
4 changed files with 249 additions and 28 deletions

View File

@ -282,13 +282,13 @@ export default function ProfilePage() {
) : (
<div className="bg-card rounded-lg border border-border overflow-hidden">
<div className={`p-4 border-b border-border ${profile?.subscribePlan === 'pro'
? 'bg-gradient-to-r from-amber-50/50 to-orange-50/50 dark:from-amber-950/10 dark:to-orange-950/10'
: 'bg-gradient-to-r from-slate-50/50 to-gray-50/50 dark:from-slate-900/10 dark:to-gray-900/10'
? 'bg-gradient-to-r from-amber-50/60 to-orange-50/60 dark:from-amber-950/10 dark:to-orange-950/10'
: 'bg-gradient-to-r from-slate-50/60 to-gray-50/60 dark:from-slate-950/5 dark:to-gray-950/5'
}`}>
<div className="flex items-center space-x-3">
<div className={`p-2.5 rounded-full ${profile?.subscribePlan === 'pro'
? 'bg-gradient-to-br from-amber-500 to-orange-500'
: 'bg-gradient-to-br from-slate-400 to-gray-500'
? 'bg-gradient-to-br from-amber-500 to-orange-500 dark:from-amber-400 dark:to-orange-400'
: 'bg-gradient-to-br from-slate-400 to-gray-500 dark:from-slate-500 dark:to-gray-400'
}`}>
{profile?.subscribePlan === 'pro' ? (
<Crown className="w-4 h-4 text-white" />
@ -299,8 +299,8 @@ export default function ProfilePage() {
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-foreground text-sm sm:text-base">{t('currentPlan')}</h3>
<p className={`text-xs sm:text-sm truncate font-medium ${profile?.subscribePlan === 'pro'
? 'text-orange-600 dark:text-orange-400'
: 'text-muted-foreground'
? 'text-orange-700 dark:text-orange-300'
: 'text-slate-600 dark:text-slate-400'
}`}>
{profile?.subscribePlan === 'pro' ? t('proPlan') : t('freePlan')}
</p>
@ -310,19 +310,19 @@ export default function ProfilePage() {
<div className="p-4 space-y-4">
{/* Credit Balance */}
<div className="bg-gradient-to-r from-green-50/50 to-emerald-50/50 dark:from-green-950/10 dark:to-emerald-950/10 rounded-lg p-4 border border-border">
<div className="bg-gradient-to-r from-green-50/60 to-emerald-50/60 dark:from-green-950/8 dark:to-emerald-950/8 rounded-lg p-4 border border-green-200/30 dark:border-green-900/30">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="p-2 rounded-full bg-gradient-to-br from-green-500 to-emerald-600">
<div className="p-2 rounded-full bg-gradient-to-br from-green-500 to-emerald-600 dark:from-green-400 dark:to-emerald-500">
<CreditCard className="w-4 h-4 text-white" />
</div>
<span className="text-sm font-semibold text-foreground">{t('creditBalance')}</span>
</div>
<div className="text-right">
<div className="text-2xl font-bold text-green-600 dark:text-green-400">
<div className="text-2xl font-bold text-green-700 dark:text-green-300">
${(creditInfo?.totalBalance || 0).toFixed(2)}
</div>
<div className="text-xs font-medium text-muted-foreground">{t('usdCredit')}</div>
<div className="text-xs font-medium text-green-600 dark:text-green-400">{t('usdCredit')}</div>
</div>
</div>
</div>

View File

@ -7,6 +7,7 @@ import { useAuth } from '@/hooks/useAuth'
import { Button } from '@/components/ui/button'
import { ThemeToggle, MobileThemeToggle } from '@/components/ui/theme-toggle'
import { LanguageToggle, MobileLanguageToggle } from '@/components/ui/language-toggle'
import { UserAvatarDropdown, MobileUserMenu } from '@/components/ui/user-avatar-dropdown'
import { Menu, X, Zap } from 'lucide-react'
export function Header() {
@ -46,14 +47,11 @@ export function Header() {
<LanguageToggle variant="dropdown" showLabel={false} />
<ThemeToggle variant="dropdown" showLabel={false} />
{user ? (
<div className="flex items-center space-x-2">
<Button variant="ghost" size="sm" onClick={() => window.location.href = '/profile'}>
{t('profile')}
</Button>
<Button variant="outline" onClick={signOut}>
{t('signOut')}
</Button>
</div>
<UserAvatarDropdown
user={user}
onSignOut={signOut}
onProfileClick={() => window.location.href = '/profile'}
/>
) : (
<div className="flex items-center space-x-2">
<Button variant="ghost" onClick={() => window.location.href = '/signin'}>
@ -107,14 +105,11 @@ export function Header() {
<div className="pt-2 pb-2 border-t">
{user ? (
<div className="space-y-2">
<Button variant="ghost" className="w-full justify-start" onClick={() => window.location.href = '/profile'}>
{t('profile')}
</Button>
<Button variant="outline" className="w-full" onClick={signOut}>
{t('signOut')}
</Button>
</div>
<MobileUserMenu
user={user}
onSignOut={signOut}
onProfileClick={() => window.location.href = '/profile'}
/>
) : (
<div className="space-y-2">
<Button variant="ghost" className="w-full" onClick={() => window.location.href = '/signin'}>

View File

@ -11,12 +11,32 @@ interface AvatarProps {
}
export function Avatar({ src, alt = "Avatar", size = 96, className = "" }: AvatarProps) {
const sizeClass = `w-${size/4} h-${size/4}`
// Convert pixel size to Tailwind classes
const getSizeClasses = (size: number) => {
if (size <= 32) return "w-8 h-8"
if (size <= 40) return "w-10 h-10"
if (size <= 48) return "w-12 h-12"
if (size <= 64) return "w-16 h-16"
if (size <= 96) return "w-24 h-24"
return "w-32 h-32"
}
const getIconSize = (size: number) => {
if (size <= 32) return "w-4 h-4"
if (size <= 40) return "w-5 h-5"
if (size <= 48) return "w-6 h-6"
if (size <= 64) return "w-8 h-8"
if (size <= 96) return "w-12 h-12"
return "w-16 h-16"
}
const sizeClass = getSizeClasses(size)
const iconClass = getIconSize(size)
if (!src) {
return (
<div className={`${sizeClass} rounded-full bg-muted flex items-center justify-center overflow-hidden ${className}`}>
<User className="w-12 h-12 text-muted-foreground" />
<User className={`${iconClass} text-muted-foreground`} />
</div>
)
}

View File

@ -0,0 +1,206 @@
'use client'
import { useState, useEffect } from 'react'
import { useTranslations } from 'next-intl'
import { User as SupabaseUser } from '@supabase/supabase-js'
import { Button } from '@/components/ui/button'
import { Avatar } from '@/components/ui/avatar'
import { ChevronDown, User, LogOut } from 'lucide-react'
import { cn } from '@/lib/utils'
interface UserAvatarDropdownProps {
user: SupabaseUser
onSignOut: () => void
onProfileClick: () => void
className?: string
}
// Mobile version for mobile menu
export function MobileUserMenu({
user,
onSignOut,
onProfileClick,
className
}: UserAvatarDropdownProps) {
const t = useTranslations('navigation')
const userName = user.user_metadata?.full_name ||
user.user_metadata?.name ||
user.email?.split('@')[0] ||
'User'
const userAvatar = user.user_metadata?.avatar_url ||
user.user_metadata?.picture
return (
<div className={cn("space-y-1", className)}>
{/* User info section */}
<div className="px-3 py-2 border-b border-border">
<div className="flex items-center gap-3">
<Avatar
src={userAvatar}
alt={userName}
size={48}
className="w-12 h-12"
/>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium truncate">
{userName}
</div>
<div className="text-xs text-muted-foreground truncate">
{user.email}
</div>
</div>
</div>
</div>
{/* Menu items */}
<div className="space-y-1">
<button
className={cn(
"w-full flex items-center gap-3 px-3 py-2 text-sm rounded-md transition-colors",
"hover:bg-accent hover:text-accent-foreground",
"focus:bg-accent focus:text-accent-foreground focus:outline-none"
)}
onClick={onProfileClick}
>
<User className="h-4 w-4" />
<span>{t('profile')}</span>
</button>
<button
className={cn(
"w-full flex items-center gap-3 px-3 py-2 text-sm rounded-md transition-colors",
"hover:bg-accent hover:text-accent-foreground",
"focus:bg-accent focus:text-accent-foreground focus:outline-none",
"text-destructive hover:text-destructive"
)}
onClick={onSignOut}
>
<LogOut className="h-4 w-4" />
<span>{t('signOut')}</span>
</button>
</div>
</div>
)
}
export function UserAvatarDropdown({
user,
onSignOut,
onProfileClick,
className
}: UserAvatarDropdownProps) {
const t = useTranslations('navigation')
const [isOpen, setIsOpen] = useState(false)
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Element
if (!target.closest('[data-user-dropdown]')) {
setIsOpen(false)
}
}
if (isOpen) {
document.addEventListener('click', handleClickOutside)
return () => document.removeEventListener('click', handleClickOutside)
}
}, [isOpen])
// Get user display name and avatar
const userName = user.user_metadata?.full_name ||
user.user_metadata?.name ||
user.email?.split('@')[0] ||
'User'
const userAvatar = user.user_metadata?.avatar_url ||
user.user_metadata?.picture
return (
<div className={cn("relative", className)} data-user-dropdown>
<Button
variant="ghost"
size="sm"
className="h-9 gap-2 px-2 hover:bg-accent/50"
onClick={() => setIsOpen(!isOpen)}
aria-expanded={isOpen}
aria-haspopup="menu"
>
<Avatar
src={userAvatar}
alt={userName}
size={32}
className="w-8 h-8"
/>
<span className="hidden sm:inline-block text-sm font-medium max-w-24 truncate">
{userName}
</span>
<ChevronDown className={cn(
"h-3 w-3 transition-transform",
isOpen && "rotate-180"
)} />
<span className="sr-only">User menu</span>
</Button>
{isOpen && (
<div className="absolute right-0 top-full mt-1 w-48 rounded-md border border-border bg-popover p-1 shadow-md z-50">
{/* User info section */}
<div className="px-3 py-2 border-b border-border">
<div className="flex items-center gap-3">
<Avatar
src={userAvatar}
alt={userName}
size={40}
className="w-10 h-10"
/>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium truncate">
{userName}
</div>
<div className="text-xs text-muted-foreground truncate">
{user.email}
</div>
</div>
</div>
</div>
{/* Menu items */}
<div className="py-1">
<button
className={cn(
"w-full flex items-center gap-3 px-3 py-2 text-sm rounded-sm transition-colors",
"hover:bg-accent hover:text-accent-foreground",
"focus:bg-accent focus:text-accent-foreground focus:outline-none"
)}
onClick={() => {
onProfileClick()
setIsOpen(false)
}}
role="menuitem"
>
<User className="h-4 w-4" />
<span>{t('profile')}</span>
</button>
<button
className={cn(
"w-full flex items-center gap-3 px-3 py-2 text-sm rounded-sm transition-colors",
"hover:bg-accent hover:text-accent-foreground",
"focus:bg-accent focus:text-accent-foreground focus:outline-none",
"text-destructive hover:text-destructive"
)}
onClick={() => {
onSignOut()
setIsOpen(false)
}}
role="menuitem"
>
<LogOut className="h-4 w-4" />
<span>{t('signOut')}</span>
</button>
</div>
</div>
)}
</div>
)
}