Compare commits
No commits in common. "47fa6db41104e304e00144e38ca7571e953dcb9b" and "c4060dad0c0da0b3fbd7b32c3d8d6a4db4ae20fb" have entirely different histories.
47fa6db411
...
c4060dad0c
|
|
@ -1,72 +0,0 @@
|
|||
import type { StorybookConfig } from '@storybook/nextjs-vite';
|
||||
import type { Plugin } from 'vite';
|
||||
|
||||
/**
|
||||
* Vite plugin that provides an empty stub module for server-only packages
|
||||
* (Payload CMS and its plugins) so they don't get bundled into
|
||||
* the browser build and cause Node.js built-in errors.
|
||||
*
|
||||
* Uses a Proxy-based default export so any named or nested import
|
||||
* resolves to a no-op function at runtime without needing to enumerate
|
||||
* every export.
|
||||
*/
|
||||
function mockServerModules(): Plugin {
|
||||
const serverPrefixes = ['payload', '@payloadcms/'];
|
||||
|
||||
const resolvedId = '\0mock-server-module';
|
||||
|
||||
return {
|
||||
name: 'mock-server-modules',
|
||||
enforce: 'pre',
|
||||
resolveId(source) {
|
||||
if (
|
||||
serverPrefixes.some(
|
||||
(prefix) => source === prefix || source.startsWith(prefix + '/'),
|
||||
)
|
||||
) {
|
||||
return { id: resolvedId, syntheticNamedExports: true };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
load(id) {
|
||||
if (id === resolvedId) {
|
||||
return `export default new Proxy({}, { get: (_, p) => p === '__esModule' ? true : () => {} });`;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const config: StorybookConfig = {
|
||||
"stories": [
|
||||
"../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"
|
||||
],
|
||||
"addons": [],
|
||||
"framework": "@storybook/nextjs-vite",
|
||||
async viteFinal(config) {
|
||||
return {
|
||||
...config,
|
||||
plugins: [...(config.plugins || []), mockServerModules()],
|
||||
optimizeDeps: {
|
||||
...config.optimizeDeps,
|
||||
exclude: [
|
||||
...(config.optimizeDeps?.exclude || []),
|
||||
'payload',
|
||||
'@payloadcms/richtext-lexical',
|
||||
'@payloadcms/db-postgres',
|
||||
'@payloadcms/storage-gcs',
|
||||
'@payloadcms/plugin-seo',
|
||||
],
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
// This tells Sass to look in the root directory for imports
|
||||
loadPaths: ["./"],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
<style>
|
||||
:root {
|
||||
--base-color: #016699;
|
||||
--shade1: #67A3C2;
|
||||
--shade2: #DDECF7;
|
||||
--shade3: #eff6ff;
|
||||
--contrast-color: #CE490F;
|
||||
--contrast-shade1: #DA764B;
|
||||
--border-radius: 13px;
|
||||
--font-size-xl: 90px;
|
||||
--font-size-lg: 60px;
|
||||
--font-size-md: 33px;
|
||||
--font-size-sm: 25px;
|
||||
--font-size-lead: 36px;
|
||||
--font-size-body: 20px;
|
||||
--font-weight-extra-bold: 800;
|
||||
--font-weight-bold: 700;
|
||||
--font-weight-light: 300;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
import type { Preview } from '@storybook/nextjs-vite'
|
||||
|
||||
import { defaultFont, headerFont } from '@/assets/fonts'
|
||||
|
||||
const preview: Preview = {
|
||||
decorators: [
|
||||
(Story) => {
|
||||
return (
|
||||
<div
|
||||
className={defaultFont.className}
|
||||
style={{
|
||||
fontSize: 'var(--font-size-body)',
|
||||
'--header-font': headerFont.style.fontFamily,
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<Story />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
],
|
||||
parameters: {
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
export default preview
|
||||
98
CLAUDE.md
|
|
@ -1,98 +0,0 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Overview
|
||||
|
||||
Church website for the Heilige Drei Könige Catholic parish, built on **Next.js 15 (App Router) + React 19 + Payload CMS v3 + PostgreSQL/PostGIS**. The frontend is fully server-rendered via Next; content is authored in the Payload admin and rendered from typed collection blocks. The repo is a **multi-tenant template**: one codebase serves multiple parish sites, selected at build/run time via `NEXT_PUBLIC_SITE_ID`.
|
||||
|
||||
## Common commands
|
||||
|
||||
```bash
|
||||
npm run dev # Next dev server (http://localhost:3000, admin at /admin)
|
||||
npm run devsafe # Same but wipes .next first (use after Payload schema changes)
|
||||
npm run build # Production build (output: 'standalone' for Docker)
|
||||
npm start # Serve production build
|
||||
npm run lint # Next/ESLint. Note: eslint.ignoreDuringBuilds is true in next.config
|
||||
npm test # Vitest (run a single test: npx vitest run path/to/file.test.ts)
|
||||
npm run storybook # Storybook on :6006 (see Storybook section for caveats)
|
||||
|
||||
# Payload
|
||||
npm run payload migrate:create --name <name> # create a migration from current schema
|
||||
npm run payload migrate # apply pending migrations
|
||||
npm run generate:types # regenerate src/payload-types.ts
|
||||
npm run payload jobs:run --cron "* * * * *" --queue default # run queued jobs locally
|
||||
```
|
||||
|
||||
Node 22+ is required (`engines`). The `predev`/`prebuild` hooks run `scripts/copy-favicon.mjs` to copy the active site's `icon.ico` into `src/app/(home)/`.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Multi-site configuration
|
||||
- `sites/<siteId>/config.ts` exports a `SiteConfig` (name, colors, fonts, contact info). Current sites: `dreikoenige`, `chemnitz`.
|
||||
- `src/config/site.ts` picks one based on `NEXT_PUBLIC_SITE_ID` and exposes `siteConfig`. Unknown IDs throw at module load.
|
||||
- Each site also provides its own `Logo.tsx`, `logoSvg.ts`, and `icon.ico`. When adding theming or site-specific assets, go through `siteConfig` — don't hardcode.
|
||||
|
||||
### Next App Router layout
|
||||
`src/app/` uses two route groups:
|
||||
- **`(home)`** — public site. Notably:
|
||||
- `[[...slug]]/page.tsx` is the catch-all that renders any CMS `Pages` document via the shared `<Blocks>` composition.
|
||||
- Dedicated routes exist for domain entities: `gemeinde/` (churches), `gruppe/` (groups), `gottesdienst/` (worship), `veranstaltungen/` (events), `blog/`, `pfarrei/` (parish), `sakramente/`, `spenden/` (donations), `suche/` (search), `gebetsanliegen-des-papstes/` (pope's prayer intentions).
|
||||
- `api/draft/` enables Payload draft mode for live preview.
|
||||
- **`(payload)`** — Payload admin (`/admin`) and Payload's own API routes. Don't edit files under this group by hand; they're scaffolded by `@payloadcms/next`.
|
||||
|
||||
### Payload as the content layer
|
||||
`src/payload.config.ts` is the single source of truth. Collections, globals, the Lexical editor config, the jobs queue, and plugins (GCS storage, search) all live there.
|
||||
|
||||
- **Collections** (`src/collections/*.ts`): `Parish`, `Churches`, `Worship`, `Events`, `Blog`, `Groups`, `Pages`, `Announcements`, `LiturgicalCalendar`, `PopesPrayerIntentions`, `Classifieds`, `ContactPerson`, `Locations`, `DonationForms`, `Prayers`, `Magazine`, `Highlight`, `Documents`, `Media`, `Users`.
|
||||
- **Globals** (`src/globals/`): `Menu`, `Footer`, plus a `ValidateHref` helper.
|
||||
- **Blocks** (`src/collections/blocks/*.ts`): reusable Lexical-adjacent block schemas referenced by `Pages.content`, `Blog.content`, etc. Each block has a matching renderer branch in `src/compositions/Blocks/Blocks.tsx` — **adding a new block means updating both the schema and `Blocks.tsx`**.
|
||||
- **Types**: `src/payload-types.ts` is generated — never edit by hand. Run `npm run generate:types` after schema changes. The TS path alias `@payload-config` resolves to the config file and `@/*` resolves to `src/*`.
|
||||
- **DB**: Postgres adapter with `idType: 'uuid'` and `push: false` (migrations only, no dev-time schema push). PostGIS extension is required because `Locations` uses geo fields.
|
||||
- **GraphQL is disabled**. Use the REST/local API.
|
||||
- **Admin UI**: German only (`@payloadcms/translations/languages/de`); logo comes from the active site's `Logo.tsx` via `serverProps`.
|
||||
|
||||
### Data fetching pattern
|
||||
Server components read via helpers in `src/fetch/` (e.g. `fetchPageBySlug`, `fetchBlogBySlug`, `fetchChurchBySlug`) rather than calling Payload directly from page files. These helpers accept a `draft` flag so draft mode works end-to-end. Authenticated previews use `isAuthenticated()` from `src/utils/auth.ts` plus the `<RefreshRouteOnSave>` client component.
|
||||
|
||||
### Rendering structure
|
||||
- **`src/components/<Name>/`** — leaf/primitive UI (`Button`, `Title`, `Container`, …). Each folder usually contains the `.tsx`, a `styles.module.scss`, and a `.stories.tsx`.
|
||||
- **`src/compositions/<Name>/`** — larger assemblies that combine components and sometimes fetch data (e.g. `Blocks`, `PageHeader`, `Footer`, `ContactSection`).
|
||||
- **`src/pageComponents/<Entity>/`** — page-level components for specific routes (`Home`, `Parish`, `Event`, `Worship`, `Search`).
|
||||
- **`src/utils/dto/`** — DTO mappers that normalize Payload shapes (e.g. `transformGallery`, `getPhoto`) before passing data into presentation components. When a block renderer needs data, prefer an existing DTO helper over inlining the reshape.
|
||||
|
||||
### Recurring masses / jobs queue
|
||||
See the README for the full walkthrough. Key points for code changes:
|
||||
- The task is `src/jobs/generateRecurringMasses.ts`; it's registered in `payload.config.ts` under `jobs.tasks`.
|
||||
- `autoRun` only fires when `NODE_ENV === 'production'` (via `shouldAutoRun`). In dev, either trigger via the admin "Run now" button or the `npm run payload jobs:run` CLI command.
|
||||
- Churches' `afterChange` hook queues a regeneration whenever the `recurringSchedule` field changes. The task only appends future occurrences — it will not overwrite existing `Worship` docs.
|
||||
- `Worship` docs produced by the task are indistinguishable from hand-created ones; editors can still cancel/modify individual occurrences.
|
||||
|
||||
### Search
|
||||
The `@payloadcms/plugin-search` plugin indexes `parish`, `pages`, `blog`, `event`, `group`. Two non-default tweaks in `payload.config.ts` worth knowing about:
|
||||
1. `beforeSync` maps `originalDoc.name` → `searchDoc.title` because `Parish` and `Groups` don't have a `title` field.
|
||||
2. `searchOverrides.fields` lifts the `doc.value` relationship's `maxDepth` to 1 so the results page (`/suche`) can read each referenced doc's `slug` to build URLs.
|
||||
|
||||
### Storage (GCS)
|
||||
The `gcsStorage` plugin is registered unconditionally but `enabled: !!process.env.GOOGLE_BUCKET`. `alwaysInsertFields: true` keeps the collection schema (and therefore `payload-types.ts` / `importMap.js`) identical whether or not GCS is active — so you don't get spurious diffs switching between local-only dev and cloud dev. Public URLs are built manually via `generateFileURL` (`https://storage.googleapis.com/<bucket>/<prefix>/<filename>`).
|
||||
|
||||
### Styling
|
||||
- SCSS Modules (`*.module.scss`) colocated with components.
|
||||
- Root-level `_template.scss` and CSS variables from `siteConfig` drive theming.
|
||||
- `next.config.mjs` pins `images.remotePatterns` to `storage.googleapis.com/<GOOGLE_BUCKET>/**`; local uploads are served from Next's default route.
|
||||
|
||||
### Storybook
|
||||
`.storybook/main.ts` contains a custom **`mockServerModules` Vite plugin** that stubs out `payload` and every `@payloadcms/*` import with a Proxy-based no-op module. This is intentional — components that incidentally import Payload utilities won't blow up the browser build. If you add a component that genuinely needs server-only behavior in a story, refactor the story to pass data as props rather than trying to unmock.
|
||||
|
||||
### Migrations
|
||||
Payload migrations live in `src/migrations/`. Every schema change should produce a new `<timestamp>_<name>.ts` + `.json` pair via `npm run payload migrate:create`. Production deploys must run `npm run payload migrate` against the prod DB (not yet wired into CD — see README todo).
|
||||
|
||||
## Path aliases
|
||||
- `@/*` → `src/*`
|
||||
- `@payload-config` → `src/payload.config.ts`
|
||||
|
||||
## Things to avoid
|
||||
- Don't edit `src/payload-types.ts` — regenerate it.
|
||||
- Don't edit files under `src/app/(payload)/admin/` or `src/app/(payload)/api/` — they're Payload-owned.
|
||||
- Don't add a new block schema without also adding its renderer branch in `src/compositions/Blocks/Blocks.tsx` — the Pages route will silently drop unknown block types.
|
||||
- Don't rely on `push: true` dev-mode schema sync; this project uses migrations exclusively.
|
||||
69
LICENSE
|
|
@ -1,69 +0,0 @@
|
|||
Parish Website Template — Commercial License Agreement
|
||||
|
||||
Copyright (c) 2026 Benno Tielen <Benno@tielen.nl>. All rights reserved.
|
||||
|
||||
This software (the "Software") is licensed, not sold. By installing, copying,
|
||||
or otherwise using the Software, the licensee ("Licensee") agrees to the terms
|
||||
below. If Licensee does not agree, Licensee must not use the Software.
|
||||
|
||||
1. Grant of License
|
||||
Subject to payment of the applicable license fee and compliance with this
|
||||
agreement, the copyright holder ("Licensor") grants Licensee a non-exclusive,
|
||||
non-transferable, perpetual license to:
|
||||
(a) install and operate the Software on servers owned or controlled by
|
||||
Licensee, for Licensee's own parish or organization, and
|
||||
(b) modify the source code of the Software for Licensee's own operational
|
||||
use.
|
||||
|
||||
Unless otherwise agreed in writing, this license covers a single production
|
||||
deployment per purchased license. Non-production environments (staging,
|
||||
development, backup) operated solely by Licensee in support of that production
|
||||
deployment are included.
|
||||
|
||||
2. Restrictions
|
||||
Licensee shall NOT, in whole or in part, whether modified or unmodified:
|
||||
(a) redistribute, publish, sublicense, lease, rent, lend, or sell the
|
||||
Software to any third party;
|
||||
(b) use the Software to provide a hosted, managed, SaaS, white-label, or
|
||||
similar service to any third party;
|
||||
(c) remove, obscure, or alter any copyright, trademark, or license notice
|
||||
contained in the Software;
|
||||
(d) reverse engineer or decompile any non-source components of the Software
|
||||
except to the extent such restriction is prohibited by applicable law.
|
||||
|
||||
3. Ownership
|
||||
All right, title, and interest in the Software, including all intellectual
|
||||
property rights, remain with Licensor. Modifications created by Licensee are
|
||||
owned by Licensee, but are derivative works of the Software and are therefore
|
||||
subject to the restrictions in Section 2.
|
||||
|
||||
4. Warranty Disclaimer
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND "AS AVAILABLE", WITHOUT WARRANTY OF ANY
|
||||
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.
|
||||
|
||||
5. Limitation of Liability
|
||||
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, LICENSOR SHALL NOT BE
|
||||
LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE
|
||||
DAMAGES, OR ANY LOSS OF PROFITS OR REVENUES. LICENSOR'S TOTAL AGGREGATE
|
||||
LIABILITY UNDER THIS AGREEMENT SHALL NOT EXCEED THE LICENSE FEE PAID BY
|
||||
LICENSEE FOR THE SOFTWARE.
|
||||
|
||||
6. Termination
|
||||
This license terminates automatically if Licensee breaches any of its terms.
|
||||
On termination, Licensee must cease all use of the Software and destroy all
|
||||
copies in its possession or control. Sections 3, 4, 5, 7, and 8 survive
|
||||
termination.
|
||||
|
||||
7. Governing Law and Jurisdiction
|
||||
This agreement is governed by the laws of the Netherlands, without regard to
|
||||
its conflict-of-laws rules. The competent courts of the Netherlands shall
|
||||
have exclusive jurisdiction over any dispute arising out of or in connection
|
||||
with this agreement.
|
||||
|
||||
8. Entire Agreement
|
||||
This agreement constitutes the entire agreement between the parties with
|
||||
respect to the Software and supersedes all prior or contemporaneous
|
||||
understandings. Any amendment must be in writing and signed by Licensor.
|
||||
|
||||
For licensing inquiries, contact: Benno Tielen <Benno@tielen.nl>.
|
||||
16
README.md
|
|
@ -1,13 +1,11 @@
|
|||
# Parish Website Template
|
||||
# Heilige Drei Könige Website
|
||||
|
||||
A white-label website template for Catholic parishes. One codebase powers multiple parish sites; the active site is selected at build/run time via the `NEXT_PUBLIC_SITE_ID` environment variable, and each site's branding (name, colors, fonts, logo, favicon, contact info) lives under `sites/<siteId>/`. The repository currently ships with `dreikoenige` (Heilige Drei Könige) and `chemnitz` as reference deployments.
|
||||
This repository contains the source code for the Heilige Drei Könige Catholic Church website, built with:
|
||||
|
||||
Built with:
|
||||
|
||||
- Next.js 15 (App Router)
|
||||
- React 19
|
||||
- Next.js
|
||||
- React
|
||||
- Payload CMS v3
|
||||
- PostgreSQL with PostGIS
|
||||
- PostgreSQL
|
||||
|
||||
## Quick start
|
||||
|
||||
|
|
@ -132,10 +130,6 @@ Open http://localhost:6006
|
|||
|
||||
Site-wide metadata (title, description, keywords, OpenGraph) is configured in `src/config/site.ts`. Update that file to change SEO defaults used in the root layout.
|
||||
|
||||
## License
|
||||
|
||||
This software is distributed under a commercial license, not as open source. Purchasing a license grants the right to deploy and modify the Software on servers you operate; redistribution, resale, and use as a hosted service for third parties are not permitted. See [`LICENSE`](./LICENSE) for the full terms, or contact Benno Tielen <Benno@tielen.nl> for licensing inquiries.
|
||||
|
||||
## A note on updating dependencies
|
||||
|
||||
Payload CMS and Next.js are pinned to specific versions. From the Payload docs we currently have the following requirements (March 2026):
|
||||
|
|
|
|||
|
|
@ -169,38 +169,42 @@ docker restart postgres
|
|||
|
||||
## Deploy via Ansible (without CI/CD)
|
||||
|
||||
Use these playbooks to deploy from your local machine — no Forgejo runner needed.
|
||||
Use the `deploy.yml` playbook to deploy from your local machine — no Forgejo runner or CI/CD pipeline needed. This is useful for hotfixes, CI outages, or production servers without Forgejo.
|
||||
|
||||
```bash
|
||||
cd infra/ansible
|
||||
|
||||
# Deploy both environments (git pull once, then build+deploy each sequentially)
|
||||
ansible-playbook playbooks/deploy.yml --ask-vault-pass
|
||||
# Deploy to test/staging VPS
|
||||
ansible-playbook playbooks/deploy.yml -i inventory/test.yml --ask-vault-pass
|
||||
|
||||
# Deploy staging only
|
||||
ansible-playbook playbooks/deploy-staging.yml --ask-vault-pass
|
||||
|
||||
# Deploy test only
|
||||
ansible-playbook playbooks/deploy-test.yml --ask-vault-pass
|
||||
# Deploy to production
|
||||
ansible-playbook playbooks/deploy.yml -i inventory/production.yml --ask-vault-pass
|
||||
```
|
||||
|
||||
**Steps executed per environment:**
|
||||
**What it does:**
|
||||
|
||||
1. Pull latest code from the configured branch (`staging`)
|
||||
2. Build app Docker image (bakes in `NEXT_PUBLIC_SERVER_URL` and `NEXT_PUBLIC_SITE_ID`)
|
||||
3. Build migration image and run `npx payload migrate`
|
||||
4. Stop and remove the old container
|
||||
5. Start the new container
|
||||
6. Fix upload volume permissions
|
||||
7. Prune old Docker images
|
||||
1. Pulls the latest code from the configured branch (`repo_branch` in inventory)
|
||||
2. Runs `deploy.sh` for each environment (sequentially to save RAM), which:
|
||||
- Builds the Docker app image with build-time env vars
|
||||
- Builds a migration image and runs `npx payload migrate`
|
||||
- Stops the old container, starts the new one
|
||||
- Prunes old Docker images
|
||||
|
||||
**Deploy a specific branch:**
|
||||
|
||||
```bash
|
||||
ansible-playbook playbooks/deploy.yml --ask-vault-pass -e repo_branch=feature/my-branch
|
||||
ansible-playbook playbooks/deploy.yml -i inventory/test.yml --ask-vault-pass \
|
||||
-e repo_branch=feature/my-branch
|
||||
```
|
||||
|
||||
> **Note:** The server must already be provisioned with `setup.yml` before deploying. The deploy playbooks only pull code and rebuild containers — they do not install Docker, Caddy, or PostgreSQL.
|
||||
**Deploy only one environment** (e.g., just staging):
|
||||
|
||||
```bash
|
||||
ansible-playbook playbooks/deploy.yml -i inventory/test.yml --ask-vault-pass \
|
||||
-e '{"app_environments": [{"name": "staging", "port": 3001}]}'
|
||||
```
|
||||
|
||||
> **Note:** The server must already be provisioned with `setup.yml` before using `deploy.yml`. The deploy playbook only pulls code and rebuilds containers — it does not install Docker, Caddy, or PostgreSQL.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
---
|
||||
- name: Deploy staging environment
|
||||
hosts: all
|
||||
become: true
|
||||
|
||||
tasks:
|
||||
- name: Pull {{ repo_branch }} branch
|
||||
ansible.builtin.git:
|
||||
repo: "{{ repo_url }}"
|
||||
dest: "{{ repo_dir }}"
|
||||
version: "{{ repo_branch }}"
|
||||
force: true
|
||||
accept_hostkey: true
|
||||
|
||||
- name: Build and deploy staging
|
||||
ansible.builtin.include_role:
|
||||
name: app
|
||||
tasks_from: deploy_env
|
||||
vars:
|
||||
env: "{{ app_environments | selectattr('name', 'equalto', 'staging') | first }}"
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
---
|
||||
- name: Deploy test environment
|
||||
hosts: all
|
||||
become: true
|
||||
|
||||
tasks:
|
||||
- name: Pull {{ repo_branch }} branch
|
||||
ansible.builtin.git:
|
||||
repo: "{{ repo_url }}"
|
||||
dest: "{{ repo_dir }}"
|
||||
version: "{{ repo_branch }}"
|
||||
force: true
|
||||
accept_hostkey: true
|
||||
|
||||
- name: Build and deploy test
|
||||
ansible.builtin.include_role:
|
||||
name: app
|
||||
tasks_from: deploy_env
|
||||
vars:
|
||||
env: "{{ app_environments | selectattr('name', 'equalto', 'test') | first }}"
|
||||
|
|
@ -1,37 +1,19 @@
|
|||
---
|
||||
- name: Pull latest code (shared for both environments)
|
||||
- name: Deploy app (rebuild + restart)
|
||||
hosts: all
|
||||
become: true
|
||||
|
||||
tasks:
|
||||
- name: Pull {{ repo_branch }} branch
|
||||
- name: Pull latest code
|
||||
ansible.builtin.git:
|
||||
repo: "{{ repo_url }}"
|
||||
dest: "{{ repo_dir }}"
|
||||
version: "{{ repo_branch }}"
|
||||
force: true
|
||||
accept_hostkey: true
|
||||
|
||||
- name: Deploy staging environment
|
||||
hosts: all
|
||||
become: true
|
||||
|
||||
tasks:
|
||||
- name: Build and deploy staging
|
||||
ansible.builtin.include_role:
|
||||
name: app
|
||||
tasks_from: deploy_env
|
||||
vars:
|
||||
env: "{{ app_environments | selectattr('name', 'equalto', 'staging') | first }}"
|
||||
|
||||
- name: Deploy test environment
|
||||
hosts: all
|
||||
become: true
|
||||
|
||||
tasks:
|
||||
- name: Build and deploy test
|
||||
ansible.builtin.include_role:
|
||||
name: app
|
||||
tasks_from: deploy_env
|
||||
vars:
|
||||
env: "{{ app_environments | selectattr('name', 'equalto', 'test') | first }}"
|
||||
- name: Deploy each environment
|
||||
ansible.builtin.shell: |
|
||||
{{ scripts_dir }}/deploy.sh {{ item.name }} {{ item.port }}
|
||||
loop: "{{ app_environments }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
---
|
||||
- name: Deploy each environment (sequentially to save RAM)
|
||||
ansible.builtin.include_tasks: deploy_env.yml
|
||||
ansible.builtin.shell: |
|
||||
{{ scripts_dir }}/deploy.sh {{ item.name }} {{ item.port }}
|
||||
loop: "{{ app_environments }}"
|
||||
loop_control:
|
||||
loop_var: env
|
||||
label: "{{ env.name }}"
|
||||
label: "{{ item.name }}"
|
||||
register: deploy_result
|
||||
changed_when: true
|
||||
|
|
|
|||
|
|
@ -1,98 +0,0 @@
|
|||
---
|
||||
- name: "{{ env.name }} | Build app image"
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- docker
|
||||
- build
|
||||
- --build-arg
|
||||
- "NEXT_PUBLIC_SERVER_URL=https://{{ env.domain }}"
|
||||
- --build-arg
|
||||
- "NEXT_PUBLIC_SITE_ID={{ env.site_id }}"
|
||||
- -t
|
||||
- "church-website:{{ env.name }}"
|
||||
- "{{ repo_dir }}"
|
||||
changed_when: true
|
||||
|
||||
- name: "{{ env.name }} | Build migration image"
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- docker
|
||||
- build
|
||||
- --target
|
||||
- builder
|
||||
- --build-arg
|
||||
- "NEXT_PUBLIC_SERVER_URL=https://{{ env.domain }}"
|
||||
- --build-arg
|
||||
- "NEXT_PUBLIC_SITE_ID={{ env.site_id }}"
|
||||
- -t
|
||||
- "church-website-migrate:{{ env.name }}"
|
||||
- "{{ repo_dir }}"
|
||||
changed_when: true
|
||||
|
||||
- name: "{{ env.name }} | Run database migrations"
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- docker
|
||||
- run
|
||||
- --rm
|
||||
- --network
|
||||
- "{{ docker_network }}"
|
||||
- --env-file
|
||||
- "{{ envs_dir }}/{{ env.name }}/.env"
|
||||
- "church-website-migrate:{{ env.name }}"
|
||||
- npx
|
||||
- payload
|
||||
- migrate
|
||||
changed_when: true
|
||||
|
||||
- name: "{{ env.name }} | Stop old container"
|
||||
ansible.builtin.command: "docker stop app-{{ env.name }}"
|
||||
failed_when: false
|
||||
changed_when: true
|
||||
|
||||
- name: "{{ env.name }} | Remove old container"
|
||||
ansible.builtin.command: "docker rm app-{{ env.name }}"
|
||||
failed_when: false
|
||||
changed_when: true
|
||||
|
||||
- name: "{{ env.name }} | Start new container"
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- docker
|
||||
- run
|
||||
- -d
|
||||
- --name
|
||||
- "app-{{ env.name }}"
|
||||
- --restart
|
||||
- unless-stopped
|
||||
- --network
|
||||
- "{{ docker_network }}"
|
||||
- --env-file
|
||||
- "{{ envs_dir }}/{{ env.name }}/.env"
|
||||
- -v
|
||||
- "uploads-{{ env.name }}-media:/app/media"
|
||||
- -v
|
||||
- "uploads-{{ env.name }}-documents:/app/documents"
|
||||
- -p
|
||||
- "127.0.0.1:{{ env.port }}:3000"
|
||||
- "church-website:{{ env.name }}"
|
||||
changed_when: true
|
||||
|
||||
- name: "{{ env.name }} | Fix upload volume permissions"
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- docker
|
||||
- exec
|
||||
- -u
|
||||
- "0"
|
||||
- "app-{{ env.name }}"
|
||||
- chown
|
||||
- -R
|
||||
- 1001:1001
|
||||
- /app/media
|
||||
- /app/documents
|
||||
changed_when: true
|
||||
|
||||
- name: "{{ env.name }} | Prune old Docker images"
|
||||
ansible.builtin.command: docker image prune -f
|
||||
changed_when: true
|
||||
|
|
@ -4,6 +4,9 @@ import { withPayload } from '@payloadcms/next/withPayload'
|
|||
const nextConfig = {
|
||||
// Your Next.js config here
|
||||
output: 'standalone',
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
{
|
||||
"name": "@bennotielen/parish-website",
|
||||
"name": "drei-koenige-v3",
|
||||
"version": "1.0.0",
|
||||
"description": "White-label parish website template (Next.js 15 + Payload CMS v3 + PostgreSQL/PostGIS).",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"author": "Benno Tielen <Benno@tielen.nl>",
|
||||
"private": true,
|
||||
"description": "A blank template to get started with Payload 3.0",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"prebuild": "node --env-file-if-exists=.env scripts/copy-favicon.mjs",
|
||||
|
|
|
|||
|
|
@ -1,166 +0,0 @@
|
|||
'use client'
|
||||
|
||||
import { CheckboxInput, useDocumentInfo } from '@payloadcms/ui'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
type Occurrence = {
|
||||
id: string
|
||||
date: string
|
||||
cancelled: boolean
|
||||
}
|
||||
|
||||
const LIMIT = 10
|
||||
|
||||
const formatDate = (iso: string): string =>
|
||||
new Date(iso).toLocaleString('de-DE', {
|
||||
weekday: 'short',
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
|
||||
export const NextOccurrencesTable = () => {
|
||||
const { id } = useDocumentInfo()
|
||||
const [occurrences, setOccurrences] = useState<Occurrence[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [pendingIds, setPendingIds] = useState<Set<string>>(new Set())
|
||||
const [errorIds, setErrorIds] = useState<Set<string>>(new Set())
|
||||
const [fetchError, setFetchError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
let cancelled = false
|
||||
setLoading(true)
|
||||
setFetchError(null)
|
||||
|
||||
const params = new URLSearchParams({
|
||||
'where[event][equals]': String(id),
|
||||
'where[date][greater_than_equal]': new Date().toISOString(),
|
||||
sort: 'date',
|
||||
limit: String(LIMIT),
|
||||
depth: '0',
|
||||
})
|
||||
|
||||
fetch(`/api/eventOccurrence?${params.toString()}`, { credentials: 'include' })
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return res.json()
|
||||
})
|
||||
.then((json: { docs: Occurrence[] }) => {
|
||||
if (cancelled) return
|
||||
setOccurrences(json.docs)
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
if (cancelled) return
|
||||
setFetchError(err.message)
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false)
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [id])
|
||||
|
||||
const toggle = useCallback(async (occ: Occurrence) => {
|
||||
const next = !occ.cancelled
|
||||
setOccurrences((prev) =>
|
||||
prev.map((o) => (o.id === occ.id ? { ...o, cancelled: next } : o)),
|
||||
)
|
||||
setPendingIds((prev) => new Set(prev).add(occ.id))
|
||||
setErrorIds((prev) => {
|
||||
const s = new Set(prev)
|
||||
s.delete(occ.id)
|
||||
return s
|
||||
})
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/eventOccurrence/${occ.id}`, {
|
||||
method: 'PATCH',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ cancelled: next }),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
} catch {
|
||||
setOccurrences((prev) =>
|
||||
prev.map((o) => (o.id === occ.id ? { ...o, cancelled: occ.cancelled } : o)),
|
||||
)
|
||||
setErrorIds((prev) => new Set(prev).add(occ.id))
|
||||
} finally {
|
||||
setPendingIds((prev) => {
|
||||
const s = new Set(prev)
|
||||
s.delete(occ.id)
|
||||
return s
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (!id) {
|
||||
return (
|
||||
<div className="field-description">
|
||||
Veranstaltung erst speichern, um Termine zu verwalten.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <div className="field-description">Wird geladen…</div>
|
||||
}
|
||||
|
||||
if (fetchError) {
|
||||
return (
|
||||
<div className="field-description" style={{ color: 'var(--theme-error-500)' }}>
|
||||
Fehler beim Laden: {fetchError}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (occurrences.length === 0) {
|
||||
return (
|
||||
<div className="field-description">
|
||||
Keine zukünftigen Termine vorhanden.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Abgesagt</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{occurrences.map((occ) => (
|
||||
<tr key={occ.id}>
|
||||
<td>{formatDate(occ.date)}</td>
|
||||
<td>
|
||||
<CheckboxInput
|
||||
checked={occ.cancelled}
|
||||
onToggle={() => toggle(occ)}
|
||||
readOnly={pendingIds.has(occ.id)}
|
||||
/>
|
||||
{errorIds.has(occ.id) && (
|
||||
<div
|
||||
className="field-description"
|
||||
style={{ color: 'var(--theme-error-500)' }}
|
||||
>
|
||||
Speichern fehlgeschlagen.
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NextOccurrencesTable
|
||||
|
|
@ -4,8 +4,9 @@ import { Blocks } from '@/compositions/Blocks/Blocks'
|
|||
import { Metadata } from 'next'
|
||||
import { AdminMenu } from '@/components/AdminMenu/AdminMenu'
|
||||
import { Section } from '@/components/Section/Section'
|
||||
import { getRequestAuth } from '@/utils/auth'
|
||||
import { isAuthenticated } from '@/utils/auth'
|
||||
import { RefreshRouteOnSave } from '@/components/RefreshRouteOnSave/RefreshRouteOnSave'
|
||||
import { draftMode } from 'next/headers'
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ slug?: string[] }>
|
||||
|
|
@ -25,8 +26,9 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|||
|
||||
export default async function DynamicPage({ params }: Props) {
|
||||
const slug = (await params).slug
|
||||
const { authenticated, isDraft } = await getRequestAuth()
|
||||
const { isEnabled: isDraft } = await draftMode()
|
||||
const page = await fetchPageBySlug(slug?.join('/') || "", isDraft)
|
||||
const authenticated = await isAuthenticated()
|
||||
|
||||
if (!page) {
|
||||
notFound()
|
||||
|
|
|
|||
|
|
@ -9,19 +9,27 @@ import { HR } from '@/components/HorizontalRule/HorizontalRule'
|
|||
import Image from 'next/image'
|
||||
import styles from './styles.module.scss'
|
||||
import { Blocks } from '@/compositions/Blocks/Blocks'
|
||||
import { canView, getRequestAuth } from '@/utils/auth'
|
||||
import { isAuthenticated } from '@/utils/auth'
|
||||
import { AdminMenu } from '@/components/AdminMenu/AdminMenu'
|
||||
import { RefreshRouteOnSave } from '@/components/RefreshRouteOnSave/RefreshRouteOnSave'
|
||||
import { draftMode } from 'next/headers'
|
||||
import { fetchBlog } from '@/fetch/blog'
|
||||
|
||||
export default async function BlogPage({ params }: { params: Promise<{id: string}>}){
|
||||
|
||||
const id = (await params).id;
|
||||
const { authenticated, isDraft } = await getRequestAuth()
|
||||
const { isEnabled: isDraft } = await draftMode()
|
||||
const data = await fetchBlog(id, isDraft) as Blog;
|
||||
const url = typeof data.photo === 'object' && data.photo?.sizes?.banner?.url;
|
||||
const authenticated = await isAuthenticated();
|
||||
|
||||
if (!canView(data, authenticated)) notFound()
|
||||
if(!data) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if(!authenticated && data._status !== "published") {
|
||||
notFound();
|
||||
}
|
||||
|
||||
// determine if some margin at the bottom should be added
|
||||
const length = data.content.content.length;
|
||||
|
|
|
|||
|
|
@ -1,22 +1,30 @@
|
|||
import { notFound } from 'next/navigation'
|
||||
import { Parish } from '@/pageComponents/Parish/Parish'
|
||||
import { fetchUpcomingOccurrences } from '@/fetch/eventOccurrences'
|
||||
import { fetchEvents } from '@/fetch/events'
|
||||
import { fetchWorship } from '@/fetch/worship'
|
||||
import { fetchParish } from '@/fetch/parish'
|
||||
import { fetchLastAnnouncement } from '@/fetch/announcement'
|
||||
import { getPhoto, transformGallery } from '@/utils/dto/gallery'
|
||||
import { fetchLastCalendar } from '@/fetch/calendar'
|
||||
import { canView, getRequestAuth } from '@/utils/auth'
|
||||
import { isAuthenticated } from '@/utils/auth'
|
||||
import { AdminMenu } from '@/components/AdminMenu/AdminMenu'
|
||||
import { RefreshRouteOnSave } from '@/components/RefreshRouteOnSave/RefreshRouteOnSave'
|
||||
import { draftMode } from 'next/headers'
|
||||
|
||||
export default async function ParishPage ({ params }: { params: Promise<{slug: string}>}) {
|
||||
|
||||
const slug = (await params).slug;
|
||||
const { authenticated, isDraft } = await getRequestAuth()
|
||||
const { isEnabled: isDraft } = await draftMode()
|
||||
const parish = await fetchParish(slug, isDraft);
|
||||
const authenticated = await isAuthenticated();
|
||||
|
||||
if (!canView(parish, authenticated)) notFound()
|
||||
if(!parish) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if(!authenticated && parish._status !== "published") {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const {
|
||||
id,
|
||||
|
|
@ -30,7 +38,7 @@ export default async function ParishPage ({ params }: { params: Promise<{slug: s
|
|||
gallery,
|
||||
content
|
||||
} = parish
|
||||
const events = await fetchUpcomingOccurrences({ parishId: id })
|
||||
const events = await fetchEvents({ parishId: id })
|
||||
const churchIds = churches.map(c => typeof c === "string" ? c : c.id)
|
||||
const worship = await fetchWorship({ locations: churchIds })
|
||||
const announcement = await fetchLastAnnouncement(id);
|
||||
|
|
|
|||
|
|
@ -11,19 +11,28 @@ import { Col } from '@/components/Flex/Col'
|
|||
import { Row } from '@/components/Flex/Row'
|
||||
import { Blocks } from '@/compositions/Blocks/Blocks'
|
||||
import { getPhoto } from '@/utils/dto/gallery'
|
||||
import { canView, getRequestAuth } from '@/utils/auth'
|
||||
import { isAuthenticated } from '@/utils/auth'
|
||||
import { AdminMenu } from '@/components/AdminMenu/AdminMenu'
|
||||
import { GroupEvents } from '@/compositions/GroupEvents/GroupEvents'
|
||||
import { RichText } from '@/components/Text/RichText'
|
||||
import { RefreshRouteOnSave } from '@/components/RefreshRouteOnSave/RefreshRouteOnSave'
|
||||
import { draftMode } from 'next/headers'
|
||||
|
||||
export default async function GroupPage({ params }: { params: Promise<{slug: string}>}) {
|
||||
|
||||
const slug = (await params).slug
|
||||
const { authenticated, isDraft } = await getRequestAuth()
|
||||
const { isEnabled: isDraft } = await draftMode()
|
||||
const group = await fetchGroup(slug, isDraft)
|
||||
|
||||
if (!canView(group, authenticated)) notFound()
|
||||
if(!group) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const authenticated = await isAuthenticated();
|
||||
|
||||
if(!authenticated && group._status !== "published") {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const {id, shortDescription, photo, name, content, text } = group
|
||||
const media = getPhoto("tablet", photo)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,40 @@
|
|||
import { fetchEvents } from '@/fetch/events'
|
||||
import { fetchWorship } from '@/fetch/worship'
|
||||
import { fetchBlogPosts } from '@/fetch/blog'
|
||||
import { fetchHighlights } from '@/fetch/highlights'
|
||||
import { Home } from '@/pageComponents/Home/Home'
|
||||
import moment from 'moment'
|
||||
import { fetchLastAnnouncement, fetchLastAnnouncements } from '@/fetch/announcement'
|
||||
import { perParish } from '@/utils/dto/perParish'
|
||||
import { fetchLastCalendars } from '@/fetch/calendar'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default async function HomePage() {
|
||||
return <Home />
|
||||
|
||||
const fromDate = moment().isoWeekday(1).hours(0).minutes(0);
|
||||
const tillDate = moment().isoWeekday(7).hours(23).minutes(59);
|
||||
const events = await fetchEvents()
|
||||
const worship = await fetchWorship({
|
||||
fromDate: fromDate.toDate(),
|
||||
tillDate: tillDate.toDate(),
|
||||
});
|
||||
const blog = await fetchBlogPosts(true)
|
||||
const highlights = await fetchHighlights()
|
||||
const announcements = await fetchLastAnnouncements();
|
||||
const announcementsLinks = announcements ? perParish(announcements) : [];
|
||||
const calendars = await fetchLastCalendars();
|
||||
const calendarsLinks = calendars ? perParish(calendars) : [];
|
||||
|
||||
|
||||
return (
|
||||
<Home
|
||||
events={events?.docs || []}
|
||||
worship={worship?.docs || []}
|
||||
blog={blog?.docs || []}
|
||||
highlights={highlights?.docs || []}
|
||||
announcements={announcementsLinks}
|
||||
calendars={calendarsLinks}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 118 KiB |
|
|
@ -1,47 +0,0 @@
|
|||
import { notFound } from 'next/navigation'
|
||||
import { EventPage } from '@/pageComponents/Event/Event'
|
||||
import { getPhoto } from '@/utils/dto/gallery'
|
||||
import { canView, getRequestAuth } from '@/utils/auth'
|
||||
import {
|
||||
fetchOccurrenceById,
|
||||
fetchUpcomingOccurrences,
|
||||
} from '@/fetch/eventOccurrences'
|
||||
import { RefreshRouteOnSave } from '@/components/RefreshRouteOnSave/RefreshRouteOnSave'
|
||||
import { eventToPageProps, transformOccurrences } from '@/utils/dto/events'
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ eventId: string; occurrenceId: string }>
|
||||
}) {
|
||||
const { eventId, occurrenceId } = await params
|
||||
const { authenticated, isDraft } = await getRequestAuth()
|
||||
|
||||
const occurrence = await fetchOccurrenceById(occurrenceId, isDraft)
|
||||
if (!occurrence) notFound()
|
||||
|
||||
const event = typeof occurrence.event === 'object' ? occurrence.event : undefined
|
||||
if (!canView(event, authenticated)) notFound()
|
||||
if (event.id !== eventId) notFound()
|
||||
|
||||
const photo = getPhoto('tablet', event.photo)
|
||||
|
||||
const upcomingRaw = await fetchUpcomingOccurrences({
|
||||
eventId,
|
||||
limit: 5,
|
||||
fromDate: new Date(new Date(occurrence.date).getTime() + 1),
|
||||
})
|
||||
const upcomingOccurrences = transformOccurrences(upcomingRaw.docs)
|
||||
|
||||
return (
|
||||
<>
|
||||
{isDraft && <RefreshRouteOnSave />}
|
||||
<EventPage
|
||||
{...eventToPageProps(event, occurrence)}
|
||||
photo={photo}
|
||||
isAuthenticated={authenticated}
|
||||
upcomingOccurrences={upcomingOccurrences}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import { notFound } from 'next/navigation'
|
||||
import { EventPage } from '@/pageComponents/Event/Event'
|
||||
import { getPhoto } from '@/utils/dto/gallery'
|
||||
import { canView, getRequestAuth } from '@/utils/auth'
|
||||
import { fetchEventById } from '@/fetch/events'
|
||||
import { fetchUpcomingOrPastOccurrences } from '@/fetch/eventOccurrences'
|
||||
import { RefreshRouteOnSave } from '@/components/RefreshRouteOnSave/RefreshRouteOnSave'
|
||||
import { eventToPageProps, transformOccurrences } from '@/utils/dto/events'
|
||||
|
||||
export default async function Page({ params }: { params: Promise<{ eventId: string }> }) {
|
||||
const { eventId } = await params
|
||||
const { authenticated, isDraft } = await getRequestAuth()
|
||||
|
||||
const event = await fetchEventById(eventId, isDraft)
|
||||
if (!canView(event, authenticated)) notFound()
|
||||
|
||||
const occurrences = await fetchUpcomingOrPastOccurrences({ eventId, limit: 5 })
|
||||
const upcomingOccurrences = transformOccurrences(occurrences.docs)
|
||||
const photo = getPhoto('tablet', event.photo)
|
||||
|
||||
return (
|
||||
<>
|
||||
{isDraft && <RefreshRouteOnSave />}
|
||||
<EventPage
|
||||
{...eventToPageProps(event)}
|
||||
photo={photo}
|
||||
isAuthenticated={authenticated}
|
||||
upcomingOccurrences={upcomingOccurrences}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
50
src/app/(home)/veranstaltungen/[id]/page.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { notFound } from 'next/navigation'
|
||||
import { EventPage } from '@/pageComponents/Event/Event'
|
||||
import { getPhoto } from '@/utils/dto/gallery'
|
||||
import { isAuthenticated } from '@/utils/auth'
|
||||
import { fetchEventById } from '@/fetch/events'
|
||||
import { RefreshRouteOnSave } from '@/components/RefreshRouteOnSave/RefreshRouteOnSave'
|
||||
import { draftMode } from 'next/headers'
|
||||
|
||||
export default async function Page({ params }: { params: Promise<{id: string}>}) {
|
||||
|
||||
const id = (await params).id;
|
||||
const { isEnabled: isDraft } = await draftMode()
|
||||
const event = await fetchEventById(id, isDraft)
|
||||
|
||||
if (!event) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const authenticated = await isAuthenticated();
|
||||
|
||||
if(!authenticated && event._status !== "published") {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const group = Array.isArray(event.group) && event.group.length > 0 && typeof event.group[0] == "object" ? event.group[0].slug : undefined;
|
||||
const photo = getPhoto("tablet", event.photo);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isDraft && <RefreshRouteOnSave />}
|
||||
<EventPage
|
||||
id={event.id}
|
||||
title={event.title}
|
||||
date={event.date}
|
||||
createdAt={event.createdAt}
|
||||
cancelled={event.cancelled}
|
||||
isRecurring={event.isRecurring}
|
||||
location={event.location}
|
||||
description={event.description}
|
||||
shortDescription={event.shortDescription}
|
||||
group={group}
|
||||
contact={event.contact || undefined}
|
||||
rsvpLink={event.rsvpLink || undefined}
|
||||
flyer={typeof event.flyer === 'object' ? event.flyer || undefined : undefined}
|
||||
photo={photo}
|
||||
isAuthenticated={authenticated}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import { fetchUpcomingOccurrences } from '@/fetch/eventOccurrences'
|
||||
import { fetchEvents } from '@/fetch/events'
|
||||
import { PageHeader } from '@/compositions/PageHeader/PageHeader'
|
||||
import { Section } from '@/components/Section/Section'
|
||||
import { Container } from '@/components/Container/Container'
|
||||
import { Title } from '@/components/Title/Title'
|
||||
import { NextPrevButtons } from '@/components/NextPrevButtons/NextPrevButtons'
|
||||
import moment from 'moment'
|
||||
import { transformOccurrences } from '@/utils/dto/events'
|
||||
import { transformEvents } from '@/utils/dto/events'
|
||||
import { weekNumber } from '@/utils/week'
|
||||
import { EventRow } from '@/components/EventRow/EventRow'
|
||||
import { fetchHighlightsBetweenDates } from '@/fetch/highlights'
|
||||
|
|
@ -38,7 +38,7 @@ export default async function EventsPage({searchParams}: {
|
|||
const toDate = moment(week).add(1, 'week');
|
||||
const lastWeek = moment(week).subtract(1, 'week');
|
||||
|
||||
const paginatedOccurrences = await fetchUpcomingOccurrences(
|
||||
const paginatedEvents = await fetchEvents(
|
||||
{
|
||||
limit: limit,
|
||||
fromDate: fromDate.toDate(),
|
||||
|
|
@ -51,11 +51,11 @@ export default async function EventsPage({searchParams}: {
|
|||
toDate.toDate(),
|
||||
))?.docs) || [];
|
||||
|
||||
if (!paginatedOccurrences) {
|
||||
if (!paginatedEvents) {
|
||||
return <Error statusCode={503} message={"Veranstaltungen konnten nicht geladen werden."}/>;
|
||||
}
|
||||
|
||||
const events = transformOccurrences(paginatedOccurrences.docs)
|
||||
const events = transformEvents(paginatedEvents.docs)
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ 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 { NextOccurrencesTable as NextOccurrencesTable_5b561f1308e1f6674a6813c6174350fc } from '@/admin/components/NextOccurrencesTable/NextOccurrencesTable'
|
||||
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'
|
||||
|
|
@ -34,7 +33,6 @@ export const importMap = {
|
|||
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@/admin/components/NextOccurrencesTable/NextOccurrencesTable#NextOccurrencesTable": NextOccurrencesTable_5b561f1308e1f6674a6813c6174350fc,
|
||||
"@payloadcms/plugin-search/client#LinkToDoc": LinkToDoc_aead06e4cbf6b2620c5c51c9ab283634,
|
||||
"@payloadcms/plugin-search/client#ReindexButton": ReindexButton_aead06e4cbf6b2620c5c51c9ab283634,
|
||||
"/components/Logo/Logo#default": default_9bcae99938dc292be0063ce32055e14c,
|
||||
|
|
|
|||
|
|
@ -1,76 +0,0 @@
|
|||
import { CollectionConfig } from 'payload'
|
||||
import { isAdminOrEmployee } from '@/collections/access/admin'
|
||||
|
||||
export const EventOccurrences: CollectionConfig = {
|
||||
slug: 'eventOccurrence',
|
||||
labels: {
|
||||
singular: {
|
||||
de: 'Veranstaltungs-Termin',
|
||||
},
|
||||
plural: {
|
||||
de: 'Veranstaltungs-Termine',
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'event',
|
||||
type: 'relationship',
|
||||
relationTo: 'event',
|
||||
required: true,
|
||||
index: true,
|
||||
label: {
|
||||
de: 'Veranstaltung',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
type: 'date',
|
||||
required: true,
|
||||
index: true,
|
||||
label: {
|
||||
de: 'Datum',
|
||||
},
|
||||
admin: {
|
||||
date: {
|
||||
pickerAppearance: 'dayAndTime',
|
||||
timeIntervals: 15,
|
||||
timeFormat: 'HH:mm',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'cancelled',
|
||||
type: 'checkbox',
|
||||
required: true,
|
||||
defaultValue: false,
|
||||
label: {
|
||||
de: 'Abgesagt',
|
||||
},
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'generated',
|
||||
type: 'checkbox',
|
||||
defaultValue: false,
|
||||
label: {
|
||||
de: 'Automatisch erzeugt',
|
||||
},
|
||||
admin: {
|
||||
readOnly: true,
|
||||
position: 'sidebar',
|
||||
},
|
||||
},
|
||||
],
|
||||
admin: {
|
||||
defaultColumns: ['date', 'event', 'cancelled'],
|
||||
hidden: false,
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
create: isAdminOrEmployee(),
|
||||
update: isAdminOrEmployee(),
|
||||
delete: isAdminOrEmployee(),
|
||||
},
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ import { CollectionConfig } from 'payload'
|
|||
import { Group, User } from '@/payload-types'
|
||||
import { fetchEventById } from '@/fetch/events'
|
||||
import { isPublishedPublic } from '@/collections/access/public'
|
||||
import { regenerateOccurrencesForEvent } from '@/jobs/generateEventOccurrences'
|
||||
|
||||
export const Events: CollectionConfig = {
|
||||
slug: 'event',
|
||||
|
|
@ -16,233 +15,170 @@ export const Events: CollectionConfig = {
|
|||
},
|
||||
fields: [
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
label: { de: 'Allgemein' },
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
label: {
|
||||
de: 'Titel',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
type: 'date',
|
||||
required: true,
|
||||
label: {
|
||||
de: 'Datum',
|
||||
},
|
||||
admin: {
|
||||
date: {
|
||||
pickerAppearance: 'dayAndTime',
|
||||
timeIntervals: 15,
|
||||
timeFormat: 'HH:mm'
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'location',
|
||||
type: 'relationship',
|
||||
relationTo: 'locations',
|
||||
required: true,
|
||||
label: {
|
||||
de: 'Location'
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'shortDescription',
|
||||
type: 'textarea',
|
||||
required: true,
|
||||
label: {
|
||||
de: 'Kurzumschreibung (max. 200)',
|
||||
},
|
||||
maxLength: 200
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'textarea',
|
||||
required: true,
|
||||
label: {
|
||||
de: 'Einladung',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'contact',
|
||||
type: 'relationship',
|
||||
relationTo: 'contactPerson',
|
||||
label: {
|
||||
de: "Ansprechperson"
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'rsvpLink',
|
||||
type: 'text',
|
||||
required: false,
|
||||
label: {
|
||||
de: "Anmeldelink"
|
||||
}
|
||||
},
|
||||
],
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
label: {
|
||||
de: 'Titel',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
type: 'date',
|
||||
required: true,
|
||||
label: {
|
||||
de: 'Datum',
|
||||
},
|
||||
admin: {
|
||||
date: {
|
||||
pickerAppearance: 'dayAndTime',
|
||||
timeIntervals: 15,
|
||||
timeFormat: 'HH:mm'
|
||||
},
|
||||
{
|
||||
label: { de: 'Zuordnung' },
|
||||
fields: [
|
||||
{
|
||||
name: 'parish',
|
||||
type: 'relationship',
|
||||
relationTo: 'parish',
|
||||
hasMany: true,
|
||||
label: {
|
||||
de: 'Gemeinde',
|
||||
},
|
||||
validate: (value, options) => {
|
||||
let user = options.req.user
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'location',
|
||||
type: 'relationship',
|
||||
relationTo: 'locations',
|
||||
required: true,
|
||||
label: {
|
||||
de: 'Location'
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'parish',
|
||||
type: 'relationship',
|
||||
relationTo: 'parish',
|
||||
hasMany: true,
|
||||
label: {
|
||||
de: 'Gemeinde',
|
||||
},
|
||||
validate: (value, options) => {
|
||||
let user = options.req.user
|
||||
|
||||
if (!user) {
|
||||
return 'You are not allowed to do this'
|
||||
}
|
||||
if (!user) {
|
||||
return 'You are not allowed to do this'
|
||||
}
|
||||
|
||||
if (user.roles === 'user' && value && value.length > 0) {
|
||||
return 'Sie sind nur erlaubt Veranstaltungen für Gruppen zu erstellen.'
|
||||
}
|
||||
if (user.roles === 'user' && value && value.length > 0) {
|
||||
return 'Sie sind nur erlaubt Veranstaltungen für Gruppen zu erstellen.'
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'group',
|
||||
type: 'relationship',
|
||||
relationTo: 'group',
|
||||
hasMany: true,
|
||||
label: {
|
||||
de: 'Gruppe',
|
||||
},
|
||||
access: {
|
||||
update: ({req: { user}, data}) => {
|
||||
if(user && (user.roles == "admin" || user.roles =="employee")) {
|
||||
return true
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'group',
|
||||
type: 'relationship',
|
||||
relationTo: 'group',
|
||||
hasMany: true,
|
||||
label: {
|
||||
de: 'Gruppe',
|
||||
},
|
||||
access: {
|
||||
update: ({req: { user}, data}) => {
|
||||
if(user && (user.roles == "admin" || user.roles =="employee")) {
|
||||
return true
|
||||
}
|
||||
|
||||
if(hasGroup(user, data)) {
|
||||
return true
|
||||
}
|
||||
if(hasGroup(user, data)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
},
|
||||
validate: (value, options: { req: { user: any } }) => {
|
||||
let user = options.req.user
|
||||
return false
|
||||
}
|
||||
},
|
||||
validate: (value, options: { req: { user: any } }) => {
|
||||
let user = options.req.user
|
||||
|
||||
if (!user) {
|
||||
return 'You are not allowed to do this'
|
||||
}
|
||||
if (!user) {
|
||||
return 'You are not allowed to do this'
|
||||
}
|
||||
|
||||
if (user.roles === 'user') {
|
||||
if(!Array.isArray(value) || value.length === 0) {
|
||||
return 'Sie müssen die Veranstaltung verknüpfen mit ihrer Gruppe.'
|
||||
}
|
||||
if (user.roles === 'user') {
|
||||
if(!Array.isArray(value) || value.length === 0) {
|
||||
return 'Sie müssen die Veranstaltung verknüpfen mit ihrer Gruppe.'
|
||||
}
|
||||
|
||||
if(!Array.isArray(user.groups) || user.groups.length === 0) {
|
||||
return "Sie sind kein Mitglied einer Gruppe, und können deswegen keine Veranstaltung erstellen."
|
||||
}
|
||||
if(!Array.isArray(user.groups) || user.groups.length === 0) {
|
||||
return "Sie sind kein Mitglied einer Gruppe, und können deswegen keine Veranstaltung erstellen."
|
||||
}
|
||||
|
||||
if(!value.every(id => user.groups.includes(id))) {
|
||||
return "Sie sind nur berechtigt Veranstaltungen für ihrer Gruppe zu erstellen"
|
||||
}
|
||||
}
|
||||
if(!value.every(id => user.groups.includes(id))) {
|
||||
return "Sie sind nur berechtigt Veranstaltungen für ihrer Gruppe zu erstellen"
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: { de: 'Medien' },
|
||||
fields: [
|
||||
{
|
||||
name: 'photo',
|
||||
label: {
|
||||
de: 'Foto',
|
||||
},
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
},
|
||||
{
|
||||
name: 'flyer',
|
||||
label: {
|
||||
de: "Flyer (PDF)"
|
||||
},
|
||||
type: 'upload',
|
||||
relationTo: 'documents'
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: { de: 'Wiederholung' },
|
||||
fields: [
|
||||
{
|
||||
name: 'recurrenceType',
|
||||
type: 'select',
|
||||
required: true,
|
||||
defaultValue: 'none',
|
||||
label: {
|
||||
de: 'Wiederholung',
|
||||
},
|
||||
options: [
|
||||
{ label: 'Einmalig', value: 'none' },
|
||||
{ label: 'Wöchentlich', value: 'weekly' },
|
||||
{ label: 'Alle 2 Wochen', value: 'biweekly' },
|
||||
],
|
||||
admin: {
|
||||
description:
|
||||
'Bei wiederkehrenden Veranstaltungen werden automatisch Termine für die nächsten Wochen erzeugt. Wochentag und Uhrzeit werden aus dem Datumsfeld übernommen.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'endDate',
|
||||
type: 'date',
|
||||
label: {
|
||||
de: 'Enddatum',
|
||||
},
|
||||
admin: {
|
||||
date: {
|
||||
pickerAppearance: 'dayOnly',
|
||||
},
|
||||
description: 'Optional. Nach diesem Datum werden keine weiteren Termine erzeugt.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'cancelled',
|
||||
type: 'checkbox',
|
||||
required: true,
|
||||
label: {
|
||||
de: 'Abgesagt',
|
||||
},
|
||||
defaultValue: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: { de: 'Nächste Termine' },
|
||||
fields: [
|
||||
{
|
||||
name: 'nextOccurrences',
|
||||
type: 'ui',
|
||||
admin: {
|
||||
components: {
|
||||
Field: {
|
||||
path: '@/admin/components/NextOccurrencesTable/NextOccurrencesTable#NextOccurrencesTable',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
return true
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'contact',
|
||||
type: 'relationship',
|
||||
relationTo: 'contactPerson',
|
||||
label: {
|
||||
de: "Ansprechperson"
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'shortDescription',
|
||||
type: 'textarea',
|
||||
required: true,
|
||||
label: {
|
||||
de: 'Kurzumschreibung (max. 200)',
|
||||
},
|
||||
maxLength: 200
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'textarea',
|
||||
required: true,
|
||||
label: {
|
||||
de: 'Einladung',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'rsvpLink',
|
||||
type: 'text',
|
||||
required: false,
|
||||
label: {
|
||||
de: "Anmeldelink"
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'photo',
|
||||
label: {
|
||||
de: 'Foto',
|
||||
},
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
},
|
||||
{
|
||||
name: 'flyer',
|
||||
label: {
|
||||
de: "Flyer (PDF)"
|
||||
},
|
||||
type: 'upload',
|
||||
relationTo: 'documents'
|
||||
},
|
||||
{
|
||||
name: 'cancelled',
|
||||
type: 'checkbox',
|
||||
required: true,
|
||||
label: {
|
||||
de: 'Abgesagt',
|
||||
},
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
name: 'isRecurring',
|
||||
type: 'checkbox',
|
||||
required: true,
|
||||
label: {
|
||||
de: 'Regelmäßig',
|
||||
},
|
||||
defaultValue: false,
|
||||
},
|
||||
],
|
||||
admin: {
|
||||
|
|
@ -279,53 +215,6 @@ export const Events: CollectionConfig = {
|
|||
return false
|
||||
},
|
||||
},
|
||||
hooks: {
|
||||
afterChange: [
|
||||
async ({ doc, previousDoc, req }) => {
|
||||
try {
|
||||
await regenerateOccurrencesForEvent({
|
||||
event: doc,
|
||||
payload: req.payload,
|
||||
req,
|
||||
})
|
||||
|
||||
// Propagate event.cancelled to all future occurrences when it flips,
|
||||
// so toggling the event-level flag acts as a master switch. Skipped
|
||||
// when unchanged so unrelated saves don't overwrite per-occurrence
|
||||
// cancellations set from the "Nächste Termine" tab.
|
||||
if (doc.cancelled !== previousDoc?.cancelled) {
|
||||
await req.payload.update({
|
||||
collection: 'eventOccurrence',
|
||||
where: {
|
||||
and: [
|
||||
{ event: { equals: doc.id } },
|
||||
{ date: { greater_than_equal: new Date().toISOString() } },
|
||||
],
|
||||
},
|
||||
data: { cancelled: doc.cancelled },
|
||||
req,
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
req.payload.logger.error(
|
||||
{ err, eventId: doc.id },
|
||||
'Failed to regenerate event occurrences',
|
||||
)
|
||||
}
|
||||
},
|
||||
],
|
||||
beforeDelete: [
|
||||
async ({ id, req }) => {
|
||||
// Cascade before the event row is removed so the FK from
|
||||
// eventOccurrence.event (required) doesn't abort the transaction.
|
||||
await req.payload.delete({
|
||||
collection: 'eventOccurrence',
|
||||
where: { event: { equals: id } },
|
||||
req,
|
||||
})
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -354,3 +243,4 @@ const hasGroup = (user: null | User , data: Partial<any> | undefined) => {
|
|||
return user.groups.includes(group.id)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ import { ButtonBlock } from '@/collections/blocks/Button'
|
|||
import { YoutubePlayerBlock } from '@/collections/blocks/YoutubePlayer'
|
||||
import { isPublishedPublic } from '@/collections/access/public'
|
||||
import { ImageCardsBlock } from '@/collections/blocks/ImageCards'
|
||||
import { CollapsiblesBlock } from '@/collections/blocks/Collapsibles'
|
||||
import { TitleBlock } from '@/collections/blocks/Title'
|
||||
|
||||
export const Groups: CollectionConfig = {
|
||||
slug: 'group',
|
||||
|
|
@ -81,7 +79,6 @@ export const Groups: CollectionConfig = {
|
|||
name: 'content',
|
||||
type: 'blocks',
|
||||
blocks: [
|
||||
TitleBlock,
|
||||
ParagraphBlock,
|
||||
GalleryBlock,
|
||||
DocumentBlock,
|
||||
|
|
@ -90,7 +87,6 @@ export const Groups: CollectionConfig = {
|
|||
ContactformBlock,
|
||||
ButtonBlock,
|
||||
ImageCardsBlock,
|
||||
CollapsiblesBlock,
|
||||
]
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -17,13 +17,11 @@ 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 { CollapsiblesBlock } from '@/collections/blocks/Collapsibles'
|
||||
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 { NextPrevButtonsBlock } from '@/collections/blocks/NextPrevButtons'
|
||||
import { isPublishedPublic } from '@/collections/access/public'
|
||||
|
||||
export const Pages: CollectionConfig = {
|
||||
|
|
@ -101,13 +99,11 @@ export const Pages: CollectionConfig = {
|
|||
HorizontalRuleBlock,
|
||||
BlogSliderBlock,
|
||||
CollapsibleImageWithTextBlock,
|
||||
CollapsiblesBlock,
|
||||
MassTimesBlock,
|
||||
EventsBlock,
|
||||
ContactPersonBlock,
|
||||
ImageCardsBlock,
|
||||
ClassifiedsBlock,
|
||||
NextPrevButtonsBlock,
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { YoutubePlayerBlock } from '@/collections/blocks/YoutubePlayer'
|
|||
import { DonationAppeal } from '@/collections/blocks/DonationAppeal'
|
||||
import { ImageCardsBlock } from '@/collections/blocks/ImageCards'
|
||||
import { TitleBlock } from '@/collections/blocks/Title'
|
||||
import { CollapsiblesBlock } from '@/collections/blocks/Collapsibles'
|
||||
import { isPublishedPublic } from '@/collections/access/public'
|
||||
|
||||
export const Parish: CollectionConfig = {
|
||||
|
|
@ -124,7 +123,6 @@ export const Parish: CollectionConfig = {
|
|||
DonationAppeal,
|
||||
ImageCardsBlock,
|
||||
TitleBlock,
|
||||
CollapsiblesBlock,
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
import { Block } from 'payload'
|
||||
|
||||
export const CollapsiblesBlock: Block = {
|
||||
slug: 'collapsibles',
|
||||
labels: {
|
||||
singular: {
|
||||
de: 'Aufklappbare Liste',
|
||||
},
|
||||
plural: {
|
||||
de: 'Aufklappbare Listen',
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'items',
|
||||
type: 'array',
|
||||
required: true,
|
||||
minRows: 1,
|
||||
labels: {
|
||||
singular: {
|
||||
de: 'Eintrag',
|
||||
},
|
||||
plural: {
|
||||
de: 'Einträge',
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
label: {
|
||||
de: 'Titel',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'content',
|
||||
type: 'richText',
|
||||
required: true,
|
||||
label: {
|
||||
de: 'Inhalt',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
@ -26,20 +26,5 @@ export const MassTimesBlock: Block = {
|
|||
de: 'Untertitel',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'churches',
|
||||
label: {
|
||||
de: 'Kirchengebäuden',
|
||||
},
|
||||
type: 'relationship',
|
||||
relationTo: 'church',
|
||||
hasMany: true,
|
||||
admin: {
|
||||
allowCreate: false,
|
||||
description: {
|
||||
de: 'Leer lassen, um alle Kirchen alphabetisch anzuzeigen.',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
import { Block } from 'payload'
|
||||
import { validateHref } from '@/globals/ValidateHref'
|
||||
|
||||
export const NextPrevButtonsBlock: Block = {
|
||||
slug: 'nextPrevButtons',
|
||||
labels: {
|
||||
singular: {
|
||||
de: 'Zurück-/Weiter-Buttons',
|
||||
},
|
||||
plural: {
|
||||
de: 'Zurück-/Weiter-Buttons',
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'prev',
|
||||
type: 'group',
|
||||
label: {
|
||||
de: 'Zurück',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
label: {
|
||||
de: 'Text',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'href',
|
||||
type: 'text',
|
||||
label: {
|
||||
de: 'Zieladresse',
|
||||
},
|
||||
validate: validateHref,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'next',
|
||||
type: 'group',
|
||||
label: {
|
||||
de: 'Weiter',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
label: {
|
||||
de: 'Text',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'href',
|
||||
type: 'text',
|
||||
label: {
|
||||
de: 'Zieladresse',
|
||||
},
|
||||
validate: validateHref,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { AdminMenu } from './AdminMenu'
|
||||
|
||||
const meta: Meta<typeof AdminMenu> = {
|
||||
component: AdminMenu,
|
||||
}
|
||||
|
||||
type Story = StoryObj<typeof AdminMenu>
|
||||
export default meta
|
||||
|
||||
export const Authenticated: Story = {
|
||||
args: {
|
||||
collection: 'blog',
|
||||
id: 'some_id',
|
||||
isAuthenticated: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const AuthenticatedWithoutId: Story = {
|
||||
args: {
|
||||
collection: 'blog',
|
||||
isAuthenticated: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const NotAuthenticated: Story = {
|
||||
args: {
|
||||
collection: 'blog',
|
||||
id: 'some_id',
|
||||
isAuthenticated: false,
|
||||
},
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { Arrow } from './Arrow'
|
||||
|
||||
const meta: Meta<typeof Arrow> = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { Banner } from './Banner'
|
||||
|
||||
const meta: Meta<typeof Banner> = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { BlogExcerpt } from './BlogExcerpt'
|
||||
|
||||
const meta: Meta<typeof BlogExcerpt> = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from "@storybook/nextjs-vite"
|
||||
import { Meta, StoryObj } from "@storybook/react"
|
||||
import { Button } from './Button'
|
||||
|
||||
const meta: Meta<typeof Button> = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { ChurchCard } from './ChurchCard'
|
||||
|
||||
const meta: Meta<typeof ChurchCard> = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { ChurchIcon } from './ChurchIcon'
|
||||
|
||||
const meta: Meta<typeof ChurchIcon> = {
|
||||
|
|
|
|||
|
|
@ -1,61 +0,0 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { SerializedEditorState } from 'lexical'
|
||||
import { Ad } from './Ad'
|
||||
|
||||
const meta: Meta<typeof Ad> = {
|
||||
component: Ad,
|
||||
}
|
||||
|
||||
type Story = StoryObj<typeof Ad>
|
||||
export default meta
|
||||
|
||||
const makeAdText = (text: string): SerializedEditorState =>
|
||||
({
|
||||
root: {
|
||||
type: 'root',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
direction: 'ltr',
|
||||
children: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
direction: 'ltr',
|
||||
textFormat: 0,
|
||||
textStyle: '',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text,
|
||||
detail: 0,
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}) as unknown as SerializedEditorState
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
text: makeAdText(
|
||||
'Suche Helfer für unseren Gemeindegarten. Wir treffen uns jeden Samstag um 10 Uhr.',
|
||||
),
|
||||
contact: 'gemeinde@drei-koenige-berlin.de',
|
||||
},
|
||||
}
|
||||
|
||||
export const LongerText: Story = {
|
||||
args: {
|
||||
text: makeAdText(
|
||||
'Biete Klavierunterricht für Anfänger und Fortgeschrittene. Unterricht im Pfarrheim oder bei Ihnen zuhause möglich. Freue mich auf Ihren Anruf oder Ihre E-Mail.',
|
||||
),
|
||||
contact: 'klavier@example.com',
|
||||
},
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { SerializedEditorState } from 'lexical'
|
||||
import { Classifieds } from './Classifieds'
|
||||
|
||||
const meta: Meta<typeof Classifieds> = {
|
||||
component: Classifieds,
|
||||
}
|
||||
|
||||
type Story = StoryObj<typeof Classifieds>
|
||||
export default meta
|
||||
|
||||
const makeAdText = (text: string): SerializedEditorState =>
|
||||
({
|
||||
root: {
|
||||
type: 'root',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
direction: 'ltr',
|
||||
children: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
direction: 'ltr',
|
||||
textFormat: 0,
|
||||
textStyle: '',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text,
|
||||
detail: 0,
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}) as unknown as SerializedEditorState
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
ads: [
|
||||
{
|
||||
id: '1',
|
||||
text: makeAdText(
|
||||
'Suche Helfer für unseren Gemeindegarten. Wir treffen uns jeden Samstag um 10 Uhr.',
|
||||
),
|
||||
email: 'gemeinde@drei-koenige-berlin.de',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
text: makeAdText(
|
||||
'Biete Klavierunterricht für Anfänger und Fortgeschrittene.',
|
||||
),
|
||||
email: 'klavier@example.com',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
text: makeAdText('Verschenke Kinderfahrrad, gut erhalten.'),
|
||||
email: 'fahrrad@example.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
ads: [],
|
||||
},
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Collapsible } from './Collapsible'
|
||||
|
||||
const meta: Meta<typeof Collapsible> = {
|
||||
component: Collapsible,
|
||||
}
|
||||
|
||||
type Story = StoryObj<typeof Collapsible>
|
||||
export default meta
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
title: 'Wann finden die Gottesdienste statt?',
|
||||
children: 'Unsere Gottesdienste finden jeden Sonntag um 10:00 Uhr statt.',
|
||||
},
|
||||
}
|
||||
|
||||
export const OpenByDefault: Story = {
|
||||
args: {
|
||||
title: 'Wo kann ich parken?',
|
||||
defaultOpen: true,
|
||||
children:
|
||||
'Direkt vor der Kirche stehen Parkplätze zur Verfügung. Weitere Parkplätze finden Sie in den umliegenden Straßen.',
|
||||
},
|
||||
}
|
||||
|
||||
export const LongContent: Story = {
|
||||
args: {
|
||||
title: 'Wie kann ich Mitglied werden?',
|
||||
children: (
|
||||
<>
|
||||
<p>
|
||||
Um Mitglied unserer Gemeinde zu werden, können Sie sich direkt an das
|
||||
Pfarrbüro wenden. Dort erhalten Sie alle notwendigen Informationen und
|
||||
Formulare.
|
||||
</p>
|
||||
<p>
|
||||
Wir freuen uns über jedes neue Mitglied und laden Sie herzlich ein,
|
||||
unsere Gemeinschaft kennenzulernen. Neben den regelmäßigen
|
||||
Gottesdiensten gibt es zahlreiche Gruppen und Veranstaltungen, an
|
||||
denen Sie teilnehmen können.
|
||||
</p>
|
||||
<p>
|
||||
Bei Fragen stehen Ihnen unsere Mitarbeiter gerne telefonisch, per
|
||||
E-Mail oder persönlich zur Verfügung.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
export const ListOfThree: Story = {
|
||||
render: () => (
|
||||
<div>
|
||||
<Collapsible title="Wann finden die Gottesdienste statt?">
|
||||
Unsere Gottesdienste finden jeden Sonntag um 10:00 Uhr statt.
|
||||
</Collapsible>
|
||||
<Collapsible title="Wo kann ich parken?">
|
||||
Direkt vor der Kirche stehen Parkplätze zur Verfügung.
|
||||
</Collapsible>
|
||||
<Collapsible title="Wie erreiche ich das Pfarrbüro?">
|
||||
Das Pfarrbüro ist montags bis freitags von 9:00 bis 12:00 Uhr geöffnet.
|
||||
</Collapsible>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
'use client'
|
||||
|
||||
import React, { useEffect, useId, useRef, useState } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import { CollapsibleArrow } from '@/components/CollapsibleArrow/CollapsibleArrow'
|
||||
import styles from './styles.module.scss'
|
||||
|
||||
type CollapsibleProps = {
|
||||
title: string
|
||||
children: React.ReactNode
|
||||
defaultOpen?: boolean
|
||||
id?: string
|
||||
}
|
||||
|
||||
export const Collapsible = ({
|
||||
title,
|
||||
children,
|
||||
defaultOpen = false,
|
||||
id,
|
||||
}: CollapsibleProps) => {
|
||||
const reactId = useId()
|
||||
const baseId = id ?? reactId
|
||||
const headerId = `${baseId}-header`
|
||||
const panelId = `${baseId}-panel`
|
||||
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen)
|
||||
const [contentHeight, setContentHeight] = useState(0)
|
||||
const contentRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const measure = () => {
|
||||
if (contentRef.current) {
|
||||
setContentHeight(contentRef.current.scrollHeight)
|
||||
}
|
||||
}
|
||||
measure()
|
||||
window.addEventListener('resize', measure)
|
||||
return () => window.removeEventListener('resize', measure)
|
||||
}, [children])
|
||||
|
||||
const toggle = () => setIsOpen(v => !v)
|
||||
|
||||
return (
|
||||
<div className={styles.collapsible}>
|
||||
<button
|
||||
type="button"
|
||||
id={headerId}
|
||||
className={styles.header}
|
||||
aria-expanded={isOpen}
|
||||
aria-controls={panelId}
|
||||
onClick={toggle}
|
||||
>
|
||||
<span className={styles.title}>{title}</span>
|
||||
<span aria-hidden="true" className={styles.arrow}>
|
||||
<CollapsibleArrow direction={isOpen ? 'UP' : 'DOWN'} />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
id={panelId}
|
||||
role="region"
|
||||
aria-labelledby={headerId}
|
||||
aria-hidden={!isOpen}
|
||||
inert={!isOpen}
|
||||
ref={contentRef}
|
||||
className={classNames(styles.content, { [styles.open]: isOpen })}
|
||||
style={{ maxHeight: isOpen ? contentHeight : 0 }}
|
||||
>
|
||||
<div className={styles.inner}>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
@import 'template.scss';
|
||||
|
||||
.collapsible {
|
||||
border-bottom: 1px solid $border-color-light;
|
||||
}
|
||||
|
||||
.header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 20px 0;
|
||||
min-height: 44px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
font-family: inherit;
|
||||
color: $base-color;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.header:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.header:focus-visible {
|
||||
outline: 2px solid $base-color;
|
||||
outline-offset: 4px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
line-height: 1.4;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 400ms ease-in-out;
|
||||
}
|
||||
|
||||
.inner {
|
||||
padding: 0 0 20px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.content {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { CollapsibleArrow } from './CollapsibleArrow'
|
||||
|
||||
const meta: Meta<typeof CollapsibleArrow> = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { ContactPerson } from './ContactPerson'
|
||||
|
||||
const meta: Meta<typeof ContactPerson> = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { ContactPersonList } from './ContactPersonList'
|
||||
|
||||
const meta: Meta<typeof ContactPersonList> = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import portrait from "./portrait.jpeg"
|
||||
import { ContactPerson2 } from './ContactPerson2'
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.photo {
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import portrait from '../ContactPerson2/portrait.jpeg'
|
||||
import { ContactPersonCard } from './ContactPersonCard'
|
||||
|
||||
const meta: Meta<typeof ContactPersonCard> = {
|
||||
component: ContactPersonCard,
|
||||
}
|
||||
|
||||
type Story = StoryObj<typeof ContactPersonCard>
|
||||
export default meta
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
contact: {
|
||||
id: 'some_id',
|
||||
name: 'Pfr. M. Mustermann',
|
||||
role: 'Pfarrer',
|
||||
email: 'm.mustermann@drei-koenige-berlin.de',
|
||||
telephone: '+49 30 1234567',
|
||||
createdAt: '2021-03-01T00:00:00.000Z',
|
||||
updatedAt: '2021-03-01T00:00:00.000Z',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const WithPhoto: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
photo: portrait,
|
||||
},
|
||||
}
|
||||
|
||||
export const MinimalWithPhoto: Story = {
|
||||
args: {
|
||||
contact: {
|
||||
id: 'some_id',
|
||||
name: 'Pfr. M. Mustermann',
|
||||
createdAt: '2021-03-01T00:00:00.000Z',
|
||||
updatedAt: '2021-03-01T00:00:00.000Z',
|
||||
},
|
||||
photo: portrait,
|
||||
},
|
||||
}
|
||||
|
||||
export const NullContact: Story = {
|
||||
args: {
|
||||
contact: null,
|
||||
},
|
||||
}
|
||||
|
||||
export const StringContact: Story = {
|
||||
args: {
|
||||
contact: 'some_id_string',
|
||||
},
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { Container } from './Container'
|
||||
|
||||
const meta: Meta<typeof Container> = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { Cross } from './Cross'
|
||||
|
||||
const meta: Meta<typeof Cross> = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { DonationAppeal } from './DonationAppeal'
|
||||
|
||||
const meta: Meta<typeof DonationAppeal> = {
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { DonationForm } from './DonationForm'
|
||||
|
||||
const meta: Meta<typeof DonationForm> = {
|
||||
component: DonationForm,
|
||||
}
|
||||
|
||||
type Story = StoryObj<typeof DonationForm>
|
||||
export default meta
|
||||
|
||||
export const Default: Story = {}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { Dropdown } from './Dropdown'
|
||||
|
||||
const meta: Meta<typeof Dropdown> = {
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import Error from './Error'
|
||||
|
||||
const meta: Meta<typeof Error> = {
|
||||
component: Error,
|
||||
}
|
||||
|
||||
type Story = StoryObj<typeof Error>
|
||||
export default meta
|
||||
|
||||
export const NotFound: Story = {
|
||||
args: {
|
||||
statusCode: 404,
|
||||
message: 'Die angeforderte Seite wurde nicht gefunden.',
|
||||
},
|
||||
}
|
||||
|
||||
export const ServerError: Story = {
|
||||
args: {
|
||||
statusCode: 500,
|
||||
message: 'Interner Serverfehler.',
|
||||
},
|
||||
}
|
||||
|
||||
export const WithoutMessage: Story = {
|
||||
args: {
|
||||
statusCode: 403,
|
||||
},
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { EventExcerpt, EventExcerptRow } from './EventExcerpt'
|
||||
import { TextDiv } from '@/components/Text/TextDiv'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { EventRow } from './EventRow'
|
||||
|
||||
const meta: Meta<typeof EventRow> = {
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Col } from './Col'
|
||||
|
||||
const meta: Meta<typeof Col> = {
|
||||
component: Col,
|
||||
}
|
||||
|
||||
type Story = StoryObj<typeof Col>
|
||||
export default meta
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: <div>Spalteninhalt</div>,
|
||||
},
|
||||
}
|
||||
|
||||
export const Empty: Story = {
|
||||
args: {},
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Row } from './Row'
|
||||
|
||||
const meta: Meta<typeof Row> = {
|
||||
component: Row,
|
||||
}
|
||||
|
||||
type Story = StoryObj<typeof Row>
|
||||
export default meta
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: (
|
||||
<>
|
||||
<div>Erste Spalte</div>
|
||||
<div>Zweite Spalte</div>
|
||||
<div>Dritte Spalte</div>
|
||||
</>
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
export const WithGap: Story = {
|
||||
args: {
|
||||
gap: 32,
|
||||
children: (
|
||||
<>
|
||||
<div>Links</div>
|
||||
<div>Rechts</div>
|
||||
</>
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
export const CenterAligned: Story = {
|
||||
args: {
|
||||
alignItems: 'center',
|
||||
gap: 16,
|
||||
children: (
|
||||
<>
|
||||
<div style={{ height: 40 }}>Kurz</div>
|
||||
<div style={{ height: 100 }}>Lang</div>
|
||||
</>
|
||||
),
|
||||
},
|
||||
}
|
||||
|
|
@ -53,7 +53,7 @@ export const AutoScroll = ({children, isScrolling, onTouch}: AutoScrollProps) =>
|
|||
}, 1);
|
||||
}
|
||||
|
||||
}}, [wrapper, step, setStep, direction, setDirection, isScrolling])
|
||||
}}, [wrapper, step, setStep, direction, setDirection])
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { Gallery } from './Gallery'
|
||||
import chris from "./../../assets/christophorus.jpeg"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { HR } from './HorizontalRule'
|
||||
|
||||
const meta: Meta<typeof HR> = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from "@storybook/nextjs-vite"
|
||||
import { Meta, StoryObj } from "@storybook/react"
|
||||
import { Image } from './Image'
|
||||
|
||||
const meta: Meta<typeof Image> = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { ImageCard } from './ImageCard'
|
||||
|
||||
const meta: Meta<typeof ImageCard> = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { Input } from './Input'
|
||||
|
||||
const meta: Meta<typeof Input> = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { Logo } from './Logo'
|
||||
|
||||
const meta: Meta<typeof Logo> = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { MainText } from './MainText'
|
||||
|
||||
const meta: Meta<typeof MainText> = {
|
||||
|
|
|
|||
|
|
@ -1,300 +0,0 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { MassGrid } from './MassGrid'
|
||||
|
||||
const meta: Meta<typeof MassGrid> = {
|
||||
component: MassGrid,
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
locations: [
|
||||
{
|
||||
id: 'clara',
|
||||
name: 'St. Clara',
|
||||
masses: [
|
||||
{
|
||||
id: '1',
|
||||
date: '2024-08-21T15:00:00.000Z',
|
||||
location: 'St. Clara',
|
||||
type: 'MASS',
|
||||
cancelled: false,
|
||||
updatedAt: '',
|
||||
createdAt: '',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
date: '2024-08-23T18:00:00.000Z',
|
||||
location: 'St. Clara',
|
||||
type: 'MASS',
|
||||
cancelled: false,
|
||||
updatedAt: '',
|
||||
createdAt: '',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
date: '2024-08-24T09:00:00.000Z',
|
||||
location: 'St. Clara',
|
||||
type: 'WORD',
|
||||
cancelled: false,
|
||||
updatedAt: '',
|
||||
createdAt: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'christopherus',
|
||||
name: 'St. Christopherus',
|
||||
masses: [
|
||||
{
|
||||
id: '4',
|
||||
date: '2024-08-22T10:00:00.000Z',
|
||||
location: 'St. Christopherus',
|
||||
type: 'MASS',
|
||||
cancelled: false,
|
||||
updatedAt: '',
|
||||
createdAt: '',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
date: '2024-08-25T17:00:00.000Z',
|
||||
location: 'St. Christopherus',
|
||||
type: 'FAMILY',
|
||||
cancelled: false,
|
||||
updatedAt: '',
|
||||
createdAt: '',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
date: '2024-08-26T15:00:00.000Z',
|
||||
location: 'St. Christopherus',
|
||||
type: 'MASS',
|
||||
cancelled: true,
|
||||
updatedAt: '',
|
||||
createdAt: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'marien',
|
||||
name: 'St. Marien',
|
||||
masses: [
|
||||
{
|
||||
id: '7',
|
||||
date: '2024-08-23T11:00:00.000Z',
|
||||
location: 'St. Marien',
|
||||
type: 'MASS',
|
||||
cancelled: false,
|
||||
updatedAt: '',
|
||||
createdAt: '',
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
date: '2024-08-24T19:00:00.000Z',
|
||||
location: 'St. Marien',
|
||||
type: 'MASS',
|
||||
cancelled: false,
|
||||
updatedAt: '',
|
||||
createdAt: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export const SingleLocation: Story = {
|
||||
args: {
|
||||
locations: [
|
||||
{
|
||||
id: 'clara',
|
||||
name: 'St. Clara',
|
||||
masses: [
|
||||
{
|
||||
id: '1',
|
||||
date: '2024-08-21T15:00:00.000Z',
|
||||
location: 'St. Clara',
|
||||
type: 'MASS',
|
||||
cancelled: false,
|
||||
updatedAt: '',
|
||||
createdAt: '',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
date: '2024-08-23T18:00:00.000Z',
|
||||
location: 'St. Clara',
|
||||
type: 'MASS',
|
||||
cancelled: false,
|
||||
updatedAt: '',
|
||||
createdAt: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
locations: [],
|
||||
},
|
||||
}
|
||||
|
||||
export const SevenChurches: Story = {
|
||||
args: {
|
||||
locations: [
|
||||
{
|
||||
id: 'clara',
|
||||
name: 'St. Clara',
|
||||
masses: [
|
||||
{
|
||||
id: '1',
|
||||
date: '2024-08-21T15:00:00.000Z',
|
||||
location: 'St. Clara',
|
||||
type: 'MASS',
|
||||
cancelled: false,
|
||||
updatedAt: '',
|
||||
createdAt: '',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
date: '2024-08-24T09:00:00.000Z',
|
||||
location: 'St. Clara',
|
||||
type: 'WORD',
|
||||
cancelled: false,
|
||||
updatedAt: '',
|
||||
createdAt: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'christopherus',
|
||||
name: 'St. Christopherus',
|
||||
masses: [
|
||||
{
|
||||
id: '3',
|
||||
date: '2024-08-22T10:00:00.000Z',
|
||||
location: 'St. Christopherus',
|
||||
type: 'MASS',
|
||||
cancelled: false,
|
||||
updatedAt: '',
|
||||
createdAt: '',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
date: '2024-08-25T17:00:00.000Z',
|
||||
location: 'St. Christopherus',
|
||||
type: 'FAMILY',
|
||||
cancelled: false,
|
||||
updatedAt: '',
|
||||
createdAt: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'marien',
|
||||
name: 'St. Marien',
|
||||
masses: [
|
||||
{
|
||||
id: '5',
|
||||
date: '2024-08-23T11:00:00.000Z',
|
||||
location: 'St. Marien',
|
||||
type: 'MASS',
|
||||
cancelled: false,
|
||||
updatedAt: '',
|
||||
createdAt: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'johannes',
|
||||
name: 'St. Johannes Nepomuk II',
|
||||
masses: [
|
||||
{
|
||||
id: '6',
|
||||
date: '2024-08-21T18:00:00.000Z',
|
||||
location: 'St. Johannes Nepomuk II',
|
||||
type: 'MASS',
|
||||
cancelled: false,
|
||||
updatedAt: '',
|
||||
createdAt: '',
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
date: '2024-08-24T10:30:00.000Z',
|
||||
location: 'St. Johannes Nepomuk II',
|
||||
type: 'MASS',
|
||||
cancelled: true,
|
||||
updatedAt: '',
|
||||
createdAt: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'peter',
|
||||
name: 'St. Peter',
|
||||
masses: [
|
||||
{
|
||||
id: '8',
|
||||
date: '2024-08-22T08:00:00.000Z',
|
||||
location: 'St. Peter',
|
||||
type: 'MASS',
|
||||
cancelled: false,
|
||||
updatedAt: '',
|
||||
createdAt: '',
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
date: '2024-08-25T11:00:00.000Z',
|
||||
location: 'St. Peter',
|
||||
type: 'MASS',
|
||||
cancelled: false,
|
||||
updatedAt: '',
|
||||
createdAt: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'paul',
|
||||
name: 'St. Paulus Of the holy Cross',
|
||||
masses: [
|
||||
{
|
||||
id: '10',
|
||||
date: '2024-08-23T17:00:00.000Z',
|
||||
location: 'St. Paul',
|
||||
type: 'WORD',
|
||||
cancelled: false,
|
||||
updatedAt: '',
|
||||
createdAt: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'elisabeth',
|
||||
name: 'St. Elisabeth',
|
||||
masses: [
|
||||
{
|
||||
id: '11',
|
||||
date: '2024-08-24T19:00:00.000Z',
|
||||
location: 'St. Elisabeth',
|
||||
type: 'FAMILY',
|
||||
cancelled: false,
|
||||
updatedAt: '',
|
||||
createdAt: '',
|
||||
},
|
||||
{
|
||||
id: '12',
|
||||
date: '2024-08-26T09:00:00.000Z',
|
||||
location: 'St. Elisabeth',
|
||||
type: 'MASS',
|
||||
cancelled: false,
|
||||
updatedAt: '',
|
||||
createdAt: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
|
@ -1,29 +1,11 @@
|
|||
import { Worship } from '@/payload-types'
|
||||
import { MassTable } from '@/components/MassTable/MassTable'
|
||||
import styles from './massgrid.module.scss'
|
||||
|
||||
export type MassGridLocation = {
|
||||
id: string
|
||||
name: string
|
||||
masses: Worship[]
|
||||
}
|
||||
|
||||
type MassGridProps = {
|
||||
locations: MassGridLocation[]
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const MassGrid = ({ locations }: MassGridProps) => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.grid}>
|
||||
{locations.map((location) => (
|
||||
<MassTable
|
||||
key={location.id}
|
||||
location={location.name}
|
||||
masses={location.masses}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
export const MassGrid = ({ children }: MassGridProps) => {
|
||||
return <div className={styles.container}>
|
||||
<div className={styles.grid}>{children}</div>
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { MassTable } from './MassTable'
|
||||
|
||||
const meta: Meta<typeof MassTable> = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { MassTableRow } from './MassTableRow'
|
||||
|
||||
const meta: Meta<typeof MassTableRow> = {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
.grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, 200px);
|
||||
justify-content: center;
|
||||
column-gap: 30px;
|
||||
row-gap: 30px;
|
||||
row-gap: 15px;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
max-width: 1200px
|
||||
}
|
||||
}
|
||||
|
|
@ -18,12 +18,6 @@
|
|||
.location {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
line-height: 1em;
|
||||
height: 70px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cancelled {
|
||||
|
|
@ -49,8 +43,8 @@
|
|||
|
||||
.table {
|
||||
width: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
display: grid;
|
||||
grid-row: span 3;
|
||||
grid-template-rows: subgrid;
|
||||
justify-items: center;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { MegaMenu } from './MegaMenu'
|
||||
|
||||
const meta: Meta<typeof MegaMenu> = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { Menu } from './Menu'
|
||||
|
||||
const meta: Meta<typeof Menu> = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { NextPrevButtons } from './NextPrevButtons'
|
||||
|
||||
const meta: Meta<typeof NextPrevButtons> = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { Pill } from './Pill'
|
||||
|
||||
const meta: Meta<typeof Pill> = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { PopupButton } from './PopupButton'
|
||||
|
||||
const meta: Meta<typeof PopupButton> = {
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { RandomPrayer } from './RandomPrayer'
|
||||
|
||||
const meta: Meta<typeof RandomPrayer> = {
|
||||
component: RandomPrayer,
|
||||
}
|
||||
|
||||
type Story = StoryObj<typeof RandomPrayer>
|
||||
export default meta
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
prayers: [
|
||||
'Vater unser im Himmel, geheiligt werde dein Name.',
|
||||
'Gegrüßet seist du, Maria, voll der Gnade, der Herr ist mit dir.',
|
||||
'Ehre sei dem Vater und dem Sohn und dem Heiligen Geist.',
|
||||
'Herr, bleibe bei uns, denn es will Abend werden.',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export const SinglePrayer: Story = {
|
||||
args: {
|
||||
prayers: ['Komm, Heiliger Geist, erfülle die Herzen deiner Gläubigen.'],
|
||||
},
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { RawHTML } from './RawHTML'
|
||||
|
||||
const meta: Meta<typeof RawHTML> = {
|
||||
component: RawHTML,
|
||||
}
|
||||
|
||||
type Story = StoryObj<typeof RawHTML>
|
||||
export default meta
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
html: '<h3>Willkommen</h3><p>Dies ist ein Absatz mit <strong>fettem</strong> und <em>kursivem</em> Text.</p>',
|
||||
},
|
||||
}
|
||||
|
||||
export const WithList: Story = {
|
||||
args: {
|
||||
html: '<h3>Gottesdienste</h3><ul><li>Sonntag 10:00 Uhr</li><li>Mittwoch 18:30 Uhr</li><li>Freitag 18:30 Uhr</li></ul>',
|
||||
},
|
||||
}
|
||||
|
||||
export const WithLink: Story = {
|
||||
args: {
|
||||
html: '<p>Besuchen Sie <a href="https://example.com">unsere Website</a> für weitere Informationen.</p>',
|
||||
},
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { Section } from './Section'
|
||||
|
||||
const meta: Meta<typeof Section> = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { SideSlider } from './SideSlider'
|
||||
|
||||
const meta: Meta<typeof SideSlider> = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
import { Testimony } from './Testimony'
|
||||
|
||||
const meta: Meta<typeof Testimony> = {
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { SerializedEditorState } from 'lexical'
|
||||
import { HTMLText } from './HTMLText'
|
||||
|
||||
const meta: Meta<typeof HTMLText> = {
|
||||
component: HTMLText,
|
||||
}
|
||||
|
||||
type Story = StoryObj<typeof HTMLText>
|
||||
export default meta
|
||||
|
||||
const sampleState: SerializedEditorState = {
|
||||
root: {
|
||||
type: 'root',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
direction: 'ltr',
|
||||
children: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
direction: 'ltr',
|
||||
textFormat: 0,
|
||||
textStyle: '',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.',
|
||||
detail: 0,
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
} as unknown as SerializedEditorState
|
||||
|
||||
export const ThreeFourth: Story = {
|
||||
args: {
|
||||
width: '3/4',
|
||||
data: sampleState,
|
||||
},
|
||||
}
|
||||
|
||||
export const Half: Story = {
|
||||
args: {
|
||||
width: '1/2',
|
||||
data: sampleState,
|
||||
},
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { P } from './Paragraph'
|
||||
|
||||
const meta: Meta<typeof P> = {
|
||||
component: P,
|
||||
}
|
||||
|
||||
type Story = StoryObj<typeof P>
|
||||
export default meta
|
||||
|
||||
export const ThreeFourth: Story = {
|
||||
args: {
|
||||
width: '3/4',
|
||||
children:
|
||||
'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.',
|
||||
},
|
||||
}
|
||||
|
||||
export const Half: Story = {
|
||||
args: {
|
||||
width: '1/2',
|
||||
children:
|
||||
'At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.',
|
||||
},
|
||||
}
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
import { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { SerializedEditorState } from 'lexical'
|
||||
import { RichText } from './RichText'
|
||||
|
||||
const meta: Meta<typeof RichText> = {
|
||||
component: RichText,
|
||||
}
|
||||
|
||||
type Story = StoryObj<typeof RichText>
|
||||
export default meta
|
||||
|
||||
const buildState = (
|
||||
children: SerializedEditorState['root']['children'],
|
||||
): SerializedEditorState =>
|
||||
({
|
||||
root: {
|
||||
type: 'root',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
direction: 'ltr',
|
||||
children,
|
||||
},
|
||||
}) as SerializedEditorState
|
||||
|
||||
const paragraph = (
|
||||
textNodes: {
|
||||
text: string
|
||||
format?: number
|
||||
}[],
|
||||
) => ({
|
||||
type: 'paragraph',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
direction: 'ltr',
|
||||
textFormat: 0,
|
||||
textStyle: '',
|
||||
children: textNodes.map((n) => ({
|
||||
type: 'text',
|
||||
format: n.format ?? 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: n.text,
|
||||
detail: 0,
|
||||
version: 1,
|
||||
})),
|
||||
})
|
||||
|
||||
export const SimpleParagraph: Story = {
|
||||
args: {
|
||||
data: buildState([
|
||||
paragraph([
|
||||
{ text: 'Willkommen bei der Pfarrei Heilige Drei Könige in Berlin.' },
|
||||
]),
|
||||
]),
|
||||
},
|
||||
}
|
||||
|
||||
export const Formatted: Story = {
|
||||
args: {
|
||||
data: buildState([
|
||||
paragraph([
|
||||
{ text: 'Dies ist ein ' },
|
||||
{ text: 'fettgedruckter', format: 1 },
|
||||
{ text: ' und ein ' },
|
||||
{ text: 'kursiver', format: 2 },
|
||||
{ text: ' Text.' },
|
||||
]),
|
||||
]),
|
||||
},
|
||||
}
|
||||
|
||||
export const MultipleParagraphs: Story = {
|
||||
args: {
|
||||
data: buildState([
|
||||
paragraph([
|
||||
{
|
||||
text: 'Unsere Gottesdienste finden jeden Sonntag um 10:00 Uhr statt.',
|
||||
},
|
||||
]),
|
||||
paragraph([
|
||||
{
|
||||
text: 'Herzlich willkommen sind alle, die mitfeiern und mitbeten möchten.',
|
||||
},
|
||||
]),
|
||||
]),
|
||||
},
|
||||
}
|
||||