Files
yingfungsolar/src/pages/admin/cases.astro
T

199 lines
7.0 KiB
Plaintext

---
import { and, asc, desc, eq, gt, lt, max } from "drizzle-orm";
import AdminLayout from "../../layouts/AdminLayout.astro";
import ImageField from "../../components/admin/ImageField.astro";
import { cases } from "../../db/schema";
import { getDb } from "../../lib/db";
import { getEnv } from "../../lib/env";
import { parseForm } from "../../lib/form";
import { deleteMedia } from "../../lib/media";
import { caseInput } from "../../schemas";
export const prerender = false;
const env = getEnv();
const db = getDb(env.DB);
let errors: Record<string, string> = {};
let failedId = "";
let failedAdd = false;
function str(form: FormData, key: string): string {
return String(form.get(key) ?? "").trim();
}
if (Astro.request.method === "POST") {
const form = await Astro.request.formData();
const action = str(form, "action");
if (action === "add" || action === "save") {
const parsed = parseForm(form, caseInput);
if (!parsed.ok) {
errors = parsed.errors;
failedId = str(form, "id");
failedAdd = action === "add";
} else if (action === "add") {
const [row] = await db.select({ m: max(cases.sortOrder) }).from(cases);
await db.insert(cases).values({
id: crypto.randomUUID(),
title: parsed.data.title,
location: parsed.data.location || null,
completedAt: parsed.data.completedAt || null,
description: parsed.data.description || null,
imageUrl: parsed.data.imageUrl || null,
sortOrder: (row?.m ?? 0) + 1,
status: parsed.data.status,
updatedAt: new Date(),
});
} else {
const id = str(form, "id");
const newImage = parsed.data.imageUrl || null;
const [current] = await db.select().from(cases).where(eq(cases.id, id)).limit(1);
if (current && current.imageUrl !== newImage) {
await deleteMedia(env, current.imageUrl);
}
await db
.update(cases)
.set({
title: parsed.data.title,
location: parsed.data.location || null,
completedAt: parsed.data.completedAt || null,
description: parsed.data.description || null,
imageUrl: newImage,
status: parsed.data.status,
updatedAt: new Date(),
})
.where(eq(cases.id, id));
}
} else if (action === "delete") {
const id = str(form, "id");
const [current] = await db.select().from(cases).where(eq(cases.id, id)).limit(1);
await deleteMedia(env, current?.imageUrl);
await db.delete(cases).where(eq(cases.id, id));
} else if (action === "up" || action === "down") {
const id = str(form, "id");
const [current] = await db.select().from(cases).where(eq(cases.id, id)).limit(1);
if (current) {
const [adj] = await db
.select()
.from(cases)
.where(
action === "up" ? lt(cases.sortOrder, current.sortOrder) : gt(cases.sortOrder, current.sortOrder),
)
.orderBy(action === "up" ? desc(cases.sortOrder) : asc(cases.sortOrder))
.limit(1);
if (adj) {
await db.update(cases).set({ sortOrder: adj.sortOrder }).where(eq(cases.id, current.id));
await db.update(cases).set({ sortOrder: current.sortOrder }).where(eq(cases.id, adj.id));
}
}
}
if (Object.keys(errors).length === 0) {
return Astro.redirect("/admin/cases");
}
}
const list = await db.select().from(cases).orderBy(asc(cases.sortOrder));
---
<AdminLayout title="完成案例">
<h1>完成案例</h1>
<p class="sub">前台「完成案例」區塊會按下面次序顯示(只顯示已發布)。</p>
<div class="card">
<h2>新增案例</h2>
<form method="post">
<input type="hidden" name="action" value="add" />
<div class="grid2">
<div>
<label for="a-title">標題</label>
{failedAdd && errors.title && <span class="field-error">{errors.title}</span>}
<input id="a-title" name="title" type="text" required placeholder="例:大埔上碗窯 — 雙玻光伏板" />
</div>
<div>
<label for="a-loc">地點</label>
<input id="a-loc" name="location" type="text" placeholder="例:大埔半山" />
</div>
<div>
<label for="a-date">施工日期</label>
<input id="a-date" name="completedAt" type="text" placeholder="例:2026年2月" />
</div>
<div>
<ImageField name="imageUrl" label="圖片" scope="cases" id="a-img" />
</div>
<div style="grid-column:1/-1">
<label for="a-desc">描述</label>
<textarea id="a-desc" name="description"></textarea>
</div>
</div>
<div class="actions"><button type="submit">+ 新增案例</button></div>
</form>
</div>
{
list.length === 0 ? (
<p class="muted">暫時未有案例。</p>
) : (
list.map((item, i) => (
<div class="card">
<form method="post">
<input type="hidden" name="id" value={item.id} />
<div class="grid2">
<div>
<label>標題</label>
{failedId === item.id && errors.title && <span class="field-error">{errors.title}</span>}
<input name="title" type="text" value={item.title} required />
</div>
<div>
<label>地點</label>
<input name="location" type="text" value={item.location ?? ""} />
</div>
<div>
<label>施工日期</label>
<input name="completedAt" type="text" value={item.completedAt ?? ""} />
</div>
<div>
<ImageField
name="imageUrl"
label="圖片"
scope="cases"
value={item.imageUrl}
id={`img-${item.id}`}
/>
</div>
<div style="grid-column:1/-1">
<label>描述</label>
<textarea name="description">{item.description ?? ""}</textarea>
</div>
<div>
<label>狀態</label>
<select name="status">
<option value="published" selected={item.status === "published"}>已發布</option>
<option value="draft" selected={item.status === "draft"}>草稿</option>
</select>
</div>
</div>
<div class="actions">
<button type="submit" name="action" value="save">儲存</button>
<button type="submit" name="action" value="up" class="secondary mini" disabled={i === 0}>↑ 上移</button>
<button type="submit" name="action" value="down" class="secondary mini" disabled={i === list.length - 1}>
↓ 下移
</button>
<button
type="submit"
name="action"
value="delete"
class="danger mini"
onclick="return confirm('確定刪除?')"
>
刪除
</button>
</div>
</form>
</div>
))
)
}
</AdminLayout>