const state = {
site: null,
snippet: "",
conversations: [],
activeId: null,
active: null,
events: null,
statusFilter: "active",
replyFiles: []
};
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);
const payload = {
message: form.get("message") || "",
attachments: await Promise.all(state.replyFiles.slice(0, 3).map(fileToPayload))
};
if (!payload.message.trim() && !payload.attachments.length) return;
await api(`/api/admin/conversations/${state.activeId}/messages`, {
method: "POST",
body: payload
});
event.currentTarget.reset();
state.replyFiles = [];
renderReplyAttachments();
await openConversation(state.activeId);
});
$("#replyForm textarea").addEventListener("keydown", (event) => {
if (event.key !== "Enter" || event.shiftKey) return;
event.preventDefault();
$("#replyForm").requestSubmit();
});
$("#replyForm textarea").addEventListener("input", debounce(() => {
if (!state.activeId) return;
api(`/api/admin/conversations/${state.activeId}/typing`, { method: "POST" }).catch(() => {});
}, 600));
$("#replyForm input[name='attachments']").addEventListener("change", (event) => {
state.replyFiles = mergeImageFiles(state.replyFiles, [...event.target.files]);
event.target.value = "";
renderReplyAttachments();
});
$(".admin-dropzone").addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
$("#replyForm input[name='attachments']").click();
}
});
for (const eventName of ["dragenter", "dragover"]) {
$(".admin-dropzone").addEventListener(eventName, (event) => {
event.preventDefault();
$(".admin-dropzone").classList.add("dragging");
});
}
for (const eventName of ["dragleave", "drop"]) {
$(".admin-dropzone").addEventListener(eventName, (event) => {
event.preventDefault();
$(".admin-dropzone").classList.remove("dragging");
});
}
$(".admin-dropzone").addEventListener("drop", (event) => {
state.replyFiles = mergeImageFiles(state.replyFiles, [...event.dataTransfer.files]);
renderReplyAttachments();
});
$("#replyAttachments").addEventListener("click", (event) => {
const removeButton = event.target.closest("[data-remove-reply]");
if (!removeButton) return;
state.replyFiles.splice(Number(removeButton.dataset.removeReply), 1);
renderReplyAttachments();
});
$("#conversationFilter").addEventListener("change", () => {
state.statusFilter = $("#conversationFilter").value;
renderConversations();
});
$("#conversationList").addEventListener("click", async (event) => {
const photoButton = event.target.closest("[data-photo-url]");
const openButton = event.target.closest("[data-open]");
const deleteButton = event.target.closest("[data-delete]");
if (photoButton) {
openPhotoPreview(photoButton.dataset.photoUrl, photoButton.dataset.photoName);
return;
}
if (deleteButton) {
await deleteConversation(deleteButton.dataset.delete);
return;
}
if (openButton) await openConversation(openButton.dataset.open);
});
$("#conversationList").addEventListener("click", async (event) => {
const statusButton = event.target.closest("[data-status-button]");
if (!statusButton) return;
await updateConversationStatus(statusButton.dataset.statusButton, statusButton.dataset.status);
});
$("#messages").addEventListener("click", (event) => {
const deleteButton = event.target.closest("[data-delete-attachment]");
if (deleteButton) {
deleteAttachment(deleteButton.dataset.deleteAttachment);
return;
}
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 (data.type === "conversation:delete" && data.conversationId === state.activeId) {
clearActiveConversation();
return;
}
if (state.activeId) await openConversation(state.activeId).catch(() => clearActiveConversation());
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() {
const conversations = filteredConversations();
$("#conversationList").innerHTML = conversations.length ? conversations.map((item) => `
Zadny chat pro vybrany filtr.
`; } function renderActive() { const { conversation, messages } = state.active; $("#conversationHeader").innerHTML = `${visitorCountry(conversation)} ${escapeHtml(conversation.currentUrl || "")}
${escapeHtml(conversation.device.userAgent || "")}
Nova konverzace se tady zvyrazni a pusti zvuk.