diff --git a/README.md b/README.md index e215bc4..39a01bd 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,20 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +## Admin panel + +Visit [http://localhost:3000/admin](http://localhost:3000/admin) and sign in with +`ADMIN_PASSWORD` (see `.env.example`; defaults to `admin` in dev). From there you can: + +- **Posts** — full create / edit / delete with Markdown bodies, slugs, tags, and dates. +- **Settings** — branding (title, subtitle, footer, version), the default theme for + new visitors, whether the public theme switcher is shown, and which skins it offers. +- **Import / export** — download a JSON backup of settings and/or posts, and import + one back (posts can replace or append). + +Auth is a single password kept in `ADMIN_PASSWORD`, with a signed session cookie +(`ADMIN_SESSION_SECRET`). All `/admin/*` routes are gated by middleware. + This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. ## Learn More diff --git a/src/app/admin/(panel)/layout.tsx b/src/app/admin/(panel)/layout.tsx new file mode 100644 index 0000000..e6afed7 --- /dev/null +++ b/src/app/admin/(panel)/layout.tsx @@ -0,0 +1,38 @@ +import type { ReactNode } from "react"; +import Link from "next/link"; +import { requireAdmin } from "@/lib/auth"; +import { logoutAction } from "../actions"; + +// Guarded chrome for every authenticated admin page. Login lives outside this +// group so it never inherits the nav or the auth gate. +export default async function PanelLayout({ + children, +}: { + children: ReactNode; +}) { + await requireAdmin(); + + return ( +
+
+ + RetroBlog Admin + + +
+ +
+
+
{children}
+
+ ); +} diff --git a/src/app/admin/(panel)/page.tsx b/src/app/admin/(panel)/page.tsx new file mode 100644 index 0000000..4c88a6c --- /dev/null +++ b/src/app/admin/(panel)/page.tsx @@ -0,0 +1,54 @@ +import Link from "next/link"; +import { getAllPosts } from "@/lib/posts"; +import { getSettings } from "@/lib/settings"; +import { themeMeta } from "@/themes/registry"; + +export default async function DashboardPage() { + const posts = getAllPosts(); + const settings = getSettings(); + + return ( +
+

Dashboard

+ +
+
+

Posts

+

{posts.length}

+ + Manage posts + + + New post + +
+ +
+

Theme

+

+ Default: {themeMeta(settings.defaultTheme).name} +

+

+ Public switcher:{" "} + {settings.publicThemeToggle ? "visible" : "hidden"} ·{" "} + {settings.allowedThemes.length} skins allowed +

+ + Edit settings + +
+ +
+

Backup

+

Export or import settings and posts.

+ + Export everything + + + Import / export + +
+
+
+ ); +} diff --git a/src/app/admin/(panel)/posts/PostForm.tsx b/src/app/admin/(panel)/posts/PostForm.tsx new file mode 100644 index 0000000..ed7cf92 --- /dev/null +++ b/src/app/admin/(panel)/posts/PostForm.tsx @@ -0,0 +1,93 @@ +import Link from "next/link"; +import type { Post } from "@/lib/posts"; +import { savePostAction } from "../../actions"; + +// Server-rendered create/edit form. Both modes post to the same action; the +// hidden `id` (present only when editing) decides insert vs update. +export default function PostForm({ + post, + titleError, +}: { + post?: Post; + titleError?: boolean; +}) { + const editing = !!post; + + return ( +
+ {editing && } + + {titleError &&

Title is required.

} + + + + + +
+ + +
+ + + +