Copy overhaul + qr designs
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user