feat: customizable footer
This commit is contained in:
parent
ac01546440
commit
0240f2df4f
10 changed files with 323 additions and 57 deletions
38
src/collections/Prayers.ts
Normal file
38
src/collections/Prayers.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { CollectionConfig } from 'payload'
|
||||
import { revalidateTag } from 'next/cache'
|
||||
import { isAdminOrEmployee } from '@/collections/access/admin'
|
||||
|
||||
export const Prayers: CollectionConfig = {
|
||||
slug: 'prayers',
|
||||
labels: {
|
||||
singular: {
|
||||
de: 'Stoßgebet',
|
||||
},
|
||||
plural: {
|
||||
de: 'Stoßgebete',
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
required: true,
|
||||
label: {
|
||||
de: 'Gebet',
|
||||
},
|
||||
},
|
||||
],
|
||||
admin: {
|
||||
useAsTitle: 'text',
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
create: isAdminOrEmployee(),
|
||||
update: isAdminOrEmployee(),
|
||||
delete: isAdminOrEmployee(),
|
||||
},
|
||||
hooks: {
|
||||
afterChange: [() => revalidateTag('prayers')],
|
||||
afterDelete: [() => revalidateTag('prayers')],
|
||||
},
|
||||
}
|
||||
|
|
@ -1,37 +1,31 @@
|
|||
"use client"
|
||||
'use client'
|
||||
|
||||
import { randomPrayer } from '@/utils/randomPrayer'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
export const RandomPrayer = () => {
|
||||
const [intervalId, setIntervalId] = useState<NodeJS.Timeout | undefined>(undefined)
|
||||
const [prayer, setPrayer] = useState("")
|
||||
type RandomPrayerProps = {
|
||||
prayers: string[]
|
||||
}
|
||||
|
||||
// Set interval to change the prayer
|
||||
const newPrayerEveryInterval = useCallback(() => {
|
||||
const i = setInterval(() => {
|
||||
setPrayer(randomPrayer())
|
||||
}, 60 * 1000)
|
||||
const pickRandom = (prayers: string[]) =>
|
||||
prayers[Math.floor(Math.random() * prayers.length)] || ''
|
||||
|
||||
setIntervalId(i)
|
||||
}, [setPrayer, setIntervalId])
|
||||
export const RandomPrayer = ({ prayers }: RandomPrayerProps) => {
|
||||
const [prayer, setPrayer] = useState('')
|
||||
|
||||
// Set new random prayer and reset timer interval
|
||||
const newPrayer = useCallback(() => {
|
||||
clearInterval(intervalId)
|
||||
setPrayer(randomPrayer())
|
||||
newPrayerEveryInterval()
|
||||
}, [intervalId, setPrayer, newPrayerEveryInterval])
|
||||
setPrayer(pickRandom(prayers))
|
||||
}, [prayers])
|
||||
|
||||
// Every 30 seconds set a new prayer
|
||||
useEffect(() => {
|
||||
setPrayer(randomPrayer())
|
||||
newPrayerEveryInterval()
|
||||
return () => clearInterval(intervalId)
|
||||
}, [newPrayerEveryInterval])
|
||||
setPrayer(pickRandom(prayers))
|
||||
const interval = setInterval(() => {
|
||||
setPrayer(pickRandom(prayers))
|
||||
}, 60 * 1000)
|
||||
return () => clearInterval(interval)
|
||||
}, [prayers])
|
||||
|
||||
return (
|
||||
<p onClick={newPrayer} style={{cursor: "pointer"}}>
|
||||
<p onClick={newPrayer} style={{ cursor: 'pointer' }}>
|
||||
{prayer}
|
||||
</p>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,20 @@
|
|||
import { Section } from '@/components/Section/Section'
|
||||
import { Container } from '@/components/Container/Container'
|
||||
import { Logo } from '@/components/Logo/Logo'
|
||||
import styles from "./styles.module.scss"
|
||||
import styles from './styles.module.scss'
|
||||
import { Row } from '@/components/Flex/Row'
|
||||
import { Col } from '@/components/Flex/Col'
|
||||
import { RandomPrayer } from '@/components/RandomPrayer/RandomPrayer'
|
||||
import Link from 'next/link'
|
||||
import { fetchPrayers } from '@/fetch/prayers'
|
||||
import { fetchFooter } from '@/fetch/footer'
|
||||
|
||||
export const Footer = async () => {
|
||||
const [prayers, footer] = await Promise.all([
|
||||
fetchPrayers(),
|
||||
fetchFooter(),
|
||||
])
|
||||
|
||||
export const Footer = () => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Section backgroundColor="soft">
|
||||
|
|
@ -17,8 +23,8 @@ export const Footer = () => {
|
|||
<Col>
|
||||
<br />
|
||||
<Logo
|
||||
color={"#ffffff"}
|
||||
textColor={"#426156"}
|
||||
color={'#ffffff'}
|
||||
textColor={'#426156'}
|
||||
withText={true}
|
||||
height={100}
|
||||
/>
|
||||
|
|
@ -26,34 +32,25 @@ export const Footer = () => {
|
|||
|
||||
<Col>
|
||||
<Row gap={30}>
|
||||
<Col>
|
||||
<strong><p>Gemeinden</p></strong>
|
||||
<ul className={styles.list}>
|
||||
<li><Link href={'/gemeinde/st-christophorus'}>St. Christophorus</Link></li>
|
||||
<li><Link href={'/gemeinde/st-clara'}>St. Clara</Link></li>
|
||||
<li><Link href={'/gemeinde/st-richard'}>St. Richard</Link></li>
|
||||
</ul>
|
||||
</Col>
|
||||
<Col>
|
||||
{footer.groups?.map((group, i) => (
|
||||
<Col key={i}>
|
||||
<p>
|
||||
<strong>Navigation</strong>
|
||||
<strong>{group.title}</strong>
|
||||
</p>
|
||||
<ul className={styles.list}>
|
||||
<li><Link href={'/kontakt'}>Kontakt</Link></li>
|
||||
<li><Link href={'/gottesdienst'}>Gottesdienste</Link></li>
|
||||
<li><Link href={'/veranstaltungen'}>Veranstaltungen</Link></li>
|
||||
<li><Link href={'/mithelfen'}>Mithelfen</Link></li>
|
||||
<li><Link href={'/datenschutz'}>Datenschutz</Link></li>
|
||||
<li><Link href={'/schutzkonzept'}>Schutzkonzept</Link></li>
|
||||
<li><Link href={'/hinweisgeber'}>Hinweisgeber</Link></li>
|
||||
<li><Link href={'/impressum'}>Impressum</Link></li>
|
||||
{group.links?.map((link, j) => (
|
||||
<li key={j}>
|
||||
<Link href={link.href}>{link.label}</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Col>
|
||||
))}
|
||||
<Col>
|
||||
<p>
|
||||
<strong>Stoßgebet</strong>
|
||||
</p>
|
||||
<RandomPrayer />
|
||||
<RandomPrayer prayers={prayers} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
|
|
@ -61,5 +58,5 @@ export const Footer = () => {
|
|||
</Container>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
13
src/fetch/footer.ts
Normal file
13
src/fetch/footer.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { Footer } from '@/payload-types'
|
||||
|
||||
export async function fetchFooter(): Promise<Footer> {
|
||||
const res = await fetch('http://localhost:3000/api/globals/footer', {
|
||||
next: { tags: ['footer'] },
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Could not fetch footer')
|
||||
}
|
||||
|
||||
return res.json()
|
||||
}
|
||||
11
src/fetch/prayers.ts
Normal file
11
src/fetch/prayers.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { Prayer } from '@/payload-types'
|
||||
|
||||
export async function fetchPrayers(): Promise<string[]> {
|
||||
const res = await fetch(
|
||||
'http://localhost:3000/api/prayers?limit=0',
|
||||
{ next: { tags: ['prayers'] } },
|
||||
)
|
||||
if (!res.ok) return []
|
||||
const data = await res.json()
|
||||
return (data.docs as Prayer[]).map((p) => p.text)
|
||||
}
|
||||
65
src/globals/Footer.ts
Normal file
65
src/globals/Footer.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { GlobalConfig } from 'payload'
|
||||
import { isAdmin } from '@/collections/access/admin'
|
||||
import { revalidateTag } from 'next/cache'
|
||||
|
||||
export const FooterGlobal: GlobalConfig = {
|
||||
slug: 'footer',
|
||||
label: {
|
||||
de: 'Fußzeile',
|
||||
},
|
||||
admin: {
|
||||
description:
|
||||
'Hier können Sie die Linkgruppen im Fußzeile konfigurieren.',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'groups',
|
||||
label: {
|
||||
de: 'Linkgruppen',
|
||||
},
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
label: {
|
||||
de: 'Titel',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'links',
|
||||
type: 'array',
|
||||
label: {
|
||||
de: 'Links',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'label',
|
||||
type: 'text',
|
||||
required: true,
|
||||
label: {
|
||||
de: 'Bezeichnung',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'href',
|
||||
type: 'text',
|
||||
required: true,
|
||||
label: {
|
||||
de: 'Zieladresse',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
access: {
|
||||
read: () => true,
|
||||
update: isAdmin(),
|
||||
},
|
||||
hooks: {
|
||||
afterChange: [() => revalidateTag('footer')],
|
||||
},
|
||||
}
|
||||
65
src/migrations/20260306_100000_prayers.ts
Normal file
65
src/migrations/20260306_100000_prayers.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
|
||||
|
||||
const PRAYERS = [
|
||||
'Herr, erbarme Dich meiner.',
|
||||
'Herr Jesus Christus, erbarme dich meiner.',
|
||||
'Ehre sei dem Vater, und dem Sohn und dem Heiligen Geist.',
|
||||
'Preiset den Herrn zu aller Zeit, denn er ist gut.',
|
||||
'Mein Herr und mein Gott.',
|
||||
'Herr, dir in die Hände sei Anfang und Ende, sei alles gelegt.',
|
||||
'Herr, du weißt alles; du weißt, dass ich dich liebe.',
|
||||
'Der Herr ist mein Licht und mein Heil, vor wem sollte ich mich fürchten?',
|
||||
'Herr, dein Wille geschehe.',
|
||||
'Ich glaube, Herr; hilf meinem Unglauben.',
|
||||
'O Gott, komm mir zu Hilfe. Herr, eile, mir zu helfen.',
|
||||
'Jesus, ich vertraue auf Dich.',
|
||||
'Gegrüßet seist du, Maria, voll der Gnade, der Herr ist mit dir.',
|
||||
'Gelobt sei Jesus Christus - in Ewigkeit. Amen.',
|
||||
'Maria mit dem Kinde lieb, uns allen deinen Segen gib.',
|
||||
'Aus der Tiefe rufe ich Herr zu dir. Herr, höre meine Stimme.',
|
||||
'Durch sein schmerzhaftes Leiden, habe Erbarmen mit uns und mit der ganzen Welt.',
|
||||
'Heiliger Gott, habe Erbarmen mit uns und mit der ganzen Welt.',
|
||||
'Heilige Maria, Mutter Gottes, bitte für uns Sünder.',
|
||||
'Gepriesen seist du, Herr. Lehre mich deine Gesetze.',
|
||||
]
|
||||
|
||||
export async function up({ db }: MigrateUpArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
CREATE TABLE IF NOT EXISTS "prayers" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"text" varchar NOT NULL,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "prayers_updated_at_idx" ON "prayers" USING btree ("updated_at");
|
||||
CREATE INDEX IF NOT EXISTS "prayers_created_at_idx" ON "prayers" USING btree ("created_at");
|
||||
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD COLUMN IF NOT EXISTS "prayers_id" uuid;
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_prayers_fk" FOREIGN KEY ("prayers_id") REFERENCES "public"."prayers"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_prayers_id_idx" ON "payload_locked_documents_rels" USING btree ("prayers_id");
|
||||
`)
|
||||
|
||||
for (const text of PRAYERS) {
|
||||
await db.execute(sql`
|
||||
INSERT INTO "prayers" ("id", "text", "updated_at", "created_at")
|
||||
VALUES (gen_random_uuid(), ${text}, now(), now())
|
||||
`)
|
||||
}
|
||||
}
|
||||
|
||||
export async function down({ db }: MigrateDownArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "prayers" DISABLE ROW LEVEL SECURITY;
|
||||
DROP TABLE IF EXISTS "prayers" CASCADE;
|
||||
ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT IF EXISTS "payload_locked_documents_rels_prayers_fk";
|
||||
DROP INDEX IF EXISTS "payload_locked_documents_rels_prayers_id_idx";
|
||||
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN IF EXISTS "prayers_id";
|
||||
`)
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import * as migration_20260106_085445_donationforms from './20260106_085445_dona
|
|||
import * as migration_20260106_103529_donation_appeal from './20260106_103529_donation_appeal';
|
||||
import * as migration_20260205_155735_version_bump from './20260205_155735_version_bump';
|
||||
import * as migration_20260305_095426 from './20260305_095426';
|
||||
import * as migration_20260306_100000_prayers from './20260306_100000_prayers';
|
||||
|
||||
export const migrations = [
|
||||
{
|
||||
|
|
@ -112,6 +113,11 @@ export const migrations = [
|
|||
{
|
||||
up: migration_20260305_095426.up,
|
||||
down: migration_20260305_095426.down,
|
||||
name: '20260305_095426'
|
||||
name: '20260305_095426',
|
||||
},
|
||||
{
|
||||
up: migration_20260306_100000_prayers.up,
|
||||
down: migration_20260306_100000_prayers.down,
|
||||
name: '20260306_100000_prayers',
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ export interface Config {
|
|||
group: Group;
|
||||
'donation-form': DonationForm;
|
||||
pages: Page;
|
||||
prayers: Prayer;
|
||||
magazine: Magazine;
|
||||
documents: Document;
|
||||
media: Media;
|
||||
|
|
@ -108,6 +109,7 @@ export interface Config {
|
|||
group: GroupSelect<false> | GroupSelect<true>;
|
||||
'donation-form': DonationFormSelect<false> | DonationFormSelect<true>;
|
||||
pages: PagesSelect<false> | PagesSelect<true>;
|
||||
prayers: PrayersSelect<false> | PrayersSelect<true>;
|
||||
magazine: MagazineSelect<false> | MagazineSelect<true>;
|
||||
documents: DocumentsSelect<false> | DocumentsSelect<true>;
|
||||
media: MediaSelect<false> | MediaSelect<true>;
|
||||
|
|
@ -123,9 +125,11 @@ export interface Config {
|
|||
fallbackLocale: null;
|
||||
globals: {
|
||||
menu: Menu;
|
||||
footer: Footer;
|
||||
};
|
||||
globalsSelect: {
|
||||
menu: MenuSelect<false> | MenuSelect<true>;
|
||||
footer: FooterSelect<false> | FooterSelect<true>;
|
||||
};
|
||||
locale: null;
|
||||
user: User & {
|
||||
|
|
@ -890,6 +894,16 @@ export interface Page {
|
|||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "prayers".
|
||||
*/
|
||||
export interface Prayer {
|
||||
id: string;
|
||||
text: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "magazine".
|
||||
|
|
@ -1013,6 +1027,10 @@ export interface PayloadLockedDocument {
|
|||
relationTo: 'pages';
|
||||
value: string | Page;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'prayers';
|
||||
value: string | Prayer;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'magazine';
|
||||
value: string | Magazine;
|
||||
|
|
@ -1613,6 +1631,15 @@ export interface PagesSelect<T extends boolean = true> {
|
|||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "prayers_select".
|
||||
*/
|
||||
export interface PrayersSelect<T extends boolean = true> {
|
||||
text?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "magazine_select".
|
||||
|
|
@ -1845,6 +1872,30 @@ export interface Menu {
|
|||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* Hier können Sie die Linkgruppen im Fußzeile konfigurieren.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "footer".
|
||||
*/
|
||||
export interface Footer {
|
||||
id: string;
|
||||
groups?:
|
||||
| {
|
||||
title: string;
|
||||
links?:
|
||||
| {
|
||||
label: string;
|
||||
href: string;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "menu_select".
|
||||
|
|
@ -1926,6 +1977,28 @@ export interface MenuSelect<T extends boolean = true> {
|
|||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "footer_select".
|
||||
*/
|
||||
export interface FooterSelect<T extends boolean = true> {
|
||||
groups?:
|
||||
| T
|
||||
| {
|
||||
title?: T;
|
||||
links?:
|
||||
| T
|
||||
| {
|
||||
label?: T;
|
||||
href?: T;
|
||||
id?: T;
|
||||
};
|
||||
id?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
|
|
|
|||
|
|
@ -37,9 +37,11 @@ import { PopesPrayerIntentions } from '@/collections/PopesPrayerIntentions'
|
|||
import { LiturgicalCalendar } from '@/collections/LiturgicalCalendar'
|
||||
import { Classifieds } from '@/collections/Classifieds'
|
||||
import { MenuGlobal } from '@/globals/Menu'
|
||||
import { FooterGlobal } from '@/globals/Footer'
|
||||
import { Magazine } from '@/collections/Magazine'
|
||||
import { DonationForms } from '@/collections/DonationForms'
|
||||
import { Pages } from '@/collections/Pages'
|
||||
import { Prayers } from '@/collections/Prayers'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
|
@ -94,13 +96,15 @@ export default buildConfig({
|
|||
Groups,
|
||||
DonationForms,
|
||||
Pages,
|
||||
Prayers,
|
||||
Magazine,
|
||||
Documents,
|
||||
Media,
|
||||
Users,
|
||||
],
|
||||
globals: [
|
||||
MenuGlobal
|
||||
MenuGlobal,
|
||||
FooterGlobal,
|
||||
],
|
||||
graphQL: {
|
||||
disable: true
|
||||
|
|
|
|||
Loading…
Reference in a new issue