better profie load

This commit is contained in:
songtianlun 2025-07-28 23:19:23 +08:00
parent 0288bf8eda
commit a58b9222c3
3 changed files with 473 additions and 266 deletions

View File

@ -9,6 +9,8 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { Avatar } from '@/components/ui/avatar' import { Avatar } from '@/components/ui/avatar'
import { LoadingSpinner, LoadingOverlay } from '@/components/ui/loading-spinner'
import { AvatarSkeleton, FormFieldSkeleton, TextAreaSkeleton } from '@/components/ui/skeleton'
import { Camera, Save, Eye, EyeOff, Globe } from 'lucide-react' import { Camera, Save, Eye, EyeOff, Globe } from 'lucide-react'
interface UserProfile { interface UserProfile {
@ -45,6 +47,9 @@ export default function ProfilePage() {
}) })
const [saveStatus, setSaveStatus] = useState<{type: 'success' | 'error' | null, message: string}>({type: null, message: ''}) const [saveStatus, setSaveStatus] = useState<{type: 'success' | 'error' | null, message: string}>({type: null, message: ''})
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [profileLoading, setProfileLoading] = useState(true)
const [fieldLoading, setFieldLoading] = useState<{[key: string]: boolean}>({})
const [avatarUploading, setAvatarUploading] = useState(false)
const supabase = createClient() const supabase = createClient()
@ -57,6 +62,7 @@ export default function ProfilePage() {
const loadProfile = async () => { const loadProfile = async () => {
if (!user) return if (!user) return
setProfileLoading(true)
try { try {
// Get user metadata and profile data // Get user metadata and profile data
const { data: { user: userData }, error: userError } = await supabase.auth.getUser() const { data: { user: userData }, error: userError } = await supabase.auth.getUser()
@ -85,12 +91,15 @@ export default function ProfilePage() {
} catch (error) { } catch (error) {
console.error('Error loading profile:', error) console.error('Error loading profile:', error)
setSaveStatus({type: 'error', message: 'Failed to load profile'}) setSaveStatus({type: 'error', message: 'Failed to load profile'})
} finally {
setProfileLoading(false)
} }
} }
const updateProfile = async (field: string, value: string) => { const updateProfile = async (field: string, value: string) => {
if (!user) return if (!user) return
setFieldLoading(prev => ({ ...prev, [field]: true }))
setIsLoading(true) setIsLoading(true)
setSaveStatus({type: null, message: ''}) setSaveStatus({type: null, message: ''})
@ -120,6 +129,7 @@ export default function ProfilePage() {
setSaveStatus({type: 'error', message: (error instanceof Error ? error.message : 'Unknown error') || `Failed to update ${field}`}) setSaveStatus({type: 'error', message: (error instanceof Error ? error.message : 'Unknown error') || `Failed to update ${field}`})
} finally { } finally {
setIsLoading(false) setIsLoading(false)
setFieldLoading(prev => ({ ...prev, [field]: false }))
} }
} }
@ -134,6 +144,7 @@ export default function ProfilePage() {
return return
} }
setFieldLoading(prev => ({ ...prev, password: true }))
setIsLoading(true) setIsLoading(true)
setSaveStatus({type: null, message: ''}) setSaveStatus({type: null, message: ''})
@ -157,6 +168,7 @@ export default function ProfilePage() {
setSaveStatus({type: 'error', message: (error instanceof Error ? error.message : 'Unknown error') || 'Failed to update password'}) setSaveStatus({type: 'error', message: (error instanceof Error ? error.message : 'Unknown error') || 'Failed to update password'})
} finally { } finally {
setIsLoading(false) setIsLoading(false)
setFieldLoading(prev => ({ ...prev, password: false }))
} }
} }
@ -164,6 +176,7 @@ export default function ProfilePage() {
const file = event.target.files?.[0] const file = event.target.files?.[0]
if (!file || !user) return if (!file || !user) return
setAvatarUploading(true)
setIsLoading(true) setIsLoading(true)
setSaveStatus({type: null, message: ''}) setSaveStatus({type: null, message: ''})
@ -187,12 +200,14 @@ export default function ProfilePage() {
await loadProfile() await loadProfile()
setSaveStatus({type: 'success', message: 'Avatar updated successfully'}) setSaveStatus({type: 'success', message: 'Avatar updated successfully'})
setIsLoading(false) setIsLoading(false)
setAvatarUploading(false)
} }
reader.readAsDataURL(file) reader.readAsDataURL(file)
} catch (error: unknown) { } catch (error: unknown) {
setSaveStatus({type: 'error', message: (error instanceof Error ? error.message : 'Unknown error') || 'Failed to upload avatar'}) setSaveStatus({type: 'error', message: (error instanceof Error ? error.message : 'Unknown error') || 'Failed to upload avatar'})
setIsLoading(false) setIsLoading(false)
setAvatarUploading(false)
} }
} }
@ -241,297 +256,372 @@ export default function ProfilePage() {
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Avatar Section */} {/* Avatar Section */}
<div className="lg:col-span-1"> <div className="lg:col-span-1">
<div className="bg-card p-6 rounded-lg border border-border"> {profileLoading ? (
<h2 className="text-xl font-semibold text-foreground mb-4">Profile Picture</h2> <AvatarSkeleton />
<div className="flex flex-col items-center"> ) : (
<div className="relative"> <LoadingOverlay isLoading={avatarUploading}>
<Avatar <div className="bg-card p-6 rounded-lg border border-border">
src={profile?.avatar_url} <h2 className="text-xl font-semibold text-foreground mb-4">Profile Picture</h2>
alt="Profile Avatar" <div className="flex flex-col items-center">
size={96} <div className="relative">
className="w-24 h-24" <Avatar
/> src={profile?.avatar_url}
<label className="absolute -bottom-2 -right-2 bg-primary text-primary-foreground p-2 rounded-full cursor-pointer hover:bg-primary/90"> alt="Profile Avatar"
<Camera className="w-4 h-4" /> size={96}
<input className="w-24 h-24"
type="file" />
accept="image/*" <label className={`absolute -bottom-2 -right-2 bg-primary text-primary-foreground p-2 rounded-full cursor-pointer hover:bg-primary/90 ${avatarUploading ? 'pointer-events-none opacity-50' : ''}`}>
className="hidden" {avatarUploading ? (
onChange={uploadAvatar} <LoadingSpinner size="sm" className="text-primary-foreground" />
disabled={isLoading} ) : (
/> <Camera className="w-4 h-4" />
</label> )}
<input
type="file"
accept="image/*"
className="hidden"
onChange={uploadAvatar}
disabled={isLoading || avatarUploading}
/>
</label>
</div>
<p className="text-sm text-muted-foreground mt-2 text-center">
{avatarUploading ? 'Uploading...' : 'Click the camera icon to upload a new picture'}
</p>
</div>
</div> </div>
<p className="text-sm text-muted-foreground mt-2 text-center"> </LoadingOverlay>
Click the camera icon to upload a new picture )}
</p>
</div>
</div>
</div> </div>
{/* Profile Information */} {/* Profile Information */}
<div className="lg:col-span-2 space-y-6"> <div className="lg:col-span-2 space-y-6">
{/* Username */} {/* Username */}
<div className="bg-card p-6 rounded-lg border border-border"> {profileLoading ? (
<div className="flex items-center justify-between mb-4"> <FormFieldSkeleton />
<h3 className="text-lg font-semibold text-foreground">Username</h3> ) : (
{!isEditing.username && ( <LoadingOverlay isLoading={fieldLoading.username}>
<Button <div className="bg-card p-6 rounded-lg border border-border">
variant="outline" <div className="flex items-center justify-between mb-4">
size="sm" <h3 className="text-lg font-semibold text-foreground">Username</h3>
onClick={() => setIsEditing(prev => ({ ...prev, username: true }))} {!isEditing.username && (
> <Button
Edit variant="outline"
</Button> size="sm"
)} onClick={() => setIsEditing(prev => ({ ...prev, username: true }))}
</div> disabled={isLoading || avatarUploading}
>
{isEditing.username ? ( Edit
<div className="space-y-4"> </Button>
<Input )}
value={formData.username}
onChange={(e) => setFormData(prev => ({ ...prev, username: e.target.value }))}
placeholder="Enter username"
/>
<div className="flex gap-2">
<Button
size="sm"
onClick={() => updateProfile('username', formData.username)}
disabled={isLoading}
>
<Save className="w-4 h-4 mr-2" />
Save
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
setIsEditing(prev => ({ ...prev, username: false }))
setFormData(prev => ({ ...prev, username: profile?.username || '' }))
}}
>
Cancel
</Button>
</div> </div>
{isEditing.username ? (
<div className="space-y-4">
<Input
value={formData.username}
onChange={(e) => setFormData(prev => ({ ...prev, username: e.target.value }))}
placeholder="Enter username"
disabled={fieldLoading.username}
/>
<div className="flex gap-2">
<Button
size="sm"
onClick={() => updateProfile('username', formData.username)}
disabled={isLoading || fieldLoading.username}
>
{fieldLoading.username ? (
<LoadingSpinner size="sm" className="mr-2" />
) : (
<Save className="w-4 h-4 mr-2" />
)}
Save
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
setIsEditing(prev => ({ ...prev, username: false }))
setFormData(prev => ({ ...prev, username: profile?.username || '' }))
}}
disabled={fieldLoading.username}
>
Cancel
</Button>
</div>
</div>
) : (
<p className="text-foreground">{profile?.username || 'No username set'}</p>
)}
</div> </div>
) : ( </LoadingOverlay>
<p className="text-foreground">{profile?.username || 'No username set'}</p> )}
)}
</div>
{/* Email */} {/* Email */}
<div className="bg-card p-6 rounded-lg border border-border"> {profileLoading ? (
<div className="flex items-center justify-between mb-4"> <FormFieldSkeleton />
<h3 className="text-lg font-semibold text-foreground">Email</h3> ) : (
{!isEditing.email && ( <LoadingOverlay isLoading={fieldLoading.email}>
<Button <div className="bg-card p-6 rounded-lg border border-border">
variant="outline" <div className="flex items-center justify-between mb-4">
size="sm" <h3 className="text-lg font-semibold text-foreground">Email</h3>
onClick={() => setIsEditing(prev => ({ ...prev, email: true }))} {!isEditing.email && (
> <Button
Edit variant="outline"
</Button> size="sm"
)} onClick={() => setIsEditing(prev => ({ ...prev, email: true }))}
</div> disabled={isLoading || avatarUploading}
>
{isEditing.email ? ( Edit
<div className="space-y-4"> </Button>
<Input )}
type="email"
value={formData.email}
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
placeholder="Enter email"
/>
<div className="flex gap-2">
<Button
size="sm"
onClick={() => updateProfile('email', formData.email)}
disabled={isLoading}
>
<Save className="w-4 h-4 mr-2" />
Save
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
setIsEditing(prev => ({ ...prev, email: false }))
setFormData(prev => ({ ...prev, email: profile?.email || '' }))
}}
>
Cancel
</Button>
</div> </div>
{isEditing.email ? (
<div className="space-y-4">
<Input
type="email"
value={formData.email}
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
placeholder="Enter email"
disabled={fieldLoading.email}
/>
<div className="flex gap-2">
<Button
size="sm"
onClick={() => updateProfile('email', formData.email)}
disabled={isLoading || fieldLoading.email}
>
{fieldLoading.email ? (
<LoadingSpinner size="sm" className="mr-2" />
) : (
<Save className="w-4 h-4 mr-2" />
)}
Save
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
setIsEditing(prev => ({ ...prev, email: false }))
setFormData(prev => ({ ...prev, email: profile?.email || '' }))
}}
disabled={fieldLoading.email}
>
Cancel
</Button>
</div>
</div>
) : (
<p className="text-foreground">{profile?.email}</p>
)}
</div> </div>
) : ( </LoadingOverlay>
<p className="text-foreground">{profile?.email}</p> )}
)}
</div>
{/* Bio */} {/* Bio */}
<div className="bg-card p-6 rounded-lg border border-border"> {profileLoading ? (
<div className="flex items-center justify-between mb-4"> <TextAreaSkeleton />
<h3 className="text-lg font-semibold text-foreground">Bio</h3> ) : (
{!isEditing.bio && ( <LoadingOverlay isLoading={fieldLoading.bio}>
<Button <div className="bg-card p-6 rounded-lg border border-border">
variant="outline" <div className="flex items-center justify-between mb-4">
size="sm" <h3 className="text-lg font-semibold text-foreground">Bio</h3>
onClick={() => setIsEditing(prev => ({ ...prev, bio: true }))} {!isEditing.bio && (
> <Button
Edit variant="outline"
</Button> size="sm"
)} onClick={() => setIsEditing(prev => ({ ...prev, bio: true }))}
</div> disabled={isLoading || avatarUploading}
>
{isEditing.bio ? ( Edit
<div className="space-y-4"> </Button>
<Textarea )}
value={formData.bio}
onChange={(e) => setFormData(prev => ({ ...prev, bio: e.target.value }))}
placeholder="Tell us about yourself"
className="min-h-[100px]"
maxLength={500}
/>
<div className="text-sm text-muted-foreground text-right">
{formData.bio.length}/500 characters
</div>
<div className="flex gap-2">
<Button
size="sm"
onClick={() => updateProfile('bio', formData.bio)}
disabled={isLoading}
>
<Save className="w-4 h-4 mr-2" />
Save
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
setIsEditing(prev => ({ ...prev, bio: false }))
setFormData(prev => ({ ...prev, bio: profile?.bio || '' }))
}}
>
Cancel
</Button>
</div> </div>
{isEditing.bio ? (
<div className="space-y-4">
<Textarea
value={formData.bio}
onChange={(e) => setFormData(prev => ({ ...prev, bio: e.target.value }))}
placeholder="Tell us about yourself"
className="min-h-[100px]"
maxLength={500}
disabled={fieldLoading.bio}
/>
<div className="text-sm text-muted-foreground text-right">
{formData.bio.length}/500 characters
</div>
<div className="flex gap-2">
<Button
size="sm"
onClick={() => updateProfile('bio', formData.bio)}
disabled={isLoading || fieldLoading.bio}
>
{fieldLoading.bio ? (
<LoadingSpinner size="sm" className="mr-2" />
) : (
<Save className="w-4 h-4 mr-2" />
)}
Save
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
setIsEditing(prev => ({ ...prev, bio: false }))
setFormData(prev => ({ ...prev, bio: profile?.bio || '' }))
}}
disabled={fieldLoading.bio}
>
Cancel
</Button>
</div>
</div>
) : (
<p className="text-foreground">{profile?.bio || 'No bio added yet'}</p>
)}
</div> </div>
) : ( </LoadingOverlay>
<p className="text-foreground">{profile?.bio || 'No bio added yet'}</p> )}
)}
</div>
{/* Language */} {/* Language */}
<div className="bg-card p-6 rounded-lg border border-border"> {profileLoading ? (
<div className="flex items-center justify-between mb-4"> <FormFieldSkeleton />
<h3 className="text-lg font-semibold text-foreground">Language</h3> ) : (
<Globe className="w-5 h-5 text-muted-foreground" /> <LoadingOverlay isLoading={fieldLoading.language}>
</div> <div className="bg-card p-6 rounded-lg border border-border">
<div className="flex items-center justify-between mb-4">
<select <h3 className="text-lg font-semibold text-foreground">Language</h3>
value={formData.language} {fieldLoading.language ? (
onChange={(e) => { <LoadingSpinner size="sm" />
const newLanguage = e.target.value as 'en' | 'zh' ) : (
setFormData(prev => ({ ...prev, language: newLanguage })) <Globe className="w-5 h-5 text-muted-foreground" />
updateProfile('language', newLanguage) )}
}} </div>
className="w-full px-3 py-2 border border-border rounded-md bg-input text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
disabled={isLoading} <select
> value={formData.language}
<option value="en">English</option> onChange={(e) => {
<option value="zh"></option> const newLanguage = e.target.value as 'en' | 'zh'
</select> setFormData(prev => ({ ...prev, language: newLanguage }))
</div> updateProfile('language', newLanguage)
}}
className="w-full px-3 py-2 border border-border rounded-md bg-input text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
disabled={isLoading || fieldLoading.language || avatarUploading}
>
<option value="en">English</option>
<option value="zh"></option>
</select>
</div>
</LoadingOverlay>
)}
{/* Password */} {/* Password */}
<div className="bg-card p-6 rounded-lg border border-border"> {profileLoading ? (
<div className="flex items-center justify-between mb-4"> <FormFieldSkeleton />
<h3 className="text-lg font-semibold text-foreground">Password</h3> ) : (
{!isEditing.password && ( <LoadingOverlay isLoading={fieldLoading.password}>
<Button <div className="bg-card p-6 rounded-lg border border-border">
variant="outline" <div className="flex items-center justify-between mb-4">
size="sm" <h3 className="text-lg font-semibold text-foreground">Password</h3>
onClick={() => setIsEditing(prev => ({ ...prev, password: true }))} {!isEditing.password && (
> <Button
Change Password variant="outline"
</Button> size="sm"
)} onClick={() => setIsEditing(prev => ({ ...prev, password: true }))}
</div> disabled={isLoading || avatarUploading}
{isEditing.password ? (
<div className="space-y-4">
<div>
<Label htmlFor="newPassword">New Password</Label>
<div className="relative mt-1">
<Input
id="newPassword"
type={showPasswords.new ? 'text' : 'password'}
value={formData.newPassword}
onChange={(e) => setFormData(prev => ({ ...prev, newPassword: e.target.value }))}
placeholder="Enter new password"
className="pr-10"
/>
<button
type="button"
onClick={() => setShowPasswords(prev => ({ ...prev, new: !prev.new }))}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-muted-foreground hover:text-foreground"
> >
{showPasswords.new ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />} Change Password
</button> </Button>
</div> )}
</div> </div>
<div> {isEditing.password ? (
<Label htmlFor="confirmPassword">Confirm New Password</Label> <div className="space-y-4">
<div className="relative mt-1"> <div>
<Input <Label htmlFor="newPassword">New Password</Label>
id="confirmPassword" <div className="relative mt-1">
type={showPasswords.confirm ? 'text' : 'password'} <Input
value={formData.confirmPassword} id="newPassword"
onChange={(e) => setFormData(prev => ({ ...prev, confirmPassword: e.target.value }))} type={showPasswords.new ? 'text' : 'password'}
placeholder="Confirm new password" value={formData.newPassword}
className="pr-10" onChange={(e) => setFormData(prev => ({ ...prev, newPassword: e.target.value }))}
/> placeholder="Enter new password"
<button className="pr-10"
type="button" disabled={fieldLoading.password}
onClick={() => setShowPasswords(prev => ({ ...prev, confirm: !prev.confirm }))} />
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-muted-foreground hover:text-foreground" <button
> type="button"
{showPasswords.confirm ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />} onClick={() => setShowPasswords(prev => ({ ...prev, new: !prev.new }))}
</button> className="absolute right-3 top-1/2 transform -translate-y-1/2 text-muted-foreground hover:text-foreground"
disabled={fieldLoading.password}
>
{showPasswords.new ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
</div>
<div>
<Label htmlFor="confirmPassword">Confirm New Password</Label>
<div className="relative mt-1">
<Input
id="confirmPassword"
type={showPasswords.confirm ? 'text' : 'password'}
value={formData.confirmPassword}
onChange={(e) => setFormData(prev => ({ ...prev, confirmPassword: e.target.value }))}
placeholder="Confirm new password"
className="pr-10"
disabled={fieldLoading.password}
/>
<button
type="button"
onClick={() => setShowPasswords(prev => ({ ...prev, confirm: !prev.confirm }))}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-muted-foreground hover:text-foreground"
disabled={fieldLoading.password}
>
{showPasswords.confirm ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
</div>
<div className="flex gap-2">
<Button
size="sm"
onClick={updatePassword}
disabled={isLoading || fieldLoading.password}
>
{fieldLoading.password ? (
<LoadingSpinner size="sm" className="mr-2" />
) : (
<Save className="w-4 h-4 mr-2" />
)}
Update Password
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
setIsEditing(prev => ({ ...prev, password: false }))
setFormData(prev => ({
...prev,
currentPassword: '',
newPassword: '',
confirmPassword: ''
}))
}}
disabled={fieldLoading.password}
>
Cancel
</Button>
</div>
</div> </div>
</div> ) : (
<p className="text-muted-foreground"></p>
<div className="flex gap-2"> )}
<Button
size="sm"
onClick={updatePassword}
disabled={isLoading}
>
<Save className="w-4 h-4 mr-2" />
Update Password
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
setIsEditing(prev => ({ ...prev, password: false }))
setFormData(prev => ({
...prev,
currentPassword: '',
newPassword: '',
confirmPassword: ''
}))
}}
>
Cancel
</Button>
</div>
</div> </div>
) : ( </LoadingOverlay>
<p className="text-muted-foreground"></p> )}
)}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,47 @@
import { cn } from '@/lib/utils'
import { Loader2 } from 'lucide-react'
interface LoadingSpinnerProps {
size?: 'sm' | 'md' | 'lg'
className?: string
}
export function LoadingSpinner({ size = 'md', className }: LoadingSpinnerProps) {
const sizeClasses = {
sm: 'h-4 w-4',
md: 'h-6 w-6',
lg: 'h-8 w-8'
}
return (
<Loader2
className={cn(
'animate-spin text-muted-foreground',
sizeClasses[size],
className
)}
/>
)
}
interface LoadingOverlayProps {
isLoading: boolean
children: React.ReactNode
className?: string
}
export function LoadingOverlay({ isLoading, children, className }: LoadingOverlayProps) {
return (
<div className={cn("relative", className)}>
{children}
{isLoading && (
<div className="absolute inset-0 bg-background/80 backdrop-blur-sm flex items-center justify-center rounded-lg">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<LoadingSpinner size="sm" />
<span>Loading...</span>
</div>
</div>
)}
</div>
)
}

