feat: design system overhaul — sidebar, AI chats, settings, brainstorm, color cleanup
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
- Sidebar: dynamic brand-accent colors, brainstorm section restyled - AI chat general: popup panel with expand/collapse, hides when contextual AI open - AI chat contextual: tabs reordered (Actions first), X close button, height fix - Settings: all tabs restyled, 6 new color presets (sage, terracotta, iron, etc.) - Global color cleanup: emerald/orange hardcoded → brand-accent dynamic - Brainstorm page: orange → brand-accent throughout - PageEntry animation component added to key pages - Floating AI button: bg-brand-accent instead of hardcoded black - i18n: all 15 locales updated with new AI/billing keys - Billing: freemium quota tracking, BYOK, stripe subscription scaffolding - Admin: integrated into new design - AGENTS.md + CLAUDE.md project rules added
This commit is contained in:
@@ -2,8 +2,6 @@ import { auth } from '@/auth'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { ProfileForm } from './profile-form'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { ProfilePageHeader } from '@/components/profile-page-header'
|
||||
import { AISettingsLinkCard } from './ai-settings-link-card'
|
||||
|
||||
export default async function ProfilePage() {
|
||||
const session = await auth()
|
||||
@@ -12,33 +10,18 @@ export default async function ProfilePage() {
|
||||
redirect('/login')
|
||||
}
|
||||
|
||||
// Parallel queries
|
||||
const [user, aiSettings] = await Promise.all([
|
||||
prisma.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
select: { name: true, email: true, role: true }
|
||||
}),
|
||||
prisma.userAISettings.findUnique({
|
||||
where: { userId: session.user.id }
|
||||
})
|
||||
])
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
select: { name: true, email: true, role: true, image: true }
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
redirect('/login')
|
||||
}
|
||||
|
||||
const userAISettings = {
|
||||
preferredLanguage: aiSettings?.preferredLanguage || 'auto',
|
||||
showRecentNotes: aiSettings?.showRecentNotes ?? false
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl">
|
||||
<ProfilePageHeader />
|
||||
<ProfileForm user={user} userAISettings={userAISettings} />
|
||||
|
||||
{/* AI Settings Link */}
|
||||
<AISettingsLinkCard />
|
||||
<ProfileForm user={user} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,91 +1,157 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
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, Lock } from 'lucide-react'
|
||||
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, userAISettings }: { user: any; userAISettings?: any }) {
|
||||
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 (
|
||||
<div className="space-y-8">
|
||||
<p className="text-[11px] font-bold uppercase tracking-[0.2em] text-muted-foreground">
|
||||
{t('profile.description')}
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Profile info card */}
|
||||
<div className="bg-card rounded-lg border border-border p-6 shadow-sm flex flex-col gap-4">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-primary shrink-0">
|
||||
<User className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-foreground">{t('profile.title')}</h3>
|
||||
<p className="text-sm text-muted-foreground">{t('profile.description')}</p>
|
||||
<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>
|
||||
<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="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor="name" className="text-sm font-medium text-foreground">{t('profile.displayName')}</label>
|
||||
<Input id="name" name="name" defaultValue={user.name} className="bg-muted border-border focus:border-primary" />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor="email" className="text-sm font-medium text-foreground">{t('profile.email')}</label>
|
||||
<Input id="email" value={user.email} disabled className="bg-muted border-border opacity-60" />
|
||||
</div>
|
||||
<Button type="submit" className="w-full mt-2">{t('general.save')}</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Password card */}
|
||||
<div className="bg-card rounded-lg border border-border p-6 shadow-sm flex flex-col gap-4">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-primary shrink-0">
|
||||
<Lock className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-foreground">{t('profile.changePassword')}</h3>
|
||||
<p className="text-sm text-muted-foreground">{t('profile.changePasswordDescription')}</p>
|
||||
{/* 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>
|
||||
<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'))
|
||||
const form = document.querySelector('form#password-form') as HTMLFormElement
|
||||
form?.reset()
|
||||
}
|
||||
}} id="password-form" className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor="currentPassword" className="text-sm font-medium text-foreground">{t('profile.currentPassword')}</label>
|
||||
<Input id="currentPassword" name="currentPassword" type="password" required className="bg-muted border-border focus:border-primary" />
|
||||
|
||||
{/* Preferences */}
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-[10px] font-bold uppercase tracking-[0.2em] text-concrete opacity-60">
|
||||
{t('profile.preferences') || 'Préférences de compte'}
|
||||
</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') || 'Notification bureau'}</p>
|
||||
<p className="text-[10px] text-concrete font-light pe-4">{t('profile.desktopNotificationsDesc') || 'Recevez des alertes pour vos rappels et activités IA.'}</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 className="space-y-1.5">
|
||||
<label htmlFor="newPassword" className="text-sm font-medium text-foreground">{t('profile.newPassword')}</label>
|
||||
<Input id="newPassword" name="newPassword" type="password" required minLength={6} className="bg-muted border-border focus:border-primary" />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor="confirmPassword" className="text-sm font-medium text-foreground">{t('profile.confirmPassword')}</label>
|
||||
<Input id="confirmPassword" name="confirmPassword" type="password" required minLength={6} className="bg-muted border-border focus:border-primary" />
|
||||
</div>
|
||||
<Button type="submit" className="w-full mt-2">{t('profile.updatePassword')}</Button>
|
||||
</form>
|
||||
</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') || 'Déconnexion'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user