feat: flatten routes and merge marketing and onboarding directories
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
// ============================================================
|
||||
// vibn-marketplace · marketplace-shells.jsx
|
||||
// ------------------------------------------------------------
|
||||
// Layout shells for marketplace pages.
|
||||
//
|
||||
// MarketplaceTopShell — public, transparent-on-scroll top
|
||||
// nav with brand left, links center, host-mode toggle and
|
||||
// avatar right. Compact variant shrinks the SearchBar.
|
||||
//
|
||||
// DashboardShell — left sidebar with side switch (Guest /
|
||||
// Host modes), workspace, nav sections.
|
||||
// ============================================================
|
||||
|
||||
// ── MarketplaceTopShell ──────────────────────────────────────
|
||||
// Props:
|
||||
// brand { name, mark }
|
||||
// showSearch bool — inline SearchBar in the header
|
||||
// searchProps { destination, dates, guests }
|
||||
// user { name, color, role }
|
||||
// onHostMode fn — called when user clicks "Switch to hosting"
|
||||
// compact bool — slimmer header (e.g. for non-home pages)
|
||||
const MarketplaceTopShell = ({
|
||||
brand = { name: "Atlas" },
|
||||
showSearch, searchProps = {},
|
||||
user, onHostMode = () => {},
|
||||
compact, children,
|
||||
footer = true,
|
||||
}) => (
|
||||
<div className="vibn-app" style={{
|
||||
width: "100%", minHeight: "100%", display: "flex",
|
||||
flexDirection: "column", background: "var(--bg)",
|
||||
fontFamily: "var(--font-sans)", color: "var(--text)",
|
||||
}}>
|
||||
<header style={{
|
||||
padding: compact ? "14px 32px" : "20px 40px",
|
||||
borderBottom: "1px solid var(--border)",
|
||||
background: "var(--surface)", position: "sticky", top: 0, zIndex: 5,
|
||||
}}>
|
||||
<div style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: showSearch ? "auto 1fr auto" : "auto 1fr auto",
|
||||
alignItems: "center", gap: 24,
|
||||
}}>
|
||||
{/* Brand */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||
{brand.mark || <AtlasMark size={compact ? 22 : 26}/>}
|
||||
<span style={{
|
||||
fontFamily: "var(--font-display)",
|
||||
fontSize: compact ? 18 : 22,
|
||||
fontWeight: "var(--weight-semibold)",
|
||||
letterSpacing: "-0.01em",
|
||||
color: "var(--accent)",
|
||||
}}>{brand.name}</span>
|
||||
</div>
|
||||
|
||||
{/* Center: search OR links */}
|
||||
{showSearch ? (
|
||||
<div style={{ maxWidth: 720, width: "100%", justifySelf: "center" }}>
|
||||
<SearchBar compact={compact} {...searchProps}/>
|
||||
</div>
|
||||
) : (
|
||||
<nav style={{
|
||||
display: "flex", justifyContent: "center", gap: 28,
|
||||
fontSize: "var(--text-sm)",
|
||||
}}>
|
||||
<span style={{ color: "var(--text)", fontWeight: 500, cursor: "pointer" }}>Stays</span>
|
||||
<span style={{ color: "var(--text-2)", cursor: "pointer" }}>Experiences</span>
|
||||
<span style={{ color: "var(--text-2)", cursor: "pointer" }}>Guides</span>
|
||||
<span style={{ color: "var(--text-2)", cursor: "pointer" }}>Gift cards</span>
|
||||
</nav>
|
||||
)}
|
||||
|
||||
{/* Right */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, justifySelf: "end" }}>
|
||||
<button onClick={onHostMode} style={{
|
||||
background: "transparent", border: "none", padding: "8px 14px",
|
||||
borderRadius: 999, fontSize: "var(--text-sm)",
|
||||
fontWeight: "var(--weight-medium)", color: "var(--text)",
|
||||
fontFamily: "var(--font-sans)", cursor: "pointer",
|
||||
whiteSpace: "nowrap",
|
||||
}}>Become a host</button>
|
||||
<IconButton name="more" size="md" variant="secondary"/>
|
||||
{user && (
|
||||
<div style={{
|
||||
display: "flex", alignItems: "center", gap: 8,
|
||||
padding: "4px 4px 4px 14px", borderRadius: 999,
|
||||
border: "1px solid var(--border)",
|
||||
background: "var(--surface)",
|
||||
boxShadow: "var(--shadow-sm)",
|
||||
}}>
|
||||
<Icon name="more" size={14}/>
|
||||
<Avatar name={user.name} color={user.color} size={28}/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main style={{ flex: 1, minWidth: 0 }}>{children}</main>
|
||||
|
||||
{footer && (
|
||||
<footer style={{
|
||||
padding: "28px 40px", borderTop: "1px solid var(--border)",
|
||||
background: "var(--surface-2)",
|
||||
display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 32,
|
||||
}}>
|
||||
{[
|
||||
["Support", ["Help centre","Cancellation options","Safety information","Report a concern"]],
|
||||
["Community",["Host an event","Atlas.org","Community forum","Refer a host"]],
|
||||
["Hosting", ["Become a host","Hosting resources","Community forum","Responsible hosting"]],
|
||||
["Atlas", ["Newsroom","Investors","Careers","Press centre"]],
|
||||
].map(([title, items]) => (
|
||||
<div key={title}>
|
||||
<div style={{
|
||||
fontSize: "var(--text-sm)", fontWeight: "var(--weight-semibold)",
|
||||
color: "var(--text)", marginBottom: 12,
|
||||
}}>{title}</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{items.map(it => (
|
||||
<span key={it} style={{
|
||||
fontSize: "var(--text-sm)", color: "var(--text-2)", cursor: "pointer",
|
||||
}}>{it}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</footer>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
// Brand mark for the Atlas marketplace — a stylized compass-ish glyph
|
||||
const AtlasMark = ({ size = 26 }) => (
|
||||
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" aria-hidden="true">
|
||||
<path d="M16 2 L19 13 L30 16 L19 19 L16 30 L13 19 L2 16 L13 13 Z"
|
||||
fill="var(--accent)"/>
|
||||
<circle cx="16" cy="16" r="2.5" fill="var(--bg)"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// ── MarketplaceDashboardShell ────────────────────────────────
|
||||
// Side nav for the demand (guest) and supply (host) dashboards.
|
||||
// role "guest" | "host"
|
||||
// active id of current nav item
|
||||
// onRoleSwitch fn
|
||||
const MarketplaceDashboardShell = ({
|
||||
brand = { name: "Atlas" },
|
||||
role = "guest",
|
||||
active,
|
||||
onRoleSwitch = () => {},
|
||||
user,
|
||||
children,
|
||||
}) => {
|
||||
const guestNav = [
|
||||
{ id: "trips", label: "My trips", icon: "briefcase" },
|
||||
{ id: "saved", label: "Saved", icon: "star" },
|
||||
{ id: "inbox", label: "Messages", icon: "inbox", count: 3 },
|
||||
{ id: "_account", section: "Account" },
|
||||
{ id: "profile", label: "Profile", icon: "people" },
|
||||
{ id: "payment", label: "Payments", icon: "doc" },
|
||||
{ id: "settings", label: "Settings", icon: "settings" },
|
||||
];
|
||||
const hostNav = [
|
||||
{ id: "today", label: "Today", icon: "home" },
|
||||
{ id: "calendar", label: "Calendar", icon: "check" },
|
||||
{ id: "listings", label: "Listings", icon: "building", count: 3 },
|
||||
{ id: "earnings", label: "Earnings", icon: "bar" },
|
||||
{ id: "inbox", label: "Inbox", icon: "inbox", count: 5 },
|
||||
{ id: "_growth", section: "Growth" },
|
||||
{ id: "insights", label: "Insights", icon: "spark" },
|
||||
{ id: "reviews", label: "Reviews", icon: "star" },
|
||||
{ id: "_account", section: "Account" },
|
||||
{ id: "profile", label: "Profile", icon: "people" },
|
||||
{ id: "payouts", label: "Payouts", icon: "doc" },
|
||||
{ id: "settings", label: "Settings", icon: "settings" },
|
||||
];
|
||||
const nav = role === "host" ? hostNav : guestNav;
|
||||
|
||||
return (
|
||||
<div className="vibn-app" style={{
|
||||
width: "100%", height: "100%",
|
||||
display: "grid", gridTemplateColumns: "260px 1fr",
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
<aside style={{
|
||||
background: "var(--surface-2)",
|
||||
borderRight: "1px solid var(--border)",
|
||||
display: "flex", flexDirection: "column",
|
||||
}}>
|
||||
{/* Brand */}
|
||||
<div style={{
|
||||
padding: "16px 18px", display: "flex", alignItems: "center", gap: 10,
|
||||
borderBottom: "1px solid var(--border)",
|
||||
}}>
|
||||
<AtlasMark size={22}/>
|
||||
<span style={{
|
||||
fontFamily: "var(--font-display)", fontSize: 20,
|
||||
fontWeight: "var(--weight-semibold)", color: "var(--accent)",
|
||||
}}>{brand.name}</span>
|
||||
</div>
|
||||
|
||||
{/* Role switch — segmented */}
|
||||
<div style={{ padding: 16 }}>
|
||||
<div style={{
|
||||
display: "flex", padding: 3,
|
||||
background: "var(--surface-alt)", borderRadius: 999,
|
||||
}}>
|
||||
{["guest", "host"].map(r => (
|
||||
<button key={r} onClick={() => onRoleSwitch(r)} style={{
|
||||
flex: 1, padding: "8px 12px", borderRadius: 999,
|
||||
background: r === role ? "var(--surface)" : "transparent",
|
||||
color: r === role ? "var(--text)" : "var(--text-2)",
|
||||
border: "none", cursor: "pointer", fontFamily: "var(--font-sans)",
|
||||
fontSize: "var(--text-sm)", fontWeight: 500,
|
||||
boxShadow: r === role ? "var(--shadow-sm)" : "none",
|
||||
textTransform: "capitalize",
|
||||
}}>{r === "guest" ? "Travelling" : "Hosting"}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Nav */}
|
||||
<nav style={{ padding: "0 10px", flex: 1, overflowY: "auto" }}>
|
||||
{nav.map(it => it.section ? (
|
||||
<div key={it.id} style={{
|
||||
fontSize: "var(--text-xs)", color: "var(--text-3)",
|
||||
padding: "16px 10px 6px", textTransform: "uppercase",
|
||||
letterSpacing: "0.06em", fontWeight: 500,
|
||||
}}>{it.section}</div>
|
||||
) : (
|
||||
<div key={it.id} style={{
|
||||
display: "flex", alignItems: "center", gap: 10,
|
||||
padding: "8px 12px", borderRadius: "var(--radius-sm)",
|
||||
fontSize: "var(--text-md)", marginBottom: 1, cursor: "pointer",
|
||||
color: it.id === active ? "var(--text)" : "var(--text-2)",
|
||||
fontWeight: it.id === active ? 500 : 400,
|
||||
background: it.id === active ? "var(--surface)" : "transparent",
|
||||
boxShadow: it.id === active ? "var(--shadow-sm)" : "none",
|
||||
}}>
|
||||
<span style={{ color: it.id === active ? "var(--accent)" : "var(--text-3)", display: "flex" }}>
|
||||
<Icon name={it.icon} size={15}/>
|
||||
</span>
|
||||
<span style={{ flex: 1 }}>{it.label}</span>
|
||||
{it.count != null && <span style={{
|
||||
fontSize: "var(--text-xs)", color: it.id === active ? "var(--accent)" : "var(--text-3)",
|
||||
fontWeight: 500,
|
||||
}}>{it.count}</span>}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Help footer */}
|
||||
<div style={{
|
||||
padding: 14, borderTop: "1px solid var(--border)",
|
||||
display: "flex", alignItems: "center", gap: 12,
|
||||
}}>
|
||||
<div style={{
|
||||
width: 36, height: 36, borderRadius: 10,
|
||||
background: "var(--accent-soft)", color: "var(--accent)",
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
}}>
|
||||
<Icon name="info" size={16}/>
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0, lineHeight: 1.3 }}>
|
||||
<div style={{ fontSize: "var(--text-sm)", fontWeight: 500 }}>Need help?</div>
|
||||
<div style={{ fontSize: "var(--text-xs)", color: "var(--text-3)" }}>24/7 support · all channels</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main style={{ overflow: "hidden", display: "flex", flexDirection: "column", background: "var(--bg)" }}>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Object.assign(window, {
|
||||
MarketplaceTopShell, MarketplaceDashboardShell, AtlasMark,
|
||||
});
|
||||
Reference in New Issue
Block a user