forked from retoor/devplacepy
chore: add docker infrastructure, gists feature, attachment system, and admin CLI tools
- Add .dockerignore, Dockerfile, and docker compose targets to Makefile for containerized deployment - Implement gists router with CRUD operations, database schema, and polymorphic comment/vote reuse - Create attachment upload system with thumbnail generation, MIME detection, and storage path management - Add attachments_prune CLI command to clean orphaned attachment records and files - Introduce rate limiting middleware with 60 requests per minute window - Add custom 404 and 500 error handlers with SEO-optimized template responses - Extend database initialization with gists and attachments indexes plus upload site settings defaults - Update load_comments to include attachment mapping for comment resources - Register gists and uploads routers in main application and update AGENTS.md documentation
This commit is contained in:
@@ -14,6 +14,15 @@ class Application {
|
||||
this.initCommentReply();
|
||||
this.initEmojiPickers();
|
||||
this.initMentionInputs();
|
||||
this.initConfirmations();
|
||||
this.initImageFallbacks();
|
||||
this.initClipboardCopy();
|
||||
this.initAutoSubmitSelects();
|
||||
this.initTogglers();
|
||||
this.initStopPropagation();
|
||||
this.initMessageThread();
|
||||
this.initPlatformTags();
|
||||
this.initAttachmentUploaders();
|
||||
}
|
||||
|
||||
loadCSS(href) {
|
||||
@@ -238,9 +247,9 @@ class Application {
|
||||
|
||||
initContentRenderer() {
|
||||
document.querySelectorAll("[data-render]").forEach((el) => {
|
||||
contentRenderer.applyTo(el);
|
||||
window.contentRenderer.applyTo(el);
|
||||
});
|
||||
contentRenderer.highlightAll();
|
||||
window.contentRenderer.highlightAll();
|
||||
}
|
||||
|
||||
initCommentReply() {
|
||||
@@ -271,11 +280,145 @@ class Application {
|
||||
}
|
||||
|
||||
initMentionInputs() {
|
||||
if (typeof MentionInput === "undefined") return;
|
||||
if (!window.MentionInput) return;
|
||||
document.querySelectorAll("[data-mention]").forEach((el) => {
|
||||
if (el.dataset.mentionInitialized) return;
|
||||
el.dataset.mentionInitialized = "true";
|
||||
new MentionInput(el);
|
||||
new window.MentionInput(el);
|
||||
});
|
||||
}
|
||||
|
||||
initConfirmations() {
|
||||
document.querySelectorAll("[data-confirm]").forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
if (!confirm(btn.dataset.confirm)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initImageFallbacks() {
|
||||
document.querySelectorAll(".image-fallback").forEach((img) => {
|
||||
img.addEventListener("error", () => {
|
||||
const container = img.closest(".news-card-image, .news-detail-image") || img.parentElement;
|
||||
if (container) container.style.display = "none";
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initClipboardCopy() {
|
||||
document.querySelectorAll("[data-copy]").forEach((btn) => {
|
||||
btn.addEventListener("click", async () => {
|
||||
const source = document.getElementById(btn.dataset.copy);
|
||||
if (!source) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(source.textContent);
|
||||
const original = btn.textContent;
|
||||
btn.textContent = "Copied!";
|
||||
setTimeout(() => { btn.textContent = original; }, 2000);
|
||||
} catch {
|
||||
// silently fail
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initAutoSubmitSelects() {
|
||||
document.querySelectorAll("[data-auto-submit]").forEach((select) => {
|
||||
select.addEventListener("change", () => {
|
||||
const form = select.closest("form");
|
||||
if (form) form.submit();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initTogglers() {
|
||||
document.querySelectorAll("[data-toggle]").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const target = document.getElementById(btn.dataset.toggle);
|
||||
if (target) target.classList.toggle("hidden");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initStopPropagation() {
|
||||
document.querySelectorAll("[data-stop-propagation]").forEach((el) => {
|
||||
el.addEventListener("click", (e) => e.stopPropagation());
|
||||
});
|
||||
}
|
||||
|
||||
initMessageThread() {
|
||||
const thread = document.querySelector(".messages-thread");
|
||||
if (thread) thread.scrollTop = thread.scrollHeight;
|
||||
}
|
||||
|
||||
initPlatformTags() {
|
||||
const platformsInput = document.getElementById("platforms-input");
|
||||
const hiddenInput = document.getElementById("platforms");
|
||||
const tagsContainer = document.getElementById("platforms-tags");
|
||||
if (!platformsInput || !hiddenInput || !tagsContainer) return;
|
||||
|
||||
const addPlatform = (val) => {
|
||||
val = val.trim();
|
||||
if (!val) return;
|
||||
const tag = document.createElement("span");
|
||||
tag.className = "platform-tag";
|
||||
tag.textContent = val;
|
||||
const remove = document.createElement("button");
|
||||
remove.type = "button";
|
||||
remove.textContent = "x";
|
||||
remove.style.marginLeft = "4px";
|
||||
remove.style.fontSize = "0.75rem";
|
||||
remove.style.padding = "0";
|
||||
remove.style.background = "none";
|
||||
remove.style.border = "none";
|
||||
remove.style.color = "inherit";
|
||||
remove.style.cursor = "pointer";
|
||||
remove.addEventListener("click", () => {
|
||||
tag.remove();
|
||||
updatePlatforms();
|
||||
});
|
||||
tag.appendChild(remove);
|
||||
tagsContainer.appendChild(tag);
|
||||
platformsInput.value = "";
|
||||
updatePlatforms();
|
||||
};
|
||||
|
||||
const updatePlatforms = () => {
|
||||
const values = [];
|
||||
tagsContainer.querySelectorAll(".platform-tag").forEach((t) => {
|
||||
values.push(t.textContent.replace("x", "").trim());
|
||||
});
|
||||
hiddenInput.value = values.join(",");
|
||||
};
|
||||
|
||||
platformsInput.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
addPlatform(platformsInput.value);
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll(".platform-preset").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
addPlatform(btn.dataset.platform);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initAttachmentManagers() {
|
||||
if (!window.AttachmentUploader) return;
|
||||
document.querySelectorAll(".attachment-upload-container").forEach((container) => {
|
||||
if (container.dataset.attachmentInitialized) return;
|
||||
container.dataset.attachmentInitialized = "true";
|
||||
const form = container.closest("form");
|
||||
if (form) {
|
||||
form.dataset.maxSize = container.dataset.maxSize;
|
||||
form.dataset.maxFiles = container.dataset.maxFiles;
|
||||
form.dataset.allowedTypes = container.dataset.allowedTypes || "";
|
||||
new window.AttachmentUploader(form);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -290,7 +433,7 @@ class Application {
|
||||
btn.title = "Add emoji";
|
||||
btn.innerHTML = "\u{1F600}";
|
||||
|
||||
const picker = new EmojiPicker(textarea);
|
||||
const picker = new window.EmojiPicker(textarea);
|
||||
btn.addEventListener("click", () => picker.toggle());
|
||||
|
||||
const parent = textarea.parentElement;
|
||||
@@ -307,3 +450,4 @@ class Application {
|
||||
}
|
||||
|
||||
const app = new Application();
|
||||
window.app = app;
|
||||
|
||||
Reference in New Issue
Block a user