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: { parameters: {
nextjs: {
appDirectory: true,
},
controls: { controls: {
matchers: { matchers: {
color: /(background|color)$/i, color: /(background|color)$/i,

View file

@ -4,9 +4,6 @@ import { withPayload } from '@payloadcms/next/withPayload'
const nextConfig = { const nextConfig = {
// Your Next.js config here // Your Next.js config here
output: 'standalone', output: 'standalone',
eslint: {
ignoreDuringBuilds: true,
},
images: { images: {
remotePatterns: [ 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 { 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 const dynamic = 'force-dynamic'
export default async function HomePage() { export default async function HomePage() {
return <Home />
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}
/>
)
} }

View file

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

View file

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

View file

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

View file

@ -1,23 +1,28 @@
import { Meta, StoryObj } from '@storybook/nextjs-vite' import { Meta, StoryObj } from '@storybook/nextjs-vite'
import { ContactForm } from './ContactForm' import { ContactFormView, ContactFormState } from './ContactFormView'
const meta: Meta<typeof ContactForm> = { const noopAction = async (): Promise<ContactFormState> => ({
component: ContactForm, 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 default meta
export const Default: Story = { export const Default: Story = {
args: { args: {
schema: 'base', schema: 'base',
toEmail: 'test@test.com'
}, },
} }
export const Schema: Story = { export const Schema: Story = {
args: { args: {
schema: 'contrast', schema: 'contrast',
toEmail: 'test@test.com' },
} }
}

View file

@ -1,44 +1,14 @@
'use client' '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 { send } from '@/utils/actions'
import { useActionState } from 'react' import { ContactFormView } from './ContactFormView'
type ContactFormProps = { type ContactFormProps = {
schema?: "base" | "contrast", schema?: 'base' | 'contrast'
toEmail: string toEmail: string
} }
const initialState = { export const ContactForm = ({ schema, toEmail }: ContactFormProps) => {
message: '', const sendWithEmail = send.bind(null, toEmail)
return <ContactFormView schema={schema} action={sendWithEmail} />
} }
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>
)
}

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 { 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> = { const noopAction = async (): Promise<ContactFormState> => ({
component: ContactSection, 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 default meta
export const Default: Story = { export const Default: Story = {
args: { args: {
title: "Kontakt", 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.', description:
toEmail: 'kontak@test.com' '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 { ContactForm } from '@/compositions/ContactForm/ContactForm'
import { Title } from '@/components/Title/Title' import { ContactSectionView } from './ContactSectionView'
import { Col } from '@/components/Flex/Col'
import { Row } from '@/components/Flex/Row'
import { TextDiv } from '@/components/Text/TextDiv'
type ContactSectionProps = { type ContactSectionProps = {
title: string, title: string
description: string, description: string
backgroundColor?: 'off-white' backgroundColor?: 'off-white'
schema?: 'base' | 'contrast' schema?: 'base' | 'contrast'
toEmail: string toEmail: string
} }
export const ContactSection = ({
export const ContactSection = ({title, description, schema, backgroundColor, toEmail}: ContactSectionProps) => { title,
description,
schema,
backgroundColor,
toEmail,
}: ContactSectionProps) => {
return ( return (
<Section backgroundColor={backgroundColor}> <ContactSectionView
<Container> title={title}
<Row> description={description}
<Col> schema={schema}
<Title title={title} size={"md"} color={schema}/> backgroundColor={backgroundColor}
<TextDiv text={description} /> form={<ContactForm schema={schema} toEmail={toEmail} />}
</Col> />
<Col>
<ContactForm schema={schema} toEmail={toEmail}/>
</Col>
</Row>
</Container>
</Section>
) )
} }

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 { 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> = { const mockFooter: Footer = {
component: 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 default meta
export const Default: Story = { 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 { fetchPrayers } from '@/fetch/prayers'
import { fetchFooter } from '@/fetch/footer' import { fetchFooter } from '@/fetch/footer'
import { FooterView } from './FooterView'
export const Footer = async () => { export const Footer = async () => {
const [prayers, footer] = await Promise.all([ const [prayers, footer] = await Promise.all([
@ -15,48 +8,5 @@ export const Footer = async () => {
fetchFooter(), fetchFooter(),
]) ])
return ( return <FooterView prayers={prayers} footer={footer} />
<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

@ -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 Sandbox from '@nyariv/sandboxjs'
import { fetchLastMagazine } from '@/fetch/magazine' import { fetchLastMagazine } from '@/fetch/magazine'
import {
type NewsletterData = { PublicationAndNewsletterView,
guid: string, NewsletterArchiveItem,
title: string, } from './PublicationAndNewsletterView'
link: string,
pubDate: string,
}
export const PublicationAndNewsletter = async () => { export const PublicationAndNewsletter = async () => {
let archiveData: NewsletterArchiveItem[] = []
let archiveData: NewsletterData[] = []
try { try {
const response = await fetch( const response = await fetch(
'https://s3-eu-west-1.amazonaws.com/files.crsend.com/37000/37866/rss/mailings.js', 'https://s3-eu-west-1.amazonaws.com/files.crsend.com/37000/37866/rss/mailings.js',
{ cache: "force-cache", next: { revalidate: 3600 }} { cache: 'force-cache', next: { revalidate: 3600 } },
); )
const jsContent = await response.text(); const jsContent = await response.text()
// safe (?) alternative to `eval()` const sandbox = new Sandbox()
const sandbox = new Sandbox(); const scope: { cr_archive: NewsletterArchiveItem[] } = { cr_archive: [] }
const scope: {cr_archive: NewsletterData[] } = { cr_archive: [] }; const exec = sandbox.compile(jsContent)
const exec = sandbox.compile(jsContent); exec(scope).run()
exec(scope).run();
archiveData = scope.cr_archive.slice(0, 3);
archiveData = scope.cr_archive.slice(0, 3)
} catch { } catch {
console.error("Could not fetch newsletters. Please check PublicationAndNewsletter component") console.error(
'Could not fetch newsletters. Please check PublicationAndNewsletter component',
)
} }
const magazine = await fetchLastMagazine(); const magazine = await fetchLastMagazine()
const magazine_url = magazine && typeof magazine.document === "object" ? magazine.document.url || undefined : undefined; const magazineUrl =
const magazine_cover = magazine && typeof magazine.cover === "object" ? magazine.cover : undefined; magazine && typeof magazine.document === 'object'
? magazine.document.url || undefined
: undefined
const magazineCover =
magazine && typeof magazine.cover === 'object' ? magazine.cover : undefined
return ( return (
<Section backgroundColor={"off-white"}> <PublicationAndNewsletterView
<Container> archiveData={archiveData}
<Row alignItems={"center"}> magazineUrl={magazineUrl}
<Col> magazineCover={magazineCover}
{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>
) )
} }

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 moment from 'moment'
import { Banner } from '@/components/Banner/Banner' import { fetchEvents } from '@/fetch/events'
import { Container } from '@/components/Container/Container' import { fetchWorship } from '@/fetch/worship'
import { Section } from '@/components/Section/Section' import { fetchBlogPosts } from '@/fetch/blog'
import { MainText } from '@/components/MainText/MainText' import { fetchHighlights } from '@/fetch/highlights'
import { HR } from '@/components/HorizontalRule/HorizontalRule' import { fetchLastAnnouncements } from '@/fetch/announcement'
import { Title } from '@/components/Title/Title' import { fetchLastCalendars } from '@/fetch/calendar'
import { MassGrid } from '@/components/MassTable/MassGrid' import { perParish } from '@/utils/dto/perParish'
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 { PublicationAndNewsletter } from '@/compositions/PublicationAndNewsletter/PublicationAndNewsletter' import { PublicationAndNewsletter } from '@/compositions/PublicationAndNewsletter/PublicationAndNewsletter'
import { Link, PopupButton } from '@/components/PopupButton/PopupButton' import { HomeView } from './HomeView'
type HomeProps = { export const Home = async () => {
events: Event[], const fromDate = moment().isoWeekday(1).hours(0).minutes(0)
worship: Worship[], const tillDate = moment().isoWeekday(7).hours(23).minutes(59)
blog: Blog[],
highlights: Highlight[],
announcements: Link[],
calendars: Link[]
}
const sortWorship = (worship: Worship[]) => { const events = await fetchEvents()
const map = new Map<string, Worship[]>() const worship = await fetchWorship({
fromDate: fromDate.toDate(),
worship.map(w => { tillDate: tillDate.toDate(),
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 blog = await fetchBlogPosts(true)
return map const highlights = await fetchHighlights()
} const announcements = await fetchLastAnnouncements()
const announcementsLinks = announcements ? perParish(announcements) : []
const calendars = await fetchLastCalendars()
export const Home = ({ const calendarsLinks = calendars ? perParish(calendars) : []
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
},
)
return ( return (
<> <HomeView
<Banner /> events={events?.docs || []}
worship={worship?.docs || []}
<Container> blog={blog?.docs || []}
<Section> highlights={highlights?.docs || []}
<MainText announcements={announcementsLinks}
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.`} /> calendars={calendarsLinks}
</Section> publicationAndNewsletter={<PublicationAndNewsletter />}
</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"}
/>
</>
) )
} }
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 chris from "../../assets/christophorus.jpeg"
import { Parish } from './Parish' import { Parish } from './Parish'
import { Menu } from '@/components/Menu/Menu' import { Menu } from '@/components/Menu/Menu'
import { Footer } from '@/compositions/Footer/Footer' import { FooterView } from '@/compositions/Footer/FooterView'
const meta: Meta<typeof Parish> = { const meta: Meta<typeof Parish> = {
component: Parish, component: Parish,
@ -28,7 +28,18 @@ const meta: Meta<typeof Parish> = {
] ]
}}/> }}/>
<Story /> <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: [ results: [
makeResult('1', 'Sonntagsgottesdienst', { makeResult('1', 'Sonntagsgottesdienst', {
relationTo: 'pages', relationTo: 'pages',
value: { value: { id: 'p1', slug: 'gottesdienst' },
id: 'p1', } as unknown as Search['doc']),
slug: 'gottesdienst',
} as Search['doc']['value'],
}),
makeResult('2', 'Ostern 2026 Liturgische Feier', { makeResult('2', 'Ostern 2026 Liturgische Feier', {
relationTo: 'event', relationTo: 'event',
value: { id: 'e1' } as Search['doc']['value'], value: { id: 'e1' },
}), } as unknown as Search['doc']),
makeResult('3', 'Ministrantengruppe', { makeResult('3', 'Ministrantengruppe', {
relationTo: 'group', relationTo: 'group',
value: { value: { id: 'g1', slug: 'ministranten' },
id: 'g1', } as unknown as Search['doc']),
slug: 'ministranten',
} as Search['doc']['value'],
}),
makeResult('4', 'Rundbrief Januar', { makeResult('4', 'Rundbrief Januar', {
relationTo: 'blog', relationTo: 'blog',
value: { id: 'b1' } as Search['doc']['value'], value: { id: 'b1' },
}), } as unknown as Search['doc']),
], ],
}, },
} }
@ -63,4 +57,4 @@ export const EmptyQuery: Story = {
query: '', query: '',
results: [], results: [],
}, },
} }