199 lines
7.0 KiB
Plaintext
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>
|