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 = { 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(); protected readonly values = signal>({ ...EMPTY_VALUES }); protected readonly touched = signal>>({}); protected readonly contactEmail = SITE_CONFIG.contactEmail; protected readonly incompleteId = 'contact-incomplete'; 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 isComplete = computed(() => this.missingLabels().length === 0); 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 onInput(fieldId: ContactFieldId, event: Event): void { const target = event.target as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement; this.values.update((current) => ({ ...current, [fieldId]: target.value })); this.markTouched(fieldId); } protected onBlur(fieldId: ContactFieldId): void { this.markTouched(fieldId); } protected onDisabledSubmit(event: Event): void { event.preventDefault(); } protected controlId(field: ContactFieldCopy): string { return `contact-${field.id}`; } protected showInvalid(field: ContactFieldCopy): boolean { return ( field.required && this.touched()[field.id] === true && this.values()[field.id].trim().length === 0 ); } protected describedBy(field: ContactFieldCopy): string | null { const ids: string[] = []; if (field.hint) { ids.push(`${this.controlId(field)}-hint`); } if (this.showInvalid(field)) { ids.push(this.incompleteId); } return ids.length > 0 ? ids.join(' ') : null; } private markTouched(fieldId: ContactFieldId): void { this.touched.update((current) => ({ ...current, [fieldId]: true })); } private assembleBody(copy: ContactPageCopy, values: Record): string { 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; } }