feature: classifieds

This commit is contained in:
Benno Tielen 2025-03-22 15:03:24 +01:00
parent b4527cba8a
commit b89aa64688
15 changed files with 6338 additions and 15 deletions

View file

@ -260,6 +260,11 @@ export default function RootLayout({
title: 'Anpacken & Gutes tun',
description: 'Hilfe bei Kirchenreinigung und anderem',
},
{
href: '/mithelfen/kleinanzeigen',
title: 'Kleinanzeigen',
description: 'Gemeinsam erreichen wir mehr'
}
]
}
]

View file

@ -0,0 +1,46 @@
import { PageHeader } from '@/compositions/PageHeader/PageHeader'
import { Section } from '@/components/Section/Section'
import { Container } from '@/components/Container/Container'
import { ClassifiedsFromApi } from '@/components/Classifieds/ClassifiedsFromApi'
import { P } from '@/components/Text/Paragraph'
import { ContactSection } from '@/compositions/ContactSection/ContactSection'
import Link from 'next/link'
export const dynamic = 'force-dynamic'
export default function Page() {
return (
<>
<PageHeader
title={'Kleinanzeigen'}
description={'Du hast den Wunsch, dich einzubringen und gemeinsam etwas zu bewirken? Wunderbar! In unserer Gemeinde gibt es zahlreiche Möglichkeiten, deine Talente und Fähigkeiten sinnstiftend einzusetzen. Auf dieser Seite findest du aktuelle Kleinanzeigen für ehrenamtliche Tätigkeiten vielleicht ist ja genau das Richtige für dich dabei?'}
/>
<Section padding={'small'} paddingBottom={"medium"}>
<Container>
<ClassifiedsFromApi />
<br />
<h3>Etwas gefunden?</h3>
<P width={'1/2'}>
Sie haben ein Projekt entdeckt, das Ihr Interesse geweckt hat? Zögern Sie nicht, <Link href={"/kontakt"}>Kontakt</Link> aufzunehmen! Die
jeweiligen Ansprechpartner/innen stehen Ihnen gerne für Fragen zur Verfügung und freuen sich darauf, Sie
kennenzulernen.
</P>
<h3>Nichts dabei?</h3>
<P width={'1/2'}>
Kein Problem! Wir sind immer offen für neue Ideen und Initiativen. Wenn du eine Idee hast, wie du dich
einbringen kannst oder ein Projekt starten möchtest, melde dich einfach bei uns. Gemeinsam finden wir eine
Möglichkeit, deine Talente und Leidenschaften in unserer Gemeinde einzusetzen.
</P>
</Container>
</Section>
<ContactSection
backgroundColor={'off-white'}
title={'Noch Fragen?'}
description={'Wir sind für Sie da! Wenn Sie noch offene Fragen haben oder mehr über unsere ehrenamtlichen Möglichkeiten erfahren möchten, schreiben Sie uns einfach. Wir freuen uns darauf, von Ihnen zu hören!'} />
</>
)
}

View file

