All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 4s
Covers architecture, configuration steps, user flows, API routes, webhooks, pricing, testing with Stripe CLI, production checklist, and troubleshooting.
127 lines
4.2 KiB
TypeScript
127 lines
4.2 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState } from 'react'
|
|
import { useLanguage } from '@/lib/i18n'
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog'
|
|
import { getConsent, setConsent } from '@/lib/consent/cookie-consent'
|
|
import { toast } from 'sonner'
|
|
|
|
interface CookiePreferencesDialogProps {
|
|
open: boolean
|
|
onOpenChange: (open: boolean) => void
|
|
}
|
|
|
|
export function CookiePreferencesDialog({ open, onOpenChange }: CookiePreferencesDialogProps) {
|
|
const { t } = useLanguage()
|
|
const [analytics, setAnalytics] = useState(false)
|
|
|
|
useEffect(() => {
|
|
if (!open) return
|
|
const current = getConsent()
|
|
setAnalytics(current?.analytics ?? false)
|
|
}, [open])
|
|
|
|
const handleSave = () => {
|
|
setConsent({ analytics, marketing: false })
|
|
toast.success(t('consent.preferences.saved'))
|
|
onOpenChange(false)
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="max-w-lg border-border bg-memento-paper dark:bg-background">
|
|
<DialogHeader>
|
|
<DialogTitle className="font-memento-serif text-xl text-ink">
|
|
{t('consent.preferences.title')}
|
|
</DialogTitle>
|
|
<DialogDescription className="text-[11px] text-concrete leading-relaxed">
|
|
{t('consent.preferences.description')}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<motionless className="space-y-6 py-2">
|
|
<CategoryRow
|
|
title={t('consent.preferences.necessaryTitle')}
|
|
description={t('consent.preferences.necessaryDesc')}
|
|
locked
|
|
checked
|
|
/>
|
|
<CategoryRow
|
|
title={t('consent.preferences.analyticsTitle')}
|
|
description={t('consent.preferences.analyticsDesc')}
|
|
checked={analytics}
|
|
onChange={setAnalytics}
|
|
/>
|
|
</motionless>
|
|
|
|
<DialogFooter className="gap-2 sm:gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => onOpenChange(false)}
|
|
className="px-5 py-2.5 text-[10px] font-bold uppercase tracking-[0.2em] text-concrete hover:text-ink transition-colors"
|
|
>
|
|
{t('common.cancel')}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={handleSave}
|
|
className="px-6 py-2.5 bg-foreground text-background rounded-xl text-[10px] font-bold uppercase tracking-[0.2em] hover:opacity-90 transition-opacity"
|
|
>
|
|
{t('consent.preferences.save')}
|
|
</button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|
|
|
|
function CategoryRow({
|
|
title,
|
|
description,
|
|
checked,
|
|
onChange,
|
|
locked,
|
|
}: {
|
|
title: string
|
|
description: string
|
|
checked: boolean
|
|
onChange?: (v: boolean) => void
|
|
locked?: boolean
|
|
}) {
|
|
return (
|
|
<motionless className="flex items-start justify-between gap-4 border-b border-border/40 pb-5 last:border-0 last:pb-0">
|
|
<div className="space-y-1 pe-4">
|
|
<p className="text-xs font-bold text-ink">{title}</p>
|
|
<p className="text-[10px] text-concrete leading-relaxed">{description}</p>
|
|
{locked && (
|
|
<p className="text-[9px] font-bold uppercase tracking-widest text-concrete/80">
|
|
{title.includes('—') ? '' : ''}
|
|
</p>
|
|
)}
|
|
</motionless>
|
|
<label className={`relative inline-flex shrink-0 items-center ${locked ? 'opacity-50 pointer-events-none' : 'cursor-pointer'}`}>
|
|
<input
|
|
type="checkbox"
|
|
className="sr-only peer"
|
|
checked={checked}
|
|
disabled={locked}
|
|
onChange={(e) => onChange?.(e.target.checked)}
|
|
/>
|
|
<motionless className="w-11 h-6 bg-gray-200 dark:bg-white/10 rounded-full peer peer-checked:after:translate-x-[20px] 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 peer-checked:bg-ink" />
|
|
</label>
|
|
</motionless>
|
|
)
|
|
}
|
|
|
|
/** Avoid pulling motion into dialog — plain wrapper */
|
|
function motionless({ children, className }: { children: React.ReactNode; className?: string }) {
|
|
return <motionless className={className}>{children}</motionless>
|
|
}
|