create new page functionality

This commit is contained in:
Miika Holmlund
2025-12-08 19:31:39 +02:00
parent 90c3fdb69b
commit d5ee935205
4 changed files with 102 additions and 4 deletions
+63
View File
@@ -0,0 +1,63 @@
"use client";
import { useState } from "react";
import { Column, Row } from "@/components/ui/layout/container";
import { Button } from "@/components/ui/button";
import layout from "@/shared/styles/layout.module.css";
import { useRouter } from "next/navigation";
export default function NewPage() {
const [type, setType] = useState("resources");
const [title, setTitle] = useState("");
const [slug, setSlug] = useState("");
const [creationMessage, setCreationMessage] = useState("");
const router = useRouter();
const submit = async () => {
const response = await fetch(`/api/admin/pages/${type}/${slug}`, {
method: "POST",
body: JSON.stringify({ title }),
});
if (response.ok) {
router.push(`/admin/edit-page/${type}/${slug}`);
} else {
setTimeout(() => setCreationMessage(""), 1000);
setCreationMessage("Slug already in use");
}
};
return (
<Column className={layout.viewContent}>
<h1>New page</h1>
{!!creationMessage && <p className="error-message">{creationMessage}</p>}
<Row gap="1rem">
<label htmlFor="title">Title</label>
<input
type="text"
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
</Row>
<Row gap="1rem">
<label htmlFor="title">Slug</label>
<input
type="text"
id="title"
value={slug}
onInput={(e) => {
setSlug(e.target.value.replace(/[^A-Za-z0-9\-\_\+]/g, ""));
}}
required
/>
</Row>
<Row>
<label htmlFor="type">Page group</label>
<select value={type} onChange={(e) => setType(e.target.value)}>
<option value="resources">Learning</option>
<option value="grammar">Grammar</option>
<option value="communications">Communication</option>
</select>
</Row>
<Button onClick={submit}>Create</Button>
</Column>
);
}
@@ -6,7 +6,7 @@ import { NextResponse } from "next/server";
import { DB } from "@/backend/db";
// Allowed page_group values stored in the consolidated `html_pages` table.
const VALID_PAGE_GROUPS = new Set(["resources", "communications"]);
const VALID_PAGE_GROUPS = new Set(["resources", "communications", "grammar"]);
/*
Route behavior (summary):
@@ -27,7 +27,6 @@ export const DELETE = withAuth(
async (req, { params }) => {
const { type, slug } = await params;
// const query = `DELETE FROM html_pages WHERE slug = $1 AND page_group = $2 RETURNING *`;
const query = `DELETE FROM html_pages WHERE slug = $1 AND page_group = $2 RETURNING *`;
const result = await DB.pool(query, [slug, type]);
@@ -62,7 +61,7 @@ export const PUT = withAuth(
// Make sure that the slug only contains characters that do not need escaping
// in the url. This ensures that the slug the user types can be used as-is
// in the browser address bar.
if (data.slug) newData.slug = data.slug.replace(/[^A-Za-z0-9\-\_\+]/g, "");
if (data.slug) newData.slug = sanitizeSlug(data.slug);
if (newData.slug !== slug && (await slugIsInUse(type, newData.slug))) {
return new NextResponse("Page slug already in use", {
@@ -83,6 +82,33 @@ export const PUT = withAuth(
}
);
export const POST = withAuth(
async (req, { params }) => {
const { type, slug } = await params;
if (
slug !== sanitizeSlug(slug) ||
(await slugIsInUse(type, slug)) ||
!VALID_PAGE_GROUPS.has(type)
) {
return new NextResponse("Invalid slug", {
status: 400,
});
}
const title = (await req.json()).title;
const query = `INSERT INTO html_pages (title, content, slug, page_group) VALUES ($1, $2, $3, $4)`;
const result = await DB.pool(query, [title, "", slug, type]);
if (result.rowCount === 0)
return NextResponse.json({ error: "Not found" }, { status: 404 });
return NextResponse.json({ message: "Created" }, { status: 200 });
},
{
requireAdmin: true,
requireAuth: true,
}
);
function sanitize(data) {
const window = new JSDOM("").window;
const purify = DOMPurify(window);
@@ -99,3 +125,7 @@ async function slugIsInUse(type, slug) {
return false;
}
}
function sanitizeSlug(slug) {
return slug.replace(/[^A-Za-z0-9\-\_\+]/g, "");
}
@@ -9,7 +9,7 @@ import DOMPurify from "dompurify";
import { NextResponse } from "next/server";
// Allowed page_group values stored in the consolidated `html_pages` table.
const VALID_PAGE_GROUPS = new Set(["resources", "communications"]);
const VALID_PAGE_GROUPS = new Set(["resources", "communications", "grammar"]);
/*
Route behavior (summary):
@@ -172,6 +172,11 @@ const adminSidebarLinks = [
link: "/admin/add-admin",
id: "add-admin",
},
{
title: "New page",
link: "/admin/new-page",
id: "new-page",
},
];
function AdminSideBar() {