From cfc7580a8ff4bf242f3e0947071d29858490e8c3 Mon Sep 17 00:00:00 2001 From: Antonio Ledebuhr Date: Tue, 25 Aug 2026 17:46:32 +0200 Subject: [PATCH] content: repair visitor-facing copy and briefing accessibility Public pages no longer describe their own pipeline, case and service labels are real accessible names, and the contact briefing writes localized fields without empty optional lines. Co-authored-by: Cursor --- .../core/content/content-completeness.spec.ts | 1 + src/app/core/content/content-parity.spec.ts | 1 + src/app/core/content/content.contracts.ts | 15 ++++++ src/app/core/content/content.service.ts | 5 ++ src/app/core/content/de/cases.ts | 4 ++ src/app/core/content/de/home.ts | 2 +- src/app/core/content/de/pages.ts | 50 +++++++----------- src/app/core/content/de/services.ts | 12 +++-- src/app/core/content/de/site.ts | 1 + src/app/core/content/en/cases.ts | 4 ++ src/app/core/content/en/home.ts | 2 +- src/app/core/content/en/pages.ts | 51 +++++++------------ src/app/core/content/en/services.ts | 4 ++ src/app/core/content/en/site.ts | 1 + src/app/core/content/shell-copy.ts | 8 --- src/app/features/projects/projects.html | 11 ++-- src/app/features/projects/projects.ts | 20 +------- .../service-page-view/service-page-view.html | 3 +- src/app/shared/case-card/case-card.html | 2 +- src/app/shared/case-study/case-study.html | 4 ++ src/app/shared/case-study/case-study.ts | 1 + .../contact-briefing/contact-briefing.html | 7 ++- .../contact-briefing/contact-briefing.spec.ts | 13 ++++- .../contact-briefing/contact-briefing.ts | 13 ++++- 24 files changed, 125 insertions(+), 110 deletions(-) diff --git a/src/app/core/content/content-completeness.spec.ts b/src/app/core/content/content-completeness.spec.ts index 746f345..b9bd088 100644 --- a/src/app/core/content/content-completeness.spec.ts +++ b/src/app/core/content/content-completeness.spec.ts @@ -40,6 +40,7 @@ describe('content completeness', () => { expect(site.pages.home).toBe(site.home); expect(site.pages.contact).toBe(site.contact); + expect(site.pages.projects).toBe(site.projects); expect(site.pages.stack).toBe(site.stack); expect(site.pages.imprint).toBe(site.legal.imprint); expect(site.pages.privacy).toBe(site.legal.privacy); diff --git a/src/app/core/content/content-parity.spec.ts b/src/app/core/content/content-parity.spec.ts index fe075d6..80bdc7e 100644 --- a/src/app/core/content/content-parity.spec.ts +++ b/src/app/core/content/content-parity.spec.ts @@ -79,6 +79,7 @@ describe('content parity', () => { const english = SITE_CONTENT_DATA.en.cases[caseId]; expect(german.summary, caseId).not.toBe(english.summary); expect(german.situation, caseId).not.toBe(english.situation); + expect(german.linkLabel, caseId).not.toBe(english.linkLabel); } expect(APP_LOCALES).toEqual(['de', 'en']); diff --git a/src/app/core/content/content.contracts.ts b/src/app/core/content/content.contracts.ts index fa9963b..ea632f9 100644 --- a/src/app/core/content/content.contracts.ts +++ b/src/app/core/content/content.contracts.ts @@ -67,6 +67,7 @@ export interface CaseStudyCopy { readonly metrics: readonly MetricCopy[]; readonly stack: readonly string[]; readonly tags: readonly string[]; + readonly linkLabel: string; readonly transparencyNote?: string; } @@ -86,6 +87,7 @@ export interface ServiceSequenceCopy { readonly implementation: SectionCopy; readonly operation: SectionCopy; readonly referenceCaseId: CaseStudyId; + readonly referenceHeadline: string; readonly referenceNote: string; readonly inquiry: SectionCopy; } @@ -162,6 +164,18 @@ export interface StackPageCopy extends PageCopy { readonly groups: readonly StackGroupCopy[]; } +export interface CaseStudyLabels { + readonly situation: string; + readonly approach: string; + readonly outcome: string; + readonly stack: string; + readonly tags: string; +} + +export interface ProjectsPageCopy extends PageCopy { + readonly caseLabels: CaseStudyLabels; +} + export type ServicePageId = 'servicesSoftware' | 'servicesHardwareNetwork' | 'servicesClusters' | 'servicesAi'; @@ -171,6 +185,7 @@ export interface SiteContent { readonly services: Record; readonly cases: Record; readonly contact: ContactPageCopy; + readonly projects: ProjectsPageCopy; readonly stack: StackPageCopy; readonly legal: Record<'imprint' | 'privacy', LegalPageCopy>; } diff --git a/src/app/core/content/content.service.ts b/src/app/core/content/content.service.ts index 3051dfb..f063c53 100644 --- a/src/app/core/content/content.service.ts +++ b/src/app/core/content/content.service.ts @@ -9,6 +9,7 @@ import { type HomePageCopy, type LegalPageCopy, type PageCopy, + type ProjectsPageCopy, type ServicePageCopy, type ServicePageId, type StackPageCopy, @@ -47,6 +48,10 @@ export class ContentService { return computed(() => this.content[this.localeService.locale()].contact); } + projects(): Signal { + return computed(() => this.content[this.localeService.locale()].projects); + } + stack(): Signal { return computed(() => this.content[this.localeService.locale()].stack); } diff --git a/src/app/core/content/de/cases.ts b/src/app/core/content/de/cases.ts index 1e9d036..70bfe2c 100644 --- a/src/app/core/content/de/cases.ts +++ b/src/app/core/content/de/cases.ts @@ -38,6 +38,7 @@ export const CASES_DE: Record<'innofocus' | 'roesterei' | 'myspa' | 'hdi', CaseS 'Azure DevOps', ], tags: ['eCommerce', 'ERP', 'Datenmigration', 'SQL'], + linkLabel: 'Fall lesen: Innofocus / reifen.com', }, roesterei: { id: 'roesterei', @@ -80,6 +81,7 @@ export const CASES_DE: Record<'innofocus' | 'roesterei' | 'myspa' | 'hdi', CaseS 'Microsoft 365', ], tags: ['eCommerce', 'interne IT', 'Shopify', 'KI-Werkzeuge'], + linkLabel: 'Fall lesen: Rösterei Tangermünde', transparencyNote: 'Antonio Ledebuhr hält eine wirtschaftliche Beteiligung an der Rösterei Tangermünde.', }, @@ -123,6 +125,7 @@ export const CASES_DE: Record<'innofocus' | 'roesterei' | 'myspa' | 'hdi', CaseS 'GitLab EE', ], tags: ['IoT', 'Backend', 'DevOps', 'Docker'], + linkLabel: 'Fall lesen: Aracom IT Services / MySpa', }, hdi: { id: 'hdi', @@ -165,5 +168,6 @@ export const CASES_DE: Record<'innofocus' | 'roesterei' | 'myspa' | 'hdi', CaseS 'Azure DevOps', ], tags: ['Versicherung', 'Fullstack', 'Kubernetes', 'Azure'], + linkLabel: 'Fall lesen: HDI Specialty', }, }; diff --git a/src/app/core/content/de/home.ts b/src/app/core/content/de/home.ts index cb0e493..da722e9 100644 --- a/src/app/core/content/de/home.ts +++ b/src/app/core/content/de/home.ts @@ -80,7 +80,7 @@ export const HOME_DE: HomePageCopy = { id: 'profile', headline: 'In dreißig Sekunden', body: [ - 'Antonio Ledebuhr arbeitet als Fullstack- und DevOps-Ingenieur aus Tangermünde. Der öffentliche Stand zeigt ausgewählte Stationen, keinen vollständigen Lebenslauf.', + 'Antonio Ledebuhr arbeitet als Fullstack- und DevOps-Ingenieur aus Tangermünde. Diese Seite zeigt eine kuratierte Auswahl von Stationen; der vollständige Lebenslauf steht als PDF bereit.', ], }, { diff --git a/src/app/core/content/de/pages.ts b/src/app/core/content/de/pages.ts index 1631c52..6b6a863 100644 --- a/src/app/core/content/de/pages.ts +++ b/src/app/core/content/de/pages.ts @@ -3,10 +3,11 @@ import { type ContactPageCopy, type LegalPageCopy, type PageCopy, + type ProjectsPageCopy, type StackPageCopy, } from '../content.contracts'; -export const PROJECTS_DE: PageCopy = { +export const PROJECTS_DE: ProjectsPageCopy = { routeId: 'projects', title: 'Projekte | Antonio Ledebuhr', description: @@ -26,36 +27,23 @@ export const PROJECTS_DE: PageCopy = { }, { id: 'reading', - headline: 'Wie die Fälle gelesen werden sollen', + headline: 'Was jeder Fall festhält', body: [ - 'Jeder Fall nennt Auftraggeber, Rolle, Zeitraum, Lage, Vorgehen, Ergebnis, Technik und Schlagworte. Zahlen stehen nur dort, wo sie belegt sind.', + 'Jeder Fall dokumentiert Auftrag, Rolle, Zeitraum, Lage, Vorgehen und Ergebnis. Zahlen stehen nur dort, wo sie belegt sind.', ], }, - { - id: 'situation', - headline: 'Lage', - body: ['Die Lage beschreibt den Ausgangspunkt des jeweiligen Auftrags.'], - }, - { - id: 'approach', - headline: 'Vorgehen', - body: ['Das Vorgehen nennt die konkreten Schritte der Arbeit.'], - }, - { - id: 'outcome', - headline: 'Ergebnis', - body: ['Das Ergebnis nennt nur Folgen, die belegt sind.'], - }, - { - id: 'stack', - headline: 'Technik', - body: ['Die Technikliste folgt dem jeweiligen Auftrag.'], - }, ], ctas: [ { label: 'Lebenslauf als PDF', href: SITE_CONFIG.cvAssetPath }, { label: 'Kontakt', routeId: 'contact' }, ], + caseLabels: { + situation: 'Lage', + approach: 'Vorgehen', + outcome: 'Ergebnis', + stack: 'Technik', + tags: 'Schlagworte', + }, }; export const STACK_DE: StackPageCopy = { @@ -66,21 +54,19 @@ export const STACK_DE: StackPageCopy = { hero: { headline: 'Der Stack, der auf diesen Seiten vorkommt', proof: - 'Die Gruppen folgen der öffentlichen Rasterdarstellung. Neue Namen werden hier nicht ergänzt.', + 'Die Gruppen gehören zur Produktarbeit in Java und Spring sowie in C# und .NET, zur Datenarbeit in SQL Server, zu Containern und Clustern und zu Servern vor Ort.', }, sections: [ { id: 'reading', - headline: 'Wie das Raster zu lesen ist', - body: [ - 'Die Kacheln zeigen die Technologien, die auf dieser Seite öffentlich geführt werden. Sie sind keine Rangliste und keine Vollständigkeitserklärung.', - ], + headline: 'Eine Auswahl, keine Rangliste', + body: ['Die Übersicht ist eine Auswahl aus der öffentlichen Arbeit, keine Rangliste.'], }, { id: 'groups', - headline: 'Die sieben Gruppen', + headline: 'Wo die Gruppen in der Arbeit vorkommen', body: [ - 'Programmierung, Datenbanken, DevOps, Betriebssysteme, Infrastructure as Code, Hypervisor und Werkzeuge. Die Titel der Gruppen sind lokalisiert, die Einträge bleiben unverändert.', + 'Produktarbeit in Java und Spring sowie in C# und .NET, mit Angular auf der Oberfläche. Datenarbeit in SQL Server und T-SQL. Container und Cluster mit Docker, Kubernetes und Azure AKS. Pipelines in GitLab CI/CD und Azure DevOps, GitOps mit Argo CD. Server und Virtualisierung vor Ort.', ], }, ], @@ -135,9 +121,9 @@ export const ABOUT_DE: PageCopy = { }, { id: 'accent', - headline: 'Ein Satz zur Rösterei', + headline: 'Rösterei und Drehmoment', body: [ - 'Die wirtschaftliche Beteiligung an der Rösterei Tangermünde steht im Lebenslauf. Mehr Biografie gehört nicht auf diese Seite.', + 'Die Rösterei-Arbeit bleibt der persönliche Akzent: Rösten und Drehmoment statt Leerlauf. Antonio Ledebuhr hält eine wirtschaftliche Beteiligung an der Rösterei Tangermünde.', ], }, ], diff --git a/src/app/core/content/de/services.ts b/src/app/core/content/de/services.ts index 04945e2..2a2e1aa 100644 --- a/src/app/core/content/de/services.ts +++ b/src/app/core/content/de/services.ts @@ -114,6 +114,7 @@ export const SERVICES_SOFTWARE_DE: ServicePageCopy = { ], }, referenceCaseId: 'innofocus', + referenceHeadline: 'Passender öffentlicher Fall', referenceNote: 'Passender öffentlicher Fall: die ERP-Datenmigration für Innofocus / reifen.com, mit T-SQL, .NET, Angular und Keycloak.', inquiry: { @@ -195,6 +196,7 @@ export const SERVICES_HARDWARE_DE: ServicePageCopy = { ], }, referenceCaseId: 'roesterei', + referenceHeadline: 'Passender öffentlicher Fall', referenceNote: 'Passender öffentlicher Fall: die Gesamtverantwortung für die Technik der Rösterei Tangermünde, vom Netz bis zum Shop.', inquiry: { @@ -275,6 +277,7 @@ export const SERVICES_CLUSTERS_DE: ServicePageCopy = { ], }, referenceCaseId: 'hdi', + referenceHeadline: 'Passender öffentlicher Fall', referenceNote: 'Passender öffentlicher Fall: die Migration der Umgebungen von HDI Specialty von Azure App Services auf Azure AKS.', inquiry: { @@ -310,7 +313,7 @@ export const SERVICES_AI_DE: ServicePageCopy = { id: 'offer-boundary', headline: 'Was Angebot bleibt', body: [ - 'Prozessanalyse, externe Modelle und APIs, lokale Modelle, Abruf angereicherter Texte mit Datenschutzfokus, Mensch in der Schleife und Mehr-Agenten-Abläufe sind Angebote. Sie werden je Auftrag geschnitten und geprüft, nicht als allgemeine Kundenerfahrung dargestellt.', + 'Prozessanalyse, externe Modelle und APIs, lokale Modelle, RAG (Abruf angereicherter Texte) mit Datenschutzfokus, Mensch in der Schleife und Mehr-Agenten-Abläufe sind Angebote. Sie werden je Auftrag geschnitten und geprüft, nicht als allgemeine Kundenerfahrung dargestellt.', ], }, { @@ -355,6 +358,7 @@ export const SERVICES_AI_DE: ServicePageCopy = { ], }, referenceCaseId: 'roesterei', + referenceHeadline: 'Passender öffentlicher Fall', referenceNote: 'Passender öffentlicher Fall: die internen KI-Werkzeuge der Rösterei Tangermünde für Bewertungen und Shop-Verwaltung.', inquiry: { @@ -389,7 +393,7 @@ export const SERVICES_AI_DE: ServicePageCopy = { { id: 'external-models', title: 'Externe Modelle und APIs', - body: 'Anbindung eines externen Modells oder einer API an einen bestehenden Ablauf. Ob das zum Auftrag passt, wird je Mandantin und Mandant geprüft und begrenzt.', + body: 'Anbindung eines externen Modells oder einer API an einen bestehenden Ablauf. Ob das zum Auftrag passt, wird je Auftrag geprüft und begrenzt.', status: 'offer', }, { @@ -400,8 +404,8 @@ export const SERVICES_AI_DE: ServicePageCopy = { }, { id: 'rag-privacy', - title: 'Textabruf mit Datenschutzgrenze', - body: 'Wenn interne Texte ein Modell stützen sollen, wird der Abruf samt Datenschutzgrenze am konkreten Bestand entworfen. Der Zuschnitt gilt nur für diesen Auftrag und wird mit der Fachseite geprüft.', + title: 'RAG mit Datenschutzgrenze', + body: 'Wenn interne Texte ein Modell stützen sollen, wird RAG — der Abruf angereicherter Texte — samt Datenschutzgrenze am konkreten Bestand entworfen. Der Zuschnitt gilt nur für diesen Auftrag und wird mit der Fachseite geprüft.', status: 'offer', }, { diff --git a/src/app/core/content/de/site.ts b/src/app/core/content/de/site.ts index 92f907b..dd298db 100644 --- a/src/app/core/content/de/site.ts +++ b/src/app/core/content/de/site.ts @@ -28,6 +28,7 @@ export const SITE_CONTENT_DE: SiteContent = { }, cases: CASES_DE, contact: CONTACT_DE, + projects: PROJECTS_DE, stack: STACK_DE, legal: { imprint: IMPRINT_DE, diff --git a/src/app/core/content/en/cases.ts b/src/app/core/content/en/cases.ts index 43c6a5c..f4338c8 100644 --- a/src/app/core/content/en/cases.ts +++ b/src/app/core/content/en/cases.ts @@ -38,6 +38,7 @@ export const CASES_EN: Record<'innofocus' | 'roesterei' | 'myspa' | 'hdi', CaseS 'Azure DevOps', ], tags: ['eCommerce', 'ERP', 'data migration', 'SQL'], + linkLabel: 'Read the case: Innofocus / reifen.com', }, roesterei: { id: 'roesterei', @@ -80,6 +81,7 @@ export const CASES_EN: Record<'innofocus' | 'roesterei' | 'myspa' | 'hdi', CaseS 'Microsoft 365', ], tags: ['eCommerce', 'internal IT', 'Shopify', 'AI tools'], + linkLabel: 'Read the case: Rösterei Tangermünde', transparencyNote: 'Antonio Ledebuhr holds an economic stake in Rösterei Tangermünde.', }, myspa: { @@ -122,6 +124,7 @@ export const CASES_EN: Record<'innofocus' | 'roesterei' | 'myspa' | 'hdi', CaseS 'GitLab EE', ], tags: ['IoT', 'backend', 'DevOps', 'Docker'], + linkLabel: 'Read the case: Aracom IT Services / MySpa', }, hdi: { id: 'hdi', @@ -164,5 +167,6 @@ export const CASES_EN: Record<'innofocus' | 'roesterei' | 'myspa' | 'hdi', CaseS 'Azure DevOps', ], tags: ['insurance', 'fullstack', 'Kubernetes', 'Azure'], + linkLabel: 'Read the case: HDI Specialty', }, }; diff --git a/src/app/core/content/en/home.ts b/src/app/core/content/en/home.ts index 6ea154b..63f8502 100644 --- a/src/app/core/content/en/home.ts +++ b/src/app/core/content/en/home.ts @@ -80,7 +80,7 @@ export const HOME_EN: HomePageCopy = { id: 'profile', headline: 'Thirty seconds', body: [ - 'Antonio Ledebuhr works as a fullstack and DevOps engineer from Tangermünde. The public site shows selected stations, not the full curriculum vitae.', + 'Antonio Ledebuhr works as a fullstack and DevOps engineer from Tangermünde. This site shows a curated selection of stations; the full curriculum vitae is available as a PDF.', ], }, { diff --git a/src/app/core/content/en/pages.ts b/src/app/core/content/en/pages.ts index 17649ce..13f02c8 100644 --- a/src/app/core/content/en/pages.ts +++ b/src/app/core/content/en/pages.ts @@ -3,10 +3,11 @@ import { type ContactPageCopy, type LegalPageCopy, type PageCopy, + type ProjectsPageCopy, type StackPageCopy, } from '../content.contracts'; -export const PROJECTS_EN: PageCopy = { +export const PROJECTS_EN: ProjectsPageCopy = { routeId: 'projects', title: 'Projects | Antonio Ledebuhr', description: @@ -26,36 +27,23 @@ export const PROJECTS_EN: PageCopy = { }, { id: 'reading', - headline: 'How to read the cases', + headline: 'What each case documents', body: [ - 'Each case names the client, role, period, situation, approach, outcome, stack and tags. Figures appear only where they are verified.', + 'Each case documents the engagement, the role, the period, the situation, the approach and the outcome. Figures appear only where they are verified.', ], }, - { - id: 'situation', - headline: 'Situation', - body: ['The situation describes the starting point of the engagement.'], - }, - { - id: 'approach', - headline: 'Approach', - body: ['The approach lists the concrete steps of the work.'], - }, - { - id: 'outcome', - headline: 'Outcome', - body: ['The outcome names only results that are verified.'], - }, - { - id: 'stack', - headline: 'Stack', - body: ['The stack list follows the individual engagement.'], - }, ], ctas: [ { label: 'Curriculum vitae as PDF', href: SITE_CONFIG.cvAssetPath }, { label: 'Contact', routeId: 'contact' }, ], + caseLabels: { + situation: 'Situation', + approach: 'Approach', + outcome: 'Outcome', + stack: 'Stack', + tags: 'Tags', + }, }; export const STACK_EN: StackPageCopy = { @@ -65,21 +53,20 @@ export const STACK_EN: StackPageCopy = { 'The public stack in seven groups: programming, databases, DevOps, operating systems, infrastructure as code, hypervisors and tools.', hero: { headline: 'The stack that appears on these pages', - proof: 'The groups follow the public grid. No extra names are added here.', + proof: + 'The groups belong to product work in Java and Spring and in C# and .NET, to SQL Server data work, to containers and clusters, and to servers on site.', }, sections: [ { id: 'reading', - headline: 'How to read the grid', - body: [ - 'The tiles show the technologies listed in public on this site. They are not a ranking and not a claim of completeness.', - ], + headline: 'A selection, not a ranking', + body: ['The overview is a selection from the public work, not a ranking.'], }, { id: 'groups', - headline: 'The seven groups', + headline: 'Where the groups show up in the work', body: [ - 'Programming, databases, DevOps, operating systems, infrastructure as code, hypervisors and tools. Group titles are localised; the entries stay as they are.', + 'Product work in Java and Spring and in C# and .NET, with Angular on the front end. SQL Server and T-SQL data work. Containers and clusters with Docker, Kubernetes and Azure AKS. Pipelines in GitLab CI/CD and Azure DevOps, GitOps with Argo CD. Servers and virtualisation on site.', ], }, ], @@ -134,9 +121,9 @@ export const ABOUT_EN: PageCopy = { }, { id: 'accent', - headline: 'One line on the roastery', + headline: 'Roastery and torque', body: [ - 'The economic stake in Rösterei Tangermünde is on the curriculum vitae. Further biography does not belong on this page.', + 'The roastery work remains the personal accent: brewing and torque rather than idle. Antonio Ledebuhr holds an economic stake in Rösterei Tangermünde.', ], }, ], diff --git a/src/app/core/content/en/services.ts b/src/app/core/content/en/services.ts index aecdf6b..f8fe11a 100644 --- a/src/app/core/content/en/services.ts +++ b/src/app/core/content/en/services.ts @@ -114,6 +114,7 @@ export const SERVICES_SOFTWARE_EN: ServicePageCopy = { ], }, referenceCaseId: 'innofocus', + referenceHeadline: 'A fitting public case', referenceNote: 'Fitting public case: the ERP data migration for Innofocus / reifen.com, with T-SQL, .NET, Angular and Keycloak.', inquiry: { @@ -195,6 +196,7 @@ export const SERVICES_HARDWARE_EN: ServicePageCopy = { ], }, referenceCaseId: 'roesterei', + referenceHeadline: 'A fitting public case', referenceNote: 'Fitting public case: full technical responsibility for Rösterei Tangermünde, from the network to the shop.', inquiry: { @@ -275,6 +277,7 @@ export const SERVICES_CLUSTERS_EN: ServicePageCopy = { ], }, referenceCaseId: 'hdi', + referenceHeadline: 'A fitting public case', referenceNote: 'Fitting public case: the migration of the HDI Specialty environments from Azure App Services to Azure AKS.', inquiry: { @@ -355,6 +358,7 @@ export const SERVICES_AI_EN: ServicePageCopy = { ], }, referenceCaseId: 'roesterei', + referenceHeadline: 'A fitting public case', referenceNote: 'Fitting public case: the in-house AI tools at Rösterei Tangermünde for reviews and shop management.', inquiry: { diff --git a/src/app/core/content/en/site.ts b/src/app/core/content/en/site.ts index ccf520c..f5aa488 100644 --- a/src/app/core/content/en/site.ts +++ b/src/app/core/content/en/site.ts @@ -28,6 +28,7 @@ export const SITE_CONTENT_EN: SiteContent = { }, cases: CASES_EN, contact: CONTACT_EN, + projects: PROJECTS_EN, stack: STACK_EN, legal: { imprint: IMPRINT_EN, diff --git a/src/app/core/content/shell-copy.ts b/src/app/core/content/shell-copy.ts index 014345f..3bfd4bc 100644 --- a/src/app/core/content/shell-copy.ts +++ b/src/app/core/content/shell-copy.ts @@ -10,8 +10,6 @@ export interface ShellCopy { readonly otherLocaleName: string; readonly cvLabel: string; readonly contactCta: string; - readonly legalReviewNotice: string; - readonly notFoundHomeLabel: string; } export const SHELL_COPY: Record = { @@ -25,9 +23,6 @@ export const SHELL_COPY: Record = { otherLocaleName: 'English', cvLabel: 'Lebenslauf als PDF', contactCta: 'Kontakt', - legalReviewNotice: - 'Dieser Rechtstext ist ungeprüft und muss vor der Veröffentlichung vom Seitenbetreiber geprüft werden.', - notFoundHomeLabel: 'Zur Startseite', }, en: { skipLink: 'Skip to content', @@ -39,8 +34,5 @@ export const SHELL_COPY: Record = { otherLocaleName: 'Deutsch', cvLabel: 'Curriculum vitae as PDF', contactCta: 'Contact', - legalReviewNotice: - 'This legal text is unreviewed and must be checked by the site owner before publication.', - notFoundHomeLabel: 'Back to home', }, }; diff --git a/src/app/features/projects/projects.html b/src/app/features/projects/projects.html index 137f2cd..04dfd43 100644 --- a/src/app/features/projects/projects.html +++ b/src/app/features/projects/projects.html @@ -1,16 +1,17 @@ @if (page(); as copy) {
- @for (section of introSections(); track section.id) { + @for (section of copy.sections; track section.id) { } @for (caseStudy of cases(); track caseStudy.id) { }
diff --git a/src/app/features/projects/projects.ts b/src/app/features/projects/projects.ts index df499ff..9994c62 100644 --- a/src/app/features/projects/projects.ts +++ b/src/app/features/projects/projects.ts @@ -1,11 +1,9 @@ -import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { ContentService } from '../../core/content/content.service'; import { CaseStudy } from '../../shared/case-study/case-study'; import { ContentSection } from '../../shared/content-section/content-section'; import { PageHero } from '../../shared/page-hero/page-hero'; -const INTRO_SECTION_IDS = new Set(['selection', 'reading']); - @Component({ selector: 'app-projects-page', changeDetection: ChangeDetectionStrategy.OnPush, @@ -16,20 +14,6 @@ const INTRO_SECTION_IDS = new Set(['selection', 'reading']); export class ProjectsPage { private readonly content = inject(ContentService); - protected readonly page = this.content.page('projects'); + protected readonly page = this.content.projects(); protected readonly cases = this.content.cases(); - protected readonly introSections = computed(() => - this.page().sections.filter((section) => INTRO_SECTION_IDS.has(section.id)), - ); - protected readonly caseLabels = computed(() => { - const headlines = new Map( - this.page().sections.map((section) => [section.id, section.headline] as const), - ); - return { - situation: headlines.get('situation') ?? '', - approach: headlines.get('approach') ?? '', - outcome: headlines.get('outcome') ?? '', - stack: headlines.get('stack') ?? '', - }; - }); } diff --git a/src/app/features/services/service-page-view/service-page-view.html b/src/app/features/services/service-page-view/service-page-view.html index b20c2c5..bf39939 100644 --- a/src/app/features/services/service-page-view/service-page-view.html +++ b/src/app/features/services/service-page-view/service-page-view.html @@ -18,7 +18,8 @@ } }
-

{{ copy.sequence.referenceNote }}

+

{{ copy.sequence.referenceHeadline }}

+

{{ copy.sequence.referenceNote }}

diff --git a/src/app/shared/case-card/case-card.html b/src/app/shared/case-card/case-card.html index 06a4d1c..37a2da3 100644 --- a/src/app/shared/case-card/case-card.html +++ b/src/app/shared/case-card/case-card.html @@ -10,5 +10,5 @@
  • {{ tag }}
  • } - {{ caseStudy().client }} + {{ caseStudy().linkLabel }} diff --git a/src/app/shared/case-study/case-study.html b/src/app/shared/case-study/case-study.html index cd3a9bf..e233e86 100644 --- a/src/app/shared/case-study/case-study.html +++ b/src/app/shared/case-study/case-study.html @@ -46,6 +46,10 @@
  • {{ item }}
  • } + + +
    +

    {{ tagsLabel() }}

      @for (tag of caseStudy().tags; track $index) {
    • {{ tag }}
    • diff --git a/src/app/shared/case-study/case-study.ts b/src/app/shared/case-study/case-study.ts index 9014a06..84d74ce 100644 --- a/src/app/shared/case-study/case-study.ts +++ b/src/app/shared/case-study/case-study.ts @@ -15,6 +15,7 @@ export class CaseStudy { readonly approachLabel = input.required(); readonly outcomeLabel = input.required(); readonly stackLabel = input.required(); + readonly tagsLabel = input.required(); protected readonly headingId = computed(() => `case-heading-${this.caseStudy().id}`); protected readonly metricsLabelId = computed(() => `case-metrics-${this.caseStudy().id}`); diff --git a/src/app/shared/contact-briefing/contact-briefing.html b/src/app/shared/contact-briefing/contact-briefing.html index c267319..a3b962b 100644 --- a/src/app/shared/contact-briefing/contact-briefing.html +++ b/src/app/shared/contact-briefing/contact-briefing.html @@ -4,7 +4,8 @@ @if (field.hint) { @@ -52,9 +53,7 @@ } - @if (incompleteMessage(); as message) { -

      {{ message }}

      - } +

      {{ incompleteMessage() }}

      {{ copy().noBackendNote }}

      diff --git a/src/app/shared/contact-briefing/contact-briefing.spec.ts b/src/app/shared/contact-briefing/contact-briefing.spec.ts index 99e6694..a7f1075 100644 --- a/src/app/shared/contact-briefing/contact-briefing.spec.ts +++ b/src/app/shared/contact-briefing/contact-briefing.spec.ts @@ -38,19 +38,20 @@ describe('ContactBriefing', () => { expect(root.querySelector('form[action]')).toBeNull(); expect(root.textContent).toContain(copy.incompleteHint); + expect(root.querySelector('[aria-live="polite"]')).toBeTruthy(); expect(root.querySelector(`a[href="${SITE_CONFIG.calendarUrl}"]`)).toBeNull(); expect(root.textContent).not.toContain(copy.calendarLabel); const special = 'Äpfel & Birnen?\nPreis=100'; setControl(root, 'contact-name', special); setControl(root, 'contact-email', 'team@example.com'); - setControl(root, 'contact-company', 'Rösterei & Co'); setControl(root, 'contact-projectType', 'software'); setControl(root, 'contact-situation', special); setControl(root, 'contact-timeframe', 'Q3'); fixture.detectChanges(); expect(root.textContent).not.toContain(copy.incompleteHint); + expect(root.querySelector('[aria-live="polite"]')).toBeTruthy(); const submit = root.querySelector('a.submit'); expect(submit?.getAttribute('href')?.startsWith('mailto:info@antoniolede.de?subject=')).toBe( @@ -70,7 +71,15 @@ describe('ContactBriefing', () => { const decodedBody = decodeURIComponent(bodyPart); expect(decodedBody).toContain(special); expect(decodedBody).toContain('team@example.com'); - expect(decodedBody).toContain('Rösterei & Co'); + const projectType = copy.fields.find((field) => field.id === 'projectType'); + const projectTypeLabel = projectType?.options?.find( + (option) => option.value === 'software', + )?.label; + expect(projectTypeLabel).toBeTruthy(); + expect(decodedBody).toContain(`${projectType!.label}: ${projectTypeLabel}`); + const company = copy.fields.find((field) => field.id === 'company'); + expect(company).toBeTruthy(); + expect(decodedBody).not.toContain(`${company!.label}:`); expect(decodeURIComponent(subjectPart.slice('subject='.length))).toBe(copy.mailSubject); expect(fetchSpy).not.toHaveBeenCalled(); diff --git a/src/app/shared/contact-briefing/contact-briefing.ts b/src/app/shared/contact-briefing/contact-briefing.ts index c8e2cf8..dac1b65 100644 --- a/src/app/shared/contact-briefing/contact-briefing.ts +++ b/src/app/shared/contact-briefing/contact-briefing.ts @@ -67,7 +67,18 @@ export class ContactBriefing { } private assembleBody(copy: ContactPageCopy, values: Record): string { - const lines = copy.fields.map((field) => `${field.label}: ${values[field.id]}`); + const lines = copy.fields.flatMap((field) => { + const raw = values[field.id]; + if (!field.required && raw.trim().length === 0) { + return []; + } + + return [`${field.label}: ${this.displayValue(field, raw)}`]; + }); return [copy.mailIntro, '', ...lines, '', copy.mailSignature].join('\n'); } + + private displayValue(field: ContactFieldCopy, value: string): string { + return field.options?.find((option) => option.value === value)?.label ?? value; + } }