Files
QR-master/src/components/dashboard/SocialMilestoneDialog.tsx

105 lines
6.7 KiB
TypeScript

'use client';
import { useEffect, useMemo, useState } from 'react';
import { QrCode, Sparkles } from 'lucide-react';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/Dialog';
import { Button } from '@/components/ui/Button';
import { useCsrf } from '@/hooks/useCsrf';
import { useTranslation } from '@/hooks/useTranslation';
import { showToast } from '@/components/ui/Toast';
type Milestone = {
id: string;
qrTitle: string;
threshold: number;
defaultXHandle: string | null;
preview: string;
language: 'en' | 'de';
card: { title: string; label: string };
};
export function SocialMilestoneDialog() {
const { fetchWithCsrf } = useCsrf();
const { locale } = useTranslation();
const [milestone, setMilestone] = useState<Milestone | null>(null);
const [withName, setWithName] = useState(false);
const [xHandle, setXHandle] = useState('');
const [saving, setSaving] = useState(false);
useEffect(() => {
fetch(`/api/social-milestones?locale=${locale}`)
.then(async (response) => response.ok && setMilestone((await response.json()).milestone))
.catch(() => undefined);
}, [locale]);
useEffect(() => setXHandle(milestone?.defaultXHandle || ''), [milestone]);
const copy = milestone?.language === 'de'
? { heading: 'Ein echter Erfolg', subtitle: 'hat gerade einen Scan-Meilenstein erreicht.', consent: 'Darf QR Master diesen Erfolg mit der unten stehenden Karte auf dem eigenen X-Account teilen?', name: 'Meinen X-Handle nennen', decline: 'Nein, danke', optOut: 'Nicht mehr anzeigen', self: 'Selbst teilen', approve: 'Auf QR Master posten', published: 'Der Beitrag wird jetzt auf dem QR Master X-Account veroeffentlicht.' }
: { heading: 'A real milestone', subtitle: 'just reached a scan milestone.', consent: 'May QR Master share this success, including the card below, from our X account?', name: 'Mention my X handle', decline: 'No thanks', optOut: 'Do not show again', self: 'Share myself', approve: 'Post from QR Master', published: 'This will now be published from the QR Master X account.' };
const preview = useMemo(() => {
if (!milestone || !withName || !xHandle.trim()) return milestone?.preview || '';
return `${milestone.preview} By @${xHandle.trim().replace(/^@/, '')}.`;
}, [milestone, withName, xHandle]);
const respond = async (action: 'approve_brand' | 'self_share' | 'decline' | 'opt_out') => {
if (!milestone) return;
setSaving(true);
try {
const response = await fetchWithCsrf(`/api/social-milestones/${milestone.id}`, {
method: 'PATCH', body: JSON.stringify({ action, withName, xHandle, language: milestone.language }),
});
const result = await response.json();
if (!response.ok) throw new Error(result.error || 'Could not save your choice');
if (action === 'self_share') {
await navigator.clipboard?.writeText(preview);
window.open(`https://x.com/intent/post?text=${encodeURIComponent(preview)}`, '_blank', 'noopener,noreferrer');
window.open('https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fwww.qrmaster.net', '_blank', 'noopener,noreferrer');
showToast('Post text copied for LinkedIn.', 'success');
} else if (action === 'approve_brand') {
showToast(copy.published, 'success');
}
setMilestone(null);
} catch (error) {
showToast(error instanceof Error ? error.message : 'Could not save your choice', 'error');
} finally {
setSaving(false);
}
};
if (!milestone) return null;
return <Dialog open onOpenChange={(open) => !open && setMilestone(null)}>
<DialogContent className="max-w-lg overflow-hidden border-slate-200 p-0 shadow-[0_30px_45px_-30px_rgba(50,50,93,0.4)]">
<div className="border-b border-slate-100 px-6 pb-5 pt-6">
<DialogHeader>
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-violet-50 text-violet-700"><Sparkles className="h-5 w-5" /></div>
<DialogTitle className="text-2xl font-semibold tracking-[-0.03em] text-[#061b31]">{copy.heading}</DialogTitle>
<DialogDescription className="pt-1 text-sm leading-6 text-slate-600"><strong className="font-medium text-slate-900">{milestone.qrTitle}</strong> {copy.subtitle}</DialogDescription>
</DialogHeader>
</div>
<div className="space-y-5 px-6 py-5">
<div className="rounded-xl bg-[#061b31] p-5 text-white shadow-[0_18px_36px_-20px_rgba(50,50,93,0.65)]">
<div className="flex items-center justify-between text-xs text-slate-300"><span className="flex items-center gap-2 font-medium tracking-wide"><QrCode className="h-4 w-4" />QR MASTER</span><span>Verified</span></div>
<div className="mt-7 text-5xl font-semibold tracking-[-0.04em] tabular-nums">{milestone.threshold.toLocaleString(milestone.language === 'de' ? 'de-DE' : 'en-US')}</div>
<div className="mt-1 text-sm text-slate-300">{milestone.language === 'de' ? 'eindeutige Scans' : 'unique scans'}</div>
<div className="mt-7 border-t border-white/15 pt-3 text-xs text-slate-300">{milestone.card.label} · {milestone.card.title}</div>
</div>
<p className="text-sm leading-6 text-slate-600">{copy.consent}</p>
<blockquote className="border-l-2 border-violet-500 pl-3 text-sm leading-6 text-slate-700">{preview}</blockquote>
<label className="flex cursor-pointer items-center gap-3 text-sm font-medium text-slate-700"><input type="checkbox" checked={withName} onChange={(event) => setWithName(event.target.checked)} className="h-4 w-4 rounded border-slate-300 text-violet-600 focus:ring-violet-500" />{copy.name}</label>
{withName && <input aria-label="X handle" value={xHandle} onChange={(event) => setXHandle(event.target.value)} placeholder="@yourhandle" className="w-full rounded-md border border-slate-200 px-3 py-2 text-sm outline-none focus:border-violet-500 focus:ring-2 focus:ring-violet-100" />}
</div>
<DialogFooter className="border-t border-slate-100 bg-slate-50 px-6 py-4">
<div className="flex w-full flex-wrap items-center justify-end gap-2">
<Button variant="outline" onClick={() => respond('decline')} disabled={saving}>{copy.decline}</Button>
<Button variant="outline" onClick={() => respond('self_share')} disabled={saving}>{copy.self}</Button>
<Button variant="primary" onClick={() => respond('approve_brand')} disabled={saving}>{copy.approve}</Button>
<button type="button" className="w-full pt-1 text-xs text-slate-500 underline underline-offset-2 hover:text-slate-700" onClick={() => respond('opt_out')} disabled={saving}>{copy.optOut}</button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>;
}