feat: announcements and calendar on homepage
This commit is contained in:
parent
6dd507fd56
commit
f210842a4a
10 changed files with 368 additions and 5 deletions
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
42
src/components/PopupButton/PopupButton.stories.tsx
Normal file
42
src/components/PopupButton/PopupButton.stories.tsx
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
72
src/components/PopupButton/PopupButton.tsx
Normal file
72
src/components/PopupButton/PopupButton.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
63
src/components/PopupButton/styles.module.scss
Normal file
63
src/components/PopupButton/styles.module.scss
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -30,7 +30,6 @@ export const fetchLastAnnouncement = async (parishId: string): Promise<Announcem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const stringifiedQuery = stringify(
|
const stringifiedQuery = stringify(
|
||||||
|
|
@ -47,3 +46,42 @@ export const fetchLastAnnouncement = async (parishId: string): Promise<Announcem
|
||||||
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>
|
||||||
|
}
|
||||||
|
|
@ -47,3 +47,43 @@ export const fetchLastCalendar = async (parishId: string): Promise<Calendar | u
|
||||||
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>
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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"}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
.center {
|
.center {
|
||||||
text-align: center;
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
38
src/utils/dto/perParish.ts
Normal file
38
src/utils/dto/perParish.ts
Normal 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
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue