Compare commits
9 Commits
feature/la
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
c8d6214322 | |
|
|
988c7946b9 | |
|
|
b2cab168bc | |
|
|
0c656d21a2 | |
|
|
4f59f775ae | |
|
|
7f37a5667b | |
|
|
d688c5890a | |
|
|
21ac410780 | |
|
|
7588817803 |
|
|
@ -42,10 +42,6 @@
|
||||||
"PERPLEXITY_TIMEOUT_MS": "600000"
|
"PERPLEXITY_TIMEOUT_MS": "600000"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"chrome-devtools": {
|
|
||||||
"command": "npx",
|
|
||||||
"args": ["-y", "chrome-devtools-mcp@latest"]
|
|
||||||
},
|
|
||||||
"browsermcp": {
|
"browsermcp": {
|
||||||
"type": "stdio",
|
"type": "stdio",
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,18 @@
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "19.1.0",
|
"@banatie/database": "workspace:*",
|
||||||
"react-dom": "19.1.0",
|
"lucide-react": "^0.400.0",
|
||||||
"next": "15.5.4",
|
"next": "15.5.4",
|
||||||
"@banatie/database": "workspace:*"
|
"react": "19.1.0",
|
||||||
|
"react-dom": "19.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"@tailwindcss/postcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"tailwindcss": "^4"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { Section } from '@/components/shared/Section';
|
|
||||||
import { GenerateFormPlaceholder } from '@/components/lab/GenerateFormPlaceholder';
|
|
||||||
|
|
||||||
const GeneratePage = () => {
|
|
||||||
return (
|
|
||||||
<Section className="py-12 md:py-16 min-h-screen">
|
|
||||||
<GenerateFormPlaceholder />
|
|
||||||
</Section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GeneratePage;
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { Section } from '@/components/shared/Section';
|
|
||||||
|
|
||||||
const ImagesPage = () => {
|
|
||||||
return (
|
|
||||||
<Section className="py-12 md:py-16 min-h-screen">
|
|
||||||
<header className="mb-8 md:mb-12">
|
|
||||||
<h1 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white mb-3">
|
|
||||||
Image Library
|
|
||||||
</h1>
|
|
||||||
<p className="text-gray-400 text-base md:text-lg">
|
|
||||||
Browse and manage your generated images
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div className="p-8 bg-slate-900/80 backdrop-blur-sm border border-slate-700 rounded-2xl">
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<div className="text-6xl mb-4">🖼️</div>
|
|
||||||
<h2 className="text-2xl font-semibold text-white mb-2">Image Browser</h2>
|
|
||||||
<p className="text-gray-400">Component will be implemented here</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ImagesPage;
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lab Section Layout
|
|
||||||
*
|
|
||||||
* Code Style:
|
|
||||||
* - Use `const` arrow function components (not function declarations)
|
|
||||||
* - Use `type` instead of `interface` for type definitions
|
|
||||||
* - Early returns for conditionals
|
|
||||||
* - No inline comments (JSDoc headers only)
|
|
||||||
* - Tailwind classes only
|
|
||||||
*
|
|
||||||
* Structure:
|
|
||||||
* - Layout components: src/components/layout/lab/
|
|
||||||
* - Feature components: src/components/lab/
|
|
||||||
* - Pages: src/app/lab/{section}/page.tsx
|
|
||||||
*
|
|
||||||
* Sub-navigation items:
|
|
||||||
* - /lab/generate - Image generation
|
|
||||||
* - /lab/images - Image library browser
|
|
||||||
* - /lab/live - Live generation testing
|
|
||||||
* - /lab/upload - File upload interface
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { ReactNode } from 'react';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import { ApiKeyWidget } from '@/components/shared/ApiKeyWidget/apikey-widget';
|
|
||||||
import { ApiKeyProvider } from '@/components/shared/ApiKeyWidget/apikey-context';
|
|
||||||
import { PageProvider } from '@/contexts/page-context';
|
|
||||||
import { LabLayout } from '@/components/layout/lab/LabLayout';
|
|
||||||
|
|
||||||
type LabLayoutWrapperProps = {
|
|
||||||
children: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
const navItems = [
|
|
||||||
{ label: 'Generate', href: '/lab/generate' },
|
|
||||||
{ label: 'Images', href: '/lab/images' },
|
|
||||||
{ label: 'Live', href: '/lab/live' },
|
|
||||||
{ label: 'Upload', href: '/lab/upload' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const LabLayoutWrapper = ({ children }: LabLayoutWrapperProps) => {
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ApiKeyProvider>
|
|
||||||
<PageProvider navItems={navItems} currentPath={pathname} rightSlot={<ApiKeyWidget />}>
|
|
||||||
<LabLayout>{children}</LabLayout>
|
|
||||||
</PageProvider>
|
|
||||||
</ApiKeyProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LabLayoutWrapper;
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { Section } from '@/components/shared/Section';
|
|
||||||
|
|
||||||
const LivePage = () => {
|
|
||||||
return (
|
|
||||||
<Section className="py-12 md:py-16 min-h-screen">
|
|
||||||
<header className="mb-8 md:mb-12">
|
|
||||||
<h1 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white mb-3">
|
|
||||||
Live Generation
|
|
||||||
</h1>
|
|
||||||
<p className="text-gray-400 text-base md:text-lg">
|
|
||||||
Real-time testing and experimentation workspace
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div className="p-8 bg-slate-900/80 backdrop-blur-sm border border-slate-700 rounded-2xl">
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<div className="text-6xl mb-4">⚡</div>
|
|
||||||
<h2 className="text-2xl font-semibold text-white mb-2">Live Testing Interface</h2>
|
|
||||||
<p className="text-gray-400">Component will be implemented here</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LivePage;
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
const LabPage = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
router.replace('/lab/generate');
|
|
||||||
}, [router]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LabPage;
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { Section } from '@/components/shared/Section';
|
|
||||||
|
|
||||||
const UploadPage = () => {
|
|
||||||
return (
|
|
||||||
<Section className="py-12 md:py-16 min-h-screen">
|
|
||||||
<header className="mb-8 md:mb-12">
|
|
||||||
<h1 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white mb-3">
|
|
||||||
File Upload
|
|
||||||
</h1>
|
|
||||||
<p className="text-gray-400 text-base md:text-lg">
|
|
||||||
Upload and manage reference images for generation
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div className="p-8 bg-slate-900/80 backdrop-blur-sm border border-slate-700 rounded-2xl">
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<div className="text-6xl mb-4">📤</div>
|
|
||||||
<h2 className="text-2xl font-semibold text-white mb-2">Upload Interface</h2>
|
|
||||||
<p className="text-gray-400">Component will be implemented here</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UploadPage;
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import Image from 'next/image';
|
|
||||||
import { LabScrollProvider, useLabScroll } from '@/contexts/lab-scroll-context';
|
|
||||||
|
|
||||||
const LabHeader = () => {
|
|
||||||
const { isScrolled } = useLabScroll();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<header
|
|
||||||
className={`
|
|
||||||
relative z-10 border-b border-white/10 backdrop-blur-sm shrink-0
|
|
||||||
transition-all duration-300 ease-in-out
|
|
||||||
${isScrolled ? 'h-0 opacity-0 overflow-hidden border-b-0' : 'h-16 opacity-100'}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<nav className="max-w-7xl mx-auto px-6 py-3 flex justify-between items-center h-16">
|
|
||||||
<div className="h-full flex items-center">
|
|
||||||
<Image
|
|
||||||
src="/banatie-logo-horisontal.png"
|
|
||||||
alt="Banatie Logo"
|
|
||||||
width={150}
|
|
||||||
height={40}
|
|
||||||
priority
|
|
||||||
className="h-full w-auto object-contain"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
href="#waitlist"
|
|
||||||
className="text-sm text-gray-300 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
Join Beta
|
|
||||||
</a>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function LabLayout({
|
|
||||||
children,
|
|
||||||
}: Readonly<{
|
|
||||||
children: React.ReactNode;
|
|
||||||
}>) {
|
|
||||||
return (
|
|
||||||
<LabScrollProvider>
|
|
||||||
<div className="h-screen overflow-hidden flex flex-col">
|
|
||||||
<LabHeader />
|
|
||||||
<div className="flex-1 min-h-0">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</LabScrollProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
import Image from 'next/image';
|
|
||||||
import { Footer } from '@/components/shared/Footer';
|
|
||||||
|
|
||||||
export default function MainLayout({
|
|
||||||
children,
|
|
||||||
}: Readonly<{
|
|
||||||
children: React.ReactNode;
|
|
||||||
}>) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<header className="relative z-10 border-b border-white/10 backdrop-blur-sm">
|
|
||||||
<nav className="max-w-7xl mx-auto px-6 py-3 flex justify-between items-center h-16">
|
|
||||||
<div className="h-full flex items-center">
|
|
||||||
<Image
|
|
||||||
src="/banatie-logo-horisontal.png"
|
|
||||||
alt="Banatie Logo"
|
|
||||||
width={150}
|
|
||||||
height={40}
|
|
||||||
priority
|
|
||||||
className="h-full w-auto object-contain"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
href="#waitlist"
|
|
||||||
className="text-sm text-gray-300 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
Join Beta
|
|
||||||
</a>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{children}
|
|
||||||
|
|
||||||
<Footer />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -45,6 +45,15 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes gradient-rotate {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.animate-gradient {
|
.animate-gradient {
|
||||||
background-size: 200% 200%;
|
background-size: 200% 200%;
|
||||||
animation: gradient-shift 3s ease infinite;
|
animation: gradient-shift 3s ease infinite;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Terminal } from 'lucide-react';
|
||||||
|
|
||||||
|
export function ApiExampleSection() {
|
||||||
|
return (
|
||||||
|
<section className="py-16 px-6">
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<div className="bg-gradient-to-b from-indigo-500/10 to-[rgba(30,27,75,0.4)] border border-indigo-500/20 backdrop-blur-[10px] rounded-2xl p-8">
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-indigo-500/20 to-purple-500/20 flex items-center justify-center">
|
||||||
|
<Terminal className="w-5 h-5 text-indigo-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold">One request. Production-ready URL.</h2>
|
||||||
|
<p className="text-gray-400 text-sm">Simple REST API that handles everything</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-black/50 border border-indigo-500/20 rounded-lg p-4 font-mono text-sm overflow-x-auto mb-4">
|
||||||
|
<div className="text-gray-500 mb-2"># Generate an image</div>
|
||||||
|
<span className="text-cyan-400">curl</span>{' '}
|
||||||
|
<span className="text-gray-300">-X POST https://api.banatie.app/v1/generate \</span>
|
||||||
|
<br />
|
||||||
|
<span className="text-gray-300 ml-4">-H</span>{' '}
|
||||||
|
<span className="text-green-400">"Authorization: Bearer $API_KEY"</span>{' '}
|
||||||
|
<span className="text-gray-300">\</span>
|
||||||
|
<br />
|
||||||
|
<span className="text-gray-300 ml-4">-d</span>{' '}
|
||||||
|
<span className="text-green-400">
|
||||||
|
'{`{"prompt": "modern office interior, natural light"}`}'
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-black/50 border border-indigo-500/20 rounded-lg p-4 font-mono text-sm overflow-x-auto">
|
||||||
|
<div className="text-gray-500 mb-2"># Response</div>
|
||||||
|
<span className="text-gray-300">{'{'}</span>
|
||||||
|
<br />
|
||||||
|
<span className="text-purple-400 ml-4">"url"</span>
|
||||||
|
<span className="text-gray-300">:</span>{' '}
|
||||||
|
<span className="text-green-400">
|
||||||
|
"https://cdn.banatie.app/img/a7x2k9.png"
|
||||||
|
</span>
|
||||||
|
<span className="text-gray-300">,</span>
|
||||||
|
<br />
|
||||||
|
<span className="text-purple-400 ml-4">"enhanced_prompt"</span>
|
||||||
|
<span className="text-gray-300">:</span>{' '}
|
||||||
|
<span className="text-green-400">"A photorealistic modern office..."</span>
|
||||||
|
<span className="text-gray-300">,</span>
|
||||||
|
<br />
|
||||||
|
<span className="text-purple-400 ml-4">"generation_time"</span>
|
||||||
|
<span className="text-gray-300">:</span> <span className="text-yellow-400">12.4</span>
|
||||||
|
<br />
|
||||||
|
<span className="text-gray-300">{'}'}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-gray-500 text-sm mt-4 text-center">
|
||||||
|
CDN-cached, optimized, ready to use. No download, no upload, no extra steps.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
const blobs = [
|
||||||
|
{
|
||||||
|
className: 'w-[600px] h-[600px] top-[-200px] right-[-100px]',
|
||||||
|
gradient: 'rgba(139, 92, 246, 0.3)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
className: 'w-[500px] h-[500px] top-[800px] left-[-150px]',
|
||||||
|
gradient: 'rgba(99, 102, 241, 0.25)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
className: 'w-[400px] h-[400px] top-[1600px] right-[-100px]',
|
||||||
|
gradient: 'rgba(236, 72, 153, 0.2)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
className: 'w-[550px] h-[550px] top-[2400px] left-[-200px]',
|
||||||
|
gradient: 'rgba(34, 211, 238, 0.15)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
className: 'w-[450px] h-[450px] top-[3200px] right-[-150px]',
|
||||||
|
gradient: 'rgba(139, 92, 246, 0.25)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
className: 'w-[500px] h-[500px] top-[4000px] left-[-100px]',
|
||||||
|
gradient: 'rgba(99, 102, 241, 0.2)',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function BackgroundBlobs() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{blobs.map((blob, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={`absolute rounded-full blur-[80px] opacity-40 pointer-events-none ${blob.className}`}
|
||||||
|
style={{ background: `radial-gradient(circle, ${blob.gradient} 0%, transparent 70%)` }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ArrowRight } from 'lucide-react';
|
||||||
|
|
||||||
|
export function FinalCtaSection() {
|
||||||
|
const scrollToTop = () => {
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
setTimeout(() => {
|
||||||
|
const input = document.querySelector('input[type="email"]') as HTMLInputElement;
|
||||||
|
input?.focus();
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id="join"
|
||||||
|
className="relative py-24 px-6 overflow-hidden"
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(180deg, #1a2744 0%, #122035 50%, #0c1628 100%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="absolute top-0 left-0 right-0 h-0.5 pointer-events-none"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
'linear-gradient(90deg, transparent 0%, rgba(34, 211, 238, 0.3) 25%, rgba(34, 211, 238, 0.6) 50%, rgba(34, 211, 238, 0.3) 75%, transparent 100%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 opacity-50 pointer-events-none"
|
||||||
|
style={{
|
||||||
|
backgroundImage:
|
||||||
|
'radial-gradient(circle at 20% 50%, rgba(34, 211, 238, 0.15) 0%, transparent 40%), radial-gradient(circle at 80% 50%, rgba(34, 211, 238, 0.1) 0%, transparent 35%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative z-10 max-w-3xl mx-auto text-center">
|
||||||
|
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold mb-6 text-white">
|
||||||
|
Ready to build?
|
||||||
|
</h2>
|
||||||
|
<p className="text-cyan-100/70 text-lg md:text-xl mb-10 max-w-2xl mx-auto">
|
||||||
|
Join developers waiting for early access. We'll notify you when your spot is ready.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={scrollToTop}
|
||||||
|
className="inline-flex items-center gap-3 px-10 py-4 bg-gradient-to-br from-indigo-500 to-purple-500 hover:from-indigo-600 hover:to-purple-600 border-none rounded-xl text-white font-semibold text-lg cursor-pointer transition-all shadow-[0_8px_30px_rgba(99,102,241,0.35)] hover:shadow-[0_14px_40px_rgba(99,102,241,0.45)] hover:-translate-y-[3px]"
|
||||||
|
>
|
||||||
|
Get Early Access
|
||||||
|
<ArrowRight className="w-5 h-5 transition-transform group-hover:translate-x-[5px]" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<p className="text-cyan-200/50 text-sm mt-8">
|
||||||
|
No credit card required • Free to start • Cancel anytime
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Zap, Check, Crown, Type, Brain, Target, Image, Award } from 'lucide-react';
|
||||||
|
|
||||||
|
const flashFeatures = [
|
||||||
|
{ text: 'Sub-3 second', detail: 'generation time' },
|
||||||
|
{ text: 'Multi-turn editing', detail: '— refine through conversation' },
|
||||||
|
{ text: 'Up to 3 reference images', detail: 'for consistency' },
|
||||||
|
{ text: '1024px', detail: 'resolution output' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const proFeatures = [
|
||||||
|
{ text: 'Up to 4K', detail: 'resolution output' },
|
||||||
|
{ text: '14 reference images', detail: 'for brand consistency' },
|
||||||
|
{ text: 'Studio controls', detail: '— lighting, focus, color grading' },
|
||||||
|
{ text: 'Thinking mode', detail: '— advanced reasoning for complex prompts' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const capabilities = [
|
||||||
|
{
|
||||||
|
icon: Type,
|
||||||
|
title: 'Perfect Text Rendering',
|
||||||
|
description:
|
||||||
|
'Legible text in images — logos, diagrams, posters. What other models still struggle with.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Brain,
|
||||||
|
title: 'Native Multimodal',
|
||||||
|
description:
|
||||||
|
'Understands text AND images in one model. Not a text model + image model bolted together.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Target,
|
||||||
|
title: 'Precise Prompt Following',
|
||||||
|
description:
|
||||||
|
'What you ask is what you get. No artistic "interpretation" that ignores your instructions.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Image,
|
||||||
|
title: 'Professional Realism',
|
||||||
|
description:
|
||||||
|
'Photorealistic output that replaces stock photos. Not fantasy art — real, usable images.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function GeminiSection() {
|
||||||
|
return (
|
||||||
|
<section className="py-20 px-6">
|
||||||
|
<div className="max-w-5xl mx-auto">
|
||||||
|
<div className="bg-gradient-to-b from-[rgba(120,90,20,0.35)] via-[rgba(60,45,10,0.5)] to-[rgba(30,20,5,0.6)] border border-yellow-500/30 rounded-2xl p-8 md:p-12">
|
||||||
|
<div className="text-center mb-10">
|
||||||
|
<div className="flex items-center justify-center gap-3 mb-4">
|
||||||
|
<Zap className="w-8 h-8 text-yellow-400" />
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold">Powered by Google Gemini</h2>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-400 max-w-2xl mx-auto">
|
||||||
|
We chose Gemini because it's the only model family that combines native
|
||||||
|
multimodal understanding with production-grade image generation. Two models, optimized
|
||||||
|
for different needs.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-6 mb-10">
|
||||||
|
<div className="bg-black/30 border border-cyan-500/30 rounded-xl p-6">
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-cyan-500/20 flex items-center justify-center">
|
||||||
|
<Zap className="w-5 h-5 text-cyan-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold text-lg">Gemini 2.5 Flash Image</h3>
|
||||||
|
<p className="text-cyan-400 text-sm">Nano Banana</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-400 text-sm mb-4">
|
||||||
|
Optimized for speed and iteration. Perfect for rapid prototyping and high-volume
|
||||||
|
generation.
|
||||||
|
</p>
|
||||||
|
<ul className="space-y-2 text-sm">
|
||||||
|
{flashFeatures.map((feature, i) => (
|
||||||
|
<li key={i} className="flex items-start gap-2">
|
||||||
|
<Check className="w-4 h-4 text-cyan-400 mt-0.5 flex-shrink-0" />
|
||||||
|
<span className="text-gray-300">
|
||||||
|
<strong className="text-white">{feature.text}</strong> {feature.detail}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-black/30 border border-yellow-500/30 rounded-xl p-6">
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-yellow-500/20 flex items-center justify-center">
|
||||||
|
<Crown className="w-5 h-5 text-yellow-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold text-lg">Gemini 3 Pro Image</h3>
|
||||||
|
<p className="text-yellow-400 text-sm">Nano Banana Pro</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-400 text-sm mb-4">
|
||||||
|
Maximum quality and creative control. For production assets and professional
|
||||||
|
workflows.
|
||||||
|
</p>
|
||||||
|
<ul className="space-y-2 text-sm">
|
||||||
|
{proFeatures.map((feature, i) => (
|
||||||
|
<li key={i} className="flex items-start gap-2">
|
||||||
|
<Check className="w-4 h-4 text-yellow-400 mt-0.5 flex-shrink-0" />
|
||||||
|
<span className="text-gray-300">
|
||||||
|
<strong className="text-white">{feature.text}</strong> {feature.detail}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-yellow-500/20 pt-8">
|
||||||
|
<h4 className="text-center font-semibold mb-6 text-gray-300">
|
||||||
|
Why Gemini outperforms competitors
|
||||||
|
</h4>
|
||||||
|
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
{capabilities.map((cap, i) => (
|
||||||
|
<div key={i} className="text-center p-4">
|
||||||
|
<div className="w-12 h-12 rounded-xl bg-yellow-500/10 flex items-center justify-center mx-auto mb-3">
|
||||||
|
<cap.icon className="w-6 h-6 text-yellow-400" />
|
||||||
|
</div>
|
||||||
|
<h5 className="font-medium text-sm mb-1">{cap.title}</h5>
|
||||||
|
<p className="text-gray-500 text-xs">{cap.description}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-center text-gray-500 text-sm mt-8">
|
||||||
|
<Award className="w-4 h-4 inline mr-1 text-yellow-400" />
|
||||||
|
Gemini 2.5 Flash Image ranked #1 on LMArena for both text-to-image and image editing
|
||||||
|
(August 2025)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { useState, useEffect, ReactNode } from 'react';
|
||||||
|
|
||||||
|
export default function GlowEffect({ children }: { children: ReactNode }) {
|
||||||
|
const [isPropertyRegistered, setIsPropertyRegistered] = useState(false);
|
||||||
|
|
||||||
|
// Register CSS property in component body (before render)
|
||||||
|
if (typeof window !== 'undefined' && 'CSS' in window && 'registerProperty' in CSS) {
|
||||||
|
try {
|
||||||
|
CSS.registerProperty({
|
||||||
|
name: '--form-angle',
|
||||||
|
syntax: '<angle>',
|
||||||
|
initialValue: '0deg',
|
||||||
|
inherits: false,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Property may already be registered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Trigger second render to add style tag
|
||||||
|
setIsPropertyRegistered(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isPropertyRegistered && (
|
||||||
|
<style>{`
|
||||||
|
@keyframes form-glow-rotate {
|
||||||
|
to {
|
||||||
|
--form-angle: 360deg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-form-wrapper {
|
||||||
|
background: linear-gradient(#0a0612, #0a0612) padding-box,
|
||||||
|
conic-gradient(from var(--form-angle, 0deg),
|
||||||
|
rgba(99, 102, 241, 0.5),
|
||||||
|
rgba(139, 92, 246, 1),
|
||||||
|
rgba(99, 102, 241, 0.5),
|
||||||
|
rgba(236, 72, 153, 0.8),
|
||||||
|
rgba(99, 102, 241, 0.5),
|
||||||
|
rgba(34, 211, 238, 1),
|
||||||
|
rgba(99, 102, 241, 0.5)
|
||||||
|
) border-box;
|
||||||
|
animation: form-glow-rotate 12s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-form-wrapper::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: -3px;
|
||||||
|
border-radius: 15px;
|
||||||
|
background: conic-gradient(from var(--form-angle, 0deg),
|
||||||
|
rgba(99, 102, 241, 0.2),
|
||||||
|
rgba(139, 92, 246, 0.7),
|
||||||
|
rgba(99, 102, 241, 0.2),
|
||||||
|
rgba(236, 72, 153, 0.5),
|
||||||
|
rgba(99, 102, 241, 0.2),
|
||||||
|
rgba(34, 211, 238, 0.7),
|
||||||
|
rgba(99, 102, 241, 0.2)
|
||||||
|
);
|
||||||
|
filter: blur(18px);
|
||||||
|
opacity: 0.75;
|
||||||
|
z-index: -1;
|
||||||
|
animation: form-glow-rotate 12s linear infinite;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center justify-center p-4">
|
||||||
|
<div className="email-form-wrapper relative isolate max-w-lg w-full mx-auto p-[2px] rounded-xl">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
export function HeroGlow() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="absolute top-0 left-1/2 -translate-x-1/2 w-full max-w-[1200px] h-[600px] pointer-events-none"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
'radial-gradient(ellipse at center top, rgba(99, 102, 241, 0.2) 0%, rgba(139, 92, 246, 0.1) 30%, transparent 70%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Zap, Globe, FlaskConical, AtSign, Link } from 'lucide-react';
|
||||||
|
import GlowEffect from './GlowEffect';
|
||||||
|
|
||||||
|
export const styles = `
|
||||||
|
.gradient-text {
|
||||||
|
background: linear-gradient(90deg, #818cf8 0%, #c084fc 25%, #f472b6 50%, #c084fc 75%, #818cf8 100%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
animation: gradient-shift 6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gradient-shift {
|
||||||
|
0% { background-position: 100% 50%; }
|
||||||
|
50% { background-position: 0% 50%; }
|
||||||
|
100% { background-position: 100% 50%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.beta-dot {
|
||||||
|
animation: beta-dot-delay 20s linear forwards, beta-dot-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) 20s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes beta-dot-delay {
|
||||||
|
0%, 99% { background-color: rgb(107, 114, 128); }
|
||||||
|
100% { background-color: rgb(74, 222, 128); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes beta-dot-pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const badges = [
|
||||||
|
{ icon: Zap, text: 'API-First', variant: 'default' },
|
||||||
|
{ icon: Globe, text: 'Built-in CDN', variant: 'default' },
|
||||||
|
{ icon: FlaskConical, text: 'Web Lab', variant: 'default' },
|
||||||
|
{ icon: AtSign, text: 'Style References', variant: 'default' },
|
||||||
|
{ icon: Link, text: 'Prompt URLs', variant: 'cyan' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function HeroSection() {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
console.log('Email submitted:', email);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="relative pt-40 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-8">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-gray-500 beta-dot" />
|
||||||
|
In Active Development
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="text-5xl md:text-6xl lg:text-7xl font-bold mb-6 leading-tight">
|
||||||
|
AI Image Generation
|
||||||
|
<br />
|
||||||
|
<span className="gradient-text">Inside Your Workflow</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="text-xl text-gray-400 mb-10 max-w-2xl mx-auto">
|
||||||
|
Generate images via API, SDK, CLI, Lab, or live URLs.
|
||||||
|
<br />
|
||||||
|
Production-ready CDN delivery in seconds.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<GlowEffect>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="flex flex-col sm:flex-row gap-2 rounded-[10px] p-1.5 sm:pl-3"
|
||||||
|
style={{ background: 'rgba(10, 6, 18, 0.95)' }}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="your@email.com"
|
||||||
|
className="flex-1 px-4 py-3 bg-transparent border-none rounded-md text-white outline-none placeholder:text-white/40 focus:bg-white/[0.03]"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-6 py-3 bg-gradient-to-br from-indigo-500 to-purple-500 hover:from-indigo-600 hover:to-purple-600 rounded-md text-white font-semibold cursor-pointer transition-all whitespace-nowrap"
|
||||||
|
>
|
||||||
|
Get Early Access
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</GlowEffect>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
{badges.map((badge, i) => (
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
className={`px-6 py-2.5 rounded-full text-sm flex items-center gap-2.5 whitespace-nowrap ${
|
||||||
|
badge.variant === 'cyan'
|
||||||
|
? 'bg-cyan-500/10 border border-cyan-500/30 text-cyan-300'
|
||||||
|
: 'bg-indigo-500/15 border border-indigo-500/30 text-indigo-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<badge.icon
|
||||||
|
className={`w-4 h-4 ${badge.variant === 'cyan' ? 'text-cyan-400' : 'text-indigo-400'}`}
|
||||||
|
/>
|
||||||
|
{badge.text}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Settings2, Check, Info } from 'lucide-react';
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{ number: 1, title: 'Your Prompt', subtitle: '"a cat on windowsill"' },
|
||||||
|
{ number: 2, title: 'Smart Enhancement', subtitle: 'Style + details added' },
|
||||||
|
{ number: 3, title: 'AI Generation', subtitle: 'Gemini creates image' },
|
||||||
|
{ number: 4, title: 'CDN Delivery', subtitle: 'Instant global URL' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const controls = [
|
||||||
|
{ text: 'Style templates', detail: '— photorealistic, illustration, minimalist, and more' },
|
||||||
|
{ text: 'Reference images', detail: '— @aliases maintain visual consistency' },
|
||||||
|
{ text: 'Output specs', detail: '— aspect ratio, dimensions, format' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function HowItWorksSection() {
|
||||||
|
return (
|
||||||
|
<section className="py-20 px-6">
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold text-center mb-4">
|
||||||
|
Your prompt. Your control. Production-ready.
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-400 text-center mb-16 max-w-2xl mx-auto">
|
||||||
|
We handle the complexity so you can focus on building.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<div className="grid md:grid-cols-4 gap-4 mb-8">
|
||||||
|
{steps.map((step) => (
|
||||||
|
<div key={step.number} className="text-center p-4">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-indigo-500 to-purple-500 shadow-[0_2px_10px_rgba(99,102,241,0.4)] flex items-center justify-center mx-auto mb-3 text-sm font-bold">
|
||||||
|
{step.number}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-medium mb-1">{step.title}</p>
|
||||||
|
<p className="text-xs text-gray-500">{step.subtitle}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gradient-to-b from-indigo-500/10 to-[rgba(30,27,75,0.4)] border border-indigo-500/20 backdrop-blur-[10px] rounded-xl p-6 mt-8">
|
||||||
|
<h3 className="font-semibold text-lg mb-4 flex items-center gap-2">
|
||||||
|
<Settings2 className="w-5 h-5 text-indigo-400" />
|
||||||
|
What you control
|
||||||
|
</h3>
|
||||||
|
<div className="grid md:grid-cols-3 gap-4 text-sm">
|
||||||
|
{controls.map((control, i) => (
|
||||||
|
<div key={i} className="flex items-start gap-2">
|
||||||
|
<Check className="w-4 h-4 text-green-400 mt-0.5 flex-shrink-0" />
|
||||||
|
<span className="text-gray-300">
|
||||||
|
<strong className="text-white">{control.text}</strong> {control.detail}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-center text-gray-500 text-sm mt-6">
|
||||||
|
<Info className="w-4 h-4 inline mr-1" />
|
||||||
|
Enhanced prompts are visible in API response. You always see what was generated.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Server, Code, Cpu, Terminal, FlaskConical, Link2 } from 'lucide-react';
|
||||||
|
|
||||||
|
const tools = [
|
||||||
|
{ icon: Server, text: 'REST API', color: 'text-cyan-400' },
|
||||||
|
{ icon: Code, text: 'TypeScript SDK', color: 'text-blue-400' },
|
||||||
|
{ icon: Cpu, text: 'MCP Server', color: 'text-purple-400' },
|
||||||
|
{ icon: Terminal, text: 'CLI', color: 'text-green-400' },
|
||||||
|
{ icon: FlaskConical, text: 'Banatie Lab', color: 'text-orange-400' },
|
||||||
|
{ icon: Link2, text: 'Prompt URLs', color: 'text-cyan-400', highlight: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function IntegrationsSection() {
|
||||||
|
return (
|
||||||
|
<section className="py-20 px-6">
|
||||||
|
<div className="max-w-6xl mx-auto text-center">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold mb-4">Works with your tools</h2>
|
||||||
|
<p className="text-gray-400 mb-12 max-w-2xl mx-auto">
|
||||||
|
Use what fits your workflow. All methods, same capabilities.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap justify-center gap-4 mb-8">
|
||||||
|
{tools.map((tool, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={`bg-[rgba(30,27,75,0.6)] border rounded-lg px-6 py-3 flex items-center gap-2 ${
|
||||||
|
tool.highlight ? 'border-cyan-500/30' : 'border-indigo-500/20'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<tool.icon className={`w-5 h-5 ${tool.color}`} />
|
||||||
|
<span>{tool.text}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-2xl mx-auto mt-8 p-4 bg-slate-900/60 border border-indigo-500/15 backdrop-blur-[10px] rounded-lg">
|
||||||
|
<p className="text-sm text-gray-400">
|
||||||
|
<strong className="text-white">Banatie Lab</strong> — Official web interface for Banatie
|
||||||
|
API. Generate images, build flows, browse your gallery, and explore all capabilities
|
||||||
|
with ready-to-use code snippets.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-gray-500 text-sm mt-6">
|
||||||
|
Perfect for Claude Code, Cursor, and any AI-powered workflow.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { AtSign, GitBranch, Palette, Globe, SlidersHorizontal, Link } from 'lucide-react';
|
||||||
|
|
||||||
|
const features = [
|
||||||
|
{
|
||||||
|
icon: AtSign,
|
||||||
|
iconColor: 'text-pink-400',
|
||||||
|
title: 'Reference Images',
|
||||||
|
description:
|
||||||
|
'Use @aliases to maintain style consistency across your project. Reference up to 3 images per generation.',
|
||||||
|
isUnique: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: GitBranch,
|
||||||
|
iconColor: 'text-purple-400',
|
||||||
|
title: 'Flows',
|
||||||
|
description:
|
||||||
|
'Chain generations, iterate on results, build image sequences with @last and @first references.',
|
||||||
|
isUnique: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Palette,
|
||||||
|
iconColor: 'text-yellow-400',
|
||||||
|
title: '7 Style Templates',
|
||||||
|
description:
|
||||||
|
'Same prompt, different styles. Photorealistic, illustration, minimalist, product, comic, sticker, and more.',
|
||||||
|
isUnique: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Globe,
|
||||||
|
iconColor: 'text-green-400',
|
||||||
|
title: 'Instant CDN Delivery',
|
||||||
|
description:
|
||||||
|
'Every image gets production-ready URL. No upload, no optimization, no hosting setup needed.',
|
||||||
|
isUnique: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: SlidersHorizontal,
|
||||||
|
iconColor: 'text-blue-400',
|
||||||
|
title: 'Output Control',
|
||||||
|
description:
|
||||||
|
'Control aspect ratio, dimensions, and format. From square thumbnails to ultra-wide banners.',
|
||||||
|
isUnique: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Link,
|
||||||
|
iconColor: 'text-cyan-400',
|
||||||
|
title: 'Prompt URLs',
|
||||||
|
description:
|
||||||
|
'Generate images via URL parameters. Put prompt in img src, get real image. Built-in caching.',
|
||||||
|
isUnique: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function KeyFeaturesSection() {
|
||||||
|
return (
|
||||||
|
<section className="py-20 px-6">
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold text-center mb-4">
|
||||||
|
Built for real development workflows
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-400 text-center mb-16 max-w-2xl mx-auto">
|
||||||
|
Everything you need to integrate AI images into your projects.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{features.map((feature, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={`rounded-xl p-6 ${
|
||||||
|
feature.isUnique
|
||||||
|
? 'bg-gradient-to-br from-cyan-500/10 to-indigo-500/[0.08] border border-cyan-500/30'
|
||||||
|
: 'bg-gradient-to-b from-indigo-500/10 to-[rgba(30,27,75,0.4)] border border-indigo-500/20 backdrop-blur-[10px]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-indigo-500/20 to-purple-500/20 flex items-center justify-center mb-4">
|
||||||
|
<feature.icon className={`w-6 h-6 ${feature.iconColor}`} />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<h3 className="font-semibold text-lg">{feature.title}</h3>
|
||||||
|
{feature.isUnique && (
|
||||||
|
<span className="px-2 py-0.5 bg-cyan-500/20 text-cyan-300 text-xs rounded">
|
||||||
|
Unique
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-400 text-sm">{feature.description}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { RefreshCw, ArrowLeftRight, Package, Layers, Check } from 'lucide-react';
|
||||||
|
|
||||||
|
const problems = [
|
||||||
|
{
|
||||||
|
icon: RefreshCw,
|
||||||
|
title: 'Placeholder hell',
|
||||||
|
problem: '"I\'ll add images later" never happens',
|
||||||
|
solution: 'Generate real images as you build',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: ArrowLeftRight,
|
||||||
|
title: 'Context switching',
|
||||||
|
problem: 'Leave IDE, generate elsewhere, come back',
|
||||||
|
solution: 'Stay in your workflow. API, SDK, MCP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Package,
|
||||||
|
title: 'Asset management',
|
||||||
|
problem: 'Download, optimize, upload, get URL',
|
||||||
|
solution: 'Production CDN URLs instantly',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Layers,
|
||||||
|
title: 'Style drift',
|
||||||
|
problem: 'Every image looks different',
|
||||||
|
solution: 'Reference images keep style consistent',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ProblemSolutionSection() {
|
||||||
|
return (
|
||||||
|
<section className="py-20 px-6">
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold text-center mb-4">
|
||||||
|
Why developers choose Banatie
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-400 text-center mb-16 max-w-2xl mx-auto">
|
||||||
|
Stop fighting your image workflow. Start building.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
|
{problems.map((item, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="bg-gradient-to-b from-[rgba(127,29,29,0.25)] to-[rgba(30,10,20,0.7)] border border-red-400/20 backdrop-blur-[10px] rounded-xl p-6 flex flex-col min-h-[240px]"
|
||||||
|
>
|
||||||
|
<div className="flex-1 flex flex-col">
|
||||||
|
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-indigo-500/20 to-purple-500/20 flex items-center justify-center mb-4">
|
||||||
|
<item.icon className="w-6 h-6 text-red-400" />
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold text-lg mb-2 text-red-400">{item.title}</h3>
|
||||||
|
<p className="text-gray-500 text-sm mb-4">{item.problem}</p>
|
||||||
|
<div className="mt-auto flex items-start gap-1 text-sm">
|
||||||
|
<Check className="w-4 h-4 text-green-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<span className="text-white">{item.solution}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Sparkles } from 'lucide-react';
|
||||||
|
|
||||||
|
export function PromptUrlsSection() {
|
||||||
|
return (
|
||||||
|
<section className="py-16 px-6">
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<div className="bg-gradient-to-br from-cyan-500/10 to-indigo-500/[0.08] border border-cyan-500/30 rounded-2xl p-8">
|
||||||
|
<div className="flex items-start gap-4 mb-6">
|
||||||
|
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-indigo-500/20 to-purple-500/20 flex items-center justify-center flex-shrink-0">
|
||||||
|
<Sparkles className="w-6 h-6 text-cyan-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="inline-block px-3 py-1 bg-cyan-500/20 text-cyan-300 text-xs rounded-full mb-2">
|
||||||
|
Unique
|
||||||
|
</span>
|
||||||
|
<h2 className="text-2xl font-bold mb-2">Prompt URLs — Images via HTML</h2>
|
||||||
|
<p className="text-gray-400">
|
||||||
|
Put a prompt in your{' '}
|
||||||
|
<code className="text-cyan-300 bg-black/30 px-1 rounded">img src</code> and get a
|
||||||
|
real image. No API calls. No JavaScript. Just HTML.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-black/50 border border-indigo-500/20 rounded-lg p-4 font-mono text-sm overflow-x-auto">
|
||||||
|
<span className="text-gray-500"><!-- Write this --></span>
|
||||||
|
<br />
|
||||||
|
<span className="text-purple-400"><img</span>{' '}
|
||||||
|
<span className="text-cyan-300">src</span>=
|
||||||
|
<span className="text-green-400">
|
||||||
|
"https://cdn.banatie.app/gen?p=modern office interior"
|
||||||
|
</span>{' '}
|
||||||
|
<span className="text-purple-400">/></span>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<span className="text-gray-500">
|
||||||
|
<!-- Get this: production-ready image, cached, CDN-delivered -->
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-gray-500 text-sm mt-4">
|
||||||
|
Perfect for static sites, prototypes, and AI coding agents that generate HTML.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { MessageCircle, Vote, Users } from 'lucide-react';
|
||||||
|
|
||||||
|
const ZIGZAG_POINTS = [
|
||||||
|
40, 0, 20, 10, 30, 50, 20, 15, 0, 20, 25, 50, 10, 20, 0, 15, 10, 25, 20, 50, 0, 20, 40, 20, 25,
|
||||||
|
10, 0, 15, 0, 20, 0, 40, 10, 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
function generateZigzagClipPath(yValues: number[]): string {
|
||||||
|
const lastIndex = yValues.length - 1;
|
||||||
|
const getX = (i: number) => `${(i / lastIndex) * 100}%`;
|
||||||
|
|
||||||
|
const topEdge = yValues.map((y, i) => `${getX(i)} ${y}px`).join(', ');
|
||||||
|
const bottomEdge = [...yValues]
|
||||||
|
.map((y, i) => [getX(i), y] as const)
|
||||||
|
.reverse()
|
||||||
|
.map(([x, y]) => `${x} calc(100% - 50px + ${y}px)`)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
return `polygon(${topEdge}, ${bottomEdge})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const styles = `
|
||||||
|
.shape-future {
|
||||||
|
clip-path: ${generateZigzagClipPath(ZIGZAG_POINTS)};
|
||||||
|
}
|
||||||
|
|
||||||
|
.metal-texture {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metal-texture::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 300 300' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
|
||||||
|
opacity: 0.5;
|
||||||
|
mix-blend-mode: multiply;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shape-future-title {
|
||||||
|
font-family: 'Caveat', cursive;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const features = [
|
||||||
|
{ icon: MessageCircle, text: 'Direct feedback channel' },
|
||||||
|
{ icon: Vote, text: 'Feature voting' },
|
||||||
|
{ icon: Users, text: 'Early adopter community' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ShapeTheFutureSection() {
|
||||||
|
return (
|
||||||
|
<div className="relative my-[60px]">
|
||||||
|
<section className="shape-future metal-texture bg-[#2a2a2a] relative z-[2]">
|
||||||
|
<section className="shape-future bg-black absolute w-full h-[500px] top-[-446px] left-[2px] opacity-30 z-[2] " />
|
||||||
|
<div className="absolute h-[200px] w-full blur-sm">
|
||||||
|
<section className="shape-future bg-black absolute w-full h-[500px] top-[-430px] left-[2px] opacity-30 z-[2] " />
|
||||||
|
</div>
|
||||||
|
<div className="absolute h-[200px] bottom-[-50px] w-full blur-sm">
|
||||||
|
<section className="shape-future bg-black absolute w-full h-[500px] bottom-[-388px] left-[2px] opacity-20 z-[2] " />
|
||||||
|
</div>
|
||||||
|
<section className="shape-future bg-white absolute w-full h-[500px] bottom-[-449px] left-[1px] opacity-30 z-[2] " />
|
||||||
|
<div className="relative z-[6] pt-[100px] pb-[60px] px-10 text-center max-w-[700px] mx-auto">
|
||||||
|
<h2 className="shape-future-title text-5xl font-semibold text-[#f5f5f5] mb-4 leading-tight">
|
||||||
|
Shape the future of Banatie
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p className="text-[1.05rem] text-[#a0a0a0] mb-6 leading-relaxed">
|
||||||
|
We're building this for developers like you. Early adopters get direct influence on
|
||||||
|
our roadmap — suggest features, vote on priorities, and help us build exactly what you
|
||||||
|
need.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-6 justify-center text-[0.95rem] text-[#888] mb-20">
|
||||||
|
{features.map((feature, i) => (
|
||||||
|
<span key={i} className="flex items-center gap-2">
|
||||||
|
<feature.icon className="w-4 h-4" />
|
||||||
|
{feature.text}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
export { BackgroundBlobs } from './BackgroundBlobs';
|
||||||
|
export { HeroGlow } from './HeroGlow';
|
||||||
|
export { HeroSection, styles as heroStyles } from './HeroSection';
|
||||||
|
export { ApiExampleSection } from './ApiExampleSection';
|
||||||
|
export { ProblemSolutionSection } from './ProblemSolutionSection';
|
||||||
|
export { PromptUrlsSection } from './PromptUrlsSection';
|
||||||
|
export { HowItWorksSection } from './HowItWorksSection';
|
||||||
|
export { KeyFeaturesSection } from './KeyFeaturesSection';
|
||||||
|
export { IntegrationsSection } from './IntegrationsSection';
|
||||||
|
export { ShapeTheFutureSection, styles as shapeFutureStyles } from './ShapeTheFutureSection';
|
||||||
|
export { GeminiSection } from './GeminiSection';
|
||||||
|
export { FinalCtaSection } from './FinalCtaSection';
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
BackgroundBlobs,
|
||||||
|
HeroGlow,
|
||||||
|
HeroSection,
|
||||||
|
heroStyles,
|
||||||
|
ApiExampleSection,
|
||||||
|
ProblemSolutionSection,
|
||||||
|
PromptUrlsSection,
|
||||||
|
HowItWorksSection,
|
||||||
|
KeyFeaturesSection,
|
||||||
|
IntegrationsSection,
|
||||||
|
ShapeTheFutureSection,
|
||||||
|
shapeFutureStyles,
|
||||||
|
GeminiSection,
|
||||||
|
FinalCtaSection,
|
||||||
|
} from './_components';
|
||||||
|
|
||||||
|
const customStyles = `
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Caveat:wght@500;600;700&display=swap');
|
||||||
|
|
||||||
|
${heroStyles}
|
||||||
|
|
||||||
|
${shapeFutureStyles}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<style dangerouslySetInnerHTML={{ __html: customStyles }} />
|
||||||
|
<div className="relative">
|
||||||
|
<BackgroundBlobs />
|
||||||
|
<HeroGlow />
|
||||||
|
|
||||||
|
<HeroSection />
|
||||||
|
<ApiExampleSection />
|
||||||
|
<ProblemSolutionSection />
|
||||||
|
<PromptUrlsSection />
|
||||||
|
<HowItWorksSection />
|
||||||
|
<KeyFeaturesSection />
|
||||||
|
<IntegrationsSection />
|
||||||
|
<ShapeTheFutureSection />
|
||||||
|
<GeminiSection />
|
||||||
|
<FinalCtaSection />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { Inter } from 'next/font/google';
|
import { Inter } from 'next/font/google';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { Footer } from '@/components/shared/Footer';
|
||||||
import './globals.css';
|
import './globals.css';
|
||||||
|
|
||||||
const inter = Inter({
|
const inter = Inter({
|
||||||
|
|
@ -69,12 +71,38 @@ export default function RootLayout({
|
||||||
</head>
|
</head>
|
||||||
<body className={`${inter.variable} antialiased`}>
|
<body className={`${inter.variable} antialiased`}>
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950">
|
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950">
|
||||||
|
{/* Animated gradient background */}
|
||||||
<div className="fixed inset-0 overflow-hidden pointer-events-none">
|
<div className="fixed inset-0 overflow-hidden pointer-events-none">
|
||||||
<div className="absolute top-1/4 -left-1/4 w-96 h-96 bg-purple-600/10 rounded-full blur-3xl animate-pulse"></div>
|
<div className="absolute top-1/4 -left-1/4 w-96 h-96 bg-purple-600/10 rounded-full blur-3xl animate-pulse"></div>
|
||||||
<div className="absolute bottom-1/4 -right-1/4 w-96 h-96 bg-cyan-600/10 rounded-full blur-3xl animate-pulse delay-700"></div>
|
<div className="absolute bottom-1/4 -right-1/4 w-96 h-96 bg-cyan-600/10 rounded-full blur-3xl animate-pulse delay-700"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<header className="relative z-10 border-b border-white/10 backdrop-blur-sm">
|
||||||
|
<nav className="max-w-7xl mx-auto px-6 py-3 flex justify-between items-center h-16">
|
||||||
|
<div className="h-full flex items-center">
|
||||||
|
<Image
|
||||||
|
src="/banatie-logo-horisontal.png"
|
||||||
|
alt="Banatie Logo"
|
||||||
|
width={150}
|
||||||
|
height={40}
|
||||||
|
priority
|
||||||
|
className="h-full w-auto object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="#waitlist"
|
||||||
|
className="text-sm text-gray-300 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
Join Beta
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Page content */}
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter Placeholder Component
|
|
||||||
*
|
|
||||||
* Checkbox/radio button group for sidebar filters.
|
|
||||||
* Supports both single-select (radio) and multi-select (checkbox) modes.
|
|
||||||
*
|
|
||||||
* Features:
|
|
||||||
* - Radio buttons for single selection
|
|
||||||
* - Checkboxes for multiple selection
|
|
||||||
* - Option counts (e.g., "All (127)")
|
|
||||||
* - Accessible keyboard navigation
|
|
||||||
* - Focus indicators
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
type FilterOption = {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
count?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type FilterPlaceholderProps = {
|
|
||||||
options: FilterOption[];
|
|
||||||
multiSelect?: boolean;
|
|
||||||
groupId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FilterPlaceholder = ({
|
|
||||||
options,
|
|
||||||
multiSelect = false,
|
|
||||||
groupId,
|
|
||||||
}: FilterPlaceholderProps) => {
|
|
||||||
const [selected, setSelected] = useState<string[]>(multiSelect ? [] : [options[0]?.id || '']);
|
|
||||||
|
|
||||||
const handleSelect = (optionId: string) => {
|
|
||||||
if (multiSelect) {
|
|
||||||
setSelected((prev) =>
|
|
||||||
prev.includes(optionId) ? prev.filter((id) => id !== optionId) : [...prev, optionId]
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setSelected([optionId]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isSelected = (optionId: string) => selected.includes(optionId);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mt-2 space-y-1" role="group" aria-label={`${groupId} filters`}>
|
|
||||||
{options.map((option) => {
|
|
||||||
const checked = isSelected(option.id);
|
|
||||||
const inputId = `${groupId}-${option.id}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<label
|
|
||||||
key={option.id}
|
|
||||||
htmlFor={inputId}
|
|
||||||
className="flex items-center gap-2 px-2 py-1.5 rounded-md text-sm cursor-pointer transition-colors hover:bg-white/5 group"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type={multiSelect ? 'checkbox' : 'radio'}
|
|
||||||
id={inputId}
|
|
||||||
name={groupId}
|
|
||||||
checked={checked}
|
|
||||||
onChange={() => handleSelect(option.id)}
|
|
||||||
className="w-4 h-4 bg-slate-800 border-slate-600 text-purple-600 focus:ring-2 focus:ring-purple-500 focus:ring-offset-0 rounded cursor-pointer"
|
|
||||||
/>
|
|
||||||
<span className={`flex-1 ${checked ? 'text-white font-medium' : 'text-gray-400 group-hover:text-gray-300'}`}>
|
|
||||||
{option.label}
|
|
||||||
</span>
|
|
||||||
{option.count !== undefined && (
|
|
||||||
<span className="text-xs text-gray-600">
|
|
||||||
{option.count}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,211 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate Form Placeholder Component
|
|
||||||
*
|
|
||||||
* Main content placeholder for lab generation interface.
|
|
||||||
* Matches the /demo/tti page visual style with form card and results area.
|
|
||||||
*
|
|
||||||
* Features:
|
|
||||||
* - Header with title and description
|
|
||||||
* - Prompt textarea (similar to TTI page)
|
|
||||||
* - Options row with selects and buttons
|
|
||||||
* - Submit button with gradient styling
|
|
||||||
* - Results area placeholder
|
|
||||||
* - Keyboard shortcuts (Ctrl+Enter)
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useState, useRef, KeyboardEvent } from 'react';
|
|
||||||
|
|
||||||
export const GenerateFormPlaceholder = () => {
|
|
||||||
const [prompt, setPrompt] = useState('');
|
|
||||||
const [aspectRatio, setAspectRatio] = useState('1:1');
|
|
||||||
const [template, setTemplate] = useState('photorealistic');
|
|
||||||
const [generating, setGenerating] = useState(false);
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
||||||
|
|
||||||
const handleGenerate = () => {
|
|
||||||
if (!prompt.trim()) return;
|
|
||||||
setGenerating(true);
|
|
||||||
setTimeout(() => setGenerating(false), 2000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
||||||
if (e.ctrlKey && e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
handleGenerate();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen">
|
|
||||||
{/* Page Header */}
|
|
||||||
<header className="mb-8 md:mb-12">
|
|
||||||
<h1 className="text-3xl md:text-4xl lg:text-5xl font-bold text-white mb-3">
|
|
||||||
Generate Workbench
|
|
||||||
</h1>
|
|
||||||
<p className="text-gray-400 text-base md:text-lg">
|
|
||||||
Create and experiment with AI-generated images
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Info Banner (Placeholder) */}
|
|
||||||
<div className="mb-6 p-5 bg-purple-900/10 border border-purple-700/50 rounded-2xl">
|
|
||||||
<div className="flex items-center justify-between flex-wrap gap-4">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold text-white mb-1">Lab Environment</h3>
|
|
||||||
<p className="text-sm text-gray-400">
|
|
||||||
This is an experimental interface for testing generation features
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-sm text-purple-400">
|
|
||||||
<span className="w-2 h-2 bg-purple-500 rounded-full animate-pulse"></span>
|
|
||||||
<span>Active</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Generation Form Card */}
|
|
||||||
<section
|
|
||||||
className="mb-8 p-6 bg-slate-900/80 backdrop-blur-sm border border-slate-700 rounded-2xl"
|
|
||||||
aria-label="Image Generation Form"
|
|
||||||
>
|
|
||||||
{/* Prompt Textarea */}
|
|
||||||
<div className="mb-4">
|
|
||||||
<label htmlFor="lab-prompt-input" className="block text-lg font-semibold text-white mb-3">
|
|
||||||
Your Prompt
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
id="lab-prompt-input"
|
|
||||||
ref={textareaRef}
|
|
||||||
value={prompt}
|
|
||||||
onChange={(e) => setPrompt(e.target.value)}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
placeholder="Describe the image you want to generate..."
|
|
||||||
disabled={generating}
|
|
||||||
rows={5}
|
|
||||||
className="w-full px-4 py-3 bg-slate-800 border border-slate-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed resize-none"
|
|
||||||
aria-label="Image generation prompt"
|
|
||||||
/>
|
|
||||||
<p className="mt-2 text-xs text-gray-500">
|
|
||||||
Tip: Be specific and descriptive for better results
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Options Row */}
|
|
||||||
<div className="mb-4 grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
||||||
{/* Aspect Ratio */}
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="lab-aspect-ratio"
|
|
||||||
className="block text-xs font-medium text-gray-400 mb-1.5"
|
|
||||||
>
|
|
||||||
Aspect Ratio
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id="lab-aspect-ratio"
|
|
||||||
value={aspectRatio}
|
|
||||||
onChange={(e) => setAspectRatio(e.target.value)}
|
|
||||||
disabled={generating}
|
|
||||||
className="w-full px-3 py-2 text-sm bg-slate-800 border border-slate-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
<option value="1:1">Square (1:1)</option>
|
|
||||||
<option value="3:4">Portrait (3:4)</option>
|
|
||||||
<option value="4:3">Landscape (4:3)</option>
|
|
||||||
<option value="9:16">Vertical (9:16)</option>
|
|
||||||
<option value="16:9">Widescreen (16:9)</option>
|
|
||||||
<option value="21:9">Ultrawide (21:9)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Template */}
|
|
||||||
<div>
|
|
||||||
<label htmlFor="lab-template" className="block text-xs font-medium text-gray-400 mb-1.5">
|
|
||||||
Style Template
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id="lab-template"
|
|
||||||
value={template}
|
|
||||||
onChange={(e) => setTemplate(e.target.value)}
|
|
||||||
disabled={generating}
|
|
||||||
className="w-full px-3 py-2 text-sm bg-slate-800 border border-slate-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
<option value="photorealistic">Photorealistic</option>
|
|
||||||
<option value="illustration">Illustration</option>
|
|
||||||
<option value="minimalist">Minimalist</option>
|
|
||||||
<option value="sticker">Sticker</option>
|
|
||||||
<option value="product">Product</option>
|
|
||||||
<option value="comic">Comic</option>
|
|
||||||
<option value="general">General</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Advanced Options Button */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-400 mb-1.5">
|
|
||||||
Advanced Options
|
|
||||||
</label>
|
|
||||||
<button
|
|
||||||
disabled={generating}
|
|
||||||
className="w-full px-3 py-2 text-sm bg-slate-800 border border-slate-700 rounded-lg text-gray-400 hover:text-white hover:bg-slate-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
|
||||||
aria-label="Advanced options"
|
|
||||||
>
|
|
||||||
<span>⚙️</span>
|
|
||||||
<span>Configure</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Submit Button Row */}
|
|
||||||
<div className="flex items-center justify-between gap-4 flex-wrap pt-2 border-t border-slate-700/50">
|
|
||||||
<div className="text-sm text-gray-500">
|
|
||||||
{generating ? (
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
<span className="w-2 h-2 bg-purple-500 rounded-full animate-pulse"></span>
|
|
||||||
Generating...
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
'Press Ctrl+Enter to submit'
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={handleGenerate}
|
|
||||||
disabled={generating || !prompt.trim()}
|
|
||||||
className="px-6 py-2.5 rounded-lg bg-gradient-to-r from-purple-600 to-cyan-600 text-white font-semibold hover:from-purple-500 hover:to-cyan-500 transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-purple-900/30 focus:ring-2 focus:ring-purple-500"
|
|
||||||
>
|
|
||||||
{generating ? 'Generating...' : 'Generate Images'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Results Area Placeholder */}
|
|
||||||
<section className="space-y-6" aria-label="Generated Results">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h2 className="text-xl md:text-2xl font-bold text-white">Results</h2>
|
|
||||||
<button className="text-sm text-gray-500 hover:text-gray-300 transition-colors">
|
|
||||||
Clear All
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Empty State */}
|
|
||||||
<div className="p-12 bg-slate-900/50 backdrop-blur-sm border border-slate-700 rounded-2xl text-center">
|
|
||||||
<div className="mb-4 text-5xl">🎨</div>
|
|
||||||
<h3 className="text-lg font-semibold text-white mb-2">No results yet</h3>
|
|
||||||
<p className="text-gray-400 text-sm">
|
|
||||||
Generate your first image to see results here
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Result Cards Placeholder (would appear after generation) */}
|
|
||||||
{generating && (
|
|
||||||
<div className="p-6 bg-slate-900/80 backdrop-blur-sm border border-slate-700 rounded-2xl animate-pulse">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div className="aspect-square bg-slate-800 rounded-lg"></div>
|
|
||||||
<div className="aspect-square bg-slate-800 rounded-lg"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lab Footer Component
|
|
||||||
*
|
|
||||||
* Simple 1-line footer for lab section with contextual navigation links.
|
|
||||||
* Displays copyright on left and contextual docs/API links on right.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
type LinkMapEntry = {
|
|
||||||
docs: string;
|
|
||||||
api: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type LinkMap = {
|
|
||||||
[key: string]: LinkMapEntry;
|
|
||||||
};
|
|
||||||
|
|
||||||
const linkMap: LinkMap = {
|
|
||||||
'/lab/generate': { docs: '/docs', api: '/docs/api/text-to-image' },
|
|
||||||
'/lab/images': { docs: '/docs', api: '/docs/api/images' },
|
|
||||||
'/lab/live': { docs: '/docs', api: '/docs/api/text-to-image' },
|
|
||||||
'/lab/upload': { docs: '/docs', api: '/docs/api/upload' },
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LabFooter = () => {
|
|
||||||
const pathname = usePathname();
|
|
||||||
const links = linkMap[pathname] || { docs: '/docs', api: '/docs/api' };
|
|
||||||
|
|
||||||
return (
|
|
||||||
<footer
|
|
||||||
className="border-t border-white/10 bg-slate-950/50 backdrop-blur-sm"
|
|
||||||
role="contentinfo"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 md:gap-0 px-6 py-4 md:h-14">
|
|
||||||
{/* Left: Copyright */}
|
|
||||||
<p className="text-sm text-gray-500 order-2 md:order-1">
|
|
||||||
© 2025 Banatie. Built for builders who create.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Right: Contextual Links */}
|
|
||||||
<nav aria-label="Footer navigation" className="flex items-center gap-6 order-1 md:order-2">
|
|
||||||
<Link
|
|
||||||
href={links.docs}
|
|
||||||
className="text-sm text-gray-500 hover:text-white transition-colors min-h-[44px] flex items-center"
|
|
||||||
>
|
|
||||||
Documentation
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href={links.api}
|
|
||||||
className="text-sm text-gray-500 hover:text-white transition-colors min-h-[44px] flex items-center"
|
|
||||||
>
|
|
||||||
API Reference
|
|
||||||
</Link>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lab Layout Component
|
|
||||||
*
|
|
||||||
* Main layout wrapper for the Lab section combining sidebar and content.
|
|
||||||
* Uses ThreeColumnLayout for consistent column structure across the app.
|
|
||||||
*
|
|
||||||
* Structure:
|
|
||||||
* - Left: LabSidebar (w-64, hidden lg:block, scrollable)
|
|
||||||
* - Center: Scrollable content area + fixed LabFooter at bottom
|
|
||||||
* - Right: Reserved for future use (TOC, preview panels, etc.)
|
|
||||||
*
|
|
||||||
* The center column uses a fixed height container with:
|
|
||||||
* - Scrollable content area (flex-1 overflow-y-auto)
|
|
||||||
* - Sticky footer always visible at bottom
|
|
||||||
*
|
|
||||||
* This layout is rendered inside PageProvider context which provides:
|
|
||||||
* - SubsectionNav at the top
|
|
||||||
* - ApiKeyWidget in the right slot
|
|
||||||
* - AnimatedBackground
|
|
||||||
*
|
|
||||||
* Scroll Detection:
|
|
||||||
* - Detects scroll in the main content area
|
|
||||||
* - After 50px threshold, collapses the main header via LabScrollContext
|
|
||||||
* - Height adjusts dynamically: 100vh-7rem (header visible) → 100vh-3rem (header hidden)
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { ReactNode, useRef, useEffect, useCallback } from 'react';
|
|
||||||
import { ThreeColumnLayout } from '@/components/layout/ThreeColumnLayout';
|
|
||||||
import { LabSidebar } from './LabSidebar';
|
|
||||||
import { LabFooter } from './LabFooter';
|
|
||||||
import { useLabScroll } from '@/contexts/lab-scroll-context';
|
|
||||||
|
|
||||||
type LabLayoutProps = {
|
|
||||||
children: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SCROLL_THRESHOLD = 50;
|
|
||||||
|
|
||||||
export const LabLayout = ({ children }: LabLayoutProps) => {
|
|
||||||
const { isScrolled, setIsScrolled } = useLabScroll();
|
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const handleScroll = useCallback(() => {
|
|
||||||
if (!contentRef.current) return;
|
|
||||||
const scrollTop = contentRef.current.scrollTop;
|
|
||||||
setIsScrolled(scrollTop > SCROLL_THRESHOLD);
|
|
||||||
}, [setIsScrolled]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const contentElement = contentRef.current;
|
|
||||||
if (!contentElement) return;
|
|
||||||
|
|
||||||
contentElement.addEventListener('scroll', handleScroll);
|
|
||||||
return () => contentElement.removeEventListener('scroll', handleScroll);
|
|
||||||
}, [handleScroll]);
|
|
||||||
|
|
||||||
const containerHeight = isScrolled ? 'h-[calc(100vh-3rem)]' : 'h-[calc(100vh-7rem)]';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ThreeColumnLayout
|
|
||||||
left={
|
|
||||||
<div className={`border-r border-white/10 bg-slate-950/50 backdrop-blur-sm ${containerHeight} overflow-y-auto transition-all duration-300`}>
|
|
||||||
<LabSidebar />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
center={
|
|
||||||
<div className={`flex flex-col ${containerHeight} transition-all duration-300`}>
|
|
||||||
<div ref={contentRef} className="flex-1 overflow-y-auto min-h-0">{children}</div>
|
|
||||||
<LabFooter />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,135 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lab Sidebar - Filter Panel
|
|
||||||
*
|
|
||||||
* Narrow left sidebar for filtering lab content.
|
|
||||||
* Matches DocsSidebar visual style with collapsible filter sections.
|
|
||||||
*
|
|
||||||
* Features:
|
|
||||||
* - Multiple filter groups (Status, Date Range, Source Type)
|
|
||||||
* - Collapsible sections
|
|
||||||
* - Checkbox/radio button controls
|
|
||||||
* - Clean, minimal design
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { FilterPlaceholder } from '@/components/lab/FilterPlaceholder';
|
|
||||||
|
|
||||||
type FilterGroupProps = {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
icon: string;
|
|
||||||
options: Array<{ id: string; label: string; count?: number }>;
|
|
||||||
multiSelect?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const filterGroups: FilterGroupProps[] = [
|
|
||||||
{
|
|
||||||
id: 'status',
|
|
||||||
label: 'Status',
|
|
||||||
icon: '📊',
|
|
||||||
options: [
|
|
||||||
{ id: 'all', label: 'All', count: 127 },
|
|
||||||
{ id: 'completed', label: 'Completed', count: 98 },
|
|
||||||
{ id: 'pending', label: 'Pending', count: 23 },
|
|
||||||
{ id: 'failed', label: 'Failed', count: 6 },
|
|
||||||
],
|
|
||||||
multiSelect: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'date',
|
|
||||||
label: 'Date Range',
|
|
||||||
icon: '📅',
|
|
||||||
options: [
|
|
||||||
{ id: 'today', label: 'Today', count: 12 },
|
|
||||||
{ id: 'week', label: 'Last 7 days', count: 45 },
|
|
||||||
{ id: 'month', label: 'Last 30 days', count: 89 },
|
|
||||||
{ id: 'all-time', label: 'All time', count: 127 },
|
|
||||||
],
|
|
||||||
multiSelect: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'source',
|
|
||||||
label: 'Source Type',
|
|
||||||
icon: '🎨',
|
|
||||||
options: [
|
|
||||||
{ id: 'text', label: 'Text-to-Image', count: 78 },
|
|
||||||
{ id: 'upload', label: 'Uploads', count: 34 },
|
|
||||||
{ id: 'reference', label: 'Reference Images', count: 15 },
|
|
||||||
],
|
|
||||||
multiSelect: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const LabSidebar = () => {
|
|
||||||
const [expandedSections, setExpandedSections] = useState<string[]>(['status', 'date', 'source']);
|
|
||||||
|
|
||||||
const toggleSection = (id: string) => {
|
|
||||||
setExpandedSections((prev) =>
|
|
||||||
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isExpanded = (id: string) => expandedSections.includes(id);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<aside className="h-full bg-slate-950 border-r border-white/10" aria-label="Lab filters">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="p-6 border-b border-slate-800">
|
|
||||||
<h2 className="text-lg font-semibold text-white">Filters</h2>
|
|
||||||
<p className="text-xs text-gray-500 mt-1">Refine your results</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filter Groups */}
|
|
||||||
<div className="p-4 space-y-4">
|
|
||||||
{filterGroups.map((group) => {
|
|
||||||
const expanded = isExpanded(group.id);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={group.id} className="border-b border-slate-800 pb-4 last:border-b-0">
|
|
||||||
{/* Section Header */}
|
|
||||||
<button
|
|
||||||
onClick={() => toggleSection(group.id)}
|
|
||||||
className="w-full flex items-center justify-between px-2 py-2 rounded-lg text-sm transition-colors text-gray-400 hover:text-white hover:bg-white/5"
|
|
||||||
aria-expanded={expanded}
|
|
||||||
>
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
<span className="text-base">{group.icon}</span>
|
|
||||||
<span className="font-medium">{group.label}</span>
|
|
||||||
</span>
|
|
||||||
<svg
|
|
||||||
className={`w-4 h-4 transition-transform ${expanded ? 'rotate-90' : ''}`}
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Filter Options */}
|
|
||||||
{expanded && (
|
|
||||||
<FilterPlaceholder
|
|
||||||
options={group.options}
|
|
||||||
multiSelect={group.multiSelect}
|
|
||||||
groupId={group.id}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bottom Actions */}
|
|
||||||
<div className="mt-auto p-4 border-t border-slate-800">
|
|
||||||
<button
|
|
||||||
className="w-full px-3 py-2 text-sm text-gray-500 hover:text-gray-300 transition-colors rounded-lg hover:bg-white/5"
|
|
||||||
aria-label="Reset all filters"
|
|
||||||
>
|
|
||||||
Reset Filters
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lab Scroll Context
|
|
||||||
*
|
|
||||||
* Shares scroll state between the main content area and the header.
|
|
||||||
* Used to collapse the main header when user scrolls in the lab section.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { createContext, useContext, useState, ReactNode } from 'react';
|
|
||||||
|
|
||||||
type LabScrollContextValue = {
|
|
||||||
isScrolled: boolean;
|
|
||||||
setIsScrolled: (value: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const LabScrollContext = createContext<LabScrollContextValue | null>(null);
|
|
||||||
|
|
||||||
export const useLabScroll = () => {
|
|
||||||
const context = useContext(LabScrollContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useLabScroll must be used within LabScrollProvider');
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|
||||||
type LabScrollProviderProps = {
|
|
||||||
children: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LabScrollProvider = ({ children }: LabScrollProviderProps) => {
|
|
||||||
const [isScrolled, setIsScrolled] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LabScrollContext.Provider value={{ isScrolled, setIsScrolled }}>
|
|
||||||
{children}
|
|
||||||
</LabScrollContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -196,6 +196,9 @@ importers:
|
||||||
'@banatie/database':
|
'@banatie/database':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/database
|
version: link:../../packages/database
|
||||||
|
lucide-react:
|
||||||
|
specifier: ^0.400.0
|
||||||
|
version: 0.400.0(react@19.1.0)
|
||||||
next:
|
next:
|
||||||
specifier: 15.5.4
|
specifier: 15.5.4
|
||||||
version: 15.5.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 15.5.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
|
@ -9192,6 +9195,10 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
|
|
||||||
|
lucide-react@0.400.0(react@19.1.0):
|
||||||
|
dependencies:
|
||||||
|
react: 19.1.0
|
||||||
|
|
||||||
magic-string@0.30.19:
|
magic-string@0.30.19:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue