Give each service its own page

A service now has its address, like a project: icon, tagline, what the
collective does and how, the projects it was delivered on, what the
people it was for said, and the technologies behind it. The landing's
service cards link to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
pigui
2026-09-29 15:10:19 +02:00
parent 06eeefc144
commit daacc2c5d6
9 changed files with 233 additions and 1 deletions

View File

@@ -0,0 +1,128 @@
import {
ChangeDetectionStrategy,
Component,
computed,
input,
} from '@angular/core';
import {
PiChipComponent,
PiIconComponent,
PiTextColorDirective,
PiTextLargeDirective,
PiTextWeightBoldDirective,
} from '@pi/ui';
import type { Locale } from '../core/api';
import type { PublicService } from '../core/api.types';
import type { Copy } from '../core/copy';
import { resolveIcon } from '../core/icons';
import { ContactCta } from './contact-cta';
import { ProjectCard } from './project-card';
import { TestimonialCard } from './testimonial-card';
/**
* A service on its own address, and the published work that backs it: the
* projects it was delivered on and what the people it was for said.
*
* The description comes from a textarea, so it is printed as text with its
* line breaks kept.
*/
@Component({
selector: 'app-service-detail',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
ContactCta,
PiChipComponent,
PiIconComponent,
PiTextColorDirective,
PiTextLargeDirective,
PiTextWeightBoldDirective,
ProjectCard,
TestimonialCard,
],
styleUrl: './service-detail.scss',
template: `
<header class="content-hero">
<div class="landing-shell">
<pi-icon
class="service-icon"
[icon]="icon()"
size="x-large"
color="primary"
/>
<p class="landing-eyebrow">{{ copy().service.eyebrow }}</p>
<h1 bold class="content-hero__title">{{ service().title }}</h1>
@if (service().tagline; as tagline) {
<p l pi-color color="grey" class="content-hero__lead">
{{ tagline }}
</p>
}
</div>
</header>
<div class="landing-shell content-page__layout">
<article class="content-page__main">
@if (service().description; as description) {
<p class="service-text">{{ description }}</p>
}
@if ((service().projects ?? []).length > 0) {
<section class="content-page__section">
<h2>{{ copy().service.projects }}</h2>
<div class="service-grid">
@for (project of service().projects; track project.id) {
<app-project-card [project]="project" [copy]="copy()" />
}
</div>
</section>
}
@if ((service().testimonials ?? []).length > 0) {
<section class="content-page__section">
<h2>{{ copy().service.testimonials }}</h2>
<div class="service-grid">
@for (
testimonial of service().testimonials;
track testimonial.id
) {
<app-testimonial-card [testimonial]="testimonial" />
}
</div>
</section>
}
</article>
<aside class="content-page__aside">
@if ((service().technologies ?? []).length > 0) {
<div class="landing-card content-page__panel">
<h2>{{ copy().service.technologies }}</h2>
<ul class="content-hero__chips service-technologies">
@for (technology of service().technologies; track technology.id) {
<li>
<pi-chip
size="small"
appearance="outline"
[label]="technology.name"
/>
</li>
}
</ul>
</div>
}
<app-contact-cta [copy]="copy()" [locale]="locale()" />
</aside>
</div>
`,
})
export class ServiceDetail {
readonly service = input.required<PublicService>();
readonly copy = input.required<Copy>();
readonly locale = input.required<Locale>();
/** The panel lets anyone type anything here, emoji included. */
protected readonly icon = computed(() =>
resolveIcon(this.service().icon, 'support'),
);
}