rspace-online/lib/folk-destination.ts

281 lines
6.4 KiB
TypeScript

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: 260px;
min-height: 180px;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: #059669;
color: white;
border-radius: 8px 8px 0 0;
font-size: 12px;
font-weight: 600;
cursor: move;
}
.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);
}
.dest-body {
padding: 12px;
}
.dest-name {
font-size: 16px;
font-weight: 700;
color: #1e293b;
margin-bottom: 2px;
}
.dest-country {
font-size: 12px;
color: #64748b;
margin-bottom: 8px;
}
.dest-dates {
display: flex;
gap: 12px;
font-size: 11px;
color: #475569;
margin-bottom: 10px;
padding: 6px 8px;
background: #f1f5f9;
border-radius: 4px;
}
.dest-dates span {
display: flex;
align-items: center;
gap: 4px;
}
.dest-dates .label {
color: #94a3b8;
font-weight: 500;
}
.dest-notes {
font-size: 12px;
color: #475569;
line-height: 1.5;
padding: 8px;
background: #fafaf9;
border-radius: 4px;
border: 1px solid #e2e8f0;
min-height: 40px;
white-space: pre-wrap;
}
.dest-notes[contenteditable]:focus {
outline: none;
border-color: #059669;
}
.coords {
font-size: 10px;
color: #94a3b8;
margin-top: 6px;
}
.empty-name {
color: #94a3b8;
font-style: italic;
}
`;
declare global {
interface HTMLElementTagNameMap {
"folk-destination": FolkDestination;
}
}
export class FolkDestination extends FolkShape {
static override tagName = "folk-destination";
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;
}
#destName = "";
#country = "";
#lat: number | null = null;
#lng: number | null = null;
#arrivalDate = "";
#departureDate = "";
#notes = "";
#nameEl: HTMLElement | null = null;
#countryEl: HTMLElement | null = null;
#notesEl: HTMLElement | null = null;
#datesEl: HTMLElement | null = null;
get destName() { return this.#destName; }
set destName(v: string) {
this.#destName = v;
if (this.#nameEl) this.#nameEl.textContent = v || "Unnamed destination";
this.dispatchEvent(new CustomEvent("content-change"));
}
get country() { return this.#country; }
set country(v: string) {
this.#country = v;
if (this.#countryEl) this.#countryEl.textContent = v;
this.dispatchEvent(new CustomEvent("content-change"));
}
get lat() { return this.#lat; }
set lat(v: number | null) { this.#lat = v; }
get lng() { return this.#lng; }
set lng(v: number | null) { this.#lng = v; }
get arrivalDate() { return this.#arrivalDate; }
set arrivalDate(v: string) {
this.#arrivalDate = v;
this.#renderDates();
this.dispatchEvent(new CustomEvent("content-change"));
}
get departureDate() { return this.#departureDate; }
set departureDate(v: string) {
this.#departureDate = v;
this.#renderDates();
this.dispatchEvent(new CustomEvent("content-change"));
}
get notes() { return this.#notes; }
set notes(v: string) {
this.#notes = v;
if (this.#notesEl && this.#notesEl.textContent !== v) {
this.#notesEl.textContent = v;
}
this.dispatchEvent(new CustomEvent("content-change"));
}
override createRenderRoot() {
const root = super.createRenderRoot();
const wrapper = document.createElement("div");
wrapper.innerHTML = html`
<div class="header">
<span class="header-title">
<span>\uD83D\uDCCD</span>
<span>Destination</span>
</span>
<div class="header-actions">
<button class="close-btn" title="Close">\u00D7</button>
</div>
</div>
<div class="dest-body">
<div class="dest-name"></div>
<div class="dest-country"></div>
<div class="dest-dates"></div>
<div class="dest-notes" contenteditable="true"></div>
</div>
`;
const slot = root.querySelector("slot");
const containerDiv = slot?.parentElement as HTMLElement;
if (containerDiv) {
containerDiv.replaceWith(wrapper);
}
this.#nameEl = wrapper.querySelector(".dest-name");
this.#countryEl = wrapper.querySelector(".dest-country");
this.#datesEl = wrapper.querySelector(".dest-dates");
this.#notesEl = wrapper.querySelector(".dest-notes");
if (this.#nameEl) {
this.#nameEl.textContent = this.#destName || "Unnamed destination";
if (!this.#destName) this.#nameEl.classList.add("empty-name");
}
if (this.#countryEl) this.#countryEl.textContent = this.#country;
const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement;
closeBtn.addEventListener("click", (e) => {
e.stopPropagation();
this.dispatchEvent(new CustomEvent("close"));
});
if (this.#notesEl) {
this.#notesEl.textContent = this.#notes;
this.#notesEl.addEventListener("input", () => {
this.#notes = this.#notesEl!.textContent || "";
this.dispatchEvent(new CustomEvent("content-change"));
});
this.#notesEl.addEventListener("click", (e) => e.stopPropagation());
}
this.#renderDates();
return root;
}
#renderDates() {
if (!this.#datesEl) return;
if (!this.#arrivalDate && !this.#departureDate) {
this.#datesEl.style.display = "none";
return;
}
this.#datesEl.style.display = "flex";
let result = "";
if (this.#arrivalDate) {
const d = new Date(this.#arrivalDate);
result += `<span><span class="label">Arrive:</span> ${d.toLocaleDateString("en-US", { month: "short", day: "numeric" })}</span>`;
}
if (this.#departureDate) {
const d = new Date(this.#departureDate);
result += `<span><span class="label">Depart:</span> ${d.toLocaleDateString("en-US", { month: "short", day: "numeric" })}</span>`;
}
this.#datesEl.innerHTML = result;
}
override toJSON() {
return {
...super.toJSON(),
type: "folk-destination",
destName: this.#destName,
country: this.#country,
lat: this.#lat,
lng: this.#lng,
arrivalDate: this.#arrivalDate,
departureDate: this.#departureDate,
notes: this.#notes,
};
}
}