feat: customize design

This commit is contained in:
Benno Tielen 2026-03-06 15:10:14 +01:00
parent 0240f2df4f
commit 4c21073001
21 changed files with 383 additions and 33 deletions

View file

@ -15,15 +15,14 @@
//$highlight-color: #fff318;
//$shadow: 3px 7px 26px -5px rgba(0, 0, 0, 0.15);
//$overlay: rgba(63, 63, 63, 0.82);
$base-color: #016699;
$shade1: #67A3C2;
$shade2: #DDECF7;
//$shade3: #E3E9E8;
$shade3: #eff6ff;
$contrast-color: #CE490F;
$contrast-shade1: #DA764B;
$base-color: var(--base-color);
$shade1: var(--shade1);
$shade2: var(--shade2);
$shade3: var(--shade3);
$contrast-color: var(--contrast-color);
$contrast-shade1: var(--contrast-shade1);
$text-color: #000000;
$border-radius: 13px;
$border-radius: var(--border-radius);
$white: #ffffff;
$light-grey: #f3f3f3;

View file

@ -1,3 +1,13 @@
:root {
--base-color: #016699;
--shade1: #67A3C2;
--shade2: #DDECF7;
--shade3: #eff6ff;
--contrast-color: #CE490F;
--contrast-shade1: #DA764B;
--border-radius: 13px;
}
html,
body {
margin: 0;

View file

@ -3,8 +3,9 @@ import './globals.css'
import { DynamicMenu, Menu } from '@/components/Menu/Menu'
import { Footer } from '@/compositions/Footer/Footer'
import { comment } from '@/app/(home)/layout-comment'
import { defaultFont } from '@/assets/fonts'
import { FONT_MAP, getFont } from '@/assets/fonts'
import { siteConfig } from '@/config/site'
import { fetchDesign } from '@/fetch/design'
export const metadata: Metadata = {
title: {
@ -24,15 +25,53 @@ export const metadata: Metadata = {
},
}
export default function RootLayout({
const DESIGN_DEFAULTS = {
baseColor: '#016699',
shade1: '#67A3C2',
shade2: '#DDECF7',
shade3: '#eff6ff',
contrastColor: '#CE490F',
contrastShade1: '#DA764B',
borderRadius: '13px',
defaultFont: 'cairo',
headerFont: 'faustina',
}
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
let design
try {
design = await fetchDesign()
} catch {
design = null
}
const selectedDefaultFont = getFont(
design?.defaultFont || DESIGN_DEFAULTS.defaultFont,
FONT_MAP.cairo,
)
const selectedHeaderFont = getFont(
design?.headerFont || DESIGN_DEFAULTS.headerFont,
FONT_MAP.faustina,
)
const themeStyle = {
'--base-color': design?.baseColor || DESIGN_DEFAULTS.baseColor,
'--shade1': design?.shade1 || DESIGN_DEFAULTS.shade1,
'--shade2': design?.shade2 || DESIGN_DEFAULTS.shade2,
'--shade3': design?.shade3 || DESIGN_DEFAULTS.shade3,
'--contrast-color': design?.contrastColor || DESIGN_DEFAULTS.contrastColor,
'--contrast-shade1':
design?.contrastShade1 || DESIGN_DEFAULTS.contrastShade1,
'--border-radius': design?.borderRadius || DESIGN_DEFAULTS.borderRadius,
'--header-font': selectedHeaderFont.style.fontFamily,
} as React.CSSProperties
return (
<html lang="de" className={defaultFont.className}>
<html lang="de" className={selectedDefaultFont.className} style={themeStyle}>
<body>
<div dangerouslySetInnerHTML={{ __html: comment }}></div>
<DynamicMenu />

15
src/assets/fontOptions.ts Normal file
View file

@ -0,0 +1,15 @@
export const FONT_OPTIONS = [
{ label: 'Cairo (Sans-Serif)', value: 'cairo' },
{ label: 'Roboto (Sans-Serif)', value: 'roboto' },
{ label: 'Open Sans (Sans-Serif)', value: 'openSans' },
{ label: 'Lato (Sans-Serif)', value: 'lato' },
{ label: 'Nunito (Sans-Serif)', value: 'nunito' },
{ label: 'Raleway (Sans-Serif)', value: 'raleway' },
{ label: 'Faustina (Serif)', value: 'faustina' },
{ label: 'Merriweather (Serif)', value: 'merriweather' },
{ label: 'Source Sans 3 (Sans-Serif)', value: 'sourceSans3' },
{ label: 'Playfair Display (Serif)', value: 'playfairDisplay' },
{ label: 'Lora (Serif)', value: 'lora' },
{ label: 'Crimson Text (Serif)', value: 'crimsonText' },
{ label: 'EB Garamond (Serif)', value: 'ebGaramond' },
] as const

View file

@ -1,11 +1,84 @@
import { Cairo, Faustina } from 'next/font/google'
import {
Cairo,
Faustina,
Merriweather,
Nunito,
Open_Sans,
Playfair_Display,
Raleway,
Roboto,
Source_Sans_3,
Crimson_Text,
EB_Garamond, Lato,
Lora
} from 'next/font/google'
import { NextFont } from 'next/dist/compiled/@next/font'
export const headerFont = Faustina({
const cairo = Cairo({
subsets: ['latin'],
weight: ['300', '400'],
display: 'swap',
})
const faustina = Faustina({ subsets: ['latin'], display: 'swap' })
const lato = Lato({
subsets: ['latin'],
weight: ['300', '400', '700'],
display: 'swap'
})
const merriweather = Merriweather({
subsets: ['latin'],
weight: ['300', '400', '700'],
display: 'swap',
})
const nunito = Nunito({ subsets: ['latin'], display: 'swap' })
const openSans = Open_Sans({ subsets: ['latin'], display: 'swap' })
const playfairDisplay = Playfair_Display({
subsets: ['latin'],
display: 'swap',
})
export const defaultFont = Cairo({
const raleway = Raleway({ subsets: ['latin'], display: 'swap' })
const roboto = Roboto({
subsets: ['latin'],
weight: ['400', '300'],
weight: ['300', '400', '700'],
display: 'swap',
})
})
const sourceSans3 = Source_Sans_3({ subsets: ['latin'], display: 'swap' })
const crimsonText = Crimson_Text({
subsets: ['latin'],
weight: ['400', '600', '700'],
display: 'swap',
})
const ebGaramond = EB_Garamond({ subsets: ['latin'], display: 'swap' })
const lora = Lora({
subsets: ['latin'],
weight: ['400', '500', '600', '700'],
display: 'swap',
})
export const FONT_MAP: Record<string, NextFont> = {
cairo,
faustina,
lato,
merriweather,
nunito,
openSans,
playfairDisplay,
raleway,
roboto,
sourceSans3,
crimsonText,
ebGaramond,
lora
}
export const defaultFont = cairo
export const headerFont = faustina
export function getFont(
key: string | null | undefined,
fallback: NextFont,
): NextFont {
if (!key) return fallback
return FONT_MAP[key] ?? fallback
}

View file

@ -1,8 +1,5 @@
import { Logo } from '@/components/Logo/Logo'
import styles from "./styles.module.scss"
import classNames from 'classnames'
import { headerFont } from '@/assets/fonts'
export interface BannerProps {
textLine1?: string | null
@ -45,7 +42,7 @@ export const Banner = ({
<div className={styles.logo}>
<Logo color={"#ffffff33"} height={200} />
</div>
<div className={classNames(headerFont.className, styles.nameContainer)}>
<div className={styles.nameContainer}>
{textLine1 && <div className={styles.catholic}>{textLine1}</div>}
{textLine2 && <div className={styles.name}>{textLine2}</div>}
{textLine3 && <div className={styles.location}>{textLine3}</div>}

View file

@ -19,6 +19,7 @@
.nameContainer {
opacity: 1;
color: $white;
font-family: var(--header-font);
position: absolute;
bottom: 50px;
right: 0;

View file

@ -5,7 +5,6 @@ import styles from '@/components/Classifieds/styles.module.scss'
import { useState } from 'react'
import { SerializedEditorState } from 'lexical'
import { RichText } from '@payloadcms/richtext-lexical/react'
import { headerFont } from '@/assets/fonts'
type AdProps = {
text: SerializedEditorState,
@ -18,7 +17,7 @@ export const Ad = ({text, contact}: AdProps) => {
return (
<>
<div className={classNames(styles.ad, headerFont.className)} onClick={() => setDisplayContact(!displayContact)}>
<div className={styles.ad} onClick={() => setDisplayContact(!displayContact)}>
<div className={styles.adText}>
<RichText data={text} />
</div>

View file

@ -10,6 +10,7 @@
}
.ad {
font-family: var(--header-font);
display: flex;
flex-direction: column;
justify-content: space-between;

View file

@ -1,6 +1,5 @@
import styles from "./styles.module.scss"
import Link from 'next/link'
import { headerFont } from '@/assets/fonts'
type MegaMenuProps = {
bibleText: string,
@ -63,7 +62,7 @@ export const MegaMenu = ({ bibleText, bibleBook, groups, onItemClick }: MegaMenu
return (
<div className={styles.menu}>
<div className={styles.bibleText}>
<div className={headerFont.className}>
<div className={styles.headerFont}>
{bibleText}
</div>
<div className={styles.book}>

View file

@ -17,6 +17,10 @@
justify-content: flex-end;
}
.headerFont {
font-family: var(--header-font);
}
.book {
text-align: right;
font-size: 14px;

View file

@ -10,11 +10,12 @@
left: 0;
top: 0;
width: 100%;
height: 62px;
box-sizing: border-box;
z-index: 1;
backdrop-filter: blur(8px);
font-size: 18px;
align-items: baseline;
align-items: center;
}
.navMobile {

View file

@ -2,8 +2,6 @@ import styles from './styles.module.scss'
import { Container } from '@/components/Container/Container'
import classNames from 'classnames'
import { headerFont } from '@/assets/fonts'
type TestimonyProps = {
name?: string
testimony: string
@ -15,7 +13,7 @@ export const Testimony = ({ name, testimony, occupation }: TestimonyProps) => {
<Container>
<div className={styles.testimony}>
<div className={styles.container}>
<p className={classNames(styles.testimonyText, headerFont.className)}>
<p className={styles.testimonyText}>
{testimony}
</p>
{typeof name === 'string' &&

View file

@ -8,6 +8,7 @@
}
.testimonyText {
font-family: var(--header-font);
font-size: 33px;
color: $base-color;
text-align: center;

View file

@ -1,8 +1,6 @@
import styles from "./styles.module.scss";
import classNames from 'classnames'
import { headerFont } from '@/assets/fonts'
type TitleProps = {
title: string;
subtitle?: string;
@ -27,7 +25,7 @@ export const Title = ({title, subtitle, align = "left", size = "lg", fontStyle =
[styles.small]: size === "sm",
[styles.left]: align === "left",
[styles.center]: align === "center",
[headerFont.className]: fontStyle == "serif",
[styles.headerFont]: fontStyle == "serif",
[styles.cancelled]: cancelled
})}>{title}</h2>
{subtitle &&
@ -39,7 +37,7 @@ export const Title = ({title, subtitle, align = "left", size = "lg", fontStyle =
[styles.small]: ["xl", "lg"].includes(size),
[styles.left]: align === "left",
[styles.center]: align === "center",
[headerFont.className]: fontStyle == "sans-serif"
[styles.headerFont]: fontStyle == "sans-serif"
})}>
{subtitle}
</div>

View file

@ -52,6 +52,10 @@
text-align: center;
}
.headerFont {
font-family: var(--header-font);
}
.cancelled {
text-decoration: line-through;
}

View file

@ -20,7 +20,7 @@
}
.list li::marker {
color: rgba($base-color, 0.6);
color: color-mix(in srgb, $base-color 60%, transparent);
}
.list li {

13
src/fetch/design.ts Normal file
View file

@ -0,0 +1,13 @@
import { Design } from '@/payload-types'
export async function fetchDesign(): Promise<Design> {
const res = await fetch('http://localhost:3000/api/globals/design', {
next: { tags: ['design'] },
})
if (!res.ok) {
throw new Error('Could not fetch design')
}
return res.json()
}

118
src/globals/Design.ts Normal file
View file

@ -0,0 +1,118 @@
import { GlobalConfig } from 'payload'
import { isAdmin } from '@/collections/access/admin'
import { revalidateTag } from 'next/cache'
import { FONT_OPTIONS } from '@/assets/fontOptions'
const hexColorValidation = (value: string | null | undefined) => {
if (!value) return true
if (/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/.test(value)) return true
return 'Bitte geben Sie einen gültigen Hex-Farbwert ein (z.B. #016699)'
}
export const DesignGlobal: GlobalConfig = {
slug: 'design',
label: {
de: 'Design',
},
admin: {
description:
'Hier können Sie die Farben und das Erscheinungsbild der Website konfigurieren.',
},
fields: [
{
type: 'collapsible',
label: 'Farben',
fields: [
{
name: 'baseColor',
type: 'text',
label: { de: 'Grundfarbe' },
defaultValue: '#016699',
validate: hexColorValidation,
},
{
name: 'shade1',
type: 'text',
label: { de: 'Farbton 1' },
defaultValue: '#67A3C2',
validate: hexColorValidation,
},
{
name: 'shade2',
type: 'text',
label: { de: 'Farbton 2' },
defaultValue: '#DDECF7',
validate: hexColorValidation,
},
{
name: 'shade3',
type: 'text',
label: { de: 'Farbton 3' },
defaultValue: '#eff6ff',
validate: hexColorValidation,
},
{
name: 'contrastColor',
type: 'text',
label: { de: 'Kontrastfarbe' },
defaultValue: '#CE490F',
validate: hexColorValidation,
},
{
name: 'contrastShade1',
type: 'text',
label: { de: 'Kontrastfarbton 1' },
defaultValue: '#DA764B',
validate: hexColorValidation,
},
],
},
{
type: 'collapsible',
label: 'Schriftarten',
fields: [
{
name: 'defaultFont',
type: 'select',
label: { de: 'Standardschrift' },
defaultValue: 'cairo',
options: FONT_OPTIONS.map((o) => ({
label: o.label,
value: o.value,
})),
admin: {
description:
'Die Hauptschrift für den gesamten Text der Website.',
},
},
{
name: 'headerFont',
type: 'select',
label: { de: 'Überschriftenschrift' },
defaultValue: 'faustina',
options: FONT_OPTIONS.map((o) => ({
label: o.label,
value: o.value,
})),
admin: {
description:
'Die Schrift für Überschriften und hervorgehobenen Text.',
},
},
],
},
{
name: 'borderRadius',
type: 'text',
label: { de: 'Eckenradius' },
defaultValue: '13px',
},
],
access: {
read: () => true,
update: isAdmin(),
},
hooks: {
afterChange: [() => revalidateTag('design')],
},
}

View file

@ -126,10 +126,12 @@ export interface Config {
globals: {
menu: Menu;
footer: Footer;
design: Design;
};
globalsSelect: {
menu: MenuSelect<false> | MenuSelect<true>;
footer: FooterSelect<false> | FooterSelect<true>;
design: DesignSelect<false> | DesignSelect<true>;
};
locale: null;
user: User & {
@ -1896,6 +1898,64 @@ export interface Footer {
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* Hier können Sie die Farben und das Erscheinungsbild der Website konfigurieren.
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "design".
*/
export interface Design {
id: string;
baseColor?: string | null;
shade1?: string | null;
shade2?: string | null;
shade3?: string | null;
contrastColor?: string | null;
contrastShade1?: string | null;
/**
* Die Hauptschrift für den gesamten Text der Website.
*/
defaultFont?:
| (
| 'cairo'
| 'roboto'
| 'openSans'
| 'lato'
| 'nunito'
| 'raleway'
| 'faustina'
| 'merriweather'
| 'sourceSans3'
| 'playfairDisplay'
| 'lora'
| 'crimsonText'
| 'ebGaramond'
)
| null;
/**
* Die Schrift für Überschriften und hervorgehobenen Text.
*/
headerFont?:
| (
| 'cairo'
| 'roboto'
| 'openSans'
| 'lato'
| 'nunito'
| 'raleway'
| 'faustina'
| 'merriweather'
| 'sourceSans3'
| 'playfairDisplay'
| 'lora'
| 'crimsonText'
| 'ebGaramond'
)
| null;
borderRadius?: string | null;
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "menu_select".
@ -1999,6 +2059,24 @@ export interface FooterSelect<T extends boolean = true> {
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "design_select".
*/
export interface DesignSelect<T extends boolean = true> {
baseColor?: T;
shade1?: T;
shade2?: T;
shade3?: T;
contrastColor?: T;
contrastShade1?: T;
defaultFont?: T;
headerFont?: T;
borderRadius?: T;
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth".

View file

@ -38,6 +38,7 @@ import { LiturgicalCalendar } from '@/collections/LiturgicalCalendar'
import { Classifieds } from '@/collections/Classifieds'
import { MenuGlobal } from '@/globals/Menu'
import { FooterGlobal } from '@/globals/Footer'
import { DesignGlobal } from '@/globals/Design'
import { Magazine } from '@/collections/Magazine'
import { DonationForms } from '@/collections/DonationForms'
import { Pages } from '@/collections/Pages'
@ -105,6 +106,7 @@ export default buildConfig({
globals: [
MenuGlobal,
FooterGlobal,
DesignGlobal,
],
graphQL: {
disable: true