From 6aa3267f26552c2693ee5de1af6bae69665142cd Mon Sep 17 00:00:00 2001 From: Timo Knuth Date: Thu, 18 Dec 2025 15:54:53 +0100 Subject: [PATCH] Newsletter comming soon --- package-lock.json | 10 + package.json | 1 + prisma/schema.prisma | 12 + src/app/(marketing)/layout.tsx | 12 +- src/app/(marketing)/newsletter/page.tsx | 367 ++++++++++++++++++ src/app/api/newsletter/admin-login/route.ts | 67 ++++ src/app/api/newsletter/broadcast/route.ts | 163 ++++++++ src/app/api/newsletter/subscribe/route.ts | 91 +++++ .../marketing/AIComingSoonBanner.tsx | 203 ++++++++++ src/components/marketing/HomePageClient.tsx | 2 + src/components/marketing/InstantGenerator.tsx | 5 +- src/lib/email.ts | 177 +++++++++ src/lib/rateLimit.ts | 8 + src/lib/validationSchemas.ts | 12 + 14 files changed, 1128 insertions(+), 2 deletions(-) create mode 100644 src/app/(marketing)/newsletter/page.tsx create mode 100644 src/app/api/newsletter/admin-login/route.ts create mode 100644 src/app/api/newsletter/broadcast/route.ts create mode 100644 src/app/api/newsletter/subscribe/route.ts create mode 100644 src/components/marketing/AIComingSoonBanner.tsx diff --git a/package-lock.json b/package-lock.json index d6ce0a7..6643a6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "i18next": "^23.7.6", "ioredis": "^5.3.2", "jszip": "^3.10.1", + "lucide-react": "^0.562.0", "next": "14.2.18", "next-auth": "^4.24.5", "papaparse": "^5.4.1", @@ -5412,6 +5413,15 @@ "node": ">=10" } }, + "node_modules/lucide-react": { + "version": "0.562.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz", + "integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/package.json b/package.json index 29658b1..a13324d 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "i18next": "^23.7.6", "ioredis": "^5.3.2", "jszip": "^3.10.1", + "lucide-react": "^0.562.0", "next": "14.2.18", "next-auth": "^4.24.5", "papaparse": "^5.4.1", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f4b5156..da00a83 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -149,4 +149,16 @@ model Integration { updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + +model NewsletterSubscription { + id String @id @default(cuid()) + email String @unique + source String @default("ai-coming-soon") + status String @default("subscribed") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([email]) + @@index([createdAt]) } \ No newline at end of file diff --git a/src/app/(marketing)/layout.tsx b/src/app/(marketing)/layout.tsx index 5b502f2..322b8c2 100644 --- a/src/app/(marketing)/layout.tsx +++ b/src/app/(marketing)/layout.tsx @@ -145,8 +145,18 @@ export default function MarketingLayout({ -
+
+ + + + + Admin +

© 2025 QR Master. All rights reserved.

+
diff --git a/src/app/(marketing)/newsletter/page.tsx b/src/app/(marketing)/newsletter/page.tsx new file mode 100644 index 0000000..72f76e1 --- /dev/null +++ b/src/app/(marketing)/newsletter/page.tsx @@ -0,0 +1,367 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { Card } from '@/components/ui/Card'; +import { Button } from '@/components/ui/Button'; +import { Badge } from '@/components/ui/Badge'; +import { Mail, Users, Send, CheckCircle, AlertCircle, Loader2, Lock, LogOut } from 'lucide-react'; + +interface Subscriber { + email: string; + createdAt: string; +} + +interface BroadcastInfo { + total: number; + recent: Subscriber[]; +} + +export default function NewsletterPage() { + const router = useRouter(); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [isAuthenticating, setIsAuthenticating] = useState(true); + const [loginError, setLoginError] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + + const [info, setInfo] = useState(null); + const [loading, setLoading] = useState(true); + const [broadcasting, setBroadcasting] = useState(false); + const [result, setResult] = useState<{ + success: boolean; + message: string; + sent?: number; + failed?: number; + } | null>(null); + + useEffect(() => { + checkAuth(); + }, []); + + const checkAuth = async () => { + try { + const response = await fetch('/api/newsletter/broadcast'); + if (response.ok) { + setIsAuthenticated(true); + const data = await response.json(); + setInfo(data); + setLoading(false); + } else { + setIsAuthenticated(false); + } + } catch (error) { + setIsAuthenticated(false); + } finally { + setIsAuthenticating(false); + } + }; + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setLoginError(''); + setIsAuthenticating(true); + + try { + const response = await fetch('/api/newsletter/admin-login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email, password }), + }); + + if (response.ok) { + setIsAuthenticated(true); + await checkAuth(); + } else { + const data = await response.json(); + setLoginError(data.error || 'Invalid credentials'); + } + } catch (error) { + setLoginError('Login failed. Please try again.'); + } finally { + setIsAuthenticating(false); + } + }; + + const handleLogout = async () => { + await fetch('/api/auth/logout', { method: 'POST' }); + router.push('/'); + }; + + const fetchSubscriberInfo = async () => { + try { + const response = await fetch('/api/newsletter/broadcast'); + if (response.ok) { + const data = await response.json(); + setInfo(data); + } + } catch (error) { + console.error('Failed to fetch subscriber info:', error); + } + }; + + const handleBroadcast = async () => { + if (!confirm(`Are you sure you want to send the AI feature launch email to ${info?.total} subscribers?`)) { + return; + } + + setBroadcasting(true); + setResult(null); + + try { + const response = await fetch('/api/newsletter/broadcast', { + method: 'POST', + }); + + const data = await response.json(); + + setResult({ + success: response.ok, + message: data.message || data.error, + sent: data.sent, + failed: data.failed, + }); + + if (response.ok) { + await fetchSubscriberInfo(); + } + } catch (error) { + setResult({ + success: false, + message: 'Failed to send broadcast. Please try again.', + }); + } finally { + setBroadcasting(false); + } + }; + + // Login Screen + if (!isAuthenticated) { + return ( +
+ +
+
+ +
+

Newsletter Admin

+

+ Sign in to manage subscribers +

+
+ +
+
+ + setEmail(e.target.value)} + placeholder="support@qrmaster.net" + required + className="w-full px-4 py-3 rounded-xl bg-background border border-border focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20 outline-none transition-all" + /> +
+ +
+ + setPassword(e.target.value)} + placeholder="••••••••" + required + className="w-full px-4 py-3 rounded-xl bg-background border border-border focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20 outline-none transition-all" + /> +
+ + {loginError && ( +

{loginError}

+ )} + + +
+ +
+

+ Admin credentials required +

+
+
+
+ ); + } + + // Loading + if (loading) { + return ( +
+ +
+ ); + } + + // Admin Dashboard + return ( +
+
+
+
+

