Compare commits
6 commits
cb6e811c52
...
925c5a66a6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
925c5a66a6 | ||
|
|
e139b35018 | ||
|
|
79a261db92 | ||
|
|
8f0c47419b | ||
|
|
1160ce4a8b | ||
|
|
96e7beab22 |
21 changed files with 27830 additions and 25 deletions
|
|
@ -278,6 +278,24 @@ cutover) is documented in [`infra/runbooks/production-migration.md`](runbooks/pr
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Backups (production)
|
||||||
|
|
||||||
|
`infra/scripts/backup.sh` runs daily at 03:45 on the production server via
|
||||||
|
`/etc/cron.d/church-website-backup` (log: `/var/log/church-website-backup.log`):
|
||||||
|
|
||||||
|
- **Local** (`/opt/backups` on production): `pg_dump -Fc` of the database +
|
||||||
|
tarball of both upload volumes, 7 days retention (~5 GB).
|
||||||
|
- **Off-site** (staging server `178.104.35.59`, `/opt/backups/production`):
|
||||||
|
DB dumps with 14 days retention + an rsync mirror of the uploads (single
|
||||||
|
current copy — the staging server is short on disk, so no tarball history
|
||||||
|
there).
|
||||||
|
|
||||||
|
Production's root SSH key (`/root/.ssh/id_ed25519`) is authorized on the
|
||||||
|
staging server for the transfer. To restore, use the drop/restore + volume
|
||||||
|
steps from the migration runbook with the backup files as source.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Build fails with OOM
|
### Build fails with OOM
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ all:
|
||||||
production-vps:
|
production-vps:
|
||||||
ansible_host: 217.154.211.139
|
ansible_host: 217.154.211.139
|
||||||
ansible_user: root
|
ansible_user: root
|
||||||
ansible_ssh_private_key_file: ~/.ssh/id_ed25519
|
ansible_ssh_private_key_file: ~/.ssh/id_rsa
|
||||||
|
|
||||||
vars:
|
vars:
|
||||||
# No Forgejo on production — code is pulled from git.skick.app
|
# No Forgejo on production — code is pulled from git.skick.app
|
||||||
|
|
@ -19,13 +19,13 @@ all:
|
||||||
postgres_container_name: postgres
|
postgres_container_name: postgres
|
||||||
postgres_image: postgis/postgis:16-3.4
|
postgres_image: postgis/postgis:16-3.4
|
||||||
postgres_volume: pgdata
|
postgres_volume: pgdata
|
||||||
# Tuned for 4 vCPU / 8 GB shared with the Next.js app and on-server builds
|
# Tuned for 4 vCPU / 4 GB shared with the Next.js app and on-server builds
|
||||||
postgres_server_args: >-
|
postgres_server_args: >-
|
||||||
-c max_connections=40
|
-c max_connections=40
|
||||||
-c shared_buffers=512MB
|
-c shared_buffers=256MB
|
||||||
-c effective_cache_size=2GB
|
-c effective_cache_size=1GB
|
||||||
-c work_mem=16MB
|
-c work_mem=8MB
|
||||||
-c maintenance_work_mem=128MB
|
-c maintenance_work_mem=64MB
|
||||||
-c checkpoint_completion_target=0.9
|
-c checkpoint_completion_target=0.9
|
||||||
-c max_wal_size=1GB
|
-c max_wal_size=1GB
|
||||||
-c random_page_cost=1.1
|
-c random_page_cost=1.1
|
||||||
|
|
@ -44,6 +44,23 @@ all:
|
||||||
caddy_redirects:
|
caddy_redirects:
|
||||||
- from: www.hl-mutter-teresa-chemnitz.de
|
- from: www.hl-mutter-teresa-chemnitz.de
|
||||||
to: hl-mutter-teresa-chemnitz.de
|
to: hl-mutter-teresa-chemnitz.de
|
||||||
|
# Parish shortcut subdomains (replace the HTTP-only IONOS forwards;
|
||||||
|
# to_path drops the incoming URI and redirects to a fixed page)
|
||||||
|
- from: st-antonius.hl-mutter-teresa-chemnitz.de
|
||||||
|
to: hl-mutter-teresa-chemnitz.de
|
||||||
|
to_path: /gemeinde/st-antonius
|
||||||
|
- from: st-franziskus.hl-mutter-teresa-chemnitz.de
|
||||||
|
to: hl-mutter-teresa-chemnitz.de
|
||||||
|
to_path: /gemeinde/st-franziskus
|
||||||
|
- from: st-johannes-nepomuk.hl-mutter-teresa-chemnitz.de
|
||||||
|
to: hl-mutter-teresa-chemnitz.de
|
||||||
|
to_path: /gemeinde/st-johannes-nepomuk
|
||||||
|
- from: st-joseph.hl-mutter-teresa-chemnitz.de
|
||||||
|
to: hl-mutter-teresa-chemnitz.de
|
||||||
|
to_path: /gemeinde/st-joseph
|
||||||
|
- from: st-marien.hl-mutter-teresa-chemnitz.de
|
||||||
|
to: hl-mutter-teresa-chemnitz.de
|
||||||
|
to_path: /gemeinde/st-marien-zschopau
|
||||||
|
|
||||||
# App environments
|
# App environments
|
||||||
app_environments:
|
app_environments:
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,15 @@ all:
|
||||||
|
|
||||||
# Caddy
|
# Caddy
|
||||||
caddy_domains:
|
caddy_domains:
|
||||||
- domain: mutter-teresa.skick.app
|
|
||||||
proxy_port: 3001
|
|
||||||
- domain: mutter-teresa-test.skick.app
|
- domain: mutter-teresa-test.skick.app
|
||||||
proxy_port: 3002
|
proxy_port: 3002
|
||||||
- domain: git.skick.app
|
- domain: git.skick.app
|
||||||
proxy_port: 3003
|
proxy_port: 3003
|
||||||
|
# Old staging domain redirects to production (app-staging container is
|
||||||
|
# stopped; the site now lives on the production VPS)
|
||||||
|
caddy_redirects:
|
||||||
|
- from: mutter-teresa.skick.app
|
||||||
|
to: hl-mutter-teresa-chemnitz.de
|
||||||
|
|
||||||
# Forgejo
|
# Forgejo
|
||||||
forgejo_domain: git.skick.app
|
forgejo_domain: git.skick.app
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for redirect in caddy_redirects | default([]) %}
|
{% for redirect in caddy_redirects | default([]) %}
|
||||||
{{ redirect.from }} {
|
{{ redirect.from }} {
|
||||||
redir https://{{ redirect.to }}{uri} permanent
|
redir https://{{ redirect.to }}{{ redirect.to_path | default('{uri}') }} permanent
|
||||||
}
|
}
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ Sanity checks:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ssh $NEW "curl -sI http://127.0.0.1:3001 | head -1" # HTTP 200/30x
|
ssh $NEW "curl -sI http://127.0.0.1:3001 | head -1" # HTTP 200/30x
|
||||||
ssh $NEW "docker exec postgres psql -U postgres -c 'SHOW shared_buffers;'" # 512MB
|
ssh $NEW "docker exec postgres psql -U postgres -c 'SHOW shared_buffers;'" # 256MB
|
||||||
ssh $NEW "swapon --show" # 4G /swapfile
|
ssh $NEW "swapon --show" # 4G /swapfile
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -112,8 +112,12 @@ ssh $OLD "docker run --rm -v uploads-staging-documents:/src:ro alpine tar -C /sr
|
||||||
|
|
||||||
### 2c. Restart + verify
|
### 2c. Restart + verify
|
||||||
|
|
||||||
|
Recreate the container rather than just starting it — Next.js caches rendered
|
||||||
|
pages in the container's filesystem, and a plain `docker start` serves stale
|
||||||
|
pages (e.g. a cached 404 for `/`) from before the restore:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ssh $NEW "docker start app-production"
|
ssh $NEW "docker rm -f app-production && docker run -d --name app-production --restart unless-stopped --network church-website-net --env-file /opt/church-website/envs/production/.env -v uploads-production-media:/app/media -v uploads-production-documents:/app/documents -p 127.0.0.1:3001:3000 church-website:production"
|
||||||
ssh $NEW "docker exec -u 0 app-production chown -R 1001:1001 /app/media /app/documents"
|
ssh $NEW "docker exec -u 0 app-production chown -R 1001:1001 /app/media /app/documents"
|
||||||
ssh $NEW "curl -sI http://127.0.0.1:3001 | head -1" # 200
|
ssh $NEW "curl -sI http://127.0.0.1:3001 | head -1" # 200
|
||||||
ssh $NEW "curl -sI http://127.0.0.1:3001/admin | head -1" # 200/30x
|
ssh $NEW "curl -sI http://127.0.0.1:3001/admin | head -1" # 200/30x
|
||||||
|
|
|
||||||
34
infra/scripts/backup.sh
Normal file
34
infra/scripts/backup.sh
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Daily production backup.
|
||||||
|
# - Local point-in-time archives (DB dump + uploads tarball), 7 days retention.
|
||||||
|
# - Off-site copy on the staging server: DB dumps (14 days) + incremental
|
||||||
|
# uploads mirror (rsync, single current copy — the staging server is short
|
||||||
|
# on disk, so no remote tarball history).
|
||||||
|
# Installed at /opt/church-website/scripts/backup.sh, run by
|
||||||
|
# /etc/cron.d/church-website-backup.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DATE=$(date +%F)
|
||||||
|
BACKUP_DIR=/opt/backups
|
||||||
|
REMOTE=root@178.104.35.59
|
||||||
|
REMOTE_DIR=/opt/backups/production
|
||||||
|
VOLUMES_DIR=/var/lib/docker/volumes
|
||||||
|
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
|
||||||
|
# Local point-in-time backups
|
||||||
|
docker exec postgres pg_dump -U postgres -Fc church_website > "$BACKUP_DIR/db-$DATE.dump"
|
||||||
|
tar -czf "$BACKUP_DIR/uploads-$DATE.tar.gz" -C "$VOLUMES_DIR" \
|
||||||
|
uploads-production-media/_data uploads-production-documents/_data
|
||||||
|
|
||||||
|
# Off-site copy to the staging server
|
||||||
|
ssh -o StrictHostKeyChecking=accept-new "$REMOTE" "mkdir -p $REMOTE_DIR/db $REMOTE_DIR/uploads/media $REMOTE_DIR/uploads/documents"
|
||||||
|
scp -q "$BACKUP_DIR/db-$DATE.dump" "$REMOTE:$REMOTE_DIR/db/"
|
||||||
|
rsync -a --delete "$VOLUMES_DIR/uploads-production-media/_data/" "$REMOTE:$REMOTE_DIR/uploads/media/"
|
||||||
|
rsync -a --delete "$VOLUMES_DIR/uploads-production-documents/_data/" "$REMOTE:$REMOTE_DIR/uploads/documents/"
|
||||||
|
|
||||||
|
# Retention
|
||||||
|
find "$BACKUP_DIR" -type f -mtime +7 -delete
|
||||||
|
ssh "$REMOTE" "find $REMOTE_DIR/db -type f -mtime +14 -delete"
|
||||||
|
|
||||||
|
echo "$(date -Is) backup OK: db-$DATE.dump + uploads-$DATE.tar.gz (local), synced to $REMOTE"
|
||||||
|
|
@ -133,13 +133,14 @@ export const Churches: CollectionConfig = {
|
||||||
{ label: '2.', value: 'second' },
|
{ label: '2.', value: 'second' },
|
||||||
{ label: '3.', value: 'third' },
|
{ label: '3.', value: 'third' },
|
||||||
{ label: '4.', value: 'fourth' },
|
{ label: '4.', value: 'fourth' },
|
||||||
|
{ label: '5.', value: 'fifth' },
|
||||||
{ label: 'Letzter', value: 'last' },
|
{ label: 'Letzter', value: 'last' },
|
||||||
],
|
],
|
||||||
admin: {
|
admin: {
|
||||||
condition: (_data, siblingData) =>
|
condition: (_data, siblingData) =>
|
||||||
siblingData?.frequency === 'monthlyByWeekday',
|
siblingData?.frequency === 'monthlyByWeekday',
|
||||||
description:
|
description:
|
||||||
'z. B. „3.“ + „Sonntag“ = jeden 3. Sonntag im Monat',
|
'z. B. „3.“ + „Sonntag“ = jeden 3. Sonntag im Monat.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -325,6 +325,7 @@ export const Events: CollectionConfig = {
|
||||||
{ label: '2.', value: 'second' },
|
{ label: '2.', value: 'second' },
|
||||||
{ label: '3.', value: 'third' },
|
{ label: '3.', value: 'third' },
|
||||||
{ label: '4.', value: 'fourth' },
|
{ label: '4.', value: 'fourth' },
|
||||||
|
{ label: '5.', value: 'fifth' },
|
||||||
{ label: 'Letzter', value: 'last' },
|
{ label: 'Letzter', value: 'last' },
|
||||||
],
|
],
|
||||||
admin: {
|
admin: {
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ export const Worship: CollectionConfig = {
|
||||||
value: 'MASS',
|
value: 'MASS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Familien Messe',
|
label: 'Familiengottesdienst',
|
||||||
value: 'FAMILY',
|
value: 'FAMILY',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,6 @@ export const MassTableRow = ({
|
||||||
}: MassTableRowProps) => {
|
}: MassTableRowProps) => {
|
||||||
const [symbol, setSymbol] = useState('')
|
const [symbol, setSymbol] = useState('')
|
||||||
const time = useTime(date)
|
const time = useTime(date)
|
||||||
//const compactDate = useCompactDate(date)
|
|
||||||
//const day = useShortDayName(date)
|
|
||||||
const day = new Date(date).toLocaleDateString("de-DE", {weekday: 'long'})
|
const day = new Date(date).toLocaleDateString("de-DE", {weekday: 'long'})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -47,7 +45,7 @@ export const MassTableRow = ({
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{!cancelled && type === 'FAMILY' && (
|
{!cancelled && type === 'FAMILY' && (
|
||||||
<Image src={family} width={18} height={18} alt={'Familien Messe'} />
|
<Image src={family} width={18} height={18} alt={'Familiengottesdienst'} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!cancelled && type === 'WORD' && (
|
{!cancelled && type === 'WORD' && (
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ export const FooterView = ({ prayers, footer }: FooterViewProps) => {
|
||||||
))}
|
))}
|
||||||
<Col>
|
<Col>
|
||||||
<p>
|
<p>
|
||||||
<strong>Stoßgebet</strong>
|
<strong>Gebet</strong>
|
||||||
</p>
|
</p>
|
||||||
<RandomPrayer prayers={prayers} />
|
<RandomPrayer prayers={prayers} />
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,37 @@ describe('generateOccurrenceDates – monthlyByWeekday', () => {
|
||||||
expect(result.map((x) => x.getDate())).toEqual([23])
|
expect(result.map((x) => x.getDate())).toEqual([23])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('yields the 5th thursday only in months that have one', () => {
|
||||||
|
const entry: ScheduleEntry = {
|
||||||
|
frequency: 'monthlyByWeekday',
|
||||||
|
day: 'thursday',
|
||||||
|
weekOfMonth: 'fifth',
|
||||||
|
}
|
||||||
|
// 2026: Jul has 5 Thursdays (2,9,16,23,30), Aug 4 (6,13,20,27), Sep 4, Oct 5 (1,8,15,22,29), Nov 4, Dec 5 (3,10,17,24,31)
|
||||||
|
const result = generateOccurrenceDates(entry, d(2026, 7, 1), d(2026, 12, 31))
|
||||||
|
expect(result.map((x) => `${x.getMonth() + 1}-${x.getDate()}`)).toEqual([
|
||||||
|
'7-30',
|
||||||
|
'10-29',
|
||||||
|
'12-31',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('"fifth" and "last" differ in months with only 4 occurrences', () => {
|
||||||
|
// Aug 2026 Thursdays: 6, 13, 20, 27 — last = 27, fifth = none
|
||||||
|
const last = generateOccurrenceDates(
|
||||||
|
{ frequency: 'monthlyByWeekday', day: 'thursday', weekOfMonth: 'last' },
|
||||||
|
d(2026, 8, 1),
|
||||||
|
d(2026, 8, 31),
|
||||||
|
)
|
||||||
|
const fifth = generateOccurrenceDates(
|
||||||
|
{ frequency: 'monthlyByWeekday', day: 'thursday', weekOfMonth: 'fifth' },
|
||||||
|
d(2026, 8, 1),
|
||||||
|
d(2026, 8, 31),
|
||||||
|
)
|
||||||
|
expect(last.map((x) => x.getDate())).toEqual([27])
|
||||||
|
expect(fifth).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
it('works when the start-of-window is after that month\'s occurrence', () => {
|
it('works when the start-of-window is after that month\'s occurrence', () => {
|
||||||
// 3rd Sunday of April 2026 is 4/19. Start the window on 4/20.
|
// 3rd Sunday of April 2026 is 4/19. Start the window on 4/20.
|
||||||
// April should be skipped; May should still appear.
|
// April should be skipped; May should still appear.
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ export type ScheduleFrequency =
|
||||||
| 'monthlyByDate'
|
| 'monthlyByDate'
|
||||||
| 'monthlyByWeekday'
|
| 'monthlyByWeekday'
|
||||||
|
|
||||||
export type WeekOfMonth = 'first' | 'second' | 'third' | 'fourth' | 'last'
|
export type WeekOfMonth = 'first' | 'second' | 'third' | 'fourth' | 'fifth' | 'last'
|
||||||
|
|
||||||
export interface ScheduleEntry {
|
export interface ScheduleEntry {
|
||||||
frequency: ScheduleFrequency
|
frequency: ScheduleFrequency
|
||||||
|
|
@ -40,6 +40,7 @@ const WEEK_OF_MONTH_N: Record<WeekOfMonth, number> = {
|
||||||
second: 2,
|
second: 2,
|
||||||
third: 3,
|
third: 3,
|
||||||
fourth: 4,
|
fourth: 4,
|
||||||
|
fifth: 5,
|
||||||
last: -1,
|
last: -1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,7 +66,7 @@ const firstOccurrenceOnOrAfter = (from: Date, targetDay: number): Date => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For a given (year, month), return the date of the Nth occurrence of
|
* For a given (year, month), return the date of the Nth occurrence of
|
||||||
* `targetDay`. `n` is 1..4 for first..fourth, or -1 for "last".
|
* `targetDay`. `n` is 1..5 for first..fifth, or -1 for "last".
|
||||||
* Returns null if the month has no such occurrence (e.g. asking for the
|
* Returns null if the month has no such occurrence (e.g. asking for the
|
||||||
* 5th Monday in a month that only has 4).
|
* 5th Monday in a month that only has 4).
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
27660
src/migrations/20260828_100000_add_fifth_week_of_month.json
Normal file
27660
src/migrations/20260828_100000_add_fifth_week_of_month.json
Normal file
File diff suppressed because it is too large
Load diff
27
src/migrations/20260828_100000_add_fifth_week_of_month.ts
Normal file
27
src/migrations/20260828_100000_add_fifth_week_of_month.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
|
||||||
|
|
||||||
|
export async function up({ db }: MigrateUpArgs): Promise<void> {
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TYPE "public"."enum_church_recurring_schedule_week_of_month" ADD VALUE 'fifth' BEFORE 'last';
|
||||||
|
ALTER TYPE "public"."enum_event_recurrence_rules_week_of_month" ADD VALUE 'fifth' BEFORE 'last';
|
||||||
|
ALTER TYPE "public"."enum__event_v_version_recurrence_rules_week_of_month" ADD VALUE 'fifth' BEFORE 'last';`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down({ db }: MigrateDownArgs): Promise<void> {
|
||||||
|
await db.execute(sql`
|
||||||
|
UPDATE "church_recurring_schedule" SET "week_of_month" = 'last' WHERE "week_of_month" = 'fifth';
|
||||||
|
UPDATE "event_recurrence_rules" SET "week_of_month" = 'last' WHERE "week_of_month" = 'fifth';
|
||||||
|
UPDATE "_event_v_version_recurrence_rules" SET "week_of_month" = 'last' WHERE "week_of_month" = 'fifth';
|
||||||
|
ALTER TABLE "church_recurring_schedule" ALTER COLUMN "week_of_month" SET DATA TYPE text;
|
||||||
|
DROP TYPE "public"."enum_church_recurring_schedule_week_of_month";
|
||||||
|
CREATE TYPE "public"."enum_church_recurring_schedule_week_of_month" AS ENUM('first', 'second', 'third', 'fourth', 'last');
|
||||||
|
ALTER TABLE "church_recurring_schedule" ALTER COLUMN "week_of_month" SET DATA TYPE "public"."enum_church_recurring_schedule_week_of_month" USING "week_of_month"::"public"."enum_church_recurring_schedule_week_of_month";
|
||||||
|
ALTER TABLE "event_recurrence_rules" ALTER COLUMN "week_of_month" SET DATA TYPE text;
|
||||||
|
DROP TYPE "public"."enum_event_recurrence_rules_week_of_month";
|
||||||
|
CREATE TYPE "public"."enum_event_recurrence_rules_week_of_month" AS ENUM('first', 'second', 'third', 'fourth', 'last');
|
||||||
|
ALTER TABLE "event_recurrence_rules" ALTER COLUMN "week_of_month" SET DATA TYPE "public"."enum_event_recurrence_rules_week_of_month" USING "week_of_month"::"public"."enum_event_recurrence_rules_week_of_month";
|
||||||
|
ALTER TABLE "_event_v_version_recurrence_rules" ALTER COLUMN "week_of_month" SET DATA TYPE text;
|
||||||
|
DROP TYPE "public"."enum__event_v_version_recurrence_rules_week_of_month";
|
||||||
|
CREATE TYPE "public"."enum__event_v_version_recurrence_rules_week_of_month" AS ENUM('first', 'second', 'third', 'fourth', 'last');
|
||||||
|
ALTER TABLE "_event_v_version_recurrence_rules" ALTER COLUMN "week_of_month" SET DATA TYPE "public"."enum__event_v_version_recurrence_rules_week_of_month" USING "week_of_month"::"public"."enum__event_v_version_recurrence_rules_week_of_month";`)
|
||||||
|
}
|
||||||
|
|
@ -54,6 +54,7 @@ import * as migration_20260611_084920_add_image_with_text_block from './20260611
|
||||||
import * as migration_20260716_090032_add_user_parish_page_assignments from './20260716_090032_add_user_parish_page_assignments';
|
import * as migration_20260716_090032_add_user_parish_page_assignments from './20260716_090032_add_user_parish_page_assignments';
|
||||||
import * as migration_20260716_113205_add_blog_pinned from './20260716_113205_add_blog_pinned';
|
import * as migration_20260716_113205_add_blog_pinned from './20260716_113205_add_blog_pinned';
|
||||||
import * as migration_20260721_090435_add_mass_times_range from './20260721_090435_add_mass_times_range';
|
import * as migration_20260721_090435_add_mass_times_range from './20260721_090435_add_mass_times_range';
|
||||||
|
import * as migration_20260828_100000_add_fifth_week_of_month from './20260828_100000_add_fifth_week_of_month';
|
||||||
|
|
||||||
export const migrations = [
|
export const migrations = [
|
||||||
{
|
{
|
||||||
|
|
@ -336,4 +337,9 @@ export const migrations = [
|
||||||
down: migration_20260721_090435_add_mass_times_range.down,
|
down: migration_20260721_090435_add_mass_times_range.down,
|
||||||
name: '20260721_090435_add_mass_times_range'
|
name: '20260721_090435_add_mass_times_range'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
up: migration_20260828_100000_add_fifth_week_of_month.up,
|
||||||
|
down: migration_20260828_100000_add_fifth_week_of_month.down,
|
||||||
|
name: '20260828_100000_add_fifth_week_of_month',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,7 @@ export function EventPage(
|
||||||
<TextDiv text={description} />
|
<TextDiv text={description} />
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
<Title title={"Location"} size={"sm"} align={"center"} fontStyle={"sans-serif"}/>
|
<Title title={"Ort"} size={"sm"} align={"center"} fontStyle={"sans-serif"}/>
|
||||||
<TextDiv text={where} />
|
<TextDiv text={where} />
|
||||||
|
|
||||||
{ typeof contact !== "undefined" &&
|
{ typeof contact !== "undefined" &&
|
||||||
|
|
|
||||||
|
|
@ -52,9 +52,12 @@ export const Worship = ({ worship }: WorshipPageProps) => {
|
||||||
</EventExcerptRow>
|
</EventExcerptRow>
|
||||||
<EventExcerptRow label={"Wo:"}>
|
<EventExcerptRow label={"Wo:"}>
|
||||||
{ typeof worship.location == "object" &&
|
{ typeof worship.location == "object" &&
|
||||||
<TextDiv text={`${worship.location.name}\n${worship.location.address}\n${time} Uhr`} />
|
<TextDiv text={`${worship.location.name}\n${worship.location.address}`} />
|
||||||
}
|
}
|
||||||
</EventExcerptRow>
|
</EventExcerptRow>
|
||||||
|
<EventExcerptRow label={"Wann:"}>
|
||||||
|
{time} Uhr
|
||||||
|
</EventExcerptRow>
|
||||||
{ worship.celebrant &&
|
{ worship.celebrant &&
|
||||||
<EventExcerptRow label={"Zelebrant:"}>
|
<EventExcerptRow label={"Zelebrant:"}>
|
||||||
{worship.celebrant}
|
{worship.celebrant}
|
||||||
|
|
|
||||||
|
|
@ -368,9 +368,9 @@ export interface Church {
|
||||||
day: 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday' | 'sunday';
|
day: 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday' | 'sunday';
|
||||||
time: string;
|
time: string;
|
||||||
/**
|
/**
|
||||||
* z. B. „3.“ + „Sonntag“ = jeden 3. Sonntag im Monat
|
* z. B. „3.“ + „Sonntag“ = jeden 3. Sonntag im Monat.
|
||||||
*/
|
*/
|
||||||
weekOfMonth?: ('first' | 'second' | 'third' | 'fourth' | 'last') | null;
|
weekOfMonth?: ('first' | 'second' | 'third' | 'fourth' | 'fifth' | 'last') | null;
|
||||||
/**
|
/**
|
||||||
* Ein Datum, an dem dieser Termin stattfindet. Davon ausgehend wird im 2-Wochen-Rhythmus weitergerechnet.
|
* Ein Datum, an dem dieser Termin stattfindet. Davon ausgehend wird im 2-Wochen-Rhythmus weitergerechnet.
|
||||||
*/
|
*/
|
||||||
|
|
@ -1231,7 +1231,7 @@ export interface Event {
|
||||||
| {
|
| {
|
||||||
frequency: 'daily' | 'weekly' | 'monthlyByDate' | 'monthlyByWeekday';
|
frequency: 'daily' | 'weekly' | 'monthlyByDate' | 'monthlyByWeekday';
|
||||||
weekday?: ('monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday' | 'sunday') | null;
|
weekday?: ('monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday' | 'sunday') | null;
|
||||||
weekOfMonth?: ('first' | 'second' | 'third' | 'fourth' | 'last') | null;
|
weekOfMonth?: ('first' | 'second' | 'third' | 'fourth' | 'fifth' | 'last') | null;
|
||||||
dayOfMonth?: number | null;
|
dayOfMonth?: number | null;
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
}[]
|
}[]
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { EventRowProps } from '@/components/EventRow/EventRow'
|
||||||
export const transformCategory = (category: "MASS" | "FAMILY" | "WORD" | "LANGUAGE" | "OTHER"): string => {
|
export const transformCategory = (category: "MASS" | "FAMILY" | "WORD" | "LANGUAGE" | "OTHER"): string => {
|
||||||
const type = {
|
const type = {
|
||||||
'MASS': 'Eucharistiefeier',
|
'MASS': 'Eucharistiefeier',
|
||||||
'FAMILY': 'Familienmesse',
|
'FAMILY': 'Familiengottesdienst',
|
||||||
'WORD': 'Wort-Gottes-Feier',
|
'WORD': 'Wort-Gottes-Feier',
|
||||||
'LANGUAGE': 'Eucharistiefeier in Fremdsprache',
|
'LANGUAGE': 'Eucharistiefeier in Fremdsprache',
|
||||||
'OTHER': 'Gottesdienst',
|
'OTHER': 'Gottesdienst',
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ const WEEK_OF_MONTH_LABEL: Record<string, string> = {
|
||||||
second: '2.',
|
second: '2.',
|
||||||
third: '3.',
|
third: '3.',
|
||||||
fourth: '4.',
|
fourth: '4.',
|
||||||
|
fifth: '5.',
|
||||||
last: 'letzten',
|
last: 'letzten',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue