This commit is contained in:
Benno Tielen 2024-08-21 17:08:42 +02:00
commit 6036af1099
97 changed files with 71187 additions and 0 deletions

2
.env.example Normal file
View file

@ -0,0 +1,2 @@
DATABASE_URI=mongodb://127.0.0.1/payload-template-blank-3-0
PAYLOAD_SECRET=YOUR_SECRET_HERE

13
.eslintrc.cjs Normal file
View file

@ -0,0 +1,13 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: ['next/core-web-vitals', 'plugin:storybook/recommended', 'prettier'],
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: __dirname,
},
plugins: ['eslint-plugin-prettier'],
rules: {
"prefer-const": "error",
"prettier": "error"
}
}

44
.gitignore vendored Normal file
View file

@ -0,0 +1,44 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
/.idea/*
!/.idea/runConfigurations
# testing
/coverage
# next.js
/.next/
/out/
/.yarn
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
.env
/media

6
.prettierrc.json Normal file
View file

@ -0,0 +1,6 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 80,
"semi": false
}

17
.storybook/main.ts Normal file
View file

@ -0,0 +1,17 @@
import type { StorybookConfig } from '@storybook/nextjs'
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@storybook/addon-onboarding',
'@storybook/addon-links',
'@storybook/addon-essentials',
'@chromatic-com/storybook',
'@storybook/addon-interactions',
],
framework: {
name: '@storybook/nextjs',
options: {},
},
}
export default config

24
.storybook/preview.tsx Normal file
View file

@ -0,0 +1,24 @@
import type { Preview } from '@storybook/react'
import { lato } from '../src/app/fonts'
const preview: Preview = {
decorators: [
(Story) => {
return (
<div className={lato.className}>
<Story/>
</div>
)
}
],
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
}
export default preview

925
.yarn/releases/yarn-4.4.0.cjs vendored Normal file

File diff suppressed because one or more lines are too long

6
.yarnrc Normal file
View file

@ -0,0 +1,6 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"--install.ignore-engines" true
yarn-path ".yarn/releases/yarn-1.22.22.cjs"

3
.yarnrc.yml Normal file
View file

@ -0,0 +1,3 @@
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.4.0.cjs

69
Dockerfile Normal file
View file

@ -0,0 +1,69 @@
# From https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile
FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN \
if [ -f yarn.lock ]; then yarn run build; \
elif [ -f package-lock.json ]; then npm run build; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
else echo "Lockfile not found." && exit 1; \
fi
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
CMD HOSTNAME="0.0.0.0" node server.js

42
README.md Normal file
View file

@ -0,0 +1,42 @@
# Payload Blank Template
A blank template for [Payload](https://github.com/payloadcms/payload) to help you get up and running quickly. This repo may have been created by running `npx create-payload-app@latest` and selecting the "blank" template or by cloning this template on [Payload Cloud](https://payloadcms.com/new/clone/blank).
See the official [Examples Directory](https://github.com/payloadcms/payload/tree/main/examples) for details on how to use Payload in a variety of different ways.
## Development
To spin up the project locally, follow these steps:
1. First clone the repo
1. Then `cd YOUR_PROJECT_REPO && cp .env.example .env`
1. Next `yarn && yarn dev` (or `docker-compose up`, see [Docker](#docker))
1. Now `open http://localhost:3000/admin` to access the admin panel
1. Create your first admin user using the form on the page
That's it! Changes made in `./src` will be reflected in your app.
### Docker
Alternatively, you can use [Docker](https://www.docker.com) to spin up this project locally. To do so, follow these steps:
1. Follow [steps 1 and 2 from above](#development), the docker-compose file will automatically use the `.env` file in your project root
1. Next run `docker-compose up`
1. Follow [steps 4 and 5 from above](#development) to login and create your first admin user
That's it! The Docker instance will help you get up and running quickly while also standardizing the development environment across your teams.
## Production
To run Payload in production, you need to build and serve the Admin panel. To do so, follow these steps:
1. First invoke the `payload build` script by running `yarn build` or `npm run build` in your project root. This creates a `./build` directory with a production-ready admin bundle.
1. Then run `yarn serve` or `npm run serve` to run Node in production and serve Payload from the `./build` directory.
### Deployment
The easiest way to deploy your project is to use [Payload Cloud](https://payloadcms.com/new/import), a one-click hosting solution to deploy production-ready instances of your Payload apps directly from your GitHub repo. You can also deploy your app manually, check out the [deployment documentation](https://payloadcms.com/docs/production/deployment) for full details.
## Questions
If you have any issues or questions, reach out to us on [Discord](https://discord.com/invite/payload) or start a [GitHub discussion](https://github.com/payloadcms/payload/discussions).

43
docker-compose.yml Normal file
View file

@ -0,0 +1,43 @@
version: '3'
services:
payload:
image: node:18-alpine
ports:
- '3000:3000'
volumes:
- .:/home/node/app
- node_modules:/home/node/app/node_modules
working_dir: /home/node/app/
command: sh -c "corepack enable && corepack prepare pnpm@latest --activate && pnpm install && pnpm dev"
depends_on:
- mongo
# - postgres
env_file:
- .env
# Ensure your DATABASE_URI uses 'mongo' as the hostname ie. mongodb://mongo/my-db-name
mongo:
image: mongo:latest
ports:
- '27017:27017'
command:
- --storageEngine=wiredTiger
volumes:
- data:/data/db
logging:
driver: none
# Uncomment the following to use postgres
# postgres:
# restart: always
# image: postgres:latest
# volumes:
# - pgdata:/var/lib/postgresql/data
# ports:
# - "5432:5432"
volumes:
data:
# pgdata:
node_modules:

8
next.config.mjs Normal file
View file

@ -0,0 +1,8 @@
import { withPayload } from '@payloadcms/next/withPayload'
/** @type {import('next').NextConfig} */
const nextConfig = {
// Your Next.js config here
}
export default withPayload(nextConfig)

21473
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

72
package.json Normal file
View file

