content: replace scaffolding with bilingual pages and case studies

Ship the final German and English copy, compose every route from shared page primitives, and cover claims, parity and contact briefing in tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-25 17:34:04 +02:00
parent 9ea2885c7b
commit d954361724
95 changed files with 4161 additions and 419 deletions

View File

@@ -0,0 +1,73 @@
import { ChangeDetectionStrategy, Component, computed, input, signal } from '@angular/core';
import {
type ContactFieldCopy,
type ContactFieldId,
type ContactPageCopy,
} from '../../core/content/content.contracts';
import { SITE_CONFIG } from '../../core/content/site-config';
const EMPTY_VALUES: Record<ContactFieldId, string> = {
name: '',
email: '',
company: '',
projectType: '',
situation: '',
timeframe: '',
};
@Component({
selector: 'app-contact-briefing',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './contact-briefing.html',
styleUrl: './contact-briefing.scss',
})
export class ContactBriefing {
readonly copy = input.required<ContactPageCopy>();
protected readonly values = signal<Record<ContactFieldId, string>>({ ...EMPTY_VALUES });
protected readonly contactEmail = SITE_CONFIG.contactEmail;
protected readonly calendarUrl = SITE_CONFIG.calendarUrl;
protected readonly missingLabels = computed(() => {
const copy = this.copy();
const values = this.values();
return copy.fields
.filter((field) => field.required && values[field.id].trim().length === 0)
.map((field) => field.label);
});
protected readonly incompleteMessage = computed(() => {
const missing = this.missingLabels();
if (missing.length === 0) {
return '';
}
return `${this.copy().incompleteHint} ${missing.join(', ')}`;
});
protected readonly mailtoHref = computed(() => {
const copy = this.copy();
const values = this.values();
const body = this.assembleBody(copy, values);
return `mailto:${SITE_CONFIG.contactEmail}?subject=${encodeURIComponent(copy.mailSubject)}&body=${encodeURIComponent(body)}`;
});
protected readonly showCalendar = computed(() => {
const url = SITE_CONFIG.calendarUrl;
return typeof url === 'string' && url.length > 0;
});
protected onInput(fieldId: ContactFieldId, event: Event): void {
const target = event.target as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
this.values.update((current) => ({ ...current, [fieldId]: target.value }));
}
protected controlId(field: ContactFieldCopy): string {
return `contact-${field.id}`;
}
private assembleBody(copy: ContactPageCopy, values: Record<ContactFieldId, string>): string {
const lines = copy.fields.map((field) => `${field.label}: ${values[field.id]}`);
return [copy.mailIntro, '', ...lines, '', copy.mailSignature].join('\n');
}
}