Newsletter Management

+

+ Manage AI feature launch notifications +

+
+ +
+ + {/* Stats Card */} + +
+
+
+ +
+
+

{info?.total || 0}

+

Total Subscribers

+
+
+ + Active + +
+ + {/* Broadcast Button */} +
+
+

+ + Broadcast AI Feature Launch +

+

+ Send the AI feature launch announcement to all {info?.total} subscribers. + This will inform them that the features are now available. +

+
+ + +
+
+ + {/* Result Message */} + {result && ( + +
+ {result.success ? ( + + ) : ( + + )} +
+

+ {result.message} +

+ {result.sent !== undefined && ( +

+ Sent: {result.sent} | Failed: {result.failed} +

+ )} +
+
+
+ )} + + {/* Recent Subscribers */} + +

Recent Subscribers

+ {info?.recent && info.recent.length > 0 ? ( +
+ {info.recent.map((subscriber, index) => ( +
+
+ + {subscriber.email} +
+ + {new Date(subscriber.createdAt).toLocaleDateString()} + +
+ ))} +
+ ) : ( +

No subscribers yet

+ )} + +
+

+ 💡 Tip: View all subscribers in{' '} + + Prisma Studio + + {' '}(NewsletterSubscription table) +

+
+
+
+
+ ); +} diff --git a/src/app/api/newsletter/admin-login/route.ts b/src/app/api/newsletter/admin-login/route.ts new file mode 100644 index 0000000..686716e --- /dev/null +++ b/src/app/api/newsletter/admin-login/route.ts @@ -0,0 +1,67 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; +import bcrypt from 'bcryptjs'; +import { db } from '@/lib/db'; +import { getAuthCookieOptions } from '@/lib/cookieConfig'; + +/** + * POST /api/newsletter/admin-login + * Simple admin login for newsletter management (no CSRF required) + */ +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { email, password } = body; + + // Validate input + if (!email || !password) { + return NextResponse.json( + { error: 'Email and password are required' }, + { status: 400 } + ); + } + + // Check if user exists + const user = await db.user.findUnique({ + where: { email: email.toLowerCase() }, + select: { + id: true, + email: true, + password: true, + }, + }); + + if (!user || !user.password) { + return NextResponse.json( + { error: 'Invalid credentials' }, + { status: 401 } + ); + } + + // Verify password + const isPasswordValid = await bcrypt.compare(password, user.password); + + if (!isPasswordValid) { + return NextResponse.json( + { error: 'Invalid credentials' }, + { status: 401 } + ); + } + + // Set auth cookie + const response = NextResponse.json({ + success: true, + message: 'Login successful', + }); + + response.cookies.set('userId', user.id, getAuthCookieOptions()); + + return response; + } catch (error) { + console.error('Newsletter admin login error:', error); + return NextResponse.json( + { error: 'Login failed. Please try again.' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/newsletter/broadcast/route.ts b/src/app/api/newsletter/broadcast/route.ts new file mode 100644 index 0000000..ee2ef51 --- /dev/null +++ b/src/app/api/newsletter/broadcast/route.ts @@ -0,0 +1,163 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; +import { db } from '@/lib/db'; +import { sendAIFeatureLaunchEmail } from '@/lib/email'; +import { rateLimit, RateLimits } from '@/lib/rateLimit'; + +/** + * POST /api/newsletter/broadcast + * Send AI feature launch email to all subscribed users + * PROTECTED: Only authenticated users can access (you may want to add admin check) + */ +export async function POST(request: NextRequest) { + try { + // Check authentication + const userId = cookies().get('userId')?.value; + + if (!userId) { + return NextResponse.json( + { error: 'Unauthorized. Please log in.' }, + { status: 401 } + ); + } + + // Optional: Add admin check here + // const user = await db.user.findUnique({ where: { id: userId } }); + // if (user?.role !== 'ADMIN') { + // return NextResponse.json({ error: 'Forbidden. Admin access required.' }, { status: 403 }); + // } + + // Rate limiting (prevent accidental spam) + const rateLimitResult = rateLimit(userId, { + name: 'newsletter-broadcast', + maxRequests: 2, // Only 2 broadcasts per hour + windowSeconds: 60 * 60, + }); + + if (!rateLimitResult.success) { + return NextResponse.json( + { + error: 'Too many broadcast attempts. Please wait before trying again.', + retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000), + }, + { status: 429 } + ); + } + + // Get all subscribed users + const subscribers = await db.newsletterSubscription.findMany({ + where: { + status: 'subscribed', + }, + select: { + email: true, + }, + }); + + if (subscribers.length === 0) { + return NextResponse.json({ + success: true, + message: 'No subscribers found', + sent: 0, + }); + } + + // Send emails in batches to avoid overwhelming Resend + const batchSize = 10; + const results = { + sent: 0, + failed: 0, + errors: [] as string[], + }; + + for (let i = 0; i < subscribers.length; i += batchSize) { + const batch = subscribers.slice(i, i + batchSize); + + // Send emails in parallel within batch + const promises = batch.map(async (subscriber) => { + try { + await sendAIFeatureLaunchEmail(subscriber.email); + results.sent++; + } catch (error) { + results.failed++; + results.errors.push(`Failed to send to ${subscriber.email}`); + console.error(`Failed to send to ${subscriber.email}:`, error); + } + }); + + await Promise.allSettled(promises); + + // Small delay between batches to be nice to the email service + if (i + batchSize < subscribers.length) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + } + + return NextResponse.json({ + success: true, + message: `Broadcast completed. Sent to ${results.sent} subscribers.`, + sent: results.sent, + failed: results.failed, + total: subscribers.length, + errors: results.errors.length > 0 ? results.errors : undefined, + }); + } catch (error) { + console.error('Newsletter broadcast error:', error); + return NextResponse.json( + { + error: 'Failed to send broadcast emails. Please try again.', + }, + { status: 500 } + ); + } +} + +/** + * GET /api/newsletter/broadcast + * Get subscriber count and preview + * PROTECTED: Only authenticated users + */ +export async function GET(request: NextRequest) { + try { + // Check authentication + const userId = cookies().get('userId')?.value; + + if (!userId) { + return NextResponse.json( + { error: 'Unauthorized. Please log in.' }, + { status: 401 } + ); + } + + const subscriberCount = await db.newsletterSubscription.count({ + where: { + status: 'subscribed', + }, + }); + + const recentSubscribers = await db.newsletterSubscription.findMany({ + where: { + status: 'subscribed', + }, + select: { + email: true, + createdAt: true, + }, + orderBy: { + createdAt: 'desc', + }, + take: 5, + }); + + return NextResponse.json({ + total: subscriberCount, + recent: recentSubscribers, + }); + } catch (error) { + console.error('Error fetching subscriber info:', error); + return NextResponse.json( + { error: 'Failed to fetch subscriber information' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/newsletter/subscribe/route.ts b/src/app/api/newsletter/subscribe/route.ts new file mode 100644 index 0000000..3c284ea --- /dev/null +++ b/src/app/api/newsletter/subscribe/route.ts @@ -0,0 +1,91 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { db } from '@/lib/db'; +import { newsletterSubscribeSchema, validateRequest } from '@/lib/validationSchemas'; +import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit'; +import { sendNewsletterWelcomeEmail } from '@/lib/email'; + +/** + * POST /api/newsletter/subscribe + * Subscribe to AI features newsletter + * Public endpoint - no authentication required + */ +export async function POST(request: NextRequest) { + try { + // Get client identifier for rate limiting + const clientId = getClientIdentifier(request); + + // Apply rate limiting (5 per hour) + const rateLimitResult = rateLimit(clientId, RateLimits.NEWSLETTER_SUBSCRIBE); + + if (!rateLimitResult.success) { + return NextResponse.json( + { + error: 'Too many subscription attempts. Please try again later.', + retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000), + }, + { + status: 429, + headers: { + 'X-RateLimit-Limit': rateLimitResult.limit.toString(), + 'X-RateLimit-Remaining': rateLimitResult.remaining.toString(), + 'X-RateLimit-Reset': rateLimitResult.reset.toString(), + 'Retry-After': Math.ceil((rateLimitResult.reset - Date.now()) / 1000).toString(), + }, + } + ); + } + + // Parse and validate request body + const body = await request.json(); + const validation = await validateRequest(newsletterSubscribeSchema, body); + + if (!validation.success) { + return NextResponse.json(validation.error, { status: 400 }); + } + + const { email } = validation.data; + + // Check if email already subscribed + const existing = await db.newsletterSubscription.findUnique({ + where: { email }, + }); + + if (existing) { + // If already subscribed, return success (idempotent) + // Don't reveal if email exists for privacy + return NextResponse.json({ + success: true, + message: 'Successfully subscribed to AI features newsletter!', + alreadySubscribed: true, + }); + } + + // Create new subscription + await db.newsletterSubscription.create({ + data: { + email, + source: 'ai-coming-soon', + status: 'subscribed', + }, + }); + + // Send welcome email (don't block response) + sendNewsletterWelcomeEmail(email).catch((error) => { + console.error('Failed to send welcome email (non-blocking):', error); + }); + + return NextResponse.json({ + success: true, + message: 'Successfully subscribed to AI features newsletter!', + alreadySubscribed: false, + }); + } catch (error) { + console.error('Newsletter subscription error:', error); + return NextResponse.json( + { + error: 'Failed to subscribe to newsletter. Please try again.', + }, + { status: 500 } + ); + } +} diff --git a/src/components/marketing/AIComingSoonBanner.tsx b/src/components/marketing/AIComingSoonBanner.tsx new file mode 100644 index 0000000..5154b36 --- /dev/null +++ b/src/components/marketing/AIComingSoonBanner.tsx @@ -0,0 +1,203 @@ +'use client'; + +import React, { useState } from 'react'; +import { Sparkles, Brain, TrendingUp, MessageSquare, Palette, ArrowRight, Mail, CheckCircle2, Lock } from 'lucide-react'; +import Link from 'next/link'; + +const AIComingSoonBanner = () => { + const [email, setEmail] = useState(''); + const [submitted, setSubmitted] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(''); + + try { + const response = await fetch('/api/newsletter/subscribe', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email }), + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || 'Failed to subscribe'); + } + + setSubmitted(true); + setEmail(''); + } catch (err) { + setError(err instanceof Error ? err.message : 'Something went wrong. Please try again.'); + } finally { + setLoading(false); + } + }; + + const features = [ + { + icon: Brain, + category: 'Smart QR Generation', + items: [ + 'AI-powered content optimization', + 'Intelligent design suggestions', + 'Auto-generate vCard from LinkedIn', + 'Smart URL shortening with SEO', + ], + }, + { + icon: TrendingUp, + category: 'Advanced Analytics & Insights', + items: [ + 'AI-powered scan predictions', + 'Anomaly detection', + 'Natural language analytics queries', + 'Automated insights reports', + ], + }, + { + icon: MessageSquare, + category: 'Smart Content Management', + items: [ + 'AI chatbot for instant support', + 'Automated QR categorization', + 'Smart bulk QR generation', + 'Content recommendations', + ], + }, + { + icon: Palette, + category: 'Creative & Marketing', + items: [ + 'AI-generated custom QR designs', + 'Marketing copy generation', + 'A/B testing suggestions', + 'Campaign optimization', + ], + }, + ]; + + return ( +
+ {/* Smooth Gradient Fade Transition from Hero */} +
+ + {/* Animated Background Orbs (matching Hero) */} +
+
+
+
+
+ + {/* Smooth Gradient Fade Transition to Next Section */} +
+ +
+ {/* Header */} +
+
+ + + Coming Soon + +
+ +

