Files
landing-app/landing/src/app/ui/call-card.ts
pigui aea8f5675d Make the button-shaped links actual links
"Web oficial" did nothing when clicked, and it was not alone: six places
wrapped `<pi-action-button>` in an `<a>`. That component renders a
`<button>`, so each one was a button inside a link -- invalid HTML, and
the button swallows the click. Besides "Web oficial" it covered Demo,
Repositorio, the contact CTA on every detail page, the 404's way home
and a call card's apply button.

They become one `app-link-button`, which renders a real anchor. The
click was only part of it: a link can also be opened in a new tab,
middle-clicked, copied, tabbed to and followed by a crawler.

Its look does not come from @pi/ui's button classes, and not for want of
trying: those rules are scoped to the element (`html button:not(…)` in
`_pi-button.scss`), so on an anchor they contribute nothing except
`pi-color__primary` -- a *text* colour, which painted the label violet on
a violet fill. The stylesheet reads the same `--pi-theme-button-*`
tokens the library reads instead, so a change of shell or theme still
moves these buttons and dark mode needs nothing of its own.

Verified: the anchor reports href, target and a real navigation to the
external site; no `<button>` inside an `<a>` remains on the page; and
the rendered pill matches the header's button in radius, gradient fill,
white text and weight, in both themes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:07:15 +02:00

157 lines
4.6 KiB
TypeScript

import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core';
import {
PiActionButtonComponent,
PiCardBottomContentDirective,
PiCardCenterContentDirective,
PiCardComponent,
PiCardTopContentDirective,
PiChipComponent,
PiTextColorDirective,
PiTextSmallDirective,
PiTextWeightBoldDirective,
PiTextXSmallDirective,
} from '@pi/ui';
import type { Copy } from '../core/copy';
import type { Locale } from '../core/api';
import type { PublicCall } from '../core/api.types';
import { TechnologyChip } from './technology-chip';
import { LinkButton } from './link-button';
@Component({
selector: 'app-call-card',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
PiCardComponent,
PiCardTopContentDirective,
PiCardCenterContentDirective,
PiCardBottomContentDirective,
PiChipComponent,
PiActionButtonComponent,
PiTextSmallDirective,
PiTextXSmallDirective,
PiTextColorDirective,
PiTextWeightBoldDirective,
TechnologyChip,
LinkButton,
],
template: `
<pi-card
class="landing-card"
[hasShadow]="true"
[isStroked]="true"
gap="m"
padding="l"
innerPadding="none"
alignment="stretch"
context="public"
>
<pi-card-top-content>
<div class="landing-card__chips">
@if (hours(); as label) {
<pi-chip size="small" color="accent" icon="time-detail" [label]="label" />
}
@if (weeks(); as label) {
<pi-chip size="small" appearance="outline" [label]="label" />
}
@if (call().seniority; as seniority) {
<pi-chip size="small" appearance="outline" [label]="copy().call.seniority[seniority]" />
}
</div>
</pi-card-top-content>
<pi-card-center-content>
<h3 bold class="landing-card__title">
@if (call().path; as path) {
<a class="landing-card__title-link" [href]="path">{{ call().title }}</a>
} @else {
{{ call().title }}
}
</h3>
@if (call().project?.title; as project) {
<p xs pi-color color="grey">{{ project }}</p>
}
@if (call().description; as description) {
<p s pi-color color="grey">{{ description }}</p>
}
@if (call().technologies?.length) {
<ul class="landing-card__chips">
@for (technology of call().technologies; track technology.id) {
<li>
<app-technology-chip
[technology]="technology"
[locale]="locale()"
/>
</li>
}
</ul>
}
</pi-card-center-content>
<pi-card-bottom-content>
<div class="landing-card__footer">
@if (closesAt(); as closes) {
<span xs pi-color color="grey"
>{{ copy().labels.closes }} {{ closes }}</span
>
}
<!-- To the call's own page and its form when it has one; to the
landing's general form otherwise. -->
@if (call().path; as path) {
<app-link-button
class="landing-card__button-link"
size="small"
appearance="outline"
[href]="path"
[text]="copy().labels.apply"
/>
} @else {
<pi-action-button
size="small"
color="primary"
appearance="outline"
[hasIcon]="true"
[text]="copy().labels.apply"
(execute)="apply.emit()"
/>
}
</div>
</pi-card-bottom-content>
</pi-card>
`,
})
export class CallCard {
readonly call = input.required<PublicCall>();
readonly copy = input.required<Copy>();
readonly locale = input.required<Locale>();
readonly apply = output<void>();
// `pi-chip-value` renders the number and its unit as adjacent spans with no
// separation, so "4h/semana" comes out as one word. Built here instead.
protected readonly hours = computed(() => {
const hours = this.call().hours_per_week;
return hours ? `${hours} ${this.copy().labels.hoursPerWeek}` : null;
});
protected readonly weeks = computed(() => {
const weeks = this.call().duration_weeks;
return weeks ? `${weeks} ${this.copy().labels.weeks}` : null;
});
protected readonly closesAt = computed(() => {
const value = this.call().closes_at;
if (!value) {
return null;
}
return new Intl.DateTimeFormat(this.locale(), {
day: 'numeric',
month: 'long',
}).format(new Date(value));
});
}