forked from retoor/devplacepy
feat: scaffold initial DevPlace project with FastAPI, SQLite auth, and SSR routing
Add complete project skeleton including .gitignore, Makefile, AGENTS.md, config, database init with indexes, main app with router mounting for auth/feed/posts/comments/projects/profile/messages/notifications/votes, Pydantic models for signup/login/post/comment/project/message/profile, and auth router with signup/login page rendering and form handling.
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
class Application {
|
||||
constructor() {
|
||||
this.initPasswordToggles();
|
||||
this.initModals();
|
||||
this.initPostForm();
|
||||
this.initCommentForms();
|
||||
this.initVoteButtons();
|
||||
this.initMessageSearch();
|
||||
this.initNotificationDismiss();
|
||||
this.initProfileEdit();
|
||||
this.initProjectForm();
|
||||
this.loadCSS("/static/css/variables.css");
|
||||
this.loadCSS("/static/css/base.css");
|
||||
}
|
||||
|
||||
loadCSS(href) {
|
||||
if (document.querySelector(`link[href="${href}"]`)) {
|
||||
return;
|
||||
}
|
||||
const link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = href;
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
initPasswordToggles() {
|
||||
document.querySelectorAll(".auth-toggle-pw").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const input = btn.parentElement.querySelector("input");
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
const type = input.type === "password" ? "text" : "password";
|
||||
input.type = type;
|
||||
btn.textContent = type === "password" ? "\u{1F441}" : "\u{1F441}";
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initModals() {
|
||||
document.querySelectorAll("[data-modal]").forEach((trigger) => {
|
||||
trigger.addEventListener("click", () => {
|
||||
const modalId = trigger.dataset.modal;
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
modal.classList.add("visible");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll(".modal-overlay").forEach((modal) => {
|
||||
modal.addEventListener("click", (e) => {
|
||||
if (e.target === modal) {
|
||||
modal.classList.remove("visible");
|
||||
}
|
||||
});
|
||||
const closeBtn = modal.querySelector(".modal-close");
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener("click", () => {
|
||||
modal.classList.remove("visible");
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initPostForm() {
|
||||
const form = document.getElementById("create-post-form");
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
const content = form.querySelector("#post-content");
|
||||
const title = form.querySelector("#post-title");
|
||||
const contentCount = form.querySelector("#post-content-count");
|
||||
const titleCount = form.querySelector("#post-title-count");
|
||||
|
||||
if (content && contentCount) {
|
||||
content.addEventListener("input", () => {
|
||||
contentCount.textContent = `${content.value.length}/2000`;
|
||||
});
|
||||
}
|
||||
if (title && titleCount) {
|
||||
title.addEventListener("input", () => {
|
||||
titleCount.textContent = `${title.value.length}/500`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
initCommentForms() {
|
||||
document.querySelectorAll(".comment-form").forEach((form) => {
|
||||
const textarea = form.querySelector("textarea");
|
||||
if (!textarea) {
|
||||
return;
|
||||
}
|
||||
textarea.addEventListener("input", () => {
|
||||
textarea.style.height = "auto";
|
||||
textarea.style.height = `${Math.min(textarea.scrollHeight, 120)}px`;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initVoteButtons() {
|
||||
document.querySelectorAll(".post-action-btn[data-vote]").forEach((btn) => {
|
||||
btn.addEventListener("click", async () => {
|
||||
const targetUid = btn.dataset.target;
|
||||
const targetType = btn.dataset.type || "post";
|
||||
const value = btn.dataset.vote;
|
||||
|
||||
const form = document.createElement("form");
|
||||
form.method = "POST";
|
||||
form.action = `/votes/${targetType}/${targetUid}`;
|
||||
const input = document.createElement("input");
|
||||
input.type = "hidden";
|
||||
input.name = "value";
|
||||
input.value = value;
|
||||
form.appendChild(input);
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initMessageSearch() {
|
||||
const searchInput = document.getElementById("message-search");
|
||||
if (!searchInput) {
|
||||
return;
|
||||
}
|
||||
searchInput.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
const query = searchInput.value.trim();
|
||||
if (query) {
|
||||
window.location.href = `/messages?search=${encodeURIComponent(query)}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initNotificationDismiss() {
|
||||
document.querySelectorAll(".notification-dismiss").forEach((btn) => {
|
||||
btn.addEventListener("click", async () => {
|
||||
const uid = btn.dataset.uid;
|
||||
if (!uid) {
|
||||
return;
|
||||
}
|
||||
const form = document.createElement("form");
|
||||
form.method = "POST";
|
||||
form.action = `/notifications/mark-read/${uid}`;
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initProfileEdit() {
|
||||
document.querySelectorAll("[data-edit-field]").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const field = btn.dataset.editField;
|
||||
const display = document.getElementById(`display-${field}`);
|
||||
const input = document.getElementById(`input-${field}`);
|
||||
if (!display || !input) {
|
||||
return;
|
||||
}
|
||||
display.classList.toggle("hidden");
|
||||
input.classList.toggle("hidden");
|
||||
if (!input.classList.contains("hidden")) {
|
||||
input.focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initProjectForm() {
|
||||
const platformsInput = document.getElementById("platforms-input");
|
||||
if (!platformsInput) {
|
||||
return;
|
||||
}
|
||||
const hiddenInput = document.getElementById("platforms");
|
||||
const tagsContainer = document.getElementById("platforms-tags");
|
||||
|
||||
platformsInput.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
const val = platformsInput.value.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.addEventListener("click", () => {
|
||||
tag.remove();
|
||||
updatePlatforms();
|
||||
});
|
||||
tag.appendChild(remove);
|
||||
tagsContainer.appendChild(tag);
|
||||
platformsInput.value = "";
|
||||
updatePlatforms();
|
||||
}
|
||||
});
|
||||
|
||||
function updatePlatforms() {
|
||||
const tags = tagsContainer.querySelectorAll(".platform-tag");
|
||||
const values = Array.from(tags).map((t) =>
|
||||
t.textContent.replace("x", "").trim()
|
||||
);
|
||||
hiddenInput.value = values.join(",");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const app = new Application();
|
||||
Reference in New Issue
Block a user