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
+
+
+
+
+
+
+
+ Admin credentials required
+
+
+
+
+ );
+ }
+
+ // Loading
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ // Admin Dashboard
+ return (
+
+
+
+
+
Newsletter Management
+
+ Manage AI feature launch notifications
+
+
+
+
+ Logout
+
+
+
+ {/* 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.
+
+
+
+
+ {broadcasting ? (
+ <>
+
+ Sending Emails...
+ >
+ ) : (
+ <>
+
+ Send Launch Notification to All
+ >
+ )}
+
+
+
+
+ {/* 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 ? (
+ <>
+
+ {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
+
+
+
+
+
+
+
+ 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
// ==========================================