View File

@ -0,0 +1,70 @@
import { cn } from '@/lib/utils'
interface SkeletonProps {
className?: string
}
export function Skeleton({ className }: SkeletonProps) {
return (
<div
className={cn(
"animate-pulse rounded-md bg-muted",
className
)}
/>
)
}
// Specific skeleton components for different profile sections
export function ProfileCardSkeleton() {
return (
<div className="bg-card p-6 rounded-lg border border-border">
<div className="flex items-center justify-between mb-4">
<Skeleton className="h-6 w-24" />
<Skeleton className="h-8 w-12" />
</div>
<Skeleton className="h-4 w-full" />
</div>
)
}
export function AvatarSkeleton() {
return (
<div className="bg-card p-6 rounded-lg border border-border">
<Skeleton className="h-6 w-32 mb-4" />
<div className="flex flex-col items-center">
<div className="relative">
<Skeleton className="w-24 h-24 rounded-full" />
<div className="absolute -bottom-2 -right-2 w-10 h-10 rounded-full bg-primary/20">
<Skeleton className="w-full h-full rounded-full" />
</div>
</div>
<Skeleton className="h-3 w-48 mt-2" />
</div>
</div>
)
}
export function FormFieldSkeleton() {
return (
<div className="bg-card p-6 rounded-lg border border-border">
<div className="flex items-center justify-between mb-4">
<Skeleton className="h-6 w-20" />
<Skeleton className="h-8 w-12" />
</div>
<Skeleton className="h-10 w-full" />
</div>
)
}
export function TextAreaSkeleton() {
return (
<div className="bg-card p-6 rounded-lg border border-border">
<div className="flex items-center justify-between mb-4">
<Skeleton className="h-6 w-16" />
<Skeleton className="h-8 w-12" />
</div>
<Skeleton className="h-24 w-full" />
</div>
)
}