10 Commits

Author SHA1 Message Date
0924e85ee7 Update src/app/api/waitlist/route.ts 2026-03-06 08:11:40 +00:00
e80dcca1a8 Update src/app/page.tsx 2026-03-06 08:00:12 +00:00
416ab0fb9c Update src/app/page.tsx 2026-03-06 07:59:09 +00:00
fc3c5dbe19 Update src/app/api/waitlist/route.ts 2026-03-06 07:59:09 +00:00
a3258ea000 Update package.json 2026-03-06 07:59:09 +00:00
80eda59319 Add .env.local.example 2026-03-06 07:59:08 +00:00
63f1f58dc9 Merge version_11 into main
Merge version_11 into main
2026-03-06 06:25:29 +00:00
8f96928c52 Update src/app/page.tsx 2026-03-06 06:25:25 +00:00
92065771c3 Update src/app/api/waitlist/route.ts 2026-03-06 06:25:25 +00:00
1792646e60 Merge version_10 into main
Merge version_10 into main
2026-03-06 06:18:37 +00:00
4 changed files with 115 additions and 82 deletions

13
.env.local.example Normal file
View File

@@ -0,0 +1,13 @@
# SMTP Configuration for Email Notifications
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=your-email@gmail.com
SMTP_PASSWORD=your-app-password
SMTP_FROM=noreply@clearance.dev
# Admin Email
ADMIN_EMAIL=admin@clearance.dev
# Database Configuration (optional for future database integration)
DATABASE_URL=

View File

@@ -1,47 +1,12 @@
{ {
"name": "webild-components-2", "name": "clearance-landing", "version": "0.1.0", "private": true,
"version": "0.1.0",
"private": true,
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint"
"build": "next build --turbopack",
"typecheck": "tsc --noEmit",
"lint": "eslint . --ext .ts,.tsx",
"start": "next start"
}, },
"dependencies": { "dependencies": {
"@gsap/react": "^2.1.2", "react": "^18", "react-dom": "^18", "next": "^14", "lucide-react": "latest", "nodemailer": "^6.9.7"
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.4.0",
"@rive-app/react-canvas": "^4.26.2",
"@tsparticles/engine": "^3.9.1",
"@tsparticles/react": "^3.0.0",
"@tsparticles/slim": "^3.9.1",
"clsx": "^2.1.1",
"cobe": "^0.6.5",
"embla-carousel-auto-scroll": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"gsap": "^3.13.0",
"lenis": "^1.3.15",
"lucide-react": "^0.555.0",
"motion-number": "^1.0.0",
"next": "16.0.7",
"react": "19.2.1",
"react-dom": "19.2.1",
"react-fast-marquee": "^1.6.5",
"recharts": "^3.6.0",
"tailwind-merge": "^3.4.0",
"three": "^0.181.2"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3", "typescript": "^5", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", "@types/nodemailer": "^6.4.14", "autoprefixer": "^10", "postcss": "^8", "tailwindcss": "^3"
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.0.7",
"tailwindcss": "^4",
"typescript": "^5"
} }
} }

View File

