149 lines
4.1 KiB
TypeScript
149 lines
4.1 KiB
TypeScript
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
|
import {
|
|
PiCardBottomContentDirective,
|
|
PiCardCenterContentDirective,
|
|
PiCardComponent,
|
|
PiCardTopContentDirective,
|
|
PiChipComponent,
|
|
PiChipStatusComponent,
|
|
PiTextColorDirective,
|
|
PiTextSmallDirective,
|
|
PiTextViewerLinkComponent,
|
|
PiTextWeightBoldDirective,
|
|
} from '@pi/ui';
|
|
|
|
import type { Copy } from '../core/copy';
|
|
import type { PublicProject } from '../core/api.types';
|
|
import { ApiImage } from './api-image';
|
|
|
|
@Component({
|
|
selector: 'app-project-card',
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
imports: [
|
|
ApiImage,
|
|
PiCardComponent,
|
|
PiCardTopContentDirective,
|
|
PiCardCenterContentDirective,
|
|
PiCardBottomContentDirective,
|
|
PiChipComponent,
|
|
PiChipStatusComponent,
|
|
PiTextViewerLinkComponent,
|
|
PiTextSmallDirective,
|
|
PiTextColorDirective,
|
|
PiTextWeightBoldDirective,
|
|
],
|
|
template: `
|
|
<pi-card
|
|
class="landing-card"
|
|
[hasShadow]="true"
|
|
[isStroked]="true"
|
|
gap="m"
|
|
padding="m"
|
|
innerPadding="none"
|
|
alignment="stretch"
|
|
context="public"
|
|
>
|
|
<pi-card-top-content>
|
|
@if (project().cover_image; as cover) {
|
|
<div class="landing-card__media">
|
|
<app-api-image [image]="cover" />
|
|
</div>
|
|
}
|
|
<div class="landing-card__chips">
|
|
<pi-chip-status
|
|
size="small"
|
|
[status]="statusTone()"
|
|
[label]="copy().projectStatus[project().status]"
|
|
/>
|
|
@if (project().needs_help) {
|
|
<pi-chip
|
|
size="small"
|
|
appearance="outline"
|
|
color="accent"
|
|
[label]="copy().labels.needsHelp"
|
|
/>
|
|
}
|
|
@if (stars() > 0) {
|
|
<pi-chip size="small" icon="star-fill" [label]="stars().toString()" />
|
|
}
|
|
</div>
|
|
</pi-card-top-content>
|
|
|
|
<pi-card-center-content>
|
|
<h3 bold class="landing-card__title">{{ project().title }}</h3>
|
|
@if (project().tagline; as tagline) {
|
|
<p s pi-color color="grey">{{ tagline }}</p>
|
|
}
|
|
@if (technologies().length > 0) {
|
|
<ul class="landing-card__chips">
|
|
@for (technology of technologies(); track technology.id) {
|
|
<li>
|
|
<pi-chip size="small" appearance="outline" [label]="technology.name" />
|
|
</li>
|
|
}
|
|
</ul>
|
|
}
|
|
</pi-card-center-content>
|
|
|
|
<pi-card-bottom-content>
|
|
<div class="landing-card__links">
|
|
@if (project().demo_url; as demo) {
|
|
<pi-text-viewer-link
|
|
icon="play-arrow"
|
|
iconSize="small"
|
|
[value]="copy().labels.demo"
|
|
[link]="demo"
|
|
target="_blank"
|
|
/>
|
|
}
|
|
@if (repositoryUrl(); as repository) {
|
|
<pi-text-viewer-link
|
|
icon="link"
|
|
iconSize="small"
|
|
[value]="copy().labels.repository"
|
|
[link]="repository"
|
|
target="_blank"
|
|
/>
|
|
}
|
|
</div>
|
|
</pi-card-bottom-content>
|
|
</pi-card>
|
|
`,
|
|
})
|
|
export class ProjectCard {
|
|
readonly project = input.required<PublicProject>();
|
|
readonly copy = input.required<Copy>();
|
|
|
|
/** Enough to read the stack at a glance; the rest is noise on a card. */
|
|
protected readonly technologies = computed(() =>
|
|
(this.project().technologies ?? []).slice(0, 5)
|
|
);
|
|
|
|
protected readonly stars = computed(() =>
|
|
(this.project().repositories ?? []).reduce(
|
|
(total, repository) => total + (repository.stars ?? 0),
|
|
0
|
|
)
|
|
);
|
|
|
|
protected readonly repositoryUrl = computed(
|
|
() => (this.project().repositories ?? []).find((repo) => repo.url)?.url ?? null
|
|
);
|
|
|
|
/** The API's five project states onto the chip's five tones. */
|
|
protected readonly statusTone = computed(() => {
|
|
const tones: Record<
|
|
PublicProject['status'],
|
|
'success' | 'error' | 'pending' | 'info' | 'disabled'
|
|
> = {
|
|
idea: 'info',
|
|
active: 'success',
|
|
paused: 'pending',
|
|
completed: 'info',
|
|
archived: 'disabled',
|
|
};
|
|
|
|
return tones[this.project().status];
|
|
});
|
|
}
|