201 lines
6.7 KiB
TypeScript
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 };
|