feature: blog page

This commit is contained in:
Benno Tielen 2025-09-09 16:04:23 +02:00
parent 46b46c77c9
commit 2f999ed437
14 changed files with 7341 additions and 163 deletions

View file

@ -1,5 +1,5 @@
import { Section } from '../../../../components/Section/Section' import { Section } from '@/components/Section/Section'
import { Container } from '../../../../components/Container/Container' import { Container } from '@/components/Container/Container'
import { Title } from '@/components/Title/Title' import { Title } from '@/components/Title/Title'
import { TextDiv } from '@/components/Text/TextDiv' import { TextDiv } from '@/components/Text/TextDiv'
import { Blog } from '@/payload-types' import { Blog } from '@/payload-types'
@ -30,15 +30,15 @@ export default async function BlogPage({ params }: { params: Promise<{id: string
} }
// determine if some margin at the bottom should be added // determine if some margin at the bottom should be added
const length = data.content.length; const length = data.content.content.length;
const shouldAddMargin = data.content[length - 1].blockType === "text" const shouldAddMargin = data.content.content[length - 1].blockType === "text"
return ( return (
<> <>
<Section paddingBottom={"small"}> <Section paddingBottom={"small"}>
<Container> <Container>
<Title title={data.title} color={"contrast"}></Title> <Title title={data.title} color={"contrast"}></Title>
<TextDiv text={data.excerpt} /> <TextDiv text={data.content.excerpt} />
<p className={styles.published}> <p className={styles.published}>
Publiziert am {readableDateTime(data.createdAt)} Publiziert am {readableDateTime(data.createdAt)}
</p> </p>
@ -53,7 +53,7 @@ export default async function BlogPage({ params }: { params: Promise<{id: string
</Container> </Container>
</Section> </Section>
<Blocks content={data.content} /> <Blocks content={data.content.content} />
<AdminMenu <AdminMenu
collection={"worship"} collection={"worship"}

View file

@ -0,0 +1,36 @@
import { PageHeader } from '@/compositions/PageHeader/PageHeader'
import { fetchBlogPosts } from '@/fetch/blog'
import { BlogExcerpt } from '@/components/BlogExcerpt/BlogExcerpt'
import { Section } from '@/components/Section/Section'
import { Container } from '@/components/Container/Container'
import { getPhoto } from '@/utils/dto/gallery'
export const dynamic = 'force-dynamic'
export default async function Page() {
const blogs = await fetchBlogPosts(false);
return (
<>
<PageHeader
title={"Aktuelle Nachrichten"}
description={"Im Blog der Katholischen Pfarrei Heilige Drei Könige finden Sie aktuelle Nachrichten und erfahren alles über das Gemeindeleben, kommende Veranstaltungen und besondere Gottesdienste."}
/>
<Section padding={"small"}>
<Container>
{
blogs?.docs.map(blog =>
<BlogExcerpt
key={blog.id}
id={blog.id}
title={blog.title}
excerpt={blog.content.excerpt}
photo={getPhoto('thumbnail', blog.photo)}
/>)
}
</Container>
</Section>
</>
)
}

View file

@ -1,6 +1,6 @@
import { fetchEvents } from '@/fetch/events' import { fetchEvents } from '@/fetch/events'
import { fetchWorship } from '@/fetch/worship' import { fetchWorship } from '@/fetch/worship'
import { fetchBlog } from '@/fetch/blog' import { fetchBlogPosts } from '@/fetch/blog'
import { fetchHighlights } from '@/fetch/highlights' import { fetchHighlights } from '@/fetch/highlights'
import { Home } from '@/pageComponents/Home/Home' import { Home } from '@/pageComponents/Home/Home'
import moment from 'moment' import moment from 'moment'
@ -16,7 +16,7 @@ export default async function HomePage() {
fromDate: fromDate.toDate(), fromDate: fromDate.toDate(),
tillDate: tillDate.toDate(), tillDate: tillDate.toDate(),
}); });
const blog = await fetchBlog() const blog = await fetchBlogPosts(true)
const highlights = await fetchHighlights() const highlights = await fetchHighlights()

View file

@ -12,16 +12,19 @@ export const Blog: CollectionConfig = {
slug: 'blog', slug: 'blog',
labels: { labels: {
singular: { singular: {
de: 'Blogpost' de: 'Blogpost',
}, },
plural: { plural: {
de: 'Blog' de: 'Blog',
} },
}, },
fields: [ fields: [
{ {
name: 'photo', name: 'photo',
type: 'upload', type: 'upload',
label: {
de: 'Bild'
},
relationTo: 'media', relationTo: 'media',
}, },
{ {
@ -29,50 +32,94 @@ export const Blog: CollectionConfig = {
type: 'text', type: 'text',
required: true, required: true,
label: { label: {
de: "Titel" de: 'Titel',
}
},
{
name: 'parish',
type: 'relationship',
relationTo: 'parish',
hasMany: true,
label: {
de: "Gemeinde"
}
},
{
name: 'excerpt',
type: 'textarea',
label: {
de: 'Auszug'
}, },
required: true,
}, },
{ {
name: 'content', type: 'tabs',
type: 'blocks', tabs: [
minRows: 1, {
maxRows: 20, name: 'content',
blocks: [ fields: [
ParagraphBlock, {
DocumentBlock, name: 'excerpt',
DonationBlock, type: 'textarea',
ContactformBlock, label: {
GalleryBlock, de: 'Auszug',
ButtonBlock },
required: true,
},
{
name: 'content',
type: 'blocks',
minRows: 1,
maxRows: 20,
blocks: [
ParagraphBlock,
DocumentBlock,
DonationBlock,
ContactformBlock,
GalleryBlock,
ButtonBlock,
],
required: true,
},
],
},
{
name: 'configuration',
label: {
de: 'Einstellungen'
},
fields: [
{
name: 'showOnFrontpage',
type: 'checkbox',
label: {
de: 'Anzeigen auf Startseite?',
},
defaultValue: true,
required: true,
},
{
name: 'displayFromDate',
type: 'date',
label: {
de: 'Anzeigen von',
},
required: false,
},
{
name: 'displayTillDate',
type: 'date',
label: {
de: 'Anzeigen bis',
},
required: false,
},
{
name: 'parish',
type: 'relationship',
relationTo: 'parish',
hasMany: true,
label: {
de: 'Gemeinde',
},
},
],
},
], ],
required: true
}, },
], ],
admin: { admin: {
useAsTitle: 'title', useAsTitle: 'title',
hidden: hide hidden: hide,
}, },
access: { access: {
read: () => true, read: () => true,
create: isAdminOrEmployee(), create: isAdminOrEmployee(),
update: isAdminOrEmployee(), update: isAdminOrEmployee(),
delete: isAdminOrEmployee(), delete: isAdminOrEmployee(),
} },
} }

View file

@ -0,0 +1,17 @@
import { Meta, StoryObj } from '@storybook/react'
import { BlogExcerpt } from './BlogExcerpt'
const meta: Meta<typeof BlogExcerpt> = {
component: BlogExcerpt,
}
type Story = StoryObj<typeof BlogExcerpt>;
export default meta
export const Default: Story = {
args: {
id: 'some_id',
title: 'Some blog title',
excerpt: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.'
},
}

View file

@ -0,0 +1,42 @@
import { Button } from '@/components/Button/Button'
import Link from 'next/link'
import styles from './styles.module.scss'
import Image, { StaticImageData } from 'next/image'
type BlogExcerptProps = {
id: string,
title: string,
excerpt: string
photo: StaticImageData | undefined
}
export const BlogExcerpt = ({id, title, excerpt, photo}: BlogExcerptProps) => {
const url = `/blog/${id}`;
return (
<div className={styles.container}>
<div>
{ photo &&
<Link href={url}>
<Image
src={photo.src}
width={photo.width}
height={photo.height}
alt={"Blogbild"}
className={styles.image}
/>
</Link>
}
</div>
<div>
<h3><Link href={url} className={styles.title}>{title}</Link></h3>
<p>{excerpt}</p>
<div className={styles.button}>
<Button size={"md"} href={url}>Weiter lesen</Button>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,22 @@
.container {
display: flex;
gap: 50px;
margin-bottom: 80px;
align-items: center;
flex-wrap: wrap;
}
.title {
text-decoration: none;
color: inherit;
}
.button {
display: flex;
flex-direction: row-reverse;
}
.image {
border-radius: 10px;
}

View file

@ -9,7 +9,7 @@ import { transformGallery } from '@/utils/dto/gallery'
import { DonationForm } from '@/components/DonationForm/DonationForm' import { DonationForm } from '@/components/DonationForm/DonationForm'
type BlocksProps = { type BlocksProps = {
content: Blog['content'] content: Blog['content']['content']
} }
export function Blocks({ content }: BlocksProps) { export function Blocks({ content }: BlocksProps) {

View file

@ -2,21 +2,72 @@ import { Blog } from '@/payload-types'
import { PaginatedDocs } from 'payload' import { PaginatedDocs } from 'payload'
import { stringify } from 'qs-esm' import { stringify } from 'qs-esm'
export const fetchBlog = async (): Promise<PaginatedDocs<Blog> | undefined> => { /**
const stringifiedQuery = stringify( * Fetches blog posts based on given criteria.
*
* @param {boolean} displayOnFrontpage - Indicates whether to display posts on the front page.
* @returns {Promise<PaginatedDocs<Blog> | undefined>} - A Promise that resolves to the paginated list of blog posts, or undefined if an error occurs.
*/
export const fetchBlogPosts = async (displayOnFrontpage: boolean): Promise<PaginatedDocs<Blog> | undefined> => {
const today = new Date();
today.setHours(23, 59);
const query: any =
{ {
sort: "-date", sort: "-date",
select: { select: {
title: true, title: true,
date: true, date: true,
photo: true photo: true,
"content": !displayOnFrontpage, // hack to fetch content only for the `/blog` page
},
where: {
and: [
{
or: [
{
"configuration.displayFromDate": {
equals: null
}
},
{
"configuration.displayFromDate": {
less_than_equal: today.toISOString(),
}
}
]
},
{
or: [
{
"configuration.displayTillDate": {
equals: null
}
},
{
"configuration.displayTillDate": {
greater_than_equal: today.toISOString(),
}
}
]
}
],
}, },
limit: 18 limit: 18
}, };
{ addQueryPrefix: true },
)
const resp = await fetch(`http://localhost:3000/api/blog`); if(displayOnFrontpage) {
query.where.and.push({
"configuration.showOnFrontpage": {
equals: true
},
})
}
const stringifiedQuery = stringify(query, {addQueryPrefix: true})
const resp = await fetch(`http://localhost:3000/api/blog${stringifiedQuery}`);
if (!resp.ok) return undefined; if (!resp.ok) return undefined;
return resp.json(); return resp.json();
} }

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,24 @@
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
await db.execute(sql`
ALTER TABLE "blog" RENAME COLUMN "excerpt" TO "content_excerpt";
ALTER TABLE "announcement" ALTER COLUMN "date" SET DEFAULT '2025-09-14T07:56:02.742Z';
ALTER TABLE "calendar" ALTER COLUMN "date" SET DEFAULT '2025-09-14T07:56:02.825Z';
ALTER TABLE "classifieds" ALTER COLUMN "until" SET DEFAULT '2025-10-09T07:56:02.879Z';
ALTER TABLE "blog" ADD COLUMN "configuration_show_on_frontpage" boolean DEFAULT true NOT NULL;
ALTER TABLE "blog" ADD COLUMN "configuration_display_from_date" timestamp(3) with time zone;
ALTER TABLE "blog" ADD COLUMN "configuration_display_till_date" timestamp(3) with time zone;`)
}
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
await db.execute(sql`
ALTER TABLE "announcement" ALTER COLUMN "date" SET DEFAULT '2025-08-31T10:51:21.316Z';
ALTER TABLE "calendar" ALTER COLUMN "date" SET DEFAULT '2025-08-31T10:51:21.398Z';
ALTER TABLE "classifieds" ALTER COLUMN "until" SET DEFAULT '2025-09-26T10:51:21.450Z';
ALTER TABLE "blog" ADD COLUMN "excerpt" varchar NOT NULL;
ALTER TABLE "blog" DROP COLUMN "content_excerpt";
ALTER TABLE "blog" DROP COLUMN "configuration_show_on_frontpage";
ALTER TABLE "blog" DROP COLUMN "configuration_display_from_date";
ALTER TABLE "blog" DROP COLUMN "configuration_display_till_date";`)
}

View file

@ -10,6 +10,7 @@ import * as migration_20250608_105124_new_blocks from './20250608_105124_new_blo
import * as migration_20250818_143115_menu from './20250818_143115_menu'; import * as migration_20250818_143115_menu from './20250818_143115_menu';
import * as migration_20250827_093559_magazine from './20250827_093559_magazine'; import * as migration_20250827_093559_magazine from './20250827_093559_magazine';
import * as migration_20250827_105121_new_payload_version from './20250827_105121_new_payload_version'; import * as migration_20250827_105121_new_payload_version from './20250827_105121_new_payload_version';
import * as migration_20250909_075603 from './20250909_075603';
export const migrations = [ export const migrations = [
{ {
@ -70,6 +71,11 @@ export const migrations = [
{ {
up: migration_20250827_105121_new_payload_version.up, up: migration_20250827_105121_new_payload_version.up,
down: migration_20250827_105121_new_payload_version.down, down: migration_20250827_105121_new_payload_version.down,
name: '20250827_105121_new_payload_version' name: '20250827_105121_new_payload_version',
},
{
up: migration_20250909_075603.up,
down: migration_20250909_075603.down,
name: '20250909_075603'
}, },
]; ];

View file

@ -67,24 +67,39 @@ export const Default: Story = {
{ {
id: 'b1', id: 'b1',
title: 'Blog 1', title: 'Blog 1',
excerpt: '', content: {
content: [], excerpt: '',
content: []
},
configuration: {
showOnFrontpage: false,
},
updatedAt: '', updatedAt: '',
createdAt: '', createdAt: '',
}, },
{ {
id: 'b2', id: 'b2',
title: 'Blog 2', title: 'Blog 2',
excerpt: '', content: {
content: [], excerpt: '',
content: []
},
configuration: {
showOnFrontpage: false,
},
updatedAt: '', updatedAt: '',
createdAt: '', createdAt: '',
}, },
{ {
id: 'b3', id: 'b3',
title: 'Blog 3', title: 'Blog 3',
excerpt: '', content: {
content: [], excerpt: '',
content: []
},
configuration: {
showOnFrontpage: false,
},
updatedAt: '', updatedAt: '',
createdAt: '', createdAt: '',
}, },

View file

@ -327,68 +327,75 @@ export interface Blog {
id: string; id: string;
photo?: (string | null) | Media; photo?: (string | null) | Media;
title: string; title: string;
parish?: (string | Parish)[] | null; content: {
excerpt: string; excerpt: string;
content: ( content: (
| { | {
content: { content: {
root: { root: {
type: string;
children: {
type: string; 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; version: number;
[k: string]: unknown; };
}[]; [k: string]: unknown;
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
}; };
[k: string]: unknown; content_html?: string | null;
}; width: '1/2' | '3/4';
content_html?: string | null;
width: '1/2' | '3/4';
id?: string | null;
blockName?: string | null;
blockType: 'text';
}
| {
file: string | Document;
button: string;
id?: string | null;
blockName?: string | null;
blockType: 'document';
}
| {
id?: string | null;
blockName?: string | null;
blockType: 'donation';
}
| {
title: string;
description: string;
email: string;
id?: string | null;
blockName?: string | null;
blockType: 'contactform';
}
| {
items: {
photo: string | Media;
id?: string | null; id?: string | null;
}[]; blockName?: string | null;
id?: string | null; blockType: 'text';
blockName?: string | null; }
blockType: 'gallery'; | {
} file: string | Document;
| { button: string;
text: string; id?: string | null;
url: string; blockName?: string | null;
id?: string | null; blockType: 'document';
blockName?: string | null; }
blockType: 'button'; | {
} id?: string | null;
)[]; blockName?: string | null;
blockType: 'donation';
}
| {
title: string;
description: string;
email: string;
id?: string | null;
blockName?: string | null;
blockType: 'contactform';
}
| {
items: {
photo: string | Media;
id?: string | null;
}[];
id?: string | null;
blockName?: string | null;
blockType: 'gallery';
}
| {
text: string;
url: string;
id?: string | null;
blockName?: string | null;
blockType: 'button';
}
)[];
};
configuration: {
showOnFrontpage: boolean;
displayFromDate?: string | null;
displayTillDate?: string | null;
parish?: (string | Parish)[] | null;
};
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
@ -841,64 +848,75 @@ export interface CalendarSelect<T extends boolean = true> {
export interface BlogSelect<T extends boolean = true> { export interface BlogSelect<T extends boolean = true> {
photo?: T; photo?: T;
title?: T; title?: T;
parish?: T;
excerpt?: T;
content?: content?:
| T | T
| { | {
text?: excerpt?: T;
content?:
| T | T
| { | {
content?: T; text?:
content_html?: T;
width?: T;
id?: T;
blockName?: T;
};
document?:
| T
| {
file?: T;
button?: T;
id?: T;
blockName?: T;
};
donation?:
| T
| {
id?: T;
blockName?: T;
};
contactform?:
| T
| {
title?: T;
description?: T;
email?: T;
id?: T;
blockName?: T;
};
gallery?:
| T
| {
items?:
| T | T
| { | {
photo?: T; content?: T;
content_html?: T;
width?: T;
id?: T; id?: T;
blockName?: T;
};
document?:
| T
| {
file?: T;
button?: T;
id?: T;
blockName?: T;
};
donation?:
| T
| {
id?: T;
blockName?: T;
};
contactform?:
| T
| {
title?: T;
description?: T;
email?: T;
id?: T;
blockName?: T;
};
gallery?:
| T
| {
items?:
| T
| {
photo?: T;
id?: T;
};
id?: T;
blockName?: T;
};
button?:
| T
| {
text?: T;
url?: T;
id?: T;
blockName?: T;
}; };
id?: T;
blockName?: T;
};
button?:
| T
| {
text?: T;
url?: T;
id?: T;
blockName?: T;
}; };
}; };
configuration?:
| T
| {
showOnFrontpage?: T;
displayFromDate?: T;
displayTillDate?: T;
parish?: T;
};
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
} }