feat: custom pages

This commit is contained in:
Benno Tielen 2026-03-06 12:46:49 +01:00
parent 3265cf7993
commit ac01546440
54 changed files with 12841 additions and 131 deletions

View file

@ -84,3 +84,13 @@ Open http://localhost:6006
## Site Metadata ## Site Metadata
Site-wide metadata (title, description, keywords, OpenGraph) is configured in `src/config/site.ts`. Update that file to change SEO defaults used in the root layout. Site-wide metadata (title, description, keywords, OpenGraph) is configured in `src/config/site.ts`. Update that file to change SEO defaults used in the root layout.
## A note on updating dependencies
Payload CMS and Next.js are pinned to specific versions. From the Payload docs we currently have the following requirements (March 2026):
> Next.js (one of the following version ranges):
15.2.9 - 15.2.x
15.3.9 - 15.3.x
15.4.11 - 15.4.x
16.2.0-canary.10+

View file

@ -1,9 +1,27 @@
$base-color: #426156; //$base-color: #426156;
$shade1: #728F8D; //$shade1: #728F8D;
$shade2: #CBD6D5; //$shade2: #CBD6D5;
$shade3: #E3E9E8; //$shade3: #E3E9E8;
$contrast-color: #7D1224; //$contrast-color: #7D1224;
$contrast-shade1: #C14953; //$contrast-shade1: #C14953;
//$text-color: #000000;
//$border-radius: 13px;
//
//$white: #ffffff;
//$light-grey: #f3f3f3;
//$border-color-light: #e1e1e1;
//$dark-text: #2c2c2c;
//$placeholder-bg: #c2c2c2;
//$highlight-color: #fff318;
//$shadow: 3px 7px 26px -5px rgba(0, 0, 0, 0.15);
//$overlay: rgba(63, 63, 63, 0.82);
$base-color: #016699;
$shade1: #67A3C2;
$shade2: #DDECF7;
//$shade3: #E3E9E8;
$shade3: #eff6ff;
$contrast-color: #CE490F;
$contrast-shade1: #DA764B;
$text-color: #000000; $text-color: #000000;
$border-radius: 13px; $border-radius: 13px;

86
package-lock.json generated
View file

