281 lines
12 KiB
JavaScript
281 lines
12 KiB
JavaScript
// ============================================================
|
|
// 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,
|
|
});
|