${escapeHtml(message.body)}
${renderAttachments(message.attachments)}const state = { site: null, snippet: "", conversations: [], activeId: null, active: null, events: null }; const $ = (selector) => document.querySelector(selector); boot(); async function boot() { const me = await api("/api/admin/me").catch(() => null); if (me?.user) showApp(); else showLogin(); } $("#loginForm").addEventListener("submit", async (event) => { event.preventDefault(); const form = new FormData(event.currentTarget); const result = await api("/api/admin/login", { method: "POST", body: { username: form.get("username"), password: form.get("password") } }).catch(() => null); if (!result?.ok) { $("#loginError").textContent = "Prihlaseni se nepodarilo."; return; } showApp(); }); $("#logoutButton").addEventListener("click", async () => { await api("/api/admin/logout", { method: "POST" }); location.reload(); }); $("#settingsButton").addEventListener("click", () => $("#settingsPanel").classList.remove("hidden")); $("#closeSettingsButton").addEventListener("click", () => $("#settingsPanel").classList.add("hidden")); $("#onlineToggle").addEventListener("change", async () => { await saveSite({ isOnline: $("#onlineToggle").checked }); }); $("#settingsForm").addEventListener("submit", async (event) => { event.preventDefault(); const form = new FormData(event.currentTarget); const settings = structuredClone(state.site.settings); settings.title.cs = form.get("titleCs"); settings.intro.cs = form.get("introCs"); settings.title.en = form.get("titleEn"); settings.intro.en = form.get("introEn"); settings.colors.brand = form.get("brand"); settings.desktop.side = form.get("desktopSide"); settings.desktop.bottomPx = Number(form.get("desktopBottom")); settings.desktop.sideOffsetPx = Number(form.get("desktopOffset")); settings.mobile.side = form.get("mobileSide"); settings.mobile.bottomPx = Number(form.get("mobileBottom")); settings.mobile.sideOffsetPx = Number(form.get("mobileOffset")); await saveSite({ settings, isOnline: state.site.isOnline }); }); $("#replyForm").addEventListener("submit", async (event) => { event.preventDefault(); if (!state.activeId) return; const form = new FormData(event.currentTarget); await api(`/api/admin/conversations/${state.activeId}/messages`, { method: "POST", body: { message: form.get("message") } }); event.currentTarget.reset(); await openConversation(state.activeId); }); $("#replyForm textarea").addEventListener("input", debounce(() => { if (!state.activeId) return; api(`/api/admin/conversations/${state.activeId}/typing`, { method: "POST" }).catch(() => {}); }, 600)); $("#statusSelect").addEventListener("change", async () => { if (!state.activeId) return; await api(`/api/admin/conversations/${state.activeId}/status`, { method: "PATCH", body: { status: $("#statusSelect").value } }); await loadConversations(); }); $("#messages").addEventListener("click", (event) => { const preview = event.target.closest("[data-photo-url]"); if (!preview) return; openPhotoPreview(preview.dataset.photoUrl, preview.dataset.photoName); }); async function showApp() { $("#loginView").classList.add("hidden"); $("#appView").classList.remove("hidden"); await loadBootstrap(); await loadConversations(); connectEvents(); } function showLogin() { $("#loginView").classList.remove("hidden"); $("#appView").classList.add("hidden"); } async function loadBootstrap() { const data = await api("/api/admin/bootstrap"); state.site = data.site; state.snippet = data.snippet; renderSite(); } async function loadConversations() { const data = await api("/api/admin/conversations"); state.conversations = data.conversations; renderConversations(); } async function openConversation(id) { state.activeId = id; const data = await api(`/api/admin/conversations/${id}`); state.active = data; renderConversations(); renderActive(); } function connectEvents() { if (state.events) state.events.close(); state.events = new EventSource("/api/admin/events"); state.events.addEventListener("message", async (event) => { const data = JSON.parse(event.data); if (data.type === "typing" && data.actor === "visitor" && data.conversationId !== state.activeId) return; if (data.type === "typing" && data.actor === "visitor") return flashTyping(); if (data.type === "site:update") { state.site = data.site; renderSite(); return; } await loadConversations(); if (state.activeId) await openConversation(state.activeId); if (data.type === "conversation:new" || data.type === "message:new") notify(); }); } function renderSite() { $("#onlineToggle").checked = state.site.isOnline; $("#snippet").textContent = state.snippet; const s = state.site.settings; const form = $("#settingsForm"); form.titleCs.value = s.title.cs || ""; form.introCs.value = s.intro.cs || ""; form.titleEn.value = s.title.en || ""; form.introEn.value = s.intro.en || ""; form.brand.value = s.colors.brand || "#0f8f6f"; form.desktopSide.value = s.desktop.side || "right"; form.desktopBottom.value = s.desktop.bottomPx || 22; form.desktopOffset.value = s.desktop.sideOffsetPx || 22; form.mobileSide.value = s.mobile.side || "right"; form.mobileBottom.value = s.mobile.bottomPx || 14; form.mobileOffset.value = s.mobile.sideOffsetPx || 12; } function renderConversations() { $("#conversationList").innerHTML = state.conversations.map((item) => ` `).join(""); for (const button of document.querySelectorAll(".conversation-item")) { button.addEventListener("click", () => openConversation(button.dataset.id)); } } function renderActive() { const { conversation, messages } = state.active; const statusSelect = $("#statusSelect"); $("#conversationHeader").innerHTML = `
${flag(conversation.countryCode)} ${escapeHtml(conversation.currentUrl || "")}
${escapeHtml(conversation.device.userAgent || "")}