diff --git a/src/app/(main)/(auth)/signup/SignupClient.tsx b/src/app/(main)/(auth)/signup/SignupClient.tsx index 8fe6304..8d4f861 100644 --- a/src/app/(main)/(auth)/signup/SignupClient.tsx +++ b/src/app/(main)/(auth)/signup/SignupClient.tsx @@ -48,10 +48,15 @@ export default function SignupClient() { body: JSON.stringify({ name, email, password }), }); - const data = await response.json(); - - if (response.ok && data.success) { - // Store user in localStorage for client-side + const data = await response.json(); + + if (response.ok && data.success) { + if (data.requiresEmailVerification) { + router.push(`/verify-email?email=${encodeURIComponent(data.email)}`); + return; + } + + // Store user in localStorage for client-side localStorage.setItem('user', JSON.stringify(data.user)); // Track successful signup with PostHog diff --git a/src/app/(main)/(auth)/verify-email/page.tsx b/src/app/(main)/(auth)/verify-email/page.tsx new file mode 100644 index 0000000..7edb59a --- /dev/null +++ b/src/app/(main)/(auth)/verify-email/page.tsx @@ -0,0 +1,28 @@ +import Link from 'next/link'; + +export const metadata = { + title: 'Check your email | QR Master', + robots: { index: false, follow: false }, +}; + +export default function VerifyEmailPage({ searchParams }: { searchParams: { email?: string; status?: string } }) { + const expired = searchParams.status === 'expired'; + + return ( +
+
+ QR MASTER +

+ {expired ? 'This confirmation link has expired.' : 'Check your inbox.'} +

+

+ {expired + ? 'Please create your account again to receive a new confirmation email.' + : <>We sent a confirmation link{searchParams.email ? <> to {searchParams.email} : ''}. Open it to finish creating your account.} +

+

The link expires in 24 hours. Check your spam folder if it does not arrive shortly.

+ Back to sign in +
+
+ ); +} diff --git a/src/app/(main)/(marketing)/newsletter/NewsletterComposer.tsx b/src/app/(main)/(marketing)/newsletter/NewsletterComposer.tsx index 6efcc65..6d017f3 100644 --- a/src/app/(main)/(marketing)/newsletter/NewsletterComposer.tsx +++ b/src/app/(main)/(marketing)/newsletter/NewsletterComposer.tsx @@ -1,7 +1,7 @@ 'use client'; import { useEffect, useMemo, useRef, useState } from 'react'; -import { Code2, Eye, FileText, ImagePlus, Loader2, MailCheck, Send, ShieldCheck, Users } from 'lucide-react'; +import { Ban, Code2, Eye, FileText, ImagePlus, MailCheck, Send, ShieldCheck, Users } from 'lucide-react'; import { Button } from '@/components/ui/Button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'; @@ -18,9 +18,12 @@ export default function NewsletterComposer() { const [format, setFormat] = useState('text'); const [audience, setAudience] = useState('all_users'); const [testEmail, setTestEmail] = useState('knuth.timo@gmail.com'); + const [testName, setTestName] = useState('Timo'); + const [bounceEmails, setBounceEmails] = useState(''); const [status, setStatus] = useState(''); const [isTesting, setIsTesting] = useState(false); const [isSending, setIsSending] = useState(false); + const [isSuppressing, setIsSuppressing] = useState(false); const [isUploadingImage, setIsUploadingImage] = useState(false); const [uploadedImageUrl, setUploadedImageUrl] = useState(''); const imageInput = useRef(null); @@ -40,10 +43,12 @@ export default function NewsletterComposer() { const audienceCount = details?.audiences[audience] ?? 0; const preview = useMemo(() => { - if (format === 'html') return content; - const escaped = content.replace(/[&<>]/g, (character) => ({ '&': '&', '<': '<', '>': '>' })[character] || character); + const firstName = testName.trim().split(/\s+/)[0] || 'there'; + const previewContent = content.replace(/\{\{\s*first_name\s*\}\}/gi, firstName); + if (format === 'html') return previewContent; + const escaped = previewContent.replace(/[&<>]/g, (character) => ({ '&': '&', '<': '<', '>': '>' })[character] || character); return `
${escaped}
`; - }, [content, format]); + }, [content, format, testName]); async function submit(action: 'test' | 'send') { if (!subject.trim() || !content.trim()) { @@ -60,10 +65,12 @@ export default function NewsletterComposer() { const response = await fetch('/api/marketing/newsletter', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ action, confirm: action === 'send', subject, content, format, audience, testEmail }), + body: JSON.stringify({ action, confirm: action === 'send', subject, content, format, audience, testEmail, testName }), }); - const payload = await response.json(); - if (!response.ok) throw new Error(payload.error || 'Sending failed.'); + const payload = response.headers.get('content-type')?.includes('application/json') + ? await response.json() + : null; + if (!response.ok) throw new Error(payload?.error || 'The server did not return a sending result. Do not send again until the delivery log has been checked.'); setStatus(action === 'test' ? `Test email sent to ${testEmail}.` : `Delivery complete: ${payload.sent} sent, ${payload.failed} failed.`); } catch (error) { setStatus(error instanceof Error ? error.message : 'Sending failed.'); @@ -73,6 +80,32 @@ export default function NewsletterComposer() { } } + async function suppressBounces() { + const emails = bounceEmails.split(/[\s,;]+/).filter(Boolean); + if (emails.length === 0) { + setStatus('Paste one or more bounced email addresses first.'); + return; + } + + setIsSuppressing(true); + setStatus(''); + try { + const response = await fetch('/api/marketing/newsletter/suppress', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ emails }), + }); + const payload = await response.json(); + if (!response.ok) throw new Error(payload.error || 'Could not suppress these addresses.'); + setBounceEmails(''); + setStatus(`${payload.suppressed} bounced address${payload.suppressed === 1 ? '' : 'es'} removed from future campaigns.`); + } catch (error) { + setStatus(error instanceof Error ? error.message : 'Could not suppress these addresses.'); + } finally { + setIsSuppressing(false); + } + } + async function uploadImage(file: File | undefined) { if (!file) return; @@ -126,7 +159,7 @@ export default function NewsletterComposer() {
-