fix: dynamic page from payload

This commit is contained in:
RizqiSyahrendra 2025-02-07 10:50:40 +07:00
parent d6d41102a7
commit dc3e009488
14 changed files with 458 additions and 18 deletions

View File

@ -0,0 +1,39 @@
import { BlogDetailContentSkeleton } from "@/components/Blogs/BlogDetail";
import Page from "@/components/Pages/Page";
import Image from "next/image";
import { Suspense } from "react";
export const metadata = {
title: "Page | Cochise Oncology",
description: "Page | Cochise Oncology",
};
export default async function CustomPage({ params }: { params?: Promise<{ pageslug?: string }> }) {
const slug = (await params)?.pageslug;
return (
<>
<Suspense fallback={<Loading />}>
<Page slug={slug} />
</Suspense>
</>
);
}
function Loading() {
return (
<>
<section className="page-section bg-dark-1 bg-gradient-gray-dark-1 light-content bg-scroll overflow-hidden">
{/* <!-- Background Shape --> */}
<div className="bg-shape-1 opacity-003">
<Image src="/assets/images/demo-fancy/bg-shape-1.svg" width={1443} height={844} alt="" />
</div>
{/* <!-- End Background Shape --> */}
</section>
{/* Section */}
<BlogDetailContentSkeleton />
{/* End Section */}
</>
);
}

View File

