better profile settle
This commit is contained in:
parent
5a7aedf11b
commit
d77be176c1
@ -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>
|
||||
|
@ -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'}>
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
206
src/components/ui/user-avatar-dropdown.tsx
Normal file
206
src/components/ui/user-avatar-dropdown.tsx
Normal 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>
|
||||
)
|
||||
}
|
Loading…
Reference in New Issue
Block a user