Add image upload to site settings with old-file cleanup
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
export type FieldType = "text" | "textarea" | "url";
|
export type FieldType = "text" | "textarea" | "url" | "image";
|
||||||
|
|
||||||
export type SettingsField = {
|
export type SettingsField = {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -39,7 +39,7 @@ export const SETTINGS_GROUPS: SettingsGroup[] = [
|
|||||||
{ key: "hero_subtitle", label: "副標題", type: "textarea" },
|
{ key: "hero_subtitle", label: "副標題", type: "textarea" },
|
||||||
{ key: "hero_primary_cta", label: "主要按鈕文字" },
|
{ key: "hero_primary_cta", label: "主要按鈕文字" },
|
||||||
{ key: "hero_secondary_cta", label: "次要按鈕文字" },
|
{ key: "hero_secondary_cta", label: "次要按鈕文字" },
|
||||||
{ key: "hero_image", label: "主視覺圖片 URL", type: "url", placeholder: "https://..." },
|
{ key: "hero_image", label: "主視覺圖片", type: "image" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -47,7 +47,7 @@ export const SETTINGS_GROUPS: SettingsGroup[] = [
|
|||||||
fields: [
|
fields: [
|
||||||
{ key: "seo_default_title", label: "預設標題" },
|
{ key: "seo_default_title", label: "預設標題" },
|
||||||
{ key: "seo_default_description", label: "預設描述", type: "textarea" },
|
{ key: "seo_default_description", label: "預設描述", type: "textarea" },
|
||||||
{ key: "og_image", label: "分享圖片 URL (OG image)", type: "url", placeholder: "https://..." },
|
{ key: "og_image", label: "分享圖片 (OG image)", type: "image" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
---
|
---
|
||||||
import AdminLayout from "../../layouts/AdminLayout.astro";
|
import AdminLayout from "../../layouts/AdminLayout.astro";
|
||||||
|
import ImageField from "../../components/admin/ImageField.astro";
|
||||||
import { ALL_SETTING_KEYS, SETTINGS_GROUPS } from "../../data/settings-fields";
|
import { ALL_SETTING_KEYS, SETTINGS_GROUPS } from "../../data/settings-fields";
|
||||||
import { siteSettings } from "../../db/schema";
|
import { siteSettings } from "../../db/schema";
|
||||||
import { getSettings } from "../../data/content";
|
import { getSettings } from "../../data/content";
|
||||||
import { getDb } from "../../lib/db";
|
import { getDb } from "../../lib/db";
|
||||||
import { getEnv } from "../../lib/env";
|
import { getEnv } from "../../lib/env";
|
||||||
import { parseForm } from "../../lib/form";
|
import { parseForm } from "../../lib/form";
|
||||||
|
import { deleteMedia } from "../../lib/media";
|
||||||
import { settingsInput } from "../../schemas";
|
import { settingsInput } from "../../schemas";
|
||||||
|
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
const db = getDb(getEnv().DB);
|
const env = getEnv();
|
||||||
|
const db = getDb(env.DB);
|
||||||
|
|
||||||
|
const IMAGE_SETTING_KEYS = new Set(
|
||||||
|
SETTINGS_GROUPS.flatMap((g) =>
|
||||||
|
g.fields.filter((f) => f.type === "image").map((f) => f.key),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
let saved = false;
|
let saved = false;
|
||||||
let error = "";
|
let error = "";
|
||||||
@@ -21,6 +30,7 @@ if (Astro.request.method === "POST") {
|
|||||||
if (!parsed.ok) {
|
if (!parsed.ok) {
|
||||||
error = Object.values(parsed.errors)[0] ?? "設定有誤。";
|
error = Object.values(parsed.errors)[0] ?? "設定有誤。";
|
||||||
} else {
|
} else {
|
||||||
|
const before = await getSettings(db);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const submitted = new Set(
|
const submitted = new Set(
|
||||||
[...form.keys()].filter((k) => ALL_SETTING_KEYS.includes(k)),
|
[...form.keys()].filter((k) => ALL_SETTING_KEYS.includes(k)),
|
||||||
@@ -28,6 +38,9 @@ if (Astro.request.method === "POST") {
|
|||||||
for (const key of ALL_SETTING_KEYS) {
|
for (const key of ALL_SETTING_KEYS) {
|
||||||
if (!submitted.has(key)) continue;
|
if (!submitted.has(key)) continue;
|
||||||
const value = String((parsed.data as Record<string, unknown>)[key] ?? "");
|
const value = String((parsed.data as Record<string, unknown>)[key] ?? "");
|
||||||
|
if (IMAGE_SETTING_KEYS.has(key) && before[key] !== value) {
|
||||||
|
await deleteMedia(env, before[key]);
|
||||||
|
}
|
||||||
await db
|
await db
|
||||||
.insert(siteSettings)
|
.insert(siteSettings)
|
||||||
.values({ key, value, updatedAt: now })
|
.values({ key, value, updatedAt: now })
|
||||||
@@ -56,11 +69,31 @@ const settings = await getSettings(db);
|
|||||||
<div class="grid2">
|
<div class="grid2">
|
||||||
{group.fields.map((f) => (
|
{group.fields.map((f) => (
|
||||||
<div style={f.type === "textarea" ? "grid-column:1/-1" : ""}>
|
<div style={f.type === "textarea" ? "grid-column:1/-1" : ""}>
|
||||||
|
{f.type === "image" ? (
|
||||||
|
<ImageField
|
||||||
|
name={f.key}
|
||||||
|
label={f.label}
|
||||||
|
value={settings[f.key] ?? ""}
|
||||||
|
scope="settings"
|
||||||
|
id={f.key}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<label for={f.key}>{f.label}</label>
|
<label for={f.key}>{f.label}</label>
|
||||||
{f.type === "textarea" ? (
|
{f.type === "textarea" ? (
|
||||||
<textarea id={f.key} name={f.key} placeholder={f.placeholder}>{settings[f.key] ?? ""}</textarea>
|
<textarea id={f.key} name={f.key} placeholder={f.placeholder}>
|
||||||
|
{settings[f.key] ?? ""}
|
||||||
|
</textarea>
|
||||||
) : (
|
) : (
|
||||||
<input id={f.key} name={f.key} type="text" value={settings[f.key] ?? ""} placeholder={f.placeholder} />
|
<input
|
||||||
|
id={f.key}
|
||||||
|
name={f.key}
|
||||||
|
type="text"
|
||||||
|
value={settings[f.key] ?? ""}
|
||||||
|
placeholder={f.placeholder}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user