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

PCC-1729, PCC-1730 Implement new author pages and author header on articles for the starter kits #328

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
1,239 changes: 880 additions & 359 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions starters/nextjs-starter-approuter-ts/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,9 @@ out
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

# playwright tests
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
118 changes: 118 additions & 0 deletions starters/nextjs-starter-approuter-ts/app/authors/[author]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { PCCConvenienceFunctions } from "@pantheon-systems/pcc-react-sdk/server";
import Image from "next/image";
import {
FaFacebookSquare,
FaInstagramSquare,
FaLinkedin,
} from "react-icons/fa";
import { FaSquareXTwitter } from "react-icons/fa6";
import { MdEmail } from "react-icons/md";
import { PiMediumLogoFill } from "react-icons/pi";
import ArticleList from "../../../components/article-list";
import Layout from "../../../components/layout";
import { PAGE_SIZE } from "../../../constants";

function fetchNextPages(author?: string | null | undefined) {
return async (cursor?: string | null | undefined) => {
"use server";
const { data, cursor: newCursor } =
await PCCConvenienceFunctions.getPaginatedArticles({
pageSize: PAGE_SIZE,
metadataFilters:
author == null
? undefined
: {
author,
},
cursor: cursor || undefined,
});

return {
data,
newCursor,
};
};
}

export default async function ArticlesListTemplate({
params,
}: {
params: { author: string };
}) {
const author = params.author ? decodeURIComponent(params.author) : undefined;

const {
data: articles,
cursor,
totalCount,
} = await PCCConvenienceFunctions.getPaginatedArticles({
pageSize: PAGE_SIZE,
metadataFilters: {
author,
},
});

if (totalCount === 0) {
return (
<Layout>
<section className="max-w-screen-3xl mx-auto px-4 pt-16 sm:w-4/5 md:w-3/4 lg:w-4/5 2xl:w-3/4">
<div>Could not find any articles by {author}</div>
</section>
</Layout>
);
}

return (
<Layout>
<ArticleList
articles={articles}
cursor={cursor}
totalCount={totalCount}
fetcher={fetchNextPages(author)}
additionalHeader={
<div
className="border-base-300 mb-14 border-b-[1px] pb-7"
data-testid="author-header"
>
<div className="flex flex-row gap-x-6">
<div>
<Image
className="m-0 rounded-full"
src="/images/no-avatar.png"
width={90}
height={90}
alt={`Avatar of ${author}`}
/>
</div>
<div className="flex flex-col justify-between">
<h1 className="text-5xl font-bold capitalize">{author}</h1>
<div>A short line about the author</div>
</div>
</div>
<div className="my-8 flex flex-row gap-x-3">
<FaLinkedin className="h-7 w-7" fill="#404040" />
<FaSquareXTwitter className="h-7 w-7" fill="#404040" />
<PiMediumLogoFill className="h-7 w-7" fill="#404040" />
<FaFacebookSquare className="h-7 w-7" fill="#404040" />
<FaInstagramSquare className="h-7 w-7" fill="#404040" />
<MdEmail className="h-7 w-7" fill="#404040" />
</div>
<div>
{author} is a passionate content writer with a flair for turning
ideas into engaging stories. When she’s not writing, Jane enjoys
cozy afternoons with a good book, exploring new coffee spots, and
finding inspiration in everyday moments.
</div>
</div>
}
/>
</Layout>
);
}

