Fix milestone sharing previews and publisher recovery
This commit is contained in:
@@ -70,10 +70,11 @@ export function SocialMilestoneDialog() {
|
||||
|
||||
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: 'Publishing to X …', posted: 'Published on X', failed: 'Publishing failed' };
|
||||
: { 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 preview = useMemo(() => {
|
||||
if (!milestone || !withName || !xHandle.trim()) return milestone?.preview || '';
|
||||
return `${milestone.preview} By @${xHandle.trim().replace(/^@/, '')}.`;
|
||||
const mention = milestone.language === 'de' ? 'Glückwunsch' : 'Congratulations';
|
||||
return `${milestone.preview}\n\n${mention} @${xHandle.trim().replace(/^@/, '')}.`;
|
||||
}, [milestone, withName, xHandle]);
|
||||
const update = async (action: 'approve_brand' | 'self_share' | 'decline' | 'opt_out') => {
|
||||
if (!milestone) return null;
|
||||
@@ -91,22 +92,15 @@ export function SocialMilestoneDialog() {
|
||||
setSaving('self');
|
||||
try {
|
||||
const result = await update('self_share');
|
||||
const shareUrl = `${window.location.origin}/s/m/${result.shareToken}`;
|
||||
const text = `${preview} ${shareUrl}`;
|
||||
const shareUrl = `${window.location.origin}/s/m/${result.shareToken}?v=${result.shareVersion}`;
|
||||
const text = `${preview}\n\n${shareUrl}`;
|
||||
const targetUrl = network === 'x'
|
||||
? `https://x.com/intent/post?text=${encodeURIComponent(text)}`
|
||||
: `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(shareUrl)}`;
|
||||
if (network === 'x') {
|
||||
if (shareWindow) shareWindow.location.href = targetUrl;
|
||||
else window.location.assign(targetUrl);
|
||||
}
|
||||
else {
|
||||
await navigator.clipboard?.writeText(text);
|
||||
if (shareWindow) shareWindow.location.href = targetUrl;
|
||||
else window.location.assign(targetUrl);
|
||||
}
|
||||
if (shareWindow) shareWindow.location.href = targetUrl;
|
||||
else window.location.assign(targetUrl);
|
||||
setBrand(result.milestone);
|
||||
showToast(network === 'linkedin' ? 'Share text copied and LinkedIn opened.' : 'X share composer opened.', 'success');
|
||||
showToast(network === 'linkedin' ? 'LinkedIn share window opened.' : 'X share composer opened.', 'success');
|
||||
} catch (error) { shareWindow?.close(); showToast(error instanceof Error ? error.message : 'Could not prepare this share', 'error'); }
|
||||
finally { setSaving(null); }
|
||||
};
|
||||
@@ -122,11 +116,18 @@ export function SocialMilestoneDialog() {
|
||||
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 <Dialog open onOpenChange={open => !open && setMilestone(null)}>
|
||||
<DialogContent className="flex max-h-[calc(100dvh-2rem)] max-w-xl flex-col overflow-hidden border-slate-200 p-0 shadow-[0_30px_45px_-30px_rgba(50,50,93,0.45)]">
|
||||
<div className="shrink-0 px-6 pb-4 pt-5"><DialogHeader><div className="mb-3 flex h-9 w-9 items-center justify-center rounded-lg bg-[#eaf1ff] text-[#0256ff]"><Sparkles className="h-4 w-4" /></div><DialogTitle className="text-2xl font-semibold tracking-[-0.03em] text-[#061b31]">{copy.heading}</DialogTitle><DialogDescription className="pt-1 text-sm leading-6 text-[#4b5e76]"><strong className="font-medium text-[#061b31]">{milestone.qrTitle}</strong> {copy.subtitle}</DialogDescription></DialogHeader></div>
|
||||
@@ -137,13 +138,13 @@ export function SocialMilestoneDialog() {
|
||||
{card.trend && <div className="mt-3 flex items-center justify-end gap-2 text-[11px] text-[#45617f]"><LineChart className="h-3.5 w-3.5 text-[#0256ff]" />{milestone.language === 'de' ? 'Kumulierte eindeutige Scans' : 'Cumulative unique scans'}</div>}
|
||||
<div className="mt-5 border-t border-slate-100 pt-3 text-sm font-medium text-[#061b31]">{card.qrTitle}</div>
|
||||
</section>
|
||||
<div><p className="text-sm leading-6 text-[#4b5e76]">{copy.consent}</p><blockquote className="mt-3 border-l-2 border-[#0256ff] pl-3 text-sm leading-6 text-[#273951]">{preview}</blockquote></div>
|
||||
<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)} disabled={status !== 'pending'} className="h-4 w-4 rounded border-slate-300 text-[#0256ff] focus:ring-[#0256ff]" />{copy.name}</label>
|
||||
{withName && <input aria-label="X handle" value={xHandle} onChange={event => setXHandle(event.target.value)} disabled={status !== 'pending'} 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" />}
|
||||
<div><p className="text-sm leading-6 text-[#4b5e76]">{copy.consent}</p><blockquote className="mt-3 whitespace-pre-line border-l border-[#0256ff] pl-3 text-sm leading-6 text-[#273951]">{preview}</blockquote></div>
|
||||
<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)} disabled={!canApprove} className="h-4 w-4 rounded border-slate-300 text-[#0256ff] focus:ring-[#0256ff]" />{copy.name}</label>
|
||||
{withName && <input aria-label="X handle" value={xHandle} onChange={event => setXHandle(event.target.value)} disabled={!canApprove} 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" />}
|
||||
<div className="flex flex-wrap items-center gap-2"><span className="mr-1 text-xs font-medium text-slate-500">{copy.self}</span><Button variant="outline" size="sm" onClick={() => shareSelf('x')} disabled={saving !== null}><X className="mr-1.5 h-3.5 w-3.5" />X</Button><Button variant="outline" size="sm" onClick={() => shareSelf('linkedin')} disabled={saving !== null}><Linkedin className="mr-1.5 h-3.5 w-3.5" />LinkedIn</Button></div>
|
||||
{status !== 'pending' && <div className={`flex items-center justify-between rounded-md px-3 py-2 text-sm ${status === 'posted' ? 'bg-emerald-50 text-emerald-800' : status === 'failed' ? 'bg-rose-50 text-rose-800' : 'bg-blue-50 text-blue-800'}`}><span>{status === 'posted' ? copy.posted : status === 'failed' ? copy.failed : copy.queued}</span>{brand?.brandPostUrl && <a href={brand.brandPostUrl} target="_blank" rel="noreferrer" className="inline-flex items-center gap-1 font-medium underline"><ExternalLink className="h-3.5 w-3.5" />View</a>}</div>}
|
||||
{status !== 'pending' && <div className={`flex items-start justify-between gap-3 rounded-md px-3 py-2 text-sm ${status === 'posted' ? 'bg-emerald-50 text-emerald-800' : status === 'failed' ? 'bg-rose-50 text-rose-800' : 'bg-blue-50 text-blue-800'}`}><span>{status === 'posted' ? copy.posted : status === 'failed' ? `${copy.failed}${brand?.brandPostError ? `: ${brand.brandPostError}` : ''}` : copy.queued}</span>{brand?.brandPostUrl && <a href={brand.brandPostUrl} target="_blank" rel="noreferrer" className="inline-flex shrink-0 items-center gap-1 font-medium underline"><ExternalLink className="h-3.5 w-3.5" />View</a>}</div>}
|
||||
</div>
|
||||
<DialogFooter className="shrink-0 bg-slate-50 px-6 py-4"><div className="flex w-full flex-wrap items-center justify-end gap-2"><Button variant="outline" onClick={() => dismiss('decline')} disabled={saving !== null}>{copy.decline}</Button><Button variant="primary" onClick={approveBrand} disabled={saving !== null || status !== 'pending'}><Send className="mr-1.5 h-4 w-4" />{status === 'approved' || status === 'processing' ? copy.queued : 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={() => dismiss('opt_out')} disabled={saving !== null}>Do not show again</button></div></DialogFooter>
|
||||
<DialogFooter className="shrink-0 bg-slate-50 px-6 py-4"><div className="flex w-full flex-wrap items-center justify-end gap-2"><Button variant="outline" onClick={() => dismiss('decline')} disabled={saving !== null}>{copy.decline}</Button><Button variant="primary" onClick={approveBrand} disabled={saving !== null || !canApprove}><Send className="mr-1.5 h-4 w-4" />{status === 'approved' || status === 'processing' ? copy.queued : status === 'failed' ? (milestone.language === 'de' ? 'Erneut versuchen' : 'Retry post') : 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={optOut} disabled={saving !== null}>{milestone.language === 'de' ? 'Nicht mehr anzeigen' : 'Do not show again'}</button></div></DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user