fix: storybook

This commit is contained in:
Benno Tielen 2026-04-17 09:09:44 +02:00
parent dba13d1d31
commit 3fab363e1f
23 changed files with 663 additions and 626 deletions

View file

@ -19,6 +19,9 @@ const preview: Preview = {
},
],
parameters: {
nextjs: {
appDirectory: true,
},
controls: {
matchers: {
color: /(background|color)$/i,

View file

@ -4,9 +4,6 @@ import { withPayload } from '@payloadcms/next/withPayload'
const nextConfig = {
// Your Next.js config here
output: 'standalone',
eslint: {
ignoreDuringBuilds: true,
},
images: {
remotePatterns: [
{

View file

@ -1,40 +1,7 @@
import { fetchEvents } from '@/fetch/events'
import { fetchWorship } from '@/fetch/worship'
import { fetchBlogPosts } from '@/fetch/blog'
import { fetchHighlights } from '@/fetch/highlights'
import { Home } from '@/pageComponents/Home/Home'
import moment from 'moment'
import { fetchLastAnnouncement, fetchLastAnnouncements } from '@/fetch/announcement'
import { perParish } from '@/utils/dto/perParish'
import { fetchLastCalendars } from '@/fetch/calendar'
export const dynamic = 'force-dynamic'
export default async function HomePage() {
const fromDate = moment().isoWeekday(1).hours(0).minutes(0);
const tillDate = moment().isoWeekday(7).hours(23).minutes(59);
const events = await fetchEvents()
const worship = await fetchWorship({
fromDate: fromDate.toDate(),
tillDate: tillDate.toDate(),
});
const blog = await fetchBlogPosts(true)
const highlights = await fetchHighlights()
const announcements = await fetchLastAnnouncements();
const announcementsLinks = announcements ? perParish(announcements) : [];
const calendars = await fetchLastCalendars();
const calendarsLinks = calendars ? perParish(calendars) : [];
return (
<Home
events={events?.docs || []}
worship={worship?.docs || []}
blog={blog?.docs || []}
highlights={highlights?.docs || []}
announcements={announcementsLinks}
calendars={calendarsLinks}
/>
)
return <Home />
}

View file

@ -40,7 +40,7 @@ const makeAdText = (text: string): SerializedEditorState =>
},
],
},
}) as SerializedEditorState
}) as unknown as SerializedEditorState
export const Default: Story = {
args: {

View file

@ -40,7 +40,7 @@ const makeAdText = (text: string): SerializedEditorState =>
},
],
},
}) as SerializedEditorState
}) as unknown as SerializedEditorState
export const Default: Story = {
args: {

View file

@ -39,7 +39,7 @@ const sampleState: SerializedEditorState = {
},
],
},
} as SerializedEditorState
} as unknown as SerializedEditorState
export const ThreeFourth: Story = {
args: {

View file

@ -1,23 +1,28 @@
import { Meta, StoryObj } from '@storybook/nextjs-vite'
import { ContactForm } from './ContactForm'
import { ContactFormView, ContactFormState } from './ContactFormView'
const meta: Meta<typeof ContactForm> = {
component: ContactForm,
const noopAction = async (): Promise<ContactFormState> => ({
message: 'Storybook: form submission is a no-op.',
})
const meta: Meta<typeof ContactFormView> = {
component: ContactFormView,
args: {
action: noopAction,
},
}
type Story = StoryObj<typeof ContactForm>;
type Story = StoryObj<typeof ContactFormView>
export default meta
export const Default: Story = {
args: {
schema: 'base',
toEmail: 'test@test.com'
},
}
export const Schema: Story = {
args: {
schema: 'contrast',
toEmail: 'test@test.com'
}
},
}

View file

@ -1,44 +1,14 @@
'use client'
import { Input } from '@/components/Input/Input'
import { Button } from '@/components/Button/Button'
import styles from "./styles.module.scss"
import classNames from 'classnames'
import { send } from '@/utils/actions'
import { useActionState } from 'react'
import { ContactFormView } from './ContactFormView'
type ContactFormProps = {
schema?: "base" | "contrast",
schema?: 'base' | 'contrast'
toEmail: string
}
const initialState = {
message: '',
}
export const ContactForm = ({ schema, toEmail }: ContactFormProps) => {
const sendWithEmail = send.bind(null, toEmail);
const [state, formAction, pending] = useActionState(sendWithEmail, initialState)
return (
<form action={formAction}>
<div className={classNames(styles.row, styles.firsRow)}>
<Input name={"name"} type={"text"} placeholder={"Name"} />
<Input name={"email"} type={"email"} placeholder={"E-Mail Adresse"} />
</div>
<div className={styles.row}>
<Input name={"subject"} type={"text"} placeholder={"Thema"} />
</div>
<div className={styles.row}>
<Input name={"message"} type={"textarea"} placeholder={"Ihre Nachricht"} />
</div>
<p>
{state.message}
</p>
<div className={styles.row}>
<Button size={"lg"} type={"submit"} schema={schema} disabled={pending}>Abschicken</Button>
</div>
</form>
)
const sendWithEmail = send.bind(null, toEmail)
return <ContactFormView schema={schema} action={sendWithEmail} />
}

View file

@ -0,0 +1,51 @@
'use client'
import { Input } from '@/components/Input/Input'
import { Button } from '@/components/Button/Button'
import styles from './styles.module.scss'
import classNames from 'classnames'
import { useActionState } from 'react'
export type ContactFormState = {
message: string
errors?: Record<string, string[] | undefined>
}
export type ContactFormAction = (
prevState: ContactFormState,
formData: FormData,
) => Promise<ContactFormState> | ContactFormState
type ContactFormViewProps = {
schema?: 'base' | 'contrast'
action: ContactFormAction
}
const initialState: ContactFormState = {
message: '',
}
export const ContactFormView = ({ schema, action }: ContactFormViewProps) => {
const [state, formAction, pending] = useActionState(action, initialState)
return (
<form action={formAction}>
<div className={classNames(styles.row, styles.firsRow)}>
<Input name={'name'} type={'text'} placeholder={'Name'} />
<Input name={'email'} type={'email'} placeholder={'E-Mail Adresse'} />
</div>
<div className={styles.row}>
<Input name={'subject'} type={'text'} placeholder={'Thema'} />
</div>
<div className={styles.row}>
<Input name={'message'} type={'textarea'} placeholder={'Ihre Nachricht'} />
</div>
<p>{state.message}</p>
<div className={styles.row}>
<Button size={'lg'} type={'submit'} schema={schema} disabled={pending}>
Abschicken
</Button>
</div>
</form>
)
}

View file

@ -1,17 +1,23 @@
import { Meta, StoryObj } from '@storybook/nextjs-vite'
import { ContactSection } from './ContactSection'
import { ContactSectionView } from './ContactSectionView'
import { ContactFormView, ContactFormState } from '@/compositions/ContactForm/ContactFormView'
const meta: Meta<typeof ContactSection> = {
component: ContactSection,
const noopAction = async (): Promise<ContactFormState> => ({
message: 'Storybook: form submission is a no-op.',
})
const meta: Meta<typeof ContactSectionView> = {
component: ContactSectionView,
}
type Story = StoryObj<typeof ContactSection>;
type Story = StoryObj<typeof ContactSectionView>
export default meta
export const Default: Story = {
args: {
title: "Kontakt",
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce vel dolor semper, consectetur augue quis, elementum tellus. Nulla ut porta lorem. Nulla posuere quam nisi, ut porttitor diam dignissim eget. Morbi imperdiet et lectus quis dapibus. Cras sollicitudin est augue, vel rhoncus massa elementum vitae. Donec sagittis pulvinar nibh ultrices tincidunt. Interdum et malesuada fames ac ante ipsum primis in faucibus.',
toEmail: 'kontak@test.com'
title: 'Kontakt',
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce vel dolor semper, consectetur augue quis, elementum tellus. Nulla ut porta lorem. Nulla posuere quam nisi, ut porttitor diam dignissim eget. Morbi imperdiet et lectus quis dapibus. Cras sollicitudin est augue, vel rhoncus massa elementum vitae. Donec sagittis pulvinar nibh ultrices tincidunt. Interdum et malesuada fames ac ante ipsum primis in faucibus.',
form: <ContactFormView action={noopAction} />,
},
}

View file

@ -1,34 +1,28 @@
import { Section } from '@/components/Section/Section'
import { Container } from '@/components/Container/Container'
import { ContactForm } from '@/compositions/ContactForm/ContactForm'
import { Title } from '@/components/Title/Title'
import { Col } from '@/components/Flex/Col'
import { Row } from '@/components/Flex/Row'
import { TextDiv } from '@/components/Text/TextDiv'
import { ContactSectionView } from './ContactSectionView'
type ContactSectionProps = {
title: string,
description: string,
title: string
description: string
backgroundColor?: 'off-white'
schema?: 'base' | 'contrast'
toEmail: string
}
export const ContactSection = ({title, description, schema, backgroundColor, toEmail}: ContactSectionProps) => {
export const ContactSection = ({
title,
description,
schema,
backgroundColor,
toEmail,
}: ContactSectionProps) => {
return (
<Section backgroundColor={backgroundColor}>
<Container>
<Row>
<Col>
<Title title={title} size={"md"} color={schema}/>
<TextDiv text={description} />
</Col>
<Col>
<ContactForm schema={schema} toEmail={toEmail}/>
</Col>
</Row>
</Container>
</Section>
<ContactSectionView
title={title}
description={description}
schema={schema}
backgroundColor={backgroundColor}
form={<ContactForm schema={schema} toEmail={toEmail} />}
/>
)
}

View file

@ -0,0 +1,37 @@
import { Section } from '@/components/Section/Section'
import { Container } from '@/components/Container/Container'
import { Title } from '@/components/Title/Title'
import { Col } from '@/components/Flex/Col'
import { Row } from '@/components/Flex/Row'
import { TextDiv } from '@/components/Text/TextDiv'
import type { ReactNode } from 'react'
type ContactSectionViewProps = {
title: string
description: string
backgroundColor?: 'off-white'
schema?: 'base' | 'contrast'
form: ReactNode
}
export const ContactSectionView = ({
title,
description,
schema,
backgroundColor,
form,
}: ContactSectionViewProps) => {
return (
<Section backgroundColor={backgroundColor}>
<Container>
<Row>
<Col>
<Title title={title} size={'md'} color={schema} />
<TextDiv text={description} />
</Col>
<Col>{form}</Col>
</Row>
</Container>
</Section>
)
}

View file

@ -1,13 +1,45 @@
import { Meta, StoryObj } from '@storybook/nextjs-vite'
import { Footer } from './Footer'
import type { Footer } from '@/payload-types'
import { FooterView } from './FooterView'
const meta: Meta<typeof Footer> = {
component: Footer,
const mockFooter: Footer = {
id: 'mock-footer',
groups: [
{
title: 'Pfarrei',
links: [
{ label: 'Über uns', href: '/ueber-uns' },
{ label: 'Kontakt', href: '/kontakt' },
{ label: 'Gemeinden', href: '/gemeinden' },
],
},
{
title: 'Service',
links: [
{ label: 'Gottesdienste', href: '/gottesdienst' },
{ label: 'Veranstaltungen', href: '/veranstaltungen' },
{ label: 'Blog', href: '/blog' },
],
},
],
}
type Story = StoryObj<typeof Footer>;
const mockPrayers = [
'Herr, gib mir heute die Gelassenheit, Dinge hinzunehmen, die ich nicht ändern kann.',
'Vater unser im Himmel, geheiligt werde dein Name.',
'Gegrüßet seist du, Maria, voll der Gnade.',
]
const meta: Meta<typeof FooterView> = {
component: FooterView,
}
type Story = StoryObj<typeof FooterView>
export default meta
export const Default: Story = {
args: {},
args: {
prayers: mockPrayers,
footer: mockFooter,
},
}

View file

@ -1,13 +1,6 @@
import { Section } from '@/components/Section/Section'
import { Container } from '@/components/Container/Container'
import { Logo } from '@/components/Logo/Logo'
import styles from './styles.module.scss'
import { Row } from '@/components/Flex/Row'
import { Col } from '@/components/Flex/Col'
import { RandomPrayer } from '@/components/RandomPrayer/RandomPrayer'
import Link from 'next/link'
import { fetchPrayers } from '@/fetch/prayers'
import { fetchFooter } from '@/fetch/footer'
import { FooterView } from './FooterView'
export const Footer = async () => {
const [prayers, footer] = await Promise.all([
@ -15,48 +8,5 @@ export const Footer = async () => {
fetchFooter(),
])
return (
<div className={styles.container}>
<Section backgroundColor="soft">
<Container>
<Row>
<Col>
<br />
<Logo
color={'#ffffff'}
textColor={'var(--base-color)'}
withText={true}
height={100}
/>
</Col>
<Col>
<Row gap={25}>
{footer.groups?.map((group, i) => (
<Col key={i}>
<p>
<strong>{group.title}</strong>
</p>
<ul className={styles.list}>
{group.links?.map((link, j) => (
<li key={j}>
<Link href={link.href}>{link.label}</Link>
</li>
))}
</ul>
</Col>
))}
<Col>
<p>
<strong>Stoßgebet</strong>
</p>
<RandomPrayer prayers={prayers} />
</Col>
</Row>
</Col>
</Row>
</Container>
</Section>
</div>
)
return <FooterView prayers={prayers} footer={footer} />
}

View file

@ -0,0 +1,61 @@
import { Section } from '@/components/Section/Section'
import { Container } from '@/components/Container/Container'
import { Logo } from '@/components/Logo/Logo'
import styles from './styles.module.scss'
import { Row } from '@/components/Flex/Row'
import { Col } from '@/components/Flex/Col'
import { RandomPrayer } from '@/components/RandomPrayer/RandomPrayer'
import Link from 'next/link'
import type { Footer } from '@/payload-types'
type FooterViewProps = {
prayers: string[]
footer: Footer
}
export const FooterView = ({ prayers, footer }: FooterViewProps) => {
return (
<div className={styles.container}>
<Section backgroundColor="soft">
<Container>
<Row>
<Col>
<br />
<Logo
color={'#ffffff'}
textColor={'var(--base-color)'}
withText={true}
height={100}
/>
</Col>
<Col>
<Row gap={25}>
{footer.groups?.map((group, i) => (
<Col key={i}>
<p>
<strong>{group.title}</strong>
</p>
<ul className={styles.list}>
{group.links?.map((link, j) => (
<li key={j}>
<Link href={link.href}>{link.label}</Link>
</li>
))}
</ul>
</Col>
))}
<Col>
<p>
<strong>Stoßgebet</strong>
</p>
<RandomPrayer prayers={prayers} />
</Col>
</Row>
</Col>
</Row>
</Container>
</Section>
</div>
)
}

View file

@ -1,87 +1,45 @@
import { Section } from '@/components/Section/Section'
import { Container } from '@/components/Container/Container'
import { Row } from '@/components/Flex/Row'
import { Col } from '@/components/Flex/Col'
import { Title } from '@/components/Title/Title'
import { NewsletterItem } from '@/compositions/PublicationAndNewsletter/NewsletterItem'
import styles from "./styles.module.scss"
import Image from 'next/image'
import envelope from "./envelope.svg"
import Sandbox from '@nyariv/sandboxjs'
import { fetchLastMagazine } from '@/fetch/magazine'
type NewsletterData = {
guid: string,
title: string,
link: string,
pubDate: string,
}
import {
PublicationAndNewsletterView,
NewsletterArchiveItem,
} from './PublicationAndNewsletterView'
export const PublicationAndNewsletter = async () => {
let archiveData: NewsletterData[] = []
let archiveData: NewsletterArchiveItem[] = []
try {
const response = await fetch(
'https://s3-eu-west-1.amazonaws.com/files.crsend.com/37000/37866/rss/mailings.js',
{ cache: "force-cache", next: { revalidate: 3600 }}
);
const jsContent = await response.text();
{ cache: 'force-cache', next: { revalidate: 3600 } },
)
const jsContent = await response.text()
// safe (?) alternative to `eval()`
const sandbox = new Sandbox();
const scope: {cr_archive: NewsletterData[] } = { cr_archive: [] };
const exec = sandbox.compile(jsContent);
exec(scope).run();
archiveData = scope.cr_archive.slice(0, 3);
const sandbox = new Sandbox()
const scope: { cr_archive: NewsletterArchiveItem[] } = { cr_archive: [] }
const exec = sandbox.compile(jsContent)
exec(scope).run()
archiveData = scope.cr_archive.slice(0, 3)
} catch {
console.error("Could not fetch newsletters. Please check PublicationAndNewsletter component")
}
const magazine = await fetchLastMagazine();
const magazine_url = magazine && typeof magazine.document === "object" ? magazine.document.url || undefined : undefined;
const magazine_cover = magazine && typeof magazine.cover === "object" ? magazine.cover : undefined;
return (
<Section backgroundColor={"off-white"}>
<Container>
<Row alignItems={"center"}>
<Col>
{magazine_url && magazine_cover && magazine_cover.url &&
<a href={magazine_url} target={'_blank'}>
<Image
className={styles.image}
src={magazine_cover.url}
width={magazine_cover.width || 500}
height={magazine_cover.height || 600}
alt={'Pfarreimagazin Ausgabe'}
unoptimized={true}
/>
</a>
}
</Col>
<Col>
<div className={styles.titleContainer}>
<Image src={envelope} alt={'Newsletter icon'} />
<Title
title={'Newsletter aus dem Bistum'}
size={'md'}
/>
</div>
{archiveData.map((item) => (
<NewsletterItem
key={item.guid}
title={item.title}
link={item.link}
pubDate={item.pubDate}>
</NewsletterItem>
))}
</Col>
</Row>
</Container>
</Section>
console.error(
'Could not fetch newsletters. Please check PublicationAndNewsletter component',
)
}
const magazine = await fetchLastMagazine()
const magazineUrl =
magazine && typeof magazine.document === 'object'
? magazine.document.url || undefined
: undefined
const magazineCover =
magazine && typeof magazine.cover === 'object' ? magazine.cover : undefined
return (
<PublicationAndNewsletterView
archiveData={archiveData}
magazineUrl={magazineUrl}
magazineCover={magazineCover}
/>
)
}

View file

@ -0,0 +1,66 @@
import { Meta, StoryObj } from '@storybook/nextjs-vite'
import { PublicationAndNewsletterView } from './PublicationAndNewsletterView'
const meta: Meta<typeof PublicationAndNewsletterView> = {
component: PublicationAndNewsletterView,
}
type Story = StoryObj<typeof PublicationAndNewsletterView>
export default meta
const archiveData = [
{
guid: '1',
title: 'Newsletter aus dem Bistum März 2026',
link: 'https://example.com/newsletter/march',
pubDate: '2026-03-01T00:00:00.000Z',
},
{
guid: '2',
title: 'Newsletter aus dem Bistum Februar 2026',
link: 'https://example.com/newsletter/february',
pubDate: '2026-02-01T00:00:00.000Z',
},
{
guid: '3',
title: 'Newsletter aus dem Bistum Januar 2026',
link: 'https://example.com/newsletter/january',
pubDate: '2026-01-01T00:00:00.000Z',
},
]
export const Default: Story = {
args: {
archiveData,
magazineUrl: 'https://example.com/magazine.pdf',
magazineCover: {
url: 'https://placehold.co/500x600/png',
width: 500,
height: 600,
},
},
}
export const NewsletterOnly: Story = {
args: {
archiveData,
},
}
export const MagazineOnly: Story = {
args: {
archiveData: [],
magazineUrl: 'https://example.com/magazine.pdf',
magazineCover: {
url: 'https://placehold.co/500x600/png',
width: 500,
height: 600,
},
},
}
export const Empty: Story = {
args: {
archiveData: [],
},
}

View file

@ -0,0 +1,70 @@
import { Section } from '@/components/Section/Section'
import { Container } from '@/components/Container/Container'
import { Row } from '@/components/Flex/Row'
import { Col } from '@/components/Flex/Col'
import { Title } from '@/components/Title/Title'
import { NewsletterItem } from '@/compositions/PublicationAndNewsletter/NewsletterItem'
import styles from './styles.module.scss'
import Image from 'next/image'
import envelope from './envelope.svg'
export type NewsletterArchiveItem = {
guid: string
title: string
link: string
pubDate: string
}
type PublicationAndNewsletterViewProps = {
archiveData: NewsletterArchiveItem[]
magazineUrl?: string
magazineCover?: {
url?: string | null
width?: number | null
height?: number | null
}
}
export const PublicationAndNewsletterView = ({
archiveData,
magazineUrl,
magazineCover,
}: PublicationAndNewsletterViewProps) => {
return (
<Section backgroundColor={'off-white'}>
<Container>
<Row alignItems={'center'}>
<Col>
{magazineUrl && magazineCover?.url && (
<a href={magazineUrl} target={'_blank'}>
<Image
className={styles.image}
src={magazineCover.url}
width={magazineCover.width || 500}
height={magazineCover.height || 600}
alt={'Pfarreimagazin Ausgabe'}
unoptimized={true}
/>
</a>
)}
</Col>
<Col>
<div className={styles.titleContainer}>
<Image src={envelope} alt={'Newsletter icon'} />
<Title title={'Newsletter aus dem Bistum'} size={'md'} />
</div>
{archiveData.map((item) => (
<NewsletterItem
key={item.guid}
title={item.title}
link={item.link}
pubDate={item.pubDate}
></NewsletterItem>
))}
</Col>
</Row>
</Container>
</Section>
)
}

View file

@ -1,175 +0,0 @@
import { Meta, StoryObj } from '@storybook/nextjs-vite'
import { Home } from './Home'
const meta: Meta<typeof Home> = {
component: Home,
}
type Story = StoryObj<typeof Home>;
export default meta
export const Default: Story = {
args: {
events: [
{
id: '1',
title: 'Event 1',
date: '2024-12-02T09:21:24Z',
location: {
id: 'l1',
name: "St. Richard",
updatedAt: "",
createdAt: ""
},
shortDescription: '',
description: "Some descripton",
cancelled: false,
updatedAt: '2024-12-02T09:21:24Z',
createdAt: '2024-12-02T09:21:24Z',
isRecurring: false
},
{
id: '2',
title: 'Event 2',
date: '2024-12-05T09:21:24Z',
location: {
id: 'l1',
name: "St. Richard",
updatedAt: "",
createdAt: ""
},
shortDescription: '',
description: "",
cancelled: false,
updatedAt: '2024-12-02T09:21:24Z',
createdAt: '2024-12-02T09:21:24Z',
isRecurring: false
},
{
id: '2',
title: 'Event 2',
date: '2024-12-08T09:21:24Z',
location: {
id: 'l2',
name: "St. Hedwig",
updatedAt: "",
createdAt: ""
},
shortDescription: '',
description: "",
cancelled: true,
updatedAt: '2024-12-02T09:21:24Z',
createdAt: '2024-12-02T09:21:24Z',
isRecurring: false
},
],
blog: [
{
id: 'b1',
title: 'Blog 1',
content: {
excerpt: '',
content: []
},
configuration: {
showOnFrontpage: false,
},
updatedAt: '',
createdAt: '',
},
{
id: 'b2',
title: 'Blog 2',
content: {
excerpt: '',
content: []
},
configuration: {
showOnFrontpage: false,
},
updatedAt: '',
createdAt: '',
},
{
id: 'b3',
title: 'Blog 3',
content: {
excerpt: '',
content: []
},
configuration: {
showOnFrontpage: false,
},
updatedAt: '',
createdAt: '',
},
],
worship: [
{
id: 'w1',
date: '2024-12-02T09:21:24Z',
location: {
id: 'c1',
name: 'St Richard',
address: '',
createdAt: '',
updatedAt: ''
},
type: 'MASS',
cancelled: false,
updatedAt: '',
createdAt: '',
},
{
id: 'w1',
date: '2024-12-07T10:00:24Z',
location: {
id: 'c1',
name: 'St Richard',
address: '',
createdAt: '',
updatedAt: ''
},
type: 'MASS',
cancelled: false,
updatedAt: '',
createdAt: '',
},
],
highlights: [],
announcements: [
{
id: "link_1",
text: "St. Clara",
href: "https://disney.com"
},
{
id: "link_2",
text: "St. Anna",
href: "https://disney.com"
},
{
id: "link_3",
text: "St. Eduard",
href: "https://disney.com"
}
],
calendars: [
{
id: "link_1",
text: "St. Clara",
href: "https://disney.com"
},
{
id: "link_2",
text: "St. Anna",
href: "https://disney.com"
},
{
id: "link_3",
text: "St. Eduard",
href: "https://disney.com"
}
],
}
}

View file

@ -1,198 +1,39 @@
import { Blog, Worship, Event, Highlight } from '@/payload-types'
import { Banner } from '@/components/Banner/Banner'
import { Container } from '@/components/Container/Container'
import { Section } from '@/components/Section/Section'
import { MainText } from '@/components/MainText/MainText'
import { HR } from '@/components/HorizontalRule/HorizontalRule'
import { Title } from '@/components/Title/Title'
import { MassGrid } from '@/components/MassTable/MassGrid'
import { MassTable } from '@/components/MassTable/MassTable'
import { ImageCardSlider } from '@/compositions/ImageCardSlider/ImageCardSlider'
import { blogToSlides } from '@/utils/dto/blog'
import forest from '@/assets/map.jpg'
import { ContentWithSlider } from '@/compositions/ContentWithSlider/ContentWithSlider'
import { EventRow } from '@/components/EventRow/EventRow'
import { highlightLink } from '@/utils/dto/highlight'
import { Events } from '@/compositions/Events/Events'
import { transformEvents } from '@/utils/dto/events'
import { ContactSection } from '@/compositions/ContactSection/ContactSection'
import { CollapsibleImageWithText } from '@/compositions/CollapsibleImageWithText/CollapsibleImageWithText'
import { MoreInformation } from '@/pageComponents/Home/MoreInformation'
import { Button } from '@/components/Button/Button'
import styles from "./styles.module.scss"
import moment from 'moment'
import { fetchEvents } from '@/fetch/events'
import { fetchWorship } from '@/fetch/worship'
import { fetchBlogPosts } from '@/fetch/blog'
import { fetchHighlights } from '@/fetch/highlights'
import { fetchLastAnnouncements } from '@/fetch/announcement'
import { fetchLastCalendars } from '@/fetch/calendar'
import { perParish } from '@/utils/dto/perParish'
import { PublicationAndNewsletter } from '@/compositions/PublicationAndNewsletter/PublicationAndNewsletter'
import { Link, PopupButton } from '@/components/PopupButton/PopupButton'
import { HomeView } from './HomeView'
type HomeProps = {
events: Event[],
worship: Worship[],
blog: Blog[],
highlights: Highlight[],
announcements: Link[],
calendars: Link[]
}
export const Home = async () => {
const fromDate = moment().isoWeekday(1).hours(0).minutes(0)
const tillDate = moment().isoWeekday(7).hours(23).minutes(59)
const sortWorship = (worship: Worship[]) => {
const map = new Map<string, Worship[]>()
worship.map(w => {
if (typeof w.location === 'object') {
const title = w.location.name
if (map.has(title)) {
map.get(title)?.push(w)
} else {
map.set(title, [w])
}
}
const events = await fetchEvents()
const worship = await fetchWorship({
fromDate: fromDate.toDate(),
tillDate: tillDate.toDate(),
})
return map
}
export const Home = ({
events,
worship,
blog,
highlights,
announcements,
calendars
}: HomeProps) => {
const worshipPerLocation = Array.from(
sortWorship(worship).entries(),
).sort(
(a, b) => {
const nameA = a[0]
const nameB = b[0]
if (nameA < nameB) {
return -1
}
if (nameA > nameB) {
return 1
}
// names must be equal
return 0
},
)
const blog = await fetchBlogPosts(true)
const highlights = await fetchHighlights()
const announcements = await fetchLastAnnouncements()
const announcementsLinks = announcements ? perParish(announcements) : []
const calendars = await fetchLastCalendars()
const calendarsLinks = calendars ? perParish(calendars) : []
return (
<>
<Banner />
<Container>
<Section>
<MainText
text={`Willkommen bei der Pfarrei Heilige Drei Könige Nord-Neukölln! Hier begegnen wir einander spirituell, kulturell, sozial und gestalten gemeinsam eine lebendige Kirche. Trete ein und entdecke einen Ort, der inspiriert, verbindet und bewegt.`} />
</Section>
</Container>
<HR />
<Container>
{blog && blog.length > 0 &&
<Section>
<Title title={'Aktuelles'} color={"contrast"} />
<ImageCardSlider slides={blogToSlides(blog)} />
</Section>
}
</Container>
<Section paddingBottom={'medium'}>
<Title
title={'Nächste Gottesdienste'}
subtitle={'Komm einfach vorbei!'}
color={"contrast"}
align={'center'}
<HomeView
events={events?.docs || []}
worship={worship?.docs || []}
blog={blog?.docs || []}
highlights={highlights?.docs || []}
announcements={announcementsLinks}
calendars={calendarsLinks}
publicationAndNewsletter={<PublicationAndNewsletter />}
/>
<Section padding={'small'}>
<MassGrid>
{worshipPerLocation.map(value => <MassTable key={value[0]} location={value[0]} masses={value[1]} />)}
</MassGrid>
</Section>
<Section padding={'small'}>
<div className={styles.center}>
{ announcements.length > 0 &&
<PopupButton
text={"Vermeldungen"}
title={"Vermeldungen"}
links={announcements}
schema={"shade"}
/>
}
{ calendars.length > 0 &&
<PopupButton
text={"Liturgischer Kalender"}
title={"Kalender"}
links={calendars}
schema={"shade"}
/>
}
<Button
href={"/gottesdienst"}
size={"md"}
>Alle Gottesdienste</Button>
</div>
</Section>
</Section>
<CollapsibleImageWithText
backgroundColor={'soft'}
title={'Über uns'}
schema={"base"}
text={'Unsere Pfarrei Hl. Drei Könige wurde am 01.01.2020 gegründet. Am 12.01.2020 feierte Erzbischof Dr. Heiner Koch mit den Gemeinden die Gründung in einer feierlichen Hl. Messe in der katholischen Marienschule. Anwesende Gäste waren Bürgermeister Martin Hikel, Christian Nottmeier, der Superintendent des evangelischen Kirchenkreises Neukölln und viele Akteuren aus Kiez und Ökumene. Die Vielfalt der Glaubenswege in unserer Pfarrei sehen wir als Schatz. Wie die drei Weisen aus dem Morgenland wollen wir uns immer wieder neu auf den Weg machen.'}
image={forest}
content={<MoreInformation />}
/>
<ContentWithSlider slider={<>
<Title title={'Aktuelle Highlights'} size={'md'} fontStyle={'sans-serif'} color={'white'} />
{highlights.map(highlight => (
<EventRow
color={'white'}
key={highlight.id}
date={highlight.date}
showDate={false}
title={highlight.text}
href={highlightLink(highlight)}
cancelled={false}
/>
))}
</>}>
<Section>
<Title
color={"contrast"}
title={'Veranstaltungen'}
/>
<Events
events={transformEvents(events)}
n={6}
schema={"contrast"}
/>
</Section>
</ContentWithSlider>
<PublicationAndNewsletter />
<ContactSection
title={'Kontakt'}
description={'Haben Sie Fragen zum Glauben, zu den Sakramenten oder unseren Angeboten? Benötigen Sie Hilfe in einer schwierigen Situation oder möchten Sie einfach Ihre Gedanken mit uns teilen?\n' +
'\n' +
'Zögern Sie nicht, uns über das Kontaktformular zu schreiben. Wir freuen uns über jede Nachricht und sind gerne für Sie da.'}
schema={"base"}
toEmail={"kontakt@dreikoenige.berlin"}
/>
</>
)
}
export default Home;

View file

@ -0,0 +1,199 @@
import { Blog, Worship, Event, Highlight } from '@/payload-types'
import { Banner } from '@/components/Banner/Banner'
import { Container } from '@/components/Container/Container'
import { Section } from '@/components/Section/Section'
import { MainText } from '@/components/MainText/MainText'
import { HR } from '@/components/HorizontalRule/HorizontalRule'
import { Title } from '@/components/Title/Title'
import { MassGrid } from '@/components/MassTable/MassGrid'
import { MassTable } from '@/components/MassTable/MassTable'
import { ImageCardSlider } from '@/compositions/ImageCardSlider/ImageCardSlider'
import { blogToSlides } from '@/utils/dto/blog'
import forest from '@/assets/map.jpg'
import { ContentWithSlider } from '@/compositions/ContentWithSlider/ContentWithSlider'
import { EventRow } from '@/components/EventRow/EventRow'
import { highlightLink } from '@/utils/dto/highlight'
import { Events } from '@/compositions/Events/Events'
import { transformEvents } from '@/utils/dto/events'
import { ContactSection } from '@/compositions/ContactSection/ContactSection'
import { CollapsibleImageWithText } from '@/compositions/CollapsibleImageWithText/CollapsibleImageWithText'
import { MoreInformation } from '@/pageComponents/Home/MoreInformation'
import { Button } from '@/components/Button/Button'
import styles from "./styles.module.scss"
import { Link, PopupButton } from '@/components/PopupButton/PopupButton'
import type { ReactNode } from 'react'
type HomeViewProps = {
events: Event[],
worship: Worship[],
blog: Blog[],
highlights: Highlight[],
announcements: Link[],
calendars: Link[],
publicationAndNewsletter?: ReactNode
}
const sortWorship = (worship: Worship[]) => {
const map = new Map<string, Worship[]>()
worship.map(w => {
if (typeof w.location === 'object') {
const title = w.location.name
if (map.has(title)) {
map.get(title)?.push(w)
} else {
map.set(title, [w])
}
}
})
return map
}
export const HomeView = ({
events,
worship,
blog,
highlights,
announcements,
calendars,
publicationAndNewsletter,
}: HomeViewProps) => {
const worshipPerLocation = Array.from(
sortWorship(worship).entries(),
).sort(
(a, b) => {
const nameA = a[0]
const nameB = b[0]
if (nameA < nameB) {
return -1
}
if (nameA > nameB) {
return 1
}
// names must be equal
return 0
},
)
return (
<>
<Banner />
<Container>
<Section>
<MainText
text={`Willkommen bei der Pfarrei Heilige Drei Könige Nord-Neukölln! Hier begegnen wir einander spirituell, kulturell, sozial und gestalten gemeinsam eine lebendige Kirche. Trete ein und entdecke einen Ort, der inspiriert, verbindet und bewegt.`} />
</Section>
</Container>
<HR />
<Container>
{blog && blog.length > 0 &&
<Section>
<Title title={'Aktuelles'} color={"contrast"} />
<ImageCardSlider slides={blogToSlides(blog)} />
</Section>
}
</Container>
<Section paddingBottom={'medium'}>
<Title
title={'Nächste Gottesdienste'}
subtitle={'Komm einfach vorbei!'}
color={"contrast"}
align={'center'}
/>
<Section padding={'small'}>
<MassGrid>
{worshipPerLocation.map(value => <MassTable key={value[0]} location={value[0]} masses={value[1]} />)}
</MassGrid>
</Section>
<Section padding={'small'}>
<div className={styles.center}>
{ announcements.length > 0 &&
<PopupButton
text={"Vermeldungen"}
title={"Vermeldungen"}
links={announcements}
schema={"shade"}
/>
}
{ calendars.length > 0 &&
<PopupButton
text={"Liturgischer Kalender"}
title={"Kalender"}
links={calendars}
schema={"shade"}
/>
}
<Button
href={"/gottesdienst"}
size={"md"}
>Alle Gottesdienste</Button>
</div>
</Section>
</Section>
<CollapsibleImageWithText
backgroundColor={'soft'}
title={'Über uns'}
schema={"base"}
text={'Unsere Pfarrei Hl. Drei Könige wurde am 01.01.2020 gegründet. Am 12.01.2020 feierte Erzbischof Dr. Heiner Koch mit den Gemeinden die Gründung in einer feierlichen Hl. Messe in der katholischen Marienschule. Anwesende Gäste waren Bürgermeister Martin Hikel, Christian Nottmeier, der Superintendent des evangelischen Kirchenkreises Neukölln und viele Akteuren aus Kiez und Ökumene. Die Vielfalt der Glaubenswege in unserer Pfarrei sehen wir als Schatz. Wie die drei Weisen aus dem Morgenland wollen wir uns immer wieder neu auf den Weg machen.'}
image={forest}
content={<MoreInformation />}
/>
<ContentWithSlider slider={<>
<Title title={'Aktuelle Highlights'} size={'md'} fontStyle={'sans-serif'} color={'white'} />
{highlights.map(highlight => (
<EventRow
color={'white'}
key={highlight.id}
date={highlight.date}
showDate={false}
title={highlight.text}
href={highlightLink(highlight)}
cancelled={false}
/>
))}
</>}>
<Section>
<Title
color={"contrast"}
title={'Veranstaltungen'}
/>
<Events
events={transformEvents(events)}
n={6}
schema={"contrast"}
/>
</Section>
</ContentWithSlider>
{publicationAndNewsletter}
<ContactSection
title={'Kontakt'}
description={'Haben Sie Fragen zum Glauben, zu den Sakramenten oder unseren Angeboten? Benötigen Sie Hilfe in einer schwierigen Situation oder möchten Sie einfach Ihre Gedanken mit uns teilen?\n' +
'\n' +
'Zögern Sie nicht, uns über das Kontaktformular zu schreiben. Wir freuen uns über jede Nachricht und sind gerne für Sie da.'}
schema={"base"}
toEmail={"kontakt@dreikoenige.berlin"}
/>
</>
)
}

View file

@ -2,7 +2,7 @@ import { Meta, StoryObj } from '@storybook/nextjs-vite'
import chris from "../../assets/christophorus.jpeg"
import { Parish } from './Parish'
import { Menu } from '@/components/Menu/Menu'
import { Footer } from '@/compositions/Footer/Footer'
import { FooterView } from '@/compositions/Footer/FooterView'
const meta: Meta<typeof Parish> = {
component: Parish,
@ -28,7 +28,18 @@ const meta: Meta<typeof Parish> = {
]
}}/>
<Story />
<Footer />
<FooterView
prayers={['Herr, gib uns deinen Frieden.']}
footer={{
id: 'mock-footer',
groups: [
{
title: 'Pfarrei',
links: [{ label: 'Kontakt', href: '/kontakt' }],
},
],
}}
/>
</>
)
]

View file

@ -27,26 +27,20 @@ export const WithResults: Story = {
results: [
makeResult('1', 'Sonntagsgottesdienst', {
relationTo: 'pages',
value: {
id: 'p1',
slug: 'gottesdienst',
} as Search['doc']['value'],
}),
value: { id: 'p1', slug: 'gottesdienst' },
} as unknown as Search['doc']),
makeResult('2', 'Ostern 2026 Liturgische Feier', {
relationTo: 'event',
value: { id: 'e1' } as Search['doc']['value'],
}),
value: { id: 'e1' },
} as unknown as Search['doc']),
makeResult('3', 'Ministrantengruppe', {
relationTo: 'group',
value: {
id: 'g1',
slug: 'ministranten',
} as Search['doc']['value'],
}),
value: { id: 'g1', slug: 'ministranten' },
} as unknown as Search['doc']),
makeResult('4', 'Rundbrief Januar', {
relationTo: 'blog',
value: { id: 'b1' } as Search['doc']['value'],
}),
value: { id: 'b1' },
} as unknown as Search['doc']),
],
},
}