Compare commits
6 Commits
65fe18a718
...
231a85ffa4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
231a85ffa4 | ||
|
|
673eaf7fd3 | ||
|
|
30b1b12e74 | ||
|
|
139b87fe93 | ||
|
|
8257866138 | ||
|
|
8de1411e34 |
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
99
src/components/marketing/PostDownloadPopup.tsx
Normal file
99
src/components/marketing/PostDownloadPopup.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
116
tmp/leads/AGENT_INSTRUCTIONS.md
Normal file
116
tmp/leads/AGENT_INSTRUCTIONS.md
Normal 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 16–34): Marketing Managers, CMOs, Digital Marketing Directors at SMBs (10–200 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: 5–200 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 1–2 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.
|
||||
74
tmp/leads/INSTRUCTIONS_competitor_pain.md
Normal file
74
tmp/leads/INSTRUCTIONS_competitor_pain.md
Normal 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.
|
||||
47
tmp/leads/INSTRUCTIONS_restaurant.md
Normal file
47
tmp/leads/INSTRUCTIONS_restaurant.md
Normal 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: 1–50 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.
|
||||
54
tmp/leads/INSTRUCTIONS_tradeshow.md
Normal file
54
tmp/leads/INSTRUCTIONS_tradeshow.md
Normal 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 4–8 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, 4–8 weeks before the event.
|
||||
|
||||
## Step 1 — Find upcoming events in 4–8 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 4–8 weeks out — those are the decision-making sweet spot.
|
||||
Reference in New Issue
Block a user