feat: flatten routes and merge marketing and onboarding directories
This commit is contained in:
262
design-templates/VIBN (2)/onboarding-owner.jsx
Normal file
262
design-templates/VIBN (2)/onboarding-owner.jsx
Normal 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: "1–3 years" },
|
||||
{ id: "3_10", label: "3–10 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 });
|
||||
Reference in New Issue
Block a user