feat: add 6 more skins (PS1, PS3, Wii, NDS, Dreamcast, JV2002)
Brings the catalog to nine. Each is a single scoped [data-theme="..."] CSS file plus a registry entry — no markup changes. - ps1: charcoal BIOS, gray panels, uppercase letterspacing - ps3: animated XMB sky gradient + drifting wave, black glass - wii: white channels, rounded pills, soft blue glow - nds: silver shell with content framed as the touch screen - dreamcast: cream BIOS, conic-gradient orange swirl, lowercase blue - jv2002: dense boxy portal, Verdana 11px, red masthead, blue nav tabs Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
"use server";
|
||||
|
||||
import { cookies } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { checkPassword } from "@/lib/auth";
|
||||
import {
|
||||
ADMIN_COOKIE,
|
||||
createSessionToken,
|
||||
SESSION_MAX_AGE,
|
||||
} from "@/lib/session";
|
||||
import {
|
||||
createPost,
|
||||
updatePost,
|
||||
deletePost,
|
||||
replaceAllPosts,
|
||||
type PostInput,
|
||||
} from "@/lib/posts";
|
||||
import {
|
||||
saveSettings,
|
||||
normalizeSettings,
|
||||
type Settings,
|
||||
} from "@/lib/settings";
|
||||
import { THEMES, isThemeId, type ThemeId } from "@/themes/registry";
|
||||
|
||||
function s(formData: FormData, key: string): string {
|
||||
const v = formData.get(key);
|
||||
return typeof v === "string" ? v : "";
|
||||
}
|
||||
|
||||
// Refresh the whole site after a content/settings change. The layout-level
|
||||
// revalidation covers branding + theme that live in the shared shell.
|
||||
function revalidateSite() {
|
||||
revalidatePath("/", "layout");
|
||||
}
|
||||
|
||||
/* ------------------------------- auth -------------------------------- */
|
||||
|
||||
export async function loginAction(formData: FormData) {
|
||||
const password = s(formData, "password");
|
||||
const next = s(formData, "next");
|
||||
if (!checkPassword(password)) {
|
||||
redirect(
|
||||
`/admin/login?error=1${next ? `&next=${encodeURIComponent(next)}` : ""}`
|
||||
);
|
||||
}
|
||||
const token = await createSessionToken();
|
||||
(await cookies()).set(ADMIN_COOKIE, token, {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
path: "/",
|
||||
maxAge: SESSION_MAX_AGE,
|
||||
});
|
||||
redirect(next && next.startsWith("/admin") ? next : "/admin");
|
||||
}
|
||||
|
||||
export async function logoutAction() {
|
||||
(await cookies()).delete(ADMIN_COOKIE);
|
||||
redirect("/admin/login");
|
||||
}
|
||||
|
||||
/* ------------------------------- posts ------------------------------- */
|
||||
|
||||
function postInputFromForm(formData: FormData): PostInput {
|
||||
return {
|
||||
slug: s(formData, "slug"),
|
||||
title: s(formData, "title"),
|
||||
excerpt: s(formData, "excerpt"),
|
||||
body: s(formData, "body"),
|
||||
author: s(formData, "author"),
|
||||
tags: s(formData, "tags"),
|
||||
createdAt: s(formData, "createdAt"),
|
||||
};
|
||||
}
|
||||
|
||||
export async function savePostAction(formData: FormData) {
|
||||
const input = postInputFromForm(formData);
|
||||
const idRaw = s(formData, "id");
|
||||
|
||||
if (!input.title.trim()) {
|
||||
redirect(idRaw ? `/admin/posts/${idRaw}/edit?error=title` : "/admin/posts/new?error=title");
|
||||
}
|
||||
|
||||
if (idRaw) {
|
||||
updatePost(Number(idRaw), input);
|
||||
} else {
|
||||
createPost(input);
|
||||
}
|
||||
revalidateSite();
|
||||
redirect("/admin/posts");
|
||||
}
|
||||
|
||||
export async function deletePostAction(formData: FormData) {
|
||||
const id = Number(s(formData, "id"));
|
||||
if (Number.isFinite(id)) deletePost(id);
|
||||
revalidateSite();
|
||||
redirect("/admin/posts");
|
||||
}
|
||||
|
||||
/* ------------------------------ settings ----------------------------- */
|
||||
|
||||
export async function saveSettingsAction(formData: FormData) {
|
||||
const allowed = formData
|
||||
.getAll("allowedThemes")
|
||||
.map(String)
|
||||
.filter(isThemeId) as ThemeId[];
|
||||
|
||||
const defaultTheme = s(formData, "defaultTheme");
|
||||
|
||||
const next: Settings = normalizeSettings({
|
||||
title: s(formData, "title"),
|
||||
subtitle: s(formData, "subtitle"),
|
||||
footer: s(formData, "footer"),
|
||||
version: s(formData, "version"),
|
||||
defaultTheme: isThemeId(defaultTheme) ? defaultTheme : undefined,
|
||||
publicThemeToggle: formData.get("publicThemeToggle") === "on",
|
||||
allowedThemes: allowed.length ? allowed : THEMES.map((t) => t.id),
|
||||
});
|
||||
|
||||
saveSettings(next);
|
||||
revalidateSite();
|
||||
redirect("/admin/settings?saved=1");
|
||||
}
|
||||
|
||||
/* ------------------------------- import ------------------------------ */
|
||||
|
||||
export async function importAction(formData: FormData) {
|
||||
const file = formData.get("file");
|
||||
const pasted = s(formData, "pasted");
|
||||
const wants = formData.getAll("what").map(String);
|
||||
const wantSettings = wants.includes("settings");
|
||||
const wantPosts = wants.includes("posts");
|
||||
const postMode = s(formData, "postMode") || "replace";
|
||||
|
||||
let raw = pasted;
|
||||
if (file && file instanceof File && file.size > 0) {
|
||||
raw = await file.text();
|
||||
}
|
||||
if (!raw.trim()) redirect("/admin/settings?import=empty");
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch {
|
||||
redirect("/admin/settings?import=invalid");
|
||||
}
|
||||
|
||||
const obj = parsed as Record<string, unknown>;
|
||||
|
||||
if (wantSettings && obj.settings) {
|
||||
saveSettings(normalizeSettings(obj.settings));
|
||||
}
|
||||
|
||||
if (wantPosts && Array.isArray(obj.posts)) {
|
||||
const posts = (obj.posts as Record<string, unknown>[]).map(
|
||||
(p): PostInput => ({
|
||||
slug: typeof p.slug === "string" ? p.slug : undefined,
|
||||
title: typeof p.title === "string" ? p.title : "Untitled",
|
||||
excerpt: typeof p.excerpt === "string" ? p.excerpt : "",
|
||||
body: typeof p.body === "string" ? p.body : "",
|
||||
author: typeof p.author === "string" ? p.author : "webmaster",
|
||||
tags: Array.isArray(p.tags)
|
||||
? (p.tags as string[]).join(",")
|
||||
: typeof p.tags === "string"
|
||||
? p.tags
|
||||
: "",
|
||||
createdAt:
|
||||
typeof p.createdAt === "string"
|
||||
? p.createdAt
|
||||
: typeof p.created_at === "string"
|
||||
? (p.created_at as string)
|
||||
: undefined,
|
||||
})
|
||||
);
|
||||
if (postMode === "append") {
|
||||
for (const p of posts) createPost(p);
|
||||
} else {
|
||||
replaceAllPosts(posts);
|
||||
}
|
||||
}
|
||||
|
||||
revalidateSite();
|
||||
redirect("/admin/settings?import=ok");
|
||||
}
|
||||
Reference in New Issue
Block a user