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 { Home } from '@/pageComponents/Home/Home'
|
||||
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'
|
||||
|
||||
|
|
@ -18,6 +21,10 @@ export default async function HomePage() {
|
|||
});
|
||||
const blog = await fetchBlogPosts(true)
|
||||
const highlights = await fetchHighlights()
|
||||
const announcements = await fetchLastAnnouncements();
|
||||
const announcementsLinks = announcements ? perParish(announcements) : [];
|
||||
const calendars = await fetchLastCalendars();
|
||||
const calendarsLinks = calendars ? perParish(calendars) : [];
|
||||
|
||||
|
||||
return (
|
||||
|
|
@ -26,6 +33,8 @@ export default async function HomePage() {
|
|||
worship={worship?.docs || []}
|
||||
blog={blog?.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(
|
||||
|
|
@ -47,3 +46,42 @@ export const fetchLastAnnouncement = async (parishId: string): Promise<Announcem
|
|||
const announcements = await response.json() as PaginatedDocs<Announcement>
|
||||
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>
|
||||
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: [],
|
||||
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 styles from "./styles.module.scss"
|
||||
import { PublicationAndNewsletter } from '@/compositions/PublicationAndNewsletter/PublicationAndNewsletter'
|
||||
import { Link, PopupButton } from '@/components/PopupButton/PopupButton'
|
||||
|
||||
type HomeProps = {
|
||||
events: Event[],
|
||||
worship: Worship[],
|
||||
blog: Blog[],
|
||||
highlights: Highlight[],
|
||||
announcements: Link[],
|
||||
calendars: Link[]
|
||||
}
|
||||
|
||||
const sortWorship = (worship: Worship[]) => {
|
||||
|
|
@ -52,7 +55,9 @@ export const Home = ({
|
|||
events,
|
||||
worship,
|
||||
blog,
|
||||
highlights
|
||||
highlights,
|
||||
announcements,
|
||||
calendars
|
||||
}: HomeProps) => {
|
||||
const worshipPerLocation = Array.from(
|
||||
sortWorship(worship).entries(),
|
||||
|
|
@ -111,6 +116,25 @@ export const Home = ({
|
|||
|
||||
<Section padding={'small'}>
|
||||
<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
|
||||
href={"/gottesdienst"}
|
||||
size={"md"}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
.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