All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 4s
Covers architecture, configuration steps, user flows, API routes, webhooks, pricing, testing with Stripe CLI, production checklist, and troubleshooting.
158 lines
8.1 KiB
TypeScript
158 lines
8.1 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { Input } from '@/components/ui/input'
|
|
import { updateProfile, changePassword } from '@/app/actions/profile'
|
|
import { updateUserSettings } from '@/app/actions/user-settings'
|
|
import { signOut } from 'next-auth/react'
|
|
import { toast } from 'sonner'
|
|
import { useLanguage } from '@/lib/i18n'
|
|
import { User, Mail, Shield, LogOut, Camera, Bell } from 'lucide-react'
|
|
import { motion } from 'motion/react'
|
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
|
|
|
export function ProfileForm({ user }: { user: { name: string | null; email: string; image: string | null } }) {
|
|
const { t } = useLanguage()
|
|
const [desktopNotif, setDesktopNotif] = useState(false)
|
|
|
|
const initial = (user.name || user.email || 'U')[0].toUpperCase()
|
|
|
|
return (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
className="space-y-12 max-w-2xl"
|
|
>
|
|
<div className="space-y-8">
|
|
{/* Avatar + Name */}
|
|
<div className="flex items-center gap-8">
|
|
<div className="relative group">
|
|
<div className="w-24 h-24 rounded-[32px] bg-brand-accent/10 border-2 border-brand-accent/20 flex items-center justify-center overflow-hidden">
|
|
{user.image ? (
|
|
<Avatar className="size-24 rounded-[32px]">
|
|
<AvatarImage src={user.image} alt="" />
|
|
<AvatarFallback className="text-2xl font-serif font-bold text-brand-accent">{initial}</AvatarFallback>
|
|
</Avatar>
|
|
) : (
|
|
<User size={40} className="text-brand-accent" />
|
|
)}
|
|
</div>
|
|
<button className="absolute -bottom-2 -right-2 p-2 bg-ink text-white rounded-xl shadow-lg border border-border opacity-0 group-hover:opacity-100 transition-all hover:scale-110">
|
|
<Camera size={14} />
|
|
</button>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<h3 className="text-2xl font-serif font-bold text-ink">{user.name || 'Utilisateur'}</h3>
|
|
<p className="text-sm text-concrete font-light">{user.email}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Personal info */}
|
|
<div className="grid grid-cols-1 gap-6">
|
|
<div className="space-y-4">
|
|
<h4 className="text-[10px] font-bold uppercase tracking-[0.2em] text-concrete opacity-60">
|
|
{t('profile.description')}
|
|
</h4>
|
|
<div className="space-y-3">
|
|
{/* Name edit */}
|
|
<form action={async (formData) => {
|
|
const result = await updateProfile({ name: formData.get('name') as string })
|
|
if (result?.error) toast.error(t('profile.updateFailed'))
|
|
else toast.success(t('profile.updateSuccess'))
|
|
}} className="p-4 bg-white/40 dark:bg-white/5 border border-border rounded-2xl flex items-center justify-between gap-4">
|
|
<div className="flex items-center gap-4">
|
|
<User size={18} className="text-concrete" />
|
|
<div>
|
|
<p className="text-[10px] uppercase font-bold text-concrete tracking-widest">{t('profile.displayName')}</p>
|
|
<Input name="name" defaultValue={user.name || ''} className="border-0 bg-transparent p-0 h-auto text-sm text-ink focus:ring-0 focus:outline-none" />
|
|
</div>
|
|
</div>
|
|
<button type="submit" className="text-[10px] font-bold text-brand-accent uppercase tracking-widest hover:underline">
|
|
{t('general.save')}
|
|
</button>
|
|
</form>
|
|
|
|
{/* Email */}
|
|
<div className="p-4 bg-white/40 dark:bg-white/5 border border-border rounded-2xl flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<Mail size={18} className="text-concrete" />
|
|
<div>
|
|
<p className="text-[10px] uppercase font-bold text-concrete tracking-widest">Email</p>
|
|
<p className="text-sm text-ink">{user.email}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Security / Password */}
|
|
<form action={async (formData) => {
|
|
const result = await changePassword(formData)
|
|
if (result?.error) {
|
|
const msg = '_form' in result.error
|
|
? result.error._form[0]
|
|
: result.error.currentPassword?.[0] || result.error.newPassword?.[0] || result.error.confirmPassword?.[0] || t('profile.passwordChangeFailed')
|
|
toast.error(msg)
|
|
} else {
|
|
toast.success(t('profile.passwordChangeSuccess'))
|
|
}
|
|
}} className="p-4 bg-white/40 dark:bg-white/5 border border-border rounded-2xl space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<Shield size={18} className="text-concrete" />
|
|
<div>
|
|
<p className="text-[10px] uppercase font-bold text-concrete tracking-widest">{t('profile.changePassword')}</p>
|
|
<p className="text-sm text-ink">{t('profile.changePasswordDescription')}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-3 gap-3 ms-10">
|
|
<Input name="currentPassword" type="password" required placeholder={t('profile.currentPassword')} className="bg-white/50 dark:bg-black/20 border-border text-sm h-9" />
|
|
<Input name="newPassword" type="password" required minLength={6} placeholder={t('profile.newPassword')} className="bg-white/50 dark:bg-black/20 border-border text-sm h-9" />
|
|
<Input name="confirmPassword" type="password" required minLength={6} placeholder={t('profile.confirmPassword')} className="bg-white/50 dark:bg-black/20 border-border text-sm h-9" />
|
|
</div>
|
|
<div className="flex justify-end ms-10">
|
|
<button type="submit" className="text-[10px] font-bold text-brand-accent uppercase tracking-widest hover:underline">
|
|
{t('profile.updatePassword')}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Preferences */}
|
|
<div className="space-y-4">
|
|
<h4 className="text-[10px] font-bold uppercase tracking-[0.2em] text-concrete opacity-60">
|
|
{t('profile.preferences')}
|
|
</h4>
|
|
<div className="p-4 bg-white/40 dark:bg-white/5 border border-border rounded-2xl flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<Bell size={18} className="text-concrete" />
|
|
<div>
|
|
<p className="text-sm text-ink">{t('profile.desktopNotifications')}</p>
|
|
<p className="text-[10px] text-concrete font-light pe-4">{t('profile.desktopNotificationsDesc')}</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => setDesktopNotif(!desktopNotif)}
|
|
className={`w-10 h-5 rounded-full relative p-0.5 cursor-pointer transition-all duration-300 ${desktopNotif ? 'bg-brand-accent' : 'bg-gray-200 dark:bg-white/10'}`}
|
|
>
|
|
<div className={`w-3.5 h-3.5 bg-white rounded-full transition-all duration-300 ${desktopNotif ? 'translate-x-[18px]' : 'translate-x-0'}`} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Logout */}
|
|
<div className="pt-8 border-t border-border/40">
|
|
<button
|
|
onClick={() => signOut({ callbackUrl: '/login' })}
|
|
className="flex items-center gap-3 px-6 py-3 bg-rose-50 dark:bg-rose-500/10 text-rose-600 rounded-xl font-bold uppercase tracking-widest text-[10px] hover:bg-rose-100 transition-colors"
|
|
>
|
|
<LogOut size={16} />
|
|
{t('sidebar.signOut')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
)
|
|
}
|