Add admin image replies and attachment removal
This commit is contained in:
+115
-8
@@ -5,7 +5,8 @@ const state = {
|
||||
activeId: null,
|
||||
active: null,
|
||||
events: null,
|
||||
statusFilter: "active"
|
||||
statusFilter: "active",
|
||||
replyFiles: []
|
||||
};
|
||||
|
||||
const $ = (selector) => document.querySelector(selector);
|
||||
@@ -66,19 +67,71 @@ $("#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: { message: form.get("message") }
|
||||
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();
|
||||
@@ -106,6 +159,11 @@ $("#conversationList").addEventListener("change", async (event) => {
|
||||
});
|
||||
|
||||
$("#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);
|
||||
@@ -216,7 +274,7 @@ function renderActive() {
|
||||
$("#conversationHeader").innerHTML = `
|
||||
<div>
|
||||
<h2>${escapeHtml(conversation.visitor.name || conversation.visitor.email || "Navstevnik")}</h2>
|
||||
<p>${flag(conversation.countryCode)} ${escapeHtml(conversation.currentUrl || "")}</p>
|
||||
<p>${visitorCountry(conversation)} ${escapeHtml(conversation.currentUrl || "")}</p>
|
||||
<p>${escapeHtml(conversation.device.userAgent || "")}</p>
|
||||
</div>
|
||||
`;
|
||||
@@ -247,7 +305,8 @@ function statusOptions(currentStatus) {
|
||||
}
|
||||
|
||||
function visitorCountry(item) {
|
||||
return item.countryCode ? `${flag(item.countryCode)} ${escapeHtml(item.countryCode)}` : "zeme ?";
|
||||
const countryCode = item.countryCode || countryFromLanguage(item.language);
|
||||
return countryCode ? `${flag(countryCode)} ${escapeHtml(countryCode)}` : "zeme ?";
|
||||
}
|
||||
|
||||
function formatDuration(seconds) {
|
||||
@@ -299,14 +358,62 @@ function renderAttachments(attachments) {
|
||||
return `<a href="${escapeHtml(attachment.url)}" target="_blank" rel="noreferrer">${escapeHtml(attachment.name)}</a>`;
|
||||
}
|
||||
return `
|
||||
<button class="photo-preview" type="button" data-photo-url="${escapeHtml(attachment.url)}" data-photo-name="${escapeHtml(attachment.name)}">
|
||||
<img src="${escapeHtml(attachment.url)}" alt="${escapeHtml(attachment.name)}">
|
||||
<span>${escapeHtml(attachment.name)}</span>
|
||||
</button>
|
||||
<figure class="photo-preview">
|
||||
<button class="photo-open" type="button" data-photo-url="${escapeHtml(attachment.url)}" data-photo-name="${escapeHtml(attachment.name)}">
|
||||
<img src="${escapeHtml(attachment.url)}" alt="${escapeHtml(attachment.name)}">
|
||||
</button>
|
||||
<figcaption>
|
||||
<span>${escapeHtml(attachment.name)}</span>
|
||||
<button class="photo-delete" type="button" data-delete-attachment="${attachment.id}" aria-label="Smazat fotku" title="Smazat fotku">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 3h6l1 2h4v2H4V5h4l1-2Zm-2 6h10l-.7 11H7.7L7 9Zm3 2 .2 7h1.6l-.2-7H10Zm4 0-.2 7h1.6l.2-7H14Z"></path></svg>
|
||||
</button>
|
||||
</figcaption>
|
||||
</figure>
|
||||
`;
|
||||
}).join("")}</div>`;
|
||||
}
|
||||
|
||||
async function deleteAttachment(id) {
|
||||
if (!confirm("Smazat tuhle fotku?")) return;
|
||||
await api(`/api/admin/attachments/${id}`, { method: "DELETE" });
|
||||
if (state.activeId) await openConversation(state.activeId);
|
||||
await loadConversations();
|
||||
}
|
||||
|
||||
function renderReplyAttachments() {
|
||||
$("#replyAttachments").innerHTML = state.replyFiles.map((file, index) => `
|
||||
<span class="reply-chip">
|
||||
<span>${escapeHtml(file.name)}</span>
|
||||
<button type="button" data-remove-reply="${index}" aria-label="Odebrat fotku">×</button>
|
||||
</span>
|
||||
`).join("");
|
||||
}
|
||||
|
||||
function mergeImageFiles(currentFiles, incomingFiles) {
|
||||
const allowedTypes = new Set(["image/jpeg", "image/png", "image/webp", "image/gif", "image/avif"]);
|
||||
const nextFiles = [...currentFiles];
|
||||
for (const file of incomingFiles) {
|
||||
if (!allowedTypes.has(file.type) || file.size <= 0 || file.size > 3_000_000) continue;
|
||||
const exists = nextFiles.some((item) => item.name === file.name && item.size === file.size && item.lastModified === file.lastModified);
|
||||
if (!exists) nextFiles.push(file);
|
||||
if (nextFiles.length === 3) break;
|
||||
}
|
||||
return nextFiles;
|
||||
}
|
||||
|
||||
function fileToPayload(file) {
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve({ name: file.name, type: file.type, size: file.size, dataBase64: reader.result });
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
function countryFromLanguage(language) {
|
||||
const match = String(language || "").match(/-([A-Za-z]{2})\b/);
|
||||
return match ? match[1].toUpperCase() : null;
|
||||
}
|
||||
|
||||
function openPhotoPreview(url, name) {
|
||||
let modal = $("#photoModal");
|
||||
if (!modal) {
|
||||
|
||||
Reference in New Issue
Block a user