feat(admin): consentement aux annonces et désabonnement sans connexion
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 5m58s
CI / Deploy production (on server) (push) Successful in 26s

Les comptes déjà créés ne sont pas inscrits. Le choix est facultatif à l’inscription et dans les paramètres. Un lien public demande confirmation avant d’arrêter les actualités, sans toucher aux messages de compte.
This commit is contained in:
Antigravity
2026-09-06 09:23:10 +00:00
parent 6fa12f6e13
commit 360e58e473
33 changed files with 1036 additions and 9 deletions

View File

@@ -6,7 +6,8 @@ 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 { Globe, Bell, Shield, Brain, HelpCircle, Mail } from 'lucide-react'
import { updateMyMarketingPreference } from '@/app/actions/marketing-preference'
import { motion } from 'motion/react'
import { openCookiePreferences } from '@/lib/consent/cookie-consent'
import { useAiConsent } from '@/components/legal/ai-consent-provider'
@@ -21,10 +22,14 @@ interface GeneralSettingsClientProps {
desktopNotifications: boolean
autoSave: boolean
}
initialMarketing?: {
optedIn: boolean
blocked: boolean
}
}
export function GeneralSettingsClient({ initialSettings }: GeneralSettingsClientProps) {
const { t, setLanguage: setContextLanguage } = useLanguage()
export function GeneralSettingsClient({ initialSettings, initialMarketing }: GeneralSettingsClientProps) {
const { t, language: uiLanguage, setLanguage: setContextLanguage } = useLanguage()
const { hasAiConsent, revokeConsent, requestAiConsent } = useAiConsent()
const router = useRouter()
const searchParams = useSearchParams()
@@ -45,6 +50,9 @@ export function GeneralSettingsClient({ initialSettings }: GeneralSettingsClient
const [emailNotifications, setEmailNotifications] = useState(initialSettings.emailNotifications ?? false)
const [desktopNotifications, setDesktopNotifications] = useState(initialSettings.desktopNotifications ?? false)
const [autoSave, setAutoSave] = useState(initialSettings.autoSave ?? true)
const [marketingOptIn, setMarketingOptIn] = useState(initialMarketing?.optedIn === true)
const [marketingBlocked] = useState(initialMarketing?.blocked === true)
const [marketingSaving, setMarketingSaving] = useState(false)
const handleLanguageChange = async (value: string) => {
setLanguage(value)
@@ -80,6 +88,20 @@ export function GeneralSettingsClient({ initialSettings }: GeneralSettingsClient
toast.success(t('settings.settingsSaved'))
}
const handleMarketingChange = async (enabled: boolean) => {
if (marketingBlocked) return
setMarketingSaving(true)
setMarketingOptIn(enabled)
const result = await updateMyMarketingPreference(enabled, uiLanguage)
setMarketingSaving(false)
if (!result.ok) {
setMarketingOptIn(!enabled)
toast.error(t(result.error === 'blocked' ? 'settings.marketingBlocked' : 'settings.settingsSaveFailed'))
return
}
toast.success(t('settings.settingsSaved'))
}
return (
<motion.div
initial={{ opacity: 0, y: 10 }}
@@ -222,6 +244,38 @@ export function GeneralSettingsClient({ initialSettings }: GeneralSettingsClient
</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">
<Mail size={18} />
</div>
<div className="space-y-1">
<h4 className="text-base font-bold text-ink">{t('settings.marketingTitle')}</h4>
<p className="text-[11px] text-concrete max-w-2xl leading-relaxed">
{t('settings.marketingDescription')}
</p>
</div>
</div>
<label className={cn('relative inline-flex items-center shrink-0', marketingBlocked || marketingSaving ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer')}>
<input
type="checkbox"
className="sr-only peer"
checked={marketingOptIn}
disabled={marketingBlocked || marketingSaving}
onChange={(e) => handleMarketingChange(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>
<p className="text-[12px] leading-relaxed text-ink/80">
{t('settings.marketingLegal')}
</p>
{marketingBlocked && (
<p className="text-[12px] text-rose-600">{t('settings.marketingBlocked')}</p>
)}
</div>
<div
ref={aiConsentSectionRef}
className={cn(