feature: add calendar

This commit is contained in:
Benno Tielen 2025-01-28 16:34:14 +01:00
parent 201ba0163f
commit a9c451b005
17 changed files with 6700 additions and 7 deletions

View file

@ -5,6 +5,7 @@ import { fetchWorship } from '@/fetch/worship'
import { fetchParish } from '@/fetch/parish' import { fetchParish } from '@/fetch/parish'
import { fetchLastAnnouncement } from '@/fetch/announcement' import { fetchLastAnnouncement } from '@/fetch/announcement'
import { transformGallery } from '@/utils/dto/gallery' import { transformGallery } from '@/utils/dto/gallery'
import { fetchLastCalendar } from '@/fetch/calendar'
export default async function ParishPage ({ params }: { params: Promise<{slug: string}>}) { export default async function ParishPage ({ params }: { params: Promise<{slug: string}>}) {
@ -30,6 +31,7 @@ export default async function ParishPage ({ params }: { params: Promise<{slug: s
const churchIds = churches.map(c => typeof c === "string" ? c : c.id) const churchIds = churches.map(c => typeof c === "string" ? c : c.id)
const worship = await fetchWorship({ locations: churchIds }) const worship = await fetchWorship({ locations: churchIds })
const announcement = await fetchLastAnnouncement(id); const announcement = await fetchLastAnnouncement(id);
const calendar = await fetchLastCalendar(id);
return ( return (
<Parish <Parish
title={name} title={name}
@ -42,6 +44,7 @@ export default async function ParishPage ({ params }: { params: Promise<{slug: s
events={events?.docs || []} events={events?.docs || []}
worship={worship?.docs || []} worship={worship?.docs || []}
announcement={announcement && typeof announcement.document === "object" ? announcement.document.url || undefined : undefined} announcement={announcement && typeof announcement.document === "object" ? announcement.document.url || undefined : undefined}
calendar={calendar && typeof calendar.document === "object" ? calendar.document.url || undefined : undefined}
gallery={gallery ? transformGallery(gallery) : undefined} gallery={gallery ? transformGallery(gallery) : undefined}
/> />
) )

View file

@ -1,5 +1,6 @@
import { CollectionConfig } from 'payload' import { CollectionConfig } from 'payload'
import { isAdminOrEmployee } from '@/collections/access/admin' import { isAdminOrEmployee } from '@/collections/access/admin'
import { nextSunday } from '@/utils/sunday'
export const Announcements: CollectionConfig = { export const Announcements: CollectionConfig = {
slug: 'announcement', slug: 'announcement',
@ -18,7 +19,8 @@ export const Announcements: CollectionConfig = {
required: true, required: true,
label: { label: {
de: 'Datum' de: 'Datum'
} },
defaultValue: nextSunday()
}, },
{ {
name: 'parish', name: 'parish',

View file

@ -0,0 +1,55 @@
import { CollectionConfig } from 'payload'
import { isAdminOrEmployee } from '@/collections/access/admin'
import { nextSunday } from '@/utils/sunday'
export const LiturgicalCalendar: CollectionConfig = {
slug: "calendar",
labels: {
singular: {
de: "Liturgischer Kalendar"
},
plural: {
de: "Liturgischer Kalendar"
}
},
fields: [
{
name: 'date',
type: 'date',
required: true,
label: {
de: 'Datum'
},
defaultValue: nextSunday()
},
{
name: 'parish',
type: "relationship",
relationTo: 'parish',
required: true,
hasMany: true,
label: {
de: "Gemeinde"
},
admin: {
allowCreate: false,
allowEdit: false
}
},
{
name: 'document',
label: {
de: "PDF-Dokument"
},
type: 'upload',
relationTo: 'documents',
required: true
}
],
access: {
read: () => true,
create: isAdminOrEmployee(),
update: isAdminOrEmployee(),
delete: isAdminOrEmployee(),
}
}

View file

@ -6,6 +6,7 @@ type ButtonProps = {
schema?: 'base' | 'shade' | 'contrast' schema?: 'base' | 'shade' | 'contrast'
type?: "button" | "submit" | "reset", type?: "button" | "submit" | "reset",
href?: string, href?: string,
target?: "_blank" | "_self"
children: React.ReactNode, children: React.ReactNode,
onClick?: () => void, onClick?: () => void,
} }
@ -17,7 +18,8 @@ export function Button(
size, size,
children, children,
onClick, onClick,
href href,
target = "_self"
}: ButtonProps }: ButtonProps
) { ) {
const style = classNames({ const style = classNames({
@ -34,6 +36,7 @@ export function Button(
href={href} href={href}
onClick={onClick} onClick={onClick}
className={style} className={style}
target={target}
> >
{children} {children}
</a> </a>

View file

@ -0,0 +1,32 @@
import { Meta, StoryObj } from '@storybook/react'
import { CalendarAnnouncementButtons } from './CalendarAnnouncementButtons'
const meta: Meta<typeof CalendarAnnouncementButtons> = {
component: CalendarAnnouncementButtons,
}
type Story = StoryObj<typeof CalendarAnnouncementButtons>;
export default meta
export const Default: Story = {
args: {
announcements: "https://disney.com",
calendar: "https://google.com"
},
}
export const OnlyAnnouncements: Story = {
args: {
announcements: "https://disney.com",
},
}
export const OnlyCalendar: Story = {
args: {
calendar: "https://disney.com",
},
}
export const Empty: Story = {
args: {},
}

View file

@ -0,0 +1,34 @@
import { Button } from '@/components/Button/Button'
import styles from "./styles.module.scss"
type CalendarAnnouncementButtonsProps = {
calendar?: string,
announcements?: string
}
export const CalendarAnnouncementButtons = ({calendar, announcements}: CalendarAnnouncementButtonsProps) => {
return (
<>
{ calendar &&
<Button
size={"md"}
schema={"shade"}
type={"button"}
href={calendar}
target={"_blank"}
>Liturgischer Kalendar</Button>
}
{ announcements &&
<span className={styles.margin}>
<Button
size={"md"}
type={"button"}
schema={"shade"}
href={announcements}
target={"_blank"}
>Vermeldungen</Button>
</span>
}
</>
)
}

View file

@ -0,0 +1,3 @@
.margin {
margin-left: 5px;
}

View file

@ -1,5 +1,5 @@
.right { .right {
margin-top: 90px; margin-top: 40px;
text-align: right; text-align: right;
} }

41
src/fetch/calendar.ts Normal file
View file

@ -0,0 +1,41 @@
import { stringify } from 'qs-esm'
import { PaginatedDocs } from 'payload'
import { Calendar } from '@/payload-types'
/**
* Fetch last calendar for a parish
*/
export const fetchLastCalendar = async (parishId: string): Promise<Calendar | undefined> => {
const date = new Date();
date.setDate(date.getDate() - 14)
const query: any = {
and: [
{
parish: {
equals: parishId
}
},
{
date: {
greater_than_equal: date.toISOString(),
}
}
]
}
const stringifiedQuery = stringify(
{
sort: "-date",
where: query,
limit: 1,
},
{ addQueryPrefix: true },
)
const response = await fetch(`http://localhost:3000/api/calendar${stringifiedQuery}`)
if (!response.ok) return undefined
const announcements = await response.json() as PaginatedDocs<Calendar>
return announcements.docs[0]
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,68 @@
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
export async function up({ payload, req }: MigrateUpArgs): Promise<void> {
await payload.db.drizzle.execute(sql`
CREATE TABLE IF NOT EXISTS "calendar" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"date" timestamp(3) with time zone DEFAULT '2025-02-02T14:51:45.077Z' NOT NULL,
"document_id" uuid NOT NULL,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);
CREATE TABLE IF NOT EXISTS "calendar_rels" (
"id" serial PRIMARY KEY NOT NULL,
"order" integer,
"parent_id" uuid NOT NULL,
"path" varchar NOT NULL,
"parish_id" uuid
);
ALTER TABLE "announcement" ALTER COLUMN "date" SET DEFAULT '2025-02-02T14:51:44.981Z';
ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "calendar_id" uuid;
DO $$ BEGIN
ALTER TABLE "calendar" ADD CONSTRAINT "calendar_document_id_documents_id_fk" FOREIGN KEY ("document_id") REFERENCES "public"."documents"("id") ON DELETE set null ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
DO $$ BEGIN
ALTER TABLE "calendar_rels" ADD CONSTRAINT "calendar_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."calendar"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
DO $$ BEGIN
ALTER TABLE "calendar_rels" ADD CONSTRAINT "calendar_rels_parish_fk" FOREIGN KEY ("parish_id") REFERENCES "public"."parish"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
CREATE INDEX IF NOT EXISTS "calendar_document_idx" ON "calendar" USING btree ("document_id");
CREATE INDEX IF NOT EXISTS "calendar_updated_at_idx" ON "calendar" USING btree ("updated_at");
CREATE INDEX IF NOT EXISTS "calendar_created_at_idx" ON "calendar" USING btree ("created_at");
CREATE INDEX IF NOT EXISTS "calendar_rels_order_idx" ON "calendar_rels" USING btree ("order");
CREATE INDEX IF NOT EXISTS "calendar_rels_parent_idx" ON "calendar_rels" USING btree ("parent_id");
CREATE INDEX IF NOT EXISTS "calendar_rels_path_idx" ON "calendar_rels" USING btree ("path");
CREATE INDEX IF NOT EXISTS "calendar_rels_parish_id_idx" ON "calendar_rels" USING btree ("parish_id");
DO $$ BEGIN
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_calendar_fk" FOREIGN KEY ("calendar_id") REFERENCES "public"."calendar"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_calendar_id_idx" ON "payload_locked_documents_rels" USING btree ("calendar_id");`)
}
export async function down({ payload, req }: MigrateDownArgs): Promise<void> {
await payload.db.drizzle.execute(sql`
ALTER TABLE "calendar" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "calendar_rels" DISABLE ROW LEVEL SECURITY;
DROP TABLE "calendar" CASCADE;
DROP TABLE "calendar_rels" CASCADE;
ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT "payload_locked_documents_rels_calendar_fk";
DROP INDEX IF EXISTS "payload_locked_documents_rels_calendar_id_idx";
ALTER TABLE "announcement" ALTER COLUMN "date" DROP DEFAULT;
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN IF EXISTS "calendar_id";`)
}

View file

@ -1,6 +1,7 @@
import * as migration_20241205_121237 from './20241205_121237'; import * as migration_20241205_121237 from './20241205_121237';
import * as migration_20241217_135114_contact_person_photo from './20241217_135114_contact_person_photo'; import * as migration_20241217_135114_contact_person_photo from './20241217_135114_contact_person_photo';
import * as migration_20250128_100809_pope_prayer_intentions from './20250128_100809_pope_prayer_intentions'; import * as migration_20250128_100809_pope_prayer_intentions from './20250128_100809_pope_prayer_intentions';
import * as migration_20250128_145145_liturgical_calendar from './20250128_145145_liturgical_calendar';
export const migrations = [ export const migrations = [
{ {
@ -16,6 +17,11 @@ export const migrations = [
{ {
up: migration_20250128_100809_pope_prayer_intentions.up, up: migration_20250128_100809_pope_prayer_intentions.up,
down: migration_20250128_100809_pope_prayer_intentions.down, down: migration_20250128_100809_pope_prayer_intentions.down,
name: '20250128_100809_pope_prayer_intentions' name: '20250128_100809_pope_prayer_intentions',
},
{
up: migration_20250128_145145_liturgical_calendar.up,
down: migration_20250128_145145_liturgical_calendar.down,
name: '20250128_145145_liturgical_calendar'
}, },
]; ];

View file

@ -198,9 +198,11 @@ export const Default: Story = {
args: { args: {
title: "St. Christophorus", title: "St. Christophorus",
image: chris, image: chris,
announcement: "https://",
calendar: "https://",
events: [], events: [],
worship: [], worship: [],
description: "Die St. Christophorus Kirche in Berlin-Neukölln ist ein bedeutendes Beispiel für modernen Kirchenbau in der Hauptstadt. Erbaut in den 1960er Jahren, spiegelt das Gebäude die Architektur und künstlerische Gestaltung dieser Zeit wider und zeichnet sich durch schlichte, klare Linien und einen funktionalen Stil aus. Die Kirche ist nach dem heiligen Christophorus benannt, dem Schutzpatron der Reisenden, und bietet den Gemeindemitgliedern und Besuchern einen Ort der Ruhe und Besinnung im lebhaften Stadtteil Neukölln. Neben Gottesdiensten finden hier regelmäßig kulturelle Veranstaltungen und soziale Projekte statt, die die Kirche zu einem wichtigen Treffpunkt im Kiez machen.", description: "Die St. Christophorus Kirche in Berlin-Neukölln ist ein bedeutendes Beispiel für modernen Kirchenbau in der Hauptstadt. Erbaut in den 1960er Jahren, spiegelt das Gebäude die Architektur und künstlerische Gestaltung dieser Zeit wider und zeichnet sich durch schlichte, klare Linien und einen funktionalen Stil aus. Die Kirche ist nach dem heiligen Christophorus benannt, dem Schutzpatron der Reisenden, und bietet den Gemeindemitgliedern und Besuchern einen Ort der Ruhe und Besinnung im lebhaften Stadtteil Neukölln. Neben Gottesdiensten.",
history: `Am 27.Juni 1929 erschien folgende Niederschrift in der Märkischen Volkszeitung, die eine berechtigte Freude über die Nachricht von dem Bau der neuen Kirche am Reuterplatz auslöste: history: `Am 27.Juni 1929 erschien folgende Niederschrift in der Märkischen Volkszeitung, die eine berechtigte Freude über die Nachricht von dem Bau der neuen Kirche am Reuterplatz auslöste:
Eine neue katholische Kirche in Neukölln. Eine neue katholische Kirche in Neukölln.
@ -238,4 +240,5 @@ pfarramt@christophorus-berlin.de
Bürozeiten: Bürozeiten:
Freitags 09:00 - 12:00 Uhr ` Freitags 09:00 - 12:00 Uhr `
}, },
}; };

View file

@ -15,6 +15,7 @@ import { tranformWorship } from '@/utils/dto/worship'
import { Button } from '@/components/Button/Button' import { Button } from '@/components/Button/Button'
import { TextDiv } from '@/components/Text/TextDiv' import { TextDiv } from '@/components/Text/TextDiv'
import { Gallery, GalleryItem } from '@/components/Gallery/Gallery' import { Gallery, GalleryItem } from '@/components/Gallery/Gallery'
import { CalendarAnnouncementButtons } from '@/compositions/CalendarAnnouncementButtons/CalendarAnnouncementButtons'
type ParishProps = { type ParishProps = {
title: string, title: string,
@ -29,7 +30,8 @@ type ParishProps = {
contact: string contact: string
events: Event[], events: Event[],
worship: Worship[] worship: Worship[]
announcement?: string announcement?: string,
calendar?: string,
gallery?: GalleryItem[] gallery?: GalleryItem[]
} }
@ -47,6 +49,7 @@ export const Parish = (
events, events,
worship, worship,
announcement, announcement,
calendar,
gallery gallery
} }
: ParishProps : ParishProps
@ -54,7 +57,12 @@ export const Parish = (
return ( return (
<> <>
<ImageWithText title={title} image={image} text={description} /> <ImageWithText
title={title}
image={image}
text={description}
link={<CalendarAnnouncementButtons calendar={calendar} announcements={announcement}/>}
/>
<Section> <Section>
<Container> <Container>

View file

@ -16,6 +16,7 @@ export interface Config {
worship: Worship; worship: Worship;
popePrayerIntentions: PopePrayerIntention; popePrayerIntentions: PopePrayerIntention;
announcement: Announcement; announcement: Announcement;
calendar: Calendar;
blog: Blog; blog: Blog;
highlight: Highlight; highlight: Highlight;
event: Event; event: Event;
@ -39,6 +40,7 @@ export interface Config {
worship: WorshipSelect<false> | WorshipSelect<true>; worship: WorshipSelect<false> | WorshipSelect<true>;
popePrayerIntentions: PopePrayerIntentionsSelect<false> | PopePrayerIntentionsSelect<true>; popePrayerIntentions: PopePrayerIntentionsSelect<false> | PopePrayerIntentionsSelect<true>;
announcement: AnnouncementSelect<false> | AnnouncementSelect<true>; announcement: AnnouncementSelect<false> | AnnouncementSelect<true>;
calendar: CalendarSelect<false> | CalendarSelect<true>;
blog: BlogSelect<false> | BlogSelect<true>; blog: BlogSelect<false> | BlogSelect<true>;
highlight: HighlightSelect<false> | HighlightSelect<true>; highlight: HighlightSelect<false> | HighlightSelect<true>;
event: EventSelect<false> | EventSelect<true>; event: EventSelect<false> | EventSelect<true>;
@ -259,6 +261,18 @@ export interface Document {
focalX?: number | null; focalX?: number | null;
focalY?: number | null; focalY?: number | null;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "calendar".
*/
export interface Calendar {
id: string;
date: string;
parish: (string | Parish)[];
document: string | Document;
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` "blog". * via the `definition` "blog".
@ -582,6 +596,10 @@ export interface PayloadLockedDocument {
relationTo: 'announcement'; relationTo: 'announcement';
value: string | Announcement; value: string | Announcement;
} | null) } | null)
| ({
relationTo: 'calendar';
value: string | Calendar;
} | null)
| ({ | ({
relationTo: 'blog'; relationTo: 'blog';
value: string | Blog; value: string | Blog;
@ -750,6 +768,17 @@ export interface AnnouncementSelect<T extends boolean = true> {
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "calendar_select".
*/
export interface CalendarSelect<T extends boolean = true> {
date?: T;
parish?: T;
document?: 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` "blog_select". * via the `definition` "blog_select".

View file

@ -37,6 +37,7 @@ import { ContactPerson } from '@/collections/ContactPerson'
import { postgresAdapter } from '@payloadcms/db-postgres' import { postgresAdapter } from '@payloadcms/db-postgres'
import { gcsStorage } from '@payloadcms/storage-gcs' import { gcsStorage } from '@payloadcms/storage-gcs'
import { PopesPrayerIntentions } from '@/collections/PopesPrayerIntentions' import { PopesPrayerIntentions } from '@/collections/PopesPrayerIntentions'
import { LiturgicalCalendar } from '@/collections/LiturgicalCalendar'
const filename = fileURLToPath(import.meta.url) const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename) const dirname = path.dirname(filename)
@ -51,6 +52,7 @@ export default buildConfig({
Worship, Worship,
PopesPrayerIntentions, PopesPrayerIntentions,
Announcements, Announcements,
LiturgicalCalendar,
Blog, Blog,
Highlight, Highlight,
Events, Events,

10
src/utils/sunday.ts Normal file
View file

@ -0,0 +1,10 @@
/**
* Get next sunday
*/
export const nextSunday = () => {
const today = new Date();
const daysUntilNextSunday = 7 - today.getDay(); // Sunday is 0, Monday is 1, etc.
const nextSunday = new Date(today);
nextSunday.setDate(today.getDate() + daysUntilNextSunday);
return nextSunday;
}