@ -53,22 +53,6 @@ function Loading() {
<Image src="/assets/images/demo-fancy/bg-shape-1.svg" width={1443} height={844} alt="" />
</div>
{/* <!-- End Background Shape --> */}
<div className="container position-relative pt-sm-40 text-center">
<div className="row">
<div className="col-md-10 offset-md-1 col-lg-8 offset-lg-2">
<h1 className="hs-title-10 mb-10 wow fadeInUp">...</h1>
{/* Author, Categories, Comments */}
<div className="blog-item-data mb-0 wow fadeIn" data-wow-delay="0.2s">
<div className="flex justify-center items-center">
<i className="mi-clock mr-2" />
<a href="#">...</a>
</div>
</div>
{/* End Author, Categories, Comments */}
</div>
</div>
</div>
</section>
{/* Section */}

View File

@ -0,0 +1,27 @@
import { Block } from "payload";
export const BeforeFooterBlock: Block = {
slug: "beforeFooterBlock",
fields: [
{
name: "title",
type: "text",
required: true,
defaultValue: "Begin your path to healing with Cochise Oncology",
},
{
name: "description",
type: "textarea",
required: true,
defaultValue:
"Our dedicated team in Sierra Vista, AZ is here to support you with hope, strength, and courage. We offer personalized cancer care using innovative treatments in our state-of-the-art facility. Take the first step towards comprehensive, patient-focused treatment by scheduling a consultation. Let us listen to your needs, answer your questions, and create a tailored plan for your journey. Fill out our form to connect with our compassionate experts and discover how Cochise Oncology can stand with you in your fight against cancer.",
},
{
type: "text",
name: "buttonText",
label: "CTA Button Text",
required: true,
defaultValue: "Get Started",
},
],
};

14
src/blocks/Content.ts Normal file
View File

@ -0,0 +1,14 @@
import { lexicalEditor } from "@payloadcms/richtext-lexical";
import { Block } from "payload";
export const ContentBlock: Block = {
slug: "contentBlock",
fields: [
{
name: "content",
type: "richText",
editor: lexicalEditor({}),
required: true,
},
],
};

62
src/collections/Pages.ts Normal file
View File

@ -0,0 +1,62 @@
import { BeforeFooterBlock } from "@/blocks/BeforeFooter";
import { ContentBlock } from "@/blocks/Content";
import formatSlug from "@/utils/formatSlug";
import { CollectionConfig } from "payload";
export const Pages: CollectionConfig = {
slug: "pages",
fields: [
{
name: "title",
label: "Page Title",
type: "text",
required: true,
},
{
name: "hero_img",
label: "Hero Image",
type: "upload",
relationTo: "media",
},
{
name: "slug",
label: "Page Slug",
type: "text",
admin: {
position: "sidebar",
},
hooks: {
beforeValidate: [formatSlug("title")],
},
},
{
name: "layout",
label: "Page Layout",
type: "blocks",
minRows: 1,
blocks: [ContentBlock, BeforeFooterBlock],
},
{
name: "meta",
label: "Page Meta",
type: "group",
fields: [
{
name: "title",
label: "Title",
type: "text",
},
{
name: "description",
label: "Description",
type: "textarea",
},
{
name: "keywords",
label: "Keywords",
type: "text",
},
],
},
],
};

View File

@ -0,0 +1,29 @@
import Link from "next/link";
export interface BeforeFooterBlockProps {
id: string;
title: string;
description: string;
buttonText: string;
}
export function BeforeFooterBlock({ title, description, buttonText }: BeforeFooterBlockProps) {
return (
<section
className={`page-section text-white text-center scrollSpysection bg-dark-1 light-content bg-scroll`}
id="about"
>
<div className="container mx-auto px-6">
<h2 className="text-3xl font-bold mb-4">{title}</h2>
<p className="text-lg mb-6">{description}</p>
{!!buttonText && (
<div className="pt-5">
<Link href="/contact" className="bg-purple-600 hover:bg-purple-700 text-white py-3 px-6 rounded-lg text-lg">
{buttonText}
</Link>
</div>
)}
</div>
</section>
);
}

View File

@ -0,0 +1,26 @@
import { RichText } from "@payloadcms/richtext-lexical/react";
// type Props = extract
export function ContentBlock(props: any) {
return (
<div className="container relative">
<div className="row">
{/* Content */}
<div className="col-md-10 offset-md-1 col-lg-8 offset-lg-2">
{/* Post */}
<div className="blog-item mb-80 mb-xs-40">
<div className="blog-item-body">
<div>
{/* @ts-ignore */}
<RichText data={props.content} />
</div>
</div>
</div>
{/* End Post */}
</div>
{/* End Content */}
</div>
</div>
);
}

View File

@ -0,0 +1,44 @@
import React, { Fragment } from "react";
import type { Page } from "@/payload-types";
import { ContentBlock } from "./Content";
import { BeforeFooterBlock } from "./BeforeFooter";
const blockComponents = {
contentBlock: ContentBlock,
beforeFooterBlock: BeforeFooterBlock,
};
export const RenderBlocks: React.FC<{
blocks: Page["layout"];
}> = (props) => {
const { blocks } = props;
const hasBlocks = blocks && Array.isArray(blocks) && blocks.length > 0;
if (hasBlocks) {
return (
<Fragment>
{blocks.map((block, index) => {
const { blockType } = block;
if (blockType && blockType in blockComponents) {
const Block = blockComponents[blockType];
if (Block) {
return (
<div className="mt-16" key={index}>
{/* @ts-ignore */}
<Block {...block} disableInnerContainer />
</div>
);
}
}
return null;
})}
</Fragment>
);
}
return null;
};

View File

@ -0,0 +1,58 @@
import { RenderBlocks } from "@/components/Blocks/RenderBlocks";
import { fetchPageBySlug } from "@/services/payload/page";
import Image from "next/image";
import { notFound } from "next/navigation";
export interface PageProps {
slug: string | undefined;
}
export default async function Page({ slug }: PageProps) {
const page = await fetchPageBySlug({ slug });
if (!page) {
return notFound();
}
return (
<>
<section className="page-section bg-dark-1 bg-gradient-gray-dark-1 light-content bg-scroll overflow-hidden">
{/* <!-- Background Shape --> */}
{!!page.heroImg?.url && (
<div className="absolute top-0 left-0 w-full h-full opacity-20">
<Image
src={page.heroImg.url}
width="0"
height="0"
sizes="100vw"
className="w-full"
alt={page.heroImg.alt}
/>
</div>
)}
{!page?.heroImg?.url && (
<div className="absolute top-0 left-0 w-full h-full opacity-003">
<Image
src="/assets/images/demo-fancy/bg-shape-1.svg"
width="0"
height="0"
sizes="100vw"
className="w-full"
alt=""
/>
</div>
)}
{/* <!-- End Background Shape --> */}
<div className="container position-relative pt-sm-40 text-center">
<div className="row">
<div className="col-md-10 offset-md-1 col-lg-8 offset-lg-2">
<h1 className="hs-title-10 mb-10 wow fadeInUp">{page.title}</h1>
</div>
</div>
</div>
</section>
<RenderBlocks blocks={page.layout} />
</>
);
}

View File

@ -4,7 +4,7 @@ export const navMenuData = [
href: "#",
text: "About",
child: [
{ href: "/slick-about-dark", text: "Our Oncology Center" },
{ href: "/our-oncology-center", text: "Our Oncology Center" },
{ href: "/our-staff", text: "Our Staff" },
{ href: "/announcements", text: "Announcements" },
],

View File

@ -14,6 +14,7 @@ export interface Config {
users: User;
media: Media;
blogs: Blog;
pages: Page;
forms: Form;
'form-submissions': FormSubmission;
'payload-locked-documents': PayloadLockedDocument;
@ -25,6 +26,7 @@ export interface Config {
users: UsersSelect<false> | UsersSelect<true>;
media: MediaSelect<false> | MediaSelect<true>;
blogs: BlogsSelect<false> | BlogsSelect<true>;
pages: PagesSelect<false> | PagesSelect<true>;
forms: FormsSelect<false> | FormsSelect<true>;
'form-submissions': FormSubmissionsSelect<false> | FormSubmissionsSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
@ -127,6 +129,55 @@ export interface Blog {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "pages".
*/
export interface Page {
id: number;
title: string;
hero_img?: (number | null) | Media;
slug?: string | null;
layout?:
| (
| {
content: {
root: {
type: string;
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
};
id?: string | null;
blockName?: string | null;
blockType: 'contentBlock';
}
| {
title: string;
description: string;
buttonText: string;
id?: string | null;
blockName?: string | null;
blockType: 'beforeFooterBlock';
}
)[]
| null;
meta?: {
title?: string | null;
description?: string | null;
keywords?: string | null;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "forms".
@ -316,6 +367,10 @@ export interface PayloadLockedDocument {
relationTo: 'blogs';
value: number | Blog;
} | null)
| ({
relationTo: 'pages';
value: number | Page;
} | null)
| ({
relationTo: 'forms';
value: number | Form;
@ -412,6 +467,44 @@ export interface BlogsSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "pages_select".
*/
export interface PagesSelect<T extends boolean = true> {
title?: T;
hero_img?: T;
slug?: T;
layout?:
| T
| {
contentBlock?:
| T
| {
content?: T;
id?: T;
blockName?: T;
};
beforeFooterBlock?:
| T
| {
title?: T;
description?: T;
buttonText?: T;
id?: T;
blockName?: T;
};
};
meta?:
| T
| {
title?: T;
description?: T;
keywords?: T;
};
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "forms_select".

View File

@ -10,6 +10,7 @@ import { fileURLToPath } from "url";
import { Blogs } from "@/collections/Blogs";
import { Media } from "@/collections/Media";
import { Pages } from "@/collections/Pages";
import { Users } from "@/collections/Users";
import {
BoldFeature,
@ -45,7 +46,7 @@ export default buildConfig({
},
theme: "dark",
},
collections: [Users, Media, Blogs],
collections: [Users, Media, Blogs, Pages],
secret: process.env.PAYLOAD_SECRET || "",
typescript: {
outputFile: path.resolve(dirname, "payload-types.ts"),

View File

@ -0,0 +1,39 @@
import payloadConfig from "@/payload.config";
import { draftMode } from "next/headers";
import { getPayload } from "payload";
export const fetchPageBySlug = async ({ slug }: { slug: string | undefined }) => {
const { isEnabled: draft } = await draftMode();
const payload = await getPayload({ config: payloadConfig });
const result = await payload.find({
collection: "pages",
// draft,
limit: 1,
pagination: false,
// overrideAccess: draft,
where: {
slug: {
equals: slug,
},
},
});
if (!result.docs?.[0]) {
return null;
}
const data = result.docs[0];
const heroImgUrl = typeof data.hero_img !== "number" ? (data?.hero_img?.url ?? "") : "";
const heroImgAlt = typeof data.hero_img !== "number" ? (data?.hero_img?.alt ?? "") : "";
return {
...data,
heroImg: {
url: heroImgUrl,
alt: heroImgAlt,
},
};
};

24
src/utils/formatSlug.ts Normal file
View File

@ -0,0 +1,24 @@
import { FieldHook } from "payload";
const format = (val: string): string =>
val
.replace(/ /g, "-")
.replace(/[^\w-/]+/g, "")
.toLowerCase();
const formatSlug =
(fallback: string): FieldHook =>
({ value, originalDoc, data }) => {
if (typeof value === "string") {
return format(value);
}
const fallbackData = (data && data[fallback]) || (originalDoc && originalDoc[fallback]);
if (fallbackData && typeof fallbackData === "string") {
return format(fallbackData);
}
return value;
};
export default formatSlug;