186 lines
6.1 KiB
TypeScript
186 lines
6.1 KiB
TypeScript
/**
|
||
* <folk-listing> — Canvas-embeddable listing card shape.
|
||
*
|
||
* Shows: cover photo placeholder, title, type icon, host name + trust badge,
|
||
* capacity, location, economy badge, availability dot, 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> = {
|
||
couch: '\u{1F6CB}', room: '\u{1F6CF}', apartment: '\u{1F3E2}', cabin: '\u{1F3E1}',
|
||
tent_site: '\u{26FA}', land: '\u{1F333}', studio: '\u{1F3A8}', loft: '\u{1F3D7}',
|
||
house: '\u{1F3E0}', other: '\u{1F3E8}',
|
||
};
|
||
|
||
class FolkListing extends HTMLElement {
|
||
static observedAttributes = ['listing-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 listingData(data: { listing: any; endorsementCount?: number; isAvailable?: boolean }) {
|
||
this.#data = data.listing;
|
||
this.#endorsementCount = data.endorsementCount ?? 0;
|
||
this.#isAvailable = data.isAvailable ?? true;
|
||
this.#render();
|
||
}
|
||
|
||
async #fetchAndRender() {
|
||
const space = this.getAttribute('space') || 'demo';
|
||
const listingId = this.getAttribute('listing-id');
|
||
if (!listingId) return;
|
||
|
||
try {
|
||
const res = await fetch(`/${space}/rbnb/api/listings/${listingId}`);
|
||
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 listing...</div>`;
|
||
return;
|
||
}
|
||
|
||
const d = this.#data;
|
||
const eco = ECONOMY_COLORS[d.economy] || ECONOMY_COLORS.gift;
|
||
const typeIcon = TYPE_ICONS[d.type] || '\u{1F3E8}';
|
||
const typeLabel = (d.type || 'room').replace(/_/g, ' ');
|
||
|
||
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(245,158,11,0.4);
|
||
box-shadow: 0 4px 20px rgba(245,158,11,0.08);
|
||
}
|
||
.cover {
|
||
width: 100%; height: 180px;
|
||
background: linear-gradient(135deg, rgba(245,158,11,0.15), rgba(239,68,68,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.5rem; line-height: 1.3;
|
||
}
|
||
.host {
|
||
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>
|
||
<div class="host">
|
||
<span>${this.#esc(d.host_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(245,158,11,0.08);color:#fbbf24;border:1px solid rgba(245,158,11,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{1F465} ${d.guest_capacity}
|
||
</span>
|
||
</div>
|
||
<div class="location">\u{1F4CD} ${this.#esc(d.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 + '–' + 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-listing')) {
|
||
customElements.define('folk-listing', FolkListing);
|
||
}
|
||
|
||
export { FolkListing };
|