diff --git a/package.json b/package.json index 526c98b..06a4e87 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "cross-env": "^7.0.3", "graphql": "^16.8.1", "mapbox-gl": "^3.5.2", + "moment": "^2.30.1", "next": "15.0.0", "payload": "^3.3.0", "qs-esm": "^7.0.2", diff --git a/src/app/gemeinde/[slug]/page.tsx b/src/app/gemeinde/[slug]/page.tsx index 6e05c1a..0232c42 100644 --- a/src/app/gemeinde/[slug]/page.tsx +++ b/src/app/gemeinde/[slug]/page.tsx @@ -26,8 +26,9 @@ export default async function ParishPage ({ params }: { params: Promise<{slug: s churches, gallery } = parish.docs[0] - const events = await fetchEvents(id) - const worship = await fetchWorship(churches.map(c => typeof c === "string" ? c : c.id)) + const events = await fetchEvents({ parishId: id }) + const churchIds = churches.map(c => typeof c === "string" ? c : c.id) + const worship = await fetchWorship({ locations: churchIds }) const announcement = await fetchLastAnnouncement(id); return ( +}) { + + const query = await searchParams; + let week = query.week; + if (!week) { + week = weekNumber(moment()); + } + + const fromDate = moment(week, true); + + if (!fromDate.isValid()) { + return + } + + 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 ; + } + + const events = tranformWorship(paginatedWorship.docs) + + return ( + <> + + +
+ + + + {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> + </> + ) +} \ No newline at end of file diff --git a/src/app/gruppe/[slug]/page.tsx b/src/app/gruppe/[slug]/page.tsx index e0ff915..5e47c1d 100644 --- a/src/app/gruppe/[slug]/page.tsx +++ b/src/app/gruppe/[slug]/page.tsx @@ -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 media = getPhoto("tablet", photo) - const events = await fetchEvents(undefined, id) + const events = await fetchEvents({groupId: id}) return ( <> diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 1af95b4..21e1b97 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -144,7 +144,7 @@ export default function RootLayout({ { title: "Gottesdienste", description: "Begegnung mit Gott", - href: "https://" + href: "/gottesdienst" }, { title: "Rosenkranz", @@ -237,6 +237,10 @@ export default function RootLayout({ ] } }, + { + text: 'Veranstaltungen', + href: '/veranstaltungen' + }, { text: 'Kontakt', href: '/kontakt' diff --git a/src/app/page.tsx b/src/app/page.tsx index 326b10f..b431fbf 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -8,7 +8,7 @@ export const dynamic = 'force-dynamic' export default async function HomePage() { - const events = await fetchEvents(undefined) + const events = await fetchEvents() const worship = await fetchWorship() const blog = await fetchBlog() const highlights = await fetchHighlights() diff --git a/src/app/veranstaltungen/page.tsx b/src/app/veranstaltungen/page.tsx new file mode 100644 index 0000000..61d8644 --- /dev/null +++ b/src/app/veranstaltungen/page.tsx @@ -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> + </> + ) +} \ No newline at end of file diff --git a/src/components/Container/styles.module.scss b/src/components/Container/styles.module.scss index 4d6e522..4c9a475 100644 --- a/src/components/Container/styles.module.scss +++ b/src/components/Container/styles.module.scss @@ -12,7 +12,13 @@ $width: 1100px; @media screen and (max-width: $width) { .container { - padding: 0 20px; + padding: 0 40px; margin: 0; } } + +@media screen and (max-width: 576px) { + .container { + padding: 0 20px; + } +} diff --git a/src/components/EventRow/EventRow.tsx b/src/components/EventRow/EventRow.tsx index 1453e29..7526b97 100644 --- a/src/components/EventRow/EventRow.tsx +++ b/src/components/EventRow/EventRow.tsx @@ -8,6 +8,7 @@ import Link from 'next/link' export type EventRowProps = { /** datetime 8601 format */ date: string, + showDate?: boolean title: string, href?: 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 dateObj = useMemo(() => new Date(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 })}> {title} <br /> - {dateObj.toLocaleDateString("de-DE", { weekday: "long" })} - {dayFormat === "long" && " " + dateObj.toLocaleDateString("de-DE", { dateStyle: "medium" })}, {dateObj.toLocaleTimeString("de-DE", { timeStyle: "short" })} Uhr - <br /> + { showDate && + <> + {dateObj.toLocaleDateString("de-DE", { weekday: "long" })} + {dayFormat === "long" && " " + dateObj.toLocaleDateString("de-DE", { dateStyle: "medium" })}, {dateObj.toLocaleTimeString("de-DE", { timeStyle: "short" })} Uhr + <br /> + </> + } {location} </div> </div> diff --git a/src/components/EventRow/styles.module.scss b/src/components/EventRow/styles.module.scss index 28a80f9..aad0b5a 100644 --- a/src/components/EventRow/styles.module.scss +++ b/src/components/EventRow/styles.module.scss @@ -7,6 +7,7 @@ text-align: center; width: 93px; transition: color 0.2s ease-in; + flex-shrink: 0; } .dayBase { diff --git a/src/components/Flex/styles.module.scss b/src/components/Flex/styles.module.scss index c51e927..9d319ea 100644 --- a/src/components/Flex/styles.module.scss +++ b/src/components/Flex/styles.module.scss @@ -8,6 +8,12 @@ flex: 1 1 0; } +@media screen and (max-width: 1024px) { + .row { + gap: 40px; + } +} + @media screen and (max-width: 576px) { .col { flex: 0 0 100%; diff --git a/src/components/Menu/styles.module.scss b/src/components/Menu/styles.module.scss index 733e45c..a3d84d6 100644 --- a/src/components/Menu/styles.module.scss +++ b/src/components/Menu/styles.module.scss @@ -75,7 +75,7 @@ max-height: 1000px; } -@media screen and (max-width: 1000px) { +@media screen and (max-width: 1100px) { .nav { flex-direction: column; padding: 15px 15px; diff --git a/src/compositions/Footer/Footer.tsx b/src/compositions/Footer/Footer.tsx index 266d1fe..9b0daf0 100644 --- a/src/compositions/Footer/Footer.tsx +++ b/src/compositions/Footer/Footer.tsx @@ -31,7 +31,7 @@ export const Footer = () => { </p> <ul className={styles.list}> <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={"/schutzkonzept"}>Schutzkonzept</Link></li> <li><Link href={"/impressum"}>Impressum</Link></li> diff --git a/src/fetch/events.ts b/src/fetch/events.ts index 9e16984..ca15d17 100644 --- a/src/fetch/events.ts +++ b/src/fetch/events.ts @@ -2,23 +2,43 @@ import { stringify } from 'qs-esm' import { PaginatedDocs } from 'payload' import { Event } from '@/payload-types' +type Args = { + parishId?: string; + groupId?: string; + limit?: number; + page?: number; + fromDate?: Date + toDate?: Date +} + + /** * Fetch a list of events * */ -export async function fetchEvents(parishId: string | undefined, groupId?: string): Promise<PaginatedDocs<Event> | undefined> { - const date = new Date() +export async function fetchEvents(args?: Args): Promise<PaginatedDocs<Event> | undefined> { + + const {parishId, groupId, limit = 30, page = 0, fromDate = new Date(), toDate} = args || {}; + const query: any = { and: [ { date: { - greater_than_equal: date.toISOString(), + greater_than_equal: fromDate.toISOString(), }, } ], } + if (toDate) { + query.and.push({ + date: { + less_than: toDate.toISOString(), + } + }) + } + if (parishId) { query.and.push({ "parish": { @@ -46,7 +66,9 @@ export async function fetchEvents(parishId: string | undefined, groupId?: string title: true, cancelled: true }, - depth: 1 + depth: 1, + limit, + page }, { addQueryPrefix: true }, ) diff --git a/src/fetch/highlights.ts b/src/fetch/highlights.ts index 3bf04c8..53f10b6 100644 --- a/src/fetch/highlights.ts +++ b/src/fetch/highlights.ts @@ -28,6 +28,36 @@ export const fetchHighlights = async (): Promise<PaginatedDocs<Highlight> | unde { 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}`) if (!response.ok) return undefined return response.json() diff --git a/src/fetch/worship.ts b/src/fetch/worship.ts index ede9a0d..2b131aa 100644 --- a/src/fetch/worship.ts +++ b/src/fetch/worship.ts @@ -2,9 +2,22 @@ import { stringify } from 'qs-esm' import { PaginatedDocs } from 'payload' import { Worship } from '@/payload-types' -export const fetchWorship = async (locations?: string[]): Promise<PaginatedDocs<Worship> | undefined> => { - const date = new Date(); - date.setHours(0, 0, 0, 0); +type FetchWorshipArgs = { + fromDate?: Date, + 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 = { 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 ) { query.and.push({ location: { diff --git a/src/pages/_error.tsx b/src/pages/_error.tsx new file mode 100644 index 0000000..a50a4e8 --- /dev/null +++ b/src/pages/_error.tsx @@ -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> + + </> + ) +} \ No newline at end of file diff --git a/src/utils/dto/events.ts b/src/utils/dto/events.ts index 2fe3e66..7c79792 100644 --- a/src/utils/dto/events.ts +++ b/src/utils/dto/events.ts @@ -1,7 +1,15 @@ 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 { id: e.id, diff --git a/src/utils/dto/worship.ts b/src/utils/dto/worship.ts index 6a8eda0..af6f1f2 100644 --- a/src/utils/dto/worship.ts +++ b/src/utils/dto/worship.ts @@ -17,7 +17,7 @@ export const transformCategory = (category: "MASS" | "FAMILY" | "WORD"): string /** * 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 { diff --git a/src/utils/week.ts b/src/utils/week.ts new file mode 100644 index 0000000..ac8a520 --- /dev/null +++ b/src/utils/week.ts @@ -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}` +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index b28dedc..c80ea98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7652,6 +7652,7 @@ __metadata: eslint-plugin-storybook: "npm:^0.8.0" graphql: "npm:^16.8.1" mapbox-gl: "npm:^3.5.2" + moment: "npm:^2.30.1" next: "npm:15.0.0" payload: "npm:^3.3.0" qs-esm: "npm:^7.0.2" @@ -12175,6 +12176,13 @@ __metadata: languageName: node 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": version: 2.0.0 resolution: "ms@npm:2.0.0"