Persönlichkeitscoach-Zertifikat über Greator Speaker

Anelias neues Greator-Zertifikat (Persönlichkeitscoach, 09.10.2026) steht
in „Über Anelia“ über dem Speaker-Abzeichen, im selben Lorbeerkranz mit
freigestelltem Siegel, Hover und Vorschau-Overlay. Das Siegel ist aus dem
PDF freigestellt (Kreis, 150-dpi-Raster), Farbe und Form unverändert.

getCertificate() wird zu getCertificates(): eine Liste, das neueste oben.
Ein einzelnes Abzeichen klebt weiter auf Höhe der „04“. Zwei passen dort
nicht ins Bild, also steht der Stapel still und beginnt auf Höhe der
Eyebrow. Unter lg stehen sie nebeneinander, auf dem Handy untereinander.

Der Abzeichen-Titel darf umbrechen (Zeilenhöhe 1,4, Ränder ausgeglichen;
einzeilig unverändert). Der Dialog-Titel trennt Silben, damit
„Persönlichkeitscoach“ auf dem Handy „Schließen“ nicht aus dem Bild
schiebt. Jeder Dialog bekommt seine eigene Titel-ID (useId).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5.5 committed 2026-10-21 17:01:12 +00:00
1 parent 30e9151de4
commit 1e7b84b27c
10 files changed
+124 -60

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

+27 -12
View File
@@ -17,8 +17,8 @@ export interface AboutProps {
photo: string | null;
/** Inhalt der Sektion. Ohne Angabe die deutsche Fassung – die englische reicht sie durch. */
content?: HomeContent["about"];
/** `getCertificate()` – `null` lässt die Sektion wie vorher rendern. */
certificate?: CertificateAsset | null;
/** `getCertificates()` – leer lässt die Sektion wie vorher rendern. */
certificates?: readonly CertificateAsset[];
}
/* H2 on ivory: Playfair 400, max 14ch (spec §3.2 / §6.5). */
@@ -34,12 +34,13 @@ const BODY = "max-w-[36rem] font-sans text-[1.125rem] leading-[1.6] text-fg [hyp
* verifiable facts. No counters, no logos.
*
* With photo: Portrait cols 3–6 (sticky) · text + ledger cols 7–12 · marginalia and
* certificate stacked under the ledger (cols 7–12) – the text already
* certificates stacked under the ledger (cols 7–12) – the text already
* owns the outer columns there, so nothing can stick to the right edge.
* Without: text cols 3–8 · ledger cols 3–9 · marginalia under the ledger ·
* certificate sticky in cols 10–12 (no monogram vignette – see below).
* certificates in cols 10–12, a single one sticky (no monogram vignette –
* see below).
*/
export function About({ photo, content: about = aboutDe, certificate = null }: AboutProps) {
export function About({ photo, content: about = aboutDe, certificates = [] }: AboutProps) {
const paragraphs = about.paragraphs.map((p, i) =>
i === about.paragraphs.length - 1 && flags.showTwins ? `${p} ${about.twins}` : p,
);
@@ -104,11 +105,13 @@ export function About({ photo, content: about = aboutDe, certificate = null }: A
<Marginalia items={about.marginalia} />
</Reveal>
{/* Das Zertifikat ist das Gegenstück zur klebenden „04“: gleiche Höhe
(`calc(50vh − .45em)`), gleiche Zeilen, nur am anderen Rand. Mit Foto
reicht der Text bis Spalte 12 – dort gäbe es keine freie Gutter, also
steht es dann still unter dem Werdegang. */}
{certificate && (
{/* Ein einzelnes Zertifikat ist das Gegenstück zur klebenden „04“: gleiche
Höhe (`calc(50vh − .45em)`), gleiche Zeilen, nur am anderen Rand. Zwei
Abzeichen (je rund 24 rem) passen dort nicht mehr ins Bild – dann steht
der Stapel still und beginnt oben auf Höhe der Eyebrow, das neueste
zuerst. Mit Foto reicht der Text bis Spalte 12 – dort gäbe es keine freie
Gutter, also stehen sie dann still unter dem Werdegang. */}
{certificates.length > 0 && (
<Reveal
className={cn(
"col-span-12 mt-14",
@@ -117,8 +120,20 @@ export function About({ photo, content: about = aboutDe, certificate = null }: A
: "lg:col-span-3 lg:col-start-10 lg:row-span-3 lg:row-start-1 lg:mt-0",
)}
>
<div className={cn(!hasPhoto && "lg:sticky lg:top-[calc(50vh_-_0.45em)]")}>
<Certificate content={about.certificate} asset={certificate} />
<div
className={cn(
"flex flex-wrap gap-x-12 gap-y-16 lg:flex-col",
!hasPhoto && certificates.length === 1 && "lg:sticky lg:top-[calc(50vh_-_0.45em)]",
)}
>
{certificates.map((asset) => (
<Certificate
key={asset.key}
content={about.certificates[asset.key]}
asset={asset}
className="w-full"
/>
))}
</div>
</Reveal>
)}
+2 -2
View File
@@ -7,7 +7,7 @@ import { About } from "@/components/home/About";
import { Audience } from "@/components/home/Audience";
import { ProcessContact } from "@/components/home/ProcessContact";
import type { HomeContent } from "@/content/home";
import { getCertificate, getPhotos } from "@/lib/assets";
import { getCertificates, getPhotos } from "@/lib/assets";
import { routesByLang, type Lang } from "@/lib/i18n";
import { rootCopy } from "@/lib/metadata";
import { site } from "@/lib/site";
@@ -37,7 +37,7 @@ export function HomeBody({ lang, content }: { lang: Lang; content: HomeContent }
<Positioning content={content.positioning} />
<ServicesIndex content={content.services} />
<WolfPrinciple mark={<BrandMark idPrefix="prinzip" />} content={content.principle} />
<About photo={aboutPhoto} content={content.about} certificate={getCertificate()} />
<About photo={aboutPhoto} content={content.about} certificates={getCertificates()} />
<Audience content={content.audience} />
<ProcessContact content={content.contact} privacyHref={routesByLang[lang].datenschutz} />
</>
+6 -2
View File
@@ -63,11 +63,15 @@ export function Certificate({ content, asset, className }: CertificateProps) {
</span>
</span>
<span className="mt-6 block font-mono text-xs uppercase leading-none tracking-[0.18em] text-accent">
{/* Zeilenhöhe 1,4 statt 1, damit ein umbrechender Titel („Greator
Persönlichkeitscoach“) nicht zusammenklebt. Die Ränder davor und danach
sind um den halben Durchschuss (0,15 rem) kürzer – einzeilig steht alles
wie zuvor mit `leading-none`, `mt-6` und `mt-5`. */}
<span className="mt-[1.35rem] block font-mono text-xs uppercase leading-[1.4] tracking-[0.18em] text-accent">
{content.title}
</span>
<span aria-hidden="true" className="rule-gold mt-5 block h-px w-full" />
<span aria-hidden="true" className="rule-gold mt-[1.1rem] block h-px w-full" />
<span className="mt-5 block font-sans text-[0.875rem] leading-[1.5] text-fg [hyphens:auto]">
{content.description}
+10 -8
View File
@@ -1,14 +1,11 @@
"use client";
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import type { CertificateAsset } from "@/lib/assets";
import type { CertificateCopy } from "@/content/home";
import { gsap, DUR, EASE, MM, getLenis, refreshSoon } from "@/lib/gsap";
/** Die Überschrift benennt den Dialog (`aria-labelledby`) – es gibt genau einen. */
const TITLE_ID = "zertifikat-titel";
/**
* Kopf, Blatt und Fußzeile teilen sich eine Breite, sonst stehen sie versetzt.
* Auf dem Desktop richtet sie sich nach der Höhe: das Blatt soll ganz ins Bild
@@ -39,6 +36,8 @@ export interface CertificateDialogProps {
* Dialog in seinem eigenen toten Bereich.
*/
export function CertificateDialog({ open, onClose, content, asset }: CertificateDialogProps) {
/* Die Überschrift benennt den Dialog (`aria-labelledby`) – je Zertifikat ein Dialog. */
const titleId = useId();
const panel = useRef<HTMLDivElement>(null);
const onCloseRef = useRef(onClose);
@@ -138,7 +137,7 @@ export function CertificateDialog({ open, onClose, content, asset }: Certificate
ref={panel}
role="dialog"
aria-modal="true"
aria-labelledby={TITLE_ID}
aria-labelledby={titleId}
data-theme="dark"
data-lenis-prevent=""
onClick={(e) => {
@@ -148,12 +147,15 @@ export function CertificateDialog({ open, onClose, content, asset }: Certificate
className="fixed inset-0 z-50 flex flex-col items-center overflow-y-auto overscroll-contain bg-ink px-5 py-[calc(var(--header-h,60px)/2+16px)] text-fg lg:px-10"
>
<div className={`mb-8 flex items-start justify-between gap-6 ${COLUMN}`}>
<div>
{/* `min-w-0` + Silbentrennung: „Persönlichkeitscoach“ ist auf dem Handy
breiter als der Platz neben „Schließen“ und schöbe den Knopf sonst
aus dem Bild. */}
<div className="min-w-0">
<p className="font-mono text-xs uppercase tracking-[0.12em] text-accent">{content.eyebrow}</p>
{/* Playfair hat hier den Platz für seine 28-px-Untergrenze – im Abzeichen selbst nicht. */}
<h2
id={TITLE_ID}
className="mt-3 font-display text-[clamp(1.75rem,1.2rem+1vw,2.25rem)] font-normal leading-none tracking-[-0.01em] text-fg"
id={titleId}
className="mt-3 font-display text-[clamp(1.75rem,1.2rem+1vw,2.25rem)] font-normal leading-none tracking-[-0.01em] text-fg [hyphens:auto]"
>
{content.title}
</h2>
+25 -11
View File
@@ -138,17 +138,31 @@ export const homeEn: HomeContent = {
{ value: "LinkedIn", href: site.linkedin, external: true },
{ value: "German · English" },
],
certificate: {
title: "Greator Speaker",
description: "Certified graduate of the Greator Speaker programme",
date: "May 2026",
cta: "View certificate",
eyebrow: "Certificate",
dialogMeta: "Greator TheKey · issued 16 May 2026",
imageAlt:
"Certificate: Anelia Wolf has successfully completed the certified Greator Speaker programme.",
pdfLabel: "Open PDF",
close: "Close",
certificates: {
persoenlichkeitscoach: {
title: "Greator Personality Coach",
description: "Certified graduate of the Greator Personality Coach programme",
date: "October 2026",
cta: "View certificate",
eyebrow: "Certificate",
dialogMeta: "Greator · issued 9 October 2026",
imageAlt:
"Certificate: Anelia Wolf has successfully completed the theoretical and practical parts of the three-month Greator Personality Coach programme.",
pdfLabel: "Open PDF",
close: "Close",
},
speaker: {
title: "Greator Speaker",
description: "Certified graduate of the Greator Speaker programme",
date: "May 2026",
cta: "View certificate",
eyebrow: "Certificate",
dialogMeta: "Greator TheKey · issued 16 May 2026",
imageAlt:
"Certificate: Anelia Wolf has successfully completed the certified Greator Speaker programme.",
pdfLabel: "Open PDF",
close: "Close",
},
},
folio: { current: "05", total: "06" },
},
+31 -15
View File
@@ -262,20 +262,36 @@ export const about = {
{ value: "LinkedIn", href: site.linkedin, external: true },
{ value: "Deutsch · Englisch" },
] satisfies readonly MarginaliaItem[],
/* Das Abzeichen in der rechten Gutter – Gegenstück zur klebenden „04“ links.
`seal` steht IM Lorbeerkranz, alles darunter ist die Bildunterschrift. */
certificate: {
title: "Greator Speaker",
description: "Zertifizierte Absolventin der Greator Speaker Ausbildung",
date: "Mai 2026",
cta: "Zertifikat ansehen",
/* Overlay */
eyebrow: "Zertifikat",
dialogMeta: "Greator TheKey · Ausstellungsdatum 16. Mai 2026",
imageAlt:
"Zertifikat: Anelia Wolf hat die Ausbildung zum zertifizierten Greator Speaker erfolgreich abgeschlossen.",
pdfLabel: "PDF öffnen",
close: "Schließen",
/* Die Abzeichen in der rechten Gutter – Gegenstück zur klebenden „04“ links.
`seal` steht IM Lorbeerkranz, alles darunter ist die Bildunterschrift.
Schlüssel wie in `getCertificates()` (src/lib/assets.ts), dort auch die Reihenfolge. */
certificates: {
persoenlichkeitscoach: {
title: "Greator Persönlichkeitscoach",
description: "Zertifizierte Absolventin der Ausbildung zum Greator Persönlichkeitscoach",
date: "Oktober 2026",
cta: "Zertifikat ansehen",
/* Overlay */
eyebrow: "Zertifikat",
dialogMeta: "Greator · Ausstellungsdatum 9. Oktober 2026",
imageAlt:
"Zertifikat: Anelia Wolf hat den theoretischen und praktischen Teil der dreimonatigen Ausbildung zum Greator Persönlichkeitscoach erfolgreich abgeschlossen.",
pdfLabel: "PDF öffnen",
close: "Schließen",
},
speaker: {
title: "Greator Speaker",
description: "Zertifizierte Absolventin der Greator Speaker Ausbildung",
date: "Mai 2026",
cta: "Zertifikat ansehen",
/* Overlay */
eyebrow: "Zertifikat",
dialogMeta: "Greator TheKey · Ausstellungsdatum 16. Mai 2026",
imageAlt:
"Zertifikat: Anelia Wolf hat die Ausbildung zum zertifizierten Greator Speaker erfolgreich abgeschlossen.",
pdfLabel: "PDF öffnen",
close: "Schließen",
},
},
folio: { current: "05", total: "06" },
} as const;
@@ -389,4 +405,4 @@ assertNoPlaceholders("src/content/home.ts", collectStrings(home));
/** Struktur, nicht Wortlaut – die englische Fassung erfüllt denselben Typ. */
export type HomeContent = Widen<typeof home>;
/** Texte des Zertifikat-Abzeichens – Abzeichen und Overlay teilen sie sich. */
export type CertificateCopy = HomeContent["about"]["certificate"];
export type CertificateCopy = HomeContent["about"]["certificates"]["speaker"];
+23 -10
View File
@@ -57,26 +57,39 @@ export function getSpeakerPdf(): string | null {
return publicUrl("speaker-profil.pdf");
}
/** Schlüssel in `about.certificates` (Texte) – Reihenfolge siehe `CERTIFICATES`. */
export type CertificateKey = "persoenlichkeitscoach" | "speaker";
export interface CertificateAsset {
/** `/zertifikate/greator-speaker.pdf` – das Original zum Öffnen/Herunterladen. */
key: CertificateKey;
/** z. B. `/zertifikate/greator-speaker.pdf` – das Original zum Öffnen/Herunterladen. */
pdf: string;
/** Seite 1 als Bild für die Vorschau (kein eingebettetes PDF, kein iframe). */
image: string;
width: number;
height: number;
/** Das runde Greator-TheKey-Siegel, freigestellt – es steht im Lorbeerkranz. */
/** Das runde Greator-Siegel, freigestellt – es steht im Lorbeerkranz. */
seal: string;
sealSize: number;
}
/** Oben steht das neueste Zertifikat. */
const CERTIFICATES = [
{ key: "persoenlichkeitscoach", file: "greator-persoenlichkeitscoach", seal: "greator-siegel-coach.webp", width: 989, height: 1398 },
{ key: "speaker", file: "greator-speaker", seal: "greator-siegel.webp", width: 989, height: 1400 },
] as const satisfies readonly { key: CertificateKey; file: string; seal: string; width: number; height: number }[];
/**
* Greator-Speaker-Zertifikat für die Sektion „Über Anelia“. Alle drei Dateien müssen
* liegen – fehlt eine, rendert die Sektion wie vorher (Variante B, wie bei den Fotos).
* Greator-Zertifikate für die Sektion „Über Anelia“. Ein Zertifikat erscheint nur,
* wenn alle drei Dateien liegen – fehlen alle, rendert die Sektion wie vorher
* (Variante B, wie bei den Fotos).
*/
export function getCertificate(): CertificateAsset | null {
const pdf = publicUrl("zertifikate/greator-speaker.pdf");
const image = publicUrl("zertifikate/greator-speaker.jpg");
const seal = publicUrl("zertifikate/greator-siegel.webp");
if (!pdf || !image || !seal) return null;
return { pdf, image, width: 989, height: 1400, seal, sealSize: 372 };
export function getCertificates(): CertificateAsset[] {
return CERTIFICATES.flatMap(({ key, file, seal: sealFile, width, height }) => {
const pdf = publicUrl(`zertifikate/${file}.pdf`);
const image = publicUrl(`zertifikate/${file}.jpg`);
const seal = publicUrl(`zertifikate/${sealFile}`);
if (!pdf || !image || !seal) return [];
return [{ key, pdf, image, width, height, seal, sealSize: 372 }];
});
}