Files
Momento/memento-note/app/(main)/settings/profile/profile-form.tsx
Antigravity db175ebff6
Some checks failed
CI / Lint, Test & Build (push) Failing after 7m49s
CI / Deploy production (on server) (push) Has been cancelled
fix(auth): revoke JWT on logout and harden Google sign-in
Logout now increments sessionVersion so existing JWTs are rejected
server-side, deletes orphaned DB sessions, and uses redirectTo for signOut.
Google OAuth requests account selection each time; optional AUTH_GOOGLE_PROMPT=login
forces Google re-authentication on shared devices.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-17 17:29:51 +00:00

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 { performSignOut } from '@/lib/auth-client'
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={() => performSignOut('/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>
)
}