Files
vibn-agent-runner/design-templates/VIBN (2)/vibn-app/src/components/Hero.jsx

211 lines
9.2 KiB
JavaScript

import { useEffect, useRef, useState } from "react";
import { Arrow, TrustStrip, Glow } from "../lib/primitives.jsx";
const HERO_PLACEHOLDERS = [
"A booking site for my dog grooming business…",
"An invoice tracker for my freelance clients…",
"A members-only recipe site for my supper club…",
"A custom CRM for our 3-person real estate team…",
"A tip calculator app for our restaurant staff…",
"A waitlist site for my new ceramics studio…",
];
const HERO_CHIPS = [
"📋 Client intake form",
"📅 Booking site",
"🧾 Invoice tracker",
"🛒 Online store",
"📰 Email newsletter",
];
export default function Hero({ onStart, variant = "promise" }) {
const [text, setText] = useState("");
const [phIdx, setPhIdx] = useState(0);
const [phChars, setPhChars] = useState(0);
const [deleting, setDeleting] = useState(false);
const taRef = useRef(null);
// Type-on placeholder while empty
useEffect(() => {
if (text.length > 0) return undefined;
const full = HERO_PLACEHOLDERS[phIdx];
const speed = deleting ? 18 : 38;
const t = setTimeout(() => {
if (!deleting) {
if (phChars < full.length) setPhChars(phChars + 1);
else setTimeout(() => setDeleting(true), 1700);
} else {
if (phChars > 0) setPhChars(phChars - 1);
else { setDeleting(false); setPhIdx((phIdx + 1) % HERO_PLACEHOLDERS.length); }
}
}, speed);
return () => clearTimeout(t);
}, [text, phIdx, phChars, deleting]);
const placeholder = HERO_PLACEHOLDERS[phIdx].slice(0, phChars);
const submit = () => {
const value = text || HERO_PLACEHOLDERS[phIdx];
onStart?.(value);
};
const useChip = (chip) => {
const clean = chip.replace(/^[^\w]+/, "").trim();
setText(`Build me ${clean.toLowerCase()} for my business.`);
taRef.current?.focus();
};
return (
<header className="relative overflow-hidden pt-[clamp(60px,9vh,120px)] pb-[clamp(60px,10vh,120px)]">
{/* Ambient glows */}
<Glow color="oklch(0.74 0.175 35 / 0.30)" size={900}
style={{ top: -200, left: "50%", transform: "translateX(-50%)" }} />
<Glow color="oklch(0.45 0.10 35 / 0.30)" size={600} style={{ top: "20%", left: -200 }} />
<Glow color="oklch(0.45 0.10 35 / 0.20)" size={500} style={{ top: "30%", right: -150 }} />
<div className="wrap relative flex flex-col items-center text-center gap-7">
{variant === "promise" ? (
<>
<h1 className="font-medium leading-[0.98] tracking-[-0.035em] text-[clamp(44px,7.4vw,104px)] text-balance">
Keep <span className="text-accent" style={{ textShadow: "0 0 30px var(--c-accent-glow)" }}>vibing</span>.
<br />All the way to launch.
</h1>
<div className="font-mono text-[12px] text-fg-faint tracking-[0.04em] inline-flex items-center gap-2 -mt-2">
<span className="w-6 h-px bg-hairline" />
idea live marketed customers
<span className="w-6 h-px bg-hairline" />
</div>
<p className="text-[clamp(20px,2.2vw,28px)] text-fg-dim tracking-[-0.01em] max-w-[720px] text-balance">
<b className="text-fg font-medium">"I built my product, now what?"</b> Vibn is the answer.
<br />Your AI handles the technical stuff, puts your idea online, and helps you find your first customers.
</p>
</>
) : (
<>
<h1 className="font-medium leading-[0.98] tracking-[-0.035em] text-[clamp(44px,7.4vw,104px)] text-balance">
<span className="text-accent" style={{ textShadow: "0 0 30px var(--c-accent-glow)" }}></span>I built my product,
<br />now what<span className="text-accent" style={{ textShadow: "0 0 30px var(--c-accent-glow)" }}>?</span>
</h1>
<div className="font-mono text-[12px] text-fg-faint tracking-[0.04em] inline-flex items-center gap-2 -mt-2">
<span className="w-6 h-px bg-hairline" />
posted 2 hours ago · r/SideProject
<span className="w-6 h-px bg-hairline" />
</div>
<p className="text-[clamp(20px,2.2vw,28px)] text-fg-dim tracking-[-0.01em] max-w-[720px] text-balance">
<b className="text-fg font-medium">Keep vibing.</b> All the way to launch.
<br />Your AI handles the technical stuff, puts your idea online, and helps you find your first customers.
</p>
</>
)}
{/* Prompt */}
<PromptInput
text={text} setText={setText}
placeholder={placeholder}
taRef={taRef}
onSubmit={submit}
/>
<div className="flex flex-wrap gap-2 justify-center mt-3 text-[13px]">
{HERO_CHIPS.map((c) => (
<button key={c} type="button"
onClick={() => useChip(c)}
className="px-3.5 py-[7px] rounded-full border border-hairline bg-[oklch(0.20_0.009_60/0.4)] text-fg-dim transition-all hover:border-hairline-2 hover:text-fg hover:-translate-y-px"
>
{c}
</button>
))}
</div>
<div className="flex gap-3 items-center mt-2.5 flex-wrap justify-center">
<button type="button" onClick={submit} className="btn btn-primary">
Start building free <Arrow />
</button>
<a href="#how" className="btn btn-ghost">See how it works</a>
</div>
<TrustStrip items={["No credit card", "No homework", "No new tools to learn"]} />
</div>
</header>
);
}
function PromptInput({ text, setText, placeholder, taRef, onSubmit }) {
return (
<div className="w-full max-w-[720px] relative mt-3.5">
<div className="relative rounded-3xl p-px"
style={{
background: "linear-gradient(180deg, oklch(0.50 0.06 35 / 0.6), oklch(0.30 0.012 60 / 0.4) 40%, oklch(0.25 0.012 60 / 0.4))",
boxShadow: "0 30px 80px -20px oklch(0 0 0 / 0.6), 0 0 80px -20px var(--c-accent-glow)",
}}
>
<div className="rounded-[27px] px-[18px] pt-[18px] pb-3.5 backdrop-blur-xl"
style={{ background: "linear-gradient(180deg, oklch(0.19 0.009 60 / 0.92), oklch(0.17 0.008 60 / 0.92))" }}
>
<div className="relative">
<textarea
ref={taRef}
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) onSubmit(); }}
className="w-full min-h-[96px] bg-transparent border-0 outline-none resize-none text-fg text-[17px] leading-[1.45] py-1.5 px-1 placeholder:text-fg-faint"
aria-label="Describe what you want to build"
placeholder=""
/>
{text.length === 0 && (
<div className="absolute top-[22px] left-[6px] right-[6px] pointer-events-none text-fg-faint text-[17px] leading-[1.45] text-left">
{placeholder}
<span className="inline-block w-2 h-[18px] align-[-3px] ml-0.5 animate-[blink_1s_steps(2)_infinite]"
style={{ background: "var(--c-accent)", boxShadow: "0 0 12px var(--c-accent-glow)" }} />
</div>
)}
</div>
<div className="flex items-center justify-between gap-3.5 mt-1.5 pt-3 border-t border-hairline">
<div className="hidden sm:flex gap-1.5 text-fg-mute">
<PromptTool icon="paperclip" label="Screenshot" />
<PromptTool icon="mic" label="Voice" />
<PromptTool icon="grid" label="Templates" />
</div>
<button
type="button" onClick={onSubmit}
className="inline-flex items-center gap-2 h-9 px-3.5 pr-3.5 rounded-full font-medium text-sm transition-transform hover:-translate-y-px"
style={{
background: "var(--c-accent)", color: "var(--c-accent-fg)",
boxShadow: "0 0 0 1px oklch(0.84 0.16 35 / 0.5) inset, 0 8px 28px -8px var(--c-accent-glow)",
}}
>
Start building <Arrow size={13} />
</button>
</div>
</div>
</div>
</div>
);
}
function PromptTool({ icon, label }) {
return (
<button type="button" title={label}
className="inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-full text-[12px] text-fg-mute border border-transparent transition-colors hover:border-hairline hover:text-fg-dim">
<PromptIcon name={icon} />
{label}
</button>
);
}
function PromptIcon({ name }) {
const props = {
width: 13, height: 13, viewBox: "0 0 16 16", fill: "none",
stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round",
};
if (name === "paperclip") return (
<svg {...props}><path d="M11.5 6.5 6.6 11.4a2 2 0 1 1-2.8-2.8l5.4-5.4a3.5 3.5 0 1 1 5 5L8.6 13.7"/></svg>
);
if (name === "mic") return (
<svg {...props}><rect x="6" y="2" width="4" height="8" rx="2"/><path d="M3.5 8a4.5 4.5 0 0 0 9 0M8 13v2"/></svg>
);
if (name === "grid") return (
<svg {...props}><rect x="2.5" y="2.5" width="4.5" height="4.5"/><rect x="9" y="2.5" width="4.5" height="4.5"/><rect x="2.5" y="9" width="4.5" height="4.5"/><rect x="9" y="9" width="4.5" height="4.5"/></svg>
);
return null;
}