better profile
This commit is contained in:
parent
d77be176c1
commit
5a7584c673
@ -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 {
|
||||
* {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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)"
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user