@ -4,6 +4,9 @@ import { Container } from '@/components/Container/Container'
import { Title } from '@/components/Title/Title'
import { P } from '@/components/Text/Paragraph'
import { DonationForm } from '@/components/DonationForm/DonationForm'
import { ClassifiedsFromApi } from '@/components/Classifieds/ClassifiedsFromApi'
export const dynamic = 'force-dynamic'
export default function Page() {
return (
@ -25,13 +28,9 @@ export default function Page() {
du unterstützt uns bei den Liturgischen Diensten. Auch bei der Organisation von Gemeinde-Events sind helfende Hände immer
willkommen. Vielleicht entdeckst du ja dein Talent für den Kirchenchor oder möchtest woanders finanziell unterstützen?
</P>
<P width={'1/2'}>
Und vergiss nicht die vielen Möglichkeiten, dich über die Gemeinde hinaus sozial zu engagieren. Ob in der
Flüchtlingshilfe, der Obdachlosenhilfe oder der Nachbarschaftshilfe es gibt viele Menschen, die deine
Unterstützung brauchen. Ist nichts dabei? Oder du hast Lust, selbst etwas auf die Beine zu stellen?
Sprich uns an!<br /><br />
</P>
<ClassifiedsFromApi />
<br/>
<Title
title={'Finanziell unterstützen'}
color={'contrast'}

View file

@ -4,7 +4,7 @@ import { P } from '@/components/Text/Paragraph'
import { Container } from '@/components/Container/Container'
import { ContactSection } from '@/compositions/ContactSection/ContactSection'
import Link from 'next/link'
import image from "./pentecost.webp"
import image from './pentecost.webp'
export default function Page() {
return (
@ -15,17 +15,23 @@ export default function Page() {
image={image}
/>
<Section padding={"small"} paddingBottom={"large"}>
<Section padding={'small'} paddingBottom={'large'}>
<Container>
<h3>Neugierig auf den Glauben?</h3>
<P width={'1/2'}>
Vielleicht stellen Sie sich Fragen über Gott, die Kirche oder den Sinn des Lebens. Bei uns
finden Sie Raum für Ihre Fragen und die Möglichkeit, den christlichen Glauben in seiner
Vielfalt zu entdecken. Schauen Sie doch einfach mal bei einem unserer Gottesdienste
vorbei, nehmen Sie gern an einem <Link href={"/gruppe/alphakurs"}>Glaubenskurs</Link> teil oder kontaktiere
vorbei, nehmen Sie gern an einem <Link href={'/gruppe/alphakurs'}>Glaubenskurs</Link> teil oder kontaktiere
Sie uns wir sind gerne für Sie da!
</P>
<h3>Wiedereintritt?</h3>
<P width={'1/2'}>
Sie waren bereits Mitglied der katholischen Kirche und möchten wieder eintreten? Auch dabei begleiten wir
Sie gerne. Wir informieren Sie über die notwendigen Schritte und stehen Ihnen bei allen Fragen zur Seite.
</P>
<h3>Schon katholisch?</h3>
<P width={'1/2'}>
Wir heißen Sie herzlich in unserer Gemeinde willkommen! Hier finden Sie eine lebendige Gemeinschaft, in der
@ -36,16 +42,19 @@ export default function Page() {
<h3>Unsere Angebote</h3>
<ul>
<li><strong>Gottesdienste:</strong> Feiern Sie mit uns die Eucharistie und erleben Sie die Gemeinschaft im Glauben.</li>
<li><strong>Gruppen:</strong> Finden Sie Gleichgesinnte und vertiefen Sie Ihren Glauben in Gemeinschaft.</li>
<li><strong>Gottesdienste:</strong> Feiern Sie mit uns die Eucharistie und erleben Sie die Gemeinschaft im
Glauben.
</li>
<li><strong>Gruppen:</strong> Finden Sie Gleichgesinnte und vertiefen Sie Ihren Glauben in Gemeinschaft.
</li>
</ul>
</Container>
</Section>
<ContactSection
backgroundColor={"off-white"}
title={"Mehr wissen?"}
description={"Zögern Sie nicht und nehmen Sie Kontakt mit uns auf! Wir freuen uns über Ihre Nachricht und sind gerne für Sie da. Füllen Sie einfach das Formular aus und wir melden uns schnellstmöglich bei Ihnen zurück."}
backgroundColor={'off-white'}
title={'Mehr wissen?'}
description={'Zögern Sie nicht und nehmen Sie Kontakt mit uns auf! Wir freuen uns über Ihre Nachricht und sind gerne für Sie da. Füllen Sie einfach das Formular aus und wir melden uns schnellstmöglich bei Ihnen zurück.'}
/>
</>
)

View file

@ -0,0 +1,74 @@
import {CollectionConfig} from 'payload'
import {
AlignFeature,
BoldFeature,
HeadingFeature, HTMLConverterFeature, InlineToolbarFeature,
ItalicFeature,
lexicalEditor, LinkFeature, ParagraphFeature,
UnderlineFeature, UnorderedListFeature,
} from '@payloadcms/richtext-lexical'
import { hide, isAdminOrEmployee } from '@/collections/access/admin'
let oneMonth = new Date();
oneMonth.setDate(oneMonth.getDate() + 30);
export const Classifieds: CollectionConfig = {
slug: 'classifieds',
labels: {
singular: {
de: 'Kleinanzeige'
},
plural: {
de: 'Kleinanzeigen'
}
},
fields: [
{
name: "until",
label: {
de: "Anzeigen bis"
},
type: 'date',
required: true,
defaultValue: oneMonth,
},
{
name: 'text',
type: 'richText',
label: {
de: "Anzeige"
},
editor: lexicalEditor( {
features: () => [
BoldFeature(),
ItalicFeature(),
UnderlineFeature(),
LinkFeature(),
InlineToolbarFeature(),
]
}),
required: true
},
{
name: 'email',
type: 'email',
label: {
de: "Kontakt E-mail"
},
required: true
}
],
admin: {
hidden: hide,
defaultColumns: ['until', 'text', 'email'],
description: {
de: "Dieser Bereich des Dashboards ermöglicht die umfassende Verwaltung aller veröffentlichten Kleinanzeigen für freiwillige Tätigkeiten. Hier können Administratoren Inserate einsehen, bearbeiten, veröffentlichen und entfernen, um die Qualität und Relevanz der angebotenen Möglichkeiten sicherzustellen."
}
},
access: {
read: () => true,
create: isAdminOrEmployee(),
update: isAdminOrEmployee(),
delete: isAdminOrEmployee(),
}
}

View file

@ -0,0 +1,44 @@
"use client"
import classNames from 'classnames'
import styles from '@/components/Classifieds/styles.module.scss'
import { faustina } from '@/assets/fonts'
import { useState } from 'react'
import { SerializedEditorState } from 'lexical'
import { RichText } from '@payloadcms/richtext-lexical/react'
type AdProps = {
text: SerializedEditorState,
contact: string
}
export const Ad = ({text, contact}: AdProps) => {
const [displayContact, setDisplayContact] = useState(false)
return (
<>
<div className={classNames(styles.ad, faustina.className)} onClick={() => setDisplayContact(!displayContact)}>
<div className={styles.adText}>
<RichText data={text} />
</div>
{!displayContact &&
<div className={styles.moreInfo} onClick={() => setDisplayContact(true)}>
Mehr wissen?
</div>
}
{displayContact &&
<div className={styles.adContact}>
Nehmen Sie Kontakt auf: <br/>
<strong>
<a
href={`mailto: ${contact}`}
onClick={e => e.stopPropagation()}
>{contact}</a>
</strong>
</div>
}
</div>
</>
)
}

View file

@ -0,0 +1,21 @@
import styles from "./styles.module.scss"
import { Ad } from "./Ad"
import { SerializedEditorState } from 'lexical'
type ClassifiedsProps = {
ads: Ad[]
}
type Ad = {
id: string,
text: SerializedEditorState,
email: string
}
export const Classifieds = ({ads}: ClassifiedsProps) => {
return (
<div className={styles.ads}>
{ads.map(ad => <Ad key={ad.id} text={ad.text} contact={ad.email} />)}
</div>
)
}

View file

@ -0,0 +1,12 @@
import { Classifieds } from '@/components/Classifieds/Classifieds'
import { fetchClassifieds } from '@/fetch/classifieds'
export const ClassifiedsFromApi = async () => {
const ads = await fetchClassifieds()
if (!ads) return null;
return (
<Classifieds ads={ads.docs} />
)
}

View file

@ -0,0 +1,66 @@
@import "template.scss";
.ads {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
background-color: $shade3;
padding: 1rem;
border: 2px solid $shade2;
}
.ad {
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: #fff;
border: 1px solid $shade1;
padding: 1rem;
transition: 200ms background-color;
}
.ad:hover {
background-color: #f3f3f3;
}
.adText {
cursor: pointer;
}
.moreInfo {
display: none;
}
.adContact {
text-align: right;
margin-top: 10px;
font-size: 16px;
animation: fadeIn 300ms ease-in-out;
}
.adContact a {
color: $base-color;
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@media screen and (max-width: 900px){
.moreInfo {
display: block;
padding: 10px;
color: #ffffff;
font-weight: bold;
font-size: 16px;
width: 110px;
text-align: center;
border-radius: $border-radius;
background-color: $shade1;
}
}

27
src/fetch/classifieds.ts Normal file
View file

@ -0,0 +1,27 @@
import { PaginatedDocs } from 'payload'
import { Classified } from '@/payload-types'
import { stringify } from 'qs-esm'
export const fetchClassifieds = async (): Promise<PaginatedDocs<Classified> | undefined> => {
const date = new Date();
date.setHours(0, 0, 0, 0);
const query = {
until: {
greater_than_equal: date.toISOString(),
}
}
const stringifiedQuery = stringify(
{
sort: "date",
where: query,
limit: 50
},
{ addQueryPrefix: true },
)
const response = await fetch(`http://localhost:3000/api/classifieds${stringifiedQuery}`)
if (!response.ok) return undefined
return response.json()
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,38 @@
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
await db.execute(sql`
CREATE TABLE IF NOT EXISTS "classifieds" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"until" timestamp(3) with time zone DEFAULT '2025-04-21T12:49:18.187Z' NOT NULL,
"text" jsonb NOT NULL,
"email" varchar NOT NULL,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);
ALTER TABLE "announcement" ALTER COLUMN "date" SET DEFAULT '2025-03-23T13:49:18.078Z';
ALTER TABLE "calendar" ALTER COLUMN "date" SET DEFAULT '2025-03-23T13:49:18.163Z';
ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "classifieds_id" uuid;
CREATE INDEX IF NOT EXISTS "classifieds_updated_at_idx" ON "classifieds" USING btree ("updated_at");
CREATE INDEX IF NOT EXISTS "classifieds_created_at_idx" ON "classifieds" USING btree ("created_at");
DO $$ BEGIN
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_classifieds_fk" FOREIGN KEY ("classifieds_id") REFERENCES "public"."classifieds"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_classifieds_id_idx" ON "payload_locked_documents_rels" USING btree ("classifieds_id");`)
}
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
await db.execute(sql`
ALTER TABLE "classifieds" DISABLE ROW LEVEL SECURITY;
DROP TABLE "classifieds" CASCADE;
ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT "payload_locked_documents_rels_classifieds_fk";
DROP INDEX IF EXISTS "payload_locked_documents_rels_classifieds_id_idx";
ALTER TABLE "announcement" ALTER COLUMN "date" SET DEFAULT '2025-03-23T10:13:37.253Z';
ALTER TABLE "calendar" ALTER COLUMN "date" SET DEFAULT '2025-03-23T10:13:37.343Z';
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN IF EXISTS "classifieds_id";`)
}

View file

@ -5,6 +5,7 @@ import * as migration_20250128_145145_liturgical_calendar from './20250128_14514
import * as migration_20250202_104742 from './20250202_104742';
import * as migration_20250224_083653_cleanup from './20250224_083653_cleanup';
import * as migration_20250319_101337_donationbox from './20250319_101337_donationbox';
import * as migration_20250322_134918_classifieds from './20250322_134918_classifieds';
export const migrations = [
{
@ -40,6 +41,11 @@ export const migrations = [
{
up: migration_20250319_101337_donationbox.up,
down: migration_20250319_101337_donationbox.down,
name: '20250319_101337_donationbox'
name: '20250319_101337_donationbox',
},
{
up: migration_20250322_134918_classifieds.up,
down: migration_20250322_134918_classifieds.down,
name: '20250322_134918_classifieds'
},
];

View file

@ -20,6 +20,7 @@ export interface Config {
blog: Blog;
highlight: Highlight;
event: Event;
classifieds: Classified;
contactPerson: ContactPerson;
locations: Location;
group: Group;
@ -41,6 +42,7 @@ export interface Config {
blog: BlogSelect<false> | BlogSelect<true>;
highlight: HighlightSelect<false> | HighlightSelect<true>;
event: EventSelect<false> | EventSelect<true>;
classifieds: ClassifiedsSelect<false> | ClassifiedsSelect<true>;
contactPerson: ContactPersonSelect<false> | ContactPersonSelect<true>;
locations: LocationsSelect<false> | LocationsSelect<true>;
group: GroupSelect<false> | GroupSelect<true>;
@ -480,6 +482,34 @@ export interface ContactPerson {
updatedAt: string;
createdAt: string;
}
/**
* Dieser Bereich des Dashboards ermöglicht die umfassende Verwaltung aller veröffentlichten Kleinanzeigen für freiwillige Tätigkeiten. Hier können Administratoren Inserate einsehen, bearbeiten, veröffentlichen und entfernen, um die Qualität und Relevanz der angebotenen Möglichkeiten sicherzustellen.
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "classifieds".
*/
export interface Classified {
id: string;
until: string;
text: {
root: {
type: string;
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
};
email: string;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
@ -543,6 +573,10 @@ export interface PayloadLockedDocument {
relationTo: 'event';
value: string | Event;
} | null)
| ({
relationTo: 'classifieds';
value: string | Classified;
} | null)
| ({
relationTo: 'contactPerson';
value: string | ContactPerson;
@ -791,6 +825,17 @@ export interface EventSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "classifieds_select".
*/
export interface ClassifiedsSelect<T extends boolean = true> {
until?: T;
text?: T;
email?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "contactPerson_select".

View file

@ -35,6 +35,7 @@ import { postgresAdapter } from '@payloadcms/db-postgres'
import { gcsStorage } from '@payloadcms/storage-gcs'
import { PopesPrayerIntentions } from '@/collections/PopesPrayerIntentions'
import { LiturgicalCalendar } from '@/collections/LiturgicalCalendar'
import { Classifieds } from '@/collections/Classifieds'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
@ -83,6 +84,7 @@ export default buildConfig({
Blog,
Highlight,
Events,
Classifieds,
ContactPerson,
Locations,
Groups,