export function generateMetadata() {
return {
title: "Decoupled Next PCC Demo",
description: "Articles by Author",
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import PageHeader from "./page-header";
import Pagination from "./pagination";

interface Props {
headerText: string;
headerText?: string | null | undefined;
articles: PaginatedArticle[] | ArticleWithoutContent[];
totalCount: number;
cursor: string;
Expand Down Expand Up @@ -44,7 +44,7 @@ export default function ArticleList({

return (
<section className="max-w-screen-3xl mx-auto px-4 pt-16 sm:w-4/5 md:w-3/4 lg:w-4/5 2xl:w-3/4">
<PageHeader title={headerText} />
{headerText ? <PageHeader title={headerText} /> : null}
{additionalHeader}
<ArticleGrid articles={currentArticles as ArticleWithoutContent[]} />
<div className="mt-4 flex flex-row items-center justify-center">
Expand Down
170 changes: 105 additions & 65 deletions starters/nextjs-starter-approuter-ts/components/article-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import {
ArticleRenderer,
useArticleTitle,
} from "@pantheon-systems/pcc-react-sdk/components";
import { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
import React from "react";
import { getSeoMetadata } from "../lib/utils";
import { clientSmartComponentMap } from "./smart-components/client-components";

const ELEMENT_STYLES_TO_OVERRIDE = [
Expand All @@ -17,120 +21,156 @@ const ELEMENT_STYLES_TO_OVERRIDE = [
/lineHeight/,
/height/,
];

const overrideElementStyles = (tag: keyof HTMLElementTagNameMap) => {
function resultFunc({ children, id, style, ...attrs }: any) {
const newStyles = { ...style };

ELEMENT_STYLES_TO_OVERRIDE.forEach((s) => {
Object.keys(newStyles).forEach((key) => {
if (s.test(key)) delete newStyles[key];
});
});

return React.createElement(
tag,
{ id, style: newStyles, ...attrs },
children,
);
}

return resultFunc;
};

const componentOverrideMap = {
h1: overrideElementStyles("h1"),
h2: overrideElementStyles("h2"),
h3: overrideElementStyles("h3"),
h4: overrideElementStyles("h4"),
h5: overrideElementStyles("h5"),
h6: overrideElementStyles("h6"),
p: overrideElementStyles("p"),
span: overrideElementStyles("span"),
};

type ArticleViewProps = {
article: Article;
onlyContent?: boolean;
};

export default function ArticleView({
const ArticleHeader = ({
article,
onlyContent,
}: ArticleViewProps) {
const { data } = useArticle(
article.id,
{
publishingLevel: article.publishingLevel,
contentType: "TREE_PANTHEON_V2",
},
{
skip: article.publishingLevel !== "REALTIME",
},
);

const hydratedArticle = data?.article ?? article;
const title = useArticleTitle(hydratedArticle);
articleTitle,
seoMetadata,
}: {
article: Article;
articleTitle: string;
seoMetadata: Metadata;
}) => {
const author = Array.isArray(seoMetadata.authors)
? seoMetadata.authors[0]
: seoMetadata.authors;

return (
<>
<div>
<h1 className="text-5xl font-bold">{title}</h1>
<div>
<div className="text-5xl font-bold">{articleTitle}</div>
<div className="border-y-base-300 text-neutral-content mb-14 mt-6 flex w-full flex-row gap-x-4 border-y-[1px] py-4">
{author?.name ? (
<>
<div>
<Link
data-testid="author"
className="flex flex-row items-center gap-x-2 font-thin uppercase text-black no-underline"
href={`/authors/${author?.name}`}
>
<div>
<Image
className="m-0 rounded-full"
src="/images/no-avatar.png"
width={24}
height={24}
alt={`Avatar of ${author?.name}`}
/>
</div>
<div className="underline">{author?.name}</div>
</Link>
</div>
<div className="h-full w-[1px] bg-[#e5e7eb]">&nbsp;</div>
</>
) : null}
{article.updatedAt ? (
<p className="py-2">
<span>
{new Date(article.updatedAt).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})}
</p>
</span>
) : null}
</div>
<ArticleRenderer
article={hydratedArticle}
componentMap={{
h1: overrideElementStyles("h1"),
h2: overrideElementStyles("h2"),
h3: overrideElementStyles("h3"),
h4: overrideElementStyles("h4"),
h5: overrideElementStyles("h5"),
h6: overrideElementStyles("h6"),
p: overrideElementStyles("p"),
span: overrideElementStyles("span"),
}}
smartComponentMap={clientSmartComponentMap}
__experimentalFlags={{
disableAllStyles: !!onlyContent,
preserveImageStyles: true,
useUnintrusiveTitleRendering: true,
}}
/>
</>
</div>
);
}
};

export function StaticArticleView({ article, onlyContent }: ArticleViewProps) {
const articleTitle = useArticleTitle(article);
const seoMetadata = getSeoMetadata(article);

return (
<>
<div>
<div className="text-5xl font-bold">{articleTitle}</div>

{article.updatedAt ? (
<p className="py-2">
{new Date(article.updatedAt).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})}
</p>
) : null}
</div>
<ArticleHeader
article={article}
articleTitle={articleTitle || ""}
seoMetadata={seoMetadata}
/>
<ArticleRenderer
article={article}
componentMap={{
h1: overrideElementStyles("h1"),
h2: overrideElementStyles("h2"),
h3: overrideElementStyles("h3"),
h4: overrideElementStyles("h4"),
h5: overrideElementStyles("h5"),
h6: overrideElementStyles("h6"),
p: overrideElementStyles("p"),
span: overrideElementStyles("span"),
}}
componentMap={componentOverrideMap}
smartComponentMap={clientSmartComponentMap}
__experimentalFlags={{
disableAllStyles: !!onlyContent,
preserveImageStyles: true,
useUnintrusiveTitleRendering: true,
}}
/>

<div className="border-base-300 mt-16 flex w-full gap-x-3 border-t-[1px] pt-7 lg:mt-32">
{seoMetadata.keywords != null
? (Array.isArray(seoMetadata.keywords)
? seoMetadata.keywords
: [seoMetadata.keywords]
).map((x, i) => (
<div
key={i}
className="text-bold text-neutral-content rounded-full border-[1px] border-[#d4d4d4] bg-[#F5F5F5] px-3 py-1 text-sm !no-underline"
>
{x}
</div>
))
: null}
</div>
</>
);
}

export default function ArticleView({
article,
onlyContent,
}: ArticleViewProps) {
const { data } = useArticle(
article.id,
{
publishingLevel: article.publishingLevel,
contentType: "TREE_PANTHEON_V2",
},
{
skip: article.publishingLevel !== "REALTIME",
},
);

const hydratedArticle = data?.article ?? article;

return (
<StaticArticleView article={hydratedArticle} onlyContent={onlyContent} />
);
}
Loading
Loading