Compare commits
No commits in common. "a12e46044879ca5992f535daa849daa153be9195" and "edaf940748019446df0627bf990989e79316bdfe" have entirely different histories.
a12e460448
...
edaf940748
59 changed files with 1478 additions and 105381 deletions
Binary file not shown.
|
|
@ -1,83 +0,0 @@
|
||||||
import { PageHeader } from '@/compositions/PageHeader/PageHeader'
|
|
||||||
import { Container } from '@/components/Container/Container'
|
|
||||||
import { Section } from '@/components/Section/Section'
|
|
||||||
import { NextPrevButtons } from '@/components/NextPrevButtons/NextPrevButtons'
|
|
||||||
import { fetchMagazines } from '@/fetch/magazine'
|
|
||||||
import { siteConfig } from '@/config/site'
|
|
||||||
import { Media } from '@/payload-types'
|
|
||||||
import Image from 'next/image'
|
|
||||||
import styles from './styles.module.scss'
|
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
searchParams: Promise<{ page?: string }>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function MagazinArchivPage({ searchParams }: Props) {
|
|
||||||
const { page: pageParam } = await searchParams
|
|
||||||
const page = Math.max(1, parseInt(pageParam ?? '1', 10) || 1)
|
|
||||||
|
|
||||||
const result = await fetchMagazines(page)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<PageHeader
|
|
||||||
title={`${siteConfig.magazineName} – Archiv`}
|
|
||||||
description={'Alle bisherigen Ausgaben unseres Pfarrei-Magazins auf einen Blick.'}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Section padding={'small'}>
|
|
||||||
<Container>
|
|
||||||
<div className={styles.grid}>
|
|
||||||
{result.docs.map((magazine) => {
|
|
||||||
const cover = typeof magazine.cover === 'object' ? (magazine.cover as Media) : null
|
|
||||||
const pdfUrl =
|
|
||||||
typeof magazine.document === 'object' ? magazine.document.url ?? null : null
|
|
||||||
|
|
||||||
if (!cover?.url || !pdfUrl) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
key={magazine.id}
|
|
||||||
href={pdfUrl}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className={styles.item}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={cover.url}
|
|
||||||
width={cover.width ?? 300}
|
|
||||||
height={cover.height ?? 400}
|
|
||||||
alt={`${siteConfig.magazineName}`}
|
|
||||||
unoptimized
|
|
||||||
className={styles.cover}
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{result.docs.length === 0 && <p>Keine Ausgaben gefunden.</p>}
|
|
||||||
</Container>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
{(result.hasPrevPage || result.hasNextPage) && (
|
|
||||||
<Section padding={'small'}>
|
|
||||||
<NextPrevButtons
|
|
||||||
prev={
|
|
||||||
result.hasPrevPage && result.prevPage
|
|
||||||
? { href: `/pfarrei/magazin/archiv?page=${result.prevPage}`, text: 'Neuere Ausgaben' }
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
next={
|
|
||||||
result.hasNextPage && result.nextPage
|
|
||||||
? { href: `/pfarrei/magazin/archiv?page=${result.nextPage}`, text: 'Ältere Ausgaben' }
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Section>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
@import 'template.scss';
|
|
||||||
|
|
||||||
.grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
display: block;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover .cover {
|
|
||||||
transform: scale(1.03);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.cover {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
border: 8px solid $light-grey;
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
@ -48,8 +48,7 @@ export default async function MagazinePage() {
|
||||||
Bleiben Sie auf dem Laufenden! Hier finden Sie die aktuelle Ausgabe mit allen Berichten und Terminen aus der Gemeinde.
|
Bleiben Sie auf dem Laufenden! Hier finden Sie die aktuelle Ausgabe mit allen Berichten und Terminen aus der Gemeinde.
|
||||||
<br/><br/>
|
<br/><br/>
|
||||||
</P>
|
</P>
|
||||||
<Button size={"md"} schema={"shade"} href={`/pfarrei/magazin/archiv`}>Archiv</Button>
|
<Button size={"lg"} target={"_blank"} href={magazine_url}>{siteConfig.magazineName} herunterladen</Button>
|
||||||
<Button size={"md"} target={"_blank"} href={magazine_url}>{siteConfig.magazineName} herunterladen</Button>
|
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,6 @@ export default async function EventsOverviewPage({
|
||||||
<EventRow
|
<EventRow
|
||||||
key={e.id}
|
key={e.id}
|
||||||
date={e.date}
|
date={e.date}
|
||||||
end={e.end}
|
|
||||||
title={e.title}
|
title={e.title}
|
||||||
href={e.href}
|
href={e.href}
|
||||||
location={e.location}
|
location={e.location}
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,6 @@ export default async function EventsPage({searchParams}: {
|
||||||
<EventRow
|
<EventRow
|
||||||
key={e.id}
|
key={e.id}
|
||||||
date={e.date}
|
date={e.date}
|
||||||
end={e.end}
|
|
||||||
title={e.title}
|
title={e.title}
|
||||||
href={e.href}
|
href={e.href}
|
||||||
location={e.location}
|
location={e.location}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import { FixedToolbarFeatureClient as FixedToolbarFeatureClient_e70f5e05f09f93e0
|
||||||
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
|
||||||
import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
|
@ -30,7 +29,6 @@ export const importMap = {
|
||||||
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
"@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
"@payloadcms/richtext-lexical/client#UploadFeatureClient": UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
|
||||||
"@payloadcms/richtext-lexical/client#UnorderedListFeatureClient": UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#UnorderedListFeatureClient": UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
"@payloadcms/richtext-lexical/client#AlignFeatureClient": AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#AlignFeatureClient": AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
|
|
||||||
|
|
@ -61,8 +61,6 @@ export const Churches: CollectionConfig = {
|
||||||
{ label: 'Heilige Messe', value: 'MASS' },
|
{ label: 'Heilige Messe', value: 'MASS' },
|
||||||
{ label: 'Familien Messe', value: 'FAMILY' },
|
{ label: 'Familien Messe', value: 'FAMILY' },
|
||||||
{ label: 'Wort-Gottes-Feier', value: 'WORD' },
|
{ label: 'Wort-Gottes-Feier', value: 'WORD' },
|
||||||
{ label: 'Fremdsprache', value: 'LANGUAGE' },
|
|
||||||
{ label: 'Andere', value: 'OTHER' },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -38,21 +38,6 @@ export const EventOccurrences: CollectionConfig = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'endDateTime',
|
|
||||||
type: 'date',
|
|
||||||
index: true,
|
|
||||||
label: {
|
|
||||||
de: 'Enduhrzeit',
|
|
||||||
},
|
|
||||||
admin: {
|
|
||||||
date: {
|
|
||||||
pickerAppearance: 'dayAndTime',
|
|
||||||
timeIntervals: 15,
|
|
||||||
timeFormat: 'HH:mm',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'cancelled',
|
name: 'cancelled',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import { CollapsibleImageWithTextBlock } from '@/collections/blocks/CollapsibleI
|
||||||
import { CollapsibleMapWithTextBlock } from '@/collections/blocks/CollapsibleMapWithText'
|
import { CollapsibleMapWithTextBlock } from '@/collections/blocks/CollapsibleMapWithText'
|
||||||
import { CollapsiblesBlock } from '@/collections/blocks/Collapsibles'
|
import { CollapsiblesBlock } from '@/collections/blocks/Collapsibles'
|
||||||
import { EventsBlock } from '@/collections/blocks/Events'
|
import { EventsBlock } from '@/collections/blocks/Events'
|
||||||
import { GroupEventsBlock } from '@/collections/blocks/GroupEvents'
|
|
||||||
import { PublicationAndNewsletterBlock } from '@/collections/blocks/PublicationAndNewsletter'
|
import { PublicationAndNewsletterBlock } from '@/collections/blocks/PublicationAndNewsletter'
|
||||||
import { ContactPersonBlock } from '@/collections/blocks/ContactPersonBlock'
|
import { ContactPersonBlock } from '@/collections/blocks/ContactPersonBlock'
|
||||||
import { ImageCardsBlock } from '@/collections/blocks/ImageCards'
|
import { ImageCardsBlock } from '@/collections/blocks/ImageCards'
|
||||||
|
|
@ -107,7 +106,6 @@ export const Pages: CollectionConfig = {
|
||||||
CollapsiblesBlock,
|
CollapsiblesBlock,
|
||||||
MassTimesBlock,
|
MassTimesBlock,
|
||||||
EventsBlock,
|
EventsBlock,
|
||||||
GroupEventsBlock,
|
|
||||||
ContactPersonBlock,
|
ContactPersonBlock,
|
||||||
ImageCardsBlock,
|
ImageCardsBlock,
|
||||||
ClassifiedsBlock,
|
ClassifiedsBlock,
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ export const Worship: CollectionConfig = {
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
label: 'Eucharistiefeier',
|
label: 'Heilige Messe',
|
||||||
value: 'MASS',
|
value: 'MASS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -55,14 +55,6 @@ export const Worship: CollectionConfig = {
|
||||||
label: 'Wort-Gottes-Feier',
|
label: 'Wort-Gottes-Feier',
|
||||||
value: 'WORD',
|
value: 'WORD',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: 'Fremdsprache',
|
|
||||||
value: 'LANGUAGE',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Andere',
|
|
||||||
value: 'OTHER',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -20,13 +20,6 @@ export const GalleryBlock: Block = {
|
||||||
relationTo: 'media',
|
relationTo: 'media',
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'caption',
|
|
||||||
label: {
|
|
||||||
de: 'Bildunterschrift'
|
|
||||||
},
|
|
||||||
type: 'text'
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
minRows: 3,
|
minRows: 3,
|
||||||
maxRows: 12
|
maxRows: 12
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
import { Block } from 'payload'
|
|
||||||
|
|
||||||
export const GroupEventsBlock: Block = {
|
|
||||||
slug: 'groupEvents',
|
|
||||||
labels: {
|
|
||||||
singular: {
|
|
||||||
de: 'Gruppen-Veranstaltungen',
|
|
||||||
},
|
|
||||||
plural: {
|
|
||||||
de: 'Gruppen-Veranstaltungen',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: 'group',
|
|
||||||
type: 'relationship',
|
|
||||||
relationTo: 'group',
|
|
||||||
required: true,
|
|
||||||
label: {
|
|
||||||
de: 'Gruppe',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import styles from "./styles.module.scss"
|
import styles from "./styles.module.scss"
|
||||||
import { ContactPerson } from '@/payload-types'
|
import { ContactPerson } from '@/payload-types'
|
||||||
import Image, { StaticImageData } from 'next/image'
|
import Image, { StaticImageData } from 'next/image'
|
||||||
import { SecureEmail } from '@/components/SecureEmail/SecureEmail'
|
|
||||||
import { extractEmailPartsEncoded } from '@/utils/dto/email'
|
|
||||||
|
|
||||||
type ContactPerson2Props = {
|
type ContactPerson2Props = {
|
||||||
contact: null | string | undefined | ContactPerson
|
contact: null | string | undefined | ContactPerson
|
||||||
|
|
@ -14,8 +12,6 @@ export const ContactPerson2 = ({contact, photo}: ContactPerson2Props) => {
|
||||||
return "Unbekannt"
|
return "Unbekannt"
|
||||||
}
|
}
|
||||||
|
|
||||||
const { user, domain } = contact.email ? extractEmailPartsEncoded(contact.email) : { user: '', domain: '' }
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.contact}>
|
<div className={styles.contact}>
|
||||||
{ photo &&
|
{ photo &&
|
||||||
|
|
@ -28,9 +24,9 @@ export const ContactPerson2 = ({contact, photo}: ContactPerson2Props) => {
|
||||||
}
|
}
|
||||||
<div>
|
<div>
|
||||||
{contact.name}
|
{contact.name}
|
||||||
{user && domain &&
|
{contact.email &&
|
||||||
<>
|
<>
|
||||||
<br/> ✉️ <SecureEmail user={user} domain={domain} />
|
<br/> <a href={`mailto:${contact.email}`} className={styles.hover}>{contact.email}</a>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact a:hover {
|
.hover:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import styles from './styles.module.scss'
|
import styles from './styles.module.scss'
|
||||||
import { ContactPerson } from '@/payload-types'
|
import { ContactPerson } from '@/payload-types'
|
||||||
import Image, { StaticImageData } from 'next/image'
|
import Image, { StaticImageData } from 'next/image'
|
||||||
import { SecureEmail } from '@/components/SecureEmail/SecureEmail'
|
|
||||||
import { extractEmailPartsEncoded } from '@/utils/dto/email'
|
|
||||||
|
|
||||||
type ContactPersonCardProps = {
|
type ContactPersonCardProps = {
|
||||||
contact: null | string | undefined | ContactPerson
|
contact: null | string | undefined | ContactPerson
|
||||||
|
|
@ -21,17 +19,10 @@ export const ContactPersonCard = ({
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
let user, domain = null;
|
|
||||||
if (contact.email) {
|
|
||||||
let data = extractEmailPartsEncoded(contact.email)
|
|
||||||
user = data.user;
|
|
||||||
domain = data.domain;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.card}>
|
<div className={styles.card}>
|
||||||
<div>
|
<div>
|
||||||
{photo ? (
|
{photo && (
|
||||||
<Image
|
<Image
|
||||||
className={styles.photo}
|
className={styles.photo}
|
||||||
src={photo}
|
src={photo}
|
||||||
|
|
@ -40,8 +31,6 @@ export const ContactPersonCard = ({
|
||||||
height={200}
|
height={200}
|
||||||
unoptimized={true}
|
unoptimized={true}
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<div className={styles.photoPlaceholder} />
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -49,20 +38,19 @@ export const ContactPersonCard = ({
|
||||||
{contact.role && (
|
{contact.role && (
|
||||||
<h3 className={styles.role}>{contact.role}</h3>
|
<h3 className={styles.role}>{contact.role}</h3>
|
||||||
)}
|
)}
|
||||||
<div className={styles.name}>{contact.name}</div>
|
<div className={styles.info}>
|
||||||
<div className={styles.phoneEmail}>
|
<span className={styles.name}>{contact.name}</span>
|
||||||
{user && domain && (
|
{contact.email && (
|
||||||
<span>✉️ <SecureEmail user={user} domain={domain} /></span>
|
<a href={`mailto:${contact.email}`} className={styles.link}>
|
||||||
|
{contact.email}
|
||||||
|
</a>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{contact.telephone && (
|
{contact.telephone && (
|
||||||
<span>
|
<a href={`tel:${contact.telephone}`} className={styles.link}>
|
||||||
📞 <a href={`tel:${contact.telephone}`}>
|
|
||||||
{contact.telephone}
|
{contact.telephone}
|
||||||
</a>
|
</a>
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -14,50 +14,27 @@
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.photoPlaceholder {
|
|
||||||
width: 200px;
|
|
||||||
height: 200px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: #efefef;
|
|
||||||
}
|
|
||||||
|
|
||||||
.role {
|
.role {
|
||||||
font-size: 25px;
|
font-size: 25px;
|
||||||
color: $base-color;
|
color: $base-color;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.phoneEmail {
|
.info {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 15px;
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.phoneEmail a {
|
.link {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 576px) {
|
|
||||||
.role {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.phoneEmail {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -18,17 +18,6 @@ export const EventInJanuary: Story = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EventWithEnd: Story = {
|
|
||||||
args: {
|
|
||||||
date: '2024-01-06T15:00:00+01:00',
|
|
||||||
end: '2024-01-06T17:00:00+01:00',
|
|
||||||
title: 'Herz Jesu Feier',
|
|
||||||
href: 'https://www.herzJesuFeier.com',
|
|
||||||
location: "St. Clara",
|
|
||||||
cancelled: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EventInMarch: Story = {
|
export const EventInMarch: Story = {
|
||||||
args: {
|
args: {
|
||||||
date: '2024-03-24T15:00:00+01:00',
|
date: '2024-03-24T15:00:00+01:00',
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import Link from 'next/link'
|
||||||
export type EventRowProps = {
|
export type EventRowProps = {
|
||||||
/** datetime 8601 format */
|
/** datetime 8601 format */
|
||||||
date: string,
|
date: string,
|
||||||
end?: string,
|
|
||||||
showDate?: boolean
|
showDate?: boolean
|
||||||
title: string,
|
title: string,
|
||||||
href?: string,
|
href?: string,
|
||||||
|
|
@ -43,10 +42,9 @@ const shortMonth = (date: string) => {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const EventRow = ({date, end, title, location, cancelled, href, color = "base", showDate = true}: EventRowProps) => {
|
export const EventRow = ({date, title, location, cancelled, href, color = "base", showDate = true}: EventRowProps) => {
|
||||||
const day = useMemo(() => date.substring(8, 10), [date]);
|
const day = useMemo(() => date.substring(8, 10), [date]);
|
||||||
const dateObj = useMemo(() => new Date(date), [date]);
|
const dateObj = useMemo(() => new Date(date), [date]);
|
||||||
const endObj = useMemo(() => end ? new Date(end) : undefined, [end]);
|
|
||||||
const month = useMemo(() => shortMonth(date), [date]);
|
const month = useMemo(() => shortMonth(date), [date]);
|
||||||
const [dayFormat, setDayFormat] = useState<"long" | "short">("long")
|
const [dayFormat, setDayFormat] = useState<"long" | "short">("long")
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -81,9 +79,7 @@ export const EventRow = ({date, end, title, location, cancelled, href, color = "
|
||||||
{ showDate &&
|
{ showDate &&
|
||||||
<>
|
<>
|
||||||
{dateObj.toLocaleDateString("de-DE", { weekday: "long" })}
|
{dateObj.toLocaleDateString("de-DE", { weekday: "long" })}
|
||||||
{dayFormat === "long" && " " + dateObj.toLocaleDateString("de-DE", { dateStyle: "short" })}, {dateObj.toLocaleTimeString("de-DE", { timeStyle: "short", timeZone: "Europe/Berlin" })}
|
{dayFormat === "long" && " " + dateObj.toLocaleDateString("de-DE", { dateStyle: "medium" })}, {dateObj.toLocaleTimeString("de-DE", { timeStyle: "short", timeZone: "Europe/Berlin" })} Uhr
|
||||||
{ endObj ? <> - {endObj.toLocaleTimeString("de-DE", { timeStyle: "short", timeZone: "Europe/Berlin" })}</> : "" }
|
|
||||||
Uhr
|
|
||||||
<br />
|
<br />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
67
src/components/Gallery/AutoScroll.tsx
Normal file
67
src/components/Gallery/AutoScroll.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
import styles from "./autoscroll.module.scss"
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
type AutoScrollProps = {
|
||||||
|
isScrolling: boolean
|
||||||
|
onTouch?: () => void
|
||||||
|
children: JSX.Element
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This component autoscroll the content from left to right
|
||||||
|
* automatically
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const AutoScroll = ({children, isScrolling, onTouch}: AutoScrollProps) => {
|
||||||
|
|
||||||
|
const [step, setStep] = useState(0)
|
||||||
|
const [direction, setDirection] = useState<'left' | 'right'>('right')
|
||||||
|
const wrapper = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(wrapper && wrapper.current) {
|
||||||
|
if (isScrolling) {
|
||||||
|
const scrollSpeed = 30; // 30px per second
|
||||||
|
const toScroll = wrapper.current.scrollWidth - window.innerWidth;
|
||||||
|
|
||||||
|
// nothing to scroll
|
||||||
|
if (toScroll <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const steps = toScroll/scrollSpeed * 1000
|
||||||
|
|
||||||
|
const t = setTimeout(() => {
|
||||||
|
let left = toScroll * step/steps
|
||||||
|
|
||||||
|
if(wrapper.current) {
|
||||||
|
wrapper.current.scrollTo({
|
||||||
|
left: left,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if(step >= steps) {
|
||||||
|
setDirection("left")
|
||||||
|
}
|
||||||
|
|
||||||
|
if(step <= 0) {
|
||||||
|
setDirection("right")
|
||||||
|
}
|
||||||
|
|
||||||
|
setStep(direction === "right" ? step + 1 : step - 1)
|
||||||
|
|
||||||
|
return () => clearTimeout(t);
|
||||||
|
}, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
}}, [wrapper, step, setStep, direction, setDirection, isScrolling])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.wrapper}
|
||||||
|
ref={wrapper}
|
||||||
|
onTouchStart={onTouch}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
31
src/components/Gallery/Fullscreen.tsx
Normal file
31
src/components/Gallery/Fullscreen.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import styles from './fullscreen.module.scss'
|
||||||
|
import Image, { StaticImageData } from 'next/image'
|
||||||
|
|
||||||
|
type FullScreenProps = {
|
||||||
|
display: boolean
|
||||||
|
image: StaticImageData
|
||||||
|
alt?: string
|
||||||
|
closeClicked: () => void
|
||||||
|
nextClicked: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Fullscreen = ({display, image, closeClicked, alt, nextClicked}: FullScreenProps) => {
|
||||||
|
if(display)
|
||||||
|
return (
|
||||||
|
<div className={styles.display}>
|
||||||
|
<div
|
||||||
|
className={styles.close}
|
||||||
|
onClick={closeClicked}
|
||||||
|
>x</div>
|
||||||
|
<Image
|
||||||
|
onClick={nextClicked}
|
||||||
|
className={styles.displayImage}
|
||||||
|
height={image.height}
|
||||||
|
width={image.width}
|
||||||
|
src={image.src}
|
||||||
|
alt={alt || ""}
|
||||||
|
unoptimized={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||||
import { Gallery, GalleryItem } from './Gallery'
|
import { Gallery } from './Gallery'
|
||||||
import chris from "./../../assets/christophorus.jpeg"
|
import chris from "./../../assets/christophorus.jpeg"
|
||||||
import forest from "./../../assets/forest.jpeg"
|
|
||||||
import map from "./../../assets/map.jpg"
|
|
||||||
|
|
||||||
const meta: Meta<typeof Gallery> = {
|
const meta: Meta<typeof Gallery> = {
|
||||||
component: Gallery,
|
component: Gallery,
|
||||||
|
|
@ -11,52 +9,27 @@ const meta: Meta<typeof Gallery> = {
|
||||||
type Story = StoryObj<typeof Gallery>;
|
type Story = StoryObj<typeof Gallery>;
|
||||||
export default meta
|
export default meta
|
||||||
|
|
||||||
// Remote image with explicit dimensions (varied aspect ratios) so the strip
|
|
||||||
// shows genuinely different items. Picsum serves a stable image per `id`.
|
|
||||||
const remote = (
|
|
||||||
id: string,
|
|
||||||
picId: number,
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
alt: string,
|
|
||||||
caption?: string,
|
|
||||||
): GalleryItem => ({
|
|
||||||
id,
|
|
||||||
thumbnail: { src: `https://picsum.photos/id/${picId}/${width}/${height}`, width, height },
|
|
||||||
image: { src: `https://picsum.photos/id/${picId}/${width * 2}/${height * 2}`, width: width * 2, height: height * 2 },
|
|
||||||
alt,
|
|
||||||
caption,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
args: {
|
args: {
|
||||||
items: [
|
items: [
|
||||||
{ id: "1", thumbnail: chris, image: chris, alt: "Heiliger Christophorus" },
|
{
|
||||||
remote("2", 1015, 600, 400, "Fluss im Tal"),
|
id: "1",
|
||||||
{ id: "3", thumbnail: forest, image: forest, alt: "Wald" },
|
thumbnail: chris,
|
||||||
remote("4", 1018, 500, 500, "Berglandschaft"),
|
image: chris,
|
||||||
{ id: "5", thumbnail: map, image: map, alt: "Karte der Pfarrei" },
|
alt: "hallo"
|
||||||
remote("6", 1039, 700, 400, "Wasserfall"),
|
},
|
||||||
remote("7", 164, 400, 600, "Stillleben"),
|
{
|
||||||
],
|
id: "2",
|
||||||
|
thumbnail: chris,
|
||||||
|
image: chris,
|
||||||
|
alt: "hallo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
thumbnail: chris,
|
||||||
|
image: chris,
|
||||||
|
alt: "hallo"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WithCaptions: Story = {
|
|
||||||
args: {
|
|
||||||
items: [
|
|
||||||
{ id: "1", thumbnail: chris, image: chris, alt: "Heiliger Christophorus", caption: "Fenster des Heiligen Christophorus, Hauptschiff" },
|
|
||||||
{ id: "2", thumbnail: forest, image: forest, alt: "Wald", caption: "Spaziergang durch den Pfarrwald" },
|
|
||||||
remote("3", 1043, 600, 400, "Innenraum", "Blick auf die Orgelempore"),
|
|
||||||
{ id: "4", thumbnail: map, image: map, alt: "Karte", caption: "Lage der Gemeinde" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SingleItem: Story = {
|
|
||||||
args: {
|
|
||||||
items: [
|
|
||||||
{ id: "1", thumbnail: chris, image: chris, alt: "Heiliger Christophorus", caption: "Einzelnes Bild ohne Autoscroll" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
@ -2,398 +2,92 @@
|
||||||
|
|
||||||
import styles from './styles.module.scss'
|
import styles from './styles.module.scss'
|
||||||
import Image, { StaticImageData } from 'next/image'
|
import Image, { StaticImageData } from 'next/image'
|
||||||
import {
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
CSSProperties,
|
import { AutoScroll } from '@/components/Gallery/AutoScroll'
|
||||||
MouseEvent as ReactMouseEvent,
|
import { Fullscreen } from '@/components/Gallery/Fullscreen'
|
||||||
Ref,
|
|
||||||
TouchEvent as ReactTouchEvent,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react'
|
|
||||||
|
|
||||||
type ImageType =
|
type ImageType = {
|
||||||
| {
|
src: string,
|
||||||
src: string
|
width: number,
|
||||||
width: number
|
height: number
|
||||||
height: number
|
} | StaticImageData
|
||||||
}
|
|
||||||
| StaticImageData
|
|
||||||
|
|
||||||
export type GalleryItem = {
|
export type GalleryItem = {
|
||||||
id: string
|
id: string;
|
||||||
thumbnail: ImageType
|
thumbnail: ImageType ;
|
||||||
image: ImageType
|
image: ImageType;
|
||||||
alt: string
|
alt: string;
|
||||||
caption?: string
|
}
|
||||||
|
|
||||||
|
type GalleryItemProps = {
|
||||||
|
thumbnail: ImageType,
|
||||||
|
alt: string,
|
||||||
|
onClick?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
type GalleryProps = {
|
type GalleryProps = {
|
||||||
items: GalleryItem[]
|
items: GalleryItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Horizontal gap between thumbnails — keep in sync with `$gap` in styles.module.scss. */
|
const GalleryItem = ({ thumbnail, alt, onClick }: GalleryItemProps) => {
|
||||||
const GAP = 30
|
return (
|
||||||
/** Autoscroll speed in pixels per second. */
|
<Image
|
||||||
const SPEED = 30
|
onClick={onClick}
|
||||||
/** Pointer travel (px) past which a press counts as a drag, not a click. */
|
className={styles.item}
|
||||||
const DRAG_THRESHOLD = 5
|
src={thumbnail.src}
|
||||||
/** Minimum swipe distance (px) to trigger navigation / close in the lightbox. */
|
height={thumbnail.height}
|
||||||
const SWIPE_THRESHOLD = 50
|
width={thumbnail.width}
|
||||||
/** Fade duration (ms) — keep in sync with the animations in styles.module.scss. */
|
alt={alt}
|
||||||
const FADE_MS = 100
|
unoptimized={true}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const Gallery = ({ items }: GalleryProps) => {
|
export const Gallery = ({ items }: GalleryProps) => {
|
||||||
const [lightboxIndex, setLightboxIndex] = useState<number | null>(null)
|
const [displayFullscreen, setDisplayFullscreen] = useState(false)
|
||||||
// Render a second copy of the strip only once we know it should loop (client, motion-ok, overflowing).
|
const [idx, setIdx] = useState(0)
|
||||||
const [loop, setLoop] = useState(false)
|
const [isScrolling, setIsScrolling] = useState(true)
|
||||||
|
const displayImage = useCallback((n: number) => {
|
||||||
|
setIdx(n);
|
||||||
|
setDisplayFullscreen(true);
|
||||||
|
setIsScrolling(false)
|
||||||
|
}, [setDisplayFullscreen, setIdx, setIsScrolling]);
|
||||||
|
|
||||||
const wrapperRef = useRef<HTMLDivElement>(null)
|
const next = useCallback(() => {
|
||||||
const firstGroupRef = useRef<HTMLDivElement>(null)
|
setIdx((idx + 1) % items.length)
|
||||||
const pausedRef = useRef(false)
|
}, [idx, setIdx, items])
|
||||||
const lightboxOpenRef = useRef(false)
|
|
||||||
const dragRef = useRef({ active: false, startX: 0, scrollStart: 0, moved: false })
|
|
||||||
|
|
||||||
const close = useCallback(() => setLightboxIndex(null), [])
|
|
||||||
const showNext = useCallback(
|
|
||||||
() => setLightboxIndex((i) => (i === null ? i : (i + 1) % items.length)),
|
|
||||||
[items.length],
|
|
||||||
)
|
|
||||||
const showPrev = useCallback(
|
|
||||||
() => setLightboxIndex((i) => (i === null ? i : (i - 1 + items.length) % items.length)),
|
|
||||||
[items.length],
|
|
||||||
)
|
|
||||||
|
|
||||||
// Keep the autoscroll loop paused while the lightbox is open.
|
if(items.length == 0) {
|
||||||
useEffect(() => {
|
return null;
|
||||||
lightboxOpenRef.current = lightboxIndex !== null
|
|
||||||
}, [lightboxIndex])
|
|
||||||
|
|
||||||
// Decide whether the strip should autoscroll/loop at all.
|
|
||||||
useEffect(() => {
|
|
||||||
const wrapper = wrapperRef.current
|
|
||||||
if (!wrapper || items.length < 2) return
|
|
||||||
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
||||||
if (prefersReduced) return
|
|
||||||
if (wrapper.scrollWidth <= wrapper.clientWidth) return
|
|
||||||
setLoop(true)
|
|
||||||
}, [items.length])
|
|
||||||
|
|
||||||
// Continuous, seamless autoscroll via requestAnimationFrame on the duplicated strip.
|
|
||||||
useEffect(() => {
|
|
||||||
if (!loop) return
|
|
||||||
const wrapper = wrapperRef.current
|
|
||||||
const firstGroup = firstGroupRef.current
|
|
||||||
if (!wrapper || !firstGroup) return
|
|
||||||
|
|
||||||
let raf = 0
|
|
||||||
let last: number | null = null
|
|
||||||
const step = (now: number) => {
|
|
||||||
if (last !== null && !pausedRef.current && !lightboxOpenRef.current) {
|
|
||||||
const dt = (now - last) / 1000
|
|
||||||
wrapper.scrollLeft += SPEED * dt
|
|
||||||
const period = firstGroup.offsetWidth + GAP
|
|
||||||
if (period > 0 && wrapper.scrollLeft >= period) {
|
|
||||||
wrapper.scrollLeft -= period
|
|
||||||
}
|
|
||||||
}
|
|
||||||
last = now
|
|
||||||
raf = requestAnimationFrame(step)
|
|
||||||
}
|
|
||||||
raf = requestAnimationFrame(step)
|
|
||||||
return () => cancelAnimationFrame(raf)
|
|
||||||
}, [loop])
|
|
||||||
|
|
||||||
// --- Pointer drag-to-scroll (mouse). Touch uses native overflow scrolling. ---
|
|
||||||
const onMouseEnter = useCallback(() => {
|
|
||||||
pausedRef.current = true
|
|
||||||
}, [])
|
|
||||||
const onMouseLeave = useCallback(() => {
|
|
||||||
pausedRef.current = false
|
|
||||||
dragRef.current.active = false
|
|
||||||
}, [])
|
|
||||||
const onMouseDown = useCallback((e: ReactMouseEvent) => {
|
|
||||||
const wrapper = wrapperRef.current
|
|
||||||
if (!wrapper) return
|
|
||||||
dragRef.current = {
|
|
||||||
active: true,
|
|
||||||
startX: e.clientX,
|
|
||||||
scrollStart: wrapper.scrollLeft,
|
|
||||||
moved: false,
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
const onMouseMove = useCallback((e: ReactMouseEvent) => {
|
|
||||||
const drag = dragRef.current
|
|
||||||
const wrapper = wrapperRef.current
|
|
||||||
if (!drag.active || !wrapper) return
|
|
||||||
const dx = e.clientX - drag.startX
|
|
||||||
if (Math.abs(dx) > DRAG_THRESHOLD) drag.moved = true
|
|
||||||
wrapper.scrollLeft = drag.scrollStart - dx
|
|
||||||
}, [])
|
|
||||||
const onMouseUp = useCallback(() => {
|
|
||||||
dragRef.current.active = false
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Touch: pause autoscroll while a finger is down; native scrolling moves the strip.
|
|
||||||
const onTouchStart = useCallback(() => {
|
|
||||||
pausedRef.current = true
|
|
||||||
}, [])
|
|
||||||
const onTouchEnd = useCallback(() => {
|
|
||||||
pausedRef.current = false
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Keyboard: pause autoscroll while a thumbnail is focused so it can be activated (WCAG 2.2.2).
|
|
||||||
const onFocus = useCallback(() => {
|
|
||||||
pausedRef.current = true
|
|
||||||
}, [])
|
|
||||||
const onBlur = useCallback(() => {
|
|
||||||
pausedRef.current = false
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleThumbClick = useCallback((index: number) => {
|
|
||||||
// Ignore the click that ends a drag.
|
|
||||||
if (dragRef.current.moved) return
|
|
||||||
setLightboxIndex(index)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
if (items.length === 0) {
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderGroup = (copy: number, ref?: Ref<HTMLDivElement>, hidden = false) => (
|
const { image, alt } = items[idx]
|
||||||
<div className={styles.group} ref={ref} aria-hidden={hidden || undefined}>
|
|
||||||
{items.map((item, index) => (
|
|
||||||
<Image
|
|
||||||
key={`${item.id}-${copy}`}
|
|
||||||
className={styles.item}
|
|
||||||
src={item.thumbnail.src}
|
|
||||||
width={item.thumbnail.width}
|
|
||||||
height={item.thumbnail.height}
|
|
||||||
alt={item.alt}
|
|
||||||
unoptimized
|
|
||||||
draggable={false}
|
|
||||||
tabIndex={hidden ? -1 : 0}
|
|
||||||
role="button"
|
|
||||||
aria-haspopup="dialog"
|
|
||||||
onClick={() => handleThumbClick(index)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault()
|
|
||||||
handleThumbClick(index)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<AutoScroll
|
||||||
className={styles.wrapper}
|
isScrolling={isScrolling}
|
||||||
ref={wrapperRef}
|
onTouch={() => setIsScrolling(false)}
|
||||||
onMouseEnter={onMouseEnter}
|
|
||||||
onMouseLeave={onMouseLeave}
|
|
||||||
onMouseDown={onMouseDown}
|
|
||||||
onMouseMove={onMouseMove}
|
|
||||||
onMouseUp={onMouseUp}
|
|
||||||
onTouchStart={onTouchStart}
|
|
||||||
onTouchEnd={onTouchEnd}
|
|
||||||
onFocus={onFocus}
|
|
||||||
onBlur={onBlur}
|
|
||||||
>
|
>
|
||||||
<div className={styles.track}>
|
<div className={styles.container}>
|
||||||
{renderGroup(0, firstGroupRef)}
|
{items.map((item, index) =>
|
||||||
{loop && renderGroup(1, undefined, true)}
|
<GalleryItem
|
||||||
|
onClick={() => displayImage(index)}
|
||||||
|
key={item.id}
|
||||||
|
thumbnail={item.thumbnail}
|
||||||
|
alt={item.alt}
|
||||||
|
/>)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AutoScroll>
|
||||||
|
|
||||||
{lightboxIndex !== null && (
|
<Fullscreen
|
||||||
<Lightbox
|
display={displayFullscreen}
|
||||||
item={items[lightboxIndex]}
|
image={image}
|
||||||
index={lightboxIndex}
|
closeClicked={() => setDisplayFullscreen(false)}
|
||||||
total={items.length}
|
nextClicked={next}
|
||||||
hasMultiple={items.length > 1}
|
|
||||||
onClose={close}
|
|
||||||
onNext={showNext}
|
|
||||||
onPrev={showPrev}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type LightboxProps = {
|
|
||||||
item: GalleryItem
|
|
||||||
index: number
|
|
||||||
total: number
|
|
||||||
hasMultiple: boolean
|
|
||||||
onClose: () => void
|
|
||||||
onNext: () => void
|
|
||||||
onPrev: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
// Visually hide content while keeping it available to assistive technology.
|
|
||||||
const srOnly: CSSProperties = {
|
|
||||||
position: 'absolute',
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
padding: 0,
|
|
||||||
margin: -1,
|
|
||||||
overflow: 'hidden',
|
|
||||||
clipPath: 'inset(50%)',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
border: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
const Lightbox = ({ item, index, total, hasMultiple, onClose, onNext, onPrev }: LightboxProps) => {
|
|
||||||
const dialogRef = useRef<HTMLDivElement>(null)
|
|
||||||
const touchRef = useRef({ x: 0, y: 0 })
|
|
||||||
const closeTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
|
|
||||||
|
|
||||||
// Play the exit fade before the parent unmounts us.
|
|
||||||
const [closing, setClosing] = useState(false)
|
|
||||||
|
|
||||||
// Fade the whole lightbox out, then actually close once the animation ends.
|
|
||||||
const requestClose = useCallback(() => {
|
|
||||||
setClosing((alreadyClosing) => {
|
|
||||||
if (!alreadyClosing) closeTimer.current = setTimeout(onClose, FADE_MS)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}, [onClose])
|
|
||||||
|
|
||||||
// Lock body scroll and restore focus on close.
|
|
||||||
useEffect(() => {
|
|
||||||
const previouslyFocused = document.activeElement as HTMLElement | null
|
|
||||||
const previousOverflow = document.body.style.overflow
|
|
||||||
document.body.style.overflow = 'hidden'
|
|
||||||
dialogRef.current?.focus()
|
|
||||||
return () => {
|
|
||||||
if (closeTimer.current) clearTimeout(closeTimer.current)
|
|
||||||
document.body.style.overflow = previousOverflow
|
|
||||||
previouslyFocused?.focus?.()
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Keyboard navigation + focus trap (keep Tab within the modal — WCAG 2.4.3).
|
|
||||||
useEffect(() => {
|
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
requestClose()
|
|
||||||
} else if (e.key === 'ArrowRight') {
|
|
||||||
onNext()
|
|
||||||
} else if (e.key === 'ArrowLeft') {
|
|
||||||
onPrev()
|
|
||||||
} else if (e.key === 'Tab') {
|
|
||||||
const dialog = dialogRef.current
|
|
||||||
if (!dialog) return
|
|
||||||
const focusables = Array.from(dialog.querySelectorAll<HTMLElement>('button'))
|
|
||||||
if (focusables.length === 0) {
|
|
||||||
e.preventDefault()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const first = focusables[0]
|
|
||||||
const last = focusables[focusables.length - 1]
|
|
||||||
const active = document.activeElement
|
|
||||||
if (e.shiftKey && (active === first || active === dialog)) {
|
|
||||||
e.preventDefault()
|
|
||||||
last.focus()
|
|
||||||
} else if (!e.shiftKey && active === last) {
|
|
||||||
e.preventDefault()
|
|
||||||
first.focus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.addEventListener('keydown', onKeyDown)
|
|
||||||
return () => window.removeEventListener('keydown', onKeyDown)
|
|
||||||
}, [requestClose, onNext, onPrev])
|
|
||||||
|
|
||||||
const onTouchStart = (e: ReactTouchEvent) => {
|
|
||||||
const t = e.changedTouches[0]
|
|
||||||
touchRef.current = { x: t.clientX, y: t.clientY }
|
|
||||||
}
|
|
||||||
const onTouchEnd = (e: ReactTouchEvent) => {
|
|
||||||
const t = e.changedTouches[0]
|
|
||||||
const dx = t.clientX - touchRef.current.x
|
|
||||||
const dy = t.clientY - touchRef.current.y
|
|
||||||
if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > SWIPE_THRESHOLD) {
|
|
||||||
if (dx < 0) onNext()
|
|
||||||
else onPrev()
|
|
||||||
} else if (dy > SWIPE_THRESHOLD && Math.abs(dy) > Math.abs(dx)) {
|
|
||||||
requestClose()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const stop = (e: ReactMouseEvent) => e.stopPropagation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`${styles.display} ${closing ? styles.closing : ''}`}
|
|
||||||
ref={dialogRef}
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-label="Bildergalerie"
|
|
||||||
tabIndex={-1}
|
|
||||||
onClick={requestClose}
|
|
||||||
onTouchStart={onTouchStart}
|
|
||||||
onTouchEnd={onTouchEnd}
|
|
||||||
>
|
|
||||||
<div style={srOnly} aria-live="polite">
|
|
||||||
{`Bild ${index + 1} von ${total}: ${item.alt}`}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={styles.close}
|
|
||||||
aria-label="Schließen"
|
|
||||||
onClick={(e) => {
|
|
||||||
stop(e)
|
|
||||||
requestClose()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{hasMultiple && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`${styles.nav} ${styles.prev}`}
|
|
||||||
aria-label="Vorheriges Bild"
|
|
||||||
onClick={(e) => {
|
|
||||||
stop(e)
|
|
||||||
onPrev()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Image
|
|
||||||
className={styles.displayImage}
|
|
||||||
src={item.image.src}
|
|
||||||
width={item.image.width}
|
|
||||||
height={item.image.height}
|
|
||||||
alt={item.alt}
|
|
||||||
unoptimized
|
|
||||||
onClick={stop}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{hasMultiple && (
|
</>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`${styles.nav} ${styles.next}`}
|
|
||||||
aria-label="Nächstes Bild"
|
|
||||||
onClick={(e) => {
|
|
||||||
stop(e)
|
|
||||||
onNext()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{item.caption && (
|
|
||||||
<div className={styles.captionBar} onClick={stop}>
|
|
||||||
{item.caption}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
4
src/components/Gallery/autoscroll.module.scss
Normal file
4
src/components/Gallery/autoscroll.module.scss
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
.wrapper {
|
||||||
|
overflow: scroll;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
42
src/components/Gallery/fullscreen.module.scss
Normal file
42
src/components/Gallery/fullscreen.module.scss
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
@import "template.scss";
|
||||||
|
|
||||||
|
.display {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 99;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: $overlay;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.displayImage {
|
||||||
|
display: block;
|
||||||
|
max-height: 90vh;
|
||||||
|
max-width: 90vw;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
color: $white;
|
||||||
|
font-weight: bold;
|
||||||
|
position: fixed;
|
||||||
|
top: 35px;
|
||||||
|
right: 35px;
|
||||||
|
background-color: $base-color;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close:hover {
|
||||||
|
background-color: $shade1;
|
||||||
|
}
|
||||||
|
|
@ -1,176 +1,12 @@
|
||||||
@import "template.scss";
|
@import "template.scss";
|
||||||
|
|
||||||
$gap: 30px;
|
.container {
|
||||||
|
|
||||||
/* ---- Thumbnail strip ---- */
|
|
||||||
.wrapper {
|
|
||||||
overflow-x: auto;
|
|
||||||
overflow-y: hidden;
|
|
||||||
scrollbar-width: none;
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
cursor: grab;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
cursor: grabbing;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.track {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: $gap;
|
gap: 30px;
|
||||||
|
flex-wrap: nowrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-width: 100%;
|
|
||||||
width: max-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group {
|
|
||||||
display: flex;
|
|
||||||
gap: $gap;
|
|
||||||
flex: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
flex: none;
|
}
|
||||||
user-select: none;
|
|
||||||
-webkit-user-drag: none;
|
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
outline: 3px solid $base-color;
|
|
||||||
outline-offset: 3px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- Fullscreen lightbox ---- */
|
|
||||||
@keyframes lightboxFadeIn {
|
|
||||||
from { opacity: 0; }
|
|
||||||
to { opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes lightboxFadeOut {
|
|
||||||
from { opacity: 1; }
|
|
||||||
to { opacity: 0; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.display {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 99;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
background-color: $overlay;
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
outline: none;
|
|
||||||
animation: lightboxFadeIn 200ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.closing {
|
|
||||||
animation: lightboxFadeOut 200ms ease forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
.displayImage {
|
|
||||||
display: block;
|
|
||||||
max-height: 90vh;
|
|
||||||
max-width: 90vw;
|
|
||||||
object-fit: contain;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close,
|
|
||||||
.nav {
|
|
||||||
color: $white;
|
|
||||||
font-weight: bold;
|
|
||||||
position: fixed;
|
|
||||||
background-color: $base-color;
|
|
||||||
border: none;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 1;
|
|
||||||
opacity: 0.6;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus-visible {
|
|
||||||
background-color: $shade1;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
outline: 3px solid $white;
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.close {
|
|
||||||
top: 35px;
|
|
||||||
right: 35px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav {
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
font-size: 0;
|
|
||||||
|
|
||||||
// Chevron drawn with CSS borders so it stays perfectly centred (no font metrics).
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
width: 9px;
|
|
||||||
height: 9px;
|
|
||||||
border: solid $white;
|
|
||||||
border-width: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.prev {
|
|
||||||
left: 35px;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
border-width: 0 0 3px 3px;
|
|
||||||
transform: rotate(45deg);
|
|
||||||
margin-left: 3px; // optical centering of the rotated chevron
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.next {
|
|
||||||
right: 35px;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
border-width: 3px 3px 0 0;
|
|
||||||
transform: rotate(45deg);
|
|
||||||
margin-right: 3px; // optical centering of the rotated chevron
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.captionBar {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
padding: 24px 16px;
|
|
||||||
color: $white;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 16px;
|
|
||||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.45), rgba(0, 0, 0, 0.0));
|
|
||||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.display,
|
|
||||||
.closing {
|
|
||||||
animation: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1169
src/components/Gallery/test.js
Normal file
1169
src/components/Gallery/test.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -85,7 +85,7 @@ export const Default: Story = {
|
||||||
id: '7',
|
id: '7',
|
||||||
date: '2024-08-23T11:00:00.000Z',
|
date: '2024-08-23T11:00:00.000Z',
|
||||||
location: 'St. Marien',
|
location: 'St. Marien',
|
||||||
type: 'LANGUAGE',
|
type: 'MASS',
|
||||||
cancelled: false,
|
cancelled: false,
|
||||||
updatedAt: '',
|
updatedAt: '',
|
||||||
createdAt: '',
|
createdAt: '',
|
||||||
|
|
@ -186,7 +186,7 @@ export const SevenChurches: Story = {
|
||||||
id: '4',
|
id: '4',
|
||||||
date: '2024-08-25T17:00:00.000Z',
|
date: '2024-08-25T17:00:00.000Z',
|
||||||
location: 'St. Christopherus',
|
location: 'St. Christopherus',
|
||||||
type: 'LANGUAGE',
|
type: 'FAMILY',
|
||||||
cancelled: false,
|
cancelled: false,
|
||||||
updatedAt: '',
|
updatedAt: '',
|
||||||
createdAt: '',
|
createdAt: '',
|
||||||
|
|
|
||||||
|
|
@ -39,24 +39,6 @@ export const Default: Story = {
|
||||||
updatedAt: '',
|
updatedAt: '',
|
||||||
createdAt: '',
|
createdAt: '',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
date: '2024-08-25T11:00:00.000Z',
|
|
||||||
location: 'St. Christopherus',
|
|
||||||
type: 'LANGUAGE',
|
|
||||||
cancelled: false,
|
|
||||||
updatedAt: '',
|
|
||||||
createdAt: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
date: '2024-08-25T19:00:00.000Z',
|
|
||||||
location: 'St. Christopherus',
|
|
||||||
type: 'OTHER',
|
|
||||||
cancelled: false,
|
|
||||||
updatedAt: '',
|
|
||||||
createdAt: '',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
date: '2024-08-26T15:00:00.000Z',
|
date: '2024-08-26T15:00:00.000Z',
|
||||||
|
|
|
||||||
|
|
@ -35,24 +35,6 @@ export const LiturgyOfTheWord: Story = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ForeignLanguage: Story = {
|
|
||||||
args: {
|
|
||||||
id: '1',
|
|
||||||
date: '2024-08-23T15:00:00.000Z',
|
|
||||||
type: 'LANGUAGE',
|
|
||||||
cancelled: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Other: Story = {
|
|
||||||
args: {
|
|
||||||
id: '1',
|
|
||||||
date: '2024-08-23T15:00:00.000Z',
|
|
||||||
type: 'OTHER',
|
|
||||||
cancelled: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Cancelled: Story = {
|
export const Cancelled: Story = {
|
||||||
args: {
|
args: {
|
||||||
id: '1',
|
id: '1',
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import styles from './styles.module.scss'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import family from './family.svg'
|
import family from './family.svg'
|
||||||
import bible from './bible.svg'
|
import bible from './bible.svg'
|
||||||
import language from './language.svg'
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
|
@ -13,7 +12,7 @@ import { useTime } from '@/hooks/useTime'
|
||||||
export type MassTableRowProps = {
|
export type MassTableRowProps = {
|
||||||
id: string
|
id: string
|
||||||
date: string
|
date: string
|
||||||
type: 'MASS' | 'FAMILY' | 'WORD' | 'LANGUAGE' | 'OTHER'
|
type: 'MASS' | 'FAMILY' | 'WORD'
|
||||||
cancelled: boolean
|
cancelled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,10 +50,6 @@ export const MassTableRow = ({
|
||||||
{type === 'WORD' && (
|
{type === 'WORD' && (
|
||||||
<Image src={bible} width={18} height={18} alt={'Wortgottesfeier'} />
|
<Image src={bible} width={18} height={18} alt={'Wortgottesfeier'} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{type === 'LANGUAGE' && (
|
|
||||||
<Image src={language} width={18} height={18} alt={'Fremdsprache'} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
|
|
||||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
|
||||||
<svg fill="#ffffff" width="201px" height="201px" viewBox="0 0 512.00 512.00" xmlns="http://www.w3.org/2000/svg" stroke="#ffffff" stroke-width="13.312000000000001">
|
|
||||||
|
|
||||||
<g id="SVGRepo_bgCarrier" stroke-width="0">
|
|
||||||
|
|
||||||
<rect x="0" y="0" width="512.00" height="512.00" rx="256" fill="#728F8D" strokewidth="0"/>
|
|
||||||
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
|
|
||||||
<g id="SVGRepo_iconCarrier">
|
|
||||||
|
|
||||||
<title>ionicons-v5-l</title>
|
|
||||||
|
|
||||||
<path d="M478.33,433.6l-90-218a22,22,0,0,0-40.67,0l-90,218a22,22,0,1,0,40.67,16.79L316.66,406H419.33l18.33,44.39A22,22,0,0,0,458,464a22,22,0,0,0,20.32-30.4ZM334.83,362,368,281.65,401.17,362Z"/>
|
|
||||||
|
|
||||||
<path d="M267.84,342.92a22,22,0,0,0-4.89-30.7c-.2-.15-15-11.13-36.49-34.73,39.65-53.68,62.11-114.75,71.27-143.49H330a22,22,0,0,0,0-44H214V70a22,22,0,0,0-44,0V90H54a22,22,0,0,0,0,44H251.25c-9.52,26.95-27.05,69.5-53.79,108.36-31.41-41.68-43.08-68.65-43.17-68.87a22,22,0,0,0-40.58,17c.58,1.38,14.55,34.23,52.86,83.93.92,1.19,1.83,2.35,2.74,3.51-39.24,44.35-77.74,71.86-93.85,80.74a22,22,0,1,0,21.07,38.63c2.16-1.18,48.6-26.89,101.63-85.59,22.52,24.08,38,35.44,38.93,36.1a22,22,0,0,0,30.75-4.9Z"/>
|
|
||||||
|
|
||||||
</g>
|
|
||||||
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
|
@ -1,23 +0,0 @@
|
||||||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
|
||||||
import { SecureEmail } from './SecureEmail'
|
|
||||||
|
|
||||||
const meta: Meta<typeof SecureEmail> = {
|
|
||||||
component: SecureEmail,
|
|
||||||
}
|
|
||||||
|
|
||||||
type Story = StoryObj<typeof SecureEmail>;
|
|
||||||
export default meta
|
|
||||||
|
|
||||||
export const Default: Story = {
|
|
||||||
args: {
|
|
||||||
user: btoa('pastoor'),
|
|
||||||
domain: btoa('parochie.nl'),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const WithLabel: Story = {
|
|
||||||
args: {
|
|
||||||
...Default.args,
|
|
||||||
label: 'Send email'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
'use client';
|
|
||||||
import { useRef } from 'react';
|
|
||||||
|
|
||||||
interface SecureEmailProps {
|
|
||||||
/**
|
|
||||||
* base64-encoded email, e.g. btoa('pastoor')
|
|
||||||
*/
|
|
||||||
user: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* base64-encoded domain, e.g. btoa('parochie.nl')
|
|
||||||
*/
|
|
||||||
domain: string;
|
|
||||||
label?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a secure email link that requires user interaction to reveal the actual "mailto:" address,
|
|
||||||
* preventing the address from being easily scraped by bots.
|
|
||||||
*
|
|
||||||
* @param {Object} props - The properties for the SecureEmail component.
|
|
||||||
* @param {string} props.user - The Base64-encoded user portion of the email address.
|
|
||||||
* @param {string} props.domain - The Base64-encoded domain portion of the email address.
|
|
||||||
* @param {string} [props.label='E-Mail schreiben'] - The label text displayed for the email link.
|
|
||||||
* @return {JSX.Element} A React component that renders an email link.
|
|
||||||
*/
|
|
||||||
export function SecureEmail({ user, domain, label = 'E-Mail schreiben' }: SecureEmailProps) {
|
|
||||||
const ref = useRef<HTMLAnchorElement>(null);
|
|
||||||
|
|
||||||
// Decode and write the real mailto: into the href, imperatively (not via state),
|
|
||||||
// so it's in place synchronously before the browser navigates.
|
|
||||||
const reveal = () => {
|
|
||||||
const el = ref.current;
|
|
||||||
if (!el || el.href.startsWith('mailto:')) return;
|
|
||||||
el.href = `mailto:${atob(user)}@${atob(domain)}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fallback: if a click somehow lands before reveal fired (some touch flows),
|
|
||||||
// assemble the address then and navigate manually.
|
|
||||||
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
|
||||||
if (!ref.current?.href.startsWith('mailto:')) {
|
|
||||||
e.preventDefault();
|
|
||||||
reveal();
|
|
||||||
window.location.href = ref.current!.href;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
ref={ref}
|
|
||||||
href="#"
|
|
||||||
onMouseEnter={reveal}
|
|
||||||
onFocus={reveal}
|
|
||||||
onTouchStart={reveal}
|
|
||||||
onClick={handleClick}>
|
|
||||||
{label}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,79 +1,41 @@
|
||||||
import type { JSXConvertersFunction } from '@payloadcms/richtext-lexical/react'
|
import type { JSXConvertersFunction } from '@payloadcms/richtext-lexical/react'
|
||||||
import { LinkJSXConverter } from '@payloadcms/richtext-lexical/react'
|
|
||||||
import type { SerializedLinkNode } from '@payloadcms/richtext-lexical'
|
import type { SerializedLinkNode } from '@payloadcms/richtext-lexical'
|
||||||
import Image from 'next/image'
|
|
||||||
import { Button } from '@/components/Button/Button'
|
import { Button } from '@/components/Button/Button'
|
||||||
import { getPhoto } from '@/utils/dto/gallery'
|
|
||||||
|
|
||||||
|
// Lexical link nodes carry their editable metadata (url, newTab, linkType, ...)
|
||||||
|
// in `fields`. We extend that shape with the custom `appearance` select that is
|
||||||
|
// added to LinkFeature in `payload.config.ts`, so editors can mark a link as a
|
||||||
|
// call-to-action button.
|
||||||
type LinkFields = SerializedLinkNode['fields'] & {
|
type LinkFields = SerializedLinkNode['fields'] & {
|
||||||
appearance?: 'link' | 'button'
|
appearance?: 'link' | 'button'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maps an internal Payload document to its public URL based on collection slug.
|
// Custom JSX converters passed to <RichText /> wherever rich text is rendered.
|
||||||
const internalDocToHref = ({ linkNode }: { linkNode: SerializedLinkNode }): string => {
|
// Only the `link` converter is overridden — every other node keeps Payload's
|
||||||
const { doc } = linkNode.fields
|
// default rendering via the spread of `defaultConverters`.
|
||||||
if (!doc?.value || typeof doc.value !== 'object') return '#'
|
|
||||||
|
|
||||||
const document = doc.value as Record<string, unknown>
|
|
||||||
|
|
||||||
switch (doc.relationTo) {
|
|
||||||
case 'group':
|
|
||||||
return `/gruppe/${document.slug ?? document.id}`
|
|
||||||
case 'event':
|
|
||||||
return `/veranstaltungen/${document.id}`
|
|
||||||
case 'worship':
|
|
||||||
return `/gottesdienst/${document.id}`
|
|
||||||
case 'pages':
|
|
||||||
return `/${document.slug ?? ''}`
|
|
||||||
case 'parish':
|
|
||||||
return `/gemeinde/${document.slug ?? document.id}`
|
|
||||||
case 'blog':
|
|
||||||
return `/blog/${document.id}`
|
|
||||||
default:
|
|
||||||
return '#'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const linkConverters = LinkJSXConverter({ internalDocToHref })
|
|
||||||
|
|
||||||
export const jsxConverters: JSXConvertersFunction = ({ defaultConverters }) => ({
|
export const jsxConverters: JSXConvertersFunction = ({ defaultConverters }) => ({
|
||||||
...defaultConverters,
|
...defaultConverters,
|
||||||
...linkConverters,
|
|
||||||
// Render uploaded media at the "gallery" size (500px tall, variable width).
|
|
||||||
upload: ({ node }) => {
|
|
||||||
if (node.relationTo !== 'media' || typeof node.value !== 'object') {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const photo = getPhoto('gallery', node.value)
|
|
||||||
if (!photo) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Image
|
|
||||||
src={photo.src}
|
|
||||||
width={photo.width}
|
|
||||||
height={photo.height}
|
|
||||||
alt={node.value.alt}
|
|
||||||
unoptimized={true}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
link: (args) => {
|
link: (args) => {
|
||||||
const { node, nodesToJSX } = args
|
const { node, nodesToJSX } = args
|
||||||
const fields = node.fields as LinkFields
|
const fields = node.fields as LinkFields
|
||||||
|
|
||||||
// Normal link → delegate to the internalDocToHref-aware link converter.
|
// Normal link → delegate to Payload's built-in link converter.
|
||||||
|
// The default converter from the package is a function, but the type
|
||||||
|
// also allows a static ReactNode, so we narrow before calling.
|
||||||
if (fields?.appearance !== 'button') {
|
if (fields?.appearance !== 'button') {
|
||||||
const linkConverter = linkConverters.link
|
const defaultLink = defaultConverters.link
|
||||||
return typeof linkConverter === 'function' ? linkConverter(args) : linkConverter
|
return typeof defaultLink === 'function' ? defaultLink(args) : defaultLink
|
||||||
}
|
}
|
||||||
|
|
||||||
// Button link → resolve href using internalDocToHref for internal links.
|
// Button link → resolve href the same way Payload's default converter does:
|
||||||
|
// internal links point at the related doc's slug, custom links use `url`.
|
||||||
const href =
|
const href =
|
||||||
fields.linkType === 'internal'
|
fields.linkType === 'internal' && typeof fields.doc?.value === 'object'
|
||||||
? internalDocToHref({ linkNode: node })
|
? `/${(fields.doc.value as { slug?: string }).slug ?? ''}`
|
||||||
: (fields.url ?? '#')
|
: (fields.url ?? '#')
|
||||||
|
|
||||||
|
// Render the existing Button component. Schema and size are intentionally
|
||||||
|
// hardcoded — editors only choose link vs. button, not the styling.
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
href={href}
|
href={href}
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,6 @@
|
||||||
color: $base-color;
|
color: $base-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container img {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container h3 {
|
.container h3 {
|
||||||
font-size: 33px;
|
font-size: 33px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ import { EventsBlock } from '@/compositions/Blocks/EventsBlock'
|
||||||
import { ImageCardsBlock } from '@/compositions/Blocks/ImageCardsBlock'
|
import { ImageCardsBlock } from '@/compositions/Blocks/ImageCardsBlock'
|
||||||
import { ClassifiedsFromApi } from '@/components/Classifieds/ClassifiedsFromApi'
|
import { ClassifiedsFromApi } from '@/components/Classifieds/ClassifiedsFromApi'
|
||||||
import { NextPrevButtons } from '@/components/NextPrevButtons/NextPrevButtons'
|
import { NextPrevButtons } from '@/components/NextPrevButtons/NextPrevButtons'
|
||||||
import { GroupEvents } from '@/compositions/GroupEvents/GroupEvents'
|
|
||||||
|
|
||||||
type BlocksProps = {
|
type BlocksProps = {
|
||||||
content: Blog['content']['content'] | NonNullable<Page['content']>
|
content: Blog['content']['content'] | NonNullable<Page['content']>
|
||||||
|
|
@ -53,12 +52,7 @@ export function Blocks({ content }: BlocksProps) {
|
||||||
return (
|
return (
|
||||||
<Container key={item.id}>
|
<Container key={item.id}>
|
||||||
<Section padding={'medium'}>
|
<Section padding={'medium'}>
|
||||||
<Button
|
<Button size={'lg'} href={item.file.url || 'notfound'} schema={'contrast'}>{item.button}</Button>
|
||||||
size={'lg'}
|
|
||||||
href={item.file.url || 'notfound'}
|
|
||||||
schema={'contrast'}
|
|
||||||
target={'_blank'}
|
|
||||||
>{item.button}</Button>
|
|
||||||
</Section>
|
</Section>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
|
|
@ -278,18 +272,6 @@ export function Blocks({ content }: BlocksProps) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.blockType === 'groupEvents') {
|
|
||||||
const groupId =
|
|
||||||
typeof item.group === 'object' ? item.group.id : item.group
|
|
||||||
return (
|
|
||||||
<Section key={item.id} padding={'small'}>
|
|
||||||
<Container>
|
|
||||||
<GroupEvents id={groupId} />
|
|
||||||
</Container>
|
|
||||||
</Section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.blockType === 'imageCards') {
|
if (item.blockType === 'imageCards') {
|
||||||
return <ImageCardsBlock key={item.id} items={item.items} />
|
return <ImageCardsBlock key={item.id} items={item.items} />
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,14 +39,7 @@ export function ParishBlocks({ content }: BlocksProps) {
|
||||||
return (
|
return (
|
||||||
<Container key={item.id}>
|
<Container key={item.id}>
|
||||||
<Section padding={"medium"}>
|
<Section padding={"medium"}>
|
||||||
<Button
|
<Button size={"lg"} href={item.file.url || "notfound"} schema={"contrast"}>{item.button}</Button>
|
||||||
size={"lg"}
|
|
||||||
href={item.file.url || "notfound"}
|
|
||||||
schema={"contrast"}
|
|
||||||
target={"_blank"}
|
|
||||||
>
|
|
||||||
{item.button}
|
|
||||||
</Button>
|
|
||||||
</Section>
|
</Section>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ export const Events = ({ events, n, schema = 'base' }: EventsProps) => {
|
||||||
<EventRow
|
<EventRow
|
||||||
key={event.href}
|
key={event.href}
|
||||||
date={event.date}
|
date={event.date}
|
||||||
end={event.end}
|
|
||||||
title={event.title}
|
title={event.title}
|
||||||
href={event.href}
|
href={event.href}
|
||||||
location={event.location}
|
location={event.location}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,15 @@
|
||||||
import { getPayload } from 'payload'
|
import { getPayload } from 'payload'
|
||||||
import config from '@/payload.config'
|
import config from '@/payload.config'
|
||||||
import { Magazine } from '@/payload-types'
|
import { Magazine } from '@/payload-types'
|
||||||
import { PaginatedDocs } from 'payload'
|
|
||||||
|
|
||||||
export const fetchLastMagazine = async (): Promise<Magazine | undefined> => {
|
/**
|
||||||
|
* Asynchronously fetches the last magazine entry.
|
||||||
|
*
|
||||||
|
* @returns {Promise<Magazine | undefined>} The last magazine entry if successful, or undefined.
|
||||||
|
*/
|
||||||
|
export const fetchLastMagazine = async (): Promise<
|
||||||
|
Magazine | undefined
|
||||||
|
> => {
|
||||||
const payload = await getPayload({ config })
|
const payload = await getPayload({ config })
|
||||||
const result = await payload.find({
|
const result = await payload.find({
|
||||||
collection: 'magazine',
|
collection: 'magazine',
|
||||||
|
|
@ -12,13 +18,3 @@ export const fetchLastMagazine = async (): Promise<Magazine | undefined> => {
|
||||||
})
|
})
|
||||||
return result.docs[0]
|
return result.docs[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchMagazines = async (page = 1, limit = 12): Promise<PaginatedDocs<Magazine>> => {
|
|
||||||
const payload = await getPayload({ config })
|
|
||||||
return payload.find({
|
|
||||||
collection: 'magazine',
|
|
||||||
sort: '-date',
|
|
||||||
limit,
|
|
||||||
page,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
export const useMassType = (type: 'MASS' | 'FAMILY' | 'WORD' | 'LANGUAGE' | 'OTHER') => {
|
export const useMassType = (type: 'MASS' | 'FAMILY' | 'WORD') => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'FAMILY':
|
case 'FAMILY':
|
||||||
return 'Familien Messe'
|
return 'Familien Messe'
|
||||||
case 'WORD':
|
case 'WORD':
|
||||||
return 'Wort-Gottes-Feier'
|
return 'Wort-Gottes-Feier'
|
||||||
case 'LANGUAGE':
|
|
||||||
return 'Fremdsprache'
|
|
||||||
case 'OTHER':
|
|
||||||
return 'Andere'
|
|
||||||
case 'MASS':
|
case 'MASS':
|
||||||
default:
|
default:
|
||||||
return 'Heilige Messe'
|
return 'Heilige Messe'
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,6 @@ type GenerateEventOccurrencesOutput = {
|
||||||
type EventLike = {
|
type EventLike = {
|
||||||
id: string
|
id: string
|
||||||
date?: string | null
|
date?: string | null
|
||||||
endDateTime?: string | null
|
|
||||||
recurrenceType?: EventRecurrenceType | null
|
recurrenceType?: EventRecurrenceType | null
|
||||||
recurrenceRules?: RecurrenceRule[] | null
|
recurrenceRules?: RecurrenceRule[] | null
|
||||||
endDate?: string | null
|
endDate?: string | null
|
||||||
|
|
@ -114,17 +113,6 @@ export const regenerateOccurrencesForEvent = async ({
|
||||||
return { created, deleted, skipped: skipped + 1 }
|
return { created, deleted, skipped: skipped + 1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Derive each occurrence's end from the event's duration (end - start),
|
|
||||||
// re-anchored to the occurrence's own start. A missing or non-positive
|
|
||||||
// duration yields no end on the occurrences.
|
|
||||||
const eventEnd = event.endDateTime ? new Date(event.endDateTime) : null
|
|
||||||
const durationMs =
|
|
||||||
eventEnd && !Number.isNaN(eventEnd.getTime()) && eventEnd.getTime() > eventDate.getTime()
|
|
||||||
? eventEnd.getTime() - eventDate.getTime()
|
|
||||||
: null
|
|
||||||
const endFor = (start: Date): string | null =>
|
|
||||||
durationMs === null ? null : new Date(start.getTime() + durationMs).toISOString()
|
|
||||||
|
|
||||||
const recurrenceType = event.recurrenceType ?? 'none'
|
const recurrenceType = event.recurrenceType ?? 'none'
|
||||||
|
|
||||||
if (recurrenceType === 'none') {
|
if (recurrenceType === 'none') {
|
||||||
|
|
@ -136,7 +124,6 @@ export const regenerateOccurrencesForEvent = async ({
|
||||||
data: {
|
data: {
|
||||||
event: event.id,
|
event: event.id,
|
||||||
date: eventDate.toISOString(),
|
date: eventDate.toISOString(),
|
||||||
endDateTime: endFor(eventDate),
|
|
||||||
cancelled: cancelledDates.has(eventDate.toISOString()),
|
cancelled: cancelledDates.has(eventDate.toISOString()),
|
||||||
generated: true,
|
generated: true,
|
||||||
},
|
},
|
||||||
|
|
@ -157,7 +144,6 @@ export const regenerateOccurrencesForEvent = async ({
|
||||||
data: {
|
data: {
|
||||||
event: event.id,
|
event: event.id,
|
||||||
date: iso,
|
date: iso,
|
||||||
endDateTime: endFor(date),
|
|
||||||
cancelled: cancelledDates.has(iso),
|
cancelled: cancelledDates.has(iso),
|
||||||
generated: true,
|
generated: true,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ export const generateRecurringMassesTask: TaskConfig<{
|
||||||
for (const rawEntry of schedule) {
|
for (const rawEntry of schedule) {
|
||||||
const entry = rawEntry as ScheduleEntry & {
|
const entry = rawEntry as ScheduleEntry & {
|
||||||
time?: string | Date | null
|
time?: string | Date | null
|
||||||
type?: 'MASS' | 'FAMILY' | 'WORD' | 'LANGUAGE' | 'OTHER'
|
type?: 'MASS' | 'FAMILY' | 'WORD'
|
||||||
defaultCelebrant?: string | null
|
defaultCelebrant?: string | null
|
||||||
defaultTitle?: string | null
|
defaultTitle?: string | null
|
||||||
defaultDescription?: string | null
|
defaultDescription?: string | null
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,50 +0,0 @@
|
||||||
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
|
|
||||||
|
|
||||||
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
|
|
||||||
await db.execute(sql`
|
|
||||||
CREATE TABLE "pages_blocks_group_events" (
|
|
||||||
"_order" integer NOT NULL,
|
|
||||||
"_parent_id" uuid NOT NULL,
|
|
||||||
"_path" text NOT NULL,
|
|
||||||
"id" varchar PRIMARY KEY NOT NULL,
|
|
||||||
"group_id" uuid,
|
|
||||||
"block_name" varchar
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE "_pages_v_blocks_group_events" (
|
|
||||||
"_order" integer NOT NULL,
|
|
||||||
"_parent_id" uuid NOT NULL,
|
|
||||||
"_path" text NOT NULL,
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"group_id" uuid,
|
|
||||||
"_uuid" varchar,
|
|
||||||
"block_name" varchar
|
|
||||||
);
|
|
||||||
|
|
||||||
ALTER TABLE "announcement" ALTER COLUMN "date" SET DEFAULT '2026-06-07T09:51:11.853Z';
|
|
||||||
ALTER TABLE "calendar" ALTER COLUMN "date" SET DEFAULT '2026-06-07T09:51:12.181Z';
|
|
||||||
ALTER TABLE "classifieds" ALTER COLUMN "until" SET DEFAULT '2026-07-05T09:51:12.265Z';
|
|
||||||
ALTER TABLE "pages_blocks_group_events" ADD CONSTRAINT "pages_blocks_group_events_group_id_group_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."group"("id") ON DELETE set null ON UPDATE no action;
|
|
||||||
ALTER TABLE "pages_blocks_group_events" ADD CONSTRAINT "pages_blocks_group_events_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
|
|
||||||
ALTER TABLE "_pages_v_blocks_group_events" ADD CONSTRAINT "_pages_v_blocks_group_events_group_id_group_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."group"("id") ON DELETE set null ON UPDATE no action;
|
|
||||||
ALTER TABLE "_pages_v_blocks_group_events" ADD CONSTRAINT "_pages_v_blocks_group_events_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_pages_v"("id") ON DELETE cascade ON UPDATE no action;
|
|
||||||
CREATE INDEX "pages_blocks_group_events_order_idx" ON "pages_blocks_group_events" USING btree ("_order");
|
|
||||||
CREATE INDEX "pages_blocks_group_events_parent_id_idx" ON "pages_blocks_group_events" USING btree ("_parent_id");
|
|
||||||
CREATE INDEX "pages_blocks_group_events_path_idx" ON "pages_blocks_group_events" USING btree ("_path");
|
|
||||||
CREATE INDEX "pages_blocks_group_events_group_idx" ON "pages_blocks_group_events" USING btree ("group_id");
|
|
||||||
CREATE INDEX "_pages_v_blocks_group_events_order_idx" ON "_pages_v_blocks_group_events" USING btree ("_order");
|
|
||||||
CREATE INDEX "_pages_v_blocks_group_events_parent_id_idx" ON "_pages_v_blocks_group_events" USING btree ("_parent_id");
|
|
||||||
CREATE INDEX "_pages_v_blocks_group_events_path_idx" ON "_pages_v_blocks_group_events" USING btree ("_path");
|
|
||||||
CREATE INDEX "_pages_v_blocks_group_events_group_idx" ON "_pages_v_blocks_group_events" USING btree ("group_id");`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
|
|
||||||
await db.execute(sql`
|
|
||||||
ALTER TABLE "pages_blocks_group_events" DISABLE ROW LEVEL SECURITY;
|
|
||||||
ALTER TABLE "_pages_v_blocks_group_events" DISABLE ROW LEVEL SECURITY;
|
|
||||||
DROP TABLE "pages_blocks_group_events" CASCADE;
|
|
||||||
DROP TABLE "_pages_v_blocks_group_events" CASCADE;
|
|
||||||
ALTER TABLE "announcement" ALTER COLUMN "date" SET DEFAULT '2026-05-10T07:03:55.605Z';
|
|
||||||
ALTER TABLE "calendar" ALTER COLUMN "date" SET DEFAULT '2026-05-10T07:03:55.910Z';
|
|
||||||
ALTER TABLE "classifieds" ALTER COLUMN "until" SET DEFAULT '2026-06-03T07:03:55.993Z';`)
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,19 +0,0 @@
|
||||||
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
|
|
||||||
|
|
||||||
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
|
|
||||||
await db.execute(sql`
|
|
||||||
ALTER TABLE "announcement" ALTER COLUMN "date" SET DEFAULT '2026-06-07T11:31:06.259Z';
|
|
||||||
ALTER TABLE "calendar" ALTER COLUMN "date" SET DEFAULT '2026-06-07T11:31:06.641Z';
|
|
||||||
ALTER TABLE "classifieds" ALTER COLUMN "until" SET DEFAULT '2026-07-05T11:31:06.720Z';
|
|
||||||
ALTER TABLE "event_occurrence" ADD COLUMN "end_date_time" timestamp(3) with time zone;
|
|
||||||
CREATE INDEX "event_occurrence_end_date_time_idx" ON "event_occurrence" USING btree ("end_date_time");`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
|
|
||||||
await db.execute(sql`
|
|
||||||
DROP INDEX "event_occurrence_end_date_time_idx";
|
|
||||||
ALTER TABLE "announcement" ALTER COLUMN "date" SET DEFAULT '2026-06-07T09:51:11.853Z';
|
|
||||||
ALTER TABLE "calendar" ALTER COLUMN "date" SET DEFAULT '2026-06-07T09:51:12.181Z';
|
|
||||||
ALTER TABLE "classifieds" ALTER COLUMN "until" SET DEFAULT '2026-07-05T09:51:12.265Z';
|
|
||||||
ALTER TABLE "event_occurrence" DROP COLUMN "end_date_time";`)
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,27 +0,0 @@
|
||||||
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
|
|
||||||
|
|
||||||
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
|
|
||||||
await db.execute(sql`
|
|
||||||
ALTER TABLE "announcement" ALTER COLUMN "date" SET DEFAULT '2026-06-07T12:41:03.305Z';
|
|
||||||
ALTER TABLE "calendar" ALTER COLUMN "date" SET DEFAULT '2026-06-07T12:41:03.609Z';
|
|
||||||
ALTER TABLE "classifieds" ALTER COLUMN "until" SET DEFAULT '2026-07-05T12:41:03.673Z';
|
|
||||||
ALTER TABLE "blog_blocks_gallery_items" ADD COLUMN "caption" varchar;
|
|
||||||
ALTER TABLE "_blog_v_blocks_gallery_items" ADD COLUMN "caption" varchar;
|
|
||||||
ALTER TABLE "group_blocks_gallery_items" ADD COLUMN "caption" varchar;
|
|
||||||
ALTER TABLE "_group_v_blocks_gallery_items" ADD COLUMN "caption" varchar;
|
|
||||||
ALTER TABLE "pages_blocks_gallery_items" ADD COLUMN "caption" varchar;
|
|
||||||
ALTER TABLE "_pages_v_blocks_gallery_items" ADD COLUMN "caption" varchar;`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
|
|
||||||
await db.execute(sql`
|
|
||||||
ALTER TABLE "announcement" ALTER COLUMN "date" SET DEFAULT '2026-06-07T11:31:06.259Z';
|
|
||||||
ALTER TABLE "calendar" ALTER COLUMN "date" SET DEFAULT '2026-06-07T11:31:06.641Z';
|
|
||||||
ALTER TABLE "classifieds" ALTER COLUMN "until" SET DEFAULT '2026-07-05T11:31:06.720Z';
|
|
||||||
ALTER TABLE "blog_blocks_gallery_items" DROP COLUMN "caption";
|
|
||||||
ALTER TABLE "_blog_v_blocks_gallery_items" DROP COLUMN "caption";
|
|
||||||
ALTER TABLE "group_blocks_gallery_items" DROP COLUMN "caption";
|
|
||||||
ALTER TABLE "_group_v_blocks_gallery_items" DROP COLUMN "caption";
|
|
||||||
ALTER TABLE "pages_blocks_gallery_items" DROP COLUMN "caption";
|
|
||||||
ALTER TABLE "_pages_v_blocks_gallery_items" DROP COLUMN "caption";`)
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,27 +0,0 @@
|
||||||
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
|
|
||||||
|
|
||||||
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
|
|
||||||
await db.execute(sql`
|
|
||||||
ALTER TYPE "public"."enum_church_recurring_schedule_type" ADD VALUE 'LANGUAGE';
|
|
||||||
ALTER TYPE "public"."enum_church_recurring_schedule_type" ADD VALUE 'OTHER';
|
|
||||||
ALTER TYPE "public"."enum_worship_type" ADD VALUE 'LANGUAGE';
|
|
||||||
ALTER TYPE "public"."enum_worship_type" ADD VALUE 'OTHER';
|
|
||||||
ALTER TABLE "announcement" ALTER COLUMN "date" SET DEFAULT '2026-06-07T13:08:42.418Z';
|
|
||||||
ALTER TABLE "calendar" ALTER COLUMN "date" SET DEFAULT '2026-06-07T13:08:42.705Z';
|
|
||||||
ALTER TABLE "classifieds" ALTER COLUMN "until" SET DEFAULT '2026-07-05T13:08:42.759Z';`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
|
|
||||||
await db.execute(sql`
|
|
||||||
ALTER TABLE "church_recurring_schedule" ALTER COLUMN "type" SET DATA TYPE text;
|
|
||||||
DROP TYPE "public"."enum_church_recurring_schedule_type";
|
|
||||||
CREATE TYPE "public"."enum_church_recurring_schedule_type" AS ENUM('MASS', 'FAMILY', 'WORD');
|
|
||||||
ALTER TABLE "church_recurring_schedule" ALTER COLUMN "type" SET DATA TYPE "public"."enum_church_recurring_schedule_type" USING "type"::"public"."enum_church_recurring_schedule_type";
|
|
||||||
ALTER TABLE "worship" ALTER COLUMN "type" SET DATA TYPE text;
|
|
||||||
DROP TYPE "public"."enum_worship_type";
|
|
||||||
CREATE TYPE "public"."enum_worship_type" AS ENUM('MASS', 'FAMILY', 'WORD');
|
|
||||||
ALTER TABLE "worship" ALTER COLUMN "type" SET DATA TYPE "public"."enum_worship_type" USING "type"::"public"."enum_worship_type";
|
|
||||||
ALTER TABLE "announcement" ALTER COLUMN "date" SET DEFAULT '2026-06-07T12:41:03.305Z';
|
|
||||||
ALTER TABLE "calendar" ALTER COLUMN "date" SET DEFAULT '2026-06-07T12:41:03.609Z';
|
|
||||||
ALTER TABLE "classifieds" ALTER COLUMN "until" SET DEFAULT '2026-07-05T12:41:03.673Z';`)
|
|
||||||
}
|
|
||||||
|
|
@ -44,10 +44,6 @@ import * as migration_20260423_115311 from './20260423_115311';
|
||||||
import * as migration_20260423_131226_add_event_end_date_time from './20260423_131226_add_event_end_date_time';
|
import * as migration_20260423_131226_add_event_end_date_time from './20260423_131226_add_event_end_date_time';
|
||||||
import * as migration_20260429_121105_collapsible_map_with_text from './20260429_121105_collapsible_map_with_text';
|
import * as migration_20260429_121105_collapsible_map_with_text from './20260429_121105_collapsible_map_with_text';
|
||||||
import * as migration_20260504_070356_next_prev_buttons from './20260504_070356_next_prev_buttons';
|
import * as migration_20260504_070356_next_prev_buttons from './20260504_070356_next_prev_buttons';
|
||||||
import * as migration_20260605_095112 from './20260605_095112';
|
|
||||||
import * as migration_20260605_113107_occurrence_end_time from './20260605_113107_occurrence_end_time';
|
|
||||||
import * as migration_20260605_124103_gallery_caption from './20260605_124103_gallery_caption';
|
|
||||||
import * as migration_20260605_130843_add_worship_language_other from './20260605_130843_add_worship_language_other';
|
|
||||||
|
|
||||||
export const migrations = [
|
export const migrations = [
|
||||||
{
|
{
|
||||||
|
|
@ -278,26 +274,6 @@ export const migrations = [
|
||||||
{
|
{
|
||||||
up: migration_20260504_070356_next_prev_buttons.up,
|
up: migration_20260504_070356_next_prev_buttons.up,
|
||||||
down: migration_20260504_070356_next_prev_buttons.down,
|
down: migration_20260504_070356_next_prev_buttons.down,
|
||||||
name: '20260504_070356_next_prev_buttons',
|
name: '20260504_070356_next_prev_buttons'
|
||||||
},
|
|
||||||
{
|
|
||||||
up: migration_20260605_095112.up,
|
|
||||||
down: migration_20260605_095112.down,
|
|
||||||
name: '20260605_095112',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
up: migration_20260605_113107_occurrence_end_time.up,
|
|
||||||
down: migration_20260605_113107_occurrence_end_time.down,
|
|
||||||
name: '20260605_113107_occurrence_end_time',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
up: migration_20260605_124103_gallery_caption.up,
|
|
||||||
down: migration_20260605_124103_gallery_caption.down,
|
|
||||||
name: '20260605_124103_gallery_caption',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
up: migration_20260605_130843_add_worship_language_other.up,
|
|
||||||
down: migration_20260605_130843_add_worship_language_other.down,
|
|
||||||
name: '20260605_130843_add_worship_language_other'
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import classNames from 'classnames'
|
||||||
type UpcomingOccurrence = {
|
type UpcomingOccurrence = {
|
||||||
id: string
|
id: string
|
||||||
date: string
|
date: string
|
||||||
end?: string
|
|
||||||
title: string
|
title: string
|
||||||
href: string
|
href: string
|
||||||
location: string
|
location: string
|
||||||
|
|
@ -205,7 +204,6 @@ export function EventPage(
|
||||||
<EventRow
|
<EventRow
|
||||||
key={o.id}
|
key={o.id}
|
||||||
date={o.date}
|
date={o.date}
|
||||||
end={o.end}
|
|
||||||
title={o.title}
|
title={o.title}
|
||||||
href={o.href}
|
href={o.href}
|
||||||
location={o.location}
|
location={o.location}
|
||||||
|
|
|
||||||
|
|
@ -337,7 +337,7 @@ export interface Church {
|
||||||
*/
|
*/
|
||||||
recurringSchedule?:
|
recurringSchedule?:
|
||||||
| {
|
| {
|
||||||
type: 'MASS' | 'FAMILY' | 'WORD' | 'LANGUAGE' | 'OTHER';
|
type: 'MASS' | 'FAMILY' | 'WORD';
|
||||||
frequency: 'weekly' | 'biweekly' | 'monthlyByWeekday';
|
frequency: 'weekly' | 'biweekly' | 'monthlyByWeekday';
|
||||||
day: 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday' | 'sunday';
|
day: 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday' | 'sunday';
|
||||||
time: string;
|
time: string;
|
||||||
|
|
@ -505,7 +505,6 @@ export interface Page {
|
||||||
| {
|
| {
|
||||||
items: {
|
items: {
|
||||||
photo: string | Media;
|
photo: string | Media;
|
||||||
caption?: string | null;
|
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
}[];
|
}[];
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
|
|
@ -676,12 +675,6 @@ export interface Page {
|
||||||
blockName?: string | null;
|
blockName?: string | null;
|
||||||
blockType: 'events';
|
blockType: 'events';
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
group: string | Group;
|
|
||||||
id?: string | null;
|
|
||||||
blockName?: string | null;
|
|
||||||
blockType: 'groupEvents';
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
contact: string | ContactPerson;
|
contact: string | ContactPerson;
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
|
|
@ -735,6 +728,20 @@ export interface Page {
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
_status?: ('draft' | 'published') | null;
|
_status?: ('draft' | 'published') | null;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "contactPerson".
|
||||||
|
*/
|
||||||
|
export interface ContactPerson {
|
||||||
|
id: string;
|
||||||
|
photo?: (string | null) | Media;
|
||||||
|
name: string;
|
||||||
|
role?: string | null;
|
||||||
|
email?: string | null;
|
||||||
|
telephone?: string | null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "group".
|
* via the `definition` "group".
|
||||||
|
|
@ -799,7 +806,6 @@ export interface Group {
|
||||||
| {
|
| {
|
||||||
items: {
|
items: {
|
||||||
photo: string | Media;
|
photo: string | Media;
|
||||||
caption?: string | null;
|
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
}[];
|
}[];
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
|
|
@ -905,20 +911,6 @@ export interface Group {
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
_status?: ('draft' | 'published') | null;
|
_status?: ('draft' | 'published') | null;
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
|
||||||
* via the `definition` "contactPerson".
|
|
||||||
*/
|
|
||||||
export interface ContactPerson {
|
|
||||||
id: string;
|
|
||||||
photo?: (string | null) | Media;
|
|
||||||
name: string;
|
|
||||||
role?: string | null;
|
|
||||||
email?: string | null;
|
|
||||||
telephone?: string | null;
|
|
||||||
updatedAt: string;
|
|
||||||
createdAt: string;
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "worship".
|
* via the `definition` "worship".
|
||||||
|
|
@ -927,7 +919,7 @@ export interface Worship {
|
||||||
id: string;
|
id: string;
|
||||||
date: string;
|
date: string;
|
||||||
location: string | Church;
|
location: string | Church;
|
||||||
type: 'MASS' | 'FAMILY' | 'WORD' | 'LANGUAGE' | 'OTHER';
|
type: 'MASS' | 'FAMILY' | 'WORD';
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
cancelled: boolean;
|
cancelled: boolean;
|
||||||
liturgicalDay?: string | null;
|
liturgicalDay?: string | null;
|
||||||
|
|
@ -1033,7 +1025,6 @@ export interface Blog {
|
||||||
| {
|
| {
|
||||||
items: {
|
items: {
|
||||||
photo: string | Media;
|
photo: string | Media;
|
||||||
caption?: string | null;
|
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
}[];
|
}[];
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
|
|
@ -1183,7 +1174,6 @@ export interface EventOccurrence {
|
||||||
id: string;
|
id: string;
|
||||||
event: string | Event;
|
event: string | Event;
|
||||||
date: string;
|
date: string;
|
||||||
endDateTime?: string | null;
|
|
||||||
cancelled: boolean;
|
cancelled: boolean;
|
||||||
generated?: boolean | null;
|
generated?: boolean | null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
|
@ -1834,7 +1824,6 @@ export interface BlogSelect<T extends boolean = true> {
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
photo?: T;
|
photo?: T;
|
||||||
caption?: T;
|
|
||||||
id?: T;
|
id?: T;
|
||||||
};
|
};
|
||||||
id?: T;
|
id?: T;
|
||||||
|
|
@ -1937,7 +1926,6 @@ export interface EventSelect<T extends boolean = true> {
|
||||||
export interface EventOccurrenceSelect<T extends boolean = true> {
|
export interface EventOccurrenceSelect<T extends boolean = true> {
|
||||||
event?: T;
|
event?: T;
|
||||||
date?: T;
|
date?: T;
|
||||||
endDateTime?: T;
|
|
||||||
cancelled?: T;
|
cancelled?: T;
|
||||||
generated?: T;
|
generated?: T;
|
||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
|
|
@ -2019,7 +2007,6 @@ export interface GroupSelect<T extends boolean = true> {
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
photo?: T;
|
photo?: T;
|
||||||
caption?: T;
|
|
||||||
id?: T;
|
id?: T;
|
||||||
};
|
};
|
||||||
id?: T;
|
id?: T;
|
||||||
|
|
@ -2179,7 +2166,6 @@ export interface PagesSelect<T extends boolean = true> {
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
photo?: T;
|
photo?: T;
|
||||||
caption?: T;
|
|
||||||
id?: T;
|
id?: T;
|
||||||
};
|
};
|
||||||
id?: T;
|
id?: T;
|
||||||
|
|
@ -2308,13 +2294,6 @@ export interface PagesSelect<T extends boolean = true> {
|
||||||
id?: T;
|
id?: T;
|
||||||
blockName?: T;
|
blockName?: T;
|
||||||
};
|
};
|
||||||
groupEvents?:
|
|
||||||
| T
|
|
||||||
| {
|
|
||||||
group?: T;
|
|
||||||
id?: T;
|
|
||||||
blockName?: T;
|
|
||||||
};
|
|
||||||
contactPersonBlock?:
|
contactPersonBlock?:
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import {
|
||||||
AlignFeature,
|
AlignFeature,
|
||||||
UnorderedListFeature,
|
UnorderedListFeature,
|
||||||
LinkFeature,
|
LinkFeature,
|
||||||
FixedToolbarFeature, UploadFeature,
|
FixedToolbarFeature,
|
||||||
} from '@payloadcms/richtext-lexical'
|
} from '@payloadcms/richtext-lexical'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { buildConfig } from 'payload'
|
import { buildConfig } from 'payload'
|
||||||
|
|
@ -142,10 +142,6 @@ export default buildConfig({
|
||||||
HeadingFeature({ enabledHeadingSizes: ["h3","h4","h5"]}),
|
HeadingFeature({ enabledHeadingSizes: ["h3","h4","h5"]}),
|
||||||
AlignFeature(),
|
AlignFeature(),
|
||||||
UnorderedListFeature(),
|
UnorderedListFeature(),
|
||||||
UploadFeature({
|
|
||||||
enabledCollections: ['media']
|
|
||||||
}
|
|
||||||
),
|
|
||||||
LinkFeature({
|
LinkFeature({
|
||||||
fields: ({ defaultFields }) => [
|
fields: ({ defaultFields }) => [
|
||||||
...defaultFields,
|
...defaultFields,
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
/**
|
|
||||||
* Extracts and encodes the user and domain parts of an email address.
|
|
||||||
*
|
|
||||||
* This function splits the given email string into two parts: the user (local-part) and the domain.
|
|
||||||
* Each part is then Base64 encoded and returned as an object.
|
|
||||||
*
|
|
||||||
* Used for obfuscating email addresses to prevent direct exposure.
|
|
||||||
*
|
|
||||||
* @param {string} email - The email address to be processed.
|
|
||||||
* @returns {{ user: string, domain: string }} An object containing the Base64 encoded user and domain parts of the email.
|
|
||||||
*/
|
|
||||||
export const extractEmailPartsEncoded = (email: string): { user: string, domain: string } => {
|
|
||||||
const [user, domain] = email.split('@');
|
|
||||||
return {
|
|
||||||
user: btoa(user),
|
|
||||||
domain: btoa(domain)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -41,7 +41,6 @@ export const eventToPageProps = (
|
||||||
type EventRowItem = {
|
type EventRowItem = {
|
||||||
id: string
|
id: string
|
||||||
date: string
|
date: string
|
||||||
end?: string
|
|
||||||
title: string
|
title: string
|
||||||
href: string
|
href: string
|
||||||
location: string
|
location: string
|
||||||
|
|
@ -52,14 +51,13 @@ export const transformOccurrences = (
|
||||||
occurrences: EventOccurrence[],
|
occurrences: EventOccurrence[],
|
||||||
): EventRowItem[] => {
|
): EventRowItem[] => {
|
||||||
return occurrences
|
return occurrences
|
||||||
.map((o): EventRowItem | undefined => {
|
.map((o) => {
|
||||||
const event = typeof o.event === 'object' ? o.event : undefined
|
const event = typeof o.event === 'object' ? o.event : undefined
|
||||||
if (!event) return undefined
|
if (!event) return undefined
|
||||||
return {
|
return {
|
||||||
id: o.id,
|
id: o.id,
|
||||||
title: event.title,
|
title: event.title,
|
||||||
date: o.date,
|
date: o.date,
|
||||||
end: o.endDateTime ?? undefined,
|
|
||||||
href: `/veranstaltungen/${event.id}/${o.id}`,
|
href: `/veranstaltungen/${event.id}/${o.id}`,
|
||||||
location: typeof event.location === 'object' ? event.location.name : 'Unbekannt',
|
location: typeof event.location === 'object' ? event.location.name : 'Unbekannt',
|
||||||
cancelled: Boolean(event.cancelled || o.cancelled),
|
cancelled: Boolean(event.cancelled || o.cancelled),
|
||||||
|
|
@ -77,7 +75,6 @@ export const transformEvents = (events: Event[]): EventRowItem[] => {
|
||||||
id: e.id,
|
id: e.id,
|
||||||
title: e.title,
|
title: e.title,
|
||||||
date: e.date,
|
date: e.date,
|
||||||
end: e.endDateTime ?? undefined,
|
|
||||||
href: `/veranstaltungen/${e.id}`,
|
href: `/veranstaltungen/${e.id}`,
|
||||||
location: typeof e.location === 'object' ? e.location.name : 'Unbekannt',
|
location: typeof e.location === 'object' ? e.location.name : 'Unbekannt',
|
||||||
cancelled: e.cancelled,
|
cancelled: e.cancelled,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import { StaticImageData } from 'next/image'
|
||||||
|
|
||||||
type Items = {
|
type Items = {
|
||||||
photo: string | Media;
|
photo: string | Media;
|
||||||
caption?: string | null;
|
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
|
|
@ -21,8 +20,7 @@ export const transformGallery = (items: Items) => {
|
||||||
alt: item.photo.alt,
|
alt: item.photo.alt,
|
||||||
id: item.id,
|
id: item.id,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
image,
|
image
|
||||||
caption: item.caption ?? undefined
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,14 @@ import { EventRowProps } from '@/components/EventRow/EventRow'
|
||||||
/**
|
/**
|
||||||
* Transform the worship category to a readable string
|
* Transform the worship category to a readable string
|
||||||
*/
|
*/
|
||||||
export const transformCategory = (category: "MASS" | "FAMILY" | "WORD" | "LANGUAGE" | "OTHER"): string => {
|
export const transformCategory = (category: "MASS" | "FAMILY" | "WORD"): string => {
|
||||||
const type = {
|
const type = {
|
||||||
'MASS': 'Eucharistiefeier',
|
'MASS': 'Eucharistiefeier',
|
||||||
'FAMILY': 'Familienmesse',
|
'FAMILY': 'Familienmesse',
|
||||||
'WORD': 'Wort-Gottes-Feier',
|
'WORD': 'Wort-Gottes-Feier',
|
||||||
'LANGUAGE': 'Eucharistiefeier in Fremdsprache',
|
|
||||||
'OTHER': 'Gottesdienst',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return type[category] ?? 'Gottesdienst'
|
return type[category]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue