|
|
|
@ -287,9 +287,8 @@ class FolkMapViewer extends HTMLElement {
|
|
|
|
if (this.userName) return true;
|
|
|
|
if (this.userName) return true;
|
|
|
|
// Use EncryptID username if authenticated
|
|
|
|
// Use EncryptID username if authenticated
|
|
|
|
const identityName = getUsername();
|
|
|
|
const identityName = getUsername();
|
|
|
|
const name = identityName || prompt("Your display name for this room:");
|
|
|
|
if (identityName) {
|
|
|
|
if (!name?.trim()) return false;
|
|
|
|
this.userName = identityName;
|
|
|
|
this.userName = name.trim();
|
|
|
|
|
|
|
|
localStorage.setItem("rmaps_user", JSON.stringify({
|
|
|
|
localStorage.setItem("rmaps_user", JSON.stringify({
|
|
|
|
id: this.participantId,
|
|
|
|
id: this.participantId,
|
|
|
|
name: this.userName,
|
|
|
|
name: this.userName,
|
|
|
|
@ -298,6 +297,99 @@ class FolkMapViewer extends HTMLElement {
|
|
|
|
}));
|
|
|
|
}));
|
|
|
|
return true;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private pendingRoomSlug = "";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private showJoinForm(slug: string) {
|
|
|
|
|
|
|
|
this.pendingRoomSlug = slug;
|
|
|
|
|
|
|
|
const saved = JSON.parse(localStorage.getItem("rmaps_user") || "null");
|
|
|
|
|
|
|
|
const savedName = saved?.name || "";
|
|
|
|
|
|
|
|
const savedEmoji = saved?.emoji || this.userEmoji;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const overlay = document.createElement("div");
|
|
|
|
|
|
|
|
overlay.id = "join-form-overlay";
|
|
|
|
|
|
|
|
overlay.style.cssText = `
|
|
|
|
|
|
|
|
position:fixed;inset:0;z-index:50;background:rgba(0,0,0,0.6);
|
|
|
|
|
|
|
|
display:flex;align-items:center;justify-content:center;
|
|
|
|
|
|
|
|
backdrop-filter:blur(4px);
|
|
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const card = document.createElement("div");
|
|
|
|
|
|
|
|
card.style.cssText = `
|
|
|
|
|
|
|
|
background:var(--rs-bg-surface);border:1px solid var(--rs-border-strong);
|
|
|
|
|
|
|
|
border-radius:16px;padding:28px;width:340px;max-width:90vw;
|
|
|
|
|
|
|
|
box-shadow:0 16px 48px rgba(0,0,0,0.4);text-align:center;
|
|
|
|
|
|
|
|
font-family:system-ui,-apple-system,sans-serif;
|
|
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
card.innerHTML = `
|
|
|
|
|
|
|
|
<div style="font-size:48px;margin-bottom:4px;" id="join-avatar-preview">${savedEmoji}</div>
|
|
|
|
|
|
|
|
<div style="font-size:18px;font-weight:700;color:var(--rs-text-primary);margin-bottom:4px;">Join ${this.esc(slug)}</div>
|
|
|
|
|
|
|
|
<div style="font-size:13px;color:var(--rs-text-muted);margin-bottom:16px;">Choose your avatar and display name</div>
|
|
|
|
|
|
|
|
<div style="display:flex;flex-wrap:wrap;gap:6px;justify-content:center;margin-bottom:16px;">
|
|
|
|
|
|
|
|
${EMOJIS.map(e => `<button class="join-emoji-opt" data-e="${e}" style="width:36px;height:36px;border-radius:8px;border:2px solid ${e === savedEmoji ? "#4f46e5" : "transparent"};background:${e === savedEmoji ? "#4f46e520" : "var(--rs-bg-surface-sunken)"};font-size:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:border-color 0.15s;">${e}</button>`).join("")}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<input id="join-name-input" type="text" placeholder="Your display name" value="${this.esc(savedName)}" style="
|
|
|
|
|
|
|
|
width:100%;padding:10px 14px;border-radius:8px;border:1px solid var(--rs-border);
|
|
|
|
|
|
|
|
background:var(--rs-input-bg);color:var(--rs-text-primary);font-size:15px;
|
|
|
|
|
|
|
|
outline:none;margin-bottom:12px;box-sizing:border-box;
|
|
|
|
|
|
|
|
">
|
|
|
|
|
|
|
|
${savedName ? `<button id="join-quick-btn" style="
|
|
|
|
|
|
|
|
width:100%;padding:10px;border-radius:8px;border:none;
|
|
|
|
|
|
|
|
background:#4f46e5;color:#fff;font-weight:600;cursor:pointer;font-size:14px;
|
|
|
|
|
|
|
|
margin-bottom:8px;
|
|
|
|
|
|
|
|
">Join as ${this.esc(savedEmoji)} ${this.esc(savedName)}</button>` : ""}
|
|
|
|
|
|
|
|
<button id="join-submit-btn" style="
|
|
|
|
|
|
|
|
width:100%;padding:10px;border-radius:8px;border:1px solid var(--rs-border);
|
|
|
|
|
|
|
|
background:var(--rs-bg-surface);color:var(--rs-text-primary);font-weight:600;cursor:pointer;font-size:14px;
|
|
|
|
|
|
|
|
">${savedName ? "Join with new name" : "Join Room"}</button>
|
|
|
|
|
|
|
|
<button id="join-cancel-btn" style="
|
|
|
|
|
|
|
|
margin-top:8px;background:none;border:none;color:var(--rs-text-muted);
|
|
|
|
|
|
|
|
cursor:pointer;font-size:12px;padding:4px;
|
|
|
|
|
|
|
|
">Cancel</button>
|
|
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
overlay.appendChild(card);
|
|
|
|
|
|
|
|
this.shadow.appendChild(overlay);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let selectedEmoji = savedEmoji;
|
|
|
|
|
|
|
|
const preview = card.querySelector("#join-avatar-preview") as HTMLElement;
|
|
|
|
|
|
|
|
const nameInput = card.querySelector("#join-name-input") as HTMLInputElement;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
card.querySelectorAll(".join-emoji-opt").forEach(btn => {
|
|
|
|
|
|
|
|
btn.addEventListener("click", () => {
|
|
|
|
|
|
|
|
selectedEmoji = (btn as HTMLElement).dataset.e!;
|
|
|
|
|
|
|
|
preview.textContent = selectedEmoji;
|
|
|
|
|
|
|
|
card.querySelectorAll(".join-emoji-opt").forEach(b => {
|
|
|
|
|
|
|
|
const isSelected = (b as HTMLElement).dataset.e === selectedEmoji;
|
|
|
|
|
|
|
|
(b as HTMLElement).style.borderColor = isSelected ? "#4f46e5" : "transparent";
|
|
|
|
|
|
|
|
(b as HTMLElement).style.background = isSelected ? "#4f46e520" : "var(--rs-bg-surface-sunken)";
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const doJoin = (name: string, emoji: string) => {
|
|
|
|
|
|
|
|
if (!name.trim()) { nameInput.style.borderColor = "#ef4444"; return; }
|
|
|
|
|
|
|
|
this.userName = name.trim();
|
|
|
|
|
|
|
|
this.userEmoji = emoji;
|
|
|
|
|
|
|
|
localStorage.setItem("rmaps_user", JSON.stringify({
|
|
|
|
|
|
|
|
id: this.participantId, name: this.userName,
|
|
|
|
|
|
|
|
emoji: this.userEmoji, color: this.userColor,
|
|
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
overlay.remove();
|
|
|
|
|
|
|
|
this.joinRoom(this.pendingRoomSlug);
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
card.querySelector("#join-quick-btn")?.addEventListener("click", () => doJoin(savedName, savedEmoji));
|
|
|
|
|
|
|
|
card.querySelector("#join-submit-btn")?.addEventListener("click", () => doJoin(nameInput.value, selectedEmoji));
|
|
|
|
|
|
|
|
nameInput.addEventListener("keydown", (e) => { if (e.key === "Enter") doJoin(nameInput.value, selectedEmoji); });
|
|
|
|
|
|
|
|
card.querySelector("#join-cancel-btn")?.addEventListener("click", () => overlay.remove());
|
|
|
|
|
|
|
|
overlay.addEventListener("click", (e) => { if (e.target === overlay) overlay.remove(); });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
nameInput.focus();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─── Demo mode ───────────────────────────────────────────────
|
|
|
|
// ─── Demo mode ───────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
@ -1071,7 +1163,10 @@ class FolkMapViewer extends HTMLElement {
|
|
|
|
// ─── Room mode: join / leave / create ────────────────────────
|
|
|
|
// ─── Room mode: join / leave / create ────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
private joinRoom(slug: string) {
|
|
|
|
private joinRoom(slug: string) {
|
|
|
|
if (!this.ensureUserProfile()) return;
|
|
|
|
if (!this.ensureUserProfile()) {
|
|
|
|
|
|
|
|
this.showJoinForm(slug);
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
this.room = slug;
|
|
|
|
this.room = slug;
|
|
|
|
this.view = "map";
|
|
|
|
this.view = "map";
|
|
|
|
this.render();
|
|
|
|
this.render();
|
|
|
|
@ -1110,10 +1205,57 @@ class FolkMapViewer extends HTMLElement {
|
|
|
|
|
|
|
|
|
|
|
|
private createRoom() {
|
|
|
|
private createRoom() {
|
|
|
|
if (!requireAuth("create map room")) return;
|
|
|
|
if (!requireAuth("create map room")) return;
|
|
|
|
const name = prompt("Room name (slug):");
|
|
|
|
this.showCreateRoomForm();
|
|
|
|
if (!name?.trim()) return;
|
|
|
|
}
|
|
|
|
const slug = name.trim().toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
|
|
|
|
|
|
|
|
|
|
private showCreateRoomForm() {
|
|
|
|
|
|
|
|
const overlay = document.createElement("div");
|
|
|
|
|
|
|
|
overlay.id = "create-room-overlay";
|
|
|
|
|
|
|
|
overlay.style.cssText = `
|
|
|
|
|
|
|
|
position:fixed;inset:0;z-index:50;background:rgba(0,0,0,0.6);
|
|
|
|
|
|
|
|
display:flex;align-items:center;justify-content:center;backdrop-filter:blur(4px);
|
|
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
const card = document.createElement("div");
|
|
|
|
|
|
|
|
card.style.cssText = `
|
|
|
|
|
|
|
|
background:var(--rs-bg-surface);border:1px solid var(--rs-border-strong);
|
|
|
|
|
|
|
|
border-radius:16px;padding:28px;width:340px;max-width:90vw;
|
|
|
|
|
|
|
|
box-shadow:0 16px 48px rgba(0,0,0,0.4);text-align:center;
|
|
|
|
|
|
|
|
font-family:system-ui,-apple-system,sans-serif;
|
|
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
card.innerHTML = `
|
|
|
|
|
|
|
|
<div style="font-size:32px;margin-bottom:8px;">🌐</div>
|
|
|
|
|
|
|
|
<div style="font-size:18px;font-weight:700;color:var(--rs-text-primary);margin-bottom:4px;">Create Room</div>
|
|
|
|
|
|
|
|
<div style="font-size:13px;color:var(--rs-text-muted);margin-bottom:16px;">Enter a name for your new map room</div>
|
|
|
|
|
|
|
|
<input id="room-name-input" type="text" placeholder="Room name (e.g. berlin-meetup)" style="
|
|
|
|
|
|
|
|
width:100%;padding:10px 14px;border-radius:8px;border:1px solid var(--rs-border);
|
|
|
|
|
|
|
|
background:var(--rs-input-bg);color:var(--rs-text-primary);font-size:15px;
|
|
|
|
|
|
|
|
outline:none;margin-bottom:12px;box-sizing:border-box;
|
|
|
|
|
|
|
|
">
|
|
|
|
|
|
|
|
<button id="room-create-btn" style="
|
|
|
|
|
|
|
|
width:100%;padding:10px;border-radius:8px;border:none;
|
|
|
|
|
|
|
|
background:#4f46e5;color:#fff;font-weight:600;cursor:pointer;font-size:14px;
|
|
|
|
|
|
|
|
">Create & Join</button>
|
|
|
|
|
|
|
|
<button id="room-cancel-btn" style="
|
|
|
|
|
|
|
|
margin-top:8px;background:none;border:none;color:var(--rs-text-muted);
|
|
|
|
|
|
|
|
cursor:pointer;font-size:12px;padding:4px;
|
|
|
|
|
|
|
|
">Cancel</button>
|
|
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
overlay.appendChild(card);
|
|
|
|
|
|
|
|
this.shadow.appendChild(overlay);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const input = card.querySelector("#room-name-input") as HTMLInputElement;
|
|
|
|
|
|
|
|
const doCreate = () => {
|
|
|
|
|
|
|
|
const name = input.value.trim();
|
|
|
|
|
|
|
|
if (!name) { input.style.borderColor = "#ef4444"; return; }
|
|
|
|
|
|
|
|
const slug = name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
|
|
|
|
|
|
overlay.remove();
|
|
|
|
this.joinRoom(slug);
|
|
|
|
this.joinRoom(slug);
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
card.querySelector("#room-create-btn")?.addEventListener("click", doCreate);
|
|
|
|
|
|
|
|
input.addEventListener("keydown", (e) => { if (e.key === "Enter") doCreate(); });
|
|
|
|
|
|
|
|
card.querySelector("#room-cancel-btn")?.addEventListener("click", () => overlay.remove());
|
|
|
|
|
|
|
|
overlay.addEventListener("click", (e) => { if (e.target === overlay) overlay.remove(); });
|
|
|
|
|
|
|
|
input.focus();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private leaveRoom() {
|
|
|
|
private leaveRoom() {
|
|
|
|
@ -1258,6 +1400,7 @@ class FolkMapViewer extends HTMLElement {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
saveRoomVisit(this.room, this.room);
|
|
|
|
saveRoomVisit(this.room, this.room);
|
|
|
|
|
|
|
|
try { localStorage.setItem("rmaps_last_room", this.room); } catch {}
|
|
|
|
|
|
|
|
|
|
|
|
// Listen for SW-forwarded location request pushes
|
|
|
|
// Listen for SW-forwarded location request pushes
|
|
|
|
if ("serviceWorker" in navigator) {
|
|
|
|
if ("serviceWorker" in navigator) {
|
|
|
|
@ -1379,6 +1522,21 @@ class FolkMapViewer extends HTMLElement {
|
|
|
|
emojiSpan.className = "marker-emoji";
|
|
|
|
emojiSpan.className = "marker-emoji";
|
|
|
|
emojiSpan.textContent = p.emoji;
|
|
|
|
emojiSpan.textContent = p.emoji;
|
|
|
|
el.appendChild(emojiSpan);
|
|
|
|
el.appendChild(emojiSpan);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Status dot overlay (bottom-right corner)
|
|
|
|
|
|
|
|
const statusDot = document.createElement("div");
|
|
|
|
|
|
|
|
statusDot.className = "marker-status-dot";
|
|
|
|
|
|
|
|
const dotColor = isStale ? "#6b7280" : (
|
|
|
|
|
|
|
|
p.status === "online" ? "#22c55e" : p.status === "away" ? "#f59e0b" :
|
|
|
|
|
|
|
|
p.status === "ghost" ? "#64748b" : "#ef4444"
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
statusDot.style.cssText = `
|
|
|
|
|
|
|
|
position:absolute;bottom:-1px;right:-1px;
|
|
|
|
|
|
|
|
width:10px;height:10px;border-radius:50%;
|
|
|
|
|
|
|
|
background:${dotColor};border:2px solid ${markerBg};
|
|
|
|
|
|
|
|
box-shadow:0 0 6px ${dotColor};
|
|
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
el.appendChild(statusDot);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Staleness tooltip
|
|
|
|
// Staleness tooltip
|
|
|
|
@ -1477,6 +1635,13 @@ class FolkMapViewer extends HTMLElement {
|
|
|
|
this.indoorView.updateParticipants(state.participants);
|
|
|
|
this.indoorView.updateParticipants(state.participants);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Update participant counts (header + mobile)
|
|
|
|
|
|
|
|
const pCount = String(Object.keys(state.participants).length);
|
|
|
|
|
|
|
|
const headerCount = this.shadow.getElementById("header-participant-count");
|
|
|
|
|
|
|
|
if (headerCount) headerCount.textContent = pCount;
|
|
|
|
|
|
|
|
const mobileCount = this.shadow.getElementById("mobile-friends-count");
|
|
|
|
|
|
|
|
if (mobileCount) mobileCount.textContent = pCount;
|
|
|
|
|
|
|
|
|
|
|
|
// Update participant list sidebar
|
|
|
|
// Update participant list sidebar
|
|
|
|
this.updateParticipantList(state);
|
|
|
|
this.updateParticipantList(state);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -1508,11 +1673,12 @@ class FolkMapViewer extends HTMLElement {
|
|
|
|
const ageSec = Math.floor(ageMs / 1000);
|
|
|
|
const ageSec = Math.floor(ageMs / 1000);
|
|
|
|
ageLabel = ageSec < 60 ? `${ageSec}s ago` : ageSec < 3600 ? `${Math.floor(ageSec / 60)}m ago` : "stale";
|
|
|
|
ageLabel = ageSec < 60 ? `${ageSec}s ago` : ageSec < 3600 ? `${Math.floor(ageSec / 60)}m ago` : "stale";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const glowColor = isStale ? "#6b7280" : statusColor;
|
|
|
|
return `
|
|
|
|
return `
|
|
|
|
<div class="participant-entry" style="display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid var(--rs-border);${isStale ? "opacity:0.6;" : ""}">
|
|
|
|
<div class="participant-entry" style="display:flex;align-items:center;gap:8px;padding:8px 4px;border-bottom:1px solid var(--rs-border);${isStale ? "opacity:0.6;" : ""}">
|
|
|
|
<div style="position:relative;">
|
|
|
|
<div style="position:relative;flex-shrink:0;">
|
|
|
|
<span style="font-size:18px">${this.esc(p.emoji)}</span>
|
|
|
|
<span style="font-size:20px;display:block;width:28px;text-align:center;">${this.esc(p.emoji)}</span>
|
|
|
|
<span style="position:absolute;bottom:-2px;right:-2px;width:8px;height:8px;border-radius:50%;background:${isStale ? "#6b7280" : statusColor};border:2px solid var(--rs-bg-surface);"></span>
|
|
|
|
<span style="position:absolute;bottom:-1px;right:-1px;width:10px;height:10px;border-radius:50%;background:${glowColor};border:2px solid var(--rs-bg-surface);box-shadow:0 0 6px ${glowColor};"></span>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div style="flex:1;min-width:0;">
|
|
|
|
<div style="flex:1;min-width:0;">
|
|
|
|
<div style="font-size:13px;font-weight:600;color:${isStale ? "#6b7280" : p.color};overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${this.esc(p.name)}</div>
|
|
|
|
<div style="font-size:13px;font-weight:600;color:${isStale ? "#6b7280" : p.color};overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${this.esc(p.name)}</div>
|
|
|
|
@ -1521,8 +1687,8 @@ class FolkMapViewer extends HTMLElement {
|
|
|
|
${distLabel ? ` \u2022 ${distLabel}` : ""}
|
|
|
|
${distLabel ? ` \u2022 ${distLabel}` : ""}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
${p.id !== this.participantId && p.location ? `<button class="nav-to-btn" data-nav-participant="${p.id}" title="Navigate" style="background:none;border:1px solid var(--rs-border);border-radius:4px;color:var(--rs-text-muted);cursor:pointer;padding:2px 6px;font-size:11px;">\u{1F9ED}</button>` : ""}
|
|
|
|
${p.id !== this.participantId && p.location ? `<button class="nav-to-btn" data-nav-participant="${p.id}" title="Navigate" style="background:none;border:1px solid var(--rs-border);border-radius:6px;color:var(--rs-text-muted);cursor:pointer;padding:3px 8px;font-size:12px;">🧭</button>` : ""}
|
|
|
|
${p.id !== this.participantId ? `<button class="ping-btn-inline" data-ping="${p.id}" title="Ping" style="background:none;border:1px solid var(--rs-border);border-radius:4px;color:var(--rs-text-muted);cursor:pointer;padding:2px 6px;font-size:11px;">\u{1F514}</button>` : ""}
|
|
|
|
${p.id !== this.participantId ? `<button class="ping-btn-inline" data-ping="${p.id}" title="Ping" style="background:none;border:1px solid var(--rs-border);border-radius:6px;color:var(--rs-text-muted);cursor:pointer;padding:3px 8px;font-size:12px;">🔔</button>` : ""}
|
|
|
|
</div>`;
|
|
|
|
</div>`;
|
|
|
|
}).join("");
|
|
|
|
}).join("");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -1562,29 +1728,33 @@ class FolkMapViewer extends HTMLElement {
|
|
|
|
const list = this.shadow.getElementById("participant-list");
|
|
|
|
const list = this.shadow.getElementById("participant-list");
|
|
|
|
const mobileList = this.shadow.getElementById("participant-list-mobile");
|
|
|
|
const mobileList = this.shadow.getElementById("participant-list-mobile");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const count = Object.keys(state.participants).length;
|
|
|
|
const html = this.buildParticipantHTML(state);
|
|
|
|
const html = this.buildParticipantHTML(state);
|
|
|
|
|
|
|
|
const headerHTML = `
|
|
|
|
|
|
|
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
|
|
|
|
|
|
|
|
<span style="font-size:12px;font-weight:600;color:var(--rs-text-secondary);text-transform:uppercase;letter-spacing:0.06em;">Friends (${count})</span>
|
|
|
|
|
|
|
|
<button id="sidebar-ping-all-btn" style="padding:3px 8px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-bg-surface);color:var(--rs-text-secondary);cursor:pointer;font-size:10px;white-space:nowrap;">📢 Ping All</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
`;
|
|
|
|
const footerHTML = `
|
|
|
|
const footerHTML = `
|
|
|
|
<div style="display:flex;gap:6px;margin-top:10px;flex-wrap:wrap;">
|
|
|
|
<div style="display:flex;gap:6px;margin-top:10px;flex-wrap:wrap;">
|
|
|
|
<button id="sidebar-meeting-btn" style="flex:1;padding:6px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-bg-surface);color:var(--rs-text-secondary);cursor:pointer;font-size:11px;white-space:nowrap;">\u{1F4CD} Meeting Point</button>
|
|
|
|
<button id="sidebar-meeting-btn" style="flex:1;padding:6px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-bg-surface);color:var(--rs-text-secondary);cursor:pointer;font-size:11px;white-space:nowrap;">📍 Meeting Point</button>
|
|
|
|
<button id="sidebar-import-btn" style="flex:1;padding:6px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-bg-surface);color:var(--rs-text-secondary);cursor:pointer;font-size:11px;white-space:nowrap;">\u{1F4E5} Import Places</button>
|
|
|
|
<button id="sidebar-import-btn" style="flex:1;padding:6px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-bg-surface);color:var(--rs-text-secondary);cursor:pointer;font-size:11px;white-space:nowrap;">📥 Import Places</button>
|
|
|
|
<button id="sidebar-ping-all-btn" style="flex:1;padding:6px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-bg-surface);color:var(--rs-text-secondary);cursor:pointer;font-size:11px;white-space:nowrap;">\u{1F4E2} Ping All</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
`;
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// Desktop sidebar
|
|
|
|
// Desktop sidebar
|
|
|
|
if (list) {
|
|
|
|
if (list) {
|
|
|
|
list.innerHTML = html + footerHTML;
|
|
|
|
list.innerHTML = headerHTML + html + footerHTML;
|
|
|
|
this.attachParticipantListeners(list);
|
|
|
|
this.attachParticipantListeners(list);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Mobile bottom sheet
|
|
|
|
// Mobile bottom sheet
|
|
|
|
if (mobileList) {
|
|
|
|
if (mobileList) {
|
|
|
|
mobileList.innerHTML = html;
|
|
|
|
mobileList.innerHTML = html + footerHTML;
|
|
|
|
this.attachParticipantListeners(mobileList);
|
|
|
|
this.attachParticipantListeners(mobileList);
|
|
|
|
// Update sheet header count
|
|
|
|
const sheetCount = this.shadow.getElementById("sheet-participant-count");
|
|
|
|
const header = this.shadow.querySelector(".sheet-header span:first-child");
|
|
|
|
if (sheetCount) sheetCount.textContent = String(count);
|
|
|
|
const count = Object.keys(state.participants).length;
|
|
|
|
|
|
|
|
if (header) header.textContent = `Participants (${count})`;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -1622,6 +1792,16 @@ class FolkMapViewer extends HTMLElement {
|
|
|
|
el.title = `${p.name} - ${p.status} (${ageLabel})`;
|
|
|
|
el.title = `${p.name} - ${p.status} (${ageLabel})`;
|
|
|
|
const arrow = el.querySelector(".heading-arrow") as HTMLElement | null;
|
|
|
|
const arrow = el.querySelector(".heading-arrow") as HTMLElement | null;
|
|
|
|
if (arrow) arrow.style.borderBottomColor = isStale ? "#6b7280" : p.color;
|
|
|
|
if (arrow) arrow.style.borderBottomColor = isStale ? "#6b7280" : p.color;
|
|
|
|
|
|
|
|
// Update status dot
|
|
|
|
|
|
|
|
const statusDot = el.querySelector(".marker-status-dot") as HTMLElement | null;
|
|
|
|
|
|
|
|
if (statusDot) {
|
|
|
|
|
|
|
|
const dotColor = isStale ? "#6b7280" : (
|
|
|
|
|
|
|
|
p.status === "online" ? "#22c55e" : p.status === "away" ? "#f59e0b" :
|
|
|
|
|
|
|
|
p.status === "ghost" ? "#64748b" : "#ef4444"
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
statusDot.style.background = dotColor;
|
|
|
|
|
|
|
|
statusDot.style.boxShadow = `0 0 6px ${dotColor}`;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -1786,7 +1966,7 @@ class FolkMapViewer extends HTMLElement {
|
|
|
|
|
|
|
|
|
|
|
|
private updateShareButton() {
|
|
|
|
private updateShareButton() {
|
|
|
|
const btn = this.shadow.getElementById("share-location");
|
|
|
|
const btn = this.shadow.getElementById("share-location");
|
|
|
|
if (!btn) return;
|
|
|
|
if (btn) {
|
|
|
|
if (this.privacySettings.precision === "hidden") {
|
|
|
|
if (this.privacySettings.precision === "hidden") {
|
|
|
|
btn.textContent = "\u{1F47B} Hidden";
|
|
|
|
btn.textContent = "\u{1F47B} Hidden";
|
|
|
|
btn.classList.remove("sharing");
|
|
|
|
btn.classList.remove("sharing");
|
|
|
|
@ -1800,6 +1980,22 @@ class FolkMapViewer extends HTMLElement {
|
|
|
|
btn.classList.remove("sharing");
|
|
|
|
btn.classList.remove("sharing");
|
|
|
|
btn.classList.remove("ghost");
|
|
|
|
btn.classList.remove("ghost");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Floating share button
|
|
|
|
|
|
|
|
const floatBtn = this.shadow.getElementById("map-share-float");
|
|
|
|
|
|
|
|
if (floatBtn) {
|
|
|
|
|
|
|
|
if (this.privacySettings.precision === "hidden") {
|
|
|
|
|
|
|
|
floatBtn.innerHTML = "👻 Hidden";
|
|
|
|
|
|
|
|
floatBtn.className = "map-share-float ghost";
|
|
|
|
|
|
|
|
} else if (this.sharingLocation) {
|
|
|
|
|
|
|
|
floatBtn.innerHTML = "📍 Sharing";
|
|
|
|
|
|
|
|
floatBtn.className = "map-share-float active";
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
floatBtn.innerHTML = "📍 Share Location";
|
|
|
|
|
|
|
|
floatBtn.className = "map-share-float";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
// Update permission indicator
|
|
|
|
// Update permission indicator
|
|
|
|
const permIndicator = this.shadow.getElementById("geo-perm-indicator");
|
|
|
|
const permIndicator = this.shadow.getElementById("geo-perm-indicator");
|
|
|
|
if (permIndicator) {
|
|
|
|
if (permIndicator) {
|
|
|
|
@ -1807,6 +2003,11 @@ class FolkMapViewer extends HTMLElement {
|
|
|
|
permIndicator.style.background = colors[this.geoPermissionState] || "#64748b";
|
|
|
|
permIndicator.style.background = colors[this.geoPermissionState] || "#64748b";
|
|
|
|
permIndicator.title = `Geolocation: ${this.geoPermissionState || "unknown"}`;
|
|
|
|
permIndicator.title = `Geolocation: ${this.geoPermissionState || "unknown"}`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update header share toggle
|
|
|
|
|
|
|
|
const headerToggle = this.shadow.getElementById("header-share-toggle");
|
|
|
|
|
|
|
|
if (headerToggle) {
|
|
|
|
|
|
|
|
headerToggle.className = `map-header__share-toggle ${this.sharingLocation ? "active" : ""}`;
|
|
|
|
|
|
|
|
}
|
|
|
|
// Also update mobile FAB
|
|
|
|
// Also update mobile FAB
|
|
|
|
this.updateMobileFab();
|
|
|
|
this.updateMobileFab();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -2369,11 +2570,49 @@ class FolkMapViewer extends HTMLElement {
|
|
|
|
:host { display: block; font-family: system-ui, -apple-system, sans-serif; color: var(--rs-text-primary); }
|
|
|
|
:host { display: block; font-family: system-ui, -apple-system, sans-serif; color: var(--rs-text-primary); }
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* Lobby nav */
|
|
|
|
.rapp-nav { display: flex; gap: 8px; margin-bottom: 16px; align-items: center; min-height: 36px; }
|
|
|
|
.rapp-nav { display: flex; gap: 8px; margin-bottom: 16px; align-items: center; min-height: 36px; }
|
|
|
|
.rapp-nav__back { padding: 4px 10px; border-radius: 6px; border: 1px solid var(--rs-border); background: transparent; color: var(--rs-text-secondary); cursor: pointer; font-size: 13px; }
|
|
|
|
.rapp-nav__back { padding: 4px 10px; border-radius: 6px; border: 1px solid var(--rs-border); background: transparent; color: var(--rs-text-secondary); cursor: pointer; font-size: 13px; }
|
|
|
|
.rapp-nav__back:hover { color: var(--rs-text-primary); border-color: var(--rs-border-strong); }
|
|
|
|
.rapp-nav__back:hover { color: var(--rs-text-primary); border-color: var(--rs-border-strong); }
|
|
|
|
.rapp-nav__title { font-size: 15px; font-weight: 600; flex: 1; color: var(--rs-text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
|
.rapp-nav__title { font-size: 15px; font-weight: 600; flex: 1; color: var(--rs-text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* Map room header — dark bar */
|
|
|
|
|
|
|
|
.map-header {
|
|
|
|
|
|
|
|
display:flex;align-items:center;gap:8px;padding:8px 14px;
|
|
|
|
|
|
|
|
background:rgba(15,23,42,0.95);backdrop-filter:blur(8px);
|
|
|
|
|
|
|
|
border-radius:10px;margin-bottom:12px;min-height:44px;
|
|
|
|
|
|
|
|
border:1px solid rgba(255,255,255,0.08);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.map-header__left { display:flex;align-items:center;gap:6px;flex:1;min-width:0; }
|
|
|
|
|
|
|
|
.map-header__back {
|
|
|
|
|
|
|
|
background:none;border:none;color:#94a3b8;cursor:pointer;
|
|
|
|
|
|
|
|
font-size:16px;padding:4px 6px;border-radius:4px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.map-header__back:hover { color:#e2e8f0; }
|
|
|
|
|
|
|
|
.map-header__logo { font-size:13px;font-weight:700;color:#e2e8f0;white-space:nowrap; }
|
|
|
|
|
|
|
|
.map-header__slug { font-size:12px;color:#64748b;font-family:monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; }
|
|
|
|
|
|
|
|
.map-header__participants {
|
|
|
|
|
|
|
|
display:flex;align-items:center;gap:4px;
|
|
|
|
|
|
|
|
padding:4px 12px;border-radius:20px;
|
|
|
|
|
|
|
|
background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.12);
|
|
|
|
|
|
|
|
color:#e2e8f0;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.map-header__participants:hover { background:rgba(255,255,255,0.14); }
|
|
|
|
|
|
|
|
.map-header__right { display:flex;align-items:center;gap:4px; }
|
|
|
|
|
|
|
|
.map-header__icon-btn {
|
|
|
|
|
|
|
|
background:none;border:none;color:#94a3b8;cursor:pointer;
|
|
|
|
|
|
|
|
font-size:15px;padding:4px 6px;border-radius:4px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.map-header__icon-btn:hover { color:#e2e8f0; }
|
|
|
|
|
|
|
|
.map-header__share-toggle {
|
|
|
|
|
|
|
|
width:32px;height:32px;border-radius:50%;
|
|
|
|
|
|
|
|
background:transparent;border:1px solid rgba(255,255,255,0.15);
|
|
|
|
|
|
|
|
color:#94a3b8;cursor:pointer;font-size:14px;
|
|
|
|
|
|
|
|
display:flex;align-items:center;justify-content:center;transition:all 0.2s;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.map-header__share-toggle.active { background:#10b981;border-color:#10b981;color:#fff; }
|
|
|
|
|
|
|
|
.map-header__share-toggle:hover { border-color:#10b981;color:#10b981; }
|
|
|
|
|
|
|
|
|
|
|
|
.status-dot {
|
|
|
|
.status-dot {
|
|
|
|
width: 8px; height: 8px; border-radius: 50%; display: inline-block;
|
|
|
|
width: 8px; height: 8px; border-radius: 50%; display: inline-block;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -2448,8 +2687,21 @@ class FolkMapViewer extends HTMLElement {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.map-locate-fab:hover { border-color: #4285f4; color: #4285f4; }
|
|
|
|
.map-locate-fab:hover { border-color: #4285f4; color: #4285f4; }
|
|
|
|
|
|
|
|
|
|
|
|
/* Mobile FAB menu — hidden on desktop */
|
|
|
|
/* Floating share-location button on map */
|
|
|
|
.mobile-fab-container { display: none; }
|
|
|
|
.map-share-float {
|
|
|
|
|
|
|
|
position:absolute;bottom:20px;right:16px;z-index:6;
|
|
|
|
|
|
|
|
padding:10px 18px;border-radius:24px;
|
|
|
|
|
|
|
|
background:#fff;border:1px solid #e2e8f0;
|
|
|
|
|
|
|
|
color:#1e293b;font-weight:600;font-size:13px;cursor:pointer;
|
|
|
|
|
|
|
|
box-shadow:0 2px 10px rgba(0,0,0,0.15);transition:all 0.2s;
|
|
|
|
|
|
|
|
font-family:system-ui,-apple-system,sans-serif;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.map-share-float.active { background:#10b981;border-color:#10b981;color:#fff; }
|
|
|
|
|
|
|
|
.map-share-float.ghost { background:#8b5cf6;border-color:#8b5cf6;color:#fff; }
|
|
|
|
|
|
|
|
.map-share-float:hover { box-shadow:0 4px 16px rgba(0,0,0,0.2); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* Mobile floating buttons — hidden on desktop */
|
|
|
|
|
|
|
|
.mobile-float-btns { display: none; }
|
|
|
|
.mobile-bottom-sheet { display: none; }
|
|
|
|
.mobile-bottom-sheet { display: none; }
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
@ -2458,52 +2710,20 @@ class FolkMapViewer extends HTMLElement {
|
|
|
|
.map-sidebar { display: none; }
|
|
|
|
.map-sidebar { display: none; }
|
|
|
|
.controls { display: none; }
|
|
|
|
.controls { display: none; }
|
|
|
|
#privacy-panel { display: none !important; }
|
|
|
|
#privacy-panel { display: none !important; }
|
|
|
|
.rapp-nav { position: absolute; top: 0; left: 0; right: 0; z-index: 7; background: var(--rs-bg-surface); border-bottom: 1px solid var(--rs-border); margin: 0; padding: 6px 12px; min-height: 48px; }
|
|
|
|
.map-header { position:absolute;top:0;left:0;right:0;z-index:7;margin:0;border-radius:0;border-bottom:1px solid rgba(255,255,255,0.08); }
|
|
|
|
.map-locate-fab { bottom: 100px; }
|
|
|
|
.map-locate-fab { bottom: 100px; }
|
|
|
|
|
|
|
|
|
|
|
|
/* Mobile FAB menu */
|
|
|
|
/* Mobile floating pill buttons */
|
|
|
|
.mobile-fab-container {
|
|
|
|
.mobile-float-btns { display:flex;position:fixed;bottom:16px;left:0;right:0;z-index:8;padding:0 16px;justify-content:space-between;pointer-events:none; }
|
|
|
|
display: block; position: fixed; bottom: 24px; right: 16px; z-index: 8;
|
|
|
|
.mobile-pill-btn {
|
|
|
|
|
|
|
|
padding:10px 16px;border-radius:24px;border:none;
|
|
|
|
|
|
|
|
background:rgba(15,23,42,0.92);color:#e2e8f0;font-weight:600;
|
|
|
|
|
|
|
|
font-size:13px;cursor:pointer;pointer-events:auto;
|
|
|
|
|
|
|
|
box-shadow:0 4px 16px rgba(0,0,0,0.3);backdrop-filter:blur(4px);
|
|
|
|
|
|
|
|
font-family:system-ui,-apple-system,sans-serif;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.fab-main {
|
|
|
|
.mobile-pill-btn:active { opacity:0.8; }
|
|
|
|
width: 52px; height: 52px; border-radius: 50%;
|
|
|
|
.map-share-float { bottom:70px; }
|
|
|
|
background: #4f46e5; border: none; color: #fff; cursor: pointer;
|
|
|
|
|
|
|
|
font-size: 22px; display: flex; align-items: center; justify-content: center;
|
|
|
|
|
|
|
|
box-shadow: 0 4px 16px rgba(79,70,229,0.5); transition: transform 0.2s;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.fab-main.open { transform: rotate(45deg); }
|
|
|
|
|
|
|
|
.fab-mini-list {
|
|
|
|
|
|
|
|
position: absolute; bottom: 62px; right: 2px;
|
|
|
|
|
|
|
|
display: flex; flex-direction: column-reverse; gap: 10px;
|
|
|
|
|
|
|
|
opacity: 0; pointer-events: none; transition: opacity 0.2s;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.fab-mini-list.open { opacity: 1; pointer-events: auto; }
|
|
|
|
|
|
|
|
.fab-mini {
|
|
|
|
|
|
|
|
display: flex; align-items: center; gap: 8px; flex-direction: row-reverse;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.fab-mini-btn {
|
|
|
|
|
|
|
|
width: 40px; height: 40px; border-radius: 50%;
|
|
|
|
|
|
|
|
border: 1px solid var(--rs-border); background: var(--rs-bg-surface);
|
|
|
|
|
|
|
|
color: var(--rs-text-primary); cursor: pointer; font-size: 16px;
|
|
|
|
|
|
|
|
display: flex; align-items: center; justify-content: center;
|
|
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
|
|
|
|
|
|
|
transform: scale(0); transition: transform 0.15s;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.fab-mini-list.open .fab-mini-btn { transform: scale(1); }
|
|
|
|
|
|
|
|
.fab-mini-list.open .fab-mini:nth-child(1) .fab-mini-btn { transition-delay: 0s; }
|
|
|
|
|
|
|
|
.fab-mini-list.open .fab-mini:nth-child(2) .fab-mini-btn { transition-delay: 0.04s; }
|
|
|
|
|
|
|
|
.fab-mini-list.open .fab-mini:nth-child(3) .fab-mini-btn { transition-delay: 0.08s; }
|
|
|
|
|
|
|
|
.fab-mini-list.open .fab-mini:nth-child(4) .fab-mini-btn { transition-delay: 0.12s; }
|
|
|
|
|
|
|
|
.fab-mini-list.open .fab-mini:nth-child(5) .fab-mini-btn { transition-delay: 0.16s; }
|
|
|
|
|
|
|
|
.fab-mini-list.open .fab-mini:nth-child(6) .fab-mini-btn { transition-delay: 0.20s; }
|
|
|
|
|
|
|
|
.fab-mini-list.open .fab-mini:nth-child(7) .fab-mini-btn { transition-delay: 0.24s; }
|
|
|
|
|
|
|
|
.fab-mini-label {
|
|
|
|
|
|
|
|
font-size: 11px; background: var(--rs-bg-surface); color: var(--rs-text-secondary);
|
|
|
|
|
|
|
|
padding: 4px 8px; border-radius: 6px; white-space: nowrap;
|
|
|
|
|
|
|
|
box-shadow: 0 1px 4px rgba(0,0,0,0.15); border: 1px solid var(--rs-border);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.fab-mini-btn.sharing { border-color: #22c55e; color: #22c55e; }
|
|
|
|
|
|
|
|
.fab-mini-btn.ghost { border-color: #8b5cf6; color: #8b5cf6; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* Mobile bottom sheet */
|
|
|
|
/* Mobile bottom sheet */
|
|
|
|
.mobile-bottom-sheet {
|
|
|
|
.mobile-bottom-sheet {
|
|
|
|
@ -2611,6 +2831,37 @@ class FolkMapViewer extends HTMLElement {
|
|
|
|
startTour() { this._tour.start(); }
|
|
|
|
startTour() { this._tour.start(); }
|
|
|
|
|
|
|
|
|
|
|
|
private renderLobby(): string {
|
|
|
|
private renderLobby(): string {
|
|
|
|
|
|
|
|
const saved = JSON.parse(localStorage.getItem("rmaps_user") || "null");
|
|
|
|
|
|
|
|
const lastRoom = localStorage.getItem("rmaps_last_room") || "";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const profileSection = `
|
|
|
|
|
|
|
|
<div style="background:var(--rs-glass-bg);border:1px solid var(--rs-border);border-radius:12px;padding:16px;margin-bottom:16px;">
|
|
|
|
|
|
|
|
<div style="font-size:12px;font-weight:600;color:var(--rs-text-secondary);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:10px;">Get Started</div>
|
|
|
|
|
|
|
|
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
|
|
|
|
|
|
|
|
<div style="position:relative;">
|
|
|
|
|
|
|
|
<span id="lobby-avatar-preview" style="font-size:32px;display:block;cursor:pointer;" title="Click to change emoji">${this.userEmoji}</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div style="flex:1;">
|
|
|
|
|
|
|
|
<input id="lobby-name-input" type="text" placeholder="Your display name" value="${this.esc(saved?.name || this.userName || "")}" style="
|
|
|
|
|
|
|
|
width:100%;padding:8px 12px;border-radius:8px;border:1px solid var(--rs-border);
|
|
|
|
|
|
|
|
background:var(--rs-input-bg);color:var(--rs-text-primary);font-size:14px;outline:none;box-sizing:border-box;
|
|
|
|
|
|
|
|
">
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div id="lobby-emoji-grid" style="display:none;flex-wrap:wrap;gap:4px;margin-bottom:10px;justify-content:center;">
|
|
|
|
|
|
|
|
${EMOJIS.map(e => `<button class="lobby-emoji-pick" data-lobby-emoji="${e}" style="width:34px;height:34px;border-radius:6px;border:2px solid ${e === this.userEmoji ? "#4f46e5" : "transparent"};background:${e === this.userEmoji ? "#4f46e520" : "var(--rs-bg-surface-sunken)"};font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center;">${e}</button>`).join("")}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
${lastRoom ? `
|
|
|
|
|
|
|
|
<button id="lobby-rejoin-btn" style="
|
|
|
|
|
|
|
|
width:100%;padding:10px;border-radius:8px;border:none;
|
|
|
|
|
|
|
|
background:#4f46e5;color:#fff;font-weight:600;cursor:pointer;font-size:13px;
|
|
|
|
|
|
|
|
margin-bottom:6px;
|
|
|
|
|
|
|
|
">Rejoin 🌐 ${this.esc(lastRoom)}</button>
|
|
|
|
|
|
|
|
` : ""}
|
|
|
|
|
|
|
|
<div style="font-size:11px;color:var(--rs-text-muted);text-align:center;">Profile is saved locally for quick rejoins</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
const history = loadRoomHistory();
|
|
|
|
const history = loadRoomHistory();
|
|
|
|
const historyCards = history.length > 0 ? `
|
|
|
|
const historyCards = history.length > 0 ? `
|
|
|
|
<div class="section-label">Recent Rooms</div>
|
|
|
|
<div class="section-label">Recent Rooms</div>
|
|
|
|
@ -2640,6 +2891,8 @@ class FolkMapViewer extends HTMLElement {
|
|
|
|
<button class="rapp-nav__btn" id="btn-tour" style="background:transparent;border:1px solid var(--rs-border,#334155);color:var(--rs-text-secondary,#94a3b8);font-weight:500">Tour</button>
|
|
|
|
<button class="rapp-nav__btn" id="btn-tour" style="background:transparent;border:1px solid var(--rs-border,#334155);color:var(--rs-text-secondary,#94a3b8);font-weight:500">Tour</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
${profileSection}
|
|
|
|
|
|
|
|
|
|
|
|
${this.rooms.length > 0 ? `
|
|
|
|
${this.rooms.length > 0 ? `
|
|
|
|
<div class="section-label">Active Rooms</div>
|
|
|
|
<div class="section-label">Active Rooms</div>
|
|
|
|
${this.rooms.map((r) => `
|
|
|
|
${this.rooms.map((r) => `
|
|
|
|
@ -2661,12 +2914,24 @@ class FolkMapViewer extends HTMLElement {
|
|
|
|
|
|
|
|
|
|
|
|
private renderMap(): string {
|
|
|
|
private renderMap(): string {
|
|
|
|
return `
|
|
|
|
return `
|
|
|
|
<div class="rapp-nav">
|
|
|
|
<div class="map-header">
|
|
|
|
${this._history.canGoBack ? '<button class="rapp-nav__back" data-back="lobby">← Rooms</button>' : ''}
|
|
|
|
<div class="map-header__left">
|
|
|
|
<span class="rapp-nav__title">\u{1F5FA} ${this.esc(this.room)}</span>
|
|
|
|
${this._history.canGoBack ? '<button class="map-header__back" data-back="lobby">←</button>' : ''}
|
|
|
|
<span class="status-dot ${this.syncStatus === "connected" ? "status-connected" : "status-disconnected"}"></span>
|
|
|
|
<span class="map-header__logo">🌐 rMaps</span>
|
|
|
|
|
|
|
|
<span class="map-header__slug">/${this.esc(this.room)}</span>
|
|
|
|
|
|
|
|
<span class="status-dot ${this.syncStatus === "connected" ? "status-connected" : "status-disconnected"}" style="margin-left:6px;"></span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<button class="map-header__participants" id="header-participants-toggle" title="Show participants">
|
|
|
|
|
|
|
|
👥 <span id="header-participant-count">0</span>
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
<div class="map-header__right">
|
|
|
|
<span id="geo-perm-indicator" style="width:6px;height:6px;border-radius:50%;background:#64748b;" title="Geolocation: unknown"></span>
|
|
|
|
<span id="geo-perm-indicator" style="width:6px;height:6px;border-radius:50%;background:#64748b;" title="Geolocation: unknown"></span>
|
|
|
|
<button id="bell-toggle" style="background:none;border:none;color:var(--rs-text-muted);cursor:pointer;font-size:16px;padding:4px;" title="Notifications">\u{1F514}</button>
|
|
|
|
<button class="map-header__icon-btn" id="bell-toggle" title="Notifications">🔔</button>
|
|
|
|
|
|
|
|
<button class="map-header__icon-btn" id="share-room-btn" title="Share rMap">📤</button>
|
|
|
|
|
|
|
|
<button class="map-header__share-toggle ${this.sharingLocation ? "active" : ""}" id="header-share-toggle" title="${this.sharingLocation ? "Sharing location" : "Share location"}">
|
|
|
|
|
|
|
|
${this.sharingLocation ? "📍" : "📍"}
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="map-layout">
|
|
|
|
<div class="map-layout">
|
|
|
|
@ -2705,54 +2970,30 @@ class FolkMapViewer extends HTMLElement {
|
|
|
|
<button class="map-locate-fab" id="locate-me-fab" title="Center on my location">\u{1F3AF}</button>
|
|
|
|
<button class="map-locate-fab" id="locate-me-fab" title="Center on my location">\u{1F3AF}</button>
|
|
|
|
<button class="map-locate-fab" id="fit-all-fab" title="Show all participants" style="bottom:80px;">\u{1F465}</button>
|
|
|
|
<button class="map-locate-fab" id="fit-all-fab" title="Show all participants" style="bottom:80px;">\u{1F465}</button>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Mobile FAB menu -->
|
|
|
|
<!-- Mobile floating buttons (rmaps-online style) -->
|
|
|
|
<div class="mobile-fab-container" id="mobile-fab-container">
|
|
|
|
<div class="mobile-float-btns">
|
|
|
|
<div class="fab-mini-list" id="fab-mini-list">
|
|
|
|
<button class="mobile-pill-btn mobile-pill-left" id="mobile-friends-btn">👥 See Friends (<span id="mobile-friends-count">0</span>)</button>
|
|
|
|
<div class="fab-mini">
|
|
|
|
<button class="mobile-pill-btn mobile-pill-right" id="mobile-qr-btn">▩ QR</button>
|
|
|
|
<button class="fab-mini-btn ${this.sharingLocation ? "sharing" : ""} ${this.privacySettings.ghostMode ? "ghost" : ""}" id="fab-share-loc" title="Share Location">\u{1F4CD}</button>
|
|
|
|
|
|
|
|
<span class="fab-mini-label">${this.privacySettings.ghostMode ? "Ghost" : this.sharingLocation ? "Stop" : "Share"}</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="fab-mini">
|
|
|
|
|
|
|
|
<button class="fab-mini-btn" id="fab-privacy" title="Privacy">\u{1F6E1}</button>
|
|
|
|
|
|
|
|
<span class="fab-mini-label">Privacy</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="fab-mini">
|
|
|
|
|
|
|
|
<button class="fab-mini-btn" id="fab-drop-pin" title="Drop Pin">\u{1F4CC}</button>
|
|
|
|
|
|
|
|
<span class="fab-mini-label">Drop Pin</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="fab-mini">
|
|
|
|
|
|
|
|
<button class="fab-mini-btn" id="fab-share-map" title="Share rMap">\u{1F4E4}</button>
|
|
|
|
|
|
|
|
<span class="fab-mini-label">Share rMap</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="fab-mini">
|
|
|
|
|
|
|
|
<button class="fab-mini-btn" id="fab-emoji" title="Emoji">${this.userEmoji}</button>
|
|
|
|
|
|
|
|
<span class="fab-mini-label">Emoji</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="fab-mini">
|
|
|
|
|
|
|
|
<button class="fab-mini-btn" id="fab-chat" title="Chat" style="position:relative;">\u{1F4AC}<span id="fab-chat-badge" style="display:${this.unreadCount > 0 ? "flex" : "none"};position:absolute;top:-4px;right:-4px;min-width:14px;height:14px;border-radius:7px;background:#ef4444;color:#fff;font-size:8px;align-items:center;justify-content:center;padding:0 3px;">${this.unreadCount || ""}</span></button>
|
|
|
|
|
|
|
|
<span class="fab-mini-label">Chat</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="fab-mini">
|
|
|
|
|
|
|
|
<button class="fab-mini-btn" id="fab-indoor" title="Indoor">${this.mapMode === "indoor" ? "\u{1F30D}" : "\u{1F3E2}"}</button>
|
|
|
|
|
|
|
|
<span class="fab-mini-label">${this.mapMode === "indoor" ? "Outdoor" : "Indoor"}</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<button class="fab-main" id="fab-main" title="Menu">\u2699</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Floating share-location button on map -->
|
|
|
|
|
|
|
|
<button class="map-share-float ${this.sharingLocation ? "active" : ""} ${this.privacySettings.precision === "hidden" ? "ghost" : ""}" id="map-share-float">
|
|
|
|
|
|
|
|
${this.privacySettings.precision === "hidden" ? "👻 Hidden" : this.sharingLocation ? "📍 Sharing" : "📍 Share Location"}
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Mobile bottom sheet (participants) -->
|
|
|
|
<!-- Mobile bottom sheet (participants) -->
|
|
|
|
<div class="mobile-bottom-sheet" id="mobile-bottom-sheet">
|
|
|
|
<div class="mobile-bottom-sheet" id="mobile-bottom-sheet">
|
|
|
|
<div class="sheet-handle" id="sheet-handle">
|
|
|
|
<div class="sheet-handle" id="sheet-handle">
|
|
|
|
<div class="sheet-handle-bar"></div>
|
|
|
|
<div class="sheet-handle-bar"></div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="sheet-header">
|
|
|
|
<div class="sheet-header">
|
|
|
|
<span>Participants</span>
|
|
|
|
<span>Friends (<span id="sheet-participant-count">0</span>)</span>
|
|
|
|
<span style="font-size:10px;font-weight:400;color:var(--rs-text-muted);text-transform:none;letter-spacing:0;">tap to expand</span>
|
|
|
|
<button id="sheet-close-btn" style="background:none;border:none;color:var(--rs-text-muted);cursor:pointer;font-size:14px;padding:2px 6px;">✕</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="sheet-content" id="participant-list-mobile"></div>
|
|
|
|
<div class="sheet-content" id="participant-list-mobile"></div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Mobile privacy popup (hidden by default) -->
|
|
|
|
<!-- Mobile privacy popup (hidden by default, triggered from bottom sheet) -->
|
|
|
|
<div class="mobile-privacy-popup" id="mobile-privacy-popup" style="display:none;"></div>
|
|
|
|
<div class="mobile-privacy-popup" id="mobile-privacy-popup" style="display:none;"></div>
|
|
|
|
`;
|
|
|
|
`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -2761,6 +3002,60 @@ class FolkMapViewer extends HTMLElement {
|
|
|
|
this.shadow.getElementById("btn-tour")?.addEventListener("click", () => this.startTour());
|
|
|
|
this.shadow.getElementById("btn-tour")?.addEventListener("click", () => this.startTour());
|
|
|
|
this.shadow.getElementById("create-room")?.addEventListener("click", () => this.createRoom());
|
|
|
|
this.shadow.getElementById("create-room")?.addEventListener("click", () => this.createRoom());
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Lobby profile section
|
|
|
|
|
|
|
|
const lobbyNameInput = this.shadow.getElementById("lobby-name-input") as HTMLInputElement;
|
|
|
|
|
|
|
|
if (lobbyNameInput) {
|
|
|
|
|
|
|
|
lobbyNameInput.addEventListener("change", () => {
|
|
|
|
|
|
|
|
const name = lobbyNameInput.value.trim();
|
|
|
|
|
|
|
|
if (name) {
|
|
|
|
|
|
|
|
this.userName = name;
|
|
|
|
|
|
|
|
localStorage.setItem("rmaps_user", JSON.stringify({
|
|
|
|
|
|
|
|
id: this.participantId, name: this.userName,
|
|
|
|
|
|
|
|
emoji: this.userEmoji, color: this.userColor,
|
|
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const lobbyAvatar = this.shadow.getElementById("lobby-avatar-preview");
|
|
|
|
|
|
|
|
const lobbyEmojiGrid = this.shadow.getElementById("lobby-emoji-grid");
|
|
|
|
|
|
|
|
if (lobbyAvatar && lobbyEmojiGrid) {
|
|
|
|
|
|
|
|
lobbyAvatar.addEventListener("click", () => {
|
|
|
|
|
|
|
|
lobbyEmojiGrid.style.display = lobbyEmojiGrid.style.display === "none" ? "flex" : "none";
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
this.shadow.querySelectorAll("[data-lobby-emoji]").forEach(btn => {
|
|
|
|
|
|
|
|
btn.addEventListener("click", () => {
|
|
|
|
|
|
|
|
const emoji = (btn as HTMLElement).dataset.lobbyEmoji!;
|
|
|
|
|
|
|
|
this.userEmoji = emoji;
|
|
|
|
|
|
|
|
if (lobbyAvatar) lobbyAvatar.textContent = emoji;
|
|
|
|
|
|
|
|
this.shadow.querySelectorAll("[data-lobby-emoji]").forEach(b => {
|
|
|
|
|
|
|
|
const sel = (b as HTMLElement).dataset.lobbyEmoji === emoji;
|
|
|
|
|
|
|
|
(b as HTMLElement).style.borderColor = sel ? "#4f46e5" : "transparent";
|
|
|
|
|
|
|
|
(b as HTMLElement).style.background = sel ? "#4f46e520" : "var(--rs-bg-surface-sunken)";
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
localStorage.setItem("rmaps_user", JSON.stringify({
|
|
|
|
|
|
|
|
id: this.participantId, name: this.userName,
|
|
|
|
|
|
|
|
emoji: this.userEmoji, color: this.userColor,
|
|
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
this.shadow.getElementById("lobby-rejoin-btn")?.addEventListener("click", () => {
|
|
|
|
|
|
|
|
const lastRoom = localStorage.getItem("rmaps_last_room");
|
|
|
|
|
|
|
|
if (lastRoom) {
|
|
|
|
|
|
|
|
// Save name from input if provided
|
|
|
|
|
|
|
|
if (lobbyNameInput?.value.trim()) {
|
|
|
|
|
|
|
|
this.userName = lobbyNameInput.value.trim();
|
|
|
|
|
|
|
|
localStorage.setItem("rmaps_user", JSON.stringify({
|
|
|
|
|
|
|
|
id: this.participantId, name: this.userName,
|
|
|
|
|
|
|
|
emoji: this.userEmoji, color: this.userColor,
|
|
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
this._history.push("lobby");
|
|
|
|
|
|
|
|
this._history.push("map", { room: lastRoom });
|
|
|
|
|
|
|
|
this.joinRoom(lastRoom);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.shadow.querySelectorAll("[data-room]").forEach((el) => {
|
|
|
|
this.shadow.querySelectorAll("[data-room]").forEach((el) => {
|
|
|
|
el.addEventListener("click", () => {
|
|
|
|
el.addEventListener("click", () => {
|
|
|
|
const room = (el as HTMLElement).dataset.room!;
|
|
|
|
const room = (el as HTMLElement).dataset.room!;
|
|
|
|
@ -2780,6 +3075,22 @@ class FolkMapViewer extends HTMLElement {
|
|
|
|
this.toggleLocationSharing();
|
|
|
|
this.toggleLocationSharing();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Header share toggle
|
|
|
|
|
|
|
|
this.shadow.getElementById("header-share-toggle")?.addEventListener("click", () => {
|
|
|
|
|
|
|
|
this.toggleLocationSharing();
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Header participants toggle
|
|
|
|
|
|
|
|
this.shadow.getElementById("header-participants-toggle")?.addEventListener("click", () => {
|
|
|
|
|
|
|
|
// Desktop: toggle sidebar visibility, Mobile: toggle bottom sheet
|
|
|
|
|
|
|
|
const sidebar = this.shadow.querySelector(".map-sidebar") as HTMLElement;
|
|
|
|
|
|
|
|
if (sidebar && window.innerWidth > 768) {
|
|
|
|
|
|
|
|
sidebar.style.display = sidebar.style.display === "none" ? "" : "none";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const sheet = this.shadow.getElementById("mobile-bottom-sheet");
|
|
|
|
|
|
|
|
if (sheet) sheet.classList.toggle("expanded");
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.shadow.getElementById("drop-waypoint")?.addEventListener("click", () => {
|
|
|
|
this.shadow.getElementById("drop-waypoint")?.addEventListener("click", () => {
|
|
|
|
this.dropWaypoint();
|
|
|
|
this.dropWaypoint();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
@ -2865,75 +3176,26 @@ class FolkMapViewer extends HTMLElement {
|
|
|
|
this.fitToParticipants();
|
|
|
|
this.fitToParticipants();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Mobile FAB menu
|
|
|
|
// Mobile floating buttons
|
|
|
|
this.shadow.getElementById("fab-main")?.addEventListener("click", () => {
|
|
|
|
this.shadow.getElementById("mobile-friends-btn")?.addEventListener("click", () => {
|
|
|
|
const main = this.shadow.getElementById("fab-main");
|
|
|
|
const sheet = this.shadow.getElementById("mobile-bottom-sheet");
|
|
|
|
const list = this.shadow.getElementById("fab-mini-list");
|
|
|
|
if (sheet) sheet.classList.toggle("expanded");
|
|
|
|
if (main && list) {
|
|
|
|
|
|
|
|
const isOpen = list.classList.contains("open");
|
|
|
|
|
|
|
|
list.classList.toggle("open");
|
|
|
|
|
|
|
|
main.classList.toggle("open");
|
|
|
|
|
|
|
|
// Close mobile privacy popup when closing FAB
|
|
|
|
|
|
|
|
if (isOpen) {
|
|
|
|
|
|
|
|
const popup = this.shadow.getElementById("mobile-privacy-popup");
|
|
|
|
|
|
|
|
if (popup) popup.style.display = "none";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.shadow.getElementById("fab-share-loc")?.addEventListener("click", () => {
|
|
|
|
this.shadow.getElementById("mobile-qr-btn")?.addEventListener("click", () => {
|
|
|
|
this.toggleLocationSharing();
|
|
|
|
|
|
|
|
this.updateMobileFab();
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.shadow.getElementById("fab-privacy")?.addEventListener("click", () => {
|
|
|
|
|
|
|
|
const popup = this.shadow.getElementById("mobile-privacy-popup");
|
|
|
|
|
|
|
|
if (popup) {
|
|
|
|
|
|
|
|
const isVisible = popup.style.display !== "none";
|
|
|
|
|
|
|
|
popup.style.display = isVisible ? "none" : "block";
|
|
|
|
|
|
|
|
if (!isVisible) this.renderPrivacyPanel(popup);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.shadow.getElementById("fab-drop-pin")?.addEventListener("click", () => {
|
|
|
|
|
|
|
|
this.closeMobileFab();
|
|
|
|
|
|
|
|
this.dropWaypoint();
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.shadow.getElementById("fab-share-map")?.addEventListener("click", () => {
|
|
|
|
|
|
|
|
this.closeMobileFab();
|
|
|
|
|
|
|
|
this.showShareModal = true;
|
|
|
|
this.showShareModal = true;
|
|
|
|
this.renderShareModal();
|
|
|
|
this.renderShareModal();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.shadow.getElementById("fab-emoji")?.addEventListener("click", () => {
|
|
|
|
// Floating share-location button on map
|
|
|
|
this.showEmojiPicker = !this.showEmojiPicker;
|
|
|
|
this.shadow.getElementById("map-share-float")?.addEventListener("click", () => {
|
|
|
|
this.updateEmojiButton();
|
|
|
|
this.toggleLocationSharing();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.shadow.getElementById("fab-chat")?.addEventListener("click", () => {
|
|
|
|
// Sheet close button
|
|
|
|
this.closeMobileFab();
|
|
|
|
this.shadow.getElementById("sheet-close-btn")?.addEventListener("click", () => {
|
|
|
|
// Toggle mobile bottom sheet to chat view
|
|
|
|
const s = this.shadow.getElementById("mobile-bottom-sheet");
|
|
|
|
const sheet = this.shadow.getElementById("mobile-bottom-sheet");
|
|
|
|
if (s) s.classList.remove("expanded");
|
|
|
|
if (sheet) {
|
|
|
|
|
|
|
|
sheet.classList.add("expanded");
|
|
|
|
|
|
|
|
const content = this.shadow.getElementById("participant-list-mobile");
|
|
|
|
|
|
|
|
if (content && !content.querySelector("map-chat-panel")) {
|
|
|
|
|
|
|
|
this.mountChatPanel(content);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
this.unreadCount = 0;
|
|
|
|
|
|
|
|
this.updateChatBadge();
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.shadow.getElementById("fab-indoor")?.addEventListener("click", () => {
|
|
|
|
|
|
|
|
this.closeMobileFab();
|
|
|
|
|
|
|
|
if (this.mapMode === "outdoor") {
|
|
|
|
|
|
|
|
const event = prompt("c3nav event code (e.g. 39c3):", "39c3");
|
|
|
|
|
|
|
|
if (event?.trim()) this.switchToIndoor(event.trim());
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
this.switchToOutdoor();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Mobile bottom sheet
|
|
|
|
// Mobile bottom sheet
|
|
|
|
@ -2974,21 +3236,8 @@ class FolkMapViewer extends HTMLElement {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private closeMobileFab() {
|
|
|
|
|
|
|
|
const main = this.shadow.getElementById("fab-main");
|
|
|
|
|
|
|
|
const list = this.shadow.getElementById("fab-mini-list");
|
|
|
|
|
|
|
|
if (main) main.classList.remove("open");
|
|
|
|
|
|
|
|
if (list) list.classList.remove("open");
|
|
|
|
|
|
|
|
const popup = this.shadow.getElementById("mobile-privacy-popup");
|
|
|
|
|
|
|
|
if (popup) popup.style.display = "none";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private updateMobileFab() {
|
|
|
|
private updateMobileFab() {
|
|
|
|
const btn = this.shadow.getElementById("fab-share-loc");
|
|
|
|
// No-op — legacy FAB removed; share state updated via updateShareButton()
|
|
|
|
if (!btn) return;
|
|
|
|
|
|
|
|
btn.className = `fab-mini-btn ${this.sharingLocation ? "sharing" : ""} ${this.privacySettings.ghostMode ? "ghost" : ""}`;
|
|
|
|
|
|
|
|
const label = btn.parentElement?.querySelector(".fab-mini-label");
|
|
|
|
|
|
|
|
if (label) label.textContent = this.privacySettings.ghostMode ? "Ghost" : this.sharingLocation ? "Stop" : "Share";
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private goBack() {
|
|
|
|
private goBack() {
|
|
|
|
|