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:
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 |
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 }];
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user