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; + } }