init
2
.env.example
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 80,
|
||||
"semi": false
|
||||
}
|
||||
17
.storybook/main.ts
Normal 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
|
|
@ -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
6
.yarnrc
Normal 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
|
|
@ -0,0 +1,3 @@
|
|||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.4.0.cjs
|
||||
69
Dockerfile
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
72
package.json
Normal 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"
|
||||
}
|
||||
22
src/app/(payload)/admin/[[...segments]]/not-found.tsx
Normal 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
|
||||
22
src/app/(payload)/admin/[[...segments]]/page.tsx
Normal 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
|
||||
10
src/app/(payload)/api/[...slug]/route.ts
Normal 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)
|
||||
6
src/app/(payload)/api/graphql-playground/route.ts
Normal 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)
|
||||
6
src/app/(payload)/api/graphql/route.ts
Normal 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)
|
||||
0
src/app/(payload)/custom.scss
Normal file
16
src/app/(payload)/layout.tsx
Normal 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
|
||||
29
src/app/BannerWithMenu.tsx
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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' mich das Verlorene wiederfinden und zeige mir so deine Güte.
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
85
src/app/page.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
4
src/app/worship/[id]/bell.svg
Normal 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 |
5
src/app/worship/[id]/location.svg
Normal 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 |
86
src/app/worship/[id]/page.tsx
Normal 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.\""}/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
4
src/app/worship/[id]/question.svg
Normal 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 |
40
src/app/worship/[id]/styles.module.css
Normal 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;
|
||||
}
|
||||
37
src/collections/Churches.ts
Normal 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
|
|
@ -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,
|
||||
}
|
||||
53
src/collections/Testimony.ts
Normal 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
|
|
@ -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
|
||||
],
|
||||
}
|
||||
86
src/collections/Worship.ts
Normal 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
|
||||
}
|
||||
}
|
||||
13
src/components/BannerText/BannerText.stories.ts
Normal 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: {}
|
||||
}
|
||||
18
src/components/BannerText/BannerText.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
42
src/components/BannerText/bannerText.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
15
src/components/Card/Card.stories.tsx
Normal 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</>
|
||||
},
|
||||
}
|
||||
13
src/components/Card/Card.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
6
src/components/Card/styles.module.css
Normal 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;
|
||||
}
|
||||
22
src/components/Container/Container.stories.tsx
Normal 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</>
|
||||
}
|
||||
}
|
||||
17
src/components/Container/Container.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
11
src/components/Container/styles.module.css
Normal 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;
|
||||
}
|
||||
19
src/components/HomeBanner/HomeBanner.css
Normal 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;
|
||||
}
|
||||
15
src/components/HomeBanner/HomeBanner.stories.ts
Normal 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
|
||||
}
|
||||
}
|
||||
72
src/components/HomeBanner/HomeBanner.tsx
Normal 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>
|
||||
)
|
||||
});
|
||||
150
src/components/HomeBanner/bg.svg
Normal 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 |
38
src/components/Map/Map.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
3
src/components/Map/styles.module.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.map {
|
||||
height: 300px;
|
||||
}
|
||||
41
src/components/MassTable/MassTable.stories.ts
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
33
src/components/MassTable/MassTable.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
49
src/components/MassTable/MassTableRow.stories.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
49
src/components/MassTable/MassTableRow.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
2
src/components/MassTable/bible.svg
Normal 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 |
42
src/components/MassTable/family.svg
Normal 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 |
28
src/components/MassTable/styles.module.css
Normal 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;
|
||||
}
|
||||
25
src/components/MassTimer/MassTimer.stories.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
50
src/components/MassTimer/MassTimer.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
19
src/components/MassTimer/masstimer.module.css
Normal 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;
|
||||
}
|
||||
42
src/components/MassTimer/useCountdown.ts
Normal 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 };
|
||||
15
src/components/MassTimerTooltip/MassTimerTooltip.stories.ts
Normal 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: {
|
||||
|
||||
}
|
||||
}
|
||||
31
src/components/MassTimerTooltip/MassTimerTooltip.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
22
src/components/MassTimerTooltip/clara.svg
Normal 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 |
28
src/components/MassTimerTooltip/massTimerTooltip.module.css
Normal 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;
|
||||
}
|
||||
23
src/components/MassTitle/MassTitle.stories.tsx
Normal 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
|
||||
},
|
||||
}
|
||||
18
src/components/MassTitle/MassTitle.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
14
src/components/MassTitle/styles.module.css
Normal 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;
|
||||
}
|
||||
15
src/components/Menu/Menu.stories.ts
Normal 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: {
|
||||
|
||||
}
|
||||
}
|
||||
41
src/components/Menu/Menu.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
4
src/components/Menu/menu.svg
Normal 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 |
63
src/components/Menu/styles.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
15
src/components/MenuBaseLayer/MenuBaseLayer.stories.tsx
Normal 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: {
|
||||
|
||||
}
|
||||
}
|
||||
5
src/components/MenuBaseLayer/MenuBaseLayer.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import styles from "./style.module.css"
|
||||
|
||||
export const MenuBaseLayer = () => {
|
||||
return <div className={styles.background}></div>
|
||||
}
|
||||
9
src/components/MenuBaseLayer/bg.svg
Normal 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 |
10
src/components/MenuBaseLayer/style.module.css
Normal 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;
|
||||
}
|
||||
15
src/components/Pill/Pill.stories.tsx
Normal 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"
|
||||
},
|
||||
}
|
||||
13
src/components/Pill/Pill.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
6
src/components/Pill/styles.module.css
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
.pill {
|
||||
padding: 8px 12px;
|
||||
background-color: #c2c2c2;
|
||||
border-radius: 20px;
|
||||
display: inline-block;
|
||||
}
|
||||
16
src/components/Testimony/Testimony.stories.tsx
Normal 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.'
|
||||
},
|
||||
}
|
||||
29
src/components/Testimony/Testimony.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
17
src/components/Testimony/styles.module.css
Normal 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
14
src/hooks/useCompactDate.ts
Normal 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)
|
||||
}
|
||||
10
src/hooks/useLiturgyCalendarTitle.ts
Normal 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
|
|
@ -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
|
||||
}
|
||||
}
|
||||
12
src/hooks/useLocationName.ts
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
||||
]
|
||||
}
|
||||