"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>
157 lines
4.6 KiB
TypeScript
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));
|
|
});
|
|
}
|