Compare commits
29 Commits
version_3
...
version_12
| Author | SHA1 | Date | |
|---|---|---|---|
| 0924e85ee7 | |||
| e80dcca1a8 | |||
| 416ab0fb9c | |||
| fc3c5dbe19 | |||
| a3258ea000 | |||
| 80eda59319 | |||
| 63f1f58dc9 | |||
| 8f96928c52 | |||
| 92065771c3 | |||
| 1792646e60 | |||
| 7d9e3379cc | |||
| a255161583 | |||
| ad8bbfecf6 | |||
| 73710d854b | |||
| 68469d88dd | |||
| 63c8304a3b | |||
| b956958798 | |||
| e91715a197 | |||
| c75dd59c0a | |||
| 961894778f | |||
| cdc373f255 | |||
| 3e6bf968af | |||
| bb70a5d116 | |||
| e3da975fa0 | |||
| efeb65da9f | |||
| c54ee001f6 | |||
| feadd88571 | |||
| 19a3f5afd7 | |||
| b1308f0f94 |
13
.env.local.example
Normal file
13
.env.local.example
Normal 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=
|
||||
43
package.json
43
package.json
@@ -1,47 +1,12 @@
|
||||
{
|
||||
"name": "webild-components-2",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"name": "clearance-landing", "version": "0.1.0", "private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build --turbopack",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "eslint . --ext .ts,.tsx",
|
||||
"start": "next start"
|
||||
"dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gsap/react": "^2.1.2",
|
||||
"@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"
|
||||
"react": "^18", "react-dom": "^18", "next": "^14", "lucide-react": "latest", "nodemailer": "^6.9.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^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"
|
||||
"typescript": "^5", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", "@types/nodemailer": "^6.4.14", "autoprefixer": "^10", "postcss": "^8", "tailwindcss": "^3"
|
||||
}
|
||||
}
|
||||
|
||||
124
src/app/api/waitlist/route.ts
Normal file
124
src/app/api/waitlist/route.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
interface WaitlistEntry {
|
||||
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) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { email, instagram, tiktok } = body;
|
||||
|
||||
// Validate email
|
||||
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid email address' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if email already exists
|
||||
if (waitlistEntries.some(entry => entry.email === email)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Email already on waitlist' },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
// Create waitlist entry
|
||||
const entry: WaitlistEntry = {
|
||||
email,
|
||||
instagram: instagram || undefined,
|
||||
tiktok: tiktok || undefined,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
waitlistEntries.push(entry);
|
||||
|
||||
// Send confirmation email to user
|
||||
const userEmailHtml = `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<h2 style="color: #333;">Welcome to Clearance Waitlist!</h2>
|
||||
<p>Hi ${email.split('@')[0]},</p>
|
||||
<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>
|
||||
<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(
|
||||
{ success: true, message: 'Successfully joined waitlist', entry },
|
||||
{ status: 201 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Waitlist submission error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to process submission' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({
|
||||
count: waitlistEntries.length,
|
||||
entries: waitlistEntries,
|
||||
});
|
||||
}
|
||||
@@ -5,7 +5,8 @@ import "./globals.css";
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Clearance - Protect Your IP Revenue", description: "Automate IP protection for creators and agencies. Track licenses, get renewal reminders, and collect payments before your content goes dark."};
|
||||
title: "Clearance - Protect Your IP Revenue", description: "Automate IP protection for creators and agencies. Track licenses, get renewal reminders, and collect payments before your content goes dark."
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
@@ -14,6 +15,9 @@ export default function RootLayout({
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script async src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
</head>
|
||||
<body className={inter.className}>{children}
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
@@ -1384,4 +1388,4 @@ export default function RootLayout({
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
190
src/app/page.tsx
190
src/app/page.tsx
@@ -1,4 +1,4 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
||||
import NavbarLayoutFloatingOverlay from '@/components/navbar/NavbarLayoutFloatingOverlay/NavbarLayoutFloatingOverlay';
|
||||
@@ -8,9 +8,10 @@ import FeatureBento from '@/components/sections/feature/FeatureBento';
|
||||
import ProductCardFour from '@/components/sections/product/ProductCardFour';
|
||||
import SocialProofOne from '@/components/sections/socialProof/SocialProofOne';
|
||||
import FaqBase from '@/components/sections/faq/FaqBase';
|
||||
import ContactSplit from '@/components/sections/contact/ContactSplit';
|
||||
import FooterSimple from '@/components/sections/footer/FooterSimple';
|
||||
import { Shield, Zap, Clock, AlertCircle, CheckCircle, Users, Sparkles, HelpCircle } from "lucide-react";
|
||||
import { useState, useRef } from "react";
|
||||
import Input from '@/components/form/Input';
|
||||
|
||||
const navItems = [
|
||||
{ name: "Problem", id: "problem" },
|
||||
@@ -25,7 +26,55 @@ const socialProofLogos = [
|
||||
"http://img.b2bpic.net/free-vector/flat-minimal-technology-labels_23-2149083696.jpg", "http://img.b2bpic.net/free-vector/hand-drawn-hub-logo-design_23-2149857667.jpg", "http://img.b2bpic.net/free-vector/gradient-accounting-logo_23-2148844138.jpg", "http://img.b2bpic.net/free-vector/design-artwork-logo-template_23-2149507369.jpg", "http://img.b2bpic.net/free-vector/gradient-colored-data-logo-template_23-2149189483.jpg", "http://img.b2bpic.net/free-vector/hand-drawn-hub-logo-design_23-2149857670.jpg", "http://img.b2bpic.net/free-vector/hand-drawn-business-workshop-labels_23-2149422820.jpg"
|
||||
];
|
||||
|
||||
interface WaitlistFormData {
|
||||
email: string;
|
||||
instagram?: string;
|
||||
tiktok?: string;
|
||||
}
|
||||
|
||||
export default function LandingPage() {
|
||||
const [waitlistData, setWaitlistData] = useState<WaitlistFormData[]>([]);
|
||||
const [formStatus, setFormStatus] = useState<'idle' | 'success' | 'error'>('idle');
|
||||
const contactFormRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleWaitlistSubmit = async (formData: WaitlistFormData) => {
|
||||
try {
|
||||
const response = await fetch('/api/waitlist', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || 'Failed to submit form');
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
setWaitlistData([...waitlistData, formData]);
|
||||
setFormStatus('success');
|
||||
setTimeout(() => setFormStatus('idle'), 3000);
|
||||
console.log('Waitlist submission successful:', responseData);
|
||||
} catch (error) {
|
||||
setFormStatus('error');
|
||||
console.error('Error submitting waitlist form:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleJoinWaitlistClick = () => {
|
||||
if (contactFormRef.current) {
|
||||
contactFormRef.current.scrollIntoView({ behavior: 'smooth' });
|
||||
setTimeout(() => {
|
||||
const emailInput = contactFormRef.current?.querySelector('input[type="email"]') as HTMLInputElement;
|
||||
if (emailInput) {
|
||||
emailInput.focus();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
defaultButtonVariant="text-stagger"
|
||||
@@ -44,7 +93,7 @@ export default function LandingPage() {
|
||||
brandName="Clearance"
|
||||
navItems={navItems}
|
||||
button={{
|
||||
text: "Join Waitlist", href: "#contact"
|
||||
text: "Join Waitlist", onClick: handleJoinWaitlistClick
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -63,7 +112,7 @@ export default function LandingPage() {
|
||||
{ value: "5min", label: "Setup Time" }
|
||||
]}
|
||||
enableKpiAnimation={true}
|
||||
buttons={[{ text: "Join Waitlist", href: "#contact" }]}
|
||||
buttons={[{ text: "Join Waitlist", onClick: handleJoinWaitlistClick }]}
|
||||
imageSrc={heroImage}
|
||||
imageAlt="Clearance fintech dashboard interface"
|
||||
mediaAnimation="slide-up"
|
||||
@@ -82,13 +131,13 @@ export default function LandingPage() {
|
||||
animationType="slide-up"
|
||||
metrics={[
|
||||
{
|
||||
id: "1", value: "Brands", title: "Run Your Content Past Expiry", items: ["No expiration tracking", "Licenses silently expire", "Brands keep profiting—you don't"]
|
||||
id: "1", value: "Brands Use Expired Licenses", title: "Content Keeps Running Past Expiry", items: ["No expiration tracking", "Licenses silently expire", "Brands keep profiting—you don't"]
|
||||
},
|
||||
{
|
||||
id: "2", value: "You Never", title: "Invoice the Renewal", items: ["Manual renewal follow-ups", "Easy to forget", "Lost revenue disappears"]
|
||||
id: "2", value: "You Miss Renewal Invoices", title: "Nobody Remembers to Bill", items: ["Manual renewal follow-ups", "Easy to forget", "Lost revenue disappears"]
|
||||
},
|
||||
{
|
||||
id: "3", value: "That's Money", title: "You'll Never See", items: ["Average loss: $1,200/creator", "Expires quarterly", "Compounds over time"]
|
||||
id: "3", value: "Revenue Slips Away Silently", title: "Money Lost to Expired Deals", items: ["Average loss: $1,200/creator", "Expires quarterly", "Compounds over time"]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
@@ -111,7 +160,8 @@ export default function LandingPage() {
|
||||
{ label: "Import licenses", detail: "Sync with Dropbox or manual upload" },
|
||||
{ label: "Set details", detail: "Price, duration, and brand info" },
|
||||
{ label: "Activate", detail: "Start tracking immediately" }
|
||||
], completedLabel: "Setup Complete"
|
||||
],
|
||||
completedLabel: "Setup Complete"
|
||||
},
|
||||
{
|
||||
title: "License Timer Runs", description: "Real-time countdown to expiration with notifications", bentoComponent: "animated-bar-chart"
|
||||
@@ -210,22 +260,8 @@ export default function LandingPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="contact" data-section="contact">
|
||||
<ContactSplit
|
||||
tag="Waitlist"
|
||||
title="Get Early Access"
|
||||
description="Join the waitlist and be among the first to protect your IP revenue. Early adopters get lifetime discounts and 1-on-1 onboarding."
|
||||
background={{ variant: "plain" }}
|
||||
useInvertedBackground={false}
|
||||
imageSrc={heroImage}
|
||||
imageAlt="Clearance waitlist early access"
|
||||
mediaAnimation="slide-up"
|
||||
mediaPosition="right"
|
||||
inputPlaceholder="Enter your email"
|
||||
buttonText="Join Waitlist"
|
||||
termsText="We respect your privacy. Unsubscribe anytime. No spam, ever."
|
||||
tagAnimation="blur-reveal"
|
||||
/>
|
||||
<div id="contact" data-section="contact" ref={contactFormRef}>
|
||||
<WaitlistFormSection onSubmit={handleWaitlistSubmit} formStatus={formStatus} />
|
||||
</div>
|
||||
|
||||
<div id="footer" data-section="footer">
|
||||
@@ -259,4 +295,108 @@ export default function LandingPage() {
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function WaitlistFormSection({ onSubmit, formStatus }: { onSubmit: (data: WaitlistFormData) => void; formStatus: 'idle' | 'success' | 'error' }) {
|
||||
const [email, setEmail] = useState('');
|
||||
const [instagram, setInstagram] = useState('');
|
||||
const [tiktok, setTiktok] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
|
||||
if (!email) {
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmit({
|
||||
email,
|
||||
instagram: instagram || undefined,
|
||||
tiktok: tiktok || undefined
|
||||
});
|
||||
|
||||
setEmail('');
|
||||
setInstagram('');
|
||||
setTiktok('');
|
||||
setIsSubmitting(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="max-w-4xl mx-auto px-4 py-12">
|
||||
<div className="bg-card rounded-lg p-8 shadow-sm">
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold mb-2 break-words overflow-hidden text-ellipsis">Get Early Access</h2>
|
||||
<p className="text-foreground/70 break-words overflow-hidden text-ellipsis">Join the waitlist and be among the first to protect your IP revenue. Early adopters get lifetime discounts and 1-on-1 onboarding.</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium mb-2 break-words overflow-hidden text-ellipsis">
|
||||
Email Address <span className="text-primary-cta">*</span>
|
||||
</label>
|
||||
<Input
|
||||
value={email}
|
||||
onChange={setEmail}
|
||||
type="email"
|
||||
placeholder="your@email.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="instagram" className="block text-sm font-medium mb-2 break-words overflow-hidden text-ellipsis">
|
||||
Instagram Handle <span className="text-foreground/50">(optional)</span>
|
||||
</label>
|
||||
<Input
|
||||
value={instagram}
|
||||
onChange={setInstagram}
|
||||
type="text"
|
||||
placeholder="@yourhandle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="tiktok" className="block text-sm font-medium mb-2 break-words overflow-hidden text-ellipsis">
|
||||
TikTok Handle <span className="text-foreground/50">(optional)</span>
|
||||
</label>
|
||||
<Input
|
||||
value={tiktok}
|
||||
onChange={setTiktok}
|
||||
type="text"
|
||||
placeholder="@yourhandle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting || !email}
|
||||
className="w-full px-6 py-3 rounded-lg bg-primary-cta text-primary-cta-text font-medium hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed transition-opacity"
|
||||
>
|
||||
{isSubmitting ? 'Joining...' : 'Join Waitlist'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{formStatus === 'success' && (
|
||||
<div className="mt-4 p-4 bg-green-100/20 border border-green-500/30 rounded-lg text-green-700 text-sm break-words overflow-hidden text-ellipsis">
|
||||
✓ Successfully joined the waitlist! Check your email for confirmation.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formStatus === 'error' && (
|
||||
<div className="mt-4 p-4 bg-red-100/20 border border-red-500/30 rounded-lg text-red-700 text-sm break-words overflow-hidden text-ellipsis">
|
||||
× Something went wrong. Please try again.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="mt-6 text-xs text-foreground/50 text-center break-words overflow-hidden text-ellipsis">
|
||||
We respect your privacy. Unsubscribe anytime. No spam, ever.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user