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

Implement category related content page #63

Merged
merged 12 commits into from
Oct 25, 2024
Merged
12 changes: 8 additions & 4 deletions src/app/(pages)/authors/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { fetchContentFromAuthor } from '@/app/_utilities/contentFetchers'
import AuthorContentGrid from '../../../_blocks/AuthorContentGrid'
import { fetchContentFromAuthorOrCategory } from '@/app/_utilities/contentFetchers'
import ContentGrid from '../../../_blocks/ContentGrid'
import { Subscribe } from '../../../_blocks/Subscribe'
import AuthorSummary from '../../../_components/AuthorSummary'
import BackButton from '../../../_components/BackButton'
Expand All @@ -22,7 +22,11 @@ const headerStyle = {
export default async function ContributorPage({ params: paramsPromise }) {
const { slug } = await paramsPromise
const author = await fetchContentBySlug({ slug: slug, type: 'authors' })
const contentFromAuthor = await fetchContentFromAuthor(author)

// Given payload typing structure an error is expected as we're passing multiple possible types
// to a function that only accepts 2 (Authors | Categories).
// @ts-expect-error
const contentFromAuthor = await fetchContentFromAuthorOrCategory({ type: 'author', target: author })

return (
<div>
Expand All @@ -31,7 +35,7 @@ export default async function ContributorPage({ params: paramsPromise }) {
<BackButton className={styles.backButton} />
<AuthorSummary author={author} />
</div>
<AuthorContentGrid content={contentFromAuthor} />
<ContentGrid content={contentFromAuthor} />
<Subscribe />

</div>
Expand Down
54 changes: 54 additions & 0 deletions src/app/(pages)/categories/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { fetchAllContentByType, fetchContentBySlug, fetchContentFromAuthorOrCategory } from '@/app/_utilities/contentFetchers';
import ContentGrid from '@/app/_blocks/ContentGrid';
import { Header } from '@/app/_components/Header';
import BackButton from '@/app/_components/BackButton';
import { Category } from '@/payload-types';
import { Subscribe } from '@/app/_blocks/Subscribe';
import CategoryPill from '@/app/_components/CategoryPill';
import styles from './styles.module.css';

const headerStyle = {
'--dynamic-background': 'var(--sub-blue-100)',
'--dynamic-color': 'var(--dark-rock-800)',
'--dynamic-width': 'calc(100% - 40px)',
};

export default async function CategoryPage({ params: paramsPromise }) {
const { slug } = await paramsPromise;

const category = await fetchContentBySlug({
slug: slug,
type: 'categories',
}) as Category;

const content = await fetchContentFromAuthorOrCategory({ type: 'category', target: category });

const otherCategories = await fetchAllContentByType('categories').then((res: Category[]) => res.map((item: Category) => item.title));

return (
<div>
<Header style={headerStyle} />

{/* Head block*/}
<div className={styles.headBlock}>
<BackButton />
<h4> {category.title} </h4>
</div>

{/* Content Grid */}
<div className={styles.contentContainer}>
<ContentGrid content={content} showCount={false}/>
<div className={styles.otherCategoriesContainer}>
<p>Recommended categories</p>
<div className={styles.otherCategories}>
{otherCategories.map(category => (
<CategoryPill title={category} />
))}
</div>
</div>
</div>

<Subscribe />
</div>
);
}
52 changes: 52 additions & 0 deletions src/app/(pages)/categories/[slug]/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
.headBlock {
background: var(--sub-blue-100);
width: 100%;
display: flex;
flex-direction: column;
gap: 30px;
padding: 30px 16px;
border-radius: 0 0 16px 16px;
}

.contentContainer {
display: flex;
flex-direction: column;
gap: 20px;
}

.otherCategoriesContainer {
padding: 16px;
font-weight: bold;

}

.otherCategories {
display: flex;
flex-wrap: wrap;
gap: 20px;
padding-top: 20px;
}

@media (min-width: 1024px) {
.headBlock {
background: var(--sub-blue-100);
width: calc(100% - 39px);
gap: 30px;
margin: auto;
padding: 40px 100px;
border-radius: 0 0 45px 45px;
}

.contentContainer {
display: grid;
grid-template-columns: 0.7fr 0.3fr;
padding: 60px;
}

.otherCategories {
display: flex;
flex-wrap: wrap;
margin-top: 20px;
gap: 20px;
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
import {
Blogpost,
CaseStudy,
Podcast,
TalksAndRoundtable,
} from "@/payload-types";
import ContentCard from "../../_components/ContentCard";
import { calculateTotalArticles } from "../../_utilities/calculateTotalArticles";
import styles from "./styles.module.css";

export default function AuthorContentGrid({ content }) {
export default function ContentGrid({ content, showCount = true }) {

return (
<div className={styles.gridContainer}>
<div className={styles.articleCounter}>
<b>{calculateTotalArticles(content)}</b> Articles
</div>

{showCount && (
<div className={styles.articleCounter}>
<b>{calculateTotalArticles(content)}</b> Articles
</div>
)}
<div className={styles.contentGrid}>
{Object.keys(content).map(key =>
content[key].map((contentPiece, i) => (
<ContentCard contentType={key} content={contentPiece} rounded/>
<ContentCard contentType={key} content={contentPiece} rounded />
)),
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
.contentGrid {
display: grid;
grid-template-columns: repeat(auto-fill, 1fr);
margin: 0 auto;
padding-left: 40px;
padding-right: 40px;
margin: auto;
padding: 16px;
gap: 16px;
}

.contentCard {
Expand Down
62 changes: 44 additions & 18 deletions src/app/_components/CategoryPill/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,65 @@
import styles from "./styles.module.css";
import { CloseIcon } from "@/app/_icons/icons";
import React, { useState } from "react";
import Link from "next/link";
import { toKebabCase } from "@/app/_utilities/toKebabCase";

interface CategoryPillProps {
title: string,
id?: string,
selected?: boolean,
enableLink?: boolean,
setActiveFilter?: React.Dispatch<React.SetStateAction<boolean>>
setActiveCategory?: React.Dispatch<React.SetStateAction<string>>
}

export default function CategoryPill({ title, id, selected = false, setActiveFilter, setActiveCategory }: CategoryPillProps) {
export default function CategoryPill({ title, id, selected = false, setActiveFilter, setActiveCategory, enableLink = true }: CategoryPillProps) {

const dynamicStyle = {
"--dynamic-color": selected ? "var(--sub-blue-300)" : "var(--sub-blue-100)",
} as React.CSSProperties;

return (
<div id={id} className={styles.categoryPill} style={dynamicStyle}>
{title}
{selected && (
<button onClick={(e) => {
// Stop propagation due to heavily nestes structure
e.stopPropagation();
if (setActiveFilter) {
setActiveFilter(false)
}
enableLink ? (
<Link href={`${process.env.NEXT_PUBLIC_SERVER_URL}/categories/${toKebabCase(title)}`}>

if (setActiveCategory) {
setActiveCategory("")
}
}}>
<CloseIcon id={id} width="16px" color="currentColor" />
</button>
)}
</div>
<div id={id} className={styles.categoryPill} style={dynamicStyle}>
{title}
{selected && (
<button onClick={(e) => {
// Stop propagation due to heavily nestes structure
e.stopPropagation();
if (setActiveFilter) {
setActiveFilter(false);
}

if (setActiveCategory) {
setActiveCategory("");
}
}}>
<CloseIcon id={id} width="16px" color="currentColor" />
</button>
)}
</div>
</Link>) : (
<div id={id} className={styles.categoryPill} style={dynamicStyle}>
{title}
{selected && (
<button onClick={(e) => {
// Stop propagation due to heavily nestes structure
e.stopPropagation();
if (setActiveFilter) {
setActiveFilter(false);
}

if (setActiveCategory) {
setActiveCategory("");
}
}}>
<CloseIcon id={id} width="16px" color="currentColor" />
</button>
)}
</div>
)
);
}
1 change: 1 addition & 0 deletions src/app/_components/SearchBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ export default function SearchBar({ currentContent, highlights, categories }) {
selected={filtering && activeCategory === category}
setActiveFilter={setFiltering}
setActiveCategory={setActiveCategory}
enableLink={false}
/>
</button>
))}
Expand Down
10 changes: 7 additions & 3 deletions src/app/_utilities/calculateTotalArticles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ export function calculateTotalArticles(content: {
CaseStudies: CaseStudy[]
TalksAndRoundtables: TalksAndRoundtable[]
}): number {
return Object.values(content).filter(
innerArray => Array.isArray(innerArray) && innerArray.length > 0,
).length
let contentCount = 0
for (const type in content) {
if (content[type].length > 0) {
contentCount += content[type].length
}
}
return contentCount
}
17 changes: 12 additions & 5 deletions src/app/_utilities/contentFetchers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getPayloadHMR } from "@payloadcms/next/utilities";
import configPromise from "@payload-config";
import type { Author, Blogpost, CaseStudy, Config, Media, Podcast, TalksAndRoundtable } from "@/payload-types";
import type { Author, Blogpost, CaseStudy, Category, Config, Media, Podcast, TalksAndRoundtable } from "@/payload-types";
import { notFound } from "next/navigation";
import { CollectionSlug, getPayload } from "payload";
import { draftMode } from "next/headers";
Expand Down Expand Up @@ -28,13 +28,13 @@ async function fetcher({ collection, limit = 10, depth = 1, draft = false, overr
});
}

export async function fetchContentBySlug({ slug, type, depth }: { slug: string, type: CollectionSlug, depth?: number }) {
export async function fetchContentBySlug({ slug, type, depth }: { slug: string, type: CollectionSlug, depth?: number }) {

if (!slug || !type) {
throw new Error("Must input slug and/or type.");
}

const { isEnabled: draft } = await draftMode()
const { isEnabled: draft } = await draftMode();

const query = { slug: { equals: slug } };

Expand All @@ -54,9 +54,16 @@ export async function fetchContentBySlug({ slug, type, depth }: { slug: string,
}


export async function fetchContentFromAuthor(author) {
export async function fetchContentFromAuthorOrCategory({ type, target }: { type: 'author' | 'category', target: Author | Category }) {
let query = {}

const query = { authors: { in: author.id } };
if (type === "author") {
query = { authors: { in: target.id } };
}

if (type === "category") {
query = { categories: { in: target.id } };
}

const blogposts = await fetcher({
collection: "blogposts",
Expand Down
2 changes: 2 additions & 0 deletions src/collections/Categories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { CollectionConfig } from 'payload'

import { anyone } from '../access/anyone'
import { authenticated } from '../access/authenticated'
import { slugField } from "@/fields/slug";

const Categories: CollectionConfig = {
slug: 'categories',
Expand All @@ -20,6 +21,7 @@ const Categories: CollectionConfig = {
type: 'text',
required: true,
},
...slugField(),
],
}

Expand Down
2 changes: 2 additions & 0 deletions src/payload-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ export interface Media {
export interface Category {
id: string;
title: string;
slug?: string | null;
slugLock?: boolean | null;
parent?: (string | null) | Category;
breadcrumbs?:
| {
Expand Down
Loading