rspace-online/modules/rbnb/components/folk-listing.ts

186 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* <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 };