diff --git a/src/back/collections/collections.ts b/src/back/collections/collections.ts
index 13e8317..5a23f97 100644
--- a/src/back/collections/collections.ts
+++ b/src/back/collections/collections.ts
@@ -4,6 +4,7 @@ import { App } from "sealious";
import _Globals from "./globals.js";
import _GroupsToUsers from "./groups-to-users.js";
import _Groups from "./groups.js";
+import _NavbarLinks from "./navbar-links.js";
import _Pages from "./pages.js";
import _PasswordResetIntents from "./password-reset-intents.js";
import _Redirects from "./redirects.js";
@@ -13,6 +14,7 @@ import _Users from "./users.js";
export const Globals = new _Globals();
export const GroupsToUsers = new _GroupsToUsers();
export const Groups = new _Groups();
+export const NavbarLinks = new _NavbarLinks();
export const Pages = new _Pages();
export const PasswordResetIntents = new _PasswordResetIntents();
export const Redirects = new _Redirects();
@@ -24,6 +26,7 @@ export const collections = {
globals: Globals,
"groups-to-users": GroupsToUsers,
groups: Groups,
+ "navbar-links": NavbarLinks,
pages: Pages,
"password-reset-intents": PasswordResetIntents,
redirects: Redirects,
diff --git a/src/back/collections/navbar-links.ts b/src/back/collections/navbar-links.ts
new file mode 100644
index 0000000..2d521b9
--- /dev/null
+++ b/src/back/collections/navbar-links.ts
@@ -0,0 +1,9 @@
+import { Collection, FieldTypes, Policies } from "sealious";
+
+export default class NavbarLinks extends Collection {
+ fields = {
+ label: new FieldTypes.Text(),
+ href: new FieldTypes.Text(),
+ };
+ defaultPolicy = new Policies.Public();
+}
diff --git a/src/back/collections/pages.ts b/src/back/collections/pages.ts
index 1116d8f..1dfa295 100644
--- a/src/back/collections/pages.ts
+++ b/src/back/collections/pages.ts
@@ -1,5 +1,7 @@
-import { Collection, FieldTypes, Policies } from "sealious";
+import { App, Collection, FieldTypes, Policies } from "sealious";
import { CRUDRoles } from "../policy-types/roles.js";
+import assert from "assert";
+import TheApp from "../app.js";
export default class Pages extends Collection {
fields = {
@@ -9,8 +11,23 @@ export default class Pages extends Collection {
title: new FieldTypes.Text(),
heading: new FieldTypes.Text(),
description: new FieldTypes.Text(),
- imageForMetadata: new FieldTypes.Image().setRequired(true),
+ imageForMetadata: new FieldTypes.Image(),
hideNavigation: new FieldTypes.Boolean(),
};
policies = CRUDRoles(["pages"], { show: new Policies.Public() });
+
+ async init(app: App, name: string) {
+ assert(app instanceof TheApp);
+ await super.init(app, name);
+ app.on("started", async () => {
+ const all_pages = await app.collections.pages.suList().fetch();
+ if (all_pages.empty) {
+ await app.collections.pages.suCreate({
+ url: "/",
+ content: [],
+ title: "Homepage",
+ });
+ }
+ });
+ }
}
diff --git a/src/back/routes/admin.page.tsx b/src/back/routes/admin.page.tsx
new file mode 100644
index 0000000..4c0c6a1
--- /dev/null
+++ b/src/back/routes/admin.page.tsx
@@ -0,0 +1,36 @@
+import type { Context } from "koa";
+import { TempstreamJSX } from "tempstream";
+import { Page } from "@sealcode/sealgen";
+import html from "src/back/html.js";
+import { button } from "../elements/button.js";
+import { LoginURL, NavbarLinksCRUDURL, PagesCRUDListURL } from "./urls.js";
+
+export const actionName = "Admin";
+
+export default new (class AdminPage extends Page {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ async canAccess(_: Context) {
+ return { canAccess: true, message: "" };
+ }
+
+ async render(ctx: Context) {
+ if (!ctx.$context.user_id) {
+ ctx.redirect(LoginURL);
+ ctx.status = 303;
+ return "";
+ }
+ return html({
+ ctx: ctx,
+ title: "Admin",
+ body: (
+
+ {[
+ { text: "Pages", href: PagesCRUDListURL },
+ { text: "Navbar", href: NavbarLinksCRUDURL },
+ ].map((e) => button({ ...e, variant: "accent1" }))}
+
+ ),
+ description: "",
+ });
+ }
+})();
diff --git a/src/back/routes/index.test.ts b/src/back/routes/admin.test.ts
similarity index 79%
rename from src/back/routes/index.test.ts
rename to src/back/routes/admin.test.ts
index 7b840ab..4fe6fe3 100644
--- a/src/back/routes/index.test.ts
+++ b/src/back/routes/admin.test.ts
@@ -1,23 +1,23 @@
import { withProdApp } from "../test_utils/with-prod-app.js";
import { VERY_LONG_TEST_TIMEOUT, webhintURL } from "../test_utils/webhint.js";
-import { HomeURL } from "./urls.js";
+import { AdminURL } from "./urls.js";
import { getBrowser } from "../test_utils/browser-creator.js";
import type { Browser, BrowserContext, Page } from "@playwright/test";
-describe("Home webhint", () => {
+describe("Admin webhint", () => {
it("doesn't crash", async function () {
return withProdApp(async ({ base_url, rest_api }) => {
- await rest_api.get(HomeURL);
- await webhintURL(base_url + HomeURL);
+ await rest_api.get(AdminURL);
+ await webhintURL(base_url + AdminURL);
// alternatively you can use webhintHTML for faster but less precise scans
// or for scanning responses of requests that use some form of authorization:
- // const response = await rest_api.get(HomeURL);
+ // const response = await rest_api.get(AdminURL);
// await webhintHTML(response);
});
}).timeout(VERY_LONG_TEST_TIMEOUT);
});
-describe("Home", () => {
+describe("Admin", () => {
let page: Page;
let browser: Browser;
let context: BrowserContext;
@@ -34,7 +34,7 @@ describe("Home", () => {
it("works as expected", async function () {
return withProdApp(async ({ base_url }) => {
- await page.goto(base_url + HomeURL);
+ await page.goto(base_url + AdminURL);
});
}).timeout(VERY_LONG_TEST_TIMEOUT);
});
diff --git a/src/back/routes/admin/navbar/index.page.ts b/src/back/routes/admin/navbar/index.page.ts
new file mode 100644
index 0000000..1ef2e59
--- /dev/null
+++ b/src/back/routes/admin/navbar/index.page.ts
@@ -0,0 +1,66 @@
+import { CRUD } from "@sealcode/crud-ui";
+import type { CollectionItem } from "sealious";
+import type { ListFilterRender } from "@sealcode/sealgen";
+import { Controls, DefaultListFilters, Fields } from "@sealcode/sealgen";
+import { NavbarLinks } from "src/back/collections/collections.js";
+import html from "src/back/html.js";
+
+export const actionName = "NavbarLinksCRUD";
+
+const edit_fields = {
+ label: new Fields.CollectionField(
+ NavbarLinks.fields.label.required,
+ NavbarLinks.fields.label
+ ),
+ href: new Fields.CollectionField(
+ NavbarLinks.fields.href.required,
+ NavbarLinks.fields.href
+ ),
+};
+
+const edit_controls = [
+ new Controls.SimpleInput(edit_fields.label, { label: "label" }),
+ new Controls.SimpleInput(edit_fields.href, { label: "href" }),
+];
+
+// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
+const fields_for_display = [
+ { field: "label", label: "label" },
+ { field: "href", label: "href" },
+] as {
+ field: keyof (typeof NavbarLinks)["fields"];
+ label: string;
+ format?: (
+ value: unknown,
+ item: CollectionItem
+ ) => string | Promise;
+}[];
+
+// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
+const fields_for_filters = [
+ { field: "label", ...DefaultListFilters["text"] },
+ { field: "href", ...DefaultListFilters["text"] },
+] as {
+ field: keyof (typeof NavbarLinks)["fields"];
+ render?: ListFilterRender;
+ prepareValue?: (filter_value: unknown) => unknown; // set this function to change what filter value is passed to Sealious
+}[];
+
+export default new CRUD({
+ collection: NavbarLinks,
+ nice_collection_name: "NavbarLinks",
+ fields_for_display,
+ fields_for_filters,
+ html,
+ list_title: "NavbarLinks list",
+ edit_title: "Edit",
+ edit_button_text: "Edit",
+ delete_button_text: "Delete",
+ back_to_list_button_text: "← Back to NavbarLinks list",
+ edit_fields,
+ edit_controls,
+ form_value_to_sealious_value: {},
+ sealious_value_to_form_value: {},
+ post_edit: async () => {},
+ post_create: async () => {},
+});
diff --git a/src/back/routes/admin/pages/[id]/content.jdd-editor.tsx b/src/back/routes/admin/pages/[id]/content.jdd-editor.tsx
index f30fa0a..f82a44d 100644
--- a/src/back/routes/admin/pages/[id]/content.jdd-editor.tsx
+++ b/src/back/routes/admin/pages/[id]/content.jdd-editor.tsx
@@ -7,6 +7,7 @@ import html from "src/back/html.js";
import { registry } from "src/back/jdd-components/registry.js";
import { makeJDDContext } from "src/back/jdd-context.js";
import { defaultHead } from "src/back/defaultHead.js";
+import { PagesCRUDListURL } from "src/back/routes/urls.js";
export const actionName = "EditPageContent";
@@ -23,7 +24,8 @@ export default new (class JDDCreatePreviewPage extends EditJDDField {
const item = await this.getItem(ctx);
return (
-
Edit pages: {item.id}
{" "}
+
← back to pages
+
Edit page: {item.get("title")}
{" "}
);
}
diff --git a/src/back/routes/admin/pages/[id]/edit.form.ts b/src/back/routes/admin/pages/[id]/edit.form.ts
index cce5c7a..9d7d043 100644
--- a/src/back/routes/admin/pages/[id]/edit.form.ts
+++ b/src/back/routes/admin/pages/[id]/edit.form.ts
@@ -48,13 +48,14 @@ export default new (class PagesCRUDEditForm extends Form {
throw new Error("Item with given id not found: " + id);
}
+ const imageForMetadata = item.get("imageForMetadata");
return {
url: item.get("url"),
domain: withFallback(item.get("domain"), ""),
title: withFallback(item.get("title"), ""),
heading: withFallback(item.get("heading"), ""),
description: withFallback(item.get("description"), ""),
- imageForMetadata: { old: item.get("imageForMetadata") },
+ imageForMetadata: imageForMetadata ? { old: imageForMetadata } : "",
hideNavigation: withFallback(
item.get("hideNavigation"),
String(item.get("hideNavigation")),
@@ -79,7 +80,7 @@ export default new (class PagesCRUDEditForm extends Form {
}
const preparedImageForMetadata =
- data.imageForMetadata.new || data.imageForMetadata.old;
+ data.imageForMetadata?.new || data.imageForMetadata?.old;
if (!preparedImageForMetadata) {
throw new Error("Missing field: imageForMetadata");
}
diff --git a/src/back/routes/admin/pages/[id]/edit.jdd-editor.tsx b/src/back/routes/admin/pages/[id]/edit.jdd-editor.tsx
index 88df822..4a92e59 100644
--- a/src/back/routes/admin/pages/[id]/edit.jdd-editor.tsx
+++ b/src/back/routes/admin/pages/[id]/edit.jdd-editor.tsx
@@ -23,7 +23,7 @@ export default new (class JDDCreatePreviewPage extends EditJDDField {
const item = await this.getItem(ctx);
return (
-
Edit pages: {item.id}
{" "}
+ Edit page: {item.get("title")}
{" "}
);
}
diff --git a/src/back/routes/admin/pages/create.form.ts b/src/back/routes/admin/pages/create.form.ts
index 01de0fa..fd085bb 100644
--- a/src/back/routes/admin/pages/create.form.ts
+++ b/src/back/routes/admin/pages/create.form.ts
@@ -32,7 +32,7 @@ export default new (class PagesCRUDCreateForm extends Form
}
const preparedImageForMetadata =
- data.imageForMetadata.new || data.imageForMetadata.old;
+ data.imageForMetadata?.new || data.imageForMetadata?.old;
if (!preparedImageForMetadata) {
throw new Error("Missing field: imageForMetadata");
}
diff --git a/src/back/routes/common/navbar.ts b/src/back/routes/common/navbar.ts
index ec81164..91be1e5 100644
--- a/src/back/routes/common/navbar.ts
+++ b/src/back/routes/common/navbar.ts
@@ -3,14 +3,13 @@ import type { FlatTemplatable } from "tempstream";
import { SignInURL, LogoutURL, PagesCRUDListURL } from "../urls.js";
export async function default_navbar(ctx: BaseContext): Promise {
- const isLoggedIn = !!ctx.$context.session_id;
-
- const linkData = isLoggedIn
- ? [
- { text: "Pages", url: PagesCRUDListURL },
- { text: "Logout", url: LogoutURL },
- ]
- : [{ text: "Sign in", url: SignInURL }];
+ const { items: navbar_items } = await ctx.$app.collections["navbar-links"]
+ .list(ctx.$context)
+ .fetch();
+ const linkData = navbar_items.map((e) => ({
+ text: e.get("label"),
+ url: e.get("href"),
+ }));
const linksHTML = linkData
.map((link) =>
diff --git a/src/back/routes/index.page.tsx b/src/back/routes/index.page.tsx
deleted file mode 100644
index 3d176ea..0000000
--- a/src/back/routes/index.page.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import type { Context } from "koa";
-import { TempstreamJSX } from "tempstream";
-import { Page } from "@sealcode/sealgen";
-import html from "../html.js";
-
-export const actionName = "Home";
-
-export default new (class HomePage extends Page {
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- async canAccess(_: Context) {
- return { canAccess: true, message: "" };
- }
-
- async render(ctx: Context) {
- return html({ ctx: ctx, title: "Home", body: Hello
, description: "" });
- }
-})();
diff --git a/src/back/routes/middlewares/customUrlView.tsx b/src/back/routes/middlewares/customUrlView.tsx
index 8974d3e..6bcfe0a 100644
--- a/src/back/routes/middlewares/customUrlView.tsx
+++ b/src/back/routes/middlewares/customUrlView.tsx
@@ -20,7 +20,9 @@ interface ContextState {
export const customUrlView =
(app: TheApp) => async (ctx: Context & ContextState, next: Next) => {
try {
+ console.log("CUSTOM URL VIEW");
if (ctx.body) return;
+ console.log("doesn't have body");
const main_domain = new URL(BASE_URL).hostname;
const {
items: [page],