diff --git a/_template.scss b/_template.scss index 73d2639..006419f 100644 --- a/_template.scss +++ b/_template.scss @@ -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; diff --git a/src/app/(home)/globals.css b/src/app/(home)/globals.css index db1abba..b7cb683 100644 --- a/src/app/(home)/globals.css +++ b/src/app/(home)/globals.css @@ -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; diff --git a/src/app/(home)/layout.tsx b/src/app/(home)/layout.tsx index 4ea8ed0..ae83975 100644 --- a/src/app/(home)/layout.tsx +++ b/src/app/(home)/layout.tsx @@ -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 ( - +
diff --git a/src/assets/fontOptions.ts b/src/assets/fontOptions.ts new file mode 100644 index 0000000..9443a80 --- /dev/null +++ b/src/assets/fontOptions.ts @@ -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 diff --git a/src/assets/fonts.ts b/src/assets/fonts.ts index 1d978f7..ea446a9 100644 --- a/src/assets/fonts.ts +++ b/src/assets/fonts.ts @@ -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', -}) \ No newline at end of file +}) +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 = { + 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 +} diff --git a/src/components/Banner/Banner.tsx b/src/components/Banner/Banner.tsx index 54c5b76..673442b 100644 --- a/src/components/Banner/Banner.tsx +++ b/src/components/Banner/Banner.tsx @@ -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 = ({
-
+
{textLine1 &&
{textLine1}
} {textLine2 &&
{textLine2}
} {textLine3 &&
{textLine3}
} diff --git a/src/components/Banner/styles.module.scss b/src/components/Banner/styles.module.scss index 9633367..c0b100b 100644 --- a/src/components/Banner/styles.module.scss +++ b/src/components/Banner/styles.module.scss @@ -19,6 +19,7 @@ .nameContainer { opacity: 1; color: $white; + font-family: var(--header-font); position: absolute; bottom: 50px; right: 0; diff --git a/src/components/Classifieds/Ad.tsx b/src/components/Classifieds/Ad.tsx index ce69791..c4d1a22 100644 --- a/src/components/Classifieds/Ad.tsx +++ b/src/components/Classifieds/Ad.tsx @@ -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 ( <> -
setDisplayContact(!displayContact)}> +
setDisplayContact(!displayContact)}>
diff --git a/src/components/Classifieds/styles.module.scss b/src/components/Classifieds/styles.module.scss index dba2c49..7adbd72 100644 --- a/src/components/Classifieds/styles.module.scss +++ b/src/components/Classifieds/styles.module.scss @@ -10,6 +10,7 @@ } .ad { + font-family: var(--header-font); display: flex; flex-direction: column; justify-content: space-between; diff --git a/src/components/MegaMenu/MegaMenu.tsx b/src/components/MegaMenu/MegaMenu.tsx index 6ddef4b..70eccf6 100644 --- a/src/components/MegaMenu/MegaMenu.tsx +++ b/src/components/MegaMenu/MegaMenu.tsx @@ -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 (
-
+
{bibleText}
diff --git a/src/components/MegaMenu/styles.module.scss b/src/components/MegaMenu/styles.module.scss index e6bdcbe..3ef15ed 100644 --- a/src/components/MegaMenu/styles.module.scss +++ b/src/components/MegaMenu/styles.module.scss @@ -17,6 +17,10 @@ justify-content: flex-end; } +.headerFont { + font-family: var(--header-font); +} + .book { text-align: right; font-size: 14px; diff --git a/src/components/Menu/styles.module.scss b/src/components/Menu/styles.module.scss index 7b3ff3d..c83bd69 100644 --- a/src/components/Menu/styles.module.scss +++ b/src/components/Menu/styles.module.scss @@ -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 { diff --git a/src/components/Testimony/Testimony.tsx b/src/components/Testimony/Testimony.tsx index 5c33186..5438874 100644 --- a/src/components/Testimony/Testimony.tsx +++ b/src/components/Testimony/Testimony.tsx @@ -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) => {
-

+

{testimony}

{typeof name === 'string' && diff --git a/src/components/Testimony/styles.module.scss b/src/components/Testimony/styles.module.scss index 3b8ace5..26bc5dd 100644 --- a/src/components/Testimony/styles.module.scss +++ b/src/components/Testimony/styles.module.scss @@ -8,6 +8,7 @@ } .testimonyText { + font-family: var(--header-font); font-size: 33px; color: $base-color; text-align: center; diff --git a/src/components/Title/Title.tsx b/src/components/Title/Title.tsx index 7f92623..c97f4c1 100644 --- a/src/components/Title/Title.tsx +++ b/src/components/Title/Title.tsx @@ -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} {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}
diff --git a/src/components/Title/styles.module.scss b/src/components/Title/styles.module.scss index fe398f5..bd9aee8 100644 --- a/src/components/Title/styles.module.scss +++ b/src/components/Title/styles.module.scss @@ -52,6 +52,10 @@ text-align: center; } +.headerFont { + font-family: var(--header-font); +} + .cancelled { text-decoration: line-through; } diff --git a/src/compositions/Footer/styles.module.scss b/src/compositions/Footer/styles.module.scss index b46debd..7d52581 100644 --- a/src/compositions/Footer/styles.module.scss +++ b/src/compositions/Footer/styles.module.scss @@ -20,7 +20,7 @@ } .list li::marker { - color: rgba($base-color, 0.6); + color: color-mix(in srgb, $base-color 60%, transparent); } .list li { diff --git a/src/fetch/design.ts b/src/fetch/design.ts new file mode 100644 index 0000000..122d589 --- /dev/null +++ b/src/fetch/design.ts @@ -0,0 +1,13 @@ +import { Design } from '@/payload-types' + +export async function fetchDesign(): Promise { + 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() +} diff --git a/src/globals/Design.ts b/src/globals/Design.ts new file mode 100644 index 0000000..499a1fc --- /dev/null +++ b/src/globals/Design.ts @@ -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')], + }, +} diff --git a/src/payload-types.ts b/src/payload-types.ts index 56c2395..14ea947 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -126,10 +126,12 @@ export interface Config { globals: { menu: Menu; footer: Footer; + design: Design; }; globalsSelect: { menu: MenuSelect | MenuSelect; footer: FooterSelect | FooterSelect; + design: DesignSelect | DesignSelect; }; 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 { createdAt?: T; globalType?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "design_select". + */ +export interface DesignSelect { + 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". diff --git a/src/payload.config.ts b/src/payload.config.ts index b7f23cf..6f026cb 100644 --- a/src/payload.config.ts +++ b/src/payload.config.ts @@ -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