Align milestone charts and self-share flow

This commit is contained in:
2026-08-14 13:04:09 +02:00
parent aa3b4d02ab
commit 8e34f97afb
4 changed files with 25 additions and 12 deletions

View File

@@ -21,7 +21,8 @@ function Trend({ trend }: { trend: NonNullable<Card['trend']> }) {
return `${x},${y}`;
}).join(' ');
const targetY = 88 - (trend.target / trend.ceiling) * 72;
return <div><svg viewBox="0 0 100 100" preserveAspectRatio="none" className="h-24 w-full overflow-visible" aria-label="Cumulative unique scan trend"><line x1="5" x2="95" y1={targetY} y2={targetY} stroke="#bfdbfe" strokeWidth="1" vectorEffect="non-scaling-stroke" /><polyline points={points} fill="none" stroke="currentColor" strokeWidth="3" vectorEffect="non-scaling-stroke" /><circle cx="95" cy={targetY} r="2.7" fill="#0256ff" /></svg><div className="mt-1 flex justify-between text-[11px] font-medium tracking-wide text-[#45617f]"><span>{trend.startLabel}</span><span className="text-[#0256ff]">{trend.target.toLocaleString()}</span><span>{trend.endLabel}</span></div></div>;
const ticks = Array.from({ length: 5 }, (_, index) => trend.target * (index + 1) / 4);
return <div className="grid grid-cols-[2.5rem_1fr] gap-2"><div className="flex h-24 flex-col justify-between py-1 text-right text-[10px] font-medium tabular-nums text-[#45617f]">{ticks.slice().reverse().map(tick => <span key={tick} className={tick === trend.target ? 'text-[#0256ff]' : undefined}>{tick.toLocaleString()}</span>)}</div><div><svg viewBox="0 0 100 100" preserveAspectRatio="none" className="h-24 w-full overflow-visible" aria-label="Cumulative unique scan trend">{ticks.map(tick => { const y = 88 - (tick / trend.ceiling) * 72; return <line key={tick} x1="5" x2="95" y1={y} y2={y} stroke={tick === trend.target ? '#bfdbfe' : '#e2e8f0'} strokeWidth={tick === trend.target ? '1.4' : '0.8'} vectorEffect="non-scaling-stroke" />; })}<polyline points={points} fill="none" stroke="currentColor" strokeWidth="3" vectorEffect="non-scaling-stroke" /><circle cx="95" cy={targetY} r="2.7" fill="#0256ff" /></svg><div className="mt-1 flex justify-between text-[11px] font-medium tracking-wide text-[#45617f]"><span>{trend.startLabel}</span><span>{trend.endLabel}</span></div></div></div>;
}
export function SocialMilestoneDialog() {
@@ -70,19 +71,22 @@ export function SocialMilestoneDialog() {
};
const shareSelf = async (network: 'x' | 'linkedin') => {
if (!milestone) return;
// 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('', '_blank', 'noopener,noreferrer');
setSaving('self');
try {
const result = await update('self_share');
const shareUrl = `${window.location.origin}/s/m/${result.shareToken}`;
const text = `${preview} ${shareUrl}`;
if (network === 'x') window.open(`https://x.com/intent/post?text=${encodeURIComponent(text)}`, '_blank', 'noopener,noreferrer');
if (network === 'x') shareWindow?.location.replace(`https://x.com/intent/post?text=${encodeURIComponent(text)}`);
else {
await navigator.clipboard?.writeText(text);
window.open(`https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(shareUrl)}`, '_blank', 'noopener,noreferrer');
shareWindow?.location.replace(`https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(shareUrl)}`);
}
setBrand(result.milestone);
showToast(network === 'linkedin' ? 'Share text copied and LinkedIn opened.' : 'X share composer opened.', 'success');
} catch (error) { showToast(error instanceof Error ? error.message : 'Could not prepare this share', 'error'); }
} catch (error) { shareWindow?.close(); showToast(error instanceof Error ? error.message : 'Could not prepare this share', 'error'); }
finally { setSaving(null); }
};
const approveBrand = async () => {