feature: map

This commit is contained in:
Benno Tielen 2026-04-29 14:21:44 +02:00
parent c0fe32f9f5
commit 4766098296
15 changed files with 27065 additions and 230 deletions

View file

@ -17,6 +17,7 @@ import { HorizontalRuleBlock } from '@/collections/blocks/HorizontalRule'
import { BlogSliderBlock } from '@/collections/blocks/BlogSlider'
import { MassTimesBlock } from '@/collections/blocks/MassTimes'
import { CollapsibleImageWithTextBlock } from '@/collections/blocks/CollapsibleImageWithText'
import { CollapsibleMapWithTextBlock } from '@/collections/blocks/CollapsibleMapWithText'
import { CollapsiblesBlock } from '@/collections/blocks/Collapsibles'
import { EventsBlock } from '@/collections/blocks/Events'
import { PublicationAndNewsletterBlock } from '@/collections/blocks/PublicationAndNewsletter'
@ -101,6 +102,7 @@ export const Pages: CollectionConfig = {
HorizontalRuleBlock,
BlogSliderBlock,
CollapsibleImageWithTextBlock,
CollapsibleMapWithTextBlock,
CollapsiblesBlock,
MassTimesBlock,
EventsBlock,

View file

@ -0,0 +1,53 @@
import { Block } from 'payload'
export const CollapsibleMapWithTextBlock: Block = {
slug: 'collapsibleMapWithText',
labels: {
singular: {
de: 'Aufklappbarer Kartentext',
},
plural: {
de: 'Aufklappbare Kartentexte',
},
},
dbName: 'collaps_map',
fields: [
{
name: 'title',
type: 'text',
required: true,
label: {
de: 'Titel',
},
},
{
name: 'text',
type: 'textarea',
required: true,
label: {
de: 'Text',
},
},
{
name: 'content',
type: 'richText',
required: true,
label: {
de: 'Aufklappbarer Inhalt',
},
},
{
name: 'backgroundColor',
type: 'select',
label: {
de: 'Hintergrundfarbe',
},
options: [
{ label: 'Standard', value: 'default' },
{ label: 'Soft', value: 'soft' },
{ label: 'Off-White', value: 'off-white' },
],
defaultValue: 'default',
},
],
}

View file

@ -16,25 +16,3 @@ type Story = StoryObj<typeof ChemnitzMap>
export default meta
export const Default: Story = {}
export const WithClickHandler: Story = {
args: {
onChurchClick: (church) => {
// eslint-disable-next-line no-alert
alert(`Clicked: ${church.replace(/_/g, ' ')}`)
},
},
}
export const CustomUrls: Story = {
args: {
churchUrls: {
'St._Marien': '/custom/marien',
'St._Joseph': '/custom/joseph',
},
onChurchClick: (church) => {
// eslint-disable-next-line no-console
console.log('Would navigate for', church)
},
},
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 145 KiB

View file

@ -3,21 +3,12 @@
:global(svg) {
width: 100%;
height: auto;
height: 100%;
display: block;
}
}
.fill {
height: 100%;
:global(svg) {
width: 100%;
height: 100%;
}
}
.church {
.parish {
cursor: pointer;
transform-box: fill-box;
transform-origin: center;
@ -27,15 +18,9 @@
transition: fill 0.3s ease;
}
&:hover {
transform: scale(1.15);
:global(path.cls-1) {
fill: #0f6898;
}
}
&:hover,
&:focus-visible {
transform: scale(1.15);
outline: none;
:global(path.cls-1) {

View file

@ -14,6 +14,7 @@ import { Banner } from '@/components/Banner/Banner'
import { MainText } from '@/components/MainText/MainText'
import { HR } from '@/components/HorizontalRule/HorizontalRule'
import { CollapsibleImageWithText } from '@/compositions/CollapsibleImageWithText/CollapsibleImageWithText'
import { CollapsibleMapWithText } from '@/compositions/CollapsibleMapWithText/CollapsibleMapWithText'
import { Collapsible } from '@/components/Collapsible/Collapsible'
import { PublicationAndNewsletter } from '@/compositions/PublicationAndNewsletter/PublicationAndNewsletter'
import { ContactPersonCard } from '@/components/ContactPersonCard/ContactPersonCard'
@ -229,6 +230,25 @@ export function Blocks({ content }: BlocksProps) {
)
}
if (item.blockType === 'collapsibleMapWithText') {
const bg = item.backgroundColor === 'soft' || item.backgroundColor === 'off-white'
? item.backgroundColor as 'soft' | 'off-white'
: undefined
return (
<CollapsibleMapWithText
key={item.id}
title={item.title}
text={item.text}
backgroundColor={bg}
content={
item.content
? <HTMLText width={'3/4'} data={item.content} />
: <></>
}
/>
)
}
if (item.blockType === 'collapsibles') {
return (
<Section key={item.id} padding={'small'}>

View file

@ -13,16 +13,11 @@ export default meta
export const OurParishes: Story = {
args: {
backgroundColor: 'soft',
schema: 'base',
title: 'Unsere Gemeinden',
text:
'Unsere Pfarrei umfasst sieben Kirchen in und um Chemnitz. Klicken Sie auf eine Kirche, um mehr über die jeweilige Gemeinde zu erfahren von Gottesdienstzeiten über Gruppen und Kreise bis hin zu Kontaktmöglichkeiten.\n' +
'\n' +
'Jede Gemeinde hat ihren eigenen Charakter und ihre eigene Geschichte, gemeinsam bilden sie ein lebendiges katholisches Netzwerk in der Region.',
onChurchClick: (church) => {
// eslint-disable-next-line no-alert
alert(`Klick: ${church.replace(/_/g, ' ')}`)
},
content: (
<>
<Title title={'Über unsere Pfarrei'} size={'md'} />
@ -36,3 +31,14 @@ export const OurParishes: Story = {
),
},
}
export const NoExtraContent: Story = {
args: {
backgroundColor: 'soft',
title: 'Unsere Gemeinden',
text:
'Unsere Pfarrei umfasst sieben Kirchen in und um Chemnitz. Klicken Sie auf eine Kirche, um mehr über die jeweilige Gemeinde zu erfahren von Gottesdienstzeiten über Gruppen und Kreise bis hin zu Kontaktmöglichkeiten.\n' +
'\n' +
'Jede Gemeinde hat ihren eigenen Charakter und ihre eigene Geschichte, gemeinsam bilden sie ein lebendiges katholisches Netzwerk in der Region.',
},
}

View file

@ -1,24 +1,20 @@
'use client'
import { BackgroundColor, Section } from '@/components/Section/Section'
import { ChemnitzMap } from '@/components/ChemnitzMap/ChemnitzMap'
import { Title } from '@/components/Title/Title'
import styles from './styles.module.scss'
import { Container } from '@/components/Container/Container'
import { TextDiv } from '@/components/Text/TextDiv'
import { Row } from '@/components/Flex/Row'
import { P } from '@/components/Text/Paragraph'
import { CollapsibleArrow } from '@/components/CollapsibleArrow/CollapsibleArrow'
import { ChemnitzMap, ChemnitzMapProps } from '@/components/ChemnitzMap/ChemnitzMap'
import React, { useEffect, useRef, useState } from 'react'
import classNames from 'classnames'
import styles from './styles.module.scss'
type CollapsibleMapWithTextProps = {
backgroundColor?: BackgroundColor
title: string
text: string
schema?: 'base' | 'contrast'
content: React.ReactNode
churchUrls?: ChemnitzMapProps['churchUrls']
onChurchClick?: ChemnitzMapProps['onChurchClick']
content?: React.ReactNode
}
type MoreInformationProps = {
@ -30,7 +26,7 @@ const MoreInformation = ({ isCollapsed, onClick }: MoreInformationProps) => {
const [direction, setDirection] = useState<'UP' | 'DOWN'>(isCollapsed ? 'DOWN' : 'UP')
const toggleDirection = () => {
setDirection(direction === 'UP' ? 'DOWN' : 'UP')
setDirection(direction == 'UP' ? 'DOWN' : 'UP')
}
const handleClick = () => {
@ -50,15 +46,12 @@ const MoreInformation = ({ isCollapsed, onClick }: MoreInformationProps) => {
)
}
export const CollapsibleMapWithText = ({
backgroundColor,
export function CollapsibleMapWithText({
title,
text,
schema = 'base',
content,
churchUrls,
onChurchClick,
}: CollapsibleMapWithTextProps) => {
backgroundColor,
}: CollapsibleMapWithTextProps) {
const ref = useRef<HTMLDivElement>(null)
const ref2 = useRef<HTMLDivElement>(null)
const [contentHeight, setContentHeight] = useState(0)
@ -69,6 +62,18 @@ export const CollapsibleMapWithText = ({
ref.current?.scrollIntoView({ behavior: 'smooth' })
}
const toggle = () => {
if (isCollapsed) {
setIsCollapsed(false)
setTimeout(() => {
ref2.current?.scrollIntoView({ behavior: 'smooth'})
}, 500)
} else {
collapse()
}
}
useEffect(() => {
if (ref2.current) {
setContentHeight(ref2.current.scrollHeight)
@ -76,41 +81,30 @@ export const CollapsibleMapWithText = ({
}, [ref2])
return (
<div ref={ref} className={styles.root}>
<div ref={ref}>
<Section backgroundColor={backgroundColor}>
<Container>
<Row alignItems={'center'}>
<div className={classNames(styles.col, styles.imageCol)} />
<div className={styles.col}>
<Title title={title} size={'lg'} color={schema} />
<TextDiv text={text} />
<div className={styles.right}>
<MoreInformation
isCollapsed={isCollapsed}
onClick={() => setIsCollapsed(!isCollapsed)}
/>
</div>
</div>
</Row>
<Title title={title} size={'lg'} color={'base'} />
</Container>
<div className={styles.mapContainer}>
<ChemnitzMap/>
</div>
<Container>
<P width={'3/4'}>{text}</P>
{content && <MoreInformation isCollapsed={isCollapsed} onClick={toggle} />}
</Container>
</Section>
<div className={styles.mapCorner}>
<ChemnitzMap churchUrls={churchUrls} onChurchClick={onChurchClick} />
</div>
{content && (
<div
ref={ref2}
className={styles.content}
className={classNames({ [styles.content]: true })}
style={{ maxHeight: isCollapsed ? undefined : contentHeight }}
>
<Section
backgroundColor={backgroundColor}
padding={'small'}
paddingBottom={'large'}
>
<Section backgroundColor={backgroundColor} padding={'small'} paddingBottom={'large'}>
<Container>{content}</Container>
<div className={styles.endButton}>
@ -118,6 +112,7 @@ export const CollapsibleMapWithText = ({
</div>
</Section>
</div>
)}
</div>
)
}

View file

@ -1,42 +1,15 @@
@import "template.scss";
.root {
position: relative;
$maxWidth: 1100px;
.mapContainer {
transition: background-color 0.2s ease-in-out;
max-width: $maxWidth;
margin: 0 auto 40px auto;
}
.mapCorner {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
pointer-events: none;
opacity: 0.5;
:global(svg) {
pointer-events: auto;
}
}
.right {
margin-top: 40px;
text-align: right;
z-index: 3;
}
.image {
border-radius: 13px;
transition: opacity 3s;
width: 100%;
height: 100%;
}
.col {
width: calc(50% - 40px);
}
.imageCol {
/* Empty column — space reserved for the map positioned in the upper-left corner. */
.mapContainer:hover {
background-color: #fbfdff;
}
.more {
@ -49,6 +22,8 @@
border: 0;
background-color: inherit;
align-items: center;
padding: 0;
margin-top: 20px;
}
.content {
@ -67,22 +42,6 @@
cursor: pointer;
}
@media screen and (max-width: 750px) {
.imageCol {
display: none;
}
.col {
width: 100%;
}
.mapCorner {
position: static;
width: 70vw;
margin: 0 auto 40px;
}
}
@media screen and (max-width: 576px) {
.endButton {
top: 20px;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,56 @@
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
await db.execute(sql`
CREATE TYPE "public"."enum_collaps_map_background_color" AS ENUM('default', 'soft', 'off-white');
CREATE TYPE "public"."enum__collaps_map_v_background_color" AS ENUM('default', 'soft', 'off-white');
CREATE TABLE "collaps_map" (
"_order" integer NOT NULL,
"_parent_id" uuid NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"title" varchar,
"text" varchar,
"content" jsonb,
"background_color" "enum_collaps_map_background_color" DEFAULT 'default',
"block_name" varchar
);
CREATE TABLE "_collaps_map_v" (
"_order" integer NOT NULL,
"_parent_id" uuid NOT NULL,
"_path" text NOT NULL,
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"title" varchar,
"text" varchar,
"content" jsonb,
"background_color" "enum__collaps_map_v_background_color" DEFAULT 'default',
"_uuid" varchar,
"block_name" varchar
);
ALTER TABLE "announcement" ALTER COLUMN "date" SET DEFAULT '2026-05-03T12:11:04.492Z';
ALTER TABLE "calendar" ALTER COLUMN "date" SET DEFAULT '2026-05-03T12:11:04.791Z';
ALTER TABLE "classifieds" ALTER COLUMN "until" SET DEFAULT '2026-05-29T12:11:04.854Z';
ALTER TABLE "collaps_map" ADD CONSTRAINT "collaps_map_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_collaps_map_v" ADD CONSTRAINT "_collaps_map_v_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_pages_v"("id") ON DELETE cascade ON UPDATE no action;
CREATE INDEX "collaps_map_order_idx" ON "collaps_map" USING btree ("_order");
CREATE INDEX "collaps_map_parent_id_idx" ON "collaps_map" USING btree ("_parent_id");
CREATE INDEX "collaps_map_path_idx" ON "collaps_map" USING btree ("_path");
CREATE INDEX "_collaps_map_v_order_idx" ON "_collaps_map_v" USING btree ("_order");
CREATE INDEX "_collaps_map_v_parent_id_idx" ON "_collaps_map_v" USING btree ("_parent_id");
CREATE INDEX "_collaps_map_v_path_idx" ON "_collaps_map_v" USING btree ("_path");`)
}
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
await db.execute(sql`
ALTER TABLE "collaps_map" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "_collaps_map_v" DISABLE ROW LEVEL SECURITY;
DROP TABLE "collaps_map" CASCADE;
DROP TABLE "_collaps_map_v" CASCADE;
ALTER TABLE "announcement" ALTER COLUMN "date" SET DEFAULT '2026-04-26T13:12:25.662Z';
ALTER TABLE "calendar" ALTER COLUMN "date" SET DEFAULT '2026-04-26T13:12:25.946Z';
ALTER TABLE "classifieds" ALTER COLUMN "until" SET DEFAULT '2026-05-23T13:12:26.003Z';
DROP TYPE "public"."enum_collaps_map_background_color";
DROP TYPE "public"."enum__collaps_map_v_background_color";`)
}

View file

@ -42,6 +42,7 @@ import * as migration_20260417_111855_event_occurrences from './20260417_111855_
import * as migration_20260417_114727_simplify_recurring_events from './20260417_114727_simplify_recurring_events';
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_20260429_121105_collapsible_map_with_text from './20260429_121105_collapsible_map_with_text';
export const migrations = [
{
@ -262,6 +263,11 @@ export const migrations = [
{
up: migration_20260423_131226_add_event_end_date_time.up,
down: migration_20260423_131226_add_event_end_date_time.down,
name: '20260423_131226_add_event_end_date_time'
name: '20260423_131226_add_event_end_date_time',
},
{
up: migration_20260429_121105_collapsible_map_with_text.up,
down: migration_20260429_121105_collapsible_map_with_text.down,
name: '20260429_121105_collapsible_map_with_text'
},
];

View file

@ -597,6 +597,29 @@ export interface Page {
blockName?: string | null;
blockType: 'collapsibleImageWithText';
}
| {
title: string;
text: string;
content: {
root: {
type: string;
children: {
type: any;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
};
backgroundColor?: ('default' | 'soft' | 'off-white') | null;
id?: string | null;
blockName?: string | null;
blockType: 'collapsibleMapWithText';
}
| {
items: {
title: string;
@ -2169,6 +2192,16 @@ export interface PagesSelect<T extends boolean = true> {
id?: T;
blockName?: T;
};
collapsibleMapWithText?:
| T
| {
title?: T;
text?: T;
content?: T;
backgroundColor?: T;
id?: T;
blockName?: T;
};
collapsibles?:
| T
| {