152 lines
7.2 KiB
Text
152 lines
7.2 KiB
Text
|
|
---
|
||
|
|
import AdminLayout from "../../../layouts/AdminLayout.astro";
|
||
|
|
---
|
||
|
|
|
||
|
|
<AdminLayout title="Edit project">
|
||
|
|
<a href="/admin/projects" class="back">← All projects</a>
|
||
|
|
<h1 id="ptitle">Project</h1>
|
||
|
|
<p class="lead">Upload or replace gallery images, set the Instagram reel, then publish.</p>
|
||
|
|
|
||
|
|
<section class="block">
|
||
|
|
<label>Instagram reel URL</label>
|
||
|
|
<input type="url" id="ig" placeholder="https://instagram.com/reel/…" />
|
||
|
|
<p class="hint">Shown as the reel beside the case study.</p>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section class="block">
|
||
|
|
<div class="head">
|
||
|
|
<label>Gallery</label>
|
||
|
|
<button type="button" class="btn ghost sm" id="addSlot">+ Add image</button>
|
||
|
|
</div>
|
||
|
|
<div id="gallery" class="grid"></div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<div class="footer">
|
||
|
|
<button type="button" class="btn" id="save">Save & publish</button>
|
||
|
|
<p class="msg" id="msg" role="status" aria-live="polite"></p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<style>
|
||
|
|
.back { font-family:var(--mono); font-size:.62rem; letter-spacing:.12em; text-transform:uppercase;
|
||
|
|
color:var(--ink4); text-decoration:none; display:inline-block; margin-bottom:1.25rem; }
|
||
|
|
.back:hover { color:var(--ink); }
|
||
|
|
.block { margin:2rem 0; }
|
||
|
|
.block .head { display:flex; align-items:center; gap:1rem; margin-bottom:1rem; }
|
||
|
|
.block .head label { margin:0; }
|
||
|
|
.hint { font-size:.78rem; color:var(--ink4); margin:.4rem 0 0; }
|
||
|
|
.btn.sm { padding:.35rem .7rem; font-size:.7rem; }
|
||
|
|
.grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(11rem,1fr)); gap:1rem; }
|
||
|
|
.slot { border:1px solid var(--rule); border-radius:2px; overflow:hidden; display:flex; flex-direction:column; }
|
||
|
|
.thumb { position:relative; aspect-ratio:4/3; background:var(--card); display:flex; align-items:center;
|
||
|
|
justify-content:center; overflow:hidden; }
|
||
|
|
.thumb img { width:100%; height:100%; object-fit:cover; }
|
||
|
|
.thumb .empty { font-family:var(--mono); font-size:.6rem; letter-spacing:.1em; text-transform:uppercase; color:var(--ink4); }
|
||
|
|
.thumb.busy::after { content:"Uploading…"; position:absolute; inset:0; display:flex; align-items:center;
|
||
|
|
justify-content:center; background:color-mix(in srgb, var(--paper) 80%, transparent);
|
||
|
|
font-family:var(--mono); font-size:.65rem; color:var(--ink3); }
|
||
|
|
.slot .cap { border:0; border-top:1px solid var(--rule); background:transparent; padding:.5rem .6rem;
|
||
|
|
font-size:.8rem; color:var(--ink); font-family:var(--sans); width:100%; }
|
||
|
|
.slot .cap:focus { outline:none; }
|
||
|
|
.slot .acts { display:flex; border-top:1px solid var(--rule); }
|
||
|
|
.slot .acts label, .slot .acts button { flex:1; text-align:center; padding:.45rem; font-family:var(--mono);
|
||
|
|
font-size:.58rem; letter-spacing:.08em; text-transform:uppercase; color:var(--ink3); cursor:pointer;
|
||
|
|
background:none; border:0; }
|
||
|
|
.slot .acts label { border-right:1px solid var(--rule); }
|
||
|
|
.slot .acts button:hover { color:var(--seal); } .slot .acts label:hover { color:var(--ink); }
|
||
|
|
.slot .acts input { display:none; }
|
||
|
|
.footer { position:sticky; bottom:0; display:flex; align-items:center; gap:1.5rem; padding:1rem 0;
|
||
|
|
margin-top:2rem; border-top:1px solid var(--rule);
|
||
|
|
background:color-mix(in srgb, var(--paper) 92%, transparent); }
|
||
|
|
</style>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
const slug = new URLSearchParams(location.search).get("slug") || "";
|
||
|
|
const galleryEl = document.getElementById("gallery");
|
||
|
|
const msg = document.getElementById("msg");
|
||
|
|
const setMsg = (t, cls = "") => { msg.textContent = t; msg.className = "msg " + cls; };
|
||
|
|
|
||
|
|
// in-memory model: [{src, alt, caption}]
|
||
|
|
let items = [];
|
||
|
|
|
||
|
|
function render() {
|
||
|
|
galleryEl.innerHTML = "";
|
||
|
|
items.forEach((it, i) => {
|
||
|
|
const slot = document.createElement("div");
|
||
|
|
slot.className = "slot";
|
||
|
|
slot.innerHTML = `
|
||
|
|
<div class="thumb" data-thumb>
|
||
|
|
${it.src ? `<img src="${it.src}" alt="">` : `<span class="empty">No image</span>`}
|
||
|
|
</div>
|
||
|
|
<input class="cap" placeholder="Caption" value="${(it.caption || "").replace(/"/g, """)}" data-cap>
|
||
|
|
<div class="acts">
|
||
|
|
<label>${it.src ? "Replace" : "Upload"}<input type="file" accept="image/*" data-file></label>
|
||
|
|
<button type="button" data-remove>Remove</button>
|
||
|
|
</div>`;
|
||
|
|
slot.querySelector("[data-cap]").addEventListener("input", (e) => { items[i].caption = e.target.value; });
|
||
|
|
slot.querySelector("[data-remove]").addEventListener("click", () => { items.splice(i, 1); render(); });
|
||
|
|
slot.querySelector("[data-file]").addEventListener("change", (e) => upload(e.target.files[0], i, slot));
|
||
|
|
galleryEl.appendChild(slot);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
async function upload(file, i, slot) {
|
||
|
|
if (!file) return;
|
||
|
|
const thumb = slot.querySelector("[data-thumb]");
|
||
|
|
thumb.classList.add("busy");
|
||
|
|
try {
|
||
|
|
const fd = new FormData();
|
||
|
|
fd.append("file", file);
|
||
|
|
const r = await fetch("/api/media/upload", { method: "POST", credentials: "same-origin", body: fd });
|
||
|
|
const d = await r.json();
|
||
|
|
if (r.ok && d?.data?.url) {
|
||
|
|
items[i].src = d.data.url;
|
||
|
|
render();
|
||
|
|
} else {
|
||
|
|
setMsg(d?.error?.message || "Upload failed.", "error");
|
||
|
|
}
|
||
|
|
} catch { setMsg("Upload failed.", "error"); }
|
||
|
|
finally { thumb.classList.remove("busy"); }
|
||
|
|
}
|
||
|
|
|
||
|
|
document.getElementById("addSlot").addEventListener("click", () => {
|
||
|
|
items.push({ src: "", alt: "", caption: "" });
|
||
|
|
render();
|
||
|
|
});
|
||
|
|
|
||
|
|
document.getElementById("save").addEventListener("click", async () => {
|
||
|
|
const btn = document.getElementById("save");
|
||
|
|
btn.disabled = true; setMsg("Saving…");
|
||
|
|
try {
|
||
|
|
const links = {};
|
||
|
|
const ig = document.getElementById("ig").value.trim();
|
||
|
|
if (ig) links.instagram = ig;
|
||
|
|
const save = await fetch("/api/adminprojects/save", {
|
||
|
|
method: "POST", credentials: "same-origin",
|
||
|
|
headers: { "content-type": "application/json" },
|
||
|
|
body: JSON.stringify({ slug, gallery: items, links }),
|
||
|
|
});
|
||
|
|
const sd = await save.json();
|
||
|
|
if (!save.ok || !sd?.data?.ok) { setMsg(sd?.error?.message || "Save failed.", "error"); btn.disabled = false; return; }
|
||
|
|
setMsg("Saved — publishing to the live site…");
|
||
|
|
const pub = await fetch("/devconsole/rebuild", { method: "POST", credentials: "same-origin" });
|
||
|
|
if (pub.ok) setMsg("Published. Changes are live.", "ok");
|
||
|
|
else setMsg("Saved, but the publish build failed. Try again.", "error");
|
||
|
|
} catch { setMsg("Something went wrong.", "error"); }
|
||
|
|
finally { btn.disabled = false; }
|
||
|
|
});
|
||
|
|
|
||
|
|
(async () => {
|
||
|
|
if (!slug) { setMsg("No project specified.", "error"); return; }
|
||
|
|
try {
|
||
|
|
const r = await fetch("/api/adminprojects/get?slug=" + encodeURIComponent(slug), { credentials: "same-origin" });
|
||
|
|
const d = await r.json();
|
||
|
|
if (!r.ok || !d?.data) { setMsg("Couldn't load the project.", "error"); return; }
|
||
|
|
document.getElementById("ptitle").textContent = d.data.title;
|
||
|
|
document.getElementById("ig").value = d.data.links?.instagram || "";
|
||
|
|
items = Array.isArray(d.data.gallery) ? d.data.gallery : [];
|
||
|
|
render();
|
||
|
|
} catch { setMsg("Couldn't load the project.", "error"); }
|
||
|
|
})();
|
||
|
|
</script>
|
||
|
|
</AdminLayout>
|