feature: add worship per week

This commit is contained in:
Benno Tielen 2025-03-11 12:55:30 +01:00
parent 3faa2a2672
commit e75b6609c3
14 changed files with 393 additions and 3 deletions

View file

@ -0,0 +1,40 @@
'use client'
import { useEffect, useState } from 'react'
import moment from 'moment'
import Day from '@/admin/components/calendar/Day'
const Calendar = () => {
const weekNr = moment().isoWeek();
const [week, setWeek] = useState(weekNr.toString())
const [days, setDays] = useState<string[]>([])
// on week change => sey days
useEffect(() => {
const date = moment().isoWeek(Number(week));
const newDays = [];
for(let i=0; i<7; i++) {
let day = date.weekday(i).toISOString();
newDays.push(day)
}
setDays(newDays)
}, [week, setDays])
return (
<div>
<div>
<select value={week} onChange={e => setWeek(e.currentTarget.value)}>
{[...Array(51).keys()].map(i => <option key={i} value={i.toString()}>Woche {i}</option>)}
</select>
</div>
{days.map(day => <Day key={day} date={day} mass={[]} />)}
</div>
)
}
export default Calendar

View file

@ -0,0 +1,22 @@
import { useSyncExternalStore } from 'react'
import { churchStore } from '@/admin/components/calendar/ChurchSelect/churchStore'
type ChurchSelectProps = {
value: string,
className?: string,
onChange: (value: string) => void,
}
export const ChurchSelect = ({value, onChange, className}: ChurchSelectProps) => {
const churches = useSyncExternalStore(churchStore.subscribe, churchStore.getSnapshot)
return (
<select
className={className}
value={value}
onChange={e => onChange(e.target.value)}
>
{churches.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
</select>
)
}

View file

@ -0,0 +1,42 @@
import { fetchChurches } from '@/fetch/churches'
type Church = {
id: string,
name: string
}
let churches: Church[] = [];
const listeners = new Set<() => void>();
/**
* ChurchStore to use with Reacts `useSyncExternalStore`
*/
export const churchStore = {
// fetch all churches from API
async init() {
const data = await fetchChurches();
if (data) {
churches = data.docs.map(c => {
return { id: c.id, name: c.name }
});
emitChange()
}
},
subscribe: (listener: () => void) => {
listeners.add(listener);
return () => listeners.delete(listener);
},
getSnapshot() {
return churches;
}
}
function emitChange() {
for (let listener of listeners) {
listener();
}
}
await churchStore.init();

View file

@ -0,0 +1,36 @@
import { liturgicalDayName } from '@/hooks/liturgicalDayName'
import styles from "./styles.module.scss"
import MassForm from '@/admin/components/calendar/MassForm'
type DayProps = {
date: string,
mass: Mass[]
}
type Mass = {
id: string,
}
export const Day = ({date}: DayProps) => {
const dayName = liturgicalDayName(date)
const dateObj = new Date(date);
return (
<div className={styles.container}>
<div className={styles.day}>
<strong>{dateObj.toLocaleDateString("de-De", {weekday: "long"})}</strong> <br/>
{dayName}
</div>
<div className={styles.date}>
{date.substring(8, 10)}
</div>
<div>
<MassForm />
</div>
</div>
)
}
export default Day

View file

@ -0,0 +1,54 @@
import styles from "./form.module.scss"
import { ChurchSelect } from '@/admin/components/calendar/ChurchSelect/ChurchSelect'
const MassForm = () => {
return (
<div className={styles.container}>
<div className={styles.time}>
<div className={styles.formRow}>
<select name="time" className={styles.select}>
<option>10:00 Uhr</option>
<option>20:00 Uhr</option>
</select>
</div>
<div>
<ChurchSelect
className={styles.select}
value={"23"}
onChange={() => console.log("chage")}
/>
</div>
</div>
<div>
<div className={styles.formRow}>
<input type="radio" id="heilige_messe" name="massType" value="Heilige Messe" required />
<label htmlFor="heilige_messe">Heilige Messe</label>
<input type="radio" id="wort_gottes_feier" name="massType" value="Wort-Gottes-Feier" required />
<label htmlFor="wort_gottes_feier">Wort-Gottes-Feier</label>
<input type="radio" id="familienmesse" name="massType" value="Familienmesse" required />
<label htmlFor="familienmesse">Familienmesse</label>
</div>
<div className={styles.formRow}>
<input type="text" id="title" name="title" placeholder={"Titel"} className={styles.input} required />
</div>
<div className={styles.formRow}>
<input type="text" id="celebrant" name="celebrant" placeholder={"Zelebrant"} className={styles.input}/>
</div>
<div className={styles.formRow}>
<textarea id="description" name="description" placeholder={"Hinweise"} className={styles.input} rows={4}></textarea>
</div>
</div>
<div>
<button>Entfernen</button>
</div>
</div>
)
}
export default MassForm

View file

@ -0,0 +1,30 @@
.container {
display: flex;
}
.time {
width: 130px;
text-align: center;
}
.formRow {
margin-bottom: 5px;
}
.input {
font-size: 12px;
padding: 3px;
width: 350px;
border: 1px solid #ababab;
border-radius: 4px;
}
.select {
font-size: 12px;
padding: 3px;
border: 1px solid #ababab;
border-radius: 4px;
width: 100px;
background-color: #ffffff;
font-family: inherit;
}

View file

@ -0,0 +1,13 @@
.container {
display: flex;
margin-bottom: 20px;
}
.day {
width: 300px;
}
.date {
font-size: 18px;
font-weight: bold;
}

View file

@ -0,0 +1,40 @@
import { Gutter } from '@payloadcms/ui'
import { DefaultTemplate } from '@payloadcms/next/templates'
import { AdminViewServerProps } from 'payload'
import Calendar from '@/admin/components/calendar/Calendar'
export default function TestPage({
initPageResult,
params,
searchParams,
}: AdminViewServerProps) {
const {
req: {
user
}
} = initPageResult
if (!user) {
return <p>You must be logged in to view this page.</p>
}
return (
<DefaultTemplate
i18n={initPageResult.req.i18n}
locale={initPageResult.locale}
params={params}
payload={initPageResult.req.payload}
permissions={initPageResult.permissions}
searchParams={searchParams}
user={initPageResult.req.user || undefined}
visibleEntities={initPageResult.visibleEntities}
>
<Gutter>
<h1>Gottesdiensten</h1>
<p>This view uses the Default Template.</p>
<Calendar />
</Gutter>
</DefaultTemplate>
)
}

View file

@ -9,6 +9,8 @@ import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1e
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { default as default_9bcae99938dc292be0063ce32055e14c } from 'src/components/Logo/Logo'
import { default as default_da280595b8aff80d788c3701c6162e87 } from 'src/admin/components/test/TestView'
export const importMap = {
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
@ -21,5 +23,7 @@ export const importMap = {
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"/components/Logo/Logo#default": default_9bcae99938dc292be0063ce32055e14c,
"/admin/components/test/TestView#default": default_da280595b8aff80d788c3701c6162e87
}

View file

@ -42,7 +42,12 @@ export const Highlight: CollectionConfig = {
relationTo: ['event', 'blog', 'worship'],
admin: {
allowCreate: false,
allowEdit: false
allowEdit: false,
sortOptions: {
"event": "-createdAt",
"blog": "-createdAt",
"worship": "-date",
}
},
required: false
},

View file

@ -421,4 +421,6 @@ export const Logo = ({withText = false, color = "#000000", height = 75, textColo
</g>
</svg>
)
}
}
export default Logo

17
src/fetch/churches.ts Normal file
View file

@ -0,0 +1,17 @@
import { Church } from '@/payload-types'
import { PaginatedDocs } from 'payload'
import { stringify } from 'qs-esm'
export const fetchChurches = async (): Promise<PaginatedDocs<Church> | undefined> => {
const stringifiedQuery = stringify(
{
sort: "name",
limit: 50
},
{ addQueryPrefix: true },
)
const resp = await fetch(`http://localhost:3000/api/church`);
if (!resp.ok) return undefined;
return resp.json();
}

View file

@ -6,10 +6,65 @@
* and re-run `payload generate:types` to regenerate this file.
*/
/**
* Supported timezones in IANA format.
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "supportedTimezones".
*/
export type SupportedTimezones =
| 'Pacific/Midway'
| 'Pacific/Niue'
| 'Pacific/Honolulu'
| 'Pacific/Rarotonga'
| 'America/Anchorage'
| 'Pacific/Gambier'
| 'America/Los_Angeles'
| 'America/Tijuana'
| 'America/Denver'
| 'America/Phoenix'
| 'America/Chicago'
| 'America/Guatemala'
| 'America/New_York'
| 'America/Bogota'
| 'America/Caracas'
| 'America/Santiago'
| 'America/Buenos_Aires'
| 'America/Sao_Paulo'
| 'Atlantic/South_Georgia'
| 'Atlantic/Azores'
| 'Atlantic/Cape_Verde'
| 'Europe/London'
| 'Europe/Berlin'
| 'Africa/Lagos'
| 'Europe/Athens'
| 'Africa/Cairo'
| 'Europe/Moscow'
| 'Asia/Riyadh'
| 'Asia/Dubai'
| 'Asia/Baku'
| 'Asia/Karachi'
| 'Asia/Tashkent'
| 'Asia/Calcutta'
| 'Asia/Dhaka'
| 'Asia/Almaty'
| 'Asia/Jakarta'
| 'Asia/Bangkok'
| 'Asia/Shanghai'
| 'Asia/Singapore'
| 'Asia/Tokyo'
| 'Asia/Seoul'
| 'Australia/Sydney'
| 'Pacific/Guam'
| 'Pacific/Noumea'
| 'Pacific/Auckland'
| 'Pacific/Fiji';
export interface Config {
auth: {
users: UserAuthOperations;
};
blocks: {};
collections: {
parish: Parish;
church: Church;

View file

@ -42,6 +42,36 @@ const dirname = path.dirname(filename)
export default buildConfig({
admin: {
user: Users.slug,
meta: {
title: 'Dashboard',
titleSuffix: '- Heilige Drei Könige'
},
importMap: {
baseDir: dirname,
},
components: {
views: {
testCustomView: {
Component: '/admin/components/test/TestView',
path: '/test'
},
},
graphics: {
Logo: {
path: '/components/Logo/Logo',
serverProps: {
withText: true,
height: 90
}
},
Icon: {
path: '/components/Logo/Logo',
serverProps: {
height: 18
}
}
}
}
},
collections: [
Parish,