+ The Future of QR Codes is{' '} + + AI-Powered + +

+ +

+ Revolutionary AI features to transform how you create, manage, and optimize QR codes +

+
+ + {/* Features Grid */} +
+ {features.map((feature, index) => ( +
+
+ +
+ +

+ {feature.category} +

+ +
    + {feature.items.map((item, itemIndex) => ( +
  • +
    + {item} +
  • + ))} +
+
+ ))} +
+ + {/* Email Capture */} +
+ {!submitted ? ( + <> +
+
+ + { + setEmail(e.target.value); + setError(''); + }} + placeholder="your@email.com" + required + disabled={loading} + className="w-full pl-12 pr-4 py-3 rounded-xl bg-white border border-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 outline-none transition-all" + /> +
+ +
+ {error && ( +

{error}

+ )} +

+ Be the first to know when AI features launch +

+ + ) : ( +
+ + + You're on the list! We'll notify you when AI features launch. + +
+ )} +
+
+
+ ); +}; + +export default AIComingSoonBanner; diff --git a/src/components/marketing/HomePageClient.tsx b/src/components/marketing/HomePageClient.tsx index bc432fd..633ce57 100644 --- a/src/components/marketing/HomePageClient.tsx +++ b/src/components/marketing/HomePageClient.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Hero } from '@/components/marketing/Hero'; +import AIComingSoonBanner from '@/components/marketing/AIComingSoonBanner'; import { StatsStrip } from '@/components/marketing/StatsStrip'; import { TemplateCards } from '@/components/marketing/TemplateCards'; import { InstantGenerator } from '@/components/marketing/InstantGenerator'; @@ -29,6 +30,7 @@ export default function HomePageClient() { return ( <> + diff --git a/src/components/marketing/InstantGenerator.tsx b/src/components/marketing/InstantGenerator.tsx index c94e96a..440e733 100644 --- a/src/components/marketing/InstantGenerator.tsx +++ b/src/components/marketing/InstantGenerator.tsx @@ -73,7 +73,10 @@ export const InstantGenerator: React.FC = ({ t }) => { }; return ( -
+
+ {/* Smooth Gradient Fade Transition from Previous Section */} +
+

diff --git a/src/lib/email.ts b/src/lib/email.ts index 2e3705a..d039ee7 100644 --- a/src/lib/email.ts +++ b/src/lib/email.ts @@ -103,3 +103,180 @@ export async function sendPasswordResetEmail(email: string, resetToken: string) throw new Error('Failed to send password reset email'); } } + +export async function sendNewsletterWelcomeEmail(email: string) { + try { + await resend.emails.send({ + from: 'QR Master ', + replyTo: 'support@qrmaster.net', + to: email, + subject: '🎉 You\'re on the list! AI-Powered QR Features Coming Soon', + html: ` + + + + + + Welcome to QR Master AI Newsletter + + + + + + +
+ + + + + + + + + + + + + + + +
+

QR Master

+

AI-Powered QR Features

+
+

Welcome to the Future! 🚀

+ +

+ Thank you for signing up to be notified about our revolutionary AI-powered QR code features! You're among the first to know when these game-changing capabilities launch. +

+ +
+

What's Coming:

+
    +
  • Smart QR Generation - AI-powered content optimization & intelligent design suggestions
  • +
  • Advanced Analytics - Scan predictions, anomaly detection & natural language queries
  • +
  • Smart Content Management - AI chatbot, auto-categorization & smart bulk generation
  • +
  • Creative & Marketing - AI-generated designs, copy generation & campaign optimization
  • +
+
+ +

+ We're working hard to bring these features to life and can't wait to share them with you. We'll send you an email as soon as they're ready to use! +

+ +

+ In the meantime, feel free to explore our existing features at qrmaster.net +

+
+

+ © 2025 QR Master. All rights reserved. +

+

+ You're receiving this email because you signed up for AI feature notifications. +

+
+
+ + + `, + }); + + console.log('Newsletter welcome email sent successfully to:', email); + return { success: true }; + } catch (error) { + console.error('Error sending newsletter welcome email:', error); + throw new Error('Failed to send newsletter welcome email'); + } +} + +export async function sendAIFeatureLaunchEmail(email: string) { + try { + await resend.emails.send({ + from: 'QR Master ', + replyTo: 'support@qrmaster.net', + to: email, + subject: '🚀 AI-Powered Features Are Here! QR Master Gets Smarter', + html: ` + + + + + + AI Features Launched! + + + + + + +
+ + + + + + + + + + + + + + + +
+

🚀 They're Here!

+

AI-Powered QR Features Are Live

+
+

The wait is over! ✨

+ +

+ We're excited to announce that the AI-powered features you've been waiting for are now live on QR Master! Your QR code creation just got a whole lot smarter. +

+ +
+

✨ What's New:

+
    +
  • Smart QR Generation - AI optimizes your content and suggests the best designs automatically
  • +
  • Advanced Analytics - Predictive insights and natural language queries for your scan data
  • +
  • Auto-Organization - Your QR codes get categorized and tagged automatically
  • +
  • AI Design Studio - Generate unique, custom QR code designs with AI
  • +
+
+ + + + + + +
+ + Try AI Features Now + +
+ +

+ Log in to your QR Master account and start exploring the new AI capabilities. We can't wait to see what you create! +

+
+

+ © 2025 QR Master. All rights reserved. +

+

+ You received this email because you subscribed to AI feature notifications. +

+
+
+ + + `, + }); + + console.log('AI feature launch email sent successfully to:', email); + return { success: true }; + } catch (error) { + console.error('Error sending AI feature launch email:', error); + throw new Error('Failed to send AI feature launch email'); + } +} diff --git a/src/lib/rateLimit.ts b/src/lib/rateLimit.ts index 1fbdf41..f4cf0b5 100644 --- a/src/lib/rateLimit.ts +++ b/src/lib/rateLimit.ts @@ -212,6 +212,14 @@ export const RateLimits = { windowSeconds: 60 * 60, }, + // Newsletter endpoints + // Newsletter subscribe: 5 per hour (prevent spam) + NEWSLETTER_SUBSCRIBE: { + name: 'newsletter-subscribe', + maxRequests: 5, + windowSeconds: 60 * 60, + }, + // General API: 100 requests per minute API: { name: 'api', diff --git a/src/lib/validationSchemas.ts b/src/lib/validationSchemas.ts index 9aea3e1..e60ead6 100644 --- a/src/lib/validationSchemas.ts +++ b/src/lib/validationSchemas.ts @@ -140,6 +140,18 @@ export const createCheckoutSchema = z.object({ priceId: z.string().min(1, 'Price ID is required'), }); +// ========================================== +// Newsletter Schemas +// ========================================== + +export const newsletterSubscribeSchema = z.object({ + email: z.string() + .email('Invalid email format') + .toLowerCase() + .trim() + .max(255, 'Email must be less than 255 characters'), +}); + // ========================================== // Helper: Format Zod Errors // ==========================================