feat: announcements and calendar on homepage

This commit is contained in:
Benno Tielen 2025-12-16 12:29:38 +01:00
parent 6dd507fd56
commit f210842a4a
10 changed files with 368 additions and 5 deletions

View file

@ -4,6 +4,9 @@ import { fetchBlogPosts } from '@/fetch/blog'
import { fetchHighlights } from '@/fetch/highlights' import { fetchHighlights } from '@/fetch/highlights'
import { Home } from '@/pageComponents/Home/Home' import { Home } from '@/pageComponents/Home/Home'
import moment from 'moment' import moment from 'moment'
import { fetchLastAnnouncement, fetchLastAnnouncements } from '@/fetch/announcement'
import { perParish } from '@/utils/dto/perParish'
import { fetchLastCalendars } from '@/fetch/calendar'
export const dynamic = 'force-dynamic' export const dynamic = 'force-dynamic'
@ -18,6 +21,10 @@ export default async function HomePage() {
}); });
const blog = await fetchBlogPosts(true) const blog = await fetchBlogPosts(true)
const highlights = await fetchHighlights() const highlights = await fetchHighlights()
const announcements = await fetchLastAnnouncements();
const announcementsLinks = announcements ? perParish(announcements) : [];
const calendars = await fetchLastCalendars();
const calendarsLinks = calendars ? perParish(calendars) : [];
return ( return (
@ -26,6 +33,8 @@ export default async function HomePage() {
worship={worship?.docs || []} worship={worship?.docs || []}
blog={blog?.docs || []} blog={blog?.docs || []}
highlights={highlights?.docs || []} highlights={highlights?.docs || []}
announcements={announcementsLinks}
calendars={calendarsLinks}
/> />
) )
} }

View file

@ -0,0 +1,42 @@
import { Meta, StoryObj } from '@storybook/react'
import { PopupButton } from './PopupButton'
const meta: Meta<typeof PopupButton> = {
component: PopupButton,
decorators: [
(Story) => (
<div style={{display: 'flex', alignItems: 'center', height: '100vh'}}>
<div style={{margin: "auto"}}>
<Story />
</div>
</div>
)
]
}
type Story = StoryObj<typeof PopupButton>;
export default meta
export const Default: Story = {
args: {
text: "Vermeldungen",
title: "Vermeldungen",
links: [
{
id: "link_1",
text: "St. Clara",
href: "https://disney.com"
},
{
id: "link_2",
text: "St. Anna",
href: "https://disney.com"
},
{
id: "link_3",
text: "St. Eduard",
href: "https://disney.com"
}
]
},
}

View file

@ -0,0 +1,72 @@
"use client"
import { useEffect, useState } from 'react'
import { Button } from '@/components/Button/Button'
import styles from "./styles.module.scss"
type PopupButtonProps = {
text: React.ReactNode,
title: string,
links: Link[],
size?: 'lg' | 'md'
schema?: 'base' | 'shade' | 'contrast'
}
export type Link = {
id: string,
text: string,
href: string
}
export const PopupButton = ({size = "md", schema, text, links, title}: PopupButtonProps) => {
const [isPopupOpen, setIsPopupOpen] = useState(false)
useEffect(() => {
if (!isPopupOpen) return
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
setIsPopupOpen(false)
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [isPopupOpen])
return (
<div className={styles.container}>
<div className={styles.button}>
<Button size={size} schema={schema} onClick={() => setIsPopupOpen(true)}>
{text}
</Button>
</div>
{isPopupOpen && (
<div className={styles.popup}>
<button
type="button"
aria-label="Close"
className={styles.closeButton}
onClick={() => setIsPopupOpen(false)}
>
×
</button>
<div className={styles.popupTitle}>{title}</div>
{links.map(link => (
<div key={link.id}>
<a
href={link.href}
onClick={() => setIsPopupOpen(false)}
className={styles.link}
target={"_blank"}>
{link.text}</a>
</div>
))}
</div>
)}
</div>
)
}

View file

@ -0,0 +1,63 @@
.container {
display: inline-grid;
place-items: center;
grid-template-areas: "a";
grid-template-rows: fit-content(0);
}
.button {
grid-area: a;
}
.popup {
grid-area: a;
background-color: white;
box-shadow: 0 0 21px 0 rgba(0,0,0,0.4);
border-radius: 7px;
min-width: 200px;
position: relative;
overflow: auto;
}
.popupTitle {
font-weight: bold;
text-align: center;
padding: 10px 0;
border-bottom: 1px solid #e1e1e1;
}
.link {
color: inherit;
text-decoration: none;
padding: 5px 15px;
display: block;
font-size: 17px;
transition: background-color 0.2s ease-in;
}
.link:hover {
background-color: #e1e1e1;
}
.closeButton {
position: absolute;
top: 10px;
right: 5px;
border: none;
background: white;
color: inherit;
cursor: pointer;
font-size: 16px;
font-weight: bold;
line-height: 1;
padding: 4px;
width: 30px;
height: 30px;
border-radius: 15px;
}
.closeButton:hover,
.closeButton:focus-visible {
background-color: #f1f1f1;
outline: none;
}

View file

@ -30,7 +30,6 @@ export const fetchLastAnnouncement = async (parishId: string): Promise<Announcem
} }
} }
] ]
} }
const stringifiedQuery = stringify( const stringifiedQuery = stringify(
@ -46,4 +45,43 @@ export const fetchLastAnnouncement = async (parishId: string): Promise<Announcem
if (!response.ok) return undefined if (!response.ok) return undefined
const announcements = await response.json() as PaginatedDocs<Announcement> const announcements = await response.json() as PaginatedDocs<Announcement>
return announcements.docs[0] return announcements.docs[0]
}
/**
* Fetch the last few announcements
*/
export const fetchLastAnnouncements = async (): Promise<PaginatedDocs<Announcement> | undefined> => {
const date = new Date();
date.setDate(date.getDate() - 14)
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(23,59,59,59);
const query: any = {
and: [
{
date: {
greater_than_equal: date.toISOString(),
}
},
{
date: {
less_than_equal: tomorrow.toISOString()
}
}
]
}
const stringifiedQuery = stringify(
{
sort: "-date",
where: query,
limit: 3,
},
{ addQueryPrefix: true },
)
const response = await fetch(`http://localhost:3000/api/announcement${stringifiedQuery}`)
if (!response.ok) return undefined
return await response.json() as PaginatedDocs<Announcement>
} }

