first commit

This commit is contained in:
2026-09-11 15:49:41 +08:00
commit 5b69bc818a
98 changed files with 30551 additions and 0 deletions
+172
View File
@@ -0,0 +1,172 @@
---
import { and, asc, desc, eq, gt, lt, max } from "drizzle-orm";
import AdminLayout from "../../layouts/AdminLayout.astro";
import { cases } from "../../db/schema";
import { getDb } from "../../lib/db";
import { getEnv } from "../../lib/env";
export const prerender = false;
const db = getDb(getEnv().DB);
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") {
const title = str(form, "title");
if (title) {
const [row] = await db.select({ m: max(cases.sortOrder) }).from(cases);
await db.insert(cases).values({
id: crypto.randomUUID(),
title,
location: str(form, "location") || null,
completedAt: str(form, "completedAt") || null,
description: str(form, "description") || null,
imageUrl: str(form, "imageUrl") || null,
sortOrder: (row?.m ?? 0) + 1,
status: str(form, "status") === "draft" ? "draft" : "published",
updatedAt: new Date(),
});
}
} else if (action === "save") {
const id = str(form, "id");
await db
.update(cases)
.set({
title: str(form, "title"),
location: str(form, "location") || null,
completedAt: str(form, "completedAt") || null,
description: str(form, "description") || null,
imageUrl: str(form, "imageUrl") || null,
status: str(form, "status") === "draft" ? "draft" : "published",
updatedAt: new Date(),
})
.where(eq(cases.id, id));
} else if (action === "delete") {
await db.delete(cases).where(eq(cases.id, str(form, "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));
}
}
}
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>
<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>
<label for="a-img">圖片 URL</label>
<input id="a-img" name="imageUrl" type="text" placeholder="https://..." />
</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>
<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>
<label>圖片 URL</label>
<input name="imageUrl" type="text" value={item.imageUrl ?? ""} />
</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>
{item.imageUrl && (
<img src={item.imageUrl} alt={item.title} style="margin-top:14px;max-width:220px;border-radius:10px;" />
)}
<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>