'use client'; import { useEffect, useMemo, useState } from 'react'; import { Check, Copy, ExternalLink, LineChart, Linkedin, Send, Sparkles, X } 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 Card = { language: 'en' | 'de'; qrTitle: string; label: string; title: string; totalScans?: number; totalUniqueScans: number; milestoneThreshold: number; trend: { points: Array<{ at: string; total: number }>; startLabel: string; endLabel: string; target: number; ceiling: number } | null }; type Milestone = { id: string; qrTitle: string; threshold: number; defaultXHandle: string | null; preview: string; shareUrl: string; language: 'en' | 'de'; card: Card; brandStatus: string; brandPostUrl: string | null; brandPostError: string | null }; type BrandState = { brandStatus: string; brandPostUrl: string | null; brandPostError: string | null; selfSharedAt: string | null }; function Trend({ trend, locale }: { trend: NonNullable; locale: 'en' | 'de' }) { const ceiling = trend.target <= 5 ? 5 : trend.target * 1.25; const ticks = trend.target <= 5 ? [1, 2, 3, 4, 5] : Array.from({ length: 5 }, (_, index) => trend.target * (index + 1) / 4); const first = new Date(trend.points[0].at).getTime(); const last = Math.max(new Date(trend.points[trend.points.length - 1].at).getTime(), first + 1); const points = trend.points.map(point => { const x = 60 + ((new Date(point.at).getTime() - first) / (last - first)) * 410; const y = 142 - (point.total / ceiling) * 126; return `${x},${y}`; }).join(' '); const targetY = 142 - (trend.target / ceiling) * 126; const number = new Intl.NumberFormat(locale === 'de' ? 'de-DE' : 'en-US', { maximumFractionDigits: 2 }); return {ticks.map(tick => { const y = 142 - (tick / ceiling) * 126; return {number.format(tick)}; })} {trend.startLabel} {trend.endLabel} ; } async function copyShareText(text: string) { try { await navigator.clipboard.writeText(text); return; } catch { const textarea = document.createElement('textarea'); textarea.value = text; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.focus(); textarea.select(); const copied = document.execCommand('copy'); textarea.remove(); if (!copied) throw new Error('Copying is blocked by this browser'); } } export function SocialMilestoneDialog() { const { fetchWithCsrf } = useCsrf(); const { locale } = useTranslation(); const [milestone, setMilestone] = useState(null); const [withName, setWithName] = useState(false); const [xHandle, setXHandle] = useState(''); const [saving, setSaving] = useState<'brand' | 'self' | 'copy' | null>(null); const [brand, setBrand] = useState(null); useEffect(() => { fetch(`/api/social-milestones?locale=${locale}`).then(async response => { if (response.ok) { const next = (await response.json()).milestone as Milestone | null; setMilestone(next); if (next) setBrand({ brandStatus: next.brandStatus, brandPostUrl: next.brandPostUrl, brandPostError: next.brandPostError, selfSharedAt: null }); } }).catch(() => undefined); }, [locale]); useEffect(() => setXHandle(milestone?.defaultXHandle || ''), [milestone]); useEffect(() => { if (!milestone || !['approved', 'processing'].includes(brand?.brandStatus || '')) return; const poll = async () => { const response = await fetch(`/api/social-milestones/${milestone.id}`); if (response.ok) setBrand((await response.json()).milestone); }; const timer = window.setInterval(poll, 3000); void poll(); return () => window.clearInterval(timer); }, [milestone, brand?.brandStatus]); const copy = milestone?.language === 'de' ? { heading: 'Ein echter Erfolg', subtitle: 'hat einen Scan-Meilenstein erreicht.', consent: 'Darf QR Master diesen Erfolg auf dem eigenen X-Account veröffentlichen?', name: 'Meinen X-Handle nennen', decline: 'Nicht jetzt', self: 'Selbst teilen', approve: 'Auf QR Master posten', queued: 'Wird auf X veröffentlicht …', posted: 'Auf X veröffentlicht', failed: 'Veröffentlichung fehlgeschlagen' } : { heading: 'A real milestone', subtitle: 'just reached a scan milestone.', consent: 'May QR Master publish this success from our X account?', name: 'Mention my X handle', decline: 'Not now', self: 'Share myself', approve: 'Post from QR Master', queued: 'Waiting for the X publisher …', posted: 'Published on X', failed: 'Publishing failed' }; const postCopy = useMemo(() => { if (!milestone || !withName || !xHandle.trim()) return milestone?.preview || ''; const mention = milestone.language === 'de' ? 'Glückwunsch' : 'Congratulations'; return `${milestone.preview}\n\n${mention} @${xHandle.trim().replace(/^@/, '')}.`; }, [milestone, withName, xHandle]); const preview = milestone ? `${postCopy}\n\n${milestone.shareUrl}` : ''; const update = async (action: 'approve_brand' | 'self_share' | 'decline' | 'opt_out') => { if (!milestone) return null; 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'); return result; }; const prepareSelfShare = async () => { const result = await update('self_share'); const shareUrl = `${result.shareUrl}?v=${result.shareVersion}`; setBrand(result.milestone); return { shareUrl, text: `${postCopy}\n\n${shareUrl}` }; }; const shareSelf = async (network: 'x' | 'linkedin') => { if (!milestone) return; // LinkedIn's public share dialog accepts only a URL. Start copying the // prepared commentary while this click still owns browser focus, then // open the LinkedIn share dialog after public-share consent is persisted. const linkedinCopy = network === 'linkedin' ? copyShareText(postCopy).then(() => true).catch(() => false) : Promise.resolve(true); // Open synchronously from the user gesture. Awaiting the API first can make // LinkedIn treat the new window as a blocked popup. const shareWindow = window.open('about:blank', '_blank'); if (shareWindow) shareWindow.opener = null; setSaving('self'); try { const { shareUrl, text } = await prepareSelfShare(); const copied = await linkedinCopy; const targetUrl = network === 'x' ? `https://x.com/intent/post?text=${encodeURIComponent(text)}` : `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(shareUrl)}`; if (shareWindow) shareWindow.location.href = targetUrl; else window.location.assign(targetUrl); showToast(network === 'linkedin' ? copied ? (milestone.language === 'de' ? 'LinkedIn-Text kopiert. Jetzt nur noch einfügen.' : 'LinkedIn text copied. Paste it into the composer.') : (milestone.language === 'de' ? 'LinkedIn ist geöffnet. Bitte nutze „Text kopieren“ im QR-Master-Fenster.' : 'LinkedIn is open. Please use “Copy text” in the QR Master window.') : 'X share composer opened.', copied ? 'success' : 'error'); } catch (error) { shareWindow?.close(); showToast(error instanceof Error ? error.message : 'Could not prepare this share', 'error'); } finally { setSaving(null); } }; const copyLinkedInText = async () => { setSaving('copy'); try { const { text } = await prepareSelfShare(); await copyShareText(text); showToast(milestone?.language === 'de' ? 'LinkedIn-Text kopiert.' : 'LinkedIn post text copied.', 'success'); } catch (error) { showToast(error instanceof Error ? error.message : 'Could not copy the LinkedIn text', 'error'); } finally { setSaving(null); } }; const approveBrand = async () => { setSaving('brand'); try { const result = await update('approve_brand'); setBrand(result.milestone); showToast(copy.queued, 'success'); } catch (error) { showToast(error instanceof Error ? error.message : 'Could not save your choice', 'error'); } finally { setSaving(null); } }; const dismiss = async (action: 'decline' | 'opt_out') => { try { await update(action); setMilestone(null); } catch (error) { showToast(error instanceof Error ? error.message : 'Could not save your choice', 'error'); } }; const optOut = () => { const message = milestone?.language === 'de' ? 'Meilenstein-Hinweise dauerhaft ausblenden? Du kannst sie später in den Einstellungen wieder aktivieren.' : 'Turn off milestone prompts? You can enable them again later in Settings.'; if (window.confirm(message)) void dismiss('opt_out'); }; if (!milestone) return null; const card = milestone.card; const count = card.totalUniqueScans || milestone.threshold; const status = brand?.brandStatus || milestone.brandStatus || 'pending'; const canApprove = ['pending', 'failed', 'revoked'].includes(status); return !open && setMilestone(null)}>
{copy.heading}{milestone.qrTitle} {copy.subtitle}
QR MASTERVerified scan milestone
UNIQUE SCANS
{count.toLocaleString(milestone.language === 'de' ? 'de-DE' : 'en-US')}
{milestone.language === 'de' ? 'SCANS INSGESAMT' : 'TOTAL SCANS'}
{(card.totalScans || count).toLocaleString(milestone.language === 'de' ? 'de-DE' : 'en-US')}
{card.trend ? :
{milestone.language === 'de' ? 'Noch keine Verlaufskurve verfügbar.' : 'No scan timeline available yet.'}
}
{card.trend &&
{milestone.language === 'de' ? 'Kumulierte eindeutige Scans' : 'Cumulative unique scans'}
}
{card.qrTitle}

{copy.consent}

{preview}
{withName && setXHandle(event.target.value)} disabled={saving !== null} maxLength={16} pattern="@?[A-Za-z0-9_]{1,15}" placeholder="@yourhandle" className="w-full rounded-md border border-slate-200 px-3 py-2 text-sm outline-none focus:border-[#0256ff] focus:ring-2 focus:ring-blue-100" />}
{copy.self}

{milestone.language === 'de' ? 'LinkedIn erlaubt kein automatisches Text-Vorausfüllen. Der Button kopiert den fertigen Text und öffnet den Beitrag.' : 'LinkedIn does not allow text prefill. The button copies the finished text and opens the composer.'}

{status !== 'pending' &&
{status === 'posted' ? copy.posted : status === 'failed' ? `${copy.failed}${brand?.brandPostError ? `: ${brand.brandPostError}` : ''}` : copy.queued}{brand?.brandPostUrl && View}
}
{status === 'pending' && }
; }