Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add generic page #129

Merged
merged 4 commits into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/cms/src/collections/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ export const Pages: CollectionConfig = {
required: true,
localized: true,
},
{
name: "description",
maxLength: 360,
type: "textarea",
localized: true,
required: true,
},
{
name: "content",
type: "richText",
Expand Down
11 changes: 9 additions & 2 deletions apps/cms/src/payload.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import path from "path";
import { webpackBundler } from "@payloadcms/bundler-webpack";
import { mongooseAdapter } from "@payloadcms/db-mongodb";
import { lexicalEditor } from "@payloadcms/richtext-lexical";
import { HeadingFeature, lexicalEditor } from "@payloadcms/richtext-lexical";
import type { Config } from "@tietokilta/cms-types/payload";
import { oAuthPlugin } from "payload-plugin-oauth";
import { buildConfig } from "payload/config";
Expand Down Expand Up @@ -76,7 +76,6 @@ export default buildConfig({
// @ts-expect-error DATABASE_URL is validated by payload on start
url: process.env.PAYLOAD_DATABASE_URL,
}),
editor: lexicalEditor({}),
plugins: [
oAuthPlugin({
databaseUri: MONGODB_URI ?? "",
Expand Down Expand Up @@ -137,4 +136,12 @@ export default buildConfig({
},
}),
],
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
HeadingFeature({
enabledHeadingSizes: ["h2", "h3"],
}),
],
}),
});
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
},
"devDependencies": {
"@next/eslint-plugin-next": "^14.0.4",
"@tailwindcss/typography": "^0.5.10",
"@tietokilta/cms-types": "workspace:*",
"@tietokilta/config-typescript": "workspace:*",
"@tietokilta/eslint-config": "workspace:*",
Expand Down
54 changes: 49 additions & 5 deletions apps/web/src/app/[lang]/[...path]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { AdminBar } from "../../../components/admin-bar";
import { LexicalSerializer } from "../../../components/lexical/lexical-serializer";
import type { SerializedLexicalEditorState } from "../../../components/lexical/types";
import { TableOfContents } from "../../../components/table-of-contents";
import { fetchPage } from "../../../lib/api/pages";
import type { Locale } from "../../../lib/dictionaries";

interface NextPage<Params extends Record<string, unknown>> {
params: Params;
searchParams: Record<string, string | string[] | undefined>;
}

const Page = async ({
params: { path, lang },
}: NextPage<{ path: string[]; lang: string }>) => {
type Props = NextPage<{ path: string[]; lang: Locale }>;

const getPage = async (path: string[], lang: Locale) => {
if (path.length !== 1 && path.length !== 2) {
return notFound();
}
Expand All @@ -26,15 +29,56 @@ const Page = async ({

if (!page) return notFound();

return page;
};

export const generateMetadata = async ({
params: { path, lang },
}: Props): Promise<Metadata> => {
const page = await getPage(path, lang);

return {
title: page.title,
description: page.description,
};
};

function Content({
content,
}: {
content: SerializedLexicalEditorState | undefined;
}) {
if (!content) return null;

return (
<article className="prose prose-headings:scroll-mt-40 prose-headings:font-mono prose-headings:xl:scroll-mt-24 max-w-prose">
<LexicalSerializer nodes={content.root.children} />
</article>
);
}

const Page = async ({ params: { path, lang } }: Props) => {
const page = await getPage(path, lang);
const content = page.content as unknown as
| SerializedLexicalEditorState
| undefined;

return (
<>
<AdminBar collection="pages" id={page.id} />
<h1>{page.title}</h1>
{content ? <LexicalSerializer nodes={content.root.children} /> : null}
<main className="relative mb-8 flex flex-col items-center gap-2 md:gap-6">
<header className="flex h-[15svh] w-full items-center justify-center bg-gray-900 text-gray-100 md:h-[25svh]">
<h1 className="font-mono text-4xl md:text-5xl">{page.title}</h1>
</header>

<div className="relative m-auto flex max-w-prose flex-col gap-8 p-4 md:p-6">
<TableOfContents content={content} />
<p className="shadow-solid max-w-prose rounded-md border-2 border-gray-900 p-4 md:p-6">
{page.description}
</p>
<Content content={content} />
</div>
</main>
</>
);
};
Expand Down
38 changes: 27 additions & 11 deletions apps/web/src/app/[lang]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import { Inter, Roboto_Mono } from "next/font/google";
import { Footer } from "../../components/footer";
import { MainNav } from "../../components/main-nav";
import { MobileNav } from "../../components/mobile-nav";
import { cn } from "../../lib/utils";
import { getDictionary, type Locale } from "../../lib/dictionaries";
import { cn } from "../../lib/utils";
import "./globals.css";

const inter = Inter({ subsets: ["latin"] });
const robotoMono = Roboto_Mono({ subsets: ["latin"] });
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
const robotoMono = Roboto_Mono({
subsets: ["latin"],
variable: "--font-roboto-mono",
});

interface LayoutProps {
params: {
Expand All @@ -19,18 +22,28 @@ interface LayoutProps {

const localizedMetadata = {
fi: {
title: "Tietokilta",
title: {
template: "%s - Tietokilta",
default: "Tietokilta",
},
description: "Tietokilta ry:n kotisivut",
},
en: {
title: "Computer Science Guild",
title: {
template: "%s - Computer Science Guild",
default: "Computer Science Guild",
},
description: "Homepage of the Computer Science Guild",
},
} as const;

export const generateMetadata = ({ params: { lang } }: LayoutProps): Metadata =>
export const generateMetadata = ({
params: { lang },
}: LayoutProps): Metadata => ({
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- false positive
localizedMetadata[lang] ?? localizedMetadata.fi;
...(localizedMetadata[lang] || localizedMetadata.fi),
metadataBase: new URL("https://tietokilta.fi"),
});

export default async function RootLayout({
children,
Expand All @@ -42,15 +55,18 @@ export default async function RootLayout({

return (
<html lang={lang}>
<body className={cn(inter.className, robotoMono.className)}>
<body className={cn(inter.variable, robotoMono.variable, "font-sans")}>
<div className="flex min-h-screen flex-col">
<MobileNav
className="sticky top-0 md:hidden"
className="sticky top-0 z-10 md:hidden"
dictionary={dictionary}
locale={lang}
/>
<MainNav className="sticky top-0 hidden md:block" locale={lang} />
<div className="flex-1">{children}</div>
<MainNav
className="sticky top-0 z-10 hidden h-20 md:block"
locale={lang}
/>
<div className="min-h-screen flex-1">{children}</div>
<Footer locale={lang} />
</div>
</body>
Expand Down
59 changes: 46 additions & 13 deletions apps/web/src/components/lexical/lexical-serializer.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/* eslint-disable react/no-array-index-key -- okay here */
/* eslint-disable no-bitwise -- lexical nodes are defined bitwise */

import type { Config } from "@tietokilta/cms-types/payload";
import Link from "next/link";
import { Fragment } from "react";
import { lexicalNodeToTextContent } from "../../lib/utils";
import {
IS_BOLD,
IS_CODE,
Expand Down Expand Up @@ -97,7 +98,17 @@ export function LexicalSerializer({
"h1" | "h2" | "h3" | "h4" | "h5" | "h6"
>;
const Tag = node.tag as Heading;
return <Tag key={index}>{serializedChildren}</Tag>;

return (
<Tag
id={lexicalNodeToTextContent(node)
.toLocaleLowerCase()
.replace(/\s/g, "-")}
key={index}
>
{serializedChildren}
</Tag>
);
}
case "list": {
type List = Extract<keyof JSX.IntrinsicElements, "ul" | "ol">;
Expand Down Expand Up @@ -137,8 +148,8 @@ export function LexicalSerializer({
return <blockquote key={index}>{serializedChildren}</blockquote>;
}
case "link": {
const attributes = node.attributes as {
doc?: { data: { path?: string } };
const fields = node.fields as {
doc?: { value: { path?: string } };
linkType?: "custom" | "internal";
newTab?: boolean;
nofollow?: boolean;
Expand All @@ -147,26 +158,26 @@ export function LexicalSerializer({
url?: string;
};

if (attributes.linkType === "custom") {
const rel = `${attributes.rel ?? ""} ${
attributes.nofollow ? " nofollow" : ""
if (fields.linkType === "custom") {
const rel = `${fields.rel ?? ""} ${
fields.nofollow ? " nofollow" : ""
}`;
return (
<a
href={attributes.url}
href={fields.url}
key={index}
rel={rel}
target={attributes.newTab ? "_blank" : undefined}
target={fields.newTab ? "_blank" : undefined}
>
{serializedChildren}
</a>
);
} else if (attributes.linkType === "internal") {
} else if (fields.linkType === "internal") {
return (
<Link
href={attributes.doc?.data.path ?? "#no-path"}
href={fields.doc?.value.path ?? "#no-path"}
key={index}
target={attributes.newTab ? "_blank" : undefined}
target={fields.newTab ? "_blank" : undefined}
>
{serializedChildren}
</Link>
Expand All @@ -185,7 +196,7 @@ export function LexicalSerializer({
);
}
case "upload": {
const data = node.data as {
const data = node.value as {
url: string;
width: number;
height: number;
Expand Down Expand Up @@ -223,6 +234,9 @@ export function LexicalSerializer({
case "mark": {
return <Fragment key={index}>{serializedChildren}</Fragment>;
}
case "relationship": {
return <Relationship node={node as unknown as RelationshipNode} />;
}
default:
// eslint-disable-next-line no-console -- Nice to know if something is missing
console.warn("Unknown node:", node);
Expand All @@ -234,3 +248,22 @@ export function LexicalSerializer({
</>
);
}

interface RelationshipNode {
type: "relationship";
relationTo: keyof Omit<
Config["collections"],
"payload-preferences" | "payloda-migrations"
>;
value: Record<string, unknown>;
}

function Relationship({ node }: { node: RelationshipNode }) {
switch (node.relationTo) {
// TODO: Implement these
default: {
console.warn("Unknown relationTo:", node.relationTo);
return null;
}
}
}
2 changes: 1 addition & 1 deletion apps/web/src/components/mobile-nav/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export async function MobileNav({
return (
<header
className={cn(
"flex items-center justify-between bg-gray-900 p-2 text-gray-100 sm:p-4",
"flex items-center justify-between bg-gray-900 p-2 text-gray-100",
className,
)}
{...rest}
Expand Down
Loading