Compare commits
No commits in common. "6650c031885cc2d0656be484a02b9d2bc12a153c" and "c8d6214322413e013983a58b7c0db340a09afe1e" have entirely different histories.
6650c03188
...
c8d6214322
|
Before Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 195 KiB |
|
|
@ -29,7 +29,7 @@ const blobs = [
|
||||||
|
|
||||||
export function BackgroundBlobs() {
|
export function BackgroundBlobs() {
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full absolute overflow-hidden">
|
<>
|
||||||
{blobs.map((blob, i) => (
|
{blobs.map((blob, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
|
|
@ -37,6 +37,6 @@ export function BackgroundBlobs() {
|
||||||
style={{ background: `radial-gradient(circle, ${blob.gradient} 0%, transparent 70%)` }}
|
style={{ background: `radial-gradient(circle, ${blob.gradient} 0%, transparent 70%)` }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Zap, Globe, FlaskConical, AtSign, Link } from 'lucide-react';
|
import { Zap, Globe, FlaskConical, AtSign, Link } from 'lucide-react';
|
||||||
import GlowEffect from './GlowEffect';
|
import GlowEffect from './GlowEffect';
|
||||||
import WaitlistPopup from './WaitlistPopup';
|
|
||||||
|
|
||||||
export const styles = `
|
export const styles = `
|
||||||
.gradient-text {
|
.gradient-text {
|
||||||
|
|
@ -46,36 +45,27 @@ const badges = [
|
||||||
|
|
||||||
export function HeroSection() {
|
export function HeroSection() {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [showPopup, setShowPopup] = useState(false);
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
console.log('Email submitted:', email);
|
console.log('Email submitted:', email);
|
||||||
setShowPopup(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleWaitlistSubmit = async (data: { selected: string[]; other: string }) => {
|
|
||||||
await fetch('/api/brevo', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ email, useCases: data.selected, other: data.other }),
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative pt-20 pb-20 px-6">
|
<section className="relative pt-40 pb-20 px-6">
|
||||||
<div className="max-w-4xl mx-auto text-center">
|
<div className="max-w-4xl mx-auto text-center">
|
||||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/5 border border-white/10 text-gray-400 text-xs mb-6">
|
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/5 border border-white/10 text-gray-400 text-xs mb-8">
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-gray-500 beta-dot" />
|
<span className="w-1.5 h-1.5 rounded-full bg-gray-500 beta-dot" />
|
||||||
In Active Development
|
In Active Development
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="text-5xl md:text-6xl lg:text-7xl font-bold mb-8 leading-tight">
|
<h1 className="text-5xl md:text-6xl lg:text-7xl font-bold mb-6 leading-tight">
|
||||||
AI Image Generation
|
AI Image Generation
|
||||||
<br />
|
<br />
|
||||||
<span className="gradient-text">Inside Your Workflow</span>
|
<span className="gradient-text">Inside Your Workflow</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className="text-xl text-gray-400 mb-20 max-w-2xl mx-auto">
|
<p className="text-xl text-gray-400 mb-10 max-w-2xl mx-auto">
|
||||||
Generate images via API, SDK, CLI, Lab, or live URLs.
|
Generate images via API, SDK, CLI, Lab, or live URLs.
|
||||||
<br />
|
<br />
|
||||||
Production-ready CDN delivery in seconds.
|
Production-ready CDN delivery in seconds.
|
||||||
|
|
@ -103,7 +93,7 @@ export function HeroSection() {
|
||||||
</form>
|
</form>
|
||||||
</GlowEffect>
|
</GlowEffect>
|
||||||
|
|
||||||
<p className="text-sm text-gray-500 mb-20">Free early access. No credit card required.</p>
|
<p className="text-sm text-gray-500 mb-10">Free early access. No credit card required.</p>
|
||||||
|
|
||||||
<div className="flex flex-nowrap gap-5 justify-center">
|
<div className="flex flex-nowrap gap-5 justify-center">
|
||||||
{badges.map((badge, i) => (
|
{badges.map((badge, i) => (
|
||||||
|
|
@ -123,11 +113,6 @@ export function HeroSection() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<WaitlistPopup
|
|
||||||
isOpen={showPopup}
|
|
||||||
onClose={() => setShowPopup(false)}
|
|
||||||
onSubmit={handleWaitlistSubmit}
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,364 +0,0 @@
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
|
||||||
import Image from 'next/image';
|
|
||||||
import {
|
|
||||||
X,
|
|
||||||
Check,
|
|
||||||
Globe,
|
|
||||||
ShoppingCart,
|
|
||||||
Gamepad2,
|
|
||||||
Smartphone,
|
|
||||||
Sparkles,
|
|
||||||
Palette,
|
|
||||||
PenTool,
|
|
||||||
Blocks,
|
|
||||||
Compass,
|
|
||||||
} from 'lucide-react';
|
|
||||||
|
|
||||||
import webDevImg from '../_assets/1.jpg';
|
|
||||||
import mobileDevImg from '../_assets/2.jpg';
|
|
||||||
import contentImg from '../_assets/3.jpg';
|
|
||||||
import ecommerceImg from '../_assets/4.jpg';
|
|
||||||
import vibeCodingImg from '../_assets/5.jpg';
|
|
||||||
import nocodeImg from '../_assets/6.jpg';
|
|
||||||
import gameDevImg from '../_assets/7.jpg';
|
|
||||||
import aiArtImg from '../_assets/8.jpg';
|
|
||||||
import justBrowsingImg from '../_assets/9.jpg';
|
|
||||||
|
|
||||||
interface WaitlistPopupProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
onSubmit: (data: { selected: string[]; other: string }) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const USE_CASES = [
|
|
||||||
{ id: 'web-dev', label: 'Web Dev', icon: Globe, image: webDevImg },
|
|
||||||
{ id: 'ecommerce', label: 'E-commerce', icon: ShoppingCart, image: ecommerceImg },
|
|
||||||
{ id: 'game-dev', label: 'Game Dev', icon: Gamepad2, image: gameDevImg },
|
|
||||||
{ id: 'mobile-dev', label: 'Mobile Dev', icon: Smartphone, image: mobileDevImg },
|
|
||||||
{ id: 'vibecoding', label: 'Vibecoding', icon: Sparkles, image: vibeCodingImg },
|
|
||||||
{ id: 'ai-art', label: 'AI Art', icon: Palette, image: aiArtImg },
|
|
||||||
{ id: 'content', label: 'Content', icon: PenTool, image: contentImg },
|
|
||||||
{ id: 'nocode', label: 'Nocode', icon: Blocks, image: nocodeImg },
|
|
||||||
{ id: 'just-browsing', label: 'Just came across', icon: Compass, image: justBrowsingImg, dashed: true },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function WaitlistPopup({ isOpen, onClose, onSubmit }: WaitlistPopupProps) {
|
|
||||||
const [selected, setSelected] = useState<string[]>([]);
|
|
||||||
const [other, setOther] = useState('');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleEscape = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === 'Escape' && isOpen) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('keydown', handleEscape);
|
|
||||||
return () => document.removeEventListener('keydown', handleEscape);
|
|
||||||
}, [isOpen, onClose]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isOpen) {
|
|
||||||
document.body.style.overflow = 'hidden';
|
|
||||||
} else {
|
|
||||||
document.body.style.overflow = '';
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
document.body.style.overflow = '';
|
|
||||||
};
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
const toggleSelection = useCallback((id: string) => {
|
|
||||||
setSelected((prev) => (prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
|
||||||
onSubmit({ selected, other });
|
|
||||||
setSelected([]);
|
|
||||||
setOther('');
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBackdropClick = (e: React.MouseEvent) => {
|
|
||||||
if (e.target === e.currentTarget) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isOpen) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<style>{`
|
|
||||||
.waitlist-popup-backdrop {
|
|
||||||
background: rgba(3, 7, 18, 0.6);
|
|
||||||
backdrop-filter: blur(4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-popup {
|
|
||||||
background: linear-gradient(180deg, rgba(15, 15, 30, 0.95) 0%, rgba(10, 10, 20, 0.98) 100%);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
box-shadow:
|
|
||||||
0 0 0 1px rgba(255, 255, 255, 0.05),
|
|
||||||
0 25px 50px -12px rgba(0, 0, 0, 0.5),
|
|
||||||
0 0 100px rgba(99, 102, 241, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-card {
|
|
||||||
background: rgba(255, 255, 255, 0.02);
|
|
||||||
border: 2px solid transparent;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-card::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background: linear-gradient(to right, rgba(10, 10, 20, 0.95) 0%, rgba(10, 10, 20, 0.7) 40%, transparent 70%);
|
|
||||||
z-index: 1;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-card-image {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 30%;
|
|
||||||
mask-image: linear-gradient(to right, transparent 0%, black 40%);
|
|
||||||
-webkit-mask-image: linear-gradient(to right, transparent 0%, black 40%);
|
|
||||||
opacity: 0.5;
|
|
||||||
transition: opacity 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-card:hover .waitlist-card-image {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-card.selected .waitlist-card-image {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-card.selected:hover .waitlist-card-image {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-card:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.04);
|
|
||||||
border-color: rgba(255, 255, 255, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-card.selected {
|
|
||||||
background: rgba(99, 102, 241, 0.12);
|
|
||||||
border-color: rgba(168, 85, 247, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-card.selected .waitlist-card-icon {
|
|
||||||
color: #a78bfa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-card-dashed {
|
|
||||||
background: transparent;
|
|
||||||
border: 2px dashed rgba(255, 255, 255, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-card-dashed:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.02);
|
|
||||||
border-color: rgba(255, 255, 255, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-card-dashed.selected {
|
|
||||||
background: rgba(255, 247, 237, 0.05);
|
|
||||||
border-color: rgba(255, 236, 210, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-card-dashed.selected .waitlist-card-icon {
|
|
||||||
color: rgba(255, 236, 210, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-card-dashed:hover span,
|
|
||||||
.waitlist-card-dashed.selected span {
|
|
||||||
color: rgba(255, 255, 255, 0.8) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-card-icon {
|
|
||||||
color: rgba(255, 255, 255, 0.5);
|
|
||||||
transition: color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-card:hover .waitlist-card-icon {
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-card.selected:hover .waitlist-card-icon {
|
|
||||||
color: #a78bfa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-card-dashed.selected:hover .waitlist-card-icon {
|
|
||||||
color: rgba(255, 236, 210, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-other-label {
|
|
||||||
transition: color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-other-container:focus-within .waitlist-other-label {
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-other-container.has-value .waitlist-other-label {
|
|
||||||
color: #a78bfa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-other-input {
|
|
||||||
background: rgba(255, 255, 255, 0.03);
|
|
||||||
border: 2px solid transparent;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-other-input:focus {
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
border-color: rgba(255, 255, 255, 0.4);
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-other-container.has-value .waitlist-other-input {
|
|
||||||
border-color: rgba(168, 85, 247, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-submit-btn {
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid rgba(139, 92, 246, 0.6);
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-submit-btn:hover {
|
|
||||||
background: rgba(139, 92, 246, 0.1);
|
|
||||||
border-color: rgba(139, 92, 246, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-close-btn {
|
|
||||||
color: rgba(255, 255, 255, 0.3);
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-close-btn:hover {
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-divider {
|
|
||||||
height: 1px;
|
|
||||||
background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.1) 50%, transparent 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-checkmark-ring {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: linear-gradient(135deg, rgba(34, 197, 94, 0.2) 0%, rgba(16, 185, 129, 0.1) 100%);
|
|
||||||
border: 1px solid rgba(34, 197, 94, 0.3);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.waitlist-checkmark-inner {
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: linear-gradient(135deg, #22c55e 0%, #10b981 100%);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
|
|
||||||
{/* Backdrop */}
|
|
||||||
<div
|
|
||||||
className="waitlist-popup-backdrop fixed inset-0 z-50 flex items-center justify-center p-4"
|
|
||||||
onClick={handleBackdropClick}
|
|
||||||
>
|
|
||||||
{/* Popup - wide horizontal layout */}
|
|
||||||
<div className="waitlist-popup rounded-2xl p-6 sm:p-8 w-full max-w-[900px] max-h-[90vh] overflow-y-auto relative">
|
|
||||||
{/* Close button */}
|
|
||||||
<button
|
|
||||||
className="waitlist-close-btn absolute top-4 right-4 sm:top-5 sm:right-5 p-1"
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
|
||||||
<X className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Header row: checkmark + title inline */}
|
|
||||||
<div className="flex items-center justify-center gap-4 mb-6">
|
|
||||||
<div className="waitlist-checkmark-ring shrink-0">
|
|
||||||
<div className="waitlist-checkmark-inner">
|
|
||||||
<Check className="w-3.5 h-3.5 text-white" strokeWidth={3} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h2 className="text-white text-xl font-semibold">Sweet! You'll hear from us soon</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Divider */}
|
|
||||||
<div className="waitlist-divider mb-5" />
|
|
||||||
|
|
||||||
{/* Question */}
|
|
||||||
<p className="text-white/70 text-center mb-5 text-base font-medium">
|
|
||||||
Quick one: what brings you here?
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Cards Grid - 3 rows of 3, stretched horizontally */}
|
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3 mb-4">
|
|
||||||
{USE_CASES.map(({ id, label, icon: Icon, image, dashed }) => (
|
|
||||||
<button
|
|
||||||
key={id}
|
|
||||||
className={`
|
|
||||||
waitlist-card rounded-xl py-6 px-5 flex items-center gap-3
|
|
||||||
${dashed ? 'waitlist-card-dashed' : ''}
|
|
||||||
${selected.includes(id) ? 'selected' : ''}
|
|
||||||
`}
|
|
||||||
onClick={() => toggleSelection(id)}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={image}
|
|
||||||
alt={label}
|
|
||||||
fill
|
|
||||||
className="waitlist-card-image object-cover"
|
|
||||||
/>
|
|
||||||
<Icon className="waitlist-card-icon w-5 h-5 shrink-0 relative z-10" />
|
|
||||||
<span
|
|
||||||
className={`text-sm font-medium relative z-10 ${dashed ? 'text-white/50' : 'text-white/80'}`}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Other input */}
|
|
||||||
<div className="mb-6">
|
|
||||||
<div className={`waitlist-other-container flex items-center gap-3 ${other ? 'has-value' : ''}`}>
|
|
||||||
<span className="waitlist-other-label text-white/40 text-sm whitespace-nowrap">Something cooler:</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={other}
|
|
||||||
onChange={(e) => setOther(e.target.value)}
|
|
||||||
className="waitlist-other-input flex-1 rounded-lg px-4 py-2.5 text-white text-sm placeholder:text-white/20"
|
|
||||||
placeholder=""
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Submit button */}
|
|
||||||
<button
|
|
||||||
className="waitlist-submit-btn w-full py-3 rounded-xl text-white font-semibold text-[15px]"
|
|
||||||
onClick={handleSubmit}
|
|
||||||
>
|
|
||||||
Complete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||