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:
2026-05-10 07:08:12 +00:00
commit 15a3b990fe
54 changed files with 5906 additions and 0 deletions
+216
View File
@@ -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();