View file

@ -46,4 +46,44 @@ export const fetchLastCalendar = async (parishId: string): Promise<Calendar | u
if (!response.ok) return undefined if (!response.ok) return undefined
const announcements = await response.json() as PaginatedDocs<Calendar> const announcements = await response.json() as PaginatedDocs<Calendar>
return announcements.docs[0] return announcements.docs[0]
}
/**
* Fetch last calendars
*/
export const fetchLastCalendars = async (): Promise<PaginatedDocs<Calendar> | undefined> => {
const date = new Date();
date.setDate(date.getDate() - 14);
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(23,59,59,59);
const query: any = {
and: [
{
date: {
greater_than_equal: date.toISOString(),
}
},
{
date: {
less_than_equal: tomorrow.toISOString()
}
}
]
}
const stringifiedQuery = stringify(
{
sort: "-date",
where: query,
limit: 3,
},
{ addQueryPrefix: true },
)
const response = await fetch(`http://localhost:3000/api/calendar${stringifiedQuery}`)
if (!response.ok) return undefined
return await response.json() as PaginatedDocs<Calendar>
} }

View file

@ -137,5 +137,39 @@ export const Default: Story = {
}, },
], ],
highlights: [], highlights: [],
}, announcements: [
{
id: "link_1",
text: "St. Clara",
href: "https://disney.com"
},
{
id: "link_2",
text: "St. Anna",
href: "https://disney.com"
},
{
id: "link_3",
text: "St. Eduard",
href: "https://disney.com"
}
],
calendars: [
{
id: "link_1",
text: "St. Clara",
href: "https://disney.com"
},
{
id: "link_2",
text: "St. Anna",
href: "https://disney.com"
},
{
id: "link_3",
text: "St. Eduard",
href: "https://disney.com"
}
],
}
} }

View file

@ -21,12 +21,15 @@ import { MoreInformation } from '@/pageComponents/Home/MoreInformation'
import { Button } from '@/components/Button/Button' import { Button } from '@/components/Button/Button'
import styles from "./styles.module.scss" import styles from "./styles.module.scss"
import { PublicationAndNewsletter } from '@/compositions/PublicationAndNewsletter/PublicationAndNewsletter' import { PublicationAndNewsletter } from '@/compositions/PublicationAndNewsletter/PublicationAndNewsletter'
import { Link, PopupButton } from '@/components/PopupButton/PopupButton'
type HomeProps = { type HomeProps = {
events: Event[], events: Event[],
worship: Worship[], worship: Worship[],
blog: Blog[], blog: Blog[],
highlights: Highlight[], highlights: Highlight[],
announcements: Link[],
calendars: Link[]
} }
const sortWorship = (worship: Worship[]) => { const sortWorship = (worship: Worship[]) => {
@ -52,9 +55,11 @@ export const Home = ({
events, events,
worship, worship,
blog, blog,
highlights highlights,
announcements,
calendars
}: HomeProps) => { }: HomeProps) => {
const worshipPerLocation = Array.from( const worshipPerLocation = Array.from(
sortWorship(worship).entries(), sortWorship(worship).entries(),
).sort( ).sort(
(a, b) => { (a, b) => {
@ -111,6 +116,25 @@ export const Home = ({
<Section padding={'small'}> <Section padding={'small'}>
<div className={styles.center}> <div className={styles.center}>
{ announcements.length > 0 &&
<PopupButton
text={"Vermeldungen"}
title={"Vermeldungen"}
links={announcements}
schema={"shade"}
/>
}
{ calendars.length > 0 &&
<PopupButton
text={"Liturgischer Kalender"}
title={"Kalender"}
links={calendars}
schema={"shade"}
/>
}
<Button <Button
href={"/gottesdienst"} href={"/gottesdienst"}
size={"md"} size={"md"}

View file

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

View file

@ -0,0 +1,38 @@
import { PaginatedDocs } from 'payload'
import { Announcement, Calendar } from '@/payload-types'
type AnnouncementLink = {
id: string,
text: string,
href: string
}
export const perParish = (data: PaginatedDocs<Announcement | Calendar>) => {
let links: AnnouncementLink[] = []
let set = new Set();
for (const announcement of data.docs) {
for (const parish of announcement.parish) {
if (typeof announcement.document === "string")
continue;
if (typeof parish === 'string') {
continue;
}
if (set.has(parish.id)) {
continue;
}
set.add(parish.id)
links.push({
id: parish.id,
text: parish.name,
href: announcement.document.url || "undefined"
})
}
}
return links
}