<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>dev portal  pawn.rip</title> <link rel="stylesheet" href="/styles.css" /> </head> <body> <div id="login-view" class="dev-page"> <div class="dev-card"> <h1>dev portal</h1> <p class="dev-sub">sign in to manage projects</p> <form id="login-form" class="dev-form"> <input id="password" type="password" placeholder="admin password" required autofocus /> <button type="submit">login</button> </form> <p id="login-msg" class="dev-msg error hidden"></p> <a href="/" class="back-link">ÔåÉ back to site</a> </div> </div> <div id="portal-view" class="dev-page wide hidden"> <div class="dev-header"> <div> <h1>dev portal</h1> <p class="dev-sub">add, edit, or remove projects</p> </div> <div class="dev-actions"> <a href="/" class="ghost-btn">view site</a> <button id="logout-btn" class="ghost-btn" type="button">logout</button> </div> </div> <p id="portal-msg" class="dev-msg hidden"></p> <div class="dev-grid"> <section class="dev-card"> <h2 id="form-title">new project</h2> <form id="project-form" class="dev-form"> <input id="project-id" type="hidden" /> <label>title<input id="title" required /></label> <label>description<textarea id="description" rows="4" required></textarea></label> <label>image url<input id="image" placeholder="https://... or upload below" required /></label> <label>upload image<input id="file" type="file" accept="image/*" /></label> <img id="preview" class="preview-img hidden" alt="preview" /> <label>project url (optional)<input id="url" placeholder="https://..." /></label> <label>tags (comma separated)<input id="tags" placeholder="web, tool" /></label> <div class="form-btns"> <button type="submit" id="save-btn">create</button> <button type="button" id="cancel-btn" class="ghost-btn hidden">cancel edit</button> </div> </form> </section> <section class="dev-card"> <h2>your projects <span id="loading-label"></span></h2> <div id="dev-list" class="dev-list"></div> </section> </div> </div> <script> const TOKEN_KEY = "bio_dev_token"; let token = sessionStorage.getItem(TOKEN_KEY) || ""; const loginView = document.getElementById("login-view"); const portalView = document.getElementById("portal-view"); const loginMsg = document.getElementById("login-msg"); const portalMsg = document.getElementById("portal-msg"); function showMsg(el, text, isError) { el.textContent = text; el.classList.remove("hidden"); el.classList.toggle("error", !!isError); } function hideMsg(el) { el.classList.add("hidden"); } function showPortal(loggedIn) { loginView.classList.toggle("hidden", loggedIn); portalView.classList.toggle("hidden", !loggedIn); if (loggedIn) loadProjects(); } if (token) showPortal(true); document.getElementById("login-form").onsubmit = async (e) => { e.preventDefault(); hideMsg(loginMsg); const password = document.getElementById("password").value; const res = await fetch("/api/auth", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ password }), }); if (res.ok) { token = password; sessionStorage.setItem(TOKEN_KEY, token); showPortal(true); } else { showMsg(loginMsg, "wrong password", true); } }; document.getElementById("logout-btn").onclick = () => { token = ""; sessionStorage.removeItem(TOKEN_KEY); showPortal(false); }; document.getElementById("file").onchange = (e) => { const file = e.target.files?.[0]; if (!file) return; if (file.size > 5 * 1024 * 1024) { showMsg(portalMsg, "image must be under 5MB", true); return; } const reader = new FileReader(); reader.onload = () => { document.getElementById("image").value = reader.result; const preview = document.getElementById("preview"); preview.src = reader.result; preview.classList.remove("hidden"); showMsg(portalMsg, "image ready ÔÇö save project to keep it", false); }; reader.readAsDataURL(file); }; function resetForm() { document.getElementById("project-form").reset(); document.getElementById("project-id").value = ""; document.getElementById("form-title").textContent = "new project"; document.getElementById("save-btn").textContent = "create"; document.getElementById("cancel-btn").classList.add("hidden"); document.getElementById("preview").classList.add("hidden"); } document.getElementById("cancel-btn").onclick = resetForm; document.getElementById("project-form").onsubmit = async (e) => { e.preventDefault(); hideMsg(portalMsg); const id = document.getElementById("project-id").value; const body = { id: id || undefined, title: document.getElementById("title").value, description: document.getElementById("description").value, image: document.getElementById("image").value, url: document.getElementById("url").value, tags: document.getElementById("tags").value.split(",").map((s) => s.trim()).filter(Boolean), }; const res = await fetch("/api/projects", { method: id ? "PUT" : "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify(body), }); const json = await res.json(); if (!res.ok) { showMsg(portalMsg, json.error || "save failed", true); return; } showMsg(portalMsg, id ? "project updated" : "project created", false); resetForm(); loadProjects(); }; async function loadProjects() { document.getElementById("loading-label").textContent = ",%À loading..."; const res = await fetch("/api/projects"); const json = await res.json(); const projects = json.projects || []; document.getElementById("loading-label").textContent = ""; const list = document.getElementById("dev-list"); if (!projects.length) { list.innerHTML = `<p class="dev-sub">no projects yet</p>`; return; } list.innerHTML = projects.map((p, i) => ` <div class="dev-item"> <img class="dev-thumb" src="${p.image}" alt="" /> <div class="dev-item-info"> <strong>${p.title}</strong> <span>${p.description.slice(0, 80)}${p.description.length > 80 ? "..." : ""}</span> </div> <div class="dev-item-btns"> <button class="ghost-btn" type="button" data-edit="${i}">edit</button> <button class="ghost-btn danger" type="button" data-delete="${p.id}">delete</button> </div> </div> `).join(""); window._projects = projects; list.querySelectorAll("[data-edit]").forEach((btn) => { btn.onclick = () => editProject(window._projects[+btn.dataset.edit]); }); list.querySelectorAll("[data-delete]").forEach((btn) => { btn.onclick = () => deleteProject(btn.dataset.delete); }); } function editProject(p) { document.getElementById("project-id").value = p.id; document.getElementById("title").value = p.title; document.getElementById("description").value = p.description; document.getElementById("image").value = p.image; document.getElementById("url").value = p.url || ""; document.getElementById("tags").value = (p.tags || []).join(", "); document.getElementById("form-title").textContent = "edit project"; document.getElementById("save-btn").textContent = "update"; document.getElementById("cancel-btn").classList.remove("hidden"); const preview = document.getElementById("preview"); preview.src = p.image; preview.classList.remove("hidden"); window.scrollTo({ top: 0, behavior: "smooth" }); }; async function deleteProject(id) { if (!confirm("delete this project?")) return; const res = await fetch(`/api/projects?id=${id}`, { method: "DELETE", headers: { Authorization: `Bearer ${token}` }, }); if (res.ok) { showMsg(portalMsg, "project deleted", false); loadProjects(); } }; </script> </body> </html>