Files
web-pwa/src/lib/components/ui/station-board/StaffServicesGrid.svelte

387 lines
8.1 KiB
Svelte

<script lang="ts">
import type { ApiStationsBoard } from '@owlboard/owlboard-ts';
import { formatUkTime, estClass, delayClassFromTimePair, isRightTime } from '$lib/utils/time';
import { fade, slide, fly } from 'svelte/transition';
import { flip } from 'svelte/animate';
let { services }: { services: ApiStationsBoard.BoardService[] } = $props();
const getRowKey = (s: ApiStationsBoard.BoardService) =>
`${s.r}${s.sta ?? ''}${s.std ?? ''}${s.wtp ?? ''}`;
</script>
<section class="departure-board">
<div class="header container">
<div class="header row">
<div class="upper-header-blank"></div>
<div class="upper-header-text">Arr</div>
<div class="upper-header-text">Dep</div>
</div>
<div class="header row">
<div class="cell">ID</div>
<div class="cell">Orig</div>
<div class="cell">Dest</div>
<div class="cell">Plt</div>
<div class="cell">Sch</div>
<div class="cell">Act</div>
<div class="cell">Sch</div>
<div class="cell">Act</div>
</div>
</div>
<div class="services">
{#each services as service (getRowKey(service))}
{@const realArrival = formatUkTime(service.ata || service.eta)}
{@const realDeparture = formatUkTime(service.atd || service.etd)}
{@const realPass = formatUkTime(service.atp || service.etp)}
<div
class="service-block"
animate:flip={{ duration: 400 }}
transition:slide={{ duration: 300 }}
>
<div
class="service row"
class:passing={service.wtp}
class:nonPax={!service.ip}
class:cancelled={service.c}
>
<div class="cell id">{service.h}</div>
<div class="cell orig">
{#key service.og.t}
<div transition:fade={{ duration: 300 }}>
{service.og.t}
</div>
{/key}
</div>
<div class="cell dest">
{#key service.dt.t}<div transition:fade={{ duration: 300 }}>{service.dt.t}</div>{/key}
</div>
<div class="cell plt" class:platsup={service.ps} class:platchange={service.pc}>
{#key service.p}<div transition:fade={{ duration: 300 }}>{service.p || '-'}</div>{/key}
</div>
{#if service.wtp}
<div class="cell pass">PASS</div>
<div class="cell sch">
{formatUkTime(service.wtp)}
</div>
<div
class="cell act {estClass(service.atp, service.etp)} {delayClassFromTimePair(service.wtp, service.atp || service.etp)}"
>
{#if isRightTime(service.wtp, service.atp || service.etp)}
RT
{:else if service.c}
CANC
{:else}
{realPass}
{/if}
</div>
{:else}
<div class="cell sch">{formatUkTime(service.sta)}</div>
<div
class="cell act {estClass(service.ata, service.eta)} {delayClassFromTimePair(service.sta, service.ata || service.eta)}"
>
{#if isRightTime(service.sta, service.ata || service.eta)}
RT
{:else if service.sta && service.c} CANC {:else}
{realArrival}
{/if}
</div>
<div class="cell sch">{formatUkTime(service.std)}</div>
<div class="cell act {estClass(service.atd, service.etd)} {delayClassFromTimePair(service.std, service.atd || service.etd)}">
{#if isRightTime(service.std, service.atd || service.etd)}
RT
{:else if service.std && service.c} CANC {:else}
{realDeparture}
{/if}
</div>
{/if}
</div>
<div class="operator row">{service.o}</div>
{#if service?.cr?.r}
{#if service.c}
<div class="cancel-reason row" transition:slide={{ duration: 300 }}>{service.cr.r}</div>
{:else}
<div class="cancel-reason row" transition:slide={{ duration: 300 }}>
Part of this service has been cancelled
</div>
{/if}
{:else if service?.dr?.r}
<div class="delay-reason row" transition:slide={{ duration: 300 }}>{service.dr.r}</div>
{/if}
</div>
{/each}
</div>
</section>
<style>
.departure-board {
width: 100%;
max-width: 100%;
height: 100%;
max-height: 100%;
margin: 0 auto;
box-sizing: border-box;
overscroll-behavior-y: contain;
overflow-y: auto;
}
.header.container {
position: sticky;
padding-top: 7px;
top: 0;
z-index: 10;
background: var(--color-bg-dark);
}
.row.header {
font-weight: 875;
}
.row.header:last-child {
padding-bottom: 1rem;
}
.upper-header-blank {
grid-column: span 4;
}
.upper-header-text {
grid-column: span 2;
text-align: center;
}
.row {
display: grid;
grid-template-columns: 10% 17% 17% 9% 12% 12% 12% 11%;
align-items: center;
text-align: center;
gap: 0;
box-sizing: border-box;
font-family: 'Inconsolata Variable', monospace;
font-size: clamp(1rem, 0.475rem + 2.8vw, 1.35rem);
font-variant-ligatures: additional-ligatures;
}
.service.row.passing {
font-style: italic;
opacity: 0.25;
font-weight: 200;
}
.service.row.nonPax {
opacity: 0.25;
font-weight: 200;
}
.service-block {
width: 100%;
max-width: 100%;
padding-bottom: 7px;
}
/* Cancellation Logic */
.service.cancelled,
.service.cancelled .cell.orig,
.service.cancelled .cell.dest {
color: rgb(255, 131, 131);
}
.service.cancelled .cell.plt,
.service.cancelled .cell.act {
text-decoration: line-through;
opacity: 0.25;
}
.service.cancelled .cell.sch {
text-decoration: line-through;
}
/* Meta Row Styles */
.operator.row,
.delay-reason.row,
.cancel-reason.row {
grid-template-columns: 1fr;
text-align: left;
font-size: 0.88rem;
font-stretch: 80%;
}
.cancel-reason.row {
color: rgb(255, 131, 131);
font-weight: 400;
letter-spacing: -0.1ch;
}
.delay-reason.row {
color: var(--delay-orange);
font-weight: 400;
letter-spacing: -0.1ch;
}
@media (min-width: 375px) {
.operator.row,
.delay-reason.row,
.cancel-reason.row {
font-size: 1.08rem;
}
}
@media (min-width: 420px) {
.operator.row,
.delay-reason.row,
.cancel-reason.row {
font-size: 1.12rem;
}
}
@media (min-width: 550px) {
.operator.row,
.delay-reason.row,
.cancel-reason.row {
font-size: 1.18rem;
}
}
@media (min-width: 620px) {
.operator.row,
.delay-reason.row,
.cancel-reason.row {
font-size: 1.2rem;
}
}
.operator.row {
font-stretch: 110%;
padding: 0;
color: rgb(187, 187, 255);
}
/* Column Specifics */
.cell.id {
text-align: left;
font-weight: 400;
font-stretch: 85%;
filter: brightness(0.75);
}
@media (min-width: 400px) {
.cell.id {
font-stretch: 90%;
}
}
.cell.orig,
.cell.dest {
font-stretch: 80%;
font-weight: 400;
}
.cell.orig {
text-align: left;
color: var(--location-yellow);
}
.cell.dest {
text-align: right;
color: var(--location-yellow);
}
@media (min-width: 350px) {
.cell.orig,
.cell.dest {
font-stretch: 85%;
}
@media (min-width: 400px) {
.cell.orig,
.cell.dest {
font-stretch: 90%;
}
}
@media (min-width: 490px) {
.cell.orig,
.cell.dest {
font-stretch: 95%;
}
}
@media (min-width: 520px) {
.cell.orig,
.cell.dest {
font-stretch: 100%;
}
.cell.orig {
text-align: right;
padding-right: 7px;
}
.cell.dest {
text-align: left;
padding-left: 7px;
}
}
@media (min-width: 600px) {
.cell.orig,
.cell.dest {
letter-spacing: 0.05ch;
}
}
}
.cell.plt {
text-align: center;
font-weight: 410;
font-stretch: 100%;
}
@media (min-width: 525px) {
.cell.plt {
font-stretch: 110%;
}
}
.cell.plt.platsup {
font-weight: 150;
}
.cell.plt.platchange {
animation: fast-pulse 2s ease-out infinite;
}
.pass-cell {
text-align: center;
font-stretch: 100%;
}
.cell.pass {
grid-column: span 2;
}
.cell.sch,
.cell.act {
text-align: center;
font-stretch: 72%;
}
.cell.act.delay-late {
color: var(--delay-orange);
}
.cell.act.delay-early {
color: var(--early-blue);
}
@media (min-width: 350px) {
.cell.sch,
.cell.act {
font-stretch: 77%;
}
}
@media (min-width: 400px) {
.cell.sch,
.cell.act {
font-stretch: 82%;
}
}
@media (min-width: 525px) {
.cell.sch,
.cell.act {
font-stretch: 90%;
}
}
@media (min-width: 600px) {
.cell.sch,
.cell.act {
font-stretch: 100%;
}
}
.cell.act.est {
font-style: italic;
font-weight: 300;
opacity: 0.5;
}
/* Time Types */
.cell.act.estimate {
font-style: italic;
opacity: 0.75;
font-weight: 350;
}
.cell.act.actual {
font-weight: 600;
}
</style>