Files
Momento/memento-note/app/(main)/settings/general/general-settings-client.tsx
Antigravity 5f9b8a9a09
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 7m7s
CI / Deploy production (on server) (push) Successful in 24s
fix(ui): thème des réglages et derniers textes de la revue
Les boutons des pages Agents, MCP, Données, Intégrations et Généraux suivent la couleur choisie. Les libellés encore techniques (recherche par le sens, notes proches, connexion unique) sont en langage courant.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 21:42:28 +00:00

312 lines
15 KiB
TypeScript

'use client'
import { useState, useEffect, useRef } from 'react'
import Link from 'next/link'
import { useLanguage } from '@/lib/i18n'
import { updateAISettings } from '@/app/actions/ai-settings'
import { toast } from 'sonner'
import { useRouter, useSearchParams } from 'next/navigation'
import { Globe, Bell, Shield, Brain, HelpCircle } from 'lucide-react'
import { motion } from 'motion/react'
import { openCookiePreferences } from '@/lib/consent/cookie-consent'
import { useAiConsent } from '@/components/legal/ai-consent-provider'
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
interface GeneralSettingsClientProps {
initialSettings: {
preferredLanguage: string
emailNotifications: boolean
desktopNotifications: boolean
autoSave: boolean
}
}
export function GeneralSettingsClient({ initialSettings }: GeneralSettingsClientProps) {
const { t, setLanguage: setContextLanguage } = useLanguage()
const { hasAiConsent, revokeConsent, requestAiConsent } = useAiConsent()
const router = useRouter()
const searchParams = useSearchParams()
const aiConsentSectionRef = useRef<HTMLDivElement>(null)
const highlightAiConsent = searchParams?.get('highlight') === 'aiConsent'
const [showConsentHighlight, setShowConsentHighlight] = useState(highlightAiConsent)
useEffect(() => {
if (highlightAiConsent && aiConsentSectionRef.current) {
setShowConsentHighlight(true)
aiConsentSectionRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' })
const timer = setTimeout(() => setShowConsentHighlight(false), 4000)
return () => clearTimeout(timer)
}
}, [highlightAiConsent])
const [language, setLanguage] = useState(initialSettings.preferredLanguage || 'auto')
const [emailNotifications, setEmailNotifications] = useState(initialSettings.emailNotifications ?? false)
const [desktopNotifications, setDesktopNotifications] = useState(initialSettings.desktopNotifications ?? false)
const [autoSave, setAutoSave] = useState(initialSettings.autoSave ?? true)
const handleLanguageChange = async (value: string) => {
setLanguage(value)
await updateAISettings({ preferredLanguage: value as any })
if (value === 'auto') {
localStorage.removeItem('user-language')
document.cookie = 'user-language=;path=/;max-age=0'
toast.success(t('settings.languageAuto'))
} else {
localStorage.setItem('user-language', value)
document.cookie = `user-language=${value};path=/;max-age=${60 * 60 * 24 * 365};samesite=lax`
setContextLanguage(value as any)
toast.success(t('profile.languageUpdateSuccess'))
}
setTimeout(() => router.refresh(), 300)
}
const handleEmailNotificationsChange = async (enabled: boolean) => {
setEmailNotifications(enabled)
await updateAISettings({ emailNotifications: enabled })
toast.success(t('settings.settingsSaved'))
}
const handleDesktopNotificationsChange = async (enabled: boolean) => {
setDesktopNotifications(enabled)
await updateAISettings({ desktopNotifications: enabled })
toast.success(t('settings.settingsSaved'))
}
const handleAutoSaveChange = async (enabled: boolean) => {
setAutoSave(enabled)
await updateAISettings({ autoSave: enabled })
toast.success(t('settings.settingsSaved'))
}
return (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-12"
>
<h3 className="text-[10px] font-bold uppercase tracking-[0.3em] text-concrete">
{t('generalSettings.description')}
</h3>
{highlightAiConsent && (
<div className="rounded-xl border border-brand-accent/30 bg-brand-accent/10 p-4 text-sm font-medium text-ink">
{t('consent.ai.settingsBanner')}
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white/40 dark:bg-white/5 border border-border rounded-xl p-8 space-y-6">
<div className="flex items-center gap-5">
<div className="p-3 bg-paper dark:bg-white/10 rounded-2xl text-concrete border border-border">
<Globe size={18} />
</div>
<div className="space-y-0.5">
<h4 className="text-base font-bold text-ink">{t('settings.language')}</h4>
<p className="text-[11px] text-concrete">{t('settings.selectLanguage')}</p>
</div>
</div>
<div className="relative group">
<select
value={language}
onChange={(e) => handleLanguageChange(e.target.value)}
className="w-full bg-white/50 dark:bg-black/40 border border-border rounded-xl px-5 py-3.5 text-sm outline-none focus:ring-1 ring-brand-accent/20 appearance-none cursor-pointer transition-all hover:bg-white dark:hover:bg-black/60 text-ink font-medium"
>
<option value="auto">{t('profile.autoDetect')}</option>
<option value="en">{t('languages.en')}</option>
<option value="fr">{t('languages.fr')}</option>
<option value="es">{t('languages.es')}</option>
<option value="de">{t('languages.de')}</option>
<option value="fa">{t('languages.fa')}</option>
<option value="it">{t('languages.it')}</option>
<option value="pt">{t('languages.pt')}</option>
<option value="ru">{t('languages.ru')}</option>
<option value="zh">{t('languages.zh')}</option>
<option value="ja">{t('languages.ja')}</option>
<option value="ko">{t('languages.ko')}</option>
<option value="ar">{t('languages.ar')}</option>
<option value="hi">{t('languages.hi')}</option>
<option value="nl">{t('languages.nl')}</option>
<option value="pl">{t('languages.pl')}</option>
</select>
<div className="absolute right-5 top-1/2 -translate-y-1/2 pointer-events-none opacity-40 text-concrete">
<svg width="10" height="6" viewBox="0 0 10 6" fill="none">
<path d="M1 1L5 5L9 1" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
</div>
</div>
<div className="bg-white/40 dark:bg-white/5 border border-border rounded-xl p-8 space-y-6">
<div className="flex items-center gap-5">
<div className="p-3 bg-paper dark:bg-white/10 rounded-2xl text-concrete border border-border">
<Bell size={18} />
</div>
<div className="space-y-0.5">
<h4 className="text-base font-bold text-ink">{t('settings.notifications')}</h4>
<p className="text-[11px] text-concrete">{t('settings.notificationsDesc')}</p>
</div>
</div>
<div className="space-y-6 divide-y divide-border/40 text-left">
<div className="flex items-center justify-between pt-0">
<div className="space-y-1">
<p className="text-xs font-bold text-ink">{t('settings.emailNotifications')}</p>
<p className="text-[10px] text-concrete leading-relaxed">{t('settings.emailNotificationsDesc')}</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={emailNotifications}
onChange={(e) => handleEmailNotificationsChange(e.target.checked)}
/>
<div className="w-11 h-6 bg-gray-200 dark:bg-white/10 rounded-full peer peer-checked:after:translate-x-[20px] peer-checked:after:border-white after:content-[''] after:absolute after:top-[4px] after:left-[4px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all duration-300 ease-in-out peer-checked:bg-brand-accent" />
</label>
</div>
<div className="flex items-center justify-between pt-6">
<div className="space-y-1">
<p className="text-xs font-bold text-ink">{t('settings.desktopNotifications')}</p>
<p className="text-[10px] text-concrete leading-relaxed">{t('settings.desktopNotificationsDesc')}</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={desktopNotifications}
onChange={(e) => handleDesktopNotificationsChange(e.target.checked)}
/>
<div className="w-11 h-6 bg-gray-200 dark:bg-white/10 rounded-full peer peer-checked:after:translate-x-[20px] peer-checked:after:border-white after:content-[''] after:absolute after:top-[4px] after:left-[4px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all duration-300 ease-in-out peer-checked:bg-brand-accent" />
</label>
</div>
<div className="flex items-center justify-between pt-6">
<div className="space-y-1">
<p className="text-xs font-bold text-ink">{t('settings.autoSave')}</p>
<p className="text-[10px] text-concrete leading-relaxed">{t('settings.autoSaveDesc')}</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={autoSave}
onChange={(e) => handleAutoSaveChange(e.target.checked)}
/>
<div className="w-11 h-6 bg-gray-200 dark:bg-white/10 rounded-full peer peer-checked:after:translate-x-[20px] peer-checked:after:border-white after:content-[''] after:absolute after:top-[4px] after:left-[4px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all duration-300 ease-in-out peer-checked:bg-brand-accent" />
</label>
</div>
</div>
</div>
<div className="bg-white/40 dark:bg-white/5 border border-border rounded-xl p-8 space-y-6">
<div className="flex items-center gap-5">
<div className="p-3 bg-paper dark:bg-white/10 rounded-2xl text-concrete border border-border">
<Shield size={18} />
</div>
<div className="space-y-0.5">
<h4 className="text-base font-bold text-ink">{t('consent.preferences.title')}</h4>
<p className="text-[11px] text-concrete">{t('consent.preferences.description')}</p>
</div>
</div>
<div className="pt-2">
<button
onClick={() => openCookiePreferences()}
className="w-full px-5 py-3.5 bg-white dark:bg-white/10 border border-border rounded-xl text-xs font-bold uppercase tracking-[0.25em] text-ink dark:text-paper hover:scale-[1.01] active:scale-95 transition-all duration-300 shadow-sm"
>
{t('consent.banner.manage')}
</button>
</div>
</div>
<div
ref={aiConsentSectionRef}
className={cn(
'bg-white/40 dark:bg-white/5 border rounded-xl p-8 space-y-6 md:col-span-2 transition-all duration-500',
showConsentHighlight
? 'border-brand-accent ring-2 ring-brand-accent/40 shadow-lg shadow-brand-accent/10'
: 'border-border'
)}
>
<div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-5">
<div className="p-3 bg-paper dark:bg-white/10 rounded-2xl text-concrete border border-border">
<Brain size={18} />
</div>
<div className="space-y-1">
<div className="flex items-center gap-2">
<h4 className="text-base font-bold text-ink">{t('consent.ai.revocationTitle')}</h4>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="inline-flex text-concrete hover:text-ink transition-colors"
aria-label={t('consent.ai.helpAriaLabel')}
>
<HelpCircle size={16} />
</button>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-sm text-balance leading-relaxed">
{t('consent.ai.helpTooltip')}
</TooltipContent>
</Tooltip>
</div>
<p className="text-[11px] text-concrete max-w-2xl">{t('consent.ai.revocationDescription')}</p>
</div>
</div>
<span
className={cn(
'shrink-0 px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider border',
hasAiConsent
? 'bg-primary/10 text-primary/80 dark:text-primary border-primary/20'
: 'bg-concrete/10 text-concrete border-border'
)}
>
{hasAiConsent ? t('consent.ai.statusActive') : t('consent.ai.statusInactive')}
</span>
</div>
{!hasAiConsent && (
<div className="rounded-xl border border-border/60 bg-paper/50 dark:bg-black/20 p-5 space-y-3 text-left">
<p className="text-xs font-semibold text-ink">{t('consent.ai.whatItMeansTitle')}</p>
<p className="text-[11px] text-concrete leading-relaxed">{t('consent.ai.inactiveHint')}</p>
<ul className="text-[11px] text-concrete leading-relaxed list-disc pl-4 space-y-1">
<li>{t('consent.ai.noCommercialUse')}</li>
<li>{t('consent.ai.affectedFeatures')}</li>
<li>{t('consent.ai.dataPortabilityHint')}</li>
</ul>
<Link
href="/settings/data"
className="inline-flex text-[11px] font-semibold text-ink underline underline-offset-2 hover:opacity-80"
>
{t('consent.ai.dataPortabilityLink')}
</Link>
</div>
)}
<div className="flex flex-col sm:flex-row gap-3 pt-1">
{hasAiConsent ? (
<button
onClick={() => revokeConsent()}
className="flex-1 px-5 py-3.5 bg-white dark:bg-white/10 border border-border rounded-xl text-xs font-bold uppercase tracking-[0.25em] text-ink dark:text-paper hover:scale-[1.01] active:scale-95 transition-all duration-300 shadow-sm"
>
{t('consent.ai.revokeButton')}
</button>
) : (
<button
onClick={() => requestAiConsent()}
className="flex-1 px-5 py-3.5 bg-brand-accent text-white border border-border rounded-xl text-xs font-bold uppercase tracking-[0.25em] hover:scale-[1.01] active:scale-95 transition-all duration-300 shadow-sm"
>
{t('consent.ai.grantButton')}
</button>
)}
</div>
</div>
</div>
</motion.div>
)
}