@ -19,7 +19,7 @@
"graphql": "^16.12.0", "graphql": "^16.12.0",
"languagedetect": "^2.0.0", "languagedetect": "^2.0.0",
"moment": "^2.30.1", "moment": "^2.30.1",
"next": "16.1.6", "next": "15.4.11",
"payload": "^3.74.0", "payload": "^3.74.0",
"qs-esm": "^7.0.3", "qs-esm": "^7.0.3",
"react": "19.2.4", "react": "19.2.4",
@ -45,7 +45,7 @@
"vitest": "^4.0.18" "vitest": "^4.0.18"
}, },
"engines": { "engines": {
"node": "^18.20.2 || >=20.9.0" "node": ">=22.0.0"
} }
}, },
"node_modules/@adobe/css-tools": { "node_modules/@adobe/css-tools": {
@ -1899,7 +1899,9 @@
} }
}, },
"node_modules/@next/env": { "node_modules/@next/env": {
"version": "16.1.6", "version": "15.4.11",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.4.11.tgz",
"integrity": "sha512-mIYp/091eYfPFezKX7ZPTWqrmSXq+ih6+LcUyKvLmeLQGhlPtot33kuEOd4U+xAA7sFfj21+OtCpIZx0g5SpvQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@next/eslint-plugin-next": { "node_modules/@next/eslint-plugin-next": {
@ -1911,9 +1913,9 @@
} }
}, },
"node_modules/@next/swc-darwin-arm64": { "node_modules/@next/swc-darwin-arm64": {
"version": "16.1.6", "version": "15.4.8",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz", "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.4.8.tgz",
"integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==", "integrity": "sha512-Pf6zXp7yyQEn7sqMxur6+kYcywx5up1J849psyET7/8pG2gQTVMjU3NzgIt8SeEP5to3If/SaWmaA6H6ysBr1A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1927,9 +1929,9 @@
} }
}, },
"node_modules/@next/swc-darwin-x64": { "node_modules/@next/swc-darwin-x64": {
"version": "16.1.6", "version": "15.4.8",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz", "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.4.8.tgz",
"integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==", "integrity": "sha512-xla6AOfz68a6kq3gRQccWEvFC/VRGJmA/QuSLENSO7CZX5WIEkSz7r1FdXUjtGCQ1c2M+ndUAH7opdfLK1PQbw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1943,9 +1945,9 @@
} }
}, },
"node_modules/@next/swc-linux-arm64-gnu": { "node_modules/@next/swc-linux-arm64-gnu": {
"version": "16.1.6", "version": "15.4.8",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.4.8.tgz",
"integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==", "integrity": "sha512-y3fmp+1Px/SJD+5ntve5QLZnGLycsxsVPkTzAc3zUiXYSOlTPqT8ynfmt6tt4fSo1tAhDPmryXpYKEAcoAPDJw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1959,9 +1961,9 @@
} }
}, },
"node_modules/@next/swc-linux-arm64-musl": { "node_modules/@next/swc-linux-arm64-musl": {
"version": "16.1.6", "version": "15.4.8",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.4.8.tgz",
"integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==", "integrity": "sha512-DX/L8VHzrr1CfwaVjBQr3GWCqNNFgyWJbeQ10Lx/phzbQo3JNAxUok1DZ8JHRGcL6PgMRgj6HylnLNndxn4Z6A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1975,7 +1977,9 @@
} }
}, },
"node_modules/@next/swc-linux-x64-gnu": { "node_modules/@next/swc-linux-x64-gnu": {
"version": "16.1.6", "version": "15.4.8",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.4.8.tgz",
"integrity": "sha512-9fLAAXKAL3xEIFdKdzG5rUSvSiZTLLTCc6JKq1z04DR4zY7DbAPcRvNm3K1inVhTiQCs19ZRAgUerHiVKMZZIA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1989,9 +1993,9 @@
} }
}, },
"node_modules/@next/swc-linux-x64-musl": { "node_modules/@next/swc-linux-x64-musl": {
"version": "16.1.6", "version": "15.4.8",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.4.8.tgz",
"integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==", "integrity": "sha512-s45V7nfb5g7dbS7JK6XZDcapicVrMMvX2uYgOHP16QuKH/JA285oy6HcxlKqwUNaFY/UC6EvQ8QZUOo19cBKSA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2005,9 +2009,9 @@
} }
}, },
"node_modules/@next/swc-win32-arm64-msvc": { "node_modules/@next/swc-win32-arm64-msvc": {
"version": "16.1.6", "version": "15.4.8",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.4.8.tgz",
"integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==", "integrity": "sha512-KjgeQyOAq7t/HzAJcWPGA8X+4WY03uSCZ2Ekk98S9OgCFsb6lfBE3dbUzUuEQAN2THbwYgFfxX2yFTCMm8Kehw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -2021,9 +2025,9 @@
} }
}, },
"node_modules/@next/swc-win32-x64-msvc": { "node_modules/@next/swc-win32-x64-msvc": {
"version": "16.1.6", "version": "15.4.8",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.4.8.tgz",
"integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==", "integrity": "sha512-Exsmf/+42fWVnLMaZHzshukTBxZrSwuuLKFvqhGHJ+mC1AokqieLY/XzAl3jc/CqhXLqLY3RRjkKJ9YnLPcRWg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -5738,6 +5742,7 @@
}, },
"node_modules/baseline-browser-mapping": { "node_modules/baseline-browser-mapping": {
"version": "2.9.19", "version": "2.9.19",
"dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"baseline-browser-mapping": "dist/cli.js" "baseline-browser-mapping": "dist/cli.js"
@ -13045,12 +13050,13 @@
"peer": true "peer": true
}, },
"node_modules/next": { "node_modules/next": {
"version": "16.1.6", "version": "15.4.11",
"resolved": "https://registry.npmjs.org/next/-/next-15.4.11.tgz",
"integrity": "sha512-IJRyXal45mIsshZI5XJne/intjusslUP1F+FHVBIyMGEqbYtIq1Irdx5vdWBBg58smviPDycmDeV6txsfkv1RQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@next/env": "16.1.6", "@next/env": "15.4.11",
"@swc/helpers": "0.5.15", "@swc/helpers": "0.5.15",
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001579", "caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31", "postcss": "8.4.31",
"styled-jsx": "5.1.6" "styled-jsx": "5.1.6"
@ -13059,18 +13065,18 @@
"next": "dist/bin/next" "next": "dist/bin/next"
}, },
"engines": { "engines": {
"node": ">=20.9.0" "node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@next/swc-darwin-arm64": "16.1.6", "@next/swc-darwin-arm64": "15.4.8",
"@next/swc-darwin-x64": "16.1.6", "@next/swc-darwin-x64": "15.4.8",
"@next/swc-linux-arm64-gnu": "16.1.6", "@next/swc-linux-arm64-gnu": "15.4.8",
"@next/swc-linux-arm64-musl": "16.1.6", "@next/swc-linux-arm64-musl": "15.4.8",
"@next/swc-linux-x64-gnu": "16.1.6", "@next/swc-linux-x64-gnu": "15.4.8",
"@next/swc-linux-x64-musl": "16.1.6", "@next/swc-linux-x64-musl": "15.4.8",
"@next/swc-win32-arm64-msvc": "16.1.6", "@next/swc-win32-arm64-msvc": "15.4.8",
"@next/swc-win32-x64-msvc": "16.1.6", "@next/swc-win32-x64-msvc": "15.4.8",
"sharp": "^0.34.4" "sharp": "^0.34.3"
}, },
"peerDependencies": { "peerDependencies": {
"@opentelemetry/api": "^1.1.0", "@opentelemetry/api": "^1.1.0",
@ -13684,10 +13690,6 @@
"graphql": "^16.8.1" "graphql": "^16.8.1"
} }
}, },
"node_modules/payload/node_modules/@next/env": {
"version": "15.1.7",
"license": "MIT"
},
"node_modules/payload/node_modules/file-type": { "node_modules/payload/node_modules/file-type": {
"version": "19.3.0", "version": "19.3.0",
"license": "MIT", "license": "MIT",

View file

@ -28,7 +28,7 @@
"graphql": "^16.12.0", "graphql": "^16.12.0",
"languagedetect": "^2.0.0", "languagedetect": "^2.0.0",
"moment": "^2.30.1", "moment": "^2.30.1",
"next": "16.1.6", "next": "15.4.11",
"payload": "^3.74.0", "payload": "^3.74.0",
"qs-esm": "^7.0.3", "qs-esm": "^7.0.3",
"react": "19.2.4", "react": "19.2.4",

View file

@ -0,0 +1,37 @@
import { notFound } from 'next/navigation'
import { fetchPageBySlug } from '@/fetch/pages'
import { Blocks } from '@/compositions/Blocks/Blocks'
import { Metadata } from 'next'
type Props = {
params: Promise<{ slug: string }>
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const slug = (await params).slug
const page = await fetchPageBySlug(slug)
if (!page) return {}
return {
title: page.title,
description: page.description || undefined,
}
}
export default async function DynamicPage({ params }: Props) {
const slug = (await params).slug
const page = await fetchPageBySlug(slug)
if (!page) {
notFound()
}
return (
<>
{page.content && page.content.length > 0 && (
<Blocks content={page.content} />
)}
</>
)
}

View file

@ -1,9 +1,9 @@
import type { Metadata } from 'next' import type { Metadata } from 'next'
import { defaultFont } from '@/assets/fonts'
import './globals.css' import './globals.css'
import { DynamicMenu, Menu } from '@/components/Menu/Menu' import { DynamicMenu, Menu } from '@/components/Menu/Menu'
import { Footer } from '@/compositions/Footer/Footer' import { Footer } from '@/compositions/Footer/Footer'
import { comment } from '@/app/(home)/layout-comment' import { comment } from '@/app/(home)/layout-comment'
import { defaultFont } from '@/assets/fonts'
import { siteConfig } from '@/config/site' import { siteConfig } from '@/config/site'
export const metadata: Metadata = { export const metadata: Metadata = {

View file

@ -1,10 +1,9 @@
import { Faustina, Cairo } from 'next/font/google' import { Cairo, Faustina } from 'next/font/google'
export const faustina = Faustina({ export const headerFont = Faustina({
subsets: ['latin'], subsets: ['latin'],
display: 'swap', display: 'swap',
}) })
export const defaultFont = Cairo({ export const defaultFont = Cairo({
subsets: ['latin'], subsets: ['latin'],
weight: ['400', '300'], weight: ['400', '300'],

106
src/collections/Pages.ts Normal file
View file

@ -0,0 +1,106 @@
import { CollectionConfig } from 'payload'
import { revalidateTag } from 'next/cache'
import { hide, isAdminOrEmployee } from '@/collections/access/admin'
import { ParagraphBlock } from '@/collections/blocks/Paragraph'
import { DocumentBlock } from '@/collections/blocks/Document'
import { ContactformBlock } from '@/collections/blocks/Contactform'
import { GalleryBlock } from '@/collections/blocks/Gallery'
import { DonationBlock } from '@/collections/blocks/Donation'
import { ButtonBlock } from '@/collections/blocks/Button'
import { YoutubePlayerBlock } from '@/collections/blocks/YoutubePlayer'
import { PageHeaderBlock } from '@/collections/blocks/PageHeader'
import { SectionBlock } from '@/collections/blocks/Section'
import { TitleBlock } from '@/collections/blocks/Title'
import { BannerBlock } from '@/collections/blocks/Banner'
import { MainTextBlock } from '@/collections/blocks/MainText'
import { HorizontalRuleBlock } from '@/collections/blocks/HorizontalRule'
import { BlogSliderBlock } from '@/collections/blocks/BlogSlider'
import { MassTimesBlock } from '@/collections/blocks/MassTimes'
import { CollapsibleImageWithTextBlock } from '@/collections/blocks/CollapsibleImageWithText'
import { EventsBlock } from '@/collections/blocks/Events'
import { PublicationAndNewsletterBlock } from '@/collections/blocks/PublicationAndNewsletter'
export const Pages: CollectionConfig = {
slug: 'pages',
labels: {
singular: {
de: 'Seite',
},
plural: {
de: 'Seiten',
},
},
fields: [
{
name: 'title',
type: 'text',
required: true,
label: {
de: 'Titel',
},
},
{
name: 'description',
type: 'textarea',
label: {
de: 'Beschreibung',
},
},
{
name: 'slug',
type: 'text',
required: true,
unique: true,
label: {
de: 'Slug',
},
admin: {
description: 'URL-Pfad der Seite (z.B. "meine-seite" → /meine-seite)',
},
},
{
name: 'content',
type: 'blocks',
label: {
de: 'Inhalt',
},
blocks: [
PageHeaderBlock,
ParagraphBlock,
TitleBlock,
SectionBlock,
GalleryBlock,
DocumentBlock,
YoutubePlayerBlock,
ButtonBlock,
ContactformBlock,
DonationBlock,
BannerBlock,
MainTextBlock,
HorizontalRuleBlock,
BlogSliderBlock,
MassTimesBlock,
CollapsibleImageWithTextBlock,
EventsBlock,
PublicationAndNewsletterBlock,
],
},
],
admin: {
useAsTitle: 'title',
hidden: hide,
},
access: {
read: () => true,
create: isAdminOrEmployee(),
update: isAdminOrEmployee(),
delete: isAdminOrEmployee(),
},
hooks: {
afterChange: [
({ doc }) => {
if (doc.slug) revalidateTag(`pages-${doc.slug}`)
},
],
},
}

View file

@ -0,0 +1,83 @@
import { Block } from 'payload'
export const BannerBlock: Block = {
slug: 'banner',
labels: {
singular: {
de: 'Banner',
},
plural: {
de: 'Banner',
},
},
fields: [
{
name: 'textLine1',
type: 'text',
label: {
de: 'Textzeile 1',
},
},
{
name: 'textLine2',
type: 'text',
label: {
de: 'Textzeile 2',
},
},
{
name: 'textLine3',
type: 'text',
label: {
de: 'Textzeile 3',
},
},
{
name: 'backgroundColor',
type: 'text',
label: {
de: 'Hintergrundfarbe',
},
},
{
name: 'backgroundImage',
type: 'upload',
relationTo: 'media',
label: {
de: 'Hintergrundbild',
},
},
{
name: 'backgroundPosition',
type: 'select',
label: {
de: 'Hintergrundposition',
},
defaultValue: 'center center',
options: [
{ label: 'Mitte', value: 'center center' },
{ label: 'Oben Mitte', value: 'top center' },
{ label: 'Unten Mitte', value: 'bottom center' },
{ label: 'Links Mitte', value: 'center left' },
{ label: 'Rechts Mitte', value: 'center right' },
{ label: 'Oben Links', value: 'top left' },
{ label: 'Oben Rechts', value: 'top right' },
{ label: 'Unten Links', value: 'bottom left' },
{ label: 'Unten Rechts', value: 'bottom right' },
],
},
{
name: 'backgroundSize',
type: 'select',
label: {
de: 'Hintergrundgröße',
},
defaultValue: 'cover',
options: [
{ label: 'Abdecken (cover)', value: 'cover' },
{ label: 'Einpassen (contain)', value: 'contain' },
{ label: 'Automatisch (auto)', value: 'auto' },
],
},
],
}

View file

@ -0,0 +1,23 @@
import { Block } from 'payload'
export const BlogSliderBlock: Block = {
slug: 'blogSlider',
labels: {
singular: {
de: 'Blog-Slider',
},
plural: {
de: 'Blog-Slider',
},
},
fields: [
{
name: 'title',
type: 'text',
label: {
de: 'Titel',
},
defaultValue: 'Aktuelles',
},
],
}

View file

@ -0,0 +1,75 @@
import { Block } from 'payload'
import { lexicalHTML } from '@payloadcms/richtext-lexical'
export const CollapsibleImageWithTextBlock: Block = {
slug: 'collapsibleImageWithText',
labels: {
singular: {
de: 'Aufklappbarer Bildtext',
},
plural: {
de: 'Aufklappbare Bildtexte',
},
},
fields: [
{
name: 'title',
type: 'text',
required: true,
label: {
de: 'Titel',
},
},
{
name: 'text',
type: 'textarea',
required: true,
label: {
de: 'Text',
},
},
{
name: 'image',
type: 'upload',
relationTo: 'media',
required: true,
label: {
de: 'Bild',
},
},
{
name: 'content',
type: 'richText',
required: true,
label: {
de: 'Aufklappbarer Inhalt',
},
},
lexicalHTML('content', { name: 'content_html' }),
{
name: 'backgroundColor',
type: 'select',
label: {
de: 'Hintergrundfarbe',
},
options: [
{ label: 'Keine', value: 'none' },
{ label: 'Soft', value: 'soft' },
{ label: 'Off-White', value: 'off-white' },
],
defaultValue: 'none',
},
{
name: 'schema',
type: 'select',
label: {
de: 'Farbschema',
},
options: [
{ label: 'Base', value: 'base' },
{ label: 'Kontrast', value: 'contrast' },
],
defaultValue: 'base',
},
],
}

View file

@ -1,4 +1,5 @@
import { Block } from 'payload' import { Block } from 'payload'
import { siteConfig } from '@/config/site'
export const ContactformBlock: Block = { export const ContactformBlock: Block = {
slug: 'contactform', slug: 'contactform',
@ -32,7 +33,7 @@ export const ContactformBlock: Block = {
{ {
name: 'email', name: 'email',
type: 'email', type: 'email',
defaultValue: "kontakt@dreikoenige.berlin", defaultValue: siteConfig.email,
required: true required: true
} }
] ]

View file

@ -0,0 +1,33 @@
import { Block } from 'payload'
export const EventsBlock: Block = {
slug: 'events',
labels: {
singular: {
de: 'Veranstaltungen',
},
plural: {
de: 'Veranstaltungen',
},
},
fields: [
{
name: 'title',
type: 'text',
label: {
de: 'Titel',
},
defaultValue: 'Veranstaltungen',
},
{
name: 'itemsPerPage',
type: 'number',
label: {
de: 'Einträge pro Seite',
},
defaultValue: 6,
min: 1,
max: 20,
},
],
}

View file

@ -0,0 +1,32 @@
import { Block } from 'payload'
export const HorizontalRuleBlock: Block = {
slug: 'horizontalRule',
labels: {
singular: {
de: 'Trennlinie',
},
plural: {
de: 'Trennlinien',
},
},
fields: [
{
name: 'color',
type: 'select',
label: {
de: 'Farbe',
},
required: true,
defaultValue: 'base',
options: [
{ label: 'Grundfarbe', value: 'base' },
{ label: 'Abstufung 1', value: 'shade1' },
{ label: 'Abstufung 2', value: 'shade2' },
{ label: 'Abstufung 3', value: 'shade3' },
{ label: 'Kontrastfarbe', value: 'contrast' },
{ label: 'Kontrast Abstufung 1', value: 'contrastShade1' },
],
},
],
}

View file

@ -0,0 +1,25 @@
import { Block } from 'payload'
export const MainTextBlock: Block = {
slug: 'mainText',
labels: {
singular: {
de: 'Haupttext',
},
plural: {
de: 'Haupttexte',
},
},
fields: [
{
name: 'text',
type: 'textarea',
required: true,
defaultValue:
'Jesus sagte zu ihm: Ich bin der Weg und die Wahrheit und das Leben; niemand kommt zum Vater außer durch mich. Wenn ihr mich erkannt habt, werdet ihr auch meinen Vater erkennen.',
label: {
de: 'Text',
},
},
],
}

View file

@ -0,0 +1,30 @@
import { Block } from 'payload'
export const MassTimesBlock: Block = {
slug: 'massTimes',
labels: {
singular: {
de: 'Gottesdienste',
},
plural: {
de: 'Gottesdienste',
},
},
fields: [
{
name: 'title',
type: 'text',
label: {
de: 'Titel',
},
defaultValue: 'Nächste Gottesdienste',
},
{
name: 'subtitle',
type: 'text',
label: {
de: 'Untertitel',
},
},
],
}

View file

@ -0,0 +1,39 @@
import { Block } from 'payload'
export const PageHeaderBlock: Block = {
slug: 'pageHeader',
labels: {
singular: {
de: 'Seitenüberschrift',
},
plural: {
de: 'Seitenüberschriften',
},
},
fields: [
{
name: 'title',
type: 'text',
required: true,
label: {
de: 'Titel',
},
},
{
name: 'description',
type: 'textarea',
required: true,
label: {
de: 'Beschreibung',
},
},
{
name: 'image',
type: 'upload',
relationTo: 'media',
label: {
de: 'Bild',
},
},
],
}

View file

@ -0,0 +1,14 @@
import { Block } from 'payload'
export const PublicationAndNewsletterBlock: Block = {
slug: 'publicationAndNewsletter',
labels: {
singular: {
de: 'Publikation & Newsletter',
},
plural: {
de: 'Publikation & Newsletter',
},
},
fields: [],
}

View file

@ -0,0 +1,41 @@
import { Block } from 'payload'
export const SectionBlock: Block = {
slug: 'section',
labels: {
singular: {
de: 'Abschnitt',
},
plural: {
de: 'Abschnitte',
},
},
fields: [
{
name: 'backgroundColor',
type: 'select',
label: {
de: 'Hintergrundfarbe',
},
options: [
{ label: 'Keine', value: 'none' },
{ label: 'Soft', value: 'soft' },
{ label: 'Off-White', value: 'off-white' },
],
defaultValue: 'none',
},
{
name: 'padding',
type: 'select',
label: {
de: 'Abstand',
},
options: [
{ label: 'Klein', value: 'small' },
{ label: 'Mittel', value: 'medium' },
{ label: 'Groß', value: 'large' },
],
defaultValue: 'large',
},
],
}

View file

@ -0,0 +1,56 @@
import { Block } from 'payload'
export const TitleBlock: Block = {
slug: 'title',
labels: {
singular: {
de: 'Titel',
},
plural: {
de: 'Titel',
},
},
fields: [
{
name: 'title',
type: 'text',
required: true,
label: {
de: 'Titel',
},
},
{
name: 'subtitle',
type: 'text',
label: {
de: 'Untertitel',
},
},
{
name: 'size',
type: 'select',
label: {
de: 'Größe',
},
options: [
{ label: 'XL', value: 'xl' },
{ label: 'Groß', value: 'lg' },
{ label: 'Mittel', value: 'md' },
{ label: 'Klein', value: 'sm' },
],
defaultValue: 'lg',
},
{
name: 'align',
type: 'select',
label: {
de: 'Ausrichtung',
},
options: [
{ label: 'Links', value: 'left' },
{ label: 'Zentriert', value: 'center' },
],
defaultValue: 'left',
},
],
}

View file

@ -1,10 +1,12 @@
import styles from './styles.module.scss'
type ArrowProps = { type ArrowProps = {
schema?: 'base' | 'contrast', schema?: 'base' | 'contrast'
direction: 'left' | 'right', direction: 'left' | 'right'
onClick?: () => void onClick?: () => void
} }
export const Arrow = ({ direction, onClick, schema = "base" }: ArrowProps) => { export const Arrow = ({ direction, onClick, schema = 'base' }: ArrowProps) => {
return ( return (
<svg <svg
width="21" width="21"
@ -13,7 +15,8 @@ export const Arrow = ({ direction, onClick, schema = "base" }: ArrowProps) => {
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
style={{ transform: `rotate(${direction === 'left' ? 0 : 180}deg)` }} style={{ transform: `rotate(${direction === 'left' ? 0 : 180}deg)` }}
stroke={schema === "base" ? '#426156' : '#7D1224'} stroke="currentColor"
className={styles[schema]}
onClick={onClick} onClick={onClick}
> >
<path d="M19 1.53406L2.68974 27.0243C2.26927 27.6814 2.26927 28.5231 2.68974 29.1802L19 54.6704" <path d="M19 1.53406L2.68974 27.0243C2.26927 27.6814 2.26927 28.5231 2.68974 29.1802L19 54.6704"

View file

@ -0,0 +1,9 @@
@import 'template.scss';
.base {
color: $base-color;
}
.contrast {
color: $contrast-color;
}

View file

@ -5,9 +5,33 @@ const meta: Meta<typeof Banner> = {
component: Banner, component: Banner,
} }
type Story = StoryObj<typeof Banner>; type Story = StoryObj<typeof Banner>
export default meta export default meta
export const Default: Story = { export const Default: Story = {
args: {}, args: {},
} }
export const CustomText: Story = {
args: {
textLine1: 'Willkommen in der',
textLine2: 'Gemeinde',
textLine3: 'Berlin-Neukölln',
},
}
export const CustomBackground: Story = {
args: {
backgroundColor: '#2a4a7f',
backgroundSize: 'contain',
backgroundPosition: 'top left',
},
}
export const BottomRight: Story = {
args: {
backgroundColor: '#4a2a7f',
backgroundSize: 'cover',
backgroundPosition: 'bottom right',
},
}

View file

@ -1,18 +1,54 @@
import { Logo } from '@/components/Logo/Logo' import { Logo } from '@/components/Logo/Logo'
import styles from "./styles.module.scss" import styles from "./styles.module.scss"
import classNames from 'classnames' import classNames from 'classnames'
import { faustina } from '@/assets/fonts'
export const Banner = () => { import { headerFont } from '@/assets/fonts'
export interface BannerProps {
textLine1?: string | null
textLine2?: string | null
textLine3?: string | null
backgroundColor?: string | null
backgroundImage?: string | null
backgroundPosition?:
| 'center center'
| 'top center'
| 'bottom center'
| 'center left'
| 'center right'
| 'top left'
| 'top right'
| 'bottom left'
| 'bottom right'
| null
backgroundSize?: 'cover' | 'contain' | 'auto' | null
}
export const Banner = ({
textLine1 = 'Katholische Pfarrei',
textLine2 = 'Heilige Drei Könige',
textLine3 = 'Nord-Neukölln',
backgroundColor,
backgroundImage,
backgroundPosition = 'center center',
backgroundSize = 'cover',
}: BannerProps) => {
const bannerStyle: React.CSSProperties = {
...(backgroundColor && { backgroundColor }),
...(backgroundImage && { backgroundImage: `url(${backgroundImage})` }),
...(backgroundPosition && { backgroundPosition }),
...(backgroundSize && { backgroundSize }),
}
return ( return (
<div className={styles.banner}> <div className={styles.banner} style={bannerStyle}>
<div className={styles.logo}> <div className={styles.logo}>
<Logo color={"#ffffff33"} height={200} /> <Logo color={"#ffffff33"} height={200} />
</div> </div>
<div className={classNames(faustina.className, styles.nameContainer)}> <div className={classNames(headerFont.className, styles.nameContainer)}>
<div className={styles.catholic}>Katholische Pfarrei</div> {textLine1 && <div className={styles.catholic}>{textLine1}</div>}
<div className={styles.name}>Heilige Drei Könige</div> {textLine2 && <div className={styles.name}>{textLine2}</div>}
<div className={styles.location}>Nord-Neukölln</div> {textLine3 && <div className={styles.location}>{textLine3}</div>}
</div> </div>
</div> </div>
) )

View file

@ -2,10 +2,10 @@
import classNames from 'classnames' import classNames from 'classnames'
import styles from '@/components/Classifieds/styles.module.scss' import styles from '@/components/Classifieds/styles.module.scss'
import { faustina } from '@/assets/fonts'
import { useState } from 'react' import { useState } from 'react'
import { SerializedEditorState } from 'lexical' import { SerializedEditorState } from 'lexical'
import { RichText } from '@payloadcms/richtext-lexical/react' import { RichText } from '@payloadcms/richtext-lexical/react'
import { headerFont } from '@/assets/fonts'
type AdProps = { type AdProps = {
text: SerializedEditorState, text: SerializedEditorState,
@ -18,7 +18,7 @@ export const Ad = ({text, contact}: AdProps) => {
return ( return (
<> <>
<div className={classNames(styles.ad, faustina.className)} onClick={() => setDisplayContact(!displayContact)}> <div className={classNames(styles.ad, headerFont.className)} onClick={() => setDisplayContact(!displayContact)}>
<div className={styles.adText}> <div className={styles.adText}>
<RichText data={text} /> <RichText data={text} />
</div> </div>

View file

@ -10,12 +10,12 @@ export default meta
export const Default: Story = { export const Default: Story = {
args: { args: {
schema: "base" color: 'base',
}, },
} }
export const Contrast: Story = { export const Contrast: Story = {
args: { args: {
schema: "contrast" color: 'contrast',
}, },
} }

View file

@ -1,15 +1,19 @@
import styles from "./styles.module.scss" import styles from './styles.module.scss'
import classNames from 'classnames'
export type ColorOption =
| 'base'
| 'shade1'
| 'shade2'
| 'shade3'
| 'contrast'
| 'contrastShade1'
type CrossProps = { type CrossProps = {
schema?: "base" | "contrast" color?: ColorOption
} }
export const Cross = ({schema = "base"}: CrossProps) => { export const Cross = ({ color = 'base' }: CrossProps) => {
const style = classNames({ const style = styles[color]
[styles.crossContrast]: schema === "contrast",
[styles.crossBase]: schema === "base",
})
return ( return (
<svg <svg

View file

@ -1,9 +1,25 @@
@import "template.scss"; @import 'template.scss';
.crossContrast { .base {
fill: $base-color;
}
.shade1 {
fill: $shade1;
}
.shade2 {
fill: $shade2;
}
.shade3 {
fill: $shade3;
}
.contrast {
fill: $contrast-color; fill: $contrast-color;
} }
.crossBase { .contrastShade1 {
fill: $base-color; fill: $contrast-shade1;
} }

View file

@ -0,0 +1,13 @@
import { Meta, StoryObj } from '@storybook/react'
import { Dropdown } from './Dropdown'
const meta: Meta<typeof Dropdown> = {
component: Dropdown,
}
type Story = StoryObj<typeof Dropdown>;
export default meta
export const Default: Story = {
args: {},
}

View file

@ -0,0 +1,25 @@
"use client"
import { Button } from '@/components/Button/Button'
import { useState } from 'react'
import styles from './styles.module.scss'
import classNames from 'classnames'
export const Dropdown = () => {
const [isOpen, setIsOpen] = useState(false)
return (
<div className={styles.container}>
<Button
size={"md"}
schema={"shade"}
onClick={() => setIsOpen(!isOpen)}
>Vermeldungen</Button>
<div className={classNames(styles.options, isOpen ? styles.open : styles.closed)}>
<a href={""}>St. Clara</a>
<a href={""}>St. Richard</a>
<a href={""}>St Christophorus</a>
</div>
</div>
)
}

View file

@ -0,0 +1,40 @@
@import "template";
.container {
display: inline-block;
position: relative;
}
.options {
width: 130px;
font-size: 14px;
background-color: white;
position: absolute;
top: 60px;
border: 1px solid seashell;
border-radius: 4px;
transition: opacity 100ms ease-in;
display: none;
opacity: 0;
}
.open {
display: block;
opacity: 1;
}
.closed {
display: none;
}
.options a {
display: block;
color: inherit;
padding: 10px 15px;
text-decoration: none;
transition: background-color 0.2s;
}
.options a:hover {
background-color: $shade2;
}

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,16 @@
import styles from "./styles.module.scss" import styles from './styles.module.scss'
import { Cross } from '@/components/Cross/Cross' import { Cross, ColorOption } from '@/components/Cross/Cross'
export const HR = () => { type HRProps = {
color?: ColorOption
}
export const HR = ({ color = 'base' }: HRProps) => {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.line}></div> <div className={`${styles.line} ${styles[color]}`}></div>
<Cross /> <Cross color={color} />
<div className={styles.line}></div> <div className={`${styles.line} ${styles[color]}`}></div>
</div> </div>
) )
} }

View file

@ -1,4 +1,4 @@
@import "template.scss"; @import 'template.scss';
.container { .container {
display: flex; display: flex;
@ -10,6 +10,30 @@
border-bottom: 1.5px solid $base-color; border-bottom: 1.5px solid $base-color;
flex: 1 1 0; flex: 1 1 0;
height: 28px; height: 28px;
&.base {
border-color: $base-color;
}
&.shade1 {
border-color: $shade1;
}
&.shade2 {
border-color: $shade2;
}
&.shade3 {
border-color: $shade3;
}
&.contrast {
border-color: $contrast-color;
}
&.contrastShade1 {
border-color: $contrast-shade1;
}
} }
@media screen and (max-width: 1100px) { @media screen and (max-width: 1100px) {

View file

@ -1,6 +1,6 @@
import styles from "./styles.module.scss" import styles from "./styles.module.scss"
import { faustina } from '@/assets/fonts'
import Link from 'next/link' import Link from 'next/link'
import { headerFont } from '@/assets/fonts'
type MegaMenuProps = { type MegaMenuProps = {
bibleText: string, bibleText: string,
@ -63,7 +63,7 @@ export const MegaMenu = ({ bibleText, bibleBook, groups, onItemClick }: MegaMenu
return ( return (
<div className={styles.menu}> <div className={styles.menu}>
<div className={styles.bibleText}> <div className={styles.bibleText}>
<div className={faustina.className}> <div className={headerFont.className}>
{bibleText} {bibleText}
</div> </div>
<div className={styles.book}> <div className={styles.book}>

View file

@ -1,7 +1,8 @@
import styles from './styles.module.scss' import styles from './styles.module.scss'
import { Container } from '@/components/Container/Container' import { Container } from '@/components/Container/Container'
import classNames from 'classnames' import classNames from 'classnames'
import { faustina } from '@/assets/fonts'
import { headerFont } from '@/assets/fonts'
type TestimonyProps = { type TestimonyProps = {
name?: string name?: string
@ -14,7 +15,7 @@ export const Testimony = ({ name, testimony, occupation }: TestimonyProps) => {
<Container> <Container>
<div className={styles.testimony}> <div className={styles.testimony}>
<div className={styles.container}> <div className={styles.container}>
<p className={classNames(styles.testimonyText, faustina.className)}> <p className={classNames(styles.testimonyText, headerFont.className)}>
{testimony} {testimony}
</p> </p>
{typeof name === 'string' && {typeof name === 'string' &&

View file

@ -1,6 +1,7 @@
import styles from "./styles.module.scss"; import styles from "./styles.module.scss";
import classNames from 'classnames' import classNames from 'classnames'
import { faustina } from '@/assets/fonts'
import { headerFont } from '@/assets/fonts'
type TitleProps = { type TitleProps = {
title: string; title: string;
@ -26,7 +27,7 @@ export const Title = ({title, subtitle, align = "left", size = "lg", fontStyle =
[styles.small]: size === "sm", [styles.small]: size === "sm",
[styles.left]: align === "left", [styles.left]: align === "left",
[styles.center]: align === "center", [styles.center]: align === "center",
[faustina.className]: fontStyle == "serif", [headerFont.className]: fontStyle == "serif",
[styles.cancelled]: cancelled [styles.cancelled]: cancelled
})}>{title}</h2> })}>{title}</h2>
{subtitle && {subtitle &&
@ -38,7 +39,7 @@ export const Title = ({title, subtitle, align = "left", size = "lg", fontStyle =
[styles.small]: ["xl", "lg"].includes(size), [styles.small]: ["xl", "lg"].includes(size),
[styles.left]: align === "left", [styles.left]: align === "left",
[styles.center]: align === "center", [styles.center]: align === "center",
[faustina.className]: fontStyle == "sans-serif" [headerFont.className]: fontStyle == "sans-serif"
})}> })}>
{subtitle} {subtitle}
</div> </div>

View file

@ -1,4 +1,4 @@
import { Blog } from "@/payload-types" import { Blog, Page } from '@/payload-types'
import { Container } from '@/components/Container/Container' import { Container } from '@/components/Container/Container'
import { HTMLText } from '@/components/Text/HTMLText' import { HTMLText } from '@/components/Text/HTMLText'
import { Section } from '@/components/Section/Section' import { Section } from '@/components/Section/Section'
@ -8,22 +8,34 @@ import { Gallery } from '@/components/Gallery/Gallery'
import { transformGallery } from '@/utils/dto/gallery' import { transformGallery } from '@/utils/dto/gallery'
import { DonationForm } from '@/components/DonationForm/DonationForm' import { DonationForm } from '@/components/DonationForm/DonationForm'
import { YoutubePlayer } from '@/components/YoutubePlayer/YoutubePlayer' import { YoutubePlayer } from '@/components/YoutubePlayer/YoutubePlayer'
import { PageHeader } from '@/compositions/PageHeader/PageHeader'
import { Title } from '@/components/Title/Title'
import { Row } from '@/components/Flex/Row'
import { Col } from '@/components/Flex/Col'
import { Banner } from '@/components/Banner/Banner'
import { MainText } from '@/components/MainText/MainText'
import { HR } from '@/components/HorizontalRule/HorizontalRule'
import { CollapsibleImageWithText } from '@/compositions/CollapsibleImageWithText/CollapsibleImageWithText'
import { PublicationAndNewsletter } from '@/compositions/PublicationAndNewsletter/PublicationAndNewsletter'
import { BlogSliderBlock } from '@/compositions/Blocks/BlogSliderBlock'
import { MassTimesBlock } from '@/compositions/Blocks/MassTimesBlock'
import { EventsBlock } from '@/compositions/Blocks/EventsBlock'
type BlocksProps = { type BlocksProps = {
content: Blog['content']['content'] content: Blog['content']['content'] | NonNullable<Page['content']>
} }
export function Blocks({ content }: BlocksProps) { export function Blocks({ content }: BlocksProps) {
// determine if some margin at the bottom should be added // determine if some margin at the bottom should be added
const length = content.length; const length = content.length;
const shouldAddMargin = content[length - 1].blockType === "text" const shouldAddMargin = content[length - 1].blockType === 'text'
return ( return (
<> <>
<div> <div>
{content.map(item => { {content.map(item => {
if (item.blockType === "text" && item.content_html) { if (item.blockType === 'text' && item.content_html) {
return ( return (
<Container key={item.id}> <Container key={item.id}>
<HTMLText width={item.width} html={item.content_html} /> <HTMLText width={item.width} html={item.content_html} />
@ -31,29 +43,29 @@ export function Blocks({ content }: BlocksProps) {
); );
} }
if (item.blockType === "document" && typeof item.file === "object") { if (item.blockType === 'document' && typeof item.file === 'object') {
return ( return (
<Container key={item.id}> <Container key={item.id}>
<Section padding={"medium"}> <Section padding={'medium'}>
<Button size={"lg"} href={item.file.url || "notfound"} schema={"contrast"}>{item.button}</Button> <Button size={'lg'} href={item.file.url || 'notfound'} schema={'contrast'}>{item.button}</Button>
</Section> </Section>
</Container> </Container>
) )
} }
if (item.blockType === "contactform") { if (item.blockType === 'contactform') {
return ( return (
<ContactSection <ContactSection
key={item.id} key={item.id}
title={item.title} title={item.title}
description={item.description} description={item.description}
toEmail={item.email} toEmail={item.email}
backgroundColor={"off-white"} backgroundColor={'off-white'}
/> />
) )
} }
if (item.blockType === "gallery") { if (item.blockType === 'gallery') {
return ( return (
<Section key={item.id}> <Section key={item.id}>
<Gallery items={transformGallery(item.items)} /> <Gallery items={transformGallery(item.items)} />
@ -61,34 +73,165 @@ export function Blocks({ content }: BlocksProps) {
) )
} }
if (item.blockType === "donation") { if (item.blockType === 'donation') {
return <Section key={item.id} padding={"small"} paddingBottom={"large"}> return <Section key={item.id} padding={'small'} paddingBottom={'large'}>
<DonationForm /> <DonationForm />
</Section> </Section>
} }
if (item.blockType === "youtube") { if (item.blockType === 'youtube') {
return <Section key={item.id} padding={"small"}> return <Section key={item.id} padding={'small'}>
<Container> <Container>
<YoutubePlayer id={item.youtube_id} /> <YoutubePlayer id={item.youtube_id} />
</Container> </Container>
</Section> </Section>
} }
if (item.blockType === "button") { if (item.blockType === 'button') {
return <Section key={item.id} padding={"small"}> return <Section key={item.id} padding={'small'}>
<Container> <Container>
<Button <Button
size={"lg"} size={'lg'}
type={"button"} type={'button'}
href={item.url} href={item.url}
target={"_blank"} target={'_blank'}
> >
{item.text} {item.text}
</Button> </Button>
</Container> </Container>
</Section> </Section>
} }
if (item.blockType === 'pageHeader') {
const imageUrl = typeof item.image === 'object' && item.image?.url
? item.image.url
: undefined
return (
<PageHeader
key={item.id}
title={item.title}
description={item.description}
image={imageUrl}
/>
)
}
if (item.blockType === 'section') {
const bg = item.backgroundColor === 'none'
? undefined
: item.backgroundColor as 'soft' | 'off-white' | undefined
return (
<Section
key={item.id}
backgroundColor={bg}
padding={item.padding as 'small' | 'medium' | 'large' | undefined}
/>
)
}
if (item.blockType === 'title') {
return (
<Container key={item.id}>
<Title
title={item.title}
subtitle={item.subtitle || undefined}
size={item.size as 'xl' | 'lg' | 'md' | 'sm' | undefined}
align={item.align as 'left' | 'center' | undefined}
/>
</Container>
)
}
if (item.blockType === 'banner') {
const bannerImageUrl =
typeof item.backgroundImage === 'object' &&
item.backgroundImage?.url
? item.backgroundImage.url
: undefined
return (
<Banner
key={item.id}
textLine1={item.textLine1}
textLine2={item.textLine2}
textLine3={item.textLine3}
backgroundColor={item.backgroundColor}
backgroundImage={bannerImageUrl}
backgroundPosition={item.backgroundPosition}
backgroundSize={item.backgroundSize}
/>
)
}
if (item.blockType === 'mainText') {
return (
<Container key={item.id}>
<Section>
<MainText text={item.text} />
</Section>
</Container>
)
}
if (item.blockType === 'horizontalRule') {
return <HR key={item.id} color={item.color} />
}
if (item.blockType === 'blogSlider') {
return (
<BlogSliderBlock
key={item.id}
title={item.title}
/>
)
}
if (item.blockType === 'massTimes') {
return (
<MassTimesBlock
key={item.id}
title={item.title}
subtitle={item.subtitle}
/>
)
}
if (item.blockType === 'collapsibleImageWithText') {
const imageUrl = typeof item.image === 'object' && item.image?.url
? item.image.url
: ''
const bg = item.backgroundColor === 'none'
? undefined
: item.backgroundColor as 'soft' | 'off-white' | undefined
return (
<CollapsibleImageWithText
key={item.id}
title={item.title}
text={item.text}
image={imageUrl}
backgroundColor={bg}
schema={item.schema as 'base' | 'contrast' | undefined}
content={
item.content_html
? <HTMLText width={'1/2'} html={item.content_html} />
: <></>
}
/>
)
}
if (item.blockType === 'events') {
return (
<EventsBlock
key={item.id}
title={item.title}
itemsPerPage={item.itemsPerPage}
/>
)
}
if (item.blockType === 'publicationAndNewsletter') {
return <PublicationAndNewsletter key={item.id} />
}
})} })}
</div> </div>

View file

@ -0,0 +1,28 @@
import { fetchBlogPosts } from '@/fetch/blog'
import { blogToSlides } from '@/utils/dto/blog'
import { Container } from '@/components/Container/Container'
import { Section } from '@/components/Section/Section'
import { Title } from '@/components/Title/Title'
import { ImageCardSlider } from '@/compositions/ImageCardSlider/ImageCardSlider'
type BlogSliderBlockProps = {
title?: string | null
}
export async function BlogSliderBlock({
title = 'Aktuelles',
}: BlogSliderBlockProps) {
const blog = await fetchBlogPosts(true)
const posts = blog?.docs || []
if (posts.length === 0) return null
return (
<Container>
<Section>
<Title title={title || 'Aktuelles'} color={'contrast'} />
<ImageCardSlider slides={blogToSlides(posts)} />
</Section>
</Container>
)
}

View file

@ -0,0 +1,31 @@
import { fetchEvents } from '@/fetch/events'
import { transformEvents } from '@/utils/dto/events'
import { Section } from '@/components/Section/Section'
import { Title } from '@/components/Title/Title'
import { Events } from '@/compositions/Events/Events'
type EventsBlockProps = {
title?: string | null
itemsPerPage?: number | null
}
export async function EventsBlock({
title = 'Veranstaltungen',
itemsPerPage = 6,
}: EventsBlockProps) {
const events = await fetchEvents()
const docs = events?.docs || []
if (docs.length === 0) return null
return (
<Section>
<Title color={'contrast'} title={title || 'Veranstaltungen'} />
<Events
events={transformEvents(docs)}
n={itemsPerPage || 6}
schema={'contrast'}
/>
</Section>
)
}

View file

@ -0,0 +1,108 @@
import { Worship } from '@/payload-types'
import { fetchWorship } from '@/fetch/worship'
import { fetchLastAnnouncements } from '@/fetch/announcement'
import { fetchLastCalendars } from '@/fetch/calendar'
import { perParish } from '@/utils/dto/perParish'
import { Section } from '@/components/Section/Section'
import { Title } from '@/components/Title/Title'
import { MassRow } from '@/components/MassTable/MassRow'
import { MassTable } from '@/components/MassTable/MassTable'
import { PopupButton } from '@/components/PopupButton/PopupButton'
import { Button } from '@/components/Button/Button'
import moment from 'moment'
import styles from './massTimesBlock.module.scss'
type MassTimesBlockProps = {
title?: string | null
subtitle?: string | null
}
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 async function MassTimesBlock({
title = 'Nächste Gottesdienste',
subtitle,
}: MassTimesBlockProps) {
const fromDate = moment().isoWeekday(1).hours(0).minutes(0)
const tillDate = moment().isoWeekday(7).hours(23).minutes(59)
const worship = await fetchWorship({
fromDate: fromDate.toDate(),
tillDate: tillDate.toDate(),
})
const announcements = await fetchLastAnnouncements()
const calendars = await fetchLastCalendars()
const worshipDocs = worship?.docs || []
const announcementsLinks = announcements ? perParish(announcements) : []
const calendarsLinks = calendars ? perParish(calendars) : []
const worshipPerLocation = Array.from(
sortWorship(worshipDocs).entries(),
).sort((a, b) => a[0].localeCompare(b[0]))
return (
<Section paddingBottom={'medium'}>
<Title
title={title || 'Nächste Gottesdienste'}
subtitle={subtitle || undefined}
color={'contrast'}
align={'center'}
/>
<Section padding={'small'}>
<MassRow>
{worshipPerLocation.map((value) => (
<MassTable
key={value[0]}
location={value[0]}
masses={value[1]}
/>
))}
</MassRow>
</Section>
<Section padding={'small'}>
<div className={styles.center}>
{announcementsLinks.length > 0 && (
<PopupButton
text={'Vermeldungen'}
title={'Vermeldungen'}
links={announcementsLinks}
schema={'shade'}
/>
)}
{calendarsLinks.length > 0 && (
<PopupButton
text={'Liturgischer Kalender'}
title={'Kalender'}
links={calendarsLinks}
schema={'shade'}
/>
)}
<Button href={'/gottesdienst'} size={'md'}>
Alle Gottesdienste
</Button>
</div>
</Section>
</Section>
)
}

View file

@ -0,0 +1,6 @@
.center {
display: flex;
gap: 5px;
justify-content: center;
flex-wrap: wrap;
}

View file

@ -12,7 +12,7 @@ import classNames from 'classnames'
type ImageWithTextProps = { type ImageWithTextProps = {
backgroundColor?: BackgroundColor, backgroundColor?: BackgroundColor,
title: string, title: string,
image: StaticImageData, image: StaticImageData | string,
text: string text: string
schema?: 'base' | 'contrast' schema?: 'base' | 'contrast'
content: React.ReactNode content: React.ReactNode

View file

@ -10,7 +10,7 @@ import styles from "./styles.module.scss"
type PageHeaderProps = { type PageHeaderProps = {
title: string, title: string,
description: string, description: string,
image?: StaticImageData image?: StaticImageData | string
alt?: string alt?: string
} }
@ -25,14 +25,23 @@ export const PageHeader = ({ title, description, image, alt }: PageHeaderProps)
<HR /> <HR />
<Container> <Container>
{image && {image && typeof image === 'string' ? (
<Image <Image
unoptimized={true} unoptimized={true}
className={styles.image} className={styles.image}
src={image} src={image}
alt={alt || ""} width={1100}
height={400}
alt={alt || ''}
/> />
} ) : image ? (
<Image
unoptimized={true}
className={styles.image}
src={image}
alt={alt || ''}
/>
) : null}
</Container> </Container>
</Section> </Section>

View file

@ -14,4 +14,5 @@ export const siteConfig = {
'Gemeinde', 'Gemeinde',
], ],
ogImage: '/og-logo.svg', ogImage: '/og-logo.svg',
email: 'kontakt@dreikoenige.berlin',
} }

25
src/fetch/pages.ts Normal file
View file

@ -0,0 +1,25 @@
import { Page } from '@/payload-types'
import { stringify } from 'qs-esm'
export async function fetchPageBySlug(
slug: string,
): Promise<Page | undefined> {
const query = stringify(
{
where: {
slug: {
equals: slug,
},
},
limit: 1,
},
{ addQueryPrefix: true },
)
const res = await fetch(`http://localhost:3000/api/pages${query}`, {
next: { tags: ['pages', `pages-${slug}`] },
})
if (!res.ok) return undefined
const data = await res.json()
return data.docs?.[0]
}

View file

@ -140,6 +140,6 @@ export const MenuGlobal: GlobalConfig = {
update: isAdmin() update: isAdmin()
}, },
hooks: { hooks: {
afterChange: [() => revalidateTag("menu", "max")] afterChange: [() => revalidateTag("menu")]
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,265 @@
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
await db.execute(sql`
CREATE TYPE "public"."enum_pages_blocks_text_width" AS ENUM('1/2', '3/4');
CREATE TYPE "public"."enum_pages_blocks_title_size" AS ENUM('xl', 'lg', 'md', 'sm');
CREATE TYPE "public"."enum_pages_blocks_title_align" AS ENUM('left', 'center');
CREATE TYPE "public"."enum_pages_blocks_section_background_color" AS ENUM('none', 'soft', 'off-white');
CREATE TYPE "public"."enum_pages_blocks_section_padding" AS ENUM('small', 'medium', 'large');
CREATE TABLE "pages_blocks_page_header" (
"_order" integer NOT NULL,
"_parent_id" uuid NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"title" varchar NOT NULL,
"description" varchar NOT NULL,
"image_id" uuid,
"block_name" varchar
);
CREATE TABLE "pages_blocks_text" (
"_order" integer NOT NULL,
"_parent_id" uuid NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"content" jsonb NOT NULL,
"content_html" varchar,
"width" "enum_pages_blocks_text_width" DEFAULT '1/2' NOT NULL,
"block_name" varchar
);
CREATE TABLE "pages_blocks_title" (
"_order" integer NOT NULL,
"_parent_id" uuid NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"title" varchar NOT NULL,
"subtitle" varchar,
"size" "enum_pages_blocks_title_size" DEFAULT 'lg',
"align" "enum_pages_blocks_title_align" DEFAULT 'left',
"block_name" varchar
);
CREATE TABLE "pages_blocks_row_columns" (
"_order" integer NOT NULL,
"_parent_id" varchar NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"content" jsonb NOT NULL,
"content_html" varchar
);
CREATE TABLE "pages_blocks_row" (
"_order" integer NOT NULL,
"_parent_id" uuid NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"block_name" varchar
);
CREATE TABLE "pages_blocks_section" (
"_order" integer NOT NULL,
"_parent_id" uuid NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"background_color" "enum_pages_blocks_section_background_color" DEFAULT 'none',
"padding" "enum_pages_blocks_section_padding" DEFAULT 'large',
"block_name" varchar
);
CREATE TABLE "pages_blocks_gallery_items" (
"_order" integer NOT NULL,
"_parent_id" varchar NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"photo_id" uuid NOT NULL
);
CREATE TABLE "pages_blocks_gallery" (
"_order" integer NOT NULL,
"_parent_id" uuid NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"block_name" varchar
);
CREATE TABLE "pages_blocks_document" (
"_order" integer NOT NULL,
"_parent_id" uuid NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"file_id" uuid NOT NULL,
"button" varchar DEFAULT 'Download Flyer' NOT NULL,
"block_name" varchar
);
CREATE TABLE "pages_blocks_youtube" (
"_order" integer NOT NULL,
"_parent_id" uuid NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"youtube_id" varchar NOT NULL,
"block_name" varchar
);
CREATE TABLE "pages_blocks_button" (
"_order" integer NOT NULL,
"_parent_id" uuid NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"text" varchar NOT NULL,
"url" varchar NOT NULL,
"block_name" varchar
);
CREATE TABLE "pages_blocks_contactform" (
"_order" integer NOT NULL,
"_parent_id" uuid NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"title" varchar DEFAULT 'Ich bin dabei!' NOT NULL,
"description" varchar DEFAULT 'Um dich anzumelden oder uns zu unterstützen, fülle bitte das Kontaktformular aus. Wir freuen uns sehr, dass du Teil unserer Gemeinschaft bist und mit deinem Engagement dazu beiträgst, unsere Ziele zu erreichen. Solltest du Fragen haben oder weitere Informationen benötigen, zögere nicht, uns zu kontaktieren wir sind gerne für dich da!' NOT NULL,
"email" varchar DEFAULT 'kontakt@dreikoenige.berlin' NOT NULL,
"block_name" varchar
);
CREATE TABLE "pages_blocks_donation" (
"_order" integer NOT NULL,
"_parent_id" uuid NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"block_name" varchar
);
CREATE TABLE "pages_blocks_banner" (
"_order" integer NOT NULL,
"_parent_id" uuid NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"block_name" varchar
);
CREATE TABLE "pages" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"title" varchar NOT NULL,
"description" varchar,
"slug" varchar NOT NULL,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);
ALTER TABLE "announcement" ALTER COLUMN "date" SET DEFAULT '2026-03-08T09:54:26.297Z';
ALTER TABLE "calendar" ALTER COLUMN "date" SET DEFAULT '2026-03-08T09:54:26.602Z';
ALTER TABLE "classifieds" ALTER COLUMN "until" SET DEFAULT '2026-04-04T08:54:26.674Z';
ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "pages_id" uuid;
ALTER TABLE "pages_blocks_page_header" ADD CONSTRAINT "pages_blocks_page_header_image_id_media_id_fk" FOREIGN KEY ("image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "pages_blocks_page_header" ADD CONSTRAINT "pages_blocks_page_header_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_blocks_text" ADD CONSTRAINT "pages_blocks_text_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_blocks_title" ADD CONSTRAINT "pages_blocks_title_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_blocks_row_columns" ADD CONSTRAINT "pages_blocks_row_columns_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."pages_blocks_row"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_blocks_row" ADD CONSTRAINT "pages_blocks_row_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_blocks_section" ADD CONSTRAINT "pages_blocks_section_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_blocks_gallery_items" ADD CONSTRAINT "pages_blocks_gallery_items_photo_id_media_id_fk" FOREIGN KEY ("photo_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "pages_blocks_gallery_items" ADD CONSTRAINT "pages_blocks_gallery_items_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."pages_blocks_gallery"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_blocks_gallery" ADD CONSTRAINT "pages_blocks_gallery_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_blocks_document" ADD CONSTRAINT "pages_blocks_document_file_id_documents_id_fk" FOREIGN KEY ("file_id") REFERENCES "public"."documents"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "pages_blocks_document" ADD CONSTRAINT "pages_blocks_document_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_blocks_youtube" ADD CONSTRAINT "pages_blocks_youtube_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_blocks_button" ADD CONSTRAINT "pages_blocks_button_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_blocks_contactform" ADD CONSTRAINT "pages_blocks_contactform_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_blocks_donation" ADD CONSTRAINT "pages_blocks_donation_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_blocks_banner" ADD CONSTRAINT "pages_blocks_banner_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
CREATE INDEX "pages_blocks_page_header_order_idx" ON "pages_blocks_page_header" USING btree ("_order");
CREATE INDEX "pages_blocks_page_header_parent_id_idx" ON "pages_blocks_page_header" USING btree ("_parent_id");
CREATE INDEX "pages_blocks_page_header_path_idx" ON "pages_blocks_page_header" USING btree ("_path");
CREATE INDEX "pages_blocks_page_header_image_idx" ON "pages_blocks_page_header" USING btree ("image_id");
CREATE INDEX "pages_blocks_text_order_idx" ON "pages_blocks_text" USING btree ("_order");
CREATE INDEX "pages_blocks_text_parent_id_idx" ON "pages_blocks_text" USING btree ("_parent_id");
CREATE INDEX "pages_blocks_text_path_idx" ON "pages_blocks_text" USING btree ("_path");
CREATE INDEX "pages_blocks_title_order_idx" ON "pages_blocks_title" USING btree ("_order");
CREATE INDEX "pages_blocks_title_parent_id_idx" ON "pages_blocks_title" USING btree ("_parent_id");
CREATE INDEX "pages_blocks_title_path_idx" ON "pages_blocks_title" USING btree ("_path");
CREATE INDEX "pages_blocks_row_columns_order_idx" ON "pages_blocks_row_columns" USING btree ("_order");
CREATE INDEX "pages_blocks_row_columns_parent_id_idx" ON "pages_blocks_row_columns" USING btree ("_parent_id");
CREATE INDEX "pages_blocks_row_order_idx" ON "pages_blocks_row" USING btree ("_order");
CREATE INDEX "pages_blocks_row_parent_id_idx" ON "pages_blocks_row" USING btree ("_parent_id");
CREATE INDEX "pages_blocks_row_path_idx" ON "pages_blocks_row" USING btree ("_path");
CREATE INDEX "pages_blocks_section_order_idx" ON "pages_blocks_section" USING btree ("_order");
CREATE INDEX "pages_blocks_section_parent_id_idx" ON "pages_blocks_section" USING btree ("_parent_id");
CREATE INDEX "pages_blocks_section_path_idx" ON "pages_blocks_section" USING btree ("_path");
CREATE INDEX "pages_blocks_gallery_items_order_idx" ON "pages_blocks_gallery_items" USING btree ("_order");
CREATE INDEX "pages_blocks_gallery_items_parent_id_idx" ON "pages_blocks_gallery_items" USING btree ("_parent_id");
CREATE INDEX "pages_blocks_gallery_items_photo_idx" ON "pages_blocks_gallery_items" USING btree ("photo_id");
CREATE INDEX "pages_blocks_gallery_order_idx" ON "pages_blocks_gallery" USING btree ("_order");
CREATE INDEX "pages_blocks_gallery_parent_id_idx" ON "pages_blocks_gallery" USING btree ("_parent_id");
CREATE INDEX "pages_blocks_gallery_path_idx" ON "pages_blocks_gallery" USING btree ("_path");
CREATE INDEX "pages_blocks_document_order_idx" ON "pages_blocks_document" USING btree ("_order");
CREATE INDEX "pages_blocks_document_parent_id_idx" ON "pages_blocks_document" USING btree ("_parent_id");
CREATE INDEX "pages_blocks_document_path_idx" ON "pages_blocks_document" USING btree ("_path");
CREATE INDEX "pages_blocks_document_file_idx" ON "pages_blocks_document" USING btree ("file_id");
CREATE INDEX "pages_blocks_youtube_order_idx" ON "pages_blocks_youtube" USING btree ("_order");
CREATE INDEX "pages_blocks_youtube_parent_id_idx" ON "pages_blocks_youtube" USING btree ("_parent_id");
CREATE INDEX "pages_blocks_youtube_path_idx" ON "pages_blocks_youtube" USING btree ("_path");
CREATE INDEX "pages_blocks_button_order_idx" ON "pages_blocks_button" USING btree ("_order");
CREATE INDEX "pages_blocks_button_parent_id_idx" ON "pages_blocks_button" USING btree ("_parent_id");
CREATE INDEX "pages_blocks_button_path_idx" ON "pages_blocks_button" USING btree ("_path");
CREATE INDEX "pages_blocks_contactform_order_idx" ON "pages_blocks_contactform" USING btree ("_order");
CREATE INDEX "pages_blocks_contactform_parent_id_idx" ON "pages_blocks_contactform" USING btree ("_parent_id");
CREATE INDEX "pages_blocks_contactform_path_idx" ON "pages_blocks_contactform" USING btree ("_path");
CREATE INDEX "pages_blocks_donation_order_idx" ON "pages_blocks_donation" USING btree ("_order");
CREATE INDEX "pages_blocks_donation_parent_id_idx" ON "pages_blocks_donation" USING btree ("_parent_id");
CREATE INDEX "pages_blocks_donation_path_idx" ON "pages_blocks_donation" USING btree ("_path");
CREATE INDEX "pages_blocks_banner_order_idx" ON "pages_blocks_banner" USING btree ("_order");
CREATE INDEX "pages_blocks_banner_parent_id_idx" ON "pages_blocks_banner" USING btree ("_parent_id");
CREATE INDEX "pages_blocks_banner_path_idx" ON "pages_blocks_banner" USING btree ("_path");
CREATE UNIQUE INDEX "pages_slug_idx" ON "pages" USING btree ("slug");
CREATE INDEX "pages_updated_at_idx" ON "pages" USING btree ("updated_at");
CREATE INDEX "pages_created_at_idx" ON "pages" USING btree ("created_at");
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_pages_fk" FOREIGN KEY ("pages_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
CREATE INDEX "payload_locked_documents_rels_pages_id_idx" ON "payload_locked_documents_rels" USING btree ("pages_id");`)
}
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
await db.execute(sql`
ALTER TABLE "pages_blocks_page_header" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "pages_blocks_text" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "pages_blocks_title" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "pages_blocks_row_columns" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "pages_blocks_row" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "pages_blocks_section" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "pages_blocks_gallery_items" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "pages_blocks_gallery" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "pages_blocks_document" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "pages_blocks_youtube" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "pages_blocks_button" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "pages_blocks_contactform" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "pages_blocks_donation" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "pages_blocks_banner" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "pages" DISABLE ROW LEVEL SECURITY;
DROP TABLE "pages_blocks_page_header" CASCADE;
DROP TABLE "pages_blocks_text" CASCADE;
DROP TABLE "pages_blocks_title" CASCADE;
DROP TABLE "pages_blocks_row_columns" CASCADE;
DROP TABLE "pages_blocks_row" CASCADE;
DROP TABLE "pages_blocks_section" CASCADE;
DROP TABLE "pages_blocks_gallery_items" CASCADE;
DROP TABLE "pages_blocks_gallery" CASCADE;
DROP TABLE "pages_blocks_document" CASCADE;
DROP TABLE "pages_blocks_youtube" CASCADE;
DROP TABLE "pages_blocks_button" CASCADE;
DROP TABLE "pages_blocks_contactform" CASCADE;
DROP TABLE "pages_blocks_donation" CASCADE;
DROP TABLE "pages_blocks_banner" CASCADE;
DROP TABLE "pages" CASCADE;
ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT "payload_locked_documents_rels_pages_fk";
DROP INDEX "payload_locked_documents_rels_pages_id_idx";
ALTER TABLE "announcement" ALTER COLUMN "date" SET DEFAULT '2026-02-08T15:57:34.492Z';
ALTER TABLE "calendar" ALTER COLUMN "date" SET DEFAULT '2026-02-08T15:57:34.801Z';
ALTER TABLE "classifieds" ALTER COLUMN "until" SET DEFAULT '2026-03-07T15:57:34.871Z';
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN "pages_id";
DROP TYPE "public"."enum_pages_blocks_text_width";
DROP TYPE "public"."enum_pages_blocks_title_size";
DROP TYPE "public"."enum_pages_blocks_title_align";
DROP TYPE "public"."enum_pages_blocks_section_background_color";
DROP TYPE "public"."enum_pages_blocks_section_padding";`)
}

View file

@ -16,6 +16,7 @@ import * as migration_20251118_150529_youtube_player from './20251118_150529_you
import * as migration_20260106_085445_donationforms from './20260106_085445_donationforms'; import * as migration_20260106_085445_donationforms from './20260106_085445_donationforms';
import * as migration_20260106_103529_donation_appeal from './20260106_103529_donation_appeal'; import * as migration_20260106_103529_donation_appeal from './20260106_103529_donation_appeal';
import * as migration_20260205_155735_version_bump from './20260205_155735_version_bump'; import * as migration_20260205_155735_version_bump from './20260205_155735_version_bump';
import * as migration_20260305_095426 from './20260305_095426';
export const migrations = [ export const migrations = [
{ {
@ -106,6 +107,11 @@ export const migrations = [
{ {
up: migration_20260205_155735_version_bump.up, up: migration_20260205_155735_version_bump.up,
down: migration_20260205_155735_version_bump.down, down: migration_20260205_155735_version_bump.down,
name: '20260205_155735_version_bump' name: '20260205_155735_version_bump',
},
{
up: migration_20260305_095426.up,
down: migration_20260305_095426.down,
name: '20260305_095426'
}, },
]; ];

View file

@ -29,7 +29,7 @@ export const Worship = ({ worship }: WorshipPageProps) => {
<> <>
<Section> <Section>
<div className={styles.textCenter}> <div className={styles.textCenter}>
<Cross schema={"contrast"} /> <Cross color={"contrast"} />
</div> </div>
<Title <Title
title={`${day}, ${localeDate}`} title={`${day}, ${localeDate}`}

View file

@ -81,6 +81,7 @@ export interface Config {
locations: Location; locations: Location;
group: Group; group: Group;
'donation-form': DonationForm; 'donation-form': DonationForm;
pages: Page;
magazine: Magazine; magazine: Magazine;
documents: Document; documents: Document;
media: Media; media: Media;
@ -106,6 +107,7 @@ export interface Config {
locations: LocationsSelect<false> | LocationsSelect<true>; locations: LocationsSelect<false> | LocationsSelect<true>;
group: GroupSelect<false> | GroupSelect<true>; group: GroupSelect<false> | GroupSelect<true>;
'donation-form': DonationFormSelect<false> | DonationFormSelect<true>; 'donation-form': DonationFormSelect<false> | DonationFormSelect<true>;
pages: PagesSelect<false> | PagesSelect<true>;
magazine: MagazineSelect<false> | MagazineSelect<true>; magazine: MagazineSelect<false> | MagazineSelect<true>;
documents: DocumentsSelect<false> | DocumentsSelect<true>; documents: DocumentsSelect<false> | DocumentsSelect<true>;
media: MediaSelect<false> | MediaSelect<true>; media: MediaSelect<false> | MediaSelect<true>;
@ -694,6 +696,200 @@ export interface DonationForm {
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "pages".
*/
export interface Page {
id: string;
title: string;
description?: string | null;
/**
* URL-Pfad der Seite (z.B. "meine-seite" /meine-seite)
*/
slug: string;
content?:
| (
| {
title: string;
description: string;
image?: (string | null) | Media;
id?: string | null;
blockName?: string | null;
blockType: 'pageHeader';
}
| {
content: {
root: {
type: string;
children: {
type: any;
version: number;
[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';
id?: string | null;
blockName?: string | null;
blockType: 'text';
}
| {
title: string;
subtitle?: string | null;
size?: ('xl' | 'lg' | 'md' | 'sm') | null;
align?: ('left' | 'center') | null;
id?: string | null;
blockName?: string | null;
blockType: 'title';
}
| {
backgroundColor?: ('none' | 'soft' | 'off-white') | null;
padding?: ('small' | 'medium' | 'large') | null;
id?: string | null;
blockName?: string | null;
blockType: 'section';
}
| {
items: {
photo: string | Media;
id?: string | null;
}[];
id?: string | null;
blockName?: string | null;
blockType: 'gallery';
}
| {
file: string | Document;
button: string;
id?: string | null;
blockName?: string | null;
blockType: 'document';
}
| {
youtube_id: string;
id?: string | null;
blockName?: string | null;
blockType: 'youtube';
}
| {
text: string;
url: string;
id?: string | null;
blockName?: string | null;
blockType: 'button';
}
| {
title: string;
description: string;
email: string;
id?: string | null;
blockName?: string | null;
blockType: 'contactform';
}
| {
id?: string | null;
blockName?: string | null;
blockType: 'donation';
}
| {
textLine1?: string | null;
textLine2?: string | null;
textLine3?: string | null;
backgroundColor?: string | null;
backgroundImage?: (string | null) | Media;
backgroundPosition?:
| (
| 'center center'
| 'top center'
| 'bottom center'
| 'center left'
| 'center right'
| 'top left'
| 'top right'
| 'bottom left'
| 'bottom right'
)
| null;
backgroundSize?: ('cover' | 'contain' | 'auto') | null;
id?: string | null;
blockName?: string | null;
blockType: 'banner';
}
| {
text: string;
id?: string | null;
blockName?: string | null;
blockType: 'mainText';
}
| {
color: 'base' | 'shade1' | 'shade2' | 'shade3' | 'contrast' | 'contrastShade1';
id?: string | null;
blockName?: string | null;
blockType: 'horizontalRule';
}
| {
title?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'blogSlider';
}
| {
title?: string | null;
subtitle?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'massTimes';
}
| {
title: string;
text: string;
image: string | Media;
content: {
root: {
type: string;
children: {
type: any;
version: number;
[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;
backgroundColor?: ('none' | 'soft' | 'off-white') | null;
schema?: ('base' | 'contrast') | null;
id?: string | null;
blockName?: string | null;
blockType: 'collapsibleImageWithText';
}
| {
title?: string | null;
itemsPerPage?: number | null;
id?: string | null;
blockName?: string | null;
blockType: 'events';
}
| {
id?: string | null;
blockName?: string | null;
blockType: 'publicationAndNewsletter';
}
)[]
| null;
updatedAt: string;
createdAt: string;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "magazine". * via the `definition` "magazine".
@ -813,6 +1009,10 @@ export interface PayloadLockedDocument {
relationTo: 'donation-form'; relationTo: 'donation-form';
value: string | DonationForm; value: string | DonationForm;
} | null) } | null)
| ({
relationTo: 'pages';
value: string | Page;
} | null)
| ({ | ({
relationTo: 'magazine'; relationTo: 'magazine';
value: string | Magazine; value: string | Magazine;
@ -1243,6 +1443,176 @@ export interface DonationFormSelect<T extends boolean = true> {
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "pages_select".
*/
export interface PagesSelect<T extends boolean = true> {
title?: T;
description?: T;
slug?: T;
content?:
| T
| {
pageHeader?:
| T
| {
title?: T;
description?: T;
image?: T;
id?: T;
blockName?: T;
};
text?:
| T
| {
content?: T;
content_html?: T;
width?: T;
id?: T;
blockName?: T;
};
title?:
| T
| {
title?: T;
subtitle?: T;
size?: T;
align?: T;
id?: T;
blockName?: T;
};
section?:
| T
| {
backgroundColor?: T;
padding?: T;
id?: T;
blockName?: T;
};
gallery?:
| T
| {
items?:
| T
| {
photo?: T;
id?: T;
};
id?: T;
blockName?: T;
};
document?:
| T
| {
file?: T;
button?: T;
id?: T;
blockName?: T;
};
youtube?:
| T
| {
youtube_id?: T;
id?: T;
blockName?: T;
};
button?:
| T
| {
text?: T;
url?: T;
id?: T;
blockName?: T;
};
contactform?:
| T
| {
title?: T;
description?: T;
email?: T;
id?: T;
blockName?: T;
};
donation?:
| T
| {
id?: T;
blockName?: T;
};
banner?:
| T
| {
textLine1?: T;
textLine2?: T;
textLine3?: T;
backgroundColor?: T;
backgroundImage?: T;
backgroundPosition?: T;
backgroundSize?: T;
id?: T;
blockName?: T;
};
mainText?:
| T
| {
text?: T;
id?: T;
blockName?: T;
};
horizontalRule?:
| T
| {
color?: T;
id?: T;
blockName?: T;
};
blogSlider?:
| T
| {
title?: T;
id?: T;
blockName?: T;
};
massTimes?:
| T
| {
title?: T;
subtitle?: T;
id?: T;
blockName?: T;
};
collapsibleImageWithText?:
| T
| {
title?: T;
text?: T;
image?: T;
content?: T;
content_html?: T;
backgroundColor?: T;
schema?: T;
id?: T;
blockName?: T;
};
events?:
| T
| {
title?: T;
itemsPerPage?: T;
id?: T;
blockName?: T;
};
publicationAndNewsletter?:
| T
| {
id?: T;
blockName?: T;
};
};
updatedAt?: T;
createdAt?: T;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "magazine_select". * via the `definition` "magazine_select".

View file

@ -39,6 +39,7 @@ import { Classifieds } from '@/collections/Classifieds'
import { MenuGlobal } from '@/globals/Menu' import { MenuGlobal } from '@/globals/Menu'
import { Magazine } from '@/collections/Magazine' import { Magazine } from '@/collections/Magazine'
import { DonationForms } from '@/collections/DonationForms' import { DonationForms } from '@/collections/DonationForms'
import { Pages } from '@/collections/Pages'
const filename = fileURLToPath(import.meta.url) const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename) const dirname = path.dirname(filename)
@ -92,6 +93,7 @@ export default buildConfig({
Locations, Locations,
Groups, Groups,
DonationForms, DonationForms,
Pages,
Magazine, Magazine,
Documents, Documents,
Media, Media,
@ -128,7 +130,7 @@ export default buildConfig({
}, },
db: postgresAdapter({ db: postgresAdapter({
idType: "uuid", idType: "uuid",
push: false, push: true,
pool: { pool: {
connectionString: process.env.DATABASE_URI, connectionString: process.env.DATABASE_URI,
} }

View file

@ -15,7 +15,7 @@
"moduleResolution": "bundler", "moduleResolution": "bundler",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "react-jsx", "jsx": "preserve",
"incremental": true, "incremental": true,
"plugins": [ "plugins": [
{ {