@@ -1,7 +1,24 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import nodemailer from 'nodemailer';
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL || ''; interface WaitlistEntry {
const SUPABASE_ANON_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || ''; email: string;
instagram?: string;
tiktok?: string;
createdAt: string;
}
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST || 'smtp.gmail.com',
port: parseInt(process.env.SMTP_PORT || '587'),
secure: process.env.SMTP_SECURE === 'true',
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASSWORD,
},
});
const waitlistEntries: WaitlistEntry[] = [];
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
@@ -9,61 +26,99 @@ export async function POST(request: NextRequest) {
const { email, instagram, tiktok } = body; const { email, instagram, tiktok } = body;
// Validate email // Validate email
if (!email || !email.includes('@')) { if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return NextResponse.json( return NextResponse.json(
{ error: 'Invalid email address' }, { error: 'Invalid email address' },
{ status: 400 } { status: 400 }
); );
} }
// Check if Supabase credentials are configured // Check if email already exists
if (!SUPABASE_URL || !SUPABASE_ANON_KEY) { if (waitlistEntries.some(entry => entry.email === email)) {
console.error('Supabase credentials not configured');
// Still return success to user, but log the error
return NextResponse.json( return NextResponse.json(
{ message: 'Submission received (database pending)' }, { error: 'Email already on waitlist' },
{ status: 202 } { status: 409 }
); );
} }
// Insert into Supabase // Create waitlist entry
const response = await fetch(`${SUPABASE_URL}/rest/v1/waitlist`, { const entry: WaitlistEntry = {
method: 'POST', email,
headers: { instagram: instagram || undefined,
'Content-Type': 'application/json', tiktok: tiktok || undefined,
'apikey': SUPABASE_ANON_KEY, createdAt: new Date().toISOString(),
'Authorization': `Bearer ${SUPABASE_ANON_KEY}`, };
},
body: JSON.stringify({
email,
instagram: instagram || null,
tiktok: tiktok || null,
created_at: new Date().toISOString(),
}),
});
if (!response.ok) { waitlistEntries.push(entry);
console.error('Supabase error:', await response.text());
// Check if it's a duplicate entry error // Send confirmation email to user
const errorText = await response.text(); const userEmailHtml = `
if (errorText.includes('duplicate') || errorText.includes('unique')) { <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
return NextResponse.json( <h2 style="color: #333;">Welcome to Clearance Waitlist!</h2>
{ error: 'This email is already on the waitlist' }, <p>Hi ${email.split('@')[0]},</p>
{ status: 409 } <p>Thank you for joining the Clearance waitlist! We're excited to have you on board.</p>
); <p>We'll be reaching out soon with early access to our platform. In the meantime, here's what you can expect:</p>
} <ul>
throw new Error(`Supabase error: ${response.status}`); <li>Exclusive early access before our public launch</li>
<li>Lifetime discounts as an early adopter</li>
<li>1-on-1 onboarding support from our team</li>
</ul>
<p>If you have any questions, feel free to reply to this email.</p>
<p>Best regards,<br/>The Clearance Team</p>
</div>
`;
try {
await transporter.sendMail({
from: process.env.SMTP_FROM || process.env.SMTP_USER,
to: email,
subject: 'Welcome to Clearance Waitlist - Early Access Confirmed',
html: userEmailHtml,
});
} catch (emailError) {
console.error('Error sending confirmation email:', emailError);
// Continue even if email fails - user still added to waitlist
}
// Send notification email to admin
const adminEmailHtml = `
<div style="font-family: Arial, sans-serif;">
<h2>New Waitlist Submission</h2>
<p><strong>Email:</strong> ${email}</p>
<p><strong>Instagram:</strong> ${instagram || 'Not provided'}</p>
<p><strong>TikTok:</strong> ${tiktok || 'Not provided'}</p>
<p><strong>Submitted:</strong> ${new Date().toLocaleString()}</p>
</div>
`;
try {
await transporter.sendMail({
from: process.env.SMTP_FROM || process.env.SMTP_USER,
to: process.env.ADMIN_EMAIL || process.env.SMTP_USER,
subject: `New Clearance Waitlist Submission: ${email}`,
html: adminEmailHtml,
});
} catch (emailError) {
console.error('Error sending admin notification:', emailError);
// Continue even if email fails
} }
return NextResponse.json( return NextResponse.json(
{ message: 'Successfully joined the waitlist' }, { success: true, message: 'Successfully joined waitlist', entry },
{ status: 201 } { status: 201 }
); );
} catch (error) { } catch (error) {
console.error('Waitlist API error:', error); console.error('Waitlist submission error:', error);
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to process waitlist submission' }, { error: 'Failed to process submission' },
{ status: 500 } { status: 500 }
); );
} }
} }
export async function GET() {
return NextResponse.json({
count: waitlistEntries.length,
entries: waitlistEntries,
});
}

View File

@@ -1,4 +1,4 @@
"use client" "use client";
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider"; import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
import NavbarLayoutFloatingOverlay from '@/components/navbar/NavbarLayoutFloatingOverlay/NavbarLayoutFloatingOverlay'; import NavbarLayoutFloatingOverlay from '@/components/navbar/NavbarLayoutFloatingOverlay/NavbarLayoutFloatingOverlay';
@@ -39,7 +39,6 @@ export default function LandingPage() {
const handleWaitlistSubmit = async (formData: WaitlistFormData) => { const handleWaitlistSubmit = async (formData: WaitlistFormData) => {
try { try {
// Send to database via API
const response = await fetch('/api/waitlist', { const response = await fetch('/api/waitlist', {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -49,13 +48,15 @@ export default function LandingPage() {
}); });
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to submit form'); const errorData = await response.json();
throw new Error(errorData.error || 'Failed to submit form');
} }
const responseData = await response.json();
setWaitlistData([...waitlistData, formData]); setWaitlistData([...waitlistData, formData]);
setFormStatus('success'); setFormStatus('success');
setTimeout(() => setFormStatus('idle'), 3000); setTimeout(() => setFormStatus('idle'), 3000);
console.log('Waitlist submission:', formData); console.log('Waitlist submission successful:', responseData);
} catch (error) { } catch (error) {
setFormStatus('error'); setFormStatus('error');
console.error('Error submitting waitlist form:', error); console.error('Error submitting waitlist form:', error);
@@ -65,7 +66,6 @@ export default function LandingPage() {
const handleJoinWaitlistClick = () => { const handleJoinWaitlistClick = () => {
if (contactFormRef.current) { if (contactFormRef.current) {
contactFormRef.current.scrollIntoView({ behavior: 'smooth' }); contactFormRef.current.scrollIntoView({ behavior: 'smooth' });
// Focus on the email input after scroll
setTimeout(() => { setTimeout(() => {
const emailInput = contactFormRef.current?.querySelector('input[type="email"]') as HTMLInputElement; const emailInput = contactFormRef.current?.querySelector('input[type="email"]') as HTMLInputElement;
if (emailInput) { if (emailInput) {