Compare commits

..

4 commits

Author SHA1 Message Date
Benno Tielen
c605420dcb feature: classifieds block
Some checks are pending
Deploy / deploy (push) Waiting to run
2026-04-13 14:28:50 +02:00
Benno Tielen
93625f753b fix: contact 2026-04-13 14:10:54 +02:00
Benno Tielen
5f78e19f07 fix: colors 2026-04-13 14:07:17 +02:00
Benno Tielen
6416d74125 feature: search 2026-04-13 14:01:10 +02:00
28 changed files with 65297 additions and 6 deletions

16
package-lock.json generated
View file

@ -13,6 +13,7 @@
"@payloadcms/db-postgres": "^3.74.0",
"@payloadcms/live-preview-react": "^3.74.0",
"@payloadcms/next": "^3.74.0",
"@payloadcms/plugin-search": "^3.74.0",
"@payloadcms/richtext-lexical": "^3.74.0",
"@payloadcms/storage-gcs": "^3.74.0",
"classnames": "^2.5.1",
@ -2011,6 +2012,21 @@
"react-dom": "^19.0.1 || ^19.1.2 || ^19.2.1"
}
},
"node_modules/@payloadcms/plugin-search": {
"version": "3.74.0",
"resolved": "https://registry.npmjs.org/@payloadcms/plugin-search/-/plugin-search-3.74.0.tgz",
"integrity": "sha512-LFExPKtv3bQPsRQ6LZ4IRUYFPs2bsvcT6ctjqwU1hp3o6+Ily9ltZcA19+Bu6gBw4DvWKHlczVgotmAewGAiPw==",
"license": "MIT",
"dependencies": {
"@payloadcms/next": "3.74.0",
"@payloadcms/ui": "3.74.0"
},
"peerDependencies": {
"payload": "3.74.0",
"react": "^19.0.1 || ^19.1.2 || ^19.2.1",
"react-dom": "^19.0.1 || ^19.1.2 || ^19.2.1"
}
},
"node_modules/@payloadcms/richtext-lexical": {
"version": "3.74.0",
"license": "MIT",

View file

@ -24,6 +24,7 @@
"@payloadcms/db-postgres": "^3.74.0",
"@payloadcms/live-preview-react": "^3.74.0",
"@payloadcms/next": "^3.74.0",
"@payloadcms/plugin-search": "^3.74.0",
"@payloadcms/richtext-lexical": "^3.74.0",
"@payloadcms/storage-gcs": "^3.74.0",
"classnames": "^2.5.1",

View file

@ -0,0 +1,15 @@
import { fetchSearchResults } from '@/fetch/search'
import { SearchPage } from '@/pageComponents/Search/SearchPage'
export const dynamic = 'force-dynamic'
export default async function Page({
searchParams,
}: {
searchParams: Promise<{ q?: string }>
}) {
const { q = '' } = await searchParams
const results = await fetchSearchResults(q)
return <SearchPage query={q} results={results} />
}

View file

@ -12,6 +12,8 @@ import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1e
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { LinkToDoc as LinkToDoc_aead06e4cbf6b2620c5c51c9ab283634 } from '@payloadcms/plugin-search/client'
import { ReindexButton as ReindexButton_aead06e4cbf6b2620c5c51c9ab283634 } from '@payloadcms/plugin-search/client'
import { default as default_9bcae99938dc292be0063ce32055e14c } from '../../../components/Logo/Logo'
import { GcsClientUploadHandler as GcsClientUploadHandler_06e62ca02c7c441053a9b643e5545934 } from '@payloadcms/storage-gcs/client'
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
@ -31,6 +33,8 @@ export const importMap = {
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/plugin-search/client#LinkToDoc": LinkToDoc_aead06e4cbf6b2620c5c51c9ab283634,
"@payloadcms/plugin-search/client#ReindexButton": ReindexButton_aead06e4cbf6b2620c5c51c9ab283634,
"/components/Logo/Logo#default": default_9bcae99938dc292be0063ce32055e14c,
"@payloadcms/storage-gcs/client#GcsClientUploadHandler": GcsClientUploadHandler_06e62ca02c7c441053a9b643e5545934,
"@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1

View file

@ -21,6 +21,7 @@ import { EventsBlock } from '@/collections/blocks/Events'
import { PublicationAndNewsletterBlock } from '@/collections/blocks/PublicationAndNewsletter'
import { ContactPersonBlock } from '@/collections/blocks/ContactPersonBlock'
import { ImageCardsBlock } from '@/collections/blocks/ImageCards'
import { ClassifiedsBlock } from '@/collections/blocks/Classifieds'
import { isPublishedPublic } from '@/collections/access/public'
export const Pages: CollectionConfig = {
@ -102,6 +103,7 @@ export const Pages: CollectionConfig = {
EventsBlock,
ContactPersonBlock,
ImageCardsBlock,
ClassifiedsBlock,
],
},
],

View file

@ -65,7 +65,7 @@ export const Parish: CollectionConfig = {
{
name: 'contactPersons',
label: {
de: "Ansprechpartner"
de: "Kontakt"
},
type: 'array',
fields: [

View file

@ -0,0 +1,14 @@
import { Block } from 'payload'
export const ClassifiedsBlock: Block = {
slug: 'classifieds',
labels: {
singular: {
de: 'Kleinanzeigen',
},
plural: {
de: 'Kleinanzeigen',
},
},
fields: [],
}

View file

@ -27,7 +27,7 @@
.shade {
background-color: $shade2;
color: $base-color;
color: $base-color !important;
}
.shade:hover {

View file

@ -71,7 +71,7 @@
line-height: 95%;
}
@media screen and (max-width: 1100px) {
@media screen and (max-width: 1400px) {
.menu {
flex-direction: column;
gap: 30px;

View file

@ -11,6 +11,7 @@ import { MegaMenu } from '@/components/MegaMenu/MegaMenu'
import { CollapsibleArrow } from '@/components/CollapsibleArrow/CollapsibleArrow'
import Link from 'next/link'
import { siteConfig } from '@/config/site'
import { MenuSearch } from './MenuSearch'
/**
* Represents a simple item component.
@ -180,6 +181,7 @@ export const Menu = ({menu}: MenuProps) => {
items={menu.rightItems}
onItemClick={() => setDisplayMenuMobile(false)}
/>
<MenuSearch onSubmitted={() => setDisplayMenuMobile(false)} />
</div>
</nav>

View file

@ -0,0 +1,41 @@
'use client'
import { FormEvent, useState } from 'react'
import { useRouter } from 'next/navigation'
import Image from 'next/image'
import SearchIcon from './search.svg'
import styles from './styles.module.scss'
type MenuSearchProps = {
onSubmitted?: () => void
}
export const MenuSearch = ({ onSubmitted }: MenuSearchProps) => {
const router = useRouter()
const [value, setValue] = useState('')
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
const q = value.trim()
if (!q) return
router.push(`/suche?q=${encodeURIComponent(q)}`)
onSubmitted?.()
}
return (
<form className={styles.search} onSubmit={handleSubmit} role="search">
<input
type="search"
name="q"
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Suchen..."
aria-label="Suche"
className={styles.searchInput}
/>
<button type="submit" className={styles.searchButton} aria-label="Suchen">
<Image src={SearchIcon} width={20} height={20} alt="" />
</button>
</form>
)
}

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="11" cy="11" r="7" stroke="#333333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M20 20L16.65 16.65" stroke="#333333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 367 B

View file

@ -80,7 +80,46 @@
max-height: 1000px;
}
@media screen and (max-width: 1100px) {
.search {
display: flex;
align-items: center;
}
.searchInput {
width: 0;
padding: 0;
border: none;
border-bottom: 1px solid transparent;
background: transparent;
color: inherit;
font: inherit;
outline: none;
box-sizing: border-box;
transition: width 0.3s ease-in-out, padding 0.3s ease-in-out, border-color 0.3s ease-in-out;
&::-webkit-search-cancel-button {
-webkit-appearance: none;
}
}
.search:hover .searchInput,
.search:focus-within .searchInput {
width: 180px;
padding: 4px 8px;
border-bottom-color: $shade1;
}
.searchButton {
background: none;
border: none;
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
color: inherit;
}
@media screen and (max-width: 1400px) {
.nav {
flex-direction: column;
padding: 15px 15px;
@ -116,4 +155,16 @@
height: 100vh;
overflow: scroll;
}
.search {
width: 100%;
}
.searchInput,
.search:hover .searchInput,
.search:focus-within .searchInput {
width: 100%;
padding: 8px;
border-bottom-color: $border-color-light;
}
}

View file

@ -21,6 +21,7 @@ import { BlogSliderBlock } from '@/compositions/Blocks/BlogSliderBlock'
import { MassTimesBlock } from '@/compositions/Blocks/MassTimesBlock'
import { EventsBlock } from '@/compositions/Blocks/EventsBlock'
import { ImageCardsBlock } from '@/compositions/Blocks/ImageCardsBlock'
import { ClassifiedsFromApi } from '@/components/Classifieds/ClassifiedsFromApi'
type BlocksProps = {
content: Blog['content']['content'] | NonNullable<Page['content']>
@ -239,6 +240,16 @@ export function Blocks({ content }: BlocksProps) {
return <ImageCardsBlock key={item.id} items={item.items} />
}
if (item.blockType === 'classifieds') {
return (
<Section key={item.id} padding={'small'}>
<Container>
<ClassifiedsFromApi />
</Container>
</Section>
)
}
if (item.blockType === 'contactPersonBlock') {
const contact = typeof item.contact === 'object'
? item.contact

23
src/fetch/search.ts Normal file
View file

@ -0,0 +1,23 @@
import { getPayload } from 'payload'
import config from '@/payload.config'
import { Search } from '@/payload-types'
export async function fetchSearchResults(query: string): Promise<Search[]> {
const trimmed = query.trim()
if (!trimmed) return []
const payload = await getPayload({ config })
const result = await payload.find({
collection: 'search',
where: {
title: {
contains: trimmed,
},
},
depth: 1,
limit: 50,
sort: 'priority',
})
return result.docs
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,59 @@
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
await db.execute(sql`
CREATE TABLE "search" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"title" varchar,
"priority" numeric,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);
CREATE TABLE "search_rels" (
"id" serial PRIMARY KEY NOT NULL,
"order" integer,
"parent_id" uuid NOT NULL,
"path" varchar NOT NULL,
"parish_id" uuid,
"blog_id" uuid,
"event_id" uuid,
"group_id" uuid
);
ALTER TABLE "announcement" ALTER COLUMN "date" SET DEFAULT '2026-04-19T09:34:10.248Z';
ALTER TABLE "calendar" ALTER COLUMN "date" SET DEFAULT '2026-04-19T09:34:10.532Z';
ALTER TABLE "classifieds" ALTER COLUMN "until" SET DEFAULT '2026-05-13T09:34:10.591Z';
ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "search_id" uuid;
ALTER TABLE "search_rels" ADD CONSTRAINT "search_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."search"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "search_rels" ADD CONSTRAINT "search_rels_parish_fk" FOREIGN KEY ("parish_id") REFERENCES "public"."parish"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "search_rels" ADD CONSTRAINT "search_rels_blog_fk" FOREIGN KEY ("blog_id") REFERENCES "public"."blog"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "search_rels" ADD CONSTRAINT "search_rels_event_fk" FOREIGN KEY ("event_id") REFERENCES "public"."event"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "search_rels" ADD CONSTRAINT "search_rels_group_fk" FOREIGN KEY ("group_id") REFERENCES "public"."group"("id") ON DELETE cascade ON UPDATE no action;
CREATE INDEX "search_updated_at_idx" ON "search" USING btree ("updated_at");
CREATE INDEX "search_created_at_idx" ON "search" USING btree ("created_at");
CREATE INDEX "search_rels_order_idx" ON "search_rels" USING btree ("order");
CREATE INDEX "search_rels_parent_idx" ON "search_rels" USING btree ("parent_id");
CREATE INDEX "search_rels_path_idx" ON "search_rels" USING btree ("path");
CREATE INDEX "search_rels_parish_id_idx" ON "search_rels" USING btree ("parish_id");
CREATE INDEX "search_rels_blog_id_idx" ON "search_rels" USING btree ("blog_id");
CREATE INDEX "search_rels_event_id_idx" ON "search_rels" USING btree ("event_id");
CREATE INDEX "search_rels_group_id_idx" ON "search_rels" USING btree ("group_id");
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_search_fk" FOREIGN KEY ("search_id") REFERENCES "public"."search"("id") ON DELETE cascade ON UPDATE no action;
CREATE INDEX "payload_locked_documents_rels_search_id_idx" ON "payload_locked_documents_rels" USING btree ("search_id");`)
}
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
await db.execute(sql`
ALTER TABLE "search" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "search_rels" DISABLE ROW LEVEL SECURITY;
DROP TABLE "search" CASCADE;
DROP TABLE "search_rels" CASCADE;
ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT "payload_locked_documents_rels_search_fk";
DROP INDEX "payload_locked_documents_rels_search_id_idx";
ALTER TABLE "announcement" ALTER COLUMN "date" SET DEFAULT '2026-04-12T12:46:39.042Z';
ALTER TABLE "calendar" ALTER COLUMN "date" SET DEFAULT '2026-04-12T12:46:39.348Z';
ALTER TABLE "classifieds" ALTER COLUMN "until" SET DEFAULT '2026-05-10T12:46:39.403Z';
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN "search_id";`)
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,22 @@
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-04-19T09:46:18.288Z';
ALTER TABLE "calendar" ALTER COLUMN "date" SET DEFAULT '2026-04-19T09:46:18.569Z';
ALTER TABLE "classifieds" ALTER COLUMN "until" SET DEFAULT '2026-05-13T09:46:18.627Z';
ALTER TABLE "search_rels" ADD COLUMN "pages_id" uuid;
ALTER TABLE "search_rels" ADD CONSTRAINT "search_rels_pages_fk" FOREIGN KEY ("pages_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
CREATE INDEX "search_rels_pages_id_idx" ON "search_rels" USING btree ("pages_id");`)
}
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
await db.execute(sql`
ALTER TABLE "search_rels" DROP CONSTRAINT "search_rels_pages_fk";
DROP INDEX "search_rels_pages_id_idx";
ALTER TABLE "announcement" ALTER COLUMN "date" SET DEFAULT '2026-04-19T09:34:10.248Z';
ALTER TABLE "calendar" ALTER COLUMN "date" SET DEFAULT '2026-04-19T09:34:10.532Z';
ALTER TABLE "classifieds" ALTER COLUMN "until" SET DEFAULT '2026-05-13T09:34:10.591Z';
ALTER TABLE "search_rels" DROP COLUMN "pages_id";`)
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,44 @@
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_classifieds" (
"_order" integer NOT NULL,
"_parent_id" uuid NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"block_name" varchar
);
CREATE TABLE "_pages_v_blocks_classifieds" (
"_order" integer NOT NULL,
"_parent_id" uuid NOT NULL,
"_path" text NOT NULL,
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"_uuid" varchar,
"block_name" varchar
);
ALTER TABLE "announcement" ALTER COLUMN "date" SET DEFAULT '2026-04-19T12:20:19.986Z';
ALTER TABLE "calendar" ALTER COLUMN "date" SET DEFAULT '2026-04-19T12:20:20.277Z';
ALTER TABLE "classifieds" ALTER COLUMN "until" SET DEFAULT '2026-05-13T12:20:20.339Z';
ALTER TABLE "pages_blocks_classifieds" ADD CONSTRAINT "pages_blocks_classifieds_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_pages_v_blocks_classifieds" ADD CONSTRAINT "_pages_v_blocks_classifieds_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_pages_v"("id") ON DELETE cascade ON UPDATE no action;
CREATE INDEX "pages_blocks_classifieds_order_idx" ON "pages_blocks_classifieds" USING btree ("_order");
CREATE INDEX "pages_blocks_classifieds_parent_id_idx" ON "pages_blocks_classifieds" USING btree ("_parent_id");
CREATE INDEX "pages_blocks_classifieds_path_idx" ON "pages_blocks_classifieds" USING btree ("_path");
CREATE INDEX "_pages_v_blocks_classifieds_order_idx" ON "_pages_v_blocks_classifieds" USING btree ("_order");
CREATE INDEX "_pages_v_blocks_classifieds_parent_id_idx" ON "_pages_v_blocks_classifieds" USING btree ("_parent_id");
CREATE INDEX "_pages_v_blocks_classifieds_path_idx" ON "_pages_v_blocks_classifieds" USING btree ("_path");`)
}
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
await db.execute(sql`
ALTER TABLE "pages_blocks_classifieds" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "_pages_v_blocks_classifieds" DISABLE ROW LEVEL SECURITY;
DROP TABLE "pages_blocks_classifieds" CASCADE;
DROP TABLE "_pages_v_blocks_classifieds" CASCADE;
ALTER TABLE "announcement" ALTER COLUMN "date" SET DEFAULT '2026-04-19T09:46:18.288Z';
ALTER TABLE "calendar" ALTER COLUMN "date" SET DEFAULT '2026-04-19T09:46:18.569Z';
ALTER TABLE "classifieds" ALTER COLUMN "until" SET DEFAULT '2026-05-13T09:46:18.627Z';`)
}

View file

@ -30,6 +30,9 @@ import * as migration_20260409_083638 from './20260409_083638';
import * as migration_20260410_114057_imagecards_block from './20260410_114057_imagecards_block';
import * as migration_20260410_115248_parish_title_block from './20260410_115248_parish_title_block';
import * as migration_20260410_124639 from './20260410_124639';
import * as migration_20260413_093410_search from './20260413_093410_search';
import * as migration_20260413_094618_search_pages from './20260413_094618_search_pages';
import * as migration_20260413_122020 from './20260413_122020';
export const migrations = [
{
@ -190,6 +193,21 @@ export const migrations = [
{
up: migration_20260410_124639.up,
down: migration_20260410_124639.down,
name: '20260410_124639'
name: '20260410_124639',
},
{
up: migration_20260413_093410_search.up,
down: migration_20260413_093410_search.down,
name: '20260413_093410_search',
},
{
up: migration_20260413_094618_search_pages.up,
down: migration_20260413_094618_search_pages.down,
name: '20260413_094618_search_pages',
},
{
up: migration_20260413_122020.up,
down: migration_20260413_122020.down,
name: '20260413_122020'
},
];

View file

@ -80,7 +80,7 @@ export const Parish = (
<Events events={tranformWorship(worship)} n={4}/>
</Col>
<Col>
<Title title={"Ansprechpersonen"} size={"md"} color={"contrast"} />
<Title title={"Kontakt"} size={"md"} color={"contrast"} />
<ContactPersonList persons={contactPersons} />
</Col>

View file

@ -0,0 +1,59 @@
import Link from 'next/link'
import { Search } from '@/payload-types'
import { Section } from '@/components/Section/Section'
import { Container } from '@/components/Container/Container'
import { PageHeader } from '@/compositions/PageHeader/PageHeader'
import { getSearchResultUrl } from '@/utils/getSearchResultUrl'
import styles from './styles.module.scss'
const RELATION_LABELS: Record<Search['doc']['relationTo'], string> = {
pages: 'Seite',
blog: 'Nachricht',
parish: 'Gemeinde',
event: 'Veranstaltung',
group: 'Gruppe',
}
type SearchPageProps = {
query: string
results: Search[]
}
export const SearchPage = ({ query, results }: SearchPageProps) => {
const linkedResults = results
.map((result) => ({ result, href: getSearchResultUrl(result.doc) }))
.filter((r): r is { result: Search; href: string } => r.href !== null)
const description = query
? `Suchergebnisse für „${query}"`
: 'Bitte geben Sie einen Suchbegriff ein.'
return (
<>
<PageHeader title="Suche" description={description} />
<Section padding="small">
<Container>
{query && linkedResults.length === 0 && (
<p className={styles.empty}>Keine Ergebnisse für {query}.</p>
)}
{linkedResults.length > 0 && (
<ul className={styles.results}>
{linkedResults.map(({ result, href }) => (
<li key={result.id} className={styles.result}>
<Link href={href} className={styles.resultLink}>
<span className={styles.resultTitle}>{result.title}</span>
<span className={styles.resultType}>
{RELATION_LABELS[result.doc.relationTo]}
</span>
</Link>
</li>
))}
</ul>
)}
</Container>
</Section>
</>
)
}

View file

@ -0,0 +1,49 @@
@import "template.scss";
.results {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 1px;
}
.result {
border-bottom: 1px solid $border-color-light;
}
.resultLink {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 20px;
padding: 16px 0;
color: inherit;
text-decoration: none;
transition: opacity 100ms ease-in;
&:hover {
opacity: 0.7;
}
}
.resultTitle {
font-size: 18px;
font-weight: 500;
}
.resultType {
font-size: 14px;
color: $base-color;
opacity: 0.6;
text-transform: uppercase;
letter-spacing: 0.05em;
flex-shrink: 0;
}
.empty {
font-size: 18px;
color: $base-color;
opacity: 0.7;
}

View file

@ -87,6 +87,7 @@ export interface Config {
documents: Document;
media: Media;
users: User;
search: Search;
'payload-kv': PayloadKv;
'payload-jobs': PayloadJob;
'payload-locked-documents': PayloadLockedDocument;
@ -115,6 +116,7 @@ export interface Config {
documents: DocumentsSelect<false> | DocumentsSelect<true>;
media: MediaSelect<false> | MediaSelect<true>;
users: UsersSelect<false> | UsersSelect<true>;
search: SearchSelect<false> | SearchSelect<true>;
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
'payload-jobs': PayloadJobsSelect<false> | PayloadJobsSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
@ -611,6 +613,11 @@ export interface Page {
blockName?: string | null;
blockType: 'imageCards';
}
| {
id?: string | null;
blockName?: string | null;
blockType: 'classifieds';
}
)[]
| null;
updatedAt: string;
@ -1068,6 +1075,40 @@ export interface User {
| null;
password?: string | null;
}
/**
* This is a collection of automatically created search results. These results are used by the global site search and will be updated automatically as documents in the CMS are created or updated.
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "search".
*/
export interface Search {
id: string;
title?: string | null;
priority?: number | null;
doc:
| {
relationTo: 'parish';
value: string | Parish;
}
| {
relationTo: 'pages';
value: string | Page;
}
| {
relationTo: 'blog';
value: string | Blog;
}
| {
relationTo: 'event';
value: string | Event;
}
| {
relationTo: 'group';
value: string | Group;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-kv".
@ -1272,6 +1313,10 @@ export interface PayloadLockedDocument {
| ({
relationTo: 'users';
value: string | User;
} | null)
| ({
relationTo: 'search';
value: string | Search;
} | null);
globalSlug?: string | null;
user: {
@ -1925,6 +1970,12 @@ export interface PagesSelect<T extends boolean = true> {
id?: T;
blockName?: T;
};
classifieds?:
| T
| {
id?: T;
blockName?: T;
};
};
updatedAt?: T;
createdAt?: T;
@ -2062,6 +2113,17 @@ export interface UsersSelect<T extends boolean = true> {
expiresAt?: T;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "search_select".
*/
export interface SearchSelect<T extends boolean = true> {
title?: T;
priority?: T;
doc?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-kv_select".

View file

@ -44,6 +44,7 @@ import { Pages } from '@/collections/Pages'
import { Prayers } from '@/collections/Prayers'
import { siteConfig } from '@/config/site'
import { generateRecurringMassesTask } from '@/jobs/generateRecurringMasses'
import { searchPlugin } from '@payloadcms/plugin-search'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
@ -196,5 +197,35 @@ export default buildConfig({
options: {},
acl: undefined,
}),
searchPlugin({
collections: ['parish', 'pages', 'blog', 'event', 'group'],
defaultPriorities: {
parish: 10,
pages: 15,
group: 20,
event: 30,
blog: 40,
},
// The plugin only reads `title` from the source doc, but Parish and
// Group use `name` instead. Map `name` → `title` so those collections
// get an indexed, searchable title.
beforeSync: ({ originalDoc, searchDoc }) => ({
...searchDoc,
title: originalDoc.title || originalDoc.name || searchDoc.title || '',
}),
// The plugin hardcodes `maxDepth: 0` on the polymorphic `doc` field,
// so `doc.value` is always returned as a string ID — even with depth>0
// at query time. We need it populated so the search results page can
// read each referenced document's `slug` to build URLs like
// /gemeinde/[slug] and /gruppe/[slug].
searchOverrides: {
fields: ({ defaultFields }) =>
defaultFields.map((field) =>
'name' in field && field.name === 'doc' && field.type === 'relationship'
? { ...field, maxDepth: 1 }
: field,
),
},
})
],
})

View file

@ -0,0 +1,20 @@
import { Search } from '@/payload-types'
export function getSearchResultUrl(doc: Search['doc']): string | null {
if (!doc || typeof doc.value === 'string') return null
switch (doc.relationTo) {
case 'pages':
return doc.value.slug ? `/${doc.value.slug}` : null
case 'blog':
return `/blog/${doc.value.id}`
case 'parish':
return doc.value.slug ? `/gemeinde/${doc.value.slug}` : null
case 'event':
return `/veranstaltungen/${doc.value.id}`
case 'group':
return doc.value.slug ? `/gruppe/${doc.value.slug}` : null
default:
return null
}
}