// ============================================================ // vibn-marketplace · marketplace-components.jsx // ------------------------------------------------------------ // Components specific to two-sided marketplaces. Built on top // of vibn-ai-templates/components.jsx — load that first so // Icon, Button, Card, Badge, Avatar etc. are available on the // global scope. // // All visual properties read from CSS variables so any theme // (atlas / minimal / dark / glass / editorial) restyles them. // // Components: // PhotoSlot, ListingCard, ListingCardHorizontal, // RatingStars, ReviewCard, RatingsSummary, // PriceTag, PriceBreakdown, // HostHeader, MessageBubble, MessageThread, // TrustBadge, AmenityChip, FilterChips, // SearchBar, MiniMap, CalendarMonth, AvailabilityHeatmap, // CategoryRail, PhotoGallery, StatTile. // ============================================================ // ── PhotoSlot ──────────────────────────────────────────────── // Placeholder image — stripe pattern with a label. Used wherever // the user is expected to plug in a real photo. const PhotoSlot = ({ label = "Photo", aspect = "4/3", tone = "warm", style, children }) => { const palettes = { warm: ["#efe2cc", "#e3d2b3"], sage: ["#e0e8d4", "#cfdcb9"], blush: ["#f1dcd2", "#e6c5b8"], ocean: ["#d6e0e8", "#bccada"], night: ["#1f2530", "#2a3140"], sand: ["#ece4d3", "#dccfb3"], }; const [a, b] = palettes[tone] || palettes.warm; const isDark = tone === "night"; return (
{label && ( {label} )} {children}
); }; // ── RatingStars ───────────────────────────────────────────── // `value` 0–5 (fractional ok). `size` and `gap` adjust scale. const RatingStars = ({ value = 4.5, max = 5, size = 14, showValue = true, style }) => { const stars = []; for (let i = 0; i < max; i++) { const fill = Math.max(0, Math.min(1, value - i)); stars.push( {/* Empty */} {/* Filled overlay clipped to fill */} {fill > 0 && ( )} ); } return ( {stars} {showValue && {value.toFixed(2)}} ); }; // ── TrustBadge ─────────────────────────────────────────────── const TrustBadge = ({ kind = "verified", label, style }) => { const kinds = { verified: { icon: "shield", txt: "Verified host", tone: "info" }, superhost: { icon: "star", txt: "Superhost", tone: "warn" }, "top-rated": { icon: "star", txt: "Top rated", tone: "accent" }, instant: { icon: "bolt", txt: "Instant book", tone: "success"}, new: { icon: "spark", txt: "New on Atlas", tone: "info" }, }; const k = kinds[kind] || kinds.verified; return ( } style={style}> {label || k.txt} ); }; // ── PriceTag ───────────────────────────────────────────────── // Inline pricing display. `period` like "/ night", "/ project". // `original` shows a strikethrough above for discounts. const PriceTag = ({ amount, currency = "$", period, original, size = "md", emphasis = true, style }) => { const sizing = { sm: { num: 14, lbl: 11 }, md: { num: 20, lbl: 12 }, lg: { num: 28, lbl: 13 }, xl: { num: 36, lbl: 14 }, }[size]; return ( {original && ( {currency}{original} )} {currency}{amount} {period && {period}} ); }; // ── ListingCard (vertical) ─────────────────────────────────── // Standard photo-on-top card. // listing.photo — { label, tone } (placeholder hints) // listing.title — string // listing.subtitle— string (location, host, dates etc.) // listing.price — { amount, period } // listing.rating — number // listing.reviews — number // listing.badges — array of TrustBadge kinds // listing.tags — short strings for top-left chips const ListingCard = ({ listing = {}, onClick, style }) => { const { photo = {}, title, subtitle, price = {}, rating, reviews, badges = [], tags = [], favorite } = listing; return (
{/* Top-left tags */} {tags.length > 0 && (
{tags.map(t => ( {t} ))}
)} {/* Favorite */}

{title}

{rating != null && ( {rating.toFixed(2)} )}
{subtitle && (
{subtitle}
)} {reviews != null && (
{reviews} reviews
)} {badges.length > 0 && (
{badges.map(b => )}
)} {price.amount != null && (
)}
); }; // ── ListingCardHorizontal — for search results lists ───────── const ListingCardHorizontal = ({ listing = {}, onClick, style }) => { const { photo = {}, title, subtitle, price = {}, rating, reviews, badges = [], description, amenities = [] } = listing; return (
{badges.map(b => )}

{title}

{subtitle && (
{subtitle}
)} {description && (

{description}

)} {amenities.length > 0 && (
{amenities.slice(0, 4).map(a => )}
)}
{rating != null && } {reviews != null &&
{reviews} reviews
}
{price.amount != null && }
); }; // ── AmenityChip ────────────────────────────────────────────── const AmenityChip = ({ label, icon, style }) => ( {icon && } {label} ); // ── ReviewCard ─────────────────────────────────────────────── const ReviewCard = ({ author, avatarColor, rating, date, body, location, style }) => (
{author}
{location && <>{location} · }{date}
{rating != null && (
)}

{body}

); // ── RatingsSummary ────────────────────────────────────────── // Header block for review section: big number + category bars. const RatingsSummary = ({ value = 4.92, total = 184, categories = [], style }) => (
★ {value.toFixed(2)} · {total} reviews
{categories.map(c => (
{c.label} {c.value.toFixed(1)}
))}
); // ── PriceBreakdown ─────────────────────────────────────────── // Receipt-style line items + total. // items: [{ label, amount, note?, strike? }] // total: number // currency const PriceBreakdown = ({ items = [], total, currency = "$", style }) => (
{items.map((it, i) => (
{it.label} {it.note === "discount" ? "−" : ""}{currency}{it.amount}
))}
Total {currency}{total}
); // ── HostHeader ─────────────────────────────────────────────── const HostHeader = ({ name, avatar, color, joined, location, languages = [], blurb, kpis = [], style }) => (
{name}
Host
{kpis.map(k => (
{k.value}
{k.label}
))}
{(joined || location) && (
{location && <>Lives in {location}{joined ? " · " : ""}} {joined && <>Hosting since {joined}}
)} {languages.length > 0 && (
Speaks {languages.join(", ")}
)} {blurb && (

{blurb}

)}
); // ── MessageBubble ──────────────────────────────────────────── const MessageBubble = ({ mine, name, body, time, avatar, color, attachment, style }) => (
{!mine && }
{body}
{attachment && (
{attachment}
)} {time &&
{time}
}
); // ── SearchBar — destination + dates + guests ──────────────── const SearchBar = ({ destination, dates, guests, compact, onSearch, style }) => { const segPad = compact ? "10px 18px" : "14px 22px"; const Segment = ({ label, value, placeholder, last }) => (
{label}
{value || placeholder}
); return (
); }; // ── CategoryRail — horizontal scrolling category chips ─────── const CategoryRail = ({ categories = [], active, onChange = () => {}, style }) => (
{categories.map(c => { const sel = c.id === active || c.label === active; return ( ); })}
); // ── FilterChips — horizontal chip strip ────────────────────── const FilterChips = ({ filters = [], onClear, style }) => (
{filters.map((f, i) => ( {f.label} {f.count != null && {f.count}} ))} {onClear && ( )}
); // ── MiniMap — schematic map with pins ──────────────────────── // Not a real map — a stylized SVG you can drop in for layout. // Pins: [{ x: 0-100, y: 0-100, price, active }] const MiniMap = ({ pins = [], style }) => (
{/* Map illustration via SVG */} {/* Faux water */} {/* Faux roads */} {/* Faux blocks */} {[[80,80,90,70],[200,60,80,100],[340,90,110,80],[470,70,80,90], [60,250,80,90],[180,260,110,80],[330,260,90,70],[460,260,80,90]].map(([x,y,w,h], i) => ( ))} {/* Pins */} {pins.map((p, i) => (
${p.price}
))} {/* Map controls */}
); // ── CalendarMonth — single-month booking calendar ──────────── // `start` and `end` are 1–31 day numbers within the displayed // month. Tinted range between them, range endpoints highlighted. const CalendarMonth = ({ month = "August 2026", firstWeekday = 6, daysInMonth = 31, start, end, today, blocked = [], style }) => { const days = []; // Blank cells before day 1 for (let i = 0; i < firstWeekday; i++) days.push(null); for (let d = 1; d <= daysInMonth; d++) days.push(d); const inRange = (d) => start != null && end != null && d > start && d < end; const isEnd = (d) => d === start || d === end; const isBlocked = (d) => blocked.includes(d); return (
{month}
{["S","M","T","W","T","F","S"].map((d, i) => {d})}
{days.map((d, i) => { if (d == null) return ; const blocked = isBlocked(d); const range = inRange(d); const end = isEnd(d); return ( ); })}
); }; // ── AvailabilityHeatmap — for host calendar ────────────────── // 4-week strip showing booked vs free vs blocked nights. // weeks: array of 7-element arrays of "open"|"booked"|"blocked" const AvailabilityHeatmap = ({ weeks = [], style }) => { const colors = { open: { bg: "var(--success-soft)", dot: "var(--success)" }, booked: { bg: "var(--accent-soft)", dot: "var(--accent)" }, blocked: { bg: "var(--surface-alt)", dot: "var(--text-3)" }, }; return (
{weeks.map((w, i) => (
{w.map((cell, j) => (
))}
))}
); }; // ── PhotoGallery — main + 4 thumbs grid ────────────────────── const PhotoGallery = ({ photos = [], style }) => { // photos: [{ label, tone }] up to 5 const [main, ...rest] = photos; return (
{rest.slice(0, 4).map((p, i) => ( ))}
); }; // ── StatTile — small dashboard stat ────────────────────────── const StatTile = ({ label, value, sub, trend, icon, style }) => (
{label} {icon && ( )}
{value}
{(sub || trend) && (
{trend != null && ( = 0 ? "var(--success)" : "var(--danger)", marginRight: 4 }}> {trend >= 0 ? "↑" : "↓"} {Math.abs(trend)}% )} {sub}
)}
); // ─── Exports ───────────────────────────────────────────────── Object.assign(window, { PhotoSlot, ListingCard, ListingCardHorizontal, RatingStars, ReviewCard, RatingsSummary, PriceTag, PriceBreakdown, HostHeader, MessageBubble, TrustBadge, AmenityChip, FilterChips, SearchBar, MiniMap, CalendarMonth, AvailabilityHeatmap, CategoryRail, PhotoGallery, StatTile, });