rspace-online/modules/rbnb/components/folk-stay-request.ts

284 lines
9.2 KiB
TypeScript

/**
* <folk-stay-request> — Stay request detail view.
*
* Shows: status banner, listing info, dates, message thread,
* action buttons (accept/decline/cancel/complete), endorsement prompt.
*/
const STATUS_STYLES: Record<string, { bg: string; fg: string; label: string }> = {
pending: { bg: 'rgba(245,158,11,0.15)', fg: '#f59e0b', label: 'Pending' },
accepted: { bg: 'rgba(52,211,153,0.15)', fg: '#34d399', label: 'Accepted' },
declined: { bg: 'rgba(239,68,68,0.15)', fg: '#ef4444', label: 'Declined' },
cancelled: { bg: 'rgba(148,163,184,0.15)', fg: '#94a3b8', label: 'Cancelled' },
completed: { bg: 'rgba(96,165,250,0.15)', fg: '#60a5fa', label: 'Completed' },
endorsed: { bg: 'rgba(167,139,250,0.15)', fg: '#a78bfa', label: 'Endorsed' },
};
class FolkStayRequest extends HTMLElement {
static observedAttributes = ['stay-id', 'space'];
#shadow: ShadowRoot;
#data: any = null;
#currentDid: string = '';
constructor() {
super();
this.#shadow = this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.#fetchAndRender();
}
attributeChangedCallback() {
this.#fetchAndRender();
}
set stayData(data: any) {
this.#data = data;
this.#render();
}
set currentDid(did: string) {
this.#currentDid = did;
this.#render();
}
async #fetchAndRender() {
const space = this.getAttribute('space') || 'demo';
const stayId = this.getAttribute('stay-id');
if (!stayId) return;
try {
const res = await fetch(`/${space}/rbnb/api/stays/${stayId}`);
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 stay request...</div>`;
return;
}
const d = this.#data;
const status = STATUS_STYLES[d.status] || STATUS_STYLES.pending;
const isHost = this.#currentDid === d.host_did;
const isGuest = this.#currentDid === d.guest_did;
const checkIn = d.check_in ? new Date(d.check_in).toLocaleDateString() : '—';
const checkOut = d.check_out ? new Date(d.check_out).toLocaleDateString() : '—';
const nights = d.check_in && d.check_out
? Math.ceil((new Date(d.check_out).getTime() - new Date(d.check_in).getTime()) / 86400000)
: 0;
// Build action buttons based on status and role
let actions = '';
if (d.status === 'pending' && isHost) {
actions = `
<button class="action action--accept" data-action="accept">\u{2705} Accept</button>
<button class="action action--decline" data-action="decline">\u{274C} Decline</button>
`;
} else if (d.status === 'pending' && isGuest) {
actions = `<button class="action action--cancel" data-action="cancel">Cancel Request</button>`;
} else if (d.status === 'accepted') {
actions = `<button class="action action--complete" data-action="complete">\u{2705} Mark as Completed</button>`;
} else if (d.status === 'completed') {
actions = `<button class="action action--endorse" data-action="endorse">\u{2B50} Write Endorsement</button>`;
}
// Build message thread
const messages = (d.messages || []).map((m: any) => {
const isSent = m.sender_did === this.#currentDid;
const time = m.sent_at ? new Date(m.sent_at).toLocaleString() : '';
return `
<div class="msg ${isSent ? 'msg--sent' : 'msg--received'}">
<div class="msg__sender">${this.#esc(m.sender_name)}</div>
<div class="msg__body">${this.#esc(m.body)}</div>
<div class="msg__time">${time}</div>
</div>
`;
}).join('');
this.#shadow.innerHTML = `
<style>
:host { display: block; }
.container {
background: var(--rs-bg, #0f172a);
border: 1px solid var(--rs-border, #334155);
border-radius: 0.75rem;
overflow: hidden;
font-family: system-ui, -apple-system, sans-serif;
}
.status-banner {
padding: 0.75rem 1.25rem;
display: flex; align-items: center; justify-content: space-between;
}
.status-label {
font-size: 0.8rem; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.04em;
}
.dates {
font-size: 0.78rem; color: var(--rs-text-muted, #94a3b8);
}
.info {
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--rs-border, #334155);
}
.info h3 {
margin: 0 0 0.5rem; font-size: 0.95rem;
color: var(--rs-text, #e2e8f0);
}
.info-row {
display: flex; gap: 1.5rem; flex-wrap: wrap;
font-size: 0.82rem; color: var(--rs-text-muted, #94a3b8);
}
.info-row span { display: flex; align-items: center; gap: 0.3rem; }
.thread {
padding: 1rem 1.25rem;
display: flex; flex-direction: column; gap: 0.75rem;
max-height: 400px; overflow-y: auto;
}
.msg {
padding: 0.6rem 0.85rem; border-radius: 0.75rem;
max-width: 80%; font-size: 0.85rem; line-height: 1.5;
}
.msg--sent {
background: rgba(245,158,11,0.1);
border: 1px solid rgba(245,158,11,0.15);
align-self: flex-end;
}
.msg--received {
background: var(--rs-surface, #1e293b);
border: 1px solid var(--rs-border, #334155);
align-self: flex-start;
}
.msg__sender {
font-size: 0.72rem; font-weight: 600;
color: var(--rs-text-muted, #94a3b8);
margin-bottom: 0.15rem;
}
.msg__body { color: var(--rs-text, #e2e8f0); }
.msg__time {
font-size: 0.65rem; color: var(--rs-text-muted, #64748b);
margin-top: 0.2rem;
}
.compose {
display: flex; gap: 0.5rem;
padding: 0.75rem 1.25rem;
border-top: 1px solid var(--rs-border, #334155);
}
.compose input {
flex: 1; padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
border: 1px solid var(--rs-border, #334155);
background: var(--rs-surface, #1e293b);
color: var(--rs-text, #e2e8f0);
font-size: 0.85rem;
}
.compose input:focus {
outline: none; border-color: #f59e0b;
}
.compose button {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
border: none;
background: linear-gradient(to right, #f59e0b, #ef4444);
color: #0b1120;
font-weight: 600; font-size: 0.82rem;
cursor: pointer;
}
.actions {
display: flex; gap: 0.5rem; flex-wrap: wrap;
padding: 0.75rem 1.25rem;
border-top: 1px solid var(--rs-border, #334155);
}
.action {
padding: 0.5rem 1rem; border-radius: 0.5rem;
border: 1px solid var(--rs-border, #334155);
background: var(--rs-surface, #1e293b);
color: var(--rs-text, #e2e8f0);
font-size: 0.82rem; cursor: pointer;
transition: background 0.15s;
}
.action:hover { background: rgba(245,158,11,0.1); }
.action--accept { border-color: rgba(52,211,153,0.3); color: #34d399; }
.action--decline { border-color: rgba(239,68,68,0.3); color: #ef4444; }
.action--complete { border-color: rgba(96,165,250,0.3); color: #60a5fa; }
.action--endorse { border-color: rgba(167,139,250,0.3); color: #a78bfa; }
.action--cancel { border-color: rgba(148,163,184,0.3); color: #94a3b8; }
.empty {
padding: 2rem; text-align: center;
color: var(--rs-text-muted, #64748b); font-size: 0.85rem;
}
</style>
<div class="container">
<div class="status-banner" style="background:${status.bg}">
<span class="status-label" style="color:${status.fg}">${status.label}</span>
<span class="dates">${checkIn} \u{2192} ${checkOut} (${nights} night${nights !== 1 ? 's' : ''})</span>
</div>
<div class="info">
<h3>${this.#esc(d.guest_name)} \u{2192} Stay Request</h3>
<div class="info-row">
<span>\u{1F465} ${d.guest_count} guest${d.guest_count !== 1 ? 's' : ''}</span>
${d.offered_amount ? `<span>\u{1F4B0} ${d.offered_currency || ''} ${d.offered_amount}</span>` : ''}
${d.offered_exchange ? `<span>\u{1F91D} ${this.#esc(d.offered_exchange)}</span>` : ''}
</div>
</div>
<div class="thread">
${messages || '<div class="empty">No messages yet</div>'}
</div>
<div class="compose">
<input type="text" placeholder="Write a message..." id="msg-input">
<button id="send-btn">Send</button>
</div>
${actions ? `<div class="actions">${actions}</div>` : ''}
</div>
`;
// Wire up event listeners
this.#shadow.getElementById('send-btn')?.addEventListener('click', () => this.#sendMessage());
this.#shadow.getElementById('msg-input')?.addEventListener('keydown', (e: Event) => {
if ((e as KeyboardEvent).key === 'Enter') this.#sendMessage();
});
for (const btn of this.#shadow.querySelectorAll('.action[data-action]')) {
btn.addEventListener('click', () => {
const action = (btn as HTMLElement).dataset.action;
this.dispatchEvent(new CustomEvent('stay-action', {
detail: { stayId: this.#data.id, action },
bubbles: true,
}));
});
}
}
#sendMessage() {
const input = this.#shadow.getElementById('msg-input') as HTMLInputElement;
const body = input?.value?.trim();
if (!body) return;
this.dispatchEvent(new CustomEvent('stay-message', {
detail: { stayId: this.#data.id, body },
bubbles: true,
}));
input.value = '';
}
#esc(s: string): string {
const el = document.createElement('span');
el.textContent = s || '';
return el.innerHTML;
}
}
if (!customElements.get('folk-stay-request')) {
customElements.define('folk-stay-request', FolkStayRequest);
}
export { FolkStayRequest };