rspace-online/lib/folk-booking.ts

360 lines
8.0 KiB
TypeScript
Raw Permalink 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.

import { FolkShape } from "./folk-shape";
import { css, html } from "./tags";
const styles = css`
:host {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
min-width: 280px;
min-height: 160px;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
color: white;
border-radius: 8px 8px 0 0;
font-size: 12px;
font-weight: 600;
cursor: move;
}
.header.FLIGHT { background: #2563eb; }
.header.HOTEL { background: #7c3aed; }
.header.CAR_RENTAL { background: #d97706; }
.header.TRAIN { background: #059669; }
.header.BUS { background: #0891b2; }
.header.FERRY { background: #0284c7; }
.header.ACTIVITY { background: #0d9488; }
.header.RESTAURANT { background: #ea580c; }
.header.OTHER { background: #475569; }
.header-title {
display: flex;
align-items: center;
gap: 6px;
}
.header-actions button {
background: transparent;
border: none;
color: white;
cursor: pointer;
padding: 2px 6px;
border-radius: 4px;
font-size: 14px;
}
.header-actions button:hover {
background: rgba(255, 255, 255, 0.2);
}
.booking-body {
padding: 12px;
}
.provider {
font-size: 16px;
font-weight: 700;
color: #1e293b;
margin-bottom: 4px;
}
.details {
font-size: 12px;
color: #475569;
line-height: 1.5;
margin-bottom: 8px;
}
.meta-row {
display: flex;
gap: 12px;
font-size: 11px;
margin-bottom: 4px;
}
.meta-item {
display: flex;
align-items: center;
gap: 4px;
}
.meta-label {
color: #94a3b8;
}
.meta-value {
color: #1e293b;
font-weight: 500;
}
.confirmation {
margin-top: 8px;
padding: 6px 8px;
background: #f1f5f9;
border-radius: 4px;
font-size: 11px;
display: flex;
justify-content: space-between;
}
.confirmation .label {
color: #94a3b8;
}
.confirmation .code {
font-family: monospace;
font-weight: 600;
color: #1e293b;
}
.cost {
font-size: 18px;
font-weight: 700;
color: #1e293b;
margin-top: 8px;
}
.status-badge {
display: inline-block;
font-size: 10px;
font-weight: 600;
padding: 2px 8px;
border-radius: 10px;
margin-top: 8px;
}
.status-badge.PLANNED {
background: #e2e8f0;
color: #475569;
}
.status-badge.BOOKED {
background: #dbeafe;
color: #1d4ed8;
}
.status-badge.CONFIRMED {
background: #dcfce7;
color: #15803d;
}
.status-badge.CANCELLED {
background: #fee2e2;
color: #dc2626;
}
`;
const BOOKING_TYPE_ICONS: Record<string, string> = {
FLIGHT: "✈️",
HOTEL: "🏨",
CAR_RENTAL: "🚗",
TRAIN: "🚄",
BUS: "🚌",
FERRY: "⛴️",
ACTIVITY: "🎯",
RESTAURANT: "🍽️",
OTHER: "📌",
};
declare global {
interface HTMLElementTagNameMap {
"folk-booking": FolkBooking;
}
}
export class FolkBooking extends FolkShape {
static override tagName = "folk-booking";
static {
const sheet = new CSSStyleSheet();
const parentRules = Array.from(FolkShape.styles.cssRules)
.map((r) => r.cssText)
.join("\n");
const childRules = Array.from(styles.cssRules)
.map((r) => r.cssText)
.join("\n");
sheet.replaceSync(`${parentRules}\n${childRules}`);
this.styles = sheet;
}
#bookingType = "OTHER";
#provider = "";
#confirmationNumber = "";
#details = "";
#cost: number | null = null;
#currency = "USD";
#startDate = "";
#endDate = "";
#bookingStatus = "PLANNED";
#headerEl: HTMLElement | null = null;
#bodyEl: HTMLElement | null = null;
get bookingType() { return this.#bookingType; }
set bookingType(v: string) {
this.#bookingType = v;
this.#render();
this.dispatchEvent(new CustomEvent("content-change"));
}
get provider() { return this.#provider; }
set provider(v: string) {
this.#provider = v;
this.#render();
this.dispatchEvent(new CustomEvent("content-change"));
}
get confirmationNumber() { return this.#confirmationNumber; }
set confirmationNumber(v: string) {
this.#confirmationNumber = v;
this.#render();
this.dispatchEvent(new CustomEvent("content-change"));
}
get details() { return this.#details; }
set details(v: string) {
this.#details = v;
this.#render();
this.dispatchEvent(new CustomEvent("content-change"));
}
get cost() { return this.#cost; }
set cost(v: number | null) {
this.#cost = v;
this.#render();
this.dispatchEvent(new CustomEvent("content-change"));
}
get currency() { return this.#currency; }
set currency(v: string) { this.#currency = v; this.#render(); }
get startDate() { return this.#startDate; }
set startDate(v: string) { this.#startDate = v; this.#render(); }
get endDate() { return this.#endDate; }
set endDate(v: string) { this.#endDate = v; this.#render(); }
get bookingStatus() { return this.#bookingStatus; }
set bookingStatus(v: string) {
this.#bookingStatus = v;
this.#render();
this.dispatchEvent(new CustomEvent("content-change"));
}
override createRenderRoot() {
const root = super.createRenderRoot();
const wrapper = document.createElement("div");
wrapper.innerHTML = html`
<div class="header OTHER">
<span class="header-title">
<span class="type-icon">📌</span>
<span class="type-label">Booking</span>
</span>
<div class="header-actions">
<button class="close-btn" title="Close">×</button>
</div>
</div>
<div class="booking-body"></div>
`;
const slot = root.querySelector("slot");
const containerDiv = slot?.parentElement as HTMLElement;
if (containerDiv) {
containerDiv.replaceWith(wrapper);
}
this.#headerEl = wrapper.querySelector(".header");
this.#bodyEl = wrapper.querySelector(".booking-body");
const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement;
closeBtn.addEventListener("click", (e) => {
e.stopPropagation();
this.dispatchEvent(new CustomEvent("close"));
});
this.#render();
return root;
}
#escapeHtml(text: string): string {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
#formatDate(dateStr: string): string {
if (!dateStr) return "";
const d = new Date(dateStr);
return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
}
#render() {
if (!this.#headerEl || !this.#bodyEl) return;
const icon = BOOKING_TYPE_ICONS[this.#bookingType] || BOOKING_TYPE_ICONS.OTHER;
// Update header
this.#headerEl.className = `header ${this.#bookingType}`;
const iconEl = this.#headerEl.querySelector(".type-icon");
const labelEl = this.#headerEl.querySelector(".type-label");
if (iconEl) iconEl.textContent = icon;
if (labelEl) labelEl.textContent = this.#bookingType.replace("_", " ");
// Build body
let bodyHTML = "";
if (this.#provider) {
bodyHTML += `<div class="provider">${this.#escapeHtml(this.#provider)}</div>`;
}
if (this.#details) {
bodyHTML += `<div class="details">${this.#escapeHtml(this.#details)}</div>`;
}
if (this.#startDate || this.#endDate) {
bodyHTML += `<div class="meta-row">`;
if (this.#startDate) {
bodyHTML += `<div class="meta-item"><span class="meta-label">From:</span><span class="meta-value">${this.#formatDate(this.#startDate)}</span></div>`;
}
if (this.#endDate) {
bodyHTML += `<div class="meta-item"><span class="meta-label">To:</span><span class="meta-value">${this.#formatDate(this.#endDate)}</span></div>`;
}
bodyHTML += `</div>`;
}
if (this.#confirmationNumber) {
bodyHTML += `<div class="confirmation"><span class="label">Confirmation</span><span class="code">${this.#escapeHtml(this.#confirmationNumber)}</span></div>`;
}
if (this.#cost !== null) {
bodyHTML += `<div class="cost">${this.#currency} ${this.#cost.toLocaleString()}</div>`;
}
bodyHTML += `<span class="status-badge ${this.#bookingStatus}">${this.#bookingStatus}</span>`;
this.#bodyEl.innerHTML = bodyHTML;
}
override toJSON() {
return {
...super.toJSON(),
type: "folk-booking",
bookingType: this.#bookingType,
provider: this.#provider,
confirmationNumber: this.#confirmationNumber,
details: this.#details,
cost: this.#cost,
currency: this.#currency,
startDate: this.#startDate,
endDate: this.#endDate,
bookingStatus: this.#bookingStatus,
};
}
}