feat: flatten routes and merge marketing and onboarding directories

This commit is contained in:
2026-06-06 18:52:03 -07:00
parent 47417d13a0
commit 0480b306f1
139 changed files with 36409 additions and 229 deletions

View File

@@ -0,0 +1,262 @@
// Owner path — 4 steps for small-business owners replacing their stack.
const OWNER_TOTAL = 4;
const OWNER_STEP_NAMES = ["Business", "Stack", "First fix", "Scale"];
const BIZ_KINDS = [
{ id: "service", icon: "🛠", label: "Trades / home services", desc: "Plumbing, HVAC, landscaping, cleaning" },
{ id: "retail", icon: "🛍", label: "Retail / shop", desc: "Vintage, boutique, market, online" },
{ id: "food", icon: "🥐", label: "Food & drink", desc: "Café, bakery, food truck, catering" },
{ id: "appointments", icon: "💈", label: "Appointment-based", desc: "Salon, studio, clinic, tutoring" },
{ id: "events", icon: "🎟", label: "Events / hospitality", desc: "Venue, rental, planning" },
{ id: "other", icon: "✦", label: "Something else", desc: "We'll learn from your answers" },
];
function OwnerBiz({ value, name, onChange, onNameChange }) {
return (
<>
<WizardQ
title="What does your business do?"
sub="Roughly. We tailor the next screens to match."
/>
<PresetGroup
options={BIZ_KINDS.map((b) => ({
id: b.id, label: b.label, desc: b.desc,
icon: <span style={{ fontSize: 14 }}>{b.icon}</span>,
}))}
value={value}
onChange={onChange}
columns={2}
/>
<Field label="Business name">
<input
className="wiz-input"
placeholder="Sunrise Plumbing, Pearl Lane Bakery…"
value={name}
onChange={(e) => onNameChange(e.target.value)}
/>
</Field>
</>
);
}
const STACK_TOOLS = [
"Square / POS",
"Stripe",
"Calendly",
"Acuity",
"Shopify",
"QuickBooks",
"Mailchimp",
"Instagram",
"Google Sheets",
"Notion / Airtable",
"Wix / Squarespace",
"WhatsApp / Slack",
"A printed binder",
"Head + notepad",
];
function OwnerStack({ tools, spend, onToolsChange, onSpendChange }) {
return (
<>
<WizardQ
title="What are you renting right now?"
sub="Tap everything you pay for. Approximate is fine."
/>
<Field label="Tools & subscriptions">
<ChipGroup
options={STACK_TOOLS}
values={tools || []}
onChange={onToolsChange}
allowOther
/>
</Field>
<Field label="About how much per month?" hint="Across all your software, ballpark.">
<Slider
min={0} max={1500} step={25}
value={spend ?? 250}
onChange={onSpendChange}
format={(v) => v === 0 ? "$0" : v === 1500 ? "$1.5k+" : `$${v}`}
/>
</Field>
{(tools || []).length > 0 && (
<div
style={{
padding: "12px 14px",
borderRadius: 10,
border: "1px solid var(--hairline)",
background: "oklch(0.18 0.009 60 / 0.6)",
fontSize: 13.5,
lineHeight: 1.5,
color: "var(--fg-dim)",
display: "flex", gap: 12, alignItems: "flex-start",
}}
>
<span style={{
width: 6, height: 6, borderRadius: "50%",
background: "var(--accent)", boxShadow: "0 0 10px var(--accent-glow)",
marginTop: 7, flexShrink: 0,
}} />
<span>
<b style={{ color: "var(--fg)", fontWeight: 500 }}>{tools.length} tool{tools.length === 1 ? "" : "s"}</b>
{spend ? <> · ~<b style={{ color: "var(--fg)", fontWeight: 500 }}>${spend}/mo</b></> : null}.
Replaced by one workspace, owned by you.
</span>
</div>
)}
</>
);
}
const OWNER_FIRST_THINGS = [
{ id: "booking", icon: "📅", label: "Bookings & scheduling", desc: "Customers book themselves." },
{ id: "invoicing", icon: "🧾", label: "Quotes, invoices, payments", desc: "Send a quote, get paid, no chasing." },
{ id: "customers", icon: "👥", label: "Customer history & portal", desc: "One place per customer." },
{ id: "inventory", icon: "📦", label: "Inventory & orders", desc: "Track stock, sales, suppliers." },
{ id: "team", icon: "🪪", label: "Team & job dispatch", desc: "Assign jobs, log hours." },
{ id: "marketing", icon: "📣", label: "Website + email + reviews", desc: "A site that converts, list that follows up." },
];
function OwnerFirstThing({ value, onChange }) {
return (
<>
<WizardQ
title="What's burning first?"
sub="The one workflow you wish was already replaced. Vibn builds it on day one."
/>
<PresetGroup
options={OWNER_FIRST_THINGS.map((f) => ({
id: f.id, label: f.label, desc: f.desc,
icon: <span style={{ fontSize: 14 }}>{f.icon}</span>,
}))}
value={value}
onChange={onChange}
columns={2}
/>
</>
);
}
const OWNER_HOW_LONG = [
{ id: "starting", label: "Just starting" },
{ id: "1_3", label: "13 years" },
{ id: "3_10", label: "310 years" },
{ id: "10_plus", label: "10+ years" },
];
function OwnerScale({ customers, team, howLong, onCustomers, onTeam, onHowLong }) {
return (
<>
<WizardQ
title="A little about scale."
sub="Sensible defaults — table view vs. cards, daily vs. monthly reports."
/>
<Field label="Customers per month">
<Slider
min={0} max={2000} step={10}
value={customers ?? 50}
onChange={onCustomers}
format={(v) => v === 0 ? "0" : v >= 2000 ? "2k+" : v.toLocaleString()}
/>
</Field>
<Field label="Team size (incl. you)">
<Slider
min={1} max={50} step={1}
value={team ?? 1}
onChange={onTeam}
format={(v) => v >= 50 ? "50+" : `${v}`}
/>
</Field>
<Field label="How long have you been at this?">
<div className="chips">
{OWNER_HOW_LONG.map((h) => (
<button
key={h.id}
type="button"
className={"chip" + (howLong === h.id ? " active" : "")}
onClick={() => onHowLong(h.id)}
>
{h.label}
</button>
))}
</div>
</Field>
</>
);
}
// ── Path wrapper ───────────────────────────────────────────────────────────
function OwnerPath({ data, onUpdate, onBack, onClose, onComplete, onJumpToStep, step }) {
const next = () => {
if (step < OWNER_TOTAL - 1) onJumpToStep(step + 1);
else onComplete();
};
const back = () => {
if (step === 0) onBack();
else onJumpToStep(step - 1);
};
let body, canNext;
if (step === 0) {
body = (
<OwnerBiz
value={data.biz}
name={data.bizName || ""}
onChange={(v) => onUpdate({ biz: v })}
onNameChange={(v) => onUpdate({ bizName: v })}
/>
);
canNext = !!data.biz && (data.bizName || "").trim().length >= 2;
} else if (step === 1) {
body = (
<OwnerStack
tools={data.tools || []}
spend={data.spend}
onToolsChange={(v) => onUpdate({ tools: v })}
onSpendChange={(v) => onUpdate({ spend: v })}
/>
);
canNext = (data.tools || []).length >= 1;
} else if (step === 2) {
body = <OwnerFirstThing value={data.firstThing} onChange={(v) => onUpdate({ firstThing: v })} />;
canNext = !!data.firstThing;
} else {
body = (
<OwnerScale
customers={data.customers}
team={data.team}
howLong={data.howLong}
onCustomers={(v) => onUpdate({ customers: v })}
onTeam={(v) => onUpdate({ team: v })}
onHowLong={(v) => onUpdate({ howLong: v })}
/>
);
canNext = !!data.howLong;
}
return (
<>
<WizardTop
onBack={back}
onClose={onClose}
lane={LANE_LABELS.owner}
stepText={OWNER_STEP_NAMES[step]}
current={step + 2}
total={5}
/>
<WizardBody width={step === 0 || step === 2 ? "wide" : null}>
{body}
<WizardFooter
onNext={next}
canNext={canNext}
nextLabel={step === OWNER_TOTAL - 1 ? "Build my workspace →" : "Continue"}
hint={canNext ? "⌘↵" : null}
/>
</WizardBody>
</>
);
}
Object.assign(window, { OwnerPath, OWNER_TOTAL });