Compare commits

..

7 Commits

Author SHA1 Message Date
Oleg Proskurin 6650c03188 feat: improve input style 2025-12-14 12:14:33 +07:00
Oleg Proskurin 4a47c53a0a feat: update cards styles 2025-12-14 12:07:31 +07:00
Oleg Proskurin 986a3063bb feat: better card styles 2025-12-14 11:53:32 +07:00
Oleg Proskurin 9c428383d7 feat: styles 2025-12-05 01:38:10 +07:00
Oleg Proskurin 84b34f0115 feat: add images 2025-12-05 01:18:05 +07:00
Oleg Proskurin 3feaed33c8 feat: update hero spacing 2025-12-05 00:10:47 +07:00
Oleg Proskurin 2190a2f55f fix: width 2025-12-04 23:06:36 +07:00
12 changed files with 386 additions and 7 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

View File

@ -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>
); );
} }

View File

@ -3,6 +3,7 @@
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 {
@ -45,27 +46,36 @@ 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-40 pb-20 px-6"> <section className="relative pt-20 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-8"> <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" /> <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-6 leading-tight"> <h1 className="text-5xl md:text-6xl lg:text-7xl font-bold mb-8 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-10 max-w-2xl mx-auto"> <p className="text-xl text-gray-400 mb-20 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.
@ -93,7 +103,7 @@ export function HeroSection() {
</form> </form>
</GlowEffect> </GlowEffect>
<p className="text-sm text-gray-500 mb-10">Free early access. No credit card required.</p> <p className="text-sm text-gray-500 mb-20">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) => (
@ -113,6 +123,11 @@ export function HeroSection() {
))} ))}
</div> </div>
</div> </div>
<WaitlistPopup
isOpen={showPopup}
onClose={() => setShowPopup(false)}
onSubmit={handleWaitlistSubmit}
/>
</section> </section>
); );
} }

View File

@ -0,0 +1,364 @@
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>
</>
);
}