feat: add images
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 195 KiB |
|
|
@ -3,6 +3,7 @@
|
|||
import { useState } from 'react';
|
||||
import { Zap, Globe, FlaskConical, AtSign, Link } from 'lucide-react';
|
||||
import GlowEffect from './GlowEffect';
|
||||
import WaitlistPopup from './WaitlistPopup';
|
||||
|
||||
export const styles = `
|
||||
.gradient-text {
|
||||
|
|
@ -45,16 +46,25 @@ const badges = [
|
|||
|
||||
export function HeroSection() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [showPopup, setShowPopup] = useState(false);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
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 (
|
||||
<section className="relative pt-20 pb-20 px-6">
|
||||
<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-10">
|
||||
<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">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-gray-500 beta-dot" />
|
||||
In Active Development
|
||||
</div>
|
||||
|
|
@ -113,6 +123,11 @@ export function HeroSection() {
|
|||
))}
|
||||
</div>
|
||||
</div>
|
||||
<WaitlistPopup
|
||||
isOpen={showPopup}
|
||||
onClose={() => setShowPopup(false)}
|
||||
onSubmit={handleWaitlistSubmit}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,318 @@
|
|||
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 ecommerceImg from '../_assets/2.jpg';
|
||||
import gameDevImg from '../_assets/3.jpg';
|
||||
import mobileDevImg from '../_assets/4.jpg';
|
||||
import vibeCodingImg from '../_assets/5.jpg';
|
||||
import aiArtImg from '../_assets/6.jpg';
|
||||
import contentImg from '../_assets/7.jpg';
|
||||
import nocodeImg 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: 1px solid rgba(255, 255, 255, 0.06);
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.waitlist-card-image {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 40%;
|
||||
mask-image: linear-gradient(to right, transparent 0%, black 50%);
|
||||
-webkit-mask-image: linear-gradient(to right, transparent 0%, black 50%);
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.waitlist-card:hover .waitlist-card-image {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.waitlist-card.selected .waitlist-card-image {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.waitlist-card:hover {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.waitlist-card.selected {
|
||||
background: rgba(99, 102, 241, 0.12);
|
||||
border-color: rgba(139, 92, 246, 0.4);
|
||||
}
|
||||
|
||||
.waitlist-card.selected .waitlist-card-icon {
|
||||
color: #a78bfa;
|
||||
}
|
||||
|
||||
.waitlist-card-dashed {
|
||||
background: transparent;
|
||||
border: 1px 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.25);
|
||||
}
|
||||
|
||||
.waitlist-card-dashed.selected {
|
||||
background: rgba(99, 102, 241, 0.08);
|
||||
border-color: rgba(139, 92, 246, 0.3);
|
||||
}
|
||||
|
||||
.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-other-input {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.waitlist-other-input:focus {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-color: rgba(139, 92, 246, 0.4);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.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="flex items-center gap-3">
|
||||
<span className="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>
|
||||
</>
|
||||
);
|
||||
}
|
||||