Copy overhaul + qr designs

This commit is contained in:
2026-07-27 17:54:59 +02:00
parent 033bc7e29d
commit 70d97aa970
144 changed files with 23107 additions and 1699 deletions

View File

@@ -2,38 +2,96 @@
import React, { useEffect, useRef } from 'react';
import Link from 'next/link';
import { X, Zap, BarChart2, RefreshCw, Palette } from 'lucide-react';
import { X } from 'lucide-react';
import { Button } from '@/components/ui/Button';
export type DownloadPopupVariant =
| 'url'
| 'googleReview'
| 'wifi'
| 'vcard'
| 'crypto'
| 'social'
| 'meeting';
interface PostDownloadPopupProps {
open: boolean;
onClose: () => void;
variant?: DownloadPopupVariant;
}
const BENEFITS = [
{ icon: RefreshCw, text: 'Edit the link anytime — QR stays the same' },
{ icon: BarChart2, text: 'See who scans, when & where' },
{ icon: Palette, text: 'Custom colors, logo & frames' },
{ icon: Zap, text: 'Free plan included — upgrade anytime for more' },
];
/**
* Copy notes.
*
* The old version led with "Your QR code is downloading!" - a status message the
* browser already shows - and buried the actual argument as the third of four
* equally weighted bullets. In a list where everything looks the same, nothing
* is important.
*
* This version opens on a fact about the file that just landed in the user's
* downloads folder. Not a warning, not a pitch: a consequence of what they did
* ten seconds ago. Then it concedes that a static code is the correct choice for
* a permanent link, which is what makes the following sentence credible rather
* than salesy.
*/
const HEADLINES: Record<DownloadPopupVariant, string> = {
url: 'This code now points at that URL forever.',
googleReview: 'This code now points at that Google profile forever.',
wifi: 'This code now carries that WiFi password forever.',
vcard: 'This code now carries those contact details forever.',
crypto: 'This code now carries that wallet address forever.',
social: 'This code now points at that profile forever.',
meeting: 'This code now points at that meeting link forever.',
};
const LS_KEY = 'qrm_download_popup_seen';
const CONCESSIONS: Record<DownloadPopupVariant, string> = {
url: 'For a permanent link, that is exactly right. If the destination ever changes, you need a new code and new printed material.',
googleReview:
'For a permanent profile, that is exactly right. If your review link ever changes, you need a new code and new printed material.',
wifi: 'Worth knowing before you print it: change the password and every printed copy stops working.',
vcard: 'For details that never change, that is exactly right. New number or new job title means a new code and new cards.',
crypto: 'For a wallet you keep, that is exactly right. Move wallets and every printed copy points at the old address.',
social: 'For a handle you keep, that is exactly right. Change the handle and every printed copy leads nowhere.',
meeting:
'Worth knowing before you print it: a recurring meeting link is fine, a one-off link expires with the meeting.',
};
const LS_KEY = 'qrm_download_popup_seen_at';
const REMIND_AFTER_DAYS = 30;
export function shouldShowDownloadPopup(): boolean {
try { return !localStorage.getItem(LS_KEY); } catch { return false; }
try {
const raw = localStorage.getItem(LS_KEY);
if (!raw) return true;
const seenAt = Number(raw);
if (!Number.isFinite(seenAt)) return true;
return Date.now() - seenAt > REMIND_AFTER_DAYS * 24 * 60 * 60 * 1000;
} catch {
return false;
}
}
export function markDownloadPopupSeen(): void {
try { localStorage.setItem(LS_KEY, '1'); } catch { /* ignore */ }
try {
localStorage.setItem(LS_KEY, String(Date.now()));
} catch {
/* ignore */
}
}
export default function PostDownloadPopup({ open, onClose }: PostDownloadPopupProps) {
export default function PostDownloadPopup({
open,
onClose,
variant = 'url',
}: PostDownloadPopupProps) {
const overlayRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!open) return;
markDownloadPopupSeen();
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
document.addEventListener('keydown', onKey);
return () => document.removeEventListener('keydown', onKey);
}, [open, onClose]);
@@ -45,52 +103,51 @@ export default function PostDownloadPopup({ open, onClose }: PostDownloadPopupPr
ref={overlayRef}
className="fixed inset-0 z-50 flex items-center justify-center p-4"
style={{ backgroundColor: 'rgba(15, 23, 42, 0.6)', backdropFilter: 'blur(4px)' }}
onClick={(e) => { if (e.target === overlayRef.current) onClose(); }}
onClick={(e) => {
if (e.target === overlayRef.current) onClose();
}}
role="dialog"
aria-modal="true"
aria-labelledby="download-popup-title"
>
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-md overflow-hidden animate-in fade-in zoom-in-95 duration-200">
{/* Header */}
<div className="relative bg-gradient-to-br from-[#4F46E5] to-[#7C3AED] p-6 text-white text-center">
<div className="w-full max-w-md overflow-hidden rounded-3xl bg-white shadow-2xl animate-in fade-in zoom-in-95 duration-200">
<div className="relative p-6">
<button
onClick={onClose}
className="absolute top-4 right-4 text-white/70 hover:text-white transition-colors"
className="absolute right-4 top-4 text-slate-400 transition-colors hover:text-slate-600"
aria-label="Close"
>
<X className="w-5 h-5" />
<X className="h-5 w-5" />
</button>
<div className="w-12 h-12 bg-white/20 rounded-2xl flex items-center justify-center mx-auto mb-3">
<Zap className="w-6 h-6 text-white" />
</div>
<h2 className="text-xl font-bold">Your QR code is downloading!</h2>
<p className="text-white/80 text-sm mt-1">
Want to make it smarter for free?
<h2
id="download-popup-title"
className="pr-8 text-xl font-bold leading-snug text-slate-900"
>
{HEADLINES[variant]}
</h2>
<p className="mt-3 text-base leading-relaxed text-slate-600">
{CONCESSIONS[variant]}
</p>
<p className="mt-3 text-base leading-relaxed text-slate-600">
A free account gives you 3 dynamic codes: same image, destination
editable any time, every scan counted.
</p>
</div>
{/* Benefits */}
<div className="p-6 space-y-3">
{BENEFITS.map(({ icon: Icon, text }) => (
<div key={text} className="flex items-center gap-3">
<div className="w-8 h-8 rounded-xl bg-indigo-50 flex items-center justify-center shrink-0">
<Icon className="w-4 h-4 text-[#4F46E5]" />
</div>
<span className="text-sm text-slate-700">{text}</span>
</div>
))}
</div>
{/* CTAs */}
<div className="px-6 pb-6 space-y-3">
<div className="space-y-3 px-6 pb-6">
<Link href="/signup" onClick={onClose} className="block">
<Button className="w-full bg-[#4F46E5] hover:bg-[#4338CA] text-white h-12 text-base font-semibold shadow-lg">
Create Free Account
<Button className="h-12 w-full bg-[#4F46E5] text-base font-semibold text-white shadow-lg hover:bg-[#4338CA]">
Create a free account - no card
</Button>
</Link>
<button
onClick={onClose}
className="w-full text-sm text-slate-400 hover:text-slate-600 transition-colors py-1"
className="w-full py-1 text-sm text-slate-400 transition-colors hover:text-slate-600"
>
No thanks, keep it static
No thanks, static is fine
</button>
</div>
</div>