@ -0,0 +1,72 @@
{
"name": "drei-koenige-v3",
"version": "1.0.0",
"description": "A blank template to get started with Payload 3.0",
"license": "MIT",
"type": "module",
"scripts": {
"build": "cross-env NODE_OPTIONS=--no-deprecation next build",
"dev": "cross-env NODE_OPTIONS=--no-deprecation next dev",
"devsafe": "rm -rf .next && cross-env NODE_OPTIONS=--no-deprecation next dev",
"generate:types": "payload generate:types",
"lint": "cross-env NODE_OPTIONS=--no-deprecation next lint",
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
"start": "cross-env NODE_OPTIONS=--no-deprecation next start",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"dependencies": {
"@payloadcms/db-mongodb": "beta",
"@payloadcms/next": "beta",
"@payloadcms/plugin-cloud": "beta",
"@payloadcms/richtext-lexical": "beta",
"@romcal/calendar.france": "^3.0.0-dev.79",
"@romcal/calendar.germany": "file:./../romcal/dist/bundles/germany",
"classnames": "^2.5.1",
"cross-env": "^7.0.3",
"graphql": "^16.8.1",
"mapbox-gl": "^3.5.2",
"next": "15.0.0-canary.123",
"payload": "beta",
"react": "^19.0.0-rc-6230622a1a-20240610",
"react-dom": "^19.0.0-rc-6230622a1a-20240610",
"romcal": "^3.0.0-dev.79",
"sharp": "0.32.6"
},
"devDependencies": {
"@chromatic-com/storybook": "^1.6.1",
"@storybook/addon-essentials": "^8.2.9",
"@storybook/addon-interactions": "^8.2.9",
"@storybook/addon-links": "^8.2.9",
"@storybook/addon-onboarding": "^8.2.9",
"@storybook/blocks": "^8.2.9",
"@storybook/nextjs": "^8.2.9",
"@storybook/react": "^8.2.9",
"@storybook/test": "^8.2.9",
"@swc/cli": "^0.4.0",
"@swc/core": "^1.7.10",
"@types/node": "^20.12.12",
"@types/react": "npm:types-react@19.0.0-rc.0",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.0",
"eslint": "^8",
"eslint-config-next": "15.0.0-canary.123",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-storybook": "^0.8.0",
"storybook": "^8.2.9",
"typescript": "5.5.4"
},
"engines": {
"node": "^18.20.2 || >=20.9.0"
},
"pnpm": {
"overrides": {
"@types/react": "npm:types-react@19.0.0-rc.0",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.0"
}
},
"overrides": {
"@types/react": "npm:types-react@19.0.0-rc.0",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.0"
},
"packageManager": "yarn@4.4.0"
}

View file

@ -0,0 +1,22 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
import type { Metadata } from 'next'
import config from '@payload-config'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
type Args = {
params: {
segments: string[]
}
searchParams: {
[key: string]: string | string[]
}
}
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const NotFound = ({ params, searchParams }: Args) => NotFoundPage({ config, params, searchParams })
export default NotFound

View file

@ -0,0 +1,22 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
import type { Metadata } from 'next'
import config from '@payload-config'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
type Args = {
params: {
segments: string[]
}
searchParams: {
[key: string]: string | string[]
}
}
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const Page = ({ params, searchParams }: Args) => RootPage({ config, params, searchParams })
export default Page

View file

@ -0,0 +1,10 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY it because it could be re-written at any time. */
import config from '@payload-config'
import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes'
export const GET = REST_GET(config)
export const POST = REST_POST(config)
export const DELETE = REST_DELETE(config)
export const PATCH = REST_PATCH(config)
export const OPTIONS = REST_OPTIONS(config)

View file

@ -0,0 +1,6 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY it because it could be re-written at any time. */
import config from '@payload-config'
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
export const GET = GRAPHQL_PLAYGROUND_GET(config)

View file

@ -0,0 +1,6 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY it because it could be re-written at any time. */
import config from '@payload-config'
import { GRAPHQL_POST } from '@payloadcms/next/routes'
export const POST = GRAPHQL_POST(config)

View file

View file

@ -0,0 +1,16 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
import configPromise from '@payload-config'
import '@payloadcms/next/css'
import { RootLayout } from '@payloadcms/next/layouts'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import React from 'react'
import './custom.scss'
type Args = {
children: React.ReactNode
}
const Layout = ({ children }: Args) => <RootLayout config={configPromise}>{children}</RootLayout>
export default Layout

View file

@ -0,0 +1,29 @@
"use client"
import {Menu} from "@/components/Menu/Menu";
import {BannerText} from "@/components/BannerText/BannerText";
import {HomeBanner, HomeBannerHandle} from "@/components/HomeBanner/HomeBanner";
import {useRef} from "react";
import { Worship } from '@/payload-types'
type BannerWithMenuProps = {
nextMass?: Worship
}
export const BannerWithMenu = ({nextMass}: BannerWithMenuProps) => {
const bannerRef = useRef<HomeBannerHandle>(null);
function addThreeNewStars() {
bannerRef.current?.newStar();
bannerRef.current?.newStar();
bannerRef.current?.newStar();
}
return (
<HomeBanner stars={40} ref={bannerRef}>
<Menu starClick={addThreeNewStars} nextMass={nextMass} />
<BannerText/>
</HomeBanner>
)
}

12
src/app/fonts.ts Normal file
View file

@ -0,0 +1,12 @@
import { Faustina, Lato } from 'next/font/google';
export const faustina = Faustina({
subsets: ['latin'],
display: "swap"
})
export const lato = Lato({
subsets: ['latin'],
weight: ["400"],
display: 'swap'
})

13
src/app/home.module.css Normal file
View file

@ -0,0 +1,13 @@
.mass {
padding-top: 30px;
padding-bottom: 300px;
}
.mass h2 {
text-align: center;
}
.table {
display: flex;
justify-content: center;
}

20
src/app/layout.tsx Normal file
View file

@ -0,0 +1,20 @@
import type { Metadata } from "next";
import {lato} from "./fonts";
export const metadata: Metadata = {
title: "Katholische Pfarrei Heilige drei Könige Berlin",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" className={lato.className}>
<body>{children}</body>
</html>
);
}

14
src/app/my-route/route.ts Normal file
View file

@ -0,0 +1,14 @@
import configPromise from '@payload-config'
import { getPayload } from 'payload'
export const GET = async () => {
const payload = await getPayload({
config: configPromise,
})
const data = await payload.find({
collection: 'users',
})
return Response.json(data)
}

11
src/app/not-found.tsx Normal file
View file

@ -0,0 +1,11 @@
export default function Custom404() {
return (
<>
<h1>404 - Seite nicht gefunden</h1>
<p>
Glorreicher heiliger Antonius, du hast die göttliche Macht ausgeübt, verlorene Dinge wiederzufinden. Hilf uns, die Gnade Gottes wiederzuerlangen und mach mich stark im Dienst an Gott und an den Tugenden. Lass&apos; mich das Verlorene wiederfinden und zeige mir so deine Güte.
</p>
</>
)
}

85
src/app/page.tsx Normal file
View file

@ -0,0 +1,85 @@
import configPromise from "@payload-config";
import { BannerWithMenu } from '@/app/BannerWithMenu'
import { Worship } from '@/payload-types'
import styles from "./home.module.css"
import { MassTable } from '@/components/MassTable/MassTable'
import { getPayloadHMR } from '@payloadcms/next/utilities'
const extractWorshipHours = (worships: Worship[]) => {
let worshipByDate = new Map<string, Worship[]>()
for (let worship of worships) {
const date = worship.date.substring(0, 10);
if (worshipByDate.has(date)) {
worshipByDate.get(date)?.push(worship);
} else {
worshipByDate.set(date, [worship]);
}
}
return worshipByDate
}
export default async function Home() {
const today = new Date();
const nextWeek = new Date(today.getTime() + 7 * 24 * 60 * 60 * 1000)
const payload = await getPayloadHMR({ config: configPromise });
const worship = await payload.find({
collection: 'worship',
where: {
and: [
{
date: {
greater_than_equal: today.toISOString().substring(0, 10)
}
},
{
date: {
less_than: nextWeek.toISOString().substring(0, 10)
}
},
]
},
limit: 30,
sort: 'date'
});
const nextMass = await payload.find({
collection: 'worship',
where: {
and: [
{
date: {
greater_than_equal: today.toISOString().substring(0, 10)
}
},
{
cancelled: {
equals: false
}
}
]
},
limit: 1,
sort: "date"
})
const worshipByDate = [...extractWorshipHours(worship.docs).entries()];
return (
<>
<BannerWithMenu nextMass={nextMass.docs[0]} />
<div className={styles.mass}>
<h2>Kommen Sie vorbei, in unsere Heilige Messe!</h2>
<div className={styles.table}>
{worshipByDate.map(([date, worships]) => <MassTable key={date} date={date} masses={worships} />)}
</div>
</div>
</>
)
}

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.00195 17H5.60636C4.34793 17 3.71872 17 3.58633 16.9023C3.4376 16.7925 3.40126 16.7277 3.38515 16.5436C3.37082 16.3797 3.75646 15.7486 4.52776 14.4866C5.32411 13.1835 6.00031 11.2862 6.00031 8.6C6.00031 7.11479 6.63245 5.69041 7.75766 4.6402C8.88288 3.59 10.409 3 12.0003 3C13.5916 3 15.1177 3.59 16.2429 4.6402C17.3682 5.69041 18.0003 7.11479 18.0003 8.6C18.0003 11.2862 18.6765 13.1835 19.4729 14.4866C20.2441 15.7486 20.6298 16.3797 20.6155 16.5436C20.5994 16.7277 20.563 16.7925 20.4143 16.9023C20.2819 17 19.6527 17 18.3943 17H15.0003M9.00195 17L9.00031 18C9.00031 19.6569 10.3435 21 12.0003 21C13.6572 21 15.0003 19.6569 15.0003 18V17M9.00195 17H15.0003" stroke="#333333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 982 B

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 21C15.5 17.4 19 14.1764 19 10.2C19 6.22355 15.866 3 12 3C8.13401 3 5 6.22355 5 10.2C5 14.1764 8.5 17.4 12 21Z" stroke="#333333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 13C13.6569 13 15 11.6569 15 10C15 8.34315 13.6569 7 12 7C10.3431 7 9 8.34315 9 10C9 11.6569 10.3431 13 12 13Z" stroke="#333333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 641 B

