211 lines
9.2 KiB
JavaScript
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;
|
|
}
|