6 Commits

26 changed files with 516 additions and 1 deletions

View File

@@ -11,6 +11,7 @@ import { showToast } from '@/components/ui/Toast';
import { cn } from '@/lib/utils';
import { toPng, toSvg, toBlob } from 'html-to-image';
import { trackEvent } from '@/components/PostHogProvider';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
// Brand Colors
const BRAND = {
@@ -54,6 +55,7 @@ export default function BarcodeGeneratorClient() {
const [lineColor, setLineColor] = useState('#000000');
const [frameType, setFrameType] = useState('none');
const [error, setError] = useState<string | null>(null);
const [showPopup, setShowPopup] = useState(false);
const barcodeRef = useRef<HTMLDivElement>(null);
@@ -109,6 +111,7 @@ export default function BarcodeGeneratorClient() {
document.body.removeChild(link);
showToast(`Barcode downloaded as ${extension.toUpperCase()}`, 'success');
if (shouldShowDownloadPopup()) setShowPopup(true);
trackEvent('barcode_downloaded', {
format: format,
extension: extension,
@@ -118,7 +121,6 @@ export default function BarcodeGeneratorClient() {
console.error('Download failed', err);
showToast('Download failed', 'error');
}
};
const copyBarcode = async () => {
if (!barcodeRef.current) return;
@@ -160,6 +162,8 @@ export default function BarcodeGeneratorClient() {
];
return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */}
@@ -453,5 +457,6 @@ export default function BarcodeGeneratorClient() {
</Link>
</div>
</div>
</>
);
}

View File

@@ -13,6 +13,7 @@ import {
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { cn } from '@/lib/utils';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
// Brand Colors
const BRAND = {
@@ -45,6 +46,7 @@ export default function PhoneGenerator() {
const [phone, setPhone] = useState('');
const [qrColor, setQrColor] = useState(BRAND.richBlue);
const [frameType, setFrameType] = useState('none');
const [showPopup, setShowPopup] = useState(false);
const qrRef = useRef<HTMLDivElement>(null);
@@ -74,6 +76,7 @@ export default function PhoneGenerator() {
} catch (err) {
console.error('Download failed', err);
}
if (shouldShowDownloadPopup()) setShowPopup(true);
};
const getFrameLabel = () => {
@@ -82,6 +85,8 @@ export default function PhoneGenerator() {
};
return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */}
@@ -243,5 +248,6 @@ export default function PhoneGenerator() {
</Link>
</div>
</div>
</>
);
}

View File

@@ -15,6 +15,7 @@ import { Input } from '@/components/ui/Input';
import { Select } from '@/components/ui/Select';
import { cn } from '@/lib/utils';
import AdBanner from '@/components/ads/AdBanner';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
// Brand Colors
const BRAND = {
@@ -58,6 +59,7 @@ export default function CryptoGenerator() {
const [qrMode, setQrMode] = useState<'universal' | 'wallet'>('universal');
const [qrColor, setQrColor] = useState('#F7931A');
const [frameType, setFrameType] = useState('none');
const [showPopup, setShowPopup] = useState(false);
const qrRef = useRef<HTMLDivElement>(null);
@@ -123,6 +125,7 @@ export default function CryptoGenerator() {
} catch (err) {
console.error('Download failed', err);
}
if (shouldShowDownloadPopup()) setShowPopup(true);
};
const getFrameLabel = () => {
@@ -131,6 +134,8 @@ export default function CryptoGenerator() {
};
return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */}
@@ -370,5 +375,6 @@ export default function CryptoGenerator() {
</Link>
</div>
</div>
</>
);
}

View File

@@ -14,6 +14,7 @@ import {
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { cn } from '@/lib/utils';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
// Brand Colors
const BRAND = {
@@ -43,6 +44,7 @@ const FRAME_OPTIONS = [
export default function EmailGenerator() {
const [formData, setFormData] = useState({
const [showPopup, setShowPopup] = useState(false);
email: '',
subject: '',
body: ''
@@ -88,6 +90,7 @@ export default function EmailGenerator() {
} catch (err) {
console.error('Download failed', err);
}
if (shouldShowDownloadPopup()) setShowPopup(true);
};
const getFrameLabel = () => {
@@ -100,6 +103,8 @@ export default function EmailGenerator() {
};
return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */}
@@ -292,5 +297,6 @@ export default function EmailGenerator() {
</Link>
</div>
</div>
</>
);
}

View File

@@ -15,6 +15,7 @@ import {
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { cn } from '@/lib/utils';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
// Brand Colors
const BRAND = {
@@ -51,6 +52,7 @@ export default function EventGenerator() {
const [qrColor, setQrColor] = useState(BRAND.primary);
const [frameType, setFrameType] = useState('none');
const [showPopup, setShowPopup] = useState(false);
const qrRef = useRef<HTMLDivElement>(null);
@@ -102,6 +104,7 @@ export default function EventGenerator() {
} catch (err) {
console.error('Download failed', err);
}
if (shouldShowDownloadPopup()) setShowPopup(true);
};
const getFrameLabel = () => {
@@ -110,6 +113,8 @@ export default function EventGenerator() {
};
return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */}
@@ -326,5 +331,6 @@ export default function EventGenerator() {
</Link>
</div>
</div>
</>
);
}

View File

@@ -14,6 +14,7 @@ import {
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { cn } from '@/lib/utils';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
// Brand Colors
const BRAND = {
@@ -46,6 +47,7 @@ export default function FacebookGenerator() {
const [url, setUrl] = useState('');
const [qrColor, setQrColor] = useState('#1877F2'); // Default to FB Blue
const [frameType, setFrameType] = useState('none');
const [showPopup, setShowPopup] = useState(false);
const qrRef = useRef<HTMLDivElement>(null);
@@ -73,6 +75,7 @@ export default function FacebookGenerator() {
} catch (err) {
console.error('Download failed', err);
}
if (shouldShowDownloadPopup()) setShowPopup(true);
};
const getFrameLabel = () => {
@@ -81,6 +84,8 @@ export default function FacebookGenerator() {
};
return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */}
@@ -243,5 +248,6 @@ export default function FacebookGenerator() {
</Link>
</div>
</div>
</>
);
}

View File

@@ -14,6 +14,7 @@ import {
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { cn } from '@/lib/utils';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
// Brand Colors
const BRAND = {
@@ -46,6 +47,7 @@ export default function GeolocationGenerator() {
const [longitude, setLongitude] = useState('');
const [qrColor, setQrColor] = useState(BRAND.primary);
const [frameType, setFrameType] = useState('none');
const [showPopup, setShowPopup] = useState(false);
const qrRef = useRef<HTMLDivElement>(null);
@@ -76,6 +78,7 @@ export default function GeolocationGenerator() {
} catch (err) {
console.error('Download failed', err);
}
if (shouldShowDownloadPopup()) setShowPopup(true);
};
const getFrameLabel = () => {
@@ -101,6 +104,8 @@ export default function GeolocationGenerator() {
};
return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */}
@@ -288,5 +293,6 @@ export default function GeolocationGenerator() {
</Link>
</div>
</div>
</>
);
}

View File

@@ -6,6 +6,7 @@ import { toPng } from 'html-to-image';
import { Star, Download, AlertCircle } from 'lucide-react';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
const QR_COLORS = [
{ name: 'Google Blue', value: '#1A73E8' },
@@ -41,6 +42,7 @@ export default function GoogleReviewGenerator() {
const [qrColor, setQrColor] = useState('#1A73E8');
const [frameType, setFrameType] = useState('review');
const [error, setError] = useState('');
const [showPopup, setShowPopup] = useState(false);
const qrRef = useRef<HTMLDivElement>(null);
@@ -76,12 +78,15 @@ export default function GoogleReviewGenerator() {
} catch (err) {
console.error('Download failed', err);
}
if (shouldShowDownloadPopup()) setShowPopup(true);
};
const frameLabel = FRAME_OPTIONS.find(f => f.id === frameType && f.id !== 'none')?.label ?? null;
const isReady = reviewUrl && !error && isValidGoogleReviewLink(reviewUrl);
return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
<div className="bg-white rounded-3xl shadow-2xl shadow-slate-900/10 overflow-hidden border border-slate-100">
<div className="grid lg:grid-cols-2">
@@ -209,5 +214,6 @@ export default function GoogleReviewGenerator() {
</div>
</div>
</div>
</>
);
}

View File

@@ -13,6 +13,7 @@ import {
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { cn } from '@/lib/utils';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
// Brand Colors
const BRAND = {
@@ -45,6 +46,7 @@ export default function InstagramGenerator() {
const [username, setUsername] = useState('');
const [qrColor, setQrColor] = useState('#E1306C');
const [frameType, setFrameType] = useState('none');
const [showPopup, setShowPopup] = useState(false);
const qrRef = useRef<HTMLDivElement>(null);
@@ -78,6 +80,7 @@ export default function InstagramGenerator() {
} catch (err) {
console.error('Download failed', err);
}
if (shouldShowDownloadPopup()) setShowPopup(true);
};
const getFrameLabel = () => {
@@ -86,6 +89,8 @@ export default function InstagramGenerator() {
};
return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */}
@@ -248,5 +253,6 @@ export default function InstagramGenerator() {
</Link>
</div>
</div>
</>
);
}

View File

@@ -14,6 +14,7 @@ import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { Select } from '@/components/ui/Select';
import { cn } from '@/lib/utils';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
// Brand Colors - PayPal Blue
const BRAND = {
@@ -64,6 +65,7 @@ export default function PayPalGenerator() {
const [currency, setCurrency] = useState('EUR');
const [qrColor, setQrColor] = useState(BRAND.primary);
const [frameType, setFrameType] = useState('none');
const [showPopup, setShowPopup] = useState(false);
const qrRef = useRef<HTMLDivElement>(null);
@@ -114,6 +116,7 @@ export default function PayPalGenerator() {
} catch (err) {
console.error('Download failed', err);
}
if (shouldShowDownloadPopup()) setShowPopup(true);
};
const getFrameLabel = () => {
@@ -122,6 +125,8 @@ export default function PayPalGenerator() {
};
return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */}
@@ -338,5 +343,6 @@ export default function PayPalGenerator() {
</Link>
</div>
</div>
</>
);
}

View File

@@ -13,6 +13,7 @@ import {
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { cn } from '@/lib/utils';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
// Brand Colors
const BRAND = {
@@ -46,6 +47,7 @@ export default function SMSGenerator() {
const [message, setMessage] = useState('');
const [qrColor, setQrColor] = useState(BRAND.primary);
const [frameType, setFrameType] = useState('none');
const [showPopup, setShowPopup] = useState(false);
const qrRef = useRef<HTMLDivElement>(null);
@@ -76,6 +78,7 @@ export default function SMSGenerator() {
} catch (err) {
console.error('Download failed', err);
}
if (shouldShowDownloadPopup()) setShowPopup(true);
};
const getFrameLabel = () => {
@@ -84,6 +87,8 @@ export default function SMSGenerator() {
};
return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */}
@@ -262,5 +267,6 @@ export default function SMSGenerator() {
</Link>
</div>
</div>
</>
);
}

View File

@@ -14,6 +14,7 @@ import {
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { cn } from '@/lib/utils';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
// Brand Colors - Microsoft Teams Purple
const BRAND = {
@@ -53,6 +54,7 @@ export default function TeamsGenerator() {
const [userEmail, setUserEmail] = useState('');
const [qrColor, setQrColor] = useState(BRAND.primary);
const [frameType, setFrameType] = useState('none');
const [showPopup, setShowPopup] = useState(false);
const qrRef = useRef<HTMLDivElement>(null);
@@ -96,6 +98,7 @@ export default function TeamsGenerator() {
} catch (err) {
console.error('Download failed', err);
}
if (shouldShowDownloadPopup()) setShowPopup(true);
};
const getFrameLabel = () => {
@@ -104,6 +107,8 @@ export default function TeamsGenerator() {
};
return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */}
@@ -314,5 +319,6 @@ export default function TeamsGenerator() {
</Link>
</div>
</div>
</>
);
}

View File

@@ -13,6 +13,7 @@ import {
} from 'lucide-react';
import { Button } from '@/components/ui/Button';
import { cn } from '@/lib/utils';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
// Brand Colors
const BRAND = {
@@ -45,6 +46,7 @@ export default function TextGenerator() {
const [text, setText] = useState('');
const [qrColor, setQrColor] = useState(BRAND.richBlue);
const [frameType, setFrameType] = useState('none');
const [showPopup, setShowPopup] = useState(false);
const qrRef = useRef<HTMLDivElement>(null);
@@ -72,6 +74,7 @@ export default function TextGenerator() {
} catch (err) {
console.error('Download failed', err);
}
if (shouldShowDownloadPopup()) setShowPopup(true);
};
const getFrameLabel = () => {
@@ -80,6 +83,8 @@ export default function TextGenerator() {
};
return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */}
@@ -241,5 +246,6 @@ export default function TextGenerator() {
</Link>
</div>
</div>
</>
);
}

View File

@@ -14,6 +14,7 @@ import {
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { cn } from '@/lib/utils';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
// Brand Colors
const BRAND = {
@@ -45,6 +46,7 @@ export default function TiktokGenerator() {
const [username, setUsername] = useState('');
const [qrColor, setQrColor] = useState('#000000');
const [frameType, setFrameType] = useState('none');
const [showPopup, setShowPopup] = useState(false);
const qrRef = useRef<HTMLDivElement>(null);
@@ -78,6 +80,7 @@ export default function TiktokGenerator() {
} catch (err) {
console.error('Download failed', err);
}
if (shouldShowDownloadPopup()) setShowPopup(true);
};
const getFrameLabel = () => {
@@ -86,6 +89,8 @@ export default function TiktokGenerator() {
};
return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */}
@@ -248,5 +253,6 @@ export default function TiktokGenerator() {
</Link>
</div>
</div>
</>
);
}

View File

@@ -13,6 +13,7 @@ import {
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { cn } from '@/lib/utils';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
// Brand Colors
const BRAND = {
@@ -45,6 +46,7 @@ export default function TwitterGenerator() {
const [username, setUsername] = useState('');
const [qrColor, setQrColor] = useState('#000000');
const [frameType, setFrameType] = useState('none');
const [showPopup, setShowPopup] = useState(false);
const qrRef = useRef<HTMLDivElement>(null);
@@ -78,6 +80,7 @@ export default function TwitterGenerator() {
} catch (err) {
console.error('Download failed', err);
}
if (shouldShowDownloadPopup()) setShowPopup(true);
};
const getFrameLabel = () => {
@@ -86,6 +89,8 @@ export default function TwitterGenerator() {
};
return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */}
@@ -248,5 +253,6 @@ export default function TwitterGenerator() {
</Link>
</div>
</div>
</>
);
}

View File

@@ -15,6 +15,7 @@ import {
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { cn } from '@/lib/utils';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
// Brand Colors
const BRAND = {
@@ -46,6 +47,7 @@ export default function URLGenerator() {
const [url, setUrl] = useState('');
const [qrColor, setQrColor] = useState(BRAND.primary);
const [frameType, setFrameType] = useState('none');
const [showPopup, setShowPopup] = useState(false);
const qrRef = useRef<HTMLDivElement>(null);
@@ -72,6 +74,7 @@ export default function URLGenerator() {
} catch (err) {
console.error('Download failed', err);
}
if (shouldShowDownloadPopup()) setShowPopup(true);
};
const getFrameLabel = () => {
@@ -80,6 +83,8 @@ export default function URLGenerator() {
};
return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */}
@@ -241,5 +246,6 @@ export default function URLGenerator() {
</Link>
</div>
</div>
</>
);
}

View File

@@ -18,6 +18,7 @@ import {
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { cn } from '@/lib/utils';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
// Brand Colors
const BRAND = {
@@ -63,6 +64,7 @@ export default function VCardGenerator() {
const [qrColor, setQrColor] = useState(BRAND.primary);
const [frameType, setFrameType] = useState('none');
const [showPopup, setShowPopup] = useState(false);
const qrRef = useRef<HTMLDivElement>(null);
@@ -108,6 +110,7 @@ export default function VCardGenerator() {
} catch (err) {
console.error('Download failed', err);
}
if (shouldShowDownloadPopup()) setShowPopup(true);
};
const getFrameLabel = () => {
@@ -116,6 +119,8 @@ export default function VCardGenerator() {
};
return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-6xl mx-auto px-4 md:px-6">
{/* Main Generator Card */}
@@ -344,5 +349,6 @@ export default function VCardGenerator() {
</Link>
</div>
</div>
</>
);
}

View File

@@ -15,6 +15,7 @@ import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { cn } from '@/lib/utils';
import { Textarea } from '@/components/ui/Textarea';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
@@ -49,6 +50,7 @@ export default function WhatsappGenerator() {
const [message, setMessage] = useState('');
const [qrColor, setQrColor] = useState('#25D366');
const [frameType, setFrameType] = useState('none');
const [showPopup, setShowPopup] = useState(false);
const qrRef = useRef<HTMLDivElement>(null);
@@ -83,6 +85,7 @@ export default function WhatsappGenerator() {
} catch (err) {
console.error('Download failed', err);
}
if (shouldShowDownloadPopup()) setShowPopup(true);
};
const getFrameLabel = () => {
@@ -91,6 +94,8 @@ export default function WhatsappGenerator() {
};
return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */}
@@ -263,5 +268,6 @@ export default function WhatsappGenerator() {
</Link>
</div>
</div>
</>
);
}

View File

@@ -17,6 +17,7 @@ import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { Select } from '@/components/ui/Select';
import { cn } from '@/lib/utils';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
// Brand Colors
const BRAND = {
@@ -55,6 +56,7 @@ export default function WiFiGenerator() {
// Customization
const [qrColor, setQrColor] = useState(BRAND.primary);
const [frameType, setFrameType] = useState('none');
const [showPopup, setShowPopup] = useState(false);
const qrRef = useRef<HTMLDivElement>(null);
@@ -84,6 +86,7 @@ export default function WiFiGenerator() {
} catch (err) {
console.error('Download failed', err);
}
if (shouldShowDownloadPopup()) setShowPopup(true);
};
const getFrameLabel = () => {
@@ -92,6 +95,8 @@ export default function WiFiGenerator() {
};
return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */}
@@ -303,5 +308,6 @@ export default function WiFiGenerator() {
</Link>
</div>
</div>
</>
);
}

View File

@@ -13,6 +13,7 @@ import {
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { cn } from '@/lib/utils';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
// Brand Colors
const BRAND = {
@@ -44,6 +45,7 @@ export default function YoutubeGenerator() {
const [url, setUrl] = useState('');
const [qrColor, setQrColor] = useState('#FF0000');
const [frameType, setFrameType] = useState('none');
const [showPopup, setShowPopup] = useState(false);
const qrRef = useRef<HTMLDivElement>(null);
@@ -71,6 +73,7 @@ export default function YoutubeGenerator() {
} catch (err) {
console.error('Download failed', err);
}
if (shouldShowDownloadPopup()) setShowPopup(true);
};
const getFrameLabel = () => {
@@ -79,6 +82,8 @@ export default function YoutubeGenerator() {
};
return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */}
@@ -241,5 +246,6 @@ export default function YoutubeGenerator() {
</Link>
</div>
</div>
</>
);
}

View File

@@ -13,6 +13,7 @@ import {
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { cn } from '@/lib/utils';
import PostDownloadPopup, { shouldShowDownloadPopup } from '@/components/marketing/PostDownloadPopup';
// Brand Colors - Zoom Blue
const BRAND = {
@@ -46,6 +47,7 @@ export default function ZoomGenerator() {
const [useDirectLink, setUseDirectLink] = useState(false); // Default to web URL for compatibility
const [qrColor, setQrColor] = useState(BRAND.primary);
const [frameType, setFrameType] = useState('none');
const [showPopup, setShowPopup] = useState(false);
const qrRef = useRef<HTMLDivElement>(null);
@@ -103,6 +105,7 @@ export default function ZoomGenerator() {
} catch (err) {
console.error('Download failed', err);
}
if (shouldShowDownloadPopup()) setShowPopup(true);
};
const getFrameLabel = () => {
@@ -111,6 +114,8 @@ export default function ZoomGenerator() {
};
return (
<>
<PostDownloadPopup open={showPopup} onClose={() => setShowPopup(false)} />
<div className="w-full max-w-5xl mx-auto px-4 md:px-6">
{/* Main Generator Card */}
@@ -298,5 +303,6 @@ export default function ZoomGenerator() {
</Link>
</div>
</div>
</>
);
}

View File

@@ -0,0 +1,99 @@
'use client';
import React, { useEffect, useRef } from 'react';
import Link from 'next/link';
import { X, Zap, BarChart2, RefreshCw, Palette } from 'lucide-react';
import { Button } from '@/components/ui/Button';
interface PostDownloadPopupProps {
open: boolean;
onClose: () => void;
}
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' },
];
const LS_KEY = 'qrm_download_popup_seen';
export function shouldShowDownloadPopup(): boolean {
try { return !localStorage.getItem(LS_KEY); } catch { return false; }
}
export function markDownloadPopupSeen(): void {
try { localStorage.setItem(LS_KEY, '1'); } catch { /* ignore */ }
}
export default function PostDownloadPopup({ open, onClose }: PostDownloadPopupProps) {
const overlayRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!open) return;
markDownloadPopupSeen();
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
document.addEventListener('keydown', onKey);
return () => document.removeEventListener('keydown', onKey);
}, [open, onClose]);
if (!open) return null;
return (
<div
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(); }}
>
<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">
<button
onClick={onClose}
className="absolute top-4 right-4 text-white/70 hover:text-white transition-colors"
aria-label="Close"
>
<X className="w-5 h-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?
</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">
<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>
</Link>
<button
onClick={onClose}
className="w-full text-sm text-slate-400 hover:text-slate-600 transition-colors py-1"
>
No thanks, keep it static
</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,116 @@
# QR Master Leads Agent — Instructions
You are a growth agent for QR Master (https://qrmaster.net), a SaaS for dynamic QR code creation, tracking, and analytics. Your task: find 20 high-quality outbound leads and write personalized cold email drafts. Save them to the repo for human review — do not send any emails.
---
## Step 1 — Derive ICPs from keywords
Read `seo-keywords.csv` from the repo root. Parse all rows. Sort by `CPC_High_EUR` descending. Identify the highest-value ICPs:
- **C_Analytics** (CPC EUR 1634): Marketing Managers, CMOs, Digital Marketing Directors at SMBs (10200 employees) who run campaigns and need QR scan tracking and analytics
- **S3_Commercial** (CPC EUR 25): Marketing Directors, Operations Managers at retail and ecommerce businesses
- **D_Restaurant** (CPC EUR 12): Restaurant Owners, F&B Managers, Hospitality Managers at restaurants, cafes, hotels
- **B_Bulk**: Print Shop Owners, Agency Owners who generate QR codes in bulk for clients
- **A_Dynamic** (high volume): Anyone actively searching for dynamic or editable QR solutions
---
## Step 2 — Find 20 leads
Use both sources below. Deduplicate by email address across both sources. Aim for 20 unique leads total.
### Geographic priority
- **Primary (60%)**: English-speaking markets — US, UK, Australia, Canada, Ireland
- **Secondary (40%)**: EU — Germany, Netherlands, France, Spain, Sweden, Denmark, Belgium
### Source A — Vibe Prospecting
Search the markets above with these filters:
- Industries: restaurants/hospitality, marketing agencies, retail, print/design, events
- Company size: 5200 employees
- Target titles: Owner, Marketing Manager, CMO, Digital Manager, Operations Manager
### Source B — Apollo.io
Search contacts by job title + industry combinations matching each ICP above.
- Verified email addresses only
- Enrich with company website, size, and industry details
**Each lead must have:** first name, last name, email, company name, industry, job title, country, ICP segment, source (Vibe/Apollo).
---
## Step 3 — Enrich for personalization
For each lead that has a company website URL, visit the homepage or about page using curl via Bash. Extract 12 specific details (product focus, tagline, recent launch) to use in the email opening line.
---
## Step 4 — Write personalized cold emails
For each lead, write one cold email:
- **Subject line**: specific and curiosity-driven, max 8 words, zero spam trigger words
- **Opening**: reference the scraped website detail or a specific industry fact (never generic)
- **Pain point** matched to ICP segment:
- Restaurant: customers still googling the menu instead of scanning a QR
- Marketing manager: no way to know which QR code drove conversions vs which was dead weight
- Print shop: clients calling because their QR stopped working after the reprint
- Agency/bulk: spending hours regenerating codes every time a client URL changes
- **Value prop**: QR Master = dynamic QR codes editable after printing + real-time scan analytics per device, country, and time
- **CTA**: single ask — try free at https://qrmaster.net, no credit card needed
- **Tone**: professional but human, max 150 words, no buzzwords, no "I hope this email finds you well"
- **Sign-off**: Timo from QR Master (timo@qrmaster.net)
---
## Step 5 — Save the draft file
1. Get today's date: run `date +%Y-%m-%d` and use as DATE
2. Create `tmp/leads/` if it does not exist
3. Write the file `tmp/leads/DATE-leads.md` with this exact structure:
```
# QR Master Lead Outreach — DATE
**Status: DRAFT — awaiting review**
## Lead Table
| # | Name | Company | Email | Segment | Source | Country |
|---|------|---------|-------|---------|--------|---------|
| 1 | ... | ... | ... | ... | ... | ... |
## Email Drafts
### Lead 1: Full Name — Company Name
**To:** email@address.com
**Subject:** Subject line here
Email body...
---
### Lead 2: ...
```
4. Commit: `git add tmp/leads/DATE-leads.md && git commit -m "leads: add outreach draft DATE"`
---
## Step 6 — Send review email
Send the draft file to timo@qrmaster.net so he can review it in his inbox.
SMTP: host=smtp.qrmaster.net, port=465, secure=true, user=timo@qrmaster.net, pass=fiesta.
From and To: timo@qrmaster.net.
Subject: [QR Master Leads] DATE — 20 new drafts ready for review.
Body: plain text with the full contents of the leads file pasted in.
Use nodemailer. Create tmp/send-review.mjs, run with node, then delete it.
If SMTP fails, skip silently — the committed file is the source of truth.
Output a final summary: leads found per source, file path, whether review email was sent.
---
## Critical rules
- **DO NOT send cold emails to leads.** Only save the file, commit, and send the review email to timo@qrmaster.net. Approval happens in Claude Code CLI.
- If fewer than 20 leads are found, include all you found with a note at the top of the file.
- Do not fabricate leads or email addresses. Only use real contacts from Vibe Prospecting and Apollo.io.

View File

@@ -0,0 +1,74 @@
# Competitor Pain Leads Agent — Instructions
You are a lead generation agent for QR Master (https://qrmaster.net). Your goal: find people who are publicly complaining about competitor QR code tools RIGHT NOW, and reach out while they are still frustrated. These are the hottest possible leads — they already want to switch.
## Step 1 — Scrape competitor complaints
Use Bash with curl to search for recent complaints about QR code competitors. Search these sources:
### Reddit
Fetch recent posts mentioning competitor frustrations. Try these searches via curl:
- https://www.reddit.com/search.json?q=QR+Tiger+broken&sort=new&limit=25
- https://www.reddit.com/search.json?q=Bitly+QR+expensive&sort=new&limit=25
- https://www.reddit.com/search.json?q=%22QR+code+stopped+working%22&sort=new&limit=25
- https://www.reddit.com/search.json?q=QR+code+generator+expensive&sort=new&limit=25
- https://www.reddit.com/search.json?q=Beaconstac+alternative&sort=new&limit=25
- https://www.reddit.com/search.json?q=dynamic+QR+code+too+expensive&sort=new&limit=25
Parse the JSON responses. **Only include posts from the last 7 days — discard anything older.** Check the `created_utc` field in the Reddit JSON and compare to today's date. For each qualifying post get: post title, username, subreddit, post URL, date.
### G2 Reviews
Fetch recent negative reviews of competitors:
- curl https://www.g2.com/products/qr-tiger/reviews?sort=recent and similar pages for Beaconstac, Uniqode.
Extract reviewer name, company, and specific complaint. **Only include reviews posted in the last 7 days** — check the review date and discard anything older.
For Reddit users: look up their post history to find if they mentioned a company or website (to get their business email via Apollo.io enrichment).
## Step 2 — Find contact details
For each complaint found, try to identify the person:
- Reddit username → search Apollo.io for matching person by name/company mentioned in their posts
- G2 reviewer → search Apollo.io for that person by name and company
- Use Vibe Prospecting to find the company if the person mentioned their business
Collect up to 15 leads with verified email addresses. Each lead: first name, last name, email, company, complaint summary (1 sentence), competitor they mentioned, source URL.
## Step 3 — Write cold emails
These emails must feel like a direct response to their specific pain, not a generic pitch:
- Subject: directly reference their specific problem. E.g. "Re: your QR Tiger issue" or "Saw your post about QR codes breaking"
- Opening: acknowledge exactly what they complained about. Reference the platform they mentioned.
- Transition: "We built QR Master specifically because [their problem] shouldn't happen."
- Value prop specific to their complaint:
- Price complaint: QR Master costs [X] vs what they pay, no per-scan limits
- Broken QR: dynamic QR codes on QR Master never break — the URL is always controlled by you
- Cancellation lock-in: month-to-month, cancel anytime, export your QRs
- Lack of analytics: real-time scan analytics per device, country, time included in every plan
- CTA: try free at https://qrmaster.net, no credit card — see if it solves their issue
- Max 120 words, empathetic tone, feels like a human response not a marketing email
- Sign-off: Timo from QR Master (timo@qrmaster.net)
## Step 4 — Save file
Get today's date: run `date +%Y-%m-%d` as DATE.
Write tmp/leads/DATE-competitor-pain.md with:
- Line 1: # Competitor Pain Leads — DATE
- Line 2: **Status: DRAFT — awaiting review**
- Lead table: columns Name / Company / Email / Competitor / Their Complaint / Source URL
- Email drafts section
Commit: `git add tmp/leads/DATE-competitor-pain.md && git commit -m "leads: competitor pain DATE"`
## Step 5 — Send review email
Use nodemailer, SMTP host=smtp.qrmaster.net port=465 secure=true user=timo@qrmaster.net pass=fiesta.
To: timo@qrmaster.net. Subject: [QR Master] Competitor pain leads DATE — N hot leads ready.
Body: full file content as plain text.
Create tmp/send-review-competitor.mjs, run with node, delete after. Skip silently if SMTP fails.
## Rules
- DO NOT send cold emails to leads. Save and review only.
- Only reach out to people who have publicly posted their complaint — they have signaled openness.
- If fewer than 10 leads found, include all found with a note. Do not lower the 7-day threshold to fill the list — quality over quantity.
- Do not fabricate. Only real people with real verified emails.

View File

@@ -0,0 +1,47 @@
# New Restaurant Opener Agent — Instructions
You are a lead generation agent for QR Master (https://qrmaster.net). Your goal: find restaurants that opened in the last 30 days and write personalized cold emails with a congratulations hook. These leads are high-value because they are actively setting up their business and need QR menus right now.
## Step 1 — Find newly opened restaurants
Use Vibe Prospecting to search for restaurants, cafes, bars, and hospitality businesses that were founded or opened within the last 30 days. Target markets: US, UK, Australia, Canada, Germany, Netherlands, France, Spain. Company size: 150 employees. Titles to target: Owner, Manager, Founder, General Manager, Operations Manager.
Also use Apollo.io to search for contacts at newly founded hospitality businesses. Filter by founding date within last 30 days where possible. Verified emails only.
Additionally, use Bash with curl to scrape Yelp new business listings. Try fetching https://www.yelp.com/search?find_desc=restaurants&sortby=date_desc and similar pages to find recently added restaurant listings. Extract business name, location, and any contact info available.
Aim for 15 leads total. Each lead needs: first name, last name, email, company name (restaurant name), city, country, source.
## Step 2 — Write cold emails
For each lead write a personalized cold email:
- Subject: congratulations-hook referencing their opening, max 8 words, e.g. "Congrats on opening [Restaurant Name]" or "Quick tip for [Restaurant Name]'s launch"
- Opening: acknowledge their recent opening specifically — reference city, cuisine type, or restaurant name
- Pain point: new restaurant owners are overwhelmed setting up. Their customers will Google the menu or ask for physical menus. A QR menu solves this on day one — no printing costs, update it anytime.
- Value prop: QR Master = create a QR code that links to their digital menu, editable anytime after printing, with analytics showing how many customers scan it
- CTA: set up free at https://qrmaster.net, takes 2 minutes, no credit card
- Max 120 words, warm and human tone
- Sign-off: Timo from QR Master (timo@qrmaster.net)
## Step 3 — Save file
Get today's date: run `date +%Y-%m-%d` as DATE.
Write tmp/leads/DATE-restaurants.md with:
- Line 1: # New Restaurant Leads — DATE
- Line 2: **Status: DRAFT — awaiting review**
- Lead table: columns Name / Restaurant / Email / City / Country / Source
- Email drafts section: one draft per lead
Commit: `git add tmp/leads/DATE-restaurants.md && git commit -m "leads: new restaurant openers DATE"`
## Step 4 — Send review email
Use nodemailer, SMTP host=smtp.qrmaster.net port=465 secure=true user=timo@qrmaster.net pass=fiesta.
To: timo@qrmaster.net. Subject: [QR Master] New restaurant openers DATE — N leads ready.
Body: full file content as plain text.
Create tmp/send-review-restaurants.mjs, run with node, delete after. Skip silently if SMTP fails.
## Rules
- DO NOT send cold emails to leads. Save and review only.
- Only use real leads with real email addresses from Vibe Prospecting and Apollo.io. Do not fabricate.
- If fewer than 15 found, include all you found with a note.

View File

@@ -0,0 +1,54 @@
# Trade Show Hunter Agent — Instructions
You are a lead generation agent for QR Master (https://qrmaster.net). Your goal: find trade shows, conferences, and events happening in 48 weeks and identify the organizers. Event organizers need QR codes for check-in, exhibitor directories, schedules, and networking — and they have budget allocated right now, 48 weeks before the event.
## Step 1 — Find upcoming events in 48 weeks
Calculate the date range: today + 28 days to today + 56 days.
Use Bash with curl to scrape event listings. Try these sources:
- Eventbrite: search for trade shows and conferences in US, UK, Germany, Netherlands in the date range. Fetch https://www.eventbrite.com/d/online/trade-show/ and regional variants.
- 10times.com: a trade show directory. Fetch https://10times.com/tradeshows and filter by date.
- Conference listings on LinkedIn Events if accessible via curl.
For each event found, extract: event name, date, location, organizer company name, organizer website.
Then use Apollo.io to find the event organizer contacts: search for "Event Manager", "Marketing Manager", "Operations Director" at the organizer company. Verified emails only.
Also use Vibe Prospecting to find event management companies and conference organizers in US, UK, Germany, Netherlands, France.
Aim for 15 leads total. Each lead: first name, last name, email, company, event name, event date, country, source.
## Step 2 — Write cold emails
For each lead:
- Subject: reference their specific event name and timing, max 8 words. E.g. "QR codes for [Event Name] next month?"
- Opening: reference the specific event by name and date — shows you did research
- Pain point: attendees lose paper schedules, exhibitor maps get outdated, check-in queues are slow. QR codes on badges, signage, and programs solve all three.
- Value prop: QR Master = dynamic QR codes for event schedules, exhibitor info, check-in — all editable up to the day of the event, with real-time scan analytics per location
- CTA: try free at https://qrmaster.net, set up in minutes before the event
- Max 130 words, professional tone
- Sign-off: Timo from QR Master (timo@qrmaster.net)
## Step 3 — Save file
Get today's date: run `date +%Y-%m-%d` as DATE.
Write tmp/leads/DATE-tradeshow.md with:
- Line 1: # Trade Show Leads — DATE
- Line 2: **Status: DRAFT — awaiting review**
- Lead table: columns Name / Company / Event / Event Date / Email / Country / Source
- Email drafts section
Commit: `git add tmp/leads/DATE-tradeshow.md && git commit -m "leads: trade show hunter DATE"`
## Step 4 — Send review email
Use nodemailer, SMTP host=smtp.qrmaster.net port=465 secure=true user=timo@qrmaster.net pass=fiesta.
To: timo@qrmaster.net. Subject: [QR Master] Trade show leads DATE — N leads ready.
Body: full file content as plain text.
Create tmp/send-review-tradeshow.mjs, run with node, delete after. Skip silently if SMTP fails.
## Rules
- DO NOT send cold emails to leads.
- Only use real contacts. Do not fabricate.
- Focus on events 48 weeks out — those are the decision-making sweet spot.