284 lines
14 KiB
TypeScript
284 lines
14 KiB
TypeScript
'use client'
|
|
|
|
import { useState } 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 } 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 [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>
|
|
|
|
<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-ink" />
|
|
</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-ink" />
|
|
</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-ink" />
|
|
</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 className="bg-white/40 dark:bg-white/5 border border-border rounded-xl p-8 space-y-6 md:col-span-2">
|
|
<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-ink text-paper 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>
|
|
)
|
|
}
|