rspace-online/modules/rvnb/components/folk-vehicle-card.ts

201 lines
6.7 KiB
TypeScript

/**
* <folk-vehicle-card> — Canvas-embeddable vehicle card shape.
*
* Shows: cover photo placeholder, title, type icon, year/make/model,
* owner name + trust badge, sleeps, mileage policy, pickup location, endorsement count.
*/
const ECONOMY_COLORS: Record<string, { bg: string; fg: string; label: string; icon: string }> = {
gift: { bg: 'rgba(52,211,153,0.12)', fg: '#34d399', label: 'Gift', icon: '\u{1F49A}' },
exchange: { bg: 'rgba(96,165,250,0.12)', fg: '#60a5fa', label: 'Exchange', icon: '\u{1F91D}' },
sliding_scale: { bg: 'rgba(245,158,11,0.12)', fg: '#f59e0b', label: 'Sliding Scale', icon: '\u{2696}' },
suggested: { bg: 'rgba(167,139,250,0.12)', fg: '#a78bfa', label: 'Suggested', icon: '\u{1F4AD}' },
fixed: { bg: 'rgba(148,163,184,0.12)', fg: '#94a3b8', label: 'Fixed', icon: '\u{1F3F7}' },
};
const TYPE_ICONS: Record<string, string> = {
motorhome: '\u{1F690}', camper_van: '\u{1F68C}', travel_trailer: '\u{1F3D5}',
truck_camper: '\u{1F6FB}', skoolie: '\u{1F68E}', other: '\u{1F3E0}',
};
const MILEAGE_LABELS: Record<string, string> = {
unlimited: '\u{267E} Unlimited',
per_mile: '\u{1F4CF} Per Mile',
included_miles: '\u{2705} Included Miles',
};
class FolkVehicleCard extends HTMLElement {
static observedAttributes = ['vehicle-id', 'space'];
#shadow: ShadowRoot;
#data: any = null;
#endorsementCount = 0;
#isAvailable = true;
constructor() {
super();
this.#shadow = this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.#fetchAndRender();
}
attributeChangedCallback() {
this.#fetchAndRender();
}
set vehicleData(data: { vehicle: any; endorsementCount?: number; isAvailable?: boolean }) {
this.#data = data.vehicle;
this.#endorsementCount = data.endorsementCount ?? 0;
this.#isAvailable = data.isAvailable ?? true;
this.#render();
}
async #fetchAndRender() {
const space = this.getAttribute('space') || 'demo';
const vehicleId = this.getAttribute('vehicle-id');
if (!vehicleId) return;
try {
const res = await fetch(`/${space}/rvnb/api/vehicles/${vehicleId}`);
if (res.ok) {
this.#data = await res.json();
this.#render();
}
} catch { /* offline */ }
}
#render() {
if (!this.#data) {
this.#shadow.innerHTML = `<div style="padding:1rem;color:#94a3b8;font-size:0.85rem">Loading vehicle...</div>`;
return;
}
const d = this.#data;
const eco = ECONOMY_COLORS[d.economy] || ECONOMY_COLORS.suggested;
const typeIcon = TYPE_ICONS[d.type] || '\u{1F690}';
const typeLabel = (d.type || 'camper_van').replace(/_/g, ' ');
const specsLine = [d.year, d.make, d.model].filter(Boolean).join(' ');
const mileageLabel = MILEAGE_LABELS[d.mileage_policy] || '\u{267E} Unlimited';
this.#shadow.innerHTML = `
<style>
:host { display: block; }
.card {
background: var(--rs-surface, #1e293b);
border: 1px solid var(--rs-border, #334155);
border-radius: 0.75rem;
overflow: hidden;
font-family: system-ui, -apple-system, sans-serif;
transition: border-color 0.15s, box-shadow 0.15s;
cursor: pointer;
}
.card:hover {
border-color: rgba(16,185,129,0.4);
box-shadow: 0 4px 20px rgba(16,185,129,0.08);
}
.cover {
width: 100%; height: 180px;
background: linear-gradient(135deg, rgba(16,185,129,0.15), rgba(20,184,166,0.1));
display: flex; align-items: center; justify-content: center;
font-size: 3rem; position: relative;
}
.cover img { width: 100%; height: 100%; object-fit: cover; }
.avail {
position: absolute; top: 0.75rem; right: 0.75rem;
width: 10px; height: 10px; border-radius: 50%;
border: 2px solid rgba(0,0,0,0.3);
}
.body { padding: 0.875rem 1rem; }
.title {
font-size: 0.95rem; font-weight: 600;
color: var(--rs-text, #e2e8f0);
margin: 0 0 0.25rem; line-height: 1.3;
}
.specs {
font-size: 0.78rem; color: var(--rs-text-muted, #94a3b8);
margin-bottom: 0.5rem;
}
.owner {
display: flex; align-items: center; gap: 0.4rem;
font-size: 0.8rem; color: var(--rs-text-muted, #94a3b8);
margin-bottom: 0.6rem;
}
.trust-badge {
font-size: 0.65rem; padding: 0.1rem 0.35rem;
border-radius: 9999px;
background: rgba(52,211,153,0.12); color: #34d399;
}
.meta {
display: flex; flex-wrap: wrap; gap: 0.4rem;
margin-bottom: 0.6rem;
}
.badge {
display: inline-flex; align-items: center; gap: 0.25rem;
font-size: 0.7rem; font-weight: 500;
padding: 0.15rem 0.45rem; border-radius: 9999px;
white-space: nowrap;
}
.location {
font-size: 0.78rem;
color: var(--rs-text-muted, #94a3b8);
display: flex; align-items: center; gap: 0.3rem;
}
.footer {
display: flex; justify-content: space-between; align-items: center;
padding: 0.5rem 1rem 0.75rem;
font-size: 0.75rem; color: var(--rs-text-muted, #64748b);
}
</style>
<div class="card">
<div class="cover">
${d.cover_photo
? `<img src="${d.cover_photo}" alt="${d.title}">`
: typeIcon}
<div class="avail" style="background:${this.#isAvailable ? '#34d399' : '#ef4444'}"></div>
</div>
<div class="body">
<div class="title">${this.#esc(d.title)}</div>
${specsLine ? `<div class="specs">${this.#esc(specsLine)}</div>` : ''}
<div class="owner">
<span>${this.#esc(d.owner_name)}</span>
${d.instant_accept ? '<span class="trust-badge">\u{26A1} Auto-accept</span>' : ''}
</div>
<div class="meta">
<span class="badge" style="background:${eco.bg};color:${eco.fg};border:1px solid ${eco.fg}22">
${eco.icon} ${eco.label}
</span>
<span class="badge" style="background:rgba(16,185,129,0.08);color:#34d399;border:1px solid rgba(16,185,129,0.15)">
${typeIcon} ${typeLabel}
</span>
<span class="badge" style="background:rgba(148,163,184,0.08);color:#94a3b8;border:1px solid rgba(148,163,184,0.15)">
\u{1F6CF} Sleeps ${d.sleeps}
</span>
<span class="badge" style="background:rgba(6,182,212,0.08);color:#22d3ee;border:1px solid rgba(6,182,212,0.15)">
${mileageLabel}
</span>
</div>
<div class="location">\u{1F4CD} ${this.#esc(d.pickup_location_name)}</div>
</div>
<div class="footer">
<span>${this.#endorsementCount} endorsement${this.#endorsementCount !== 1 ? 's' : ''}</span>
${d.suggested_amount ? `<span>${d.currency || ''} ${d.sliding_min ? d.sliding_min + '\u{2013}' + d.sliding_max : d.suggested_amount}/night</span>` : ''}
</div>
</div>
`;
}
#esc(s: string): string {
const el = document.createElement('span');
el.textContent = s || '';
return el.innerHTML;
}
}
if (!customElements.get('folk-vehicle-card')) {
customElements.define('folk-vehicle-card', FolkVehicleCard);
}
export { FolkVehicleCard };