rspace-online/modules/rmaps/components/map-meeting-modal.ts

164 lines
9.6 KiB
TypeScript

/**
* <map-meeting-modal> — meeting point creation modal for rMaps.
* Dispatches 'meeting-create' CustomEvent with { name, lat, lng, emoji } detail.
* Dispatches 'modal-close' on dismiss.
*/
const MODAL_STYLE = `position:fixed;inset:0;z-index:100;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.6);backdrop-filter:blur(4px);`;
const MEETING_EMOJIS = ["\u{1F4CD}", "\u{2B50}", "\u{1F3E0}", "\u{1F37D}", "\u{26FA}", "\u{1F3AF}", "\u{1F680}", "\u{1F33F}", "\u{26A1}", "\u{1F48E}"];
class MapMeetingModal extends HTMLElement {
private _centerLat = 0;
private _centerLng = 0;
private _myLat: number | null = null;
private _myLng: number | null = null;
private _selectedEmoji = "\u{1F4CD}";
private _searchResults: { display_name: string; lat: string; lon: string }[] = [];
set center(v: { lat: number; lng: number }) { this._centerLat = v.lat; this._centerLng = v.lng; }
set myLocation(v: { lat: number; lng: number } | null) {
if (v) { this._myLat = v.lat; this._myLng = v.lng; } else { this._myLat = null; this._myLng = null; }
}
connectedCallback() { this.render(); }
private esc(s: string): string { const d = document.createElement("div"); d.textContent = s || ""; return d.innerHTML; }
private close() {
this.dispatchEvent(new CustomEvent("modal-close", { bubbles: true, composed: true }));
this.remove();
}
private render() {
this.style.cssText = MODAL_STYLE;
this.innerHTML = `
<div style="background:var(--rs-bg-surface);border:1px solid var(--rs-border-strong);border-radius:14px;padding:20px;max-width:400px;width:90%;max-height:80vh;overflow-y:auto;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
<div style="font-size:16px;font-weight:600;color:var(--rs-text-primary);">\u{1F4CD} Set Meeting Point</div>
<button id="m-close" style="background:none;border:none;color:var(--rs-text-muted);cursor:pointer;font-size:18px;">\u2715</button>
</div>
<label style="font-size:12px;color:var(--rs-text-secondary);display:block;margin-bottom:4px;">Name</label>
<input type="text" id="m-name" placeholder="Meeting point" value="Meeting point"
style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-input-bg);color:var(--rs-text-primary);font-size:13px;margin-bottom:12px;">
<label style="font-size:12px;color:var(--rs-text-secondary);display:block;margin-bottom:6px;">Emoji</label>
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px;" id="m-emoji-picker">
${MEETING_EMOJIS.map((e, i) => `<button class="m-emoji" data-emoji="${e}" style="width:32px;height:32px;border-radius:6px;border:1px solid ${i === 0 ? "#4f46e5" : "var(--rs-border)"};background:var(--rs-input-bg);font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;">${e}</button>`).join("")}
</div>
<label style="font-size:12px;color:var(--rs-text-secondary);display:block;margin-bottom:4px;">Location</label>
<div style="display:flex;gap:6px;margin-bottom:10px;">
<button id="m-gps" style="flex:1;padding:8px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-input-bg);color:var(--rs-text-secondary);cursor:pointer;font-size:11px;">\u{1F4CD} Current GPS</button>
<button id="m-search" style="flex:1;padding:8px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-input-bg);color:var(--rs-text-secondary);cursor:pointer;font-size:11px;">\u{1F50E} Search</button>
<button id="m-manual" style="flex:1;padding:8px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-input-bg);color:var(--rs-text-secondary);cursor:pointer;font-size:11px;">\u{1F4DD} Manual</button>
</div>
<div id="m-loc-content">
<div style="font-size:12px;color:var(--rs-text-muted);text-align:center;padding:12px;">
Map center: ${this._centerLat.toFixed(5)}, ${this._centerLng.toFixed(5)}
</div>
</div>
<input type="hidden" id="m-lat" value="${this._centerLat}">
<input type="hidden" id="m-lng" value="${this._centerLng}">
<button id="m-create" style="width:100%;margin-top:14px;padding:10px;border-radius:8px;border:none;background:#4f46e5;color:#fff;font-weight:600;cursor:pointer;font-size:13px;">Drop Meeting Point</button>
</div>
`;
// Close
this.querySelector("#m-close")?.addEventListener("click", () => this.close());
this.addEventListener("click", (e) => { if (e.target === this) this.close(); });
// Emoji picker
this.querySelectorAll(".m-emoji").forEach(btn => {
btn.addEventListener("click", () => {
this._selectedEmoji = (btn as HTMLElement).dataset.emoji!;
this.querySelectorAll(".m-emoji").forEach(b => (b as HTMLElement).style.borderColor = "var(--rs-border)");
(btn as HTMLElement).style.borderColor = "#4f46e5";
});
});
// GPS
this.querySelector("#m-gps")?.addEventListener("click", () => {
const content = this.querySelector("#m-loc-content")!;
if (this._myLat !== null && this._myLng !== null) {
(this.querySelector("#m-lat") as HTMLInputElement).value = String(this._myLat);
(this.querySelector("#m-lng") as HTMLInputElement).value = String(this._myLng);
content.innerHTML = `<div style="font-size:12px;color:#22c55e;text-align:center;padding:12px;">\u2713 GPS: ${this._myLat!.toFixed(5)}, ${this._myLng!.toFixed(5)}</div>`;
} else {
content.innerHTML = `<div style="font-size:12px;color:#f59e0b;text-align:center;padding:12px;">Share your location first</div>`;
}
});
// Search
this.querySelector("#m-search")?.addEventListener("click", () => {
const content = this.querySelector("#m-loc-content")!;
content.innerHTML = `
<div style="display:flex;gap:6px;margin-bottom:8px;">
<input type="text" id="m-addr" placeholder="Search address..." style="flex:1;padding:8px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-input-bg);color:var(--rs-text-primary);font-size:12px;">
<button id="m-addr-go" style="padding:8px 12px;border-radius:6px;border:none;background:#4f46e5;color:#fff;cursor:pointer;font-size:12px;">Search</button>
</div>
<div id="m-sr"></div>
`;
this.querySelector("#m-addr-go")?.addEventListener("click", async () => {
const q = (this.querySelector("#m-addr") as HTMLInputElement).value.trim();
if (!q) return;
const sr = this.querySelector("#m-sr")!;
sr.innerHTML = '<div style="font-size:11px;color:var(--rs-text-muted);padding:6px;">Searching...</div>';
try {
const res = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(q)}&limit=5`, {
headers: { "User-Agent": "rMaps/1.0" }, signal: AbortSignal.timeout(5000),
});
this._searchResults = await res.json();
sr.innerHTML = this._searchResults.length ? this._searchResults.map((r, i) => `
<div data-sr="${i}" style="padding:6px 8px;border-radius:6px;cursor:pointer;font-size:12px;color:var(--rs-text-secondary);border:1px solid transparent;margin-bottom:4px;">${this.esc(r.display_name?.substring(0, 80))}</div>
`).join("") : '<div style="font-size:11px;color:var(--rs-text-muted);padding:6px;">No results</div>';
sr.querySelectorAll("[data-sr]").forEach(el => {
el.addEventListener("click", () => {
const r = this._searchResults[parseInt((el as HTMLElement).dataset.sr!, 10)];
(this.querySelector("#m-lat") as HTMLInputElement).value = r.lat;
(this.querySelector("#m-lng") as HTMLInputElement).value = r.lon;
sr.querySelectorAll("[data-sr]").forEach(e => (e as HTMLElement).style.borderColor = "transparent");
(el as HTMLElement).style.borderColor = "#4f46e5";
});
});
} catch { sr.innerHTML = '<div style="font-size:11px;color:#ef4444;padding:6px;">Search failed</div>'; }
});
});
// Manual
this.querySelector("#m-manual")?.addEventListener("click", () => {
const content = this.querySelector("#m-loc-content")!;
content.innerHTML = `
<div style="display:flex;gap:8px;">
<div style="flex:1;"><label style="font-size:11px;color:var(--rs-text-muted);">Latitude</label>
<input type="number" step="any" id="m-mlat" value="${this._centerLat}" style="width:100%;padding:6px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-input-bg);color:var(--rs-text-primary);font-size:12px;"></div>
<div style="flex:1;"><label style="font-size:11px;color:var(--rs-text-muted);">Longitude</label>
<input type="number" step="any" id="m-mlng" value="${this._centerLng}" style="width:100%;padding:6px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-input-bg);color:var(--rs-text-primary);font-size:12px;"></div>
</div>
`;
this.querySelector("#m-mlat")?.addEventListener("input", (e) => { (this.querySelector("#m-lat") as HTMLInputElement).value = (e.target as HTMLInputElement).value; });
this.querySelector("#m-mlng")?.addEventListener("input", (e) => { (this.querySelector("#m-lng") as HTMLInputElement).value = (e.target as HTMLInputElement).value; });
});
// Create
this.querySelector("#m-create")?.addEventListener("click", () => {
const name = (this.querySelector("#m-name") as HTMLInputElement).value.trim() || "Meeting point";
const lat = parseFloat((this.querySelector("#m-lat") as HTMLInputElement).value);
const lng = parseFloat((this.querySelector("#m-lng") as HTMLInputElement).value);
if (isNaN(lat) || isNaN(lng)) return;
this.dispatchEvent(new CustomEvent("meeting-create", {
detail: { name, lat, lng, emoji: this._selectedEmoji },
bubbles: true, composed: true,
}));
this.close();
});
}
}
customElements.define("map-meeting-modal", MapMeetingModal);
export { MapMeetingModal };