View file

@ -0,0 +1,86 @@
import { getPayloadHMR } from '@payloadcms/next/utilities'
import configPromise from '@payload-config'
import { Menu } from '@/components/Menu/Menu'
import { useLocation } from '@/hooks/useLocation'
import { useLiturgyCalendarTitle } from '@/hooks/useLiturgyCalendarTitle'
import { Container } from '@/components/Container/Container'
import { Card } from '@/components/Card/Card'
import styles from "./styles.module.css"
import { MassTitle } from '@/components/MassTitle/MassTitle'
import { useCompactDate, useDate } from '@/hooks/useCompactDate'
import { useTime } from '@/hooks/useTime'
import { Pill } from '@/components/Pill/Pill'
import { useMassType } from '@/hooks/useMassType'
import Image from 'next/image'
import bell from "./bell.svg";
import locationIcon from "./location.svg"
import question from "./question.svg"
import { LocationMap } from '@/components/Map/Map'
import { Testimony } from '@/components/Testimony/Testimony'
export default async function Page({params}: {params: {id: string}}) {
const payload = await getPayloadHMR({ config: configPromise });
const worship = await payload.findByID({
id: params.id,
collection: 'worship'
});
const location = useLocation(worship.location)
const title = useLiturgyCalendarTitle(worship.date);
const date = useDate(worship.date);
const time = useTime(worship.date);
const type = useMassType(worship.type)
return (
<>
<Menu />
<Container>
<MassTitle title={title} cancelled={worship.cancelled} />
<div className={styles.info}>
<Card>
<div className={styles.centerIcon}>
<Image src={bell} alt={'Location'} width={60} className={styles.cardIcon} />
</div>
<div className={styles.cardContent}>
<div className={styles.marginBottom}>
{date} <br />
{time} Uhr <br />
</div>
<Pill>{type}</Pill>
</div>
</Card>
<Card>
<div className={styles.centerIcon}>
<Image src={locationIcon} alt={'Location'} width={60} className={styles.cardIcon} />
</div>
<div className={styles.cardContent}>
<div className={styles.address}>
{location.name} <br />
{location.address}
</div>
</div>
</Card>
{worship.description &&
<Card>
<div className={styles.centerIcon}>
<Image src={question} alt={'Location'} width={60} className={styles.cardIcon} />
</div>
<div className={styles.cardText}>
{worship.description}
</div>
</Card>
}
</div>
</Container>
<LocationMap />
<Testimony name={"Johan Shafer"} testimony={"\"Die Eucharistie ist für mich wie ein spiritueller Boost. Wenn ich die Hostie empfange, fühle ich mich krass verbunden mit Jesus. Es ist wie ein Reminder, dass ich nicht allein bin, egal was abgeht. Dieser Moment gibt mir richtig Power und lässt mich mit einem starken Gefühl von Frieden und Hoffnung rausgehen.\""}/>
</>
)
}

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.967 12.75C12.967 11.75 13.967 11.3546 13.967 10.25C13.967 9.14543 13.0716 8.25 11.967 8.25C11.0351 8.25 10.252 8.88739 10.03 9.75M11.967 15.75H11.977M21.0039 12C21.0039 16.9706 16.9745 21 12.0039 21C9.9675 21 3.00463 21 3.00463 21C3.00463 21 4.56382 17.2561 3.93982 16.0008C3.34076 14.7956 3.00391 13.4372 3.00391 12C3.00391 7.02944 7.03334 3 12.0039 3C16.9745 3 21.0039 7.02944 21.0039 12Z" stroke="#333333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 715 B

View file

@ -0,0 +1,40 @@
.info {
display: flex;
gap: 30px;
margin-bottom: 80px;
}
.cardContent {
padding-top: 30px;
padding-left: 50px;
}
.cardText {
padding-top: 30px;
padding-left: 20px;
padding-right: 20px;
}
.marginBottom {
margin-bottom: 20px;
}
.address {
white-space: pre;
}
.cardIcon {
margin-bottom: 10px;
margin-left: auto;
margin-right: auto;
transition: transform 100ms;
}
.cardIcon:hover {
transform: rotateZ(-20deg);
}
.centerIcon {
padding-top: 30px;
text-align: center;
}

View file

@ -0,0 +1,37 @@
import { CollectionConfig } from 'payload'
export const Churches: CollectionConfig = {
slug: 'church',
labels: {
singular: {
de: 'Kirche'
},
plural: {
de: 'Kirchen'
}
},
fields: [
{
name: 'name',
label: {
de: 'Name'
},
type: 'text',
required: true,
},
{
name: 'address',
label: {
de: 'Addresse'
},
type: 'textarea',
required: true,
}
],
admin: {
useAsTitle: 'name'
},
access: {
read: () => true
}
}

16
src/collections/Media.ts Normal file
View file

@ -0,0 +1,16 @@
import type { CollectionConfig } from 'payload'
export const Media: CollectionConfig = {
slug: 'media',
access: {
read: () => true,
},
fields: [
{
name: 'alt',
type: 'text',
required: true,
},
],
upload: true,
}

View file

@ -0,0 +1,53 @@
import { CollectionConfig } from 'payload'
export const Testimony: CollectionConfig = {
slug: 'testimony',
labels: {
singular: {
de: 'Zeugnis'
},
plural: {
de: 'Zeugnisse'
}
},
fields: [
{
name: 'testimony',
label: {
de: 'Zeugnis'
},
type: 'textarea',
required: true,
},
{
name: 'name',
label: {
de: 'Name',
},
type: 'text',
required: true,
},
{
name: 'occupation',
label: {
de: 'Beschäftigung'
},
type: 'text',
required: false,
},
{
name: 'category',
label: {
de: 'Kategorie'
},
type: 'select',
options: [
{
value: 'EUCHARIST',
label: 'Eucharistie'
}
],
required: true,
}
]
}

13
src/collections/Users.ts Normal file
View file

@ -0,0 +1,13 @@
import type { CollectionConfig } from 'payload'
export const Users: CollectionConfig = {
slug: 'users',
admin: {
useAsTitle: 'email',
},
auth: true,
fields: [
// Email added by default
// Add more fields as needed
],
}

View file

@ -0,0 +1,86 @@
import { CollectionConfig } from 'payload'
export const Worship: CollectionConfig = {
slug: 'worship',
labels: {
singular: {
de: 'Gottesdienst',
},
plural: {
de: 'Gottesdienst',
}
},
fields: [
{
name: 'date',
label: {
de: 'Datum'
},
type: 'date',
required: true,
admin: {
date: {
pickerAppearance: 'dayAndTime'
}
}
},
{
name: 'location',
label: {
de: 'Location'
},
type: "relationship",
relationTo: 'church',
required: true,
},
{
name: 'type',
label: {
de: 'Categorie'
},
type: 'radio',
options: [
{
label: 'Heilige Messe',
value: "MASS"
},
{
label: 'Familien Messe',
value: "FAMILY"
},
{
label: "Wort-Gottes-Feier",
value: "WORD"
}
],
required: true,
},
{
name: 'cancelled',
type: 'checkbox',
required: true,
defaultValue: false,
label: {
de: 'Abgesagt'
}
},
{
name: 'title',
type: 'text',
required: false,
label: {
de: 'Liturgischer Tag'
}
},
{
name: 'description',
type: 'textarea',
label: {
de: 'Hinweise'
}
},
],
access: {
read: () => true
}
}

View file

@ -0,0 +1,13 @@
import type {Meta, StoryObj} from "@storybook/react";
import {BannerText} from "./BannerText";
const meta: Meta<typeof BannerText> = {
component: BannerText
}
type Story = StoryObj<typeof BannerText>;
export default meta;
export const Default: Story = {
args: {}
}

View file

@ -0,0 +1,18 @@
import styles from "./bannerText.module.css"
import {faustina, lato} from "@/app/fonts";
export const BannerText = () => {
return (
<div className={faustina.className + " " + styles.container}>
<div className={styles.catholic}>KATHOLISCHE</div>
<div className={styles.name}>
PFARREI <br/>
HEILIGE <br/>
DREI KÖNIGE <br/>
</div>
<div className={styles.berlin + " " + lato.className}>
Berlin Nord-Neukölln
</div>
</div>
)
}

View file

@ -0,0 +1,42 @@
.catholic {
font-weight: 600;
font-size: 35px;
position: relative;
bottom: -15px;
left: 10px;
}
.container {
position: absolute;
bottom: 100px;
}
.name {
font-weight: 600;
font-size: 120px;
line-height: 105px;
padding: 20px 0;
}
.berlin {
position: relative;
left: 10px;
}
@media screen and (max-width: 420px) {
.name {
font-size: 42px;
line-height: 42px;
padding: 5px 0;
}
.catholic {
font-size: 18px;
bottom: 0;
left: 0;
}
.berlin {
left: 0;
}
}

View file

@ -0,0 +1,15 @@
import { Meta, StoryObj } from '@storybook/react'
import { Card } from './Card'
const meta: Meta<typeof Card> = {
component: Card,
}
type Story = StoryObj<typeof Card>;
export default meta
export const Default: Story = {
args: {
children: <>Some content</>
},
}

View file

@ -0,0 +1,13 @@
import styles from "./styles.module.css";
type CardProps = {
children?: JSX.Element | JSX.Element[];
}
export const Card = ({children}: CardProps) => {
return (
<div className={styles.card}>
{children}
</div>
)
}

View file

@ -0,0 +1,6 @@
.card {
height: 260px;
width: 235px;
box-shadow: 0 0 11px 0 rgba(79,66,79,0.26);
background-color: #ffffff;
}

View file

@ -0,0 +1,22 @@
import {Meta, StoryObj} from "@storybook/react";
import { Container } from "./Container";
const meta: Meta<typeof Container> = {
component: Container
}
type Story = StoryObj<typeof Container>;
export default meta;
export const Default: Story = {
args: {
children: <>Some content</>
}
}
export const Yellow: Story = {
args: {
background: "yellow",
children: <>Some content</>
}
}

View file

@ -0,0 +1,17 @@
import styles from "./styles.module.css"
import classNames from 'classnames'
type ContainerProps = {
background?: "yellow"
children: JSX.Element | JSX.Element[]
}
export const Container = ({children, background}: ContainerProps) => {
return (
<div className={classNames({[styles.yellow]: background === "yellow"})}>
<div className={styles.container}>
{children}
</div>
</div>
)
}

View file

@ -0,0 +1,11 @@
.container {
width: 800px;
margin: 0 auto;
}
.yellow {
background: rgb(255,255,97);
background: linear-gradient(180deg, rgba(255,255,97,0.0) 0%, rgb(255 250 163) 20%);
position: relative;
top: -70px;
}

View file

@ -0,0 +1,19 @@
.splash-bg {
background-color: #8d5fd3;
}
.splash {
height: 80vh;
background: url("bg.svg") center center;
background-size: cover;
color: #FFFFFF;
padding: 20px;
position: relative;
}
.stars {
position: absolute;
top: 0;
left: 0;
z-index: 0;
}

View file

@ -0,0 +1,15 @@
import type { Meta, StoryObj} from '@storybook/react';
import {HomeBanner} from "@/components/HomeBanner/HomeBanner";
const meta: Meta<typeof HomeBanner> = {
component: HomeBanner,
}
type Story = StoryObj<typeof HomeBanner>;
export default meta;
export const Default: Story = {
args: {
stars: 50
}
}

View file

@ -0,0 +1,72 @@
import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef} from "react";
import "./HomeBanner.css"
type HomeBannerProps = {
children?: React.ReactNode,
stars: number
}
export type HomeBannerHandle = {
newStar: () => void;
}
export const HomeBanner = forwardRef<HomeBannerHandle, HomeBannerProps>(function HomeBanner({children, stars}: HomeBannerProps, ref) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const drawStarAtPosition = useCallback((ctx: CanvasRenderingContext2D, x: number, y: number) => {
let r = 2.5 * Math.random();
//Draw the stars;
ctx.beginPath();
ctx.fillStyle = "white";
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fill();
}, []);
const drawStar = useCallback((ctx: CanvasRenderingContext2D) => {
//Random position and size of stars;
let x = ctx.canvas.width * Math.random();
let y = ctx.canvas.height * Math.random();
drawStarAtPosition(ctx, x, y);
}, [drawStarAtPosition]);
useImperativeHandle(ref, () => {
return {
newStar() {
const context = canvasRef.current?.getContext("2d")
if (context) {
drawStar(context)
}
}
}
}, [drawStar]);
useEffect(() => {
if (canvasRef.current) {
canvasRef.current.width = window.innerWidth;
canvasRef.current.height = 0.8 * window.innerHeight;
}
const context = canvasRef.current?.getContext("2d");
if(context) {
for (let i = 0; i < stars; i++) {
//Glow effect;
context.shadowBlur = 10;
context.shadowColor = "white";
drawStar(context)
}
}
}, [drawStar, stars]);
return (
<div className="splash-bg">
<canvas ref={canvasRef} className="stars"></canvas>
<div className="splash">
{children}
</div>
</div>
)
});

View file

@ -0,0 +1,150 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="1280"
height="720"
viewBox="0 0 338.66667 190.5"
version="1.1"
id="svg1"
inkscape:version="1.3.1 (9b9bdc1480, 2023-11-25, custom)"
sodipodi:docname="bg2.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:lockguides="false"
inkscape:zoom="1.0476431"
inkscape:cx="869.09368"
inkscape:cy="315.94729"
inkscape:window-width="3368"
inkscape:window-height="1376"
inkscape:window-x="72"
inkscape:window-y="1107"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
showgrid="true">
<inkscape:grid
id="grid1"
units="mm"
originx="0"
originy="0"
spacingx="1"
spacingy="1"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="5"
dotted="false"
gridanglex="30"
gridanglez="30"
visible="true" />
</sodipodi:namedview>
<defs
id="defs1">
<linearGradient
id="linearGradient8"
inkscape:collect="always">
<stop
style="stop-color:#b380ff;stop-opacity:1"
offset="0"
id="stop8" />
<stop
style="stop-color:#5fd3bc;stop-opacity:0;"
offset="1"
id="stop9" />
</linearGradient>
<linearGradient
id="linearGradient6"
inkscape:collect="always">
<stop
style="stop-color:#5fd3bc;stop-opacity:1;"
offset="0"
id="stop6" />
<stop
style="stop-color:#5fd3bc;stop-opacity:0;"
offset="1"
id="stop7" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient6"
id="linearGradient7"
x1="11.633274"
y1="180.37541"
x2="185.67206"
y2="-7.3487453"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.99320034,0,0,1.0022678,1.7447527,6.0845905e-4)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient8"
id="linearGradient9"
x1="329.23218"
y1="182.37346"
x2="226.32838"
y2="17.87619"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0022746,0,0,1,7.7762959e-4,0)" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="display:none;fill:#8d5fd3;fill-opacity:1;stroke-width:0.264832"
id="rect3"
width="338.71875"
height="190.45135"
x="0.089720808"
y="-0.030377436" />
<path
style="fill:#5fd3bc;fill-opacity:1;stroke-width:0.264583"
d="m 123.81562,155.12003 34.30109,-17.03714 40.81875,19.87171 -18.75878,28.51298 -51.8867,-3.49732 z"
id="path6" />
<path
style="fill:#8787de;fill-opacity:1;stroke-width:0.264269"
d="m -0.102359,160.41197 72.554869,-32.2325 126.91331,60.8393 -199.03795731,1.50639 z"
id="path4"
sodipodi:nodetypes="ccccc" />
<path
style="fill:#afafe9;fill-opacity:1;stroke-width:0.264583"
d="m 126.81453,190.57041 c 6.23387,-2.16904 151.87751,-93.709178 151.87751,-93.709178 l 60.12563,39.370248 -0.78639,53.91102 z"
id="path5"
sodipodi:nodetypes="ccccc" />
</g>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline"
sodipodi:insensitive="true">
<rect
style="fill:url(#linearGradient9);fill-opacity:1;stroke-width:0.264884"
id="rect7"
width="339.0639"
height="190.00063"
x="-0.34187973"
y="0.55117559" />
<rect
style="display:inline;fill:url(#linearGradient7);fill-opacity:1;stroke-width:0.26398"
id="rect6"
width="338.85544"
height="190.7338"
x="0.019881012"
y="-0.26830679"
inkscape:label="rect6" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View file

@ -0,0 +1,38 @@
"use client"
import { useEffect, useRef, useState } from 'react'
import mapboxgl from 'mapbox-gl'
import styles from "./styles.module.css"
import 'mapbox-gl/dist/mapbox-gl.css'
// todo: as env variable
mapboxgl.accessToken ='pk.eyJ1IjoiYnRpZWxlbiIsImEiOiJjbHpzNmNoNjAxdmxqMmpzaWtxOGsxNnY2In0.4XrA_ZlvlmKZ7MG_tLo-mQ'
export const LocationMap = () => {
const mapContainer = useRef<HTMLDivElement>(null);
const map = useRef<mapboxgl.Map>(null);
const [lng, setLng] = useState(13.436093);
const [lat, setLat] = useState(52.477608);
const [zoom, setZoom] = useState(15.42);
useEffect(() => {
if (map.current) return; // initialize map only once
if(mapContainer.current) {
map.current = new mapboxgl.Map({
container: mapContainer.current,
logoPosition: "top-left",
attributionControl: false,
style: 'mapbox://styles/btielen/clzs6etam008801qu6hpn9qbo',
center: [lng, lat],
zoom: zoom,
});
}
});
return (
<div ref={mapContainer} className={styles.map}></div>
)
}

View file

@ -0,0 +1,3 @@
.map {
height: 300px;
}

View file

@ -0,0 +1,41 @@
import { Meta, StoryObj } from '@storybook/react';
import { MassTable } from './MassTable';
const meta: Meta<typeof MassTable> = {
component: MassTable,
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
date: "2024-08-04",
masses: [
{
id: "1",
date: "10:00",
locationName: "St. Christopherus",
type: null
},
{
id: "1",
date: "11:00",
locationName: "St. Richard",
type: "FAMILY"
},
{
id: "1",
date: "11:00",
locationName: "St. Clara",
type: "WORD"
},
{
id: "1",
date: "19:00",
locationName: "St. Clara",
type: null
}
]
}
}

View file

@ -0,0 +1,33 @@
import {MassTableRow} from "@/components/MassTable/MassTableRow";
import {useMemo} from "react";
import styles from "./styles.module.css"
import {faustina} from "@/app/fonts";
import { Worship } from '@/payload-types'
import { useCompactDate } from '@/hooks/useCompactDate'
type MassTableProps = {
date: string,
masses: Worship[]
}
export const MassTable = ({date, masses}: MassTableProps) => {
let dateObj = useMemo(() => new Date(date), [date]);
let compactDate = useCompactDate(date);
return (
<div className={styles.table}>
<h3 className={faustina.className}>{dateObj.toLocaleDateString("de-DE", {weekday: 'long'})} <small>{compactDate}</small></h3>
{ masses.map(mass =>
<MassTableRow
key={mass.id}
id={mass.id}
locationName={typeof mass.location == "string" ? mass.location : mass.location.name}
date={mass.date}
type={mass.type}
cancelled={mass.cancelled}
/>
)}
</div>
)
}

View file

@ -0,0 +1,49 @@
import { Meta, StoryObj } from '@storybook/react';
import { MassTableRow } from './MassTableRow';
const meta: Meta<typeof MassTableRow> = {
component: MassTableRow,
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
id: "1",
locationName: "St. Clara",
date: "2024-08-23T15:00:00.000Z",
type: 'MASS',
cancelled: false,
}
}
export const FamilyMass: Story = {
args: {
id: "1",
locationName: "St. Christopherus",
date: "2024-08-23T15:00:00.000Z",
type: "FAMILY",
cancelled: false,
}
}
export const LiturgyOfTheWord: Story = {
args: {
id: "1",
locationName: "St. Richard",
date: "2024-08-23T15:00:00.000Z",
type: "WORD",
cancelled: false,
}
}
export const Cancelled: Story = {
args: {
id: "1",
locationName: "St. Richard",
date: "2024-08-23T15:00:00.000Z",
type: "WORD",
cancelled: true,
}
}

View file

@ -0,0 +1,49 @@
"use client"
import styles from "./styles.module.css"
import Image from "next/image";
import family from "./family.svg"
import bible from "./bible.svg"
import {useState} from "react";
import Link from 'next/link'
import classNames from 'classnames'
import { useTime } from '@/hooks/useTime'
export type MassTableRowProps = {
id: string,
locationName: string,
date: string,
type: 'MASS' | 'FAMILY' | 'WORD',
cancelled: boolean
}
export const MassTableRow = ({id, locationName, date, type, cancelled}: MassTableRowProps) => {
const [symbol, setSymbol] = useState("-");
const time = useTime(date);
return (
<Link href={`/worship/${id}`} className={classNames({ [styles.cancelled]: cancelled }, styles.link)}>
<div
className={styles.row}
onMouseEnter={() => setSymbol("†")}
onMouseLeave={() => setSymbol("-")}
>
<div className={styles.time}>{time}</div>
<div className={styles.symbol}>
{symbol}
</div>
<div>
{locationName}
</div>
<div>
{ type === "FAMILY" &&
<Image src={family} width={18} height={18} alt={"Familien Messe"} />
}
{ type === "WORD" &&
<Image src={bible} width={18} height={18} alt={"Wortgottesfeier"} />
}
</div>
</div>
</Link>
)
}

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" width="800px" height="800px" viewBox="-32 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M448 358.4V25.6c0-16-9.6-25.6-25.6-25.6H96C41.6 0 0 41.6 0 96v320c0 54.4 41.6 96 96 96h326.4c12.8 0 25.6-9.6 25.6-25.6v-16c0-6.4-3.2-12.8-9.6-19.2-3.2-16-3.2-60.8 0-73.6 6.4-3.2 9.6-9.6 9.6-19.2zM144 144c0-8.84 7.16-16 16-16h48V80c0-8.84 7.16-16 16-16h32c8.84 0 16 7.16 16 16v48h48c8.84 0 16 7.16 16 16v32c0 8.84-7.16 16-16 16h-48v112c0 8.84-7.16 16-16 16h-32c-8.84 0-16-7.16-16-16V192h-48c-8.84 0-16-7.16-16-16v-32zm236.8 304H96c-19.2 0-32-12.8-32-32s16-32 32-32h284.8v64z"/></svg>

After

Width:  |  Height:  |  Size: 718 B

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
fill="#000000"
width="800px"
height="800px"
viewBox="-1 0 19 19"
class="cf-icon-svg"
version="1.1"
id="svg1"
sodipodi:docname="family.svg"
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.039447"
inkscape:cx="210.20793"
inkscape:cy="424.26407"
inkscape:window-width="3368"
inkscape:window-height="1376"
inkscape:window-x="72"
inkscape:window-y="1107"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M 18,9.5 A 9.5,9.5 0 1 1 8.5,0 9.5,9.5 0 0 1 18,9.5 Z M 9.4179613,9.9607806 A 2.7886826,2.7886826 0 0 1 9.1347733,9.4472022 2.7358848,2.7358848 0 0 1 9.021978,9.1268157 H 4.4837691 A 2.6962865,2.6962865 0 0 0 1.7958823,11.814703 v 1.613932 A 1.0787546,1.0787546 0 0 0 2.871037,14.503789 H 7.4632437 A 1.6439308,1.6439308 0 0 1 7.288051,13.76342 V 12.652267 A 2.7766831,2.7766831 0 0 1 8.9775799,10.104775 2.6962865,2.6962865 0 0 1 9.4179613,9.9607806 Z M 6.9040672,8.3204497 A 2.7478843,2.7478843 0 1 0 4.1561829,5.5725654 2.7478843,2.7478843 0 0 0 6.9040672,8.3204497 Z m 6.4821268,2.4814953 h -3.331059 a 1.8563218,1.8563218 0 0 0 -1.8515225,1.850322 v 1.111153 a 0.74276873,0.74276873 0 0 0 0.7403688,0.740369 h 5.5533657 a 0.74276873,0.74276873 0 0 0 0.740369,-0.740369 v -1.111153 a 1.8563218,1.8563218 0 0 0 -1.851522,-1.850322 z m -1.66553,-0.555576 a 1.8911204,1.8911204 0 1 0 -1.89112,-1.8923207 1.8911204,1.8911204 0 0 0 1.89112,1.8923207 z"
id="path1"
style="stroke-width:1.19995" />
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,28 @@
.row {
display: flex;
gap: 10px;
height: 24px;
cursor: pointer;
}
.link {
text-decoration: none;
color: inherit;
}
.cancelled {
text-decoration: line-through;
}
.symbol {
width: 14px;
text-align: center;
}
.time {
width: 40px;
}
.table {
width: 300px;
}

View file

@ -0,0 +1,25 @@
import type { Meta, StoryObj} from '@storybook/react';
import { MassTimer } from './MassTimer';
import {fn} from "@storybook/test";
const meta: Meta<typeof MassTimer> = {
component: MassTimer
};
type Story = StoryObj<typeof MassTimer>;
export default meta
export const OneDay: Story = {
args: {
timeout: new Date().getTime() + 1000 * 60 * 60 * 24,
onStarClick: fn()
}
}
export const TimeOut: Story = {
args: {
timeout: new Date().getTime(),
onStarClick: fn()
}
}

View file

@ -0,0 +1,50 @@
"use client"
import {useCountdown} from "@/components/MassTimer/useCountdown";
import styles from "./masstimer.module.css"
import {useState} from "react";
import {MassTimerTooltip} from "@/components/MassTimerTooltip/MassTimerTooltip";
import { Worship } from '@/payload-types'
type MassTimerProps = {
nextMass: Worship
/**
* Optional click handler
*/
onStarClick?: () => void
}
export const MassTimer = ({nextMass, onStarClick}: MassTimerProps) => {
const [displayTooltip, setDisplayTooltip] = useState<boolean>(false);
const [days, hours, minutes, seconds] = useCountdown(new Date(nextMass.date).getTime());
return (
<div className={styles.container}>
<div>
<button
className={styles.starButton}
type={"button"}
onClick={onStarClick}
>🌟
</button>
<span onMouseEnter={() => setDisplayTooltip(true)}>
{days}T {hours}S {minutes}M {seconds}S
</span>
</div>
<div
className={styles.tooltip}
style={{
visibility: displayTooltip ? "visible" : "hidden",
opacity: displayTooltip ? 1 : 0,
transition: "ease-out 0.2s",
}} onMouseLeave={() => setDisplayTooltip(false)}>
<MassTimerTooltip nextMass={nextMass}/>
</div>
</div>
)
}

View file

@ -0,0 +1,19 @@
.container {
position: relative;
}
.starButton {
background: none;
border: none;
margin-right: 5px;
}
.starButton:hover {
cursor: pointer;
}
.tooltip {
position: absolute;
right: 0;
top: 35px;
}

View file

@ -0,0 +1,42 @@
import { useEffect, useState } from 'react';
/**
* Countdown until targetDate,
* return an array of four numbers representing the
* number of days, hours, minutes and seconds until targetDate
*
*/
const useCountdown = (targetDate: number) => {
const [countDown, setCountDown] = useState(
targetDate - new Date().getTime()
);
useEffect(() => {
if (countDown > 0) {
setTimeout(() => setCountDown(countDown - 1000), 1000);
} else {
setCountDown(0)
}
}, [countDown]);
return getDaysHoursMinutesAndSeconds(countDown);
};
/**
* Return an array of four numbers, representing the
* numbers days, hours, minutes and seconds
*/
const getDaysHoursMinutesAndSeconds = (countDown: number) => {
// calculate time left
const days = Math.floor(countDown / (1000 * 60 * 60 * 24));
const hours = Math.floor(
(countDown % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)
);
const minutes = Math.floor((countDown % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((countDown % (1000 * 60)) / 1000);
return [days, hours, minutes, seconds];
};
export { useCountdown };

View file

@ -0,0 +1,15 @@
import type { Meta, StoryObj} from "@storybook/react";
import {MassTimerTooltip} from "./MassTimerTooltip";
const meta: Meta<typeof MassTimerTooltip> = {
component: MassTimerTooltip
}
type Story = StoryObj<typeof MassTimerTooltip>;
export default meta;
export const Default: Story = {
args: {
}
}

View file

@ -0,0 +1,31 @@
import styles from "./massTimerTooltip.module.css";
import clara from "./clara.svg"
import Image from "next/image";
import { Worship } from '@/payload-types'
import { useTime } from '@/hooks/useTime'
import { useLocationName } from '@/hooks/useLocationName'
import { useCompactDate } from '@/hooks/useCompactDate'
type MassTimerTooltipProps = {
nextMass: Worship
}
export const MassTimerTooltip = ({nextMass}: MassTimerTooltipProps) => {
const time = useTime(nextMass.date);
const location = useLocationName(nextMass.location);
const date = useCompactDate(nextMass.date);
return (
<div
className={styles.tooltip}>
<div className={styles.church}>
<Image src={clara} width={75} height={75} alt={""}/>
</div>
<div>
Die nächste Messe is am {date} um {time} Uhr in {location}.
</div>
</div>
)
}

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="120"
height="120"
viewBox="0 0 120 120"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="layer2">
<path
id="path1"
style="fill:#555555;stroke-width:1.60965"
d="m 42.761659,6.1722208 v 3.627996 h -4.879246 v 1.5279092 h 4.866672 V 26.846143 L 21.006255,64.119566 V 186.45283 H 181.97111 V 99.531834 L 112.21714,89.552843 86.964115,74.406226 V 62.509917 h 4.637171 l 0.0032,-1.600218 h -4.640347 v -3.555687 h -1.559346 v 3.624852 h -4.879248 v 1.531053 h 4.866673 V 73.777455 L 66.076412,89.873941 V 64.119566 L 44.321008,26.824136 v -15.49601 h 4.637169 l 0.0032,-1.5970722 h -4.640388 v -3.558833 z"
clip-path="none" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 905 B

View file

@ -0,0 +1,28 @@
.tooltip {
width: 300px;
padding: 20px;
color: #4d4d4d;
background: rgb(244,244,244);
background: linear-gradient(40deg, rgba(244,244,244,1) 0%, rgba(228,228,228,1) 100%);
border: solid 1px #dddddd;
border-radius: 6px;
display: flex;
gap: 20px;
align-items: center;
}
.church {
width: 75px;
height: 75px;
border-radius: 50%;
flex-shrink: 0;
background-color: #f6f6f6;
border: solid 2px #c2c2c2;
transition: background-color 200ms ease-in;
overflow: clip;
}
.church:hover {
cursor: pointer;
background-color: #fff318;
}

View file

@ -0,0 +1,23 @@
import { Meta, StoryObj } from '@storybook/react'
import { MassTitle } from './MassTitle'
const meta: Meta<typeof MassTitle> = {
component: MassTitle,
}
type Story = StoryObj<typeof MassTitle>;
export default meta
export const Default: Story = {
args: {
title: 'Mittwoch der 19. Woche im Jahreskreis',
cancelled: false
},
}
export const Cancelled: Story = {
args: {
title: 'Mittwoch der 19. Woche im Jahreskreis',
cancelled: true
},
}

View file

@ -0,0 +1,18 @@
import classNames from 'classnames'
import { faustina } from '@/app/fonts'
import styles from "./styles.module.css"
import { Pill } from '@/components/Pill/Pill'
type MassTitleProps = {
title: string,
cancelled: boolean
}
export const MassTitle = ({title, cancelled}: MassTitleProps) => {
return (
<div>
<div className={styles.mass}>Gottesdienst { cancelled && <Pill>ABGESAGT</Pill> }</div>
<h1 className={classNames(faustina.className, styles.title, { [styles.cancelled]: cancelled})}>{title}</h1>
</div>
)
}

View file

@ -0,0 +1,14 @@
.title {
font-size: 48px;
font-weight: 700;
margin-block-start: 0;
}
.mass {
font-size: 16px;
font-weight: 700;
}
.cancelled {
text-decoration: line-through;
}

View file

@ -0,0 +1,15 @@
import {Meta, StoryObj} from "@storybook/react";
import { Menu } from "./Menu";
const meta: Meta<typeof Menu> = {
component: Menu
}
type Story = StoryObj<typeof Menu>;
export default meta;
export const Default: Story = {
args: {
}
}

View file

@ -0,0 +1,41 @@
import {MassTimer} from "@/components/MassTimer/MassTimer";
import styles from "./styles.module.css"
import MenuIcon from "./menu.svg"
import Image from "next/image";
import { Worship } from '@/payload-types'
import { MenuBaseLayer } from '@/components/MenuBaseLayer/MenuBaseLayer'
type MenuProps = {
starClick?: () => void
nextMass?: Worship,
}
export const Menu = (props: MenuProps) => {
return (
<nav className={styles.nav}>
<MenuBaseLayer />
<div className={styles.navMobile}>
<Image src={MenuIcon} width={25} height={25} alt={"Menu"} />
</div>
<div className={styles.itemsLeft}>
<a className={styles.menuLink} href={""}>Home</a>
<a className={styles.menuLink} href={""}>Gemeinschaft</a>
<a className={styles.menuLink} href={""}>Sakramenten</a>
<a className={styles.menuLink} href={""}>Kontakt</a>
</div>
<div className={styles.itemsRight}>
<div>
<a className={styles.menuLink} href={""}>Spenden</a>
</div>
<div>
<button className={styles.button}>Neu hier?</button>
</div>
{ props.nextMass &&
<MassTimer nextMass={props.nextMass} onStarClick={props.starClick}/>
}
</div>
</nav>
)
}

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 6H20M4 12H20M4 18H20" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 338 B

View file

@ -0,0 +1,63 @@
.nav {
display: flex;
align-items: baseline;
gap: 20px;
color: #3d3d3d;
padding-top: 15px;
height: 50px;
border-bottom: 1px solid rgba(217, 217, 217, 0.19);
}
.navMobile {
display: none;
}
.itemsLeft {
display: flex;
gap: 20px;
}
.menuLink {
color: inherit;
text-decoration: none;
font-weight: 600;
transition: opacity 100ms ease-in;
}
.menuLink:hover {
opacity: 0.7;
}
.itemsRight {
margin-left: auto;
display: flex;
gap: 20px;
justify-content: flex-end;
align-items: baseline;
}
.button {
padding: 10px;
border-radius: 10px;
border: none;
background-color: #eeeeee;
transition: background-color 0.1s ease-in-out;
font-family: inherit;
font-weight: 600;
}
.button:hover {
background-color: #fff318;
cursor: pointer;
}
@media screen and (max-width: 800px) {
.navMobile {
display: block;
}
.itemsLeft {
display: none;
}
}

View file

@ -0,0 +1,15 @@
import {Meta, StoryObj} from "@storybook/react";
import { MenuBaseLayer } from '@/components/MenuBaseLayer/MenuBaseLayer'
const meta: Meta<typeof MenuBaseLayer> = {
component: MenuBaseLayer
}
type Story = StoryObj<typeof MenuBaseLayer>;
export default meta;
export const Default: Story = {
args: {
}
}

View file

@ -0,0 +1,5 @@
import styles from "./style.module.css"
export const MenuBaseLayer = () => {
return <div className={styles.background}></div>
}

View file

@ -0,0 +1,9 @@
<svg width="952" height="348" viewBox="0 0 952 348" fill="none" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 348V0H952V153.638L0 348Z" fill="url(#paint0_linear_13_7)" fill-opacity="0.5"/>
<defs>
<linearGradient id="paint0_linear_13_7" x1="907.131" y1="1.77536e-05" x2="16.2387" y2="104.935" gradientUnits="userSpaceOnUse">
<stop stop-color="#A790EA"/>
<stop offset="1" stop-color="#3BEBB9"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 455 B

View file

@ -0,0 +1,10 @@
.background {
width: 100%;
height: 340px;
background-image: url("./bg.svg");
background-size: 100% 100%;
position: absolute;
top: 0;
left: 0;
z-index: -1;
}

View file

@ -0,0 +1,15 @@
import { Meta, StoryObj } from '@storybook/react'
import { Pill } from './Pill'
const meta: Meta<typeof Pill> = {
component: Pill,
}
type Story = StoryObj<typeof Pill>;
export default meta
export const Default: Story = {
args: {
children: "Default"
},
}

View file

@ -0,0 +1,13 @@
import styles from "./styles.module.css"
type PillProps = {
children: JSX.Element | string | JSX.Element[]
}
export const Pill = ({children}: PillProps) => {
return (
<div className={styles.pill}>
{children}
</div>
)
}

View file

@ -0,0 +1,6 @@
.pill {
padding: 8px 12px;
background-color: #c2c2c2;
border-radius: 20px;
display: inline-block;
}

View file

@ -0,0 +1,16 @@
import { Meta, StoryObj } from '@storybook/react'
import { Testimony } from './Testimony'
const meta: Meta<typeof Testimony> = {
component: Testimony,
}
type Story = StoryObj<typeof Testimony>;
export default meta
export const Default: Story = {
args: {
name: 'Johan Schäfer',
testimony: 'Die Eucharistie ist für mich wie ein spiritueller Boost. Wenn ich die Hostie empfange, fühle ich mich krass verbunden mit Jesus. Es ist wie ein Reminder, dass ich nicht allein bin, egal was abgeht. Dieser Moment gibt mir richtig Power und lässt mich mit einem starken Gefühl von Frieden und Hoffnung rausgehen.'
},
}

View file

@ -0,0 +1,29 @@
import styles from "./styles.module.css";
import { Container } from '@/components/Container/Container'
import classNames from 'classnames'
import { faustina } from '@/app/fonts'
type TestimonyProps = {
name: string,
testimony: string,
}
export const Testimony = ({name, testimony}: TestimonyProps) => {
return (
<Container background={"yellow"}>
<div className={styles.testimony}>
<div className={styles.person}>
</div>
<div>
<p className={classNames(styles.testimonyText, faustina.className)}>
{testimony}
</p>
<p>
{name}
</p>
</div>
</div>
</Container>
)
}

View file

@ -0,0 +1,17 @@
.testimony {
display: flex;
padding: 80px 0 50px 0;
gap: 40px;
align-items: center;
}
.testimonyText {
font-style: italic;
}
.person {
height: 150px;
width: 150px;
background-color: white;
flex-shrink: 0;
}

29233
src/hooks/calendars.ts Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,14 @@
/**
* Return date in compact format e.G. 13.08
*/
export const useCompactDate = (date: string) => {
return date.substring(8, 10) + '.' + date.substring(5, 7) + '.'
}
/**
* Return date in user friendly format
*
*/
export const useDate = (date: string) => {
return date.substring(8, 10) + '.' + date.substring(5, 7) + '.' + date.substring(0, 4)
}

View file

@ -0,0 +1,10 @@
import { calendar } from '@/hooks/calendars'
/**
* Return liturgical name of the date
* e.G. "2024-12-25" => Christmas
*/
export const useLiturgyCalendarTitle = (date: string) => {
const day = calendar[date.substring(0, 10)];
return day.name;
}

16
src/hooks/useLocation.ts Normal file
View file

@ -0,0 +1,16 @@
import {Church} from '@/payload-types'
export const useLocation = (location: string | Church) : Church => {
if(typeof location === 'string') {
return {
address: '',
createdAt: '',
name: 'Unknown',
updatedAt: '',
id: location
}
} else {
return location
}
}

View file

@ -0,0 +1,12 @@
import { Church } from '@/payload-types'
/**
* Get user friendly location name
*/
export const useLocationName = (location: string | Church) => {
if (typeof location == "string") {
return location
} else {
return location.name
}
}

12
src/hooks/useMassType.ts Normal file
View file

@ -0,0 +1,12 @@
export const useMassType = (type: "MASS" | "FAMILY" | "WORD") => {
switch (type) {
case "FAMILY":
return "Familien Messe";
case "WORD":
return "Wort-Gottes-Feier";
case 'MASS':
default:
return "Heilige Messe";
}
}

7
src/hooks/useTime.ts Normal file
View file

@ -0,0 +1,7 @@
/**
* From a UTC datetime, return the time in HH:MM format
*/
export const useTime = (datetime: string) => {
let date = new Date(datetime);
return date.toLocaleTimeString('de-De', { timeStyle: "short"});
}

169
src/payload-types.ts Normal file
View file

@ -0,0 +1,169 @@
/* tslint:disable */
/* eslint-disable */
/**
* This file was automatically generated by Payload.
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
* and re-run `payload generate:types` to regenerate this file.
*/
export interface Config {
auth: {
users: UserAuthOperations;
};
collections: {
users: User;
media: Media;
worship: Worship;
church: Church;
testimony: Testimony;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
db: {
defaultIDType: string;
};
globals: {};
locale: null;
user: User & {
collection: 'users';
};
}
export interface UserAuthOperations {
forgotPassword: {
email: string;
password: string;
};
login: {
email: string;
password: string;
};
registerFirstUser: {
email: string;
password: string;
};
unlock: {
email: string;
password: string;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
*/
export interface User {
id: string;
updatedAt: string;
createdAt: string;
email: string;
resetPasswordToken?: string | null;
resetPasswordExpiration?: string | null;
salt?: string | null;
hash?: string | null;
loginAttempts?: number | null;
lockUntil?: string | null;
password?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "media".
*/
export interface Media {
id: string;
alt: string;
updatedAt: string;
createdAt: string;
url?: string | null;
thumbnailURL?: string | null;
filename?: string | null;
mimeType?: string | null;
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "worship".
*/
export interface Worship {
id: string;
date: string;
location: string | Church;
type: 'MASS' | 'FAMILY' | 'WORD';
cancelled: boolean;
title?: string | null;
description?: string | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "church".
*/
export interface Church {
id: string;
name: string;
address: string;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "testimony".
*/
export interface Testimony {
id: string;
testimony: string;
name: string;
occupation?: string | null;
category: 'EUCHARIST';
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences".
*/
export interface PayloadPreference {
id: string;
user: {
relationTo: 'users';
value: string | User;
};
key?: string | null;
value?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration {
id: string;
name?: string | null;
batch?: number | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth".
*/
export interface Auth {
[k: string]: unknown;
}
declare module 'payload' {
export interface GeneratedTypes extends Config {}
}

39
src/payload.config.ts Normal file
View file

@ -0,0 +1,39 @@
// storage-adapter-import-placeholder
import { mongooseAdapter } from '@payloadcms/db-mongodb'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import path from 'path'
import { buildConfig } from 'payload'
import { fileURLToPath } from 'url'
import sharp from 'sharp'
import { Users } from './collections/Users'
import { Media } from './collections/Media'
import { Worship } from '@/collections/Worship'
import { Churches } from '@/collections/Churches'
import { de } from '@payloadcms/translations/languages/de'
import { Testimony } from '@/collections/Testimony'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export default buildConfig({
admin: {
user: Users.slug,
},
collections: [Users, Media, Worship, Churches, Testimony],
editor: lexicalEditor(),
secret: process.env.PAYLOAD_SECRET || '',
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
i18n: {
supportedLanguages: { de }
},
db: mongooseAdapter({
url: process.env.DATABASE_URI || '',
}),
sharp,
plugins: [
// storage-adapter-placeholder
],
})

44
tsconfig.json Normal file
View file

@ -0,0 +1,44 @@
{
"compilerOptions": {
"baseUrl": ".",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./src/*"
],
"@payload-config": [
"./src/payload.config.ts"
]
},
"target": "ES2017"
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}

17031
yarn.lock Normal file

File diff suppressed because it is too large Load diff