better profile

This commit is contained in:
songtianlun 2025-08-03 10:24:50 +08:00
parent d77be176c1
commit 5a7584c673
4 changed files with 160 additions and 45 deletions

View File

@ -135,6 +135,16 @@ body {
background: rgb(var(--muted-foreground) / 0.5);
}
/* Custom animations */
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
/* Print styles */
@media print {
* {

View File

@ -10,8 +10,7 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Avatar } from '@/components/ui/avatar'
import { LoadingSpinner, LoadingOverlay } from '@/components/ui/loading-spinner'
import { FullScreenLoading } from '@/components/ui/full-screen-loading'
import { LoadingSpinner, LoadingOverlay, GradientLoading } from '@/components/ui/loading-spinner'
import { AvatarSkeleton, FormFieldSkeleton, TextAreaSkeleton } from '@/components/ui/skeleton'
import { Save, Eye, EyeOff, CreditCard, Crown, Star } from 'lucide-react'
@ -216,19 +215,49 @@ export default function ProfilePage() {
}
// Show skeleton screens immediately when auth is loading
if (loading) {
return <FullScreenLoading isVisible={true} message={t('loadingStudio')} />
return (
<div className="min-h-screen">
<Header />
<div className="max-w-4xl mx-auto px-4 py-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-foreground mb-2">{t('title')}</h1>
<p className="text-muted-foreground">{t('subtitle')}</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 lg:gap-8">
<div className="lg:col-span-1 space-y-6">
<AvatarSkeleton />
<FormFieldSkeleton />
</div>
<div className="lg:col-span-2 space-y-6">
<FormFieldSkeleton />
<FormFieldSkeleton />
<TextAreaSkeleton />
<FormFieldSkeleton />
<FormFieldSkeleton />
</div>
</div>
</div>
</div>
)
}
if (!user) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold text-foreground mb-4">{t('accessDenied')}</h1>
<p className="text-muted-foreground mb-4">{t('pleaseSignIn')}</p>
<Button onClick={() => window.location.href = '/signin'}>
{tAuth('signIn')}
</Button>
<div className="min-h-screen">
<Header />
<div className="max-w-4xl mx-auto px-4 py-8">
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<h1 className="text-2xl font-bold text-foreground mb-4">{t('accessDenied')}</h1>
<p className="text-muted-foreground mb-4">{t('pleaseSignIn')}</p>
<Button onClick={() => window.location.href = '/signin'}>
{tAuth('signIn')}
</Button>
</div>
</div>
</div>
</div>
)
@ -258,9 +287,12 @@ export default function ProfilePage() {
<div className="lg:col-span-1 space-y-6">
{/* Profile Picture */}
{profileLoading ? (
<AvatarSkeleton />
<div className="animate-in fade-in-0 duration-300">
<AvatarSkeleton />
</div>
) : (
<div className="bg-card p-6 rounded-lg border border-border">
<div className="animate-in fade-in-0 slide-in-from-left-2 duration-500">
<div className="bg-card p-6 rounded-lg border border-border transition-all duration-200 hover:shadow-sm">
<h2 className="text-xl font-semibold text-foreground mb-4">{t('profilePicture')}</h2>
<div className="flex flex-col items-center">
<Avatar
@ -274,13 +306,17 @@ export default function ProfilePage() {
</p>
</div>
</div>
</div>
)}
{/* Subscription Status */}
{profileLoading ? (
<FormFieldSkeleton />
<div className="animate-in fade-in-0 duration-300">
<FormFieldSkeleton />
</div>
) : (
<div className="bg-card rounded-lg border border-border overflow-hidden">
<div className="animate-in fade-in-0 slide-in-from-left-2 duration-500 delay-100">
<div className="bg-card rounded-lg border border-border overflow-hidden transition-all duration-200 hover:shadow-sm">
<div className={`p-4 border-b border-border ${profile?.subscribePlan === 'pro'
? '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'
@ -351,6 +387,7 @@ export default function ProfilePage() {
</div>
</div>
</div>
</div>
</div>
)}
</div>
@ -359,10 +396,13 @@ export default function ProfilePage() {
<div className="lg:col-span-2 space-y-6">
{/* Username */}
{profileLoading ? (
<FormFieldSkeleton />
<div className="animate-in fade-in-0 duration-300">
<FormFieldSkeleton />
</div>
) : (
<LoadingOverlay isLoading={fieldLoading.username}>
<div className="bg-card p-6 rounded-lg border border-border">
<div className="animate-in fade-in-0 slide-in-from-bottom-2 duration-500">
<LoadingOverlay isLoading={fieldLoading.username}>
<div className="bg-card p-6 rounded-lg border border-border transition-all duration-200 hover:shadow-sm">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-foreground">{t('username')}</h3>
{!isEditing.username && (
@ -414,16 +454,20 @@ export default function ProfilePage() {
) : (
<p className="text-foreground">{profile?.username || t('noUsernameSet')}</p>
)}
</div>
</LoadingOverlay>
</div>
</LoadingOverlay>
</div>
)}
{/* Email */}
{profileLoading ? (
<FormFieldSkeleton />
<div className="animate-in fade-in-0 duration-300">
<FormFieldSkeleton />
</div>
) : (
<LoadingOverlay isLoading={fieldLoading.email}>
<div className="bg-card p-6 rounded-lg border border-border">
<div className="animate-in fade-in-0 slide-in-from-bottom-2 duration-500 delay-75">
<LoadingOverlay isLoading={fieldLoading.email}>
<div className="bg-card p-6 rounded-lg border border-border transition-all duration-200 hover:shadow-sm">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-foreground">{t('email')}</h3>
{!isEditing.email && (
@ -476,16 +520,20 @@ export default function ProfilePage() {
) : (
<p className="text-foreground">{profile?.email}</p>
)}
</div>
</LoadingOverlay>
</div>
</LoadingOverlay>
</div>
)}
{/* Bio */}
{profileLoading ? (
<TextAreaSkeleton />
<div className="animate-in fade-in-0 duration-300">
<TextAreaSkeleton />
</div>
) : (
<LoadingOverlay isLoading={fieldLoading.bio}>
<div className="bg-card p-6 rounded-lg border border-border">
<div className="animate-in fade-in-0 slide-in-from-bottom-2 duration-500 delay-150">
<LoadingOverlay isLoading={fieldLoading.bio}>
<div className="bg-card p-6 rounded-lg border border-border transition-all duration-200 hover:shadow-sm">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-foreground">{t('bio')}</h3>
{!isEditing.bio && (
@ -542,17 +590,21 @@ export default function ProfilePage() {
) : (
<p className="text-foreground">{profile?.bio || t('noBioAdded')}</p>
)}
</div>
</LoadingOverlay>
</div>
</LoadingOverlay>
</div>
)}
{/* Password */}
{profileLoading ? (
<FormFieldSkeleton />
<div className="animate-in fade-in-0 duration-300">
<FormFieldSkeleton />
</div>
) : (
<LoadingOverlay isLoading={fieldLoading.password}>
<div className="bg-card p-6 rounded-lg border border-border">
<div className="animate-in fade-in-0 slide-in-from-bottom-2 duration-500 delay-200">
<LoadingOverlay isLoading={fieldLoading.password}>
<div className="bg-card p-6 rounded-lg border border-border transition-all duration-200 hover:shadow-sm">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-foreground">{tAuth('password')}</h3>
{!isEditing.password && (
@ -649,16 +701,20 @@ export default function ProfilePage() {
) : (
<p className="text-muted-foreground"></p>
)}
</div>
</LoadingOverlay>
</div>
</LoadingOverlay>
</div>
)}
{/* Version Limit Settings */}
{profileLoading ? (
<FormFieldSkeleton />
<div className="animate-in fade-in-0 duration-300">
<FormFieldSkeleton />
</div>
) : (
<LoadingOverlay isLoading={fieldLoading.versionLimit}>
<div className="bg-card p-6 rounded-lg border border-border">
<div className="animate-in fade-in-0 slide-in-from-bottom-2 duration-500 delay-300">
<LoadingOverlay isLoading={fieldLoading.versionLimit}>
<div className="bg-card p-6 rounded-lg border border-border transition-all duration-200 hover:shadow-sm">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-foreground">Version Limit</h3>
@ -739,8 +795,9 @@ export default function ProfilePage() {
</div>
</div>
)}
</div>
</LoadingOverlay>
</div>
</LoadingOverlay>
</div>
)}
</div>
</div>

View File

@ -24,21 +24,65 @@ export function LoadingSpinner({ size = 'md', className }: LoadingSpinnerProps)
)
}
interface GradientLoadingProps {
size?: 'sm' | 'md' | 'lg'
className?: string
}
export function GradientLoading({ size = 'md', className }: GradientLoadingProps) {
const sizeClasses = {
sm: 'w-5 h-5',
md: 'w-8 h-8',
lg: 'w-12 h-12'
}
return (
<div className={cn('flex items-center justify-center', className)}>
<div className={cn(
'relative',
sizeClasses[size]
)}>
<div className={cn(
'absolute inset-0 rounded-full bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 opacity-75',
'animate-spin'
)} />
<div className={cn(
'absolute inset-1 rounded-full bg-background'
)} />
<div className={cn(
'absolute inset-2 rounded-full bg-gradient-to-r from-blue-400 via-purple-400 to-pink-400 opacity-60',
'animate-ping'
)} />
</div>
</div>
)
}
interface LoadingOverlayProps {
isLoading: boolean
children: React.ReactNode
className?: string
showText?: boolean
text?: string
}
export function LoadingOverlay({ isLoading, children, className }: LoadingOverlayProps) {
export function LoadingOverlay({
isLoading,
children,
className,
showText = false,
text = "Loading..."
}: 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 className="absolute inset-0 bg-background/60 backdrop-blur-sm flex items-center justify-center rounded-lg transition-all duration-200 ease-in-out">
<div className="flex items-center gap-3">
<GradientLoading size="sm" />
{showText && (
<span className="text-sm text-muted-foreground font-medium">{text}</span>
)}
</div>
</div>
)}

View File

@ -8,9 +8,13 @@ export function Skeleton({ className }: SkeletonProps) {
return (
<div
className={cn(
"animate-pulse rounded-md bg-muted",
"animate-pulse rounded-md bg-gradient-to-r from-muted via-muted/50 to-muted bg-[length:200%_100%]",
"animate-[shimmer_1.5s_ease-in-out_infinite]",
className
)}
style={{
backgroundImage: "linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent)"
}}
/>
)
}