feature: event and worship pages

This commit is contained in:
Benno Tielen 2024-12-12 19:13:58 +01:00
parent 3fcde82b6c
commit 2cce2676db
21 changed files with 405 additions and 22 deletions

View file

@ -26,6 +26,7 @@
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"graphql": "^16.8.1", "graphql": "^16.8.1",
"mapbox-gl": "^3.5.2", "mapbox-gl": "^3.5.2",
"moment": "^2.30.1",
"next": "15.0.0", "next": "15.0.0",
"payload": "^3.3.0", "payload": "^3.3.0",
"qs-esm": "^7.0.2", "qs-esm": "^7.0.2",

View file

@ -26,8 +26,9 @@ export default async function ParishPage ({ params }: { params: Promise<{slug: s
churches, churches,
gallery gallery
} = parish.docs[0] } = parish.docs[0]
const events = await fetchEvents(id) const events = await fetchEvents({ parishId: id })
const worship = await fetchWorship(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 announcement = await fetchLastAnnouncement(id); const announcement = await fetchLastAnnouncement(id);
return ( return (
<Parish <Parish

View file

@ -0,0 +1,94 @@
import { PageHeader } from '@/compositions/PageHeader/PageHeader'
import { Section } from '@/components/Section/Section'
import { Container } from '@/components/Container/Container'
import { Title } from '@/components/Title/Title'
import { NextPrevButtons } from '@/components/NextPrevButtons/NextPrevButtons'
import moment from 'moment'
import { weekNumber } from '@/utils/week'
import { EventRow } from '@/components/EventRow/EventRow'
import Error from '@/pages/_error'
import { fetchWorship } from '@/fetch/worship'
import { tranformWorship } from '@/utils/dto/worship'
export default async function WorshipPage({searchParams}: {
searchParams: Promise<{ week: string | undefined }>
}) {
const query = await searchParams;
let week = query.week;
if (!week) {
week = weekNumber(moment());
}
const fromDate = moment(week, true);
if (!fromDate.isValid()) {
return <Error statusCode={422} message={"Woche fehlerhaft formatiert."}/>
}
const toDate = moment(week).add(1, 'week');
const lastWeek = moment(week).subtract(1, 'week');
const paginatedWorship = await fetchWorship(
{
fromDate: fromDate.toDate(),
tillDate: toDate.toDate()
}
);
if (!paginatedWorship) {
return <Error statusCode={503} message={"Gottesdienste konnten nicht geladen werden."}/>;
}
const events = tranformWorship(paginatedWorship.docs)
return (
<>
<PageHeader
title={"Gottesdienste"}
description={"Erleben Sie unsere Heilige Messe und feiern Sie mit uns! Auf dieser Seite finden Sie alle Termine, Details und besondere Highlights unserer Gottesdienste im Überblick. Seien Sie herzlich willkommen!"}
/>
<Section padding={"small"} paddingBottom={"large"}>
<Container>
<Title
color={"contrast"}
title={`Woche ${week.substring(5)} - ${week.substring(0, 4)}`}
size={"md"}
/>
{events.map(e =>
<EventRow
key={e.id}
date={e.date}
title={e.title}
href={e.href}
location={e.location}
cancelled={e.cancelled}
/>
)}
{events.length == 0 &&
<p>
Keine Gottesdienste gefunden
</p>
}
</Container>
</Section>
<Section padding={"small"}>
<NextPrevButtons
prev={{
href: `/gottesdienst?week=${weekNumber(lastWeek)}`,
text: "Vorige Woche"
}}
next={{
href: `/gottesdienst?week=${weekNumber(toDate)}`,
text: "Nächste Woche"
}}
/>
</Section>
</>
)
}

View file

@ -28,7 +28,7 @@ export default async function GroupPage({ params }: { params: Promise<{slug: str
const {id, shortDescription, photo,name, text_html, content } = groups.docs[0] const {id, shortDescription, photo,name, text_html, content } = groups.docs[0]
const media = getPhoto("tablet", photo) const media = getPhoto("tablet", photo)
const events = await fetchEvents(undefined, id) const events = await fetchEvents({groupId: id})
return ( return (
<> <>

View file

@ -144,7 +144,7 @@ export default function RootLayout({
{ {
title: "Gottesdienste", title: "Gottesdienste",
description: "Begegnung mit Gott", description: "Begegnung mit Gott",
href: "https://" href: "/gottesdienst"
}, },
{ {
title: "Rosenkranz", title: "Rosenkranz",
@ -237,6 +237,10 @@ export default function RootLayout({
] ]
} }
}, },
{
text: 'Veranstaltungen',
href: '/veranstaltungen'
},
{ {
text: 'Kontakt', text: 'Kontakt',
href: '/kontakt' href: '/kontakt'

View file

@ -8,7 +8,7 @@ export const dynamic = 'force-dynamic'
export default async function HomePage() { export default async function HomePage() {
const events = await fetchEvents(undefined) const events = await fetchEvents()
const worship = await fetchWorship() const worship = await fetchWorship()
const blog = await fetchBlog() const blog = await fetchBlog()
const highlights = await fetchHighlights() const highlights = await fetchHighlights()

View file

@ -0,0 +1,134 @@
import { fetchEvents } from '@/fetch/events'
import { PageHeader } from '@/compositions/PageHeader/PageHeader'
import { Section } from '@/components/Section/Section'
import { Container } from '@/components/Container/Container'
import { Title } from '@/components/Title/Title'
import { NextPrevButtons } from '@/components/NextPrevButtons/NextPrevButtons'
import moment from 'moment'
import { transformEvents } from '@/utils/dto/events'
import { weekNumber } from '@/utils/week'
import { EventRow } from '@/components/EventRow/EventRow'
import { fetchHighlightsBetweenDates } from '@/fetch/highlights'
import { Row } from '@/components/Flex/Row'
import { Col } from '@/components/Flex/Col'
import { highlightLink } from '@/utils/dto/highlight'
import Error from '@/pages/_error'
export default async function EventsPage({searchParams}: {
searchParams: Promise<{ week: string | undefined }>
}) {
const query = await searchParams;
const limit = 100;
let week = query.week;
if (!week) {
week = weekNumber(moment());
}
const fromDate = moment(week, true);
if (!fromDate.isValid()) {
return <Error statusCode={422} message={"Woche fehlerhaft formatiert."}/>
}
const toDate = moment(week).add(1, 'week');
const lastWeek = moment(week).subtract(1, 'week');
const paginatedEvents = await fetchEvents(
{
limit: limit,
fromDate: fromDate.toDate(),
toDate: toDate.toDate()
}
);
const paginatedHighlights = ((await fetchHighlightsBetweenDates(
fromDate.toDate(),
toDate.toDate(),
))?.docs) || [];
if (!paginatedEvents) {
return <Error statusCode={503} message={"Veranstaltungen konnten nicht geladen werden."}/>;
}
const events = transformEvents(paginatedEvents.docs)
return (
<>
<PageHeader
title={"Veranstaltungen"}
description={"Entdecken Sie unsere kommenden Veranstaltungen und seien Sie dabei! Hier finden Sie alle Termine, Informationen und Highlights auf einen Blick."}
/>
<Section padding={"small"} paddingBottom={"large"}>
<Container>
<Row>
<Col>
<Title
color={"contrast"}
title={`Woche ${week.substring(5)} - ${week.substring(0, 4)}`}
size={"md"}
/>
{events.map(e =>
<EventRow
key={e.id}
date={e.date}
title={e.title}
href={e.href}
location={e.location}
cancelled={e.cancelled}
/>
)}
{events.length == 0 &&
<p>
Keine Veranstaltungen gefunden
</p>
}
</Col>
<Col>
{ paginatedHighlights.length > 0 &&
<>
<Title
color={"base"}
title={`Highlights`}
size={"md"}
/>
{ paginatedHighlights.map(h =>
<EventRow
key={h.id}
date={h.date}
title={h.text}
href={highlightLink(h)}
cancelled={false}
showDate={false}
/>
)}
</>
}
</Col>
</Row>
</Container>
</Section>
<Section padding={"small"}>
<NextPrevButtons
prev={{
href: `/veranstaltungen?week=${weekNumber(lastWeek)}`,
text: "Vorige Woche"
}}
next={{
href: `/veranstaltungen?week=${weekNumber(toDate)}`,
text: "Nächste Woche"
}}
/>
</Section>
</>
)
}

View file

@ -12,7 +12,13 @@ $width: 1100px;
@media screen and (max-width: $width) { @media screen and (max-width: $width) {
.container { .container {
padding: 0 20px; padding: 0 40px;
margin: 0; margin: 0;
} }
} }
@media screen and (max-width: 576px) {
.container {
padding: 0 20px;
}
}

View file

@ -8,6 +8,7 @@ import Link from 'next/link'
export type EventRowProps = { export type EventRowProps = {
/** datetime 8601 format */ /** datetime 8601 format */
date: string, date: string,
showDate?: boolean
title: string, title: string,
href?: string, href?: string,
location?: string, location?: string,
@ -41,7 +42,7 @@ const shortMonth = (date: string) => {
export const EventRow = ({date, title, location, cancelled, href, color = "base"}: EventRowProps) => { export const EventRow = ({date, title, location, cancelled, href, color = "base", showDate = true}: EventRowProps) => {
const day = useMemo(() => date.substring(8, 10), [date]); const day = useMemo(() => date.substring(8, 10), [date]);
const dateObj = useMemo(() => new Date(date), [date]); const dateObj = useMemo(() => new Date(date), [date]);
const month = useMemo(() => shortMonth(date), [date]); const month = useMemo(() => shortMonth(date), [date]);
@ -75,9 +76,13 @@ export const EventRow = ({date, title, location, cancelled, href, color = "base"
[styles.cancelled]: cancelled [styles.cancelled]: cancelled
})}> })}>
{title} <br /> {title} <br />
{dateObj.toLocaleDateString("de-DE", { weekday: "long" })} { showDate &&
{dayFormat === "long" && " " + dateObj.toLocaleDateString("de-DE", { dateStyle: "medium" })}, {dateObj.toLocaleTimeString("de-DE", { timeStyle: "short" })} Uhr <>
<br /> {dateObj.toLocaleDateString("de-DE", { weekday: "long" })}
{dayFormat === "long" && " " + dateObj.toLocaleDateString("de-DE", { dateStyle: "medium" })}, {dateObj.toLocaleTimeString("de-DE", { timeStyle: "short" })} Uhr
<br />
</>
}
{location} {location}
</div> </div>
</div> </div>

View file

@ -7,6 +7,7 @@
text-align: center; text-align: center;
width: 93px; width: 93px;
transition: color 0.2s ease-in; transition: color 0.2s ease-in;
flex-shrink: 0;
} }
.dayBase { .dayBase {

View file

@ -8,6 +8,12 @@
flex: 1 1 0; flex: 1 1 0;
} }
@media screen and (max-width: 1024px) {
.row {
gap: 40px;
}
}
@media screen and (max-width: 576px) { @media screen and (max-width: 576px) {
.col { .col {
flex: 0 0 100%; flex: 0 0 100%;

View file

@ -75,7 +75,7 @@
max-height: 1000px; max-height: 1000px;
} }
@media screen and (max-width: 1000px) { @media screen and (max-width: 1100px) {
.nav { .nav {
flex-direction: column; flex-direction: column;
padding: 15px 15px; padding: 15px 15px;

View file

@ -31,7 +31,7 @@ export const Footer = () => {
</p> </p>
<ul className={styles.list}> <ul className={styles.list}>
<li><Link href={"/kontakt"}>Kontakt</Link></li> <li><Link href={"/kontakt"}>Kontakt</Link></li>
<li><Link href={"/gottesdiensten"}>Gottesdiensten</Link></li> <li><Link href={"/gottesdienst"}>Gottesdiensten</Link></li>
<li><Link href={"/datenschutz"}>Datenschutz</Link></li> <li><Link href={"/datenschutz"}>Datenschutz</Link></li>
<li><Link href={"/schutzkonzept"}>Schutzkonzept</Link></li> <li><Link href={"/schutzkonzept"}>Schutzkonzept</Link></li>
<li><Link href={"/impressum"}>Impressum</Link></li> <li><Link href={"/impressum"}>Impressum</Link></li>

View file

@ -2,23 +2,43 @@ import { stringify } from 'qs-esm'
import { PaginatedDocs } from 'payload' import { PaginatedDocs } from 'payload'
import { Event } from '@/payload-types' import { Event } from '@/payload-types'
type Args = {
parishId?: string;
groupId?: string;
limit?: number;
page?: number;
fromDate?: Date
toDate?: Date
}
/** /**
* Fetch a list of events * Fetch a list of events
* *
*/ */
export async function fetchEvents(parishId: string | undefined, groupId?: string): Promise<PaginatedDocs<Event> | undefined> { export async function fetchEvents(args?: Args): Promise<PaginatedDocs<Event> | undefined> {
const date = new Date()
const {parishId, groupId, limit = 30, page = 0, fromDate = new Date(), toDate} = args || {};
const query: any = { const query: any = {
and: [ and: [
{ {
date: { date: {
greater_than_equal: date.toISOString(), greater_than_equal: fromDate.toISOString(),
}, },
} }
], ],
} }
if (toDate) {
query.and.push({
date: {
less_than: toDate.toISOString(),
}
})
}
if (parishId) { if (parishId) {
query.and.push({ query.and.push({
"parish": { "parish": {
@ -46,7 +66,9 @@ export async function fetchEvents(parishId: string | undefined, groupId?: string
title: true, title: true,
cancelled: true cancelled: true
}, },
depth: 1 depth: 1,
limit,
page
}, },
{ addQueryPrefix: true }, { addQueryPrefix: true },
) )

View file

@ -28,6 +28,36 @@ export const fetchHighlights = async (): Promise<PaginatedDocs<Highlight> | unde
{ addQueryPrefix: true }, { addQueryPrefix: true },
) )
const response = await fetch(`http://localhost:3000/api/highlight${stringifiedQuery}`)
if (!response.ok) return undefined
return response.json()
}
export const fetchHighlightsBetweenDates = async (from: Date, until: Date): Promise<PaginatedDocs<Highlight> | undefined> => {
const query: any = {
and: [
{
date: {
greater_than_equal: from.toISOString(),
}
},
{
date: {
less_than: until.toISOString(),
}
}
],
}
const stringifiedQuery = stringify(
{
sort: "date",
where: query,
limit: 5
},
{ addQueryPrefix: true },
)
const response = await fetch(`http://localhost:3000/api/highlight${stringifiedQuery}`) const response = await fetch(`http://localhost:3000/api/highlight${stringifiedQuery}`)
if (!response.ok) return undefined if (!response.ok) return undefined
return response.json() return response.json()

View file

@ -2,9 +2,22 @@ import { stringify } from 'qs-esm'
import { PaginatedDocs } from 'payload' import { PaginatedDocs } from 'payload'
import { Worship } from '@/payload-types' import { Worship } from '@/payload-types'
export const fetchWorship = async (locations?: string[]): Promise<PaginatedDocs<Worship> | undefined> => { type FetchWorshipArgs = {
const date = new Date(); fromDate?: Date,
date.setHours(0, 0, 0, 0); tillDate?: Date,
locations?: string[]
}
export const fetchWorship = async (args?: FetchWorshipArgs): Promise<PaginatedDocs<Worship> | undefined> => {
const {fromDate, tillDate, locations} = args || {}
let date = fromDate;
if (!date) {
date = new Date();
date.setHours(0, 0, 0, 0);
}
const query: any = { const query: any = {
and: [ and: [
@ -16,6 +29,14 @@ export const fetchWorship = async (locations?: string[]): Promise<PaginatedDocs<
], ],
} }
if (tillDate) {
query.and.push({
date: {
less_than: tillDate.toISOString()
}
})
}
if (locations ) { if (locations ) {
query.and.push({ query.and.push({
location: { location: {

29
src/pages/_error.tsx Normal file
View file

@ -0,0 +1,29 @@
import { PageHeader } from '@/compositions/PageHeader/PageHeader'
import { Section } from '@/components/Section/Section'
import { Container } from '@/components/Container/Container'
import { TextDiv } from '@/components/Text/TextDiv'
type ErrorProps = {
statusCode: number;
message?: string
}
export default function Error({statusCode, message}: ErrorProps) {
return (
<>
<PageHeader
title={"Fehler aufgetreten"}
description={"Leider ist etwas schiefgelaufen. Bitte entschuldigen Sie die Unannehmlichkeiten!"}
/>
<Section padding={"small"}>
<Container>
<h3>Fehler {statusCode}</h3>
<TextDiv text={message || ""} />
</Container>
</Section>
</>
)
}

View file

@ -1,7 +1,15 @@
import { Event } from '@/payload-types' import { Event } from '@/payload-types'
import { EventRowProps } from '@/components/EventRow/EventRow'
export const transformEvents = (events: Event[]): EventRowProps[] => { type EventWithId = {
id: string,
date: string,
title: string,
href: string,
location: string,
cancelled: boolean
}
export const transformEvents = (events: Event[]): EventWithId[] => {
return events.map(e => { return events.map(e => {
return { return {
id: e.id, id: e.id,

View file

@ -17,7 +17,7 @@ export const transformCategory = (category: "MASS" | "FAMILY" | "WORD"): string
/** /**
* Transform worship data to `EventRow` component properties * Transform worship data to `EventRow` component properties
*/ */
export const tranformWorship = (worship: Worship[]): EventRowProps[] => { export const tranformWorship = (worship: Worship[]): (EventRowProps & {id: string})[] => {
return worship.map(w => { return worship.map(w => {
return { return {

13
src/utils/week.ts Normal file
View file

@ -0,0 +1,13 @@
import moment from 'moment/moment'
/**
* Get week number string
*
* e.G. "2024W05"
*/
export const weekNumber = (date: moment.Moment) => {
const year = date.isoWeekYear()
const weekNr = date.isoWeek()
const leadingZero = weekNr < 10 ? '0' : ''
return `${year}W${leadingZero}${weekNr}`
}

View file

@ -7652,6 +7652,7 @@ __metadata:
eslint-plugin-storybook: "npm:^0.8.0" eslint-plugin-storybook: "npm:^0.8.0"
graphql: "npm:^16.8.1" graphql: "npm:^16.8.1"
mapbox-gl: "npm:^3.5.2" mapbox-gl: "npm:^3.5.2"
moment: "npm:^2.30.1"
next: "npm:15.0.0" next: "npm:15.0.0"
payload: "npm:^3.3.0" payload: "npm:^3.3.0"
qs-esm: "npm:^7.0.2" qs-esm: "npm:^7.0.2"
@ -12175,6 +12176,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"moment@npm:^2.30.1":
version: 2.30.1
resolution: "moment@npm:2.30.1"
checksum: 10c0/865e4279418c6de666fca7786607705fd0189d8a7b7624e2e56be99290ac846f90878a6f602e34b4e0455c549b85385b1baf9966845962b313699e7cb847543a
languageName: node
linkType: hard
"ms@npm:2.0.0": "ms@npm:2.0.0":
version: 2.0.0 version: 2.0.0
resolution: "ms@npm:2.0.0" resolution: "ms@npm:2.0.0"