162 lines
4.4 KiB
TypeScript
162 lines
4.4 KiB
TypeScript
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
|
import { RouterLink } from '@angular/router';
|
|
import { PiEmptyStateComponent } from '@pi/ui';
|
|
import { ProjectFacade } from '@task-stream/web/tracker/application';
|
|
import { clock, initials, timeAgo } from '../ui/copy';
|
|
|
|
/** Live feed of everything that happens in the project, as a log. */
|
|
@Component({
|
|
selector: 'ts-activity-page',
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
imports: [RouterLink, PiEmptyStateComponent],
|
|
template: `
|
|
<div class="ts-log-head">
|
|
<p class="ts-eyebrow"><span class="ts-live"></span> En directo</p>
|
|
<span class="ts-muted ts-mono"
|
|
>{{ facade.activity().length }} eventos</span
|
|
>
|
|
</div>
|
|
@if (facade.activity().length) {
|
|
<ol class="ts-log ts-panel" aria-live="polite">
|
|
@for (entry of facade.activity(); track entry.id) {
|
|
<li>
|
|
<time class="ts-log__time" [attr.datetime]="entry.occurredAt">{{
|
|
clock(entry.occurredAt)
|
|
}}</time>
|
|
<span class="ts-log__who" [attr.title]="entry.actor.name">{{
|
|
initialsOf(entry.actor.name)
|
|
}}</span>
|
|
<span class="ts-log__what">{{ entry.summary }}</span>
|
|
@if (entry.issueKey) {
|
|
<a class="ts-key" [routerLink]="['../issues', entry.issueKey]">{{
|
|
entry.issueKey
|
|
}}</a>
|
|
} @else {
|
|
<span></span>
|
|
}
|
|
<span class="ts-log__ago">{{ ago(entry.occurredAt) }}</span>
|
|
</li>
|
|
}
|
|
</ol>
|
|
} @else {
|
|
<pi-empty-state icon="activity" title="Sin actividad todavía" />
|
|
}
|
|
`,
|
|
styles: `
|
|
.ts-log-head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 0.75rem;
|
|
.ts-eyebrow {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
.ts-muted {
|
|
font-size: 0.75rem;
|
|
}
|
|
}
|
|
.ts-live {
|
|
width: 0.5rem;
|
|
height: 0.5rem;
|
|
border-radius: 50%;
|
|
background: var(--ts-s-done);
|
|
animation: ts-pulse 2s ease-out infinite;
|
|
}
|
|
@keyframes ts-pulse {
|
|
0% {
|
|
box-shadow: 0 0 0 0
|
|
color-mix(in srgb, var(--ts-s-done) 60%, transparent);
|
|
}
|
|
70%,
|
|
100% {
|
|
box-shadow: 0 0 0 0.5rem transparent;
|
|
}
|
|
}
|
|
.ts-log {
|
|
margin: 0;
|
|
padding: 0.25rem 0;
|
|
list-style: none;
|
|
li {
|
|
display: grid;
|
|
grid-template-columns: 3.5rem 1.75rem minmax(0, 1fr) auto 8.5rem;
|
|
align-items: center;
|
|
gap: 0.875rem;
|
|
margin: 0;
|
|
padding: 0.625rem 1.125rem;
|
|
border-bottom: 1px dashed var(--ts-line);
|
|
animation: ts-in 0.35s ease-out;
|
|
&:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
&:first-child {
|
|
animation-name: ts-in, ts-flash;
|
|
animation-duration: 0.35s, 1.6s;
|
|
}
|
|
}
|
|
}
|
|
.ts-log__time,
|
|
.ts-log__ago {
|
|
font-family: var(--ts-font-mono);
|
|
font-size: 0.75rem;
|
|
color: var(--ts-muted);
|
|
}
|
|
.ts-log__ago {
|
|
text-align: right;
|
|
white-space: nowrap;
|
|
}
|
|
.ts-log__who {
|
|
display: inline-grid;
|
|
place-items: center;
|
|
width: 1.75rem;
|
|
height: 1.75rem;
|
|
border-radius: 50%;
|
|
background: color-mix(in srgb, var(--ts-brand) 16%, var(--ts-surface));
|
|
color: color-mix(in srgb, var(--ts-brand) 70%, var(--ts-ink));
|
|
font-family: var(--ts-font-mono);
|
|
font-size: 0.625rem;
|
|
font-weight: 600;
|
|
}
|
|
.ts-log__what {
|
|
font-size: 0.875rem;
|
|
color: var(--ts-ink-soft);
|
|
}
|
|
a.ts-key {
|
|
text-decoration: none;
|
|
&:hover {
|
|
color: var(--ts-brand);
|
|
border-color: var(--ts-brand);
|
|
}
|
|
}
|
|
@keyframes ts-in {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(-4px);
|
|
}
|
|
}
|
|
@keyframes ts-flash {
|
|
from {
|
|
background: color-mix(in srgb, var(--ts-brand) 10%, transparent);
|
|
}
|
|
}
|
|
@media (max-width: 640px) {
|
|
.ts-log li {
|
|
grid-template-columns: 1.75rem minmax(0, 1fr) auto;
|
|
gap: 0.625rem;
|
|
padding-inline: 0.875rem;
|
|
}
|
|
.ts-log__time,
|
|
.ts-log__ago {
|
|
display: none;
|
|
}
|
|
}
|
|
`,
|
|
})
|
|
export class ActivityPage {
|
|
protected readonly facade = inject(ProjectFacade);
|
|
protected readonly ago = timeAgo;
|
|
protected readonly initialsOf = initials;
|
|
protected readonly clock = clock;
|
|
}
|