feat: Laufende Zeiten in der Rangliste und als sticky Leiste, Monospace raus
Die eigene Karte "Laufende Zeiten" entfaellt: Laufende stehen jetzt oben in derselben Tabelle wie die gewerteten, mit "laeuft"-Badge statt Platzziffer. Die Zeile ist Klickziel fuer den Stopp-Dialog, der Knopf daneben macht sie auch per Tastatur bedienbar. Neu ist eine dauerhaft sichtbare Leiste ueber allen Dashboard-Seiten, solange irgendwo eine Zeit laeuft - bei mehreren eingeklappt mit Anzahl und laengster Zeit, aufklappbar. Damit bleibt die Uhr im Blick, waehrend man zwischen zwei Startern woanders unterwegs ist. Der Stopp-Dialog wandert dafuer in eine eigene Komponente, die sich Run-Seite und Leiste teilen, statt die Logik zweimal zu haben. Monospace faellt appweit weg; einzige Ausnahme bleibt die grosse mitlaufende Uhr im Stopp-Dialog. tabular-nums bleibt stehen: gleich breite Ziffern derselben Schrift, ohne die eine laufende Uhr bei jedem Zehntel springt. Tabellenkoepfe hatten den Hover-Hintergrund der Body-Zeilen geerbt; die Regel sitzt jetzt im thead und gilt fuer alle Tabellen der App. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o
This commit is contained in:
parent
245f8d7924
commit
514b5de27b
8 changed files with 289 additions and 126 deletions
127
frontend/src/lib/components/RunningTimesToast.svelte
Normal file
127
frontend/src/lib/components/RunningTimesToast.svelte
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Dauerhaft sichtbare Leiste über allen Dashboard-Seiten, solange
|
||||
* irgendwo eine Zeit läuft. Gedacht für die Strecke: Man legt zwischen
|
||||
* zwei Startern die Trails oder die Fahrerliste an, und die laufende Uhr
|
||||
* bleibt trotzdem im Blick — samt Stopp ohne Umweg über die Run-Seite.
|
||||
*
|
||||
* Bei mehreren Zeiten zeigt die Leiste eingeklappt nur die Zahl und die
|
||||
* längste Zeit; ausgeklappt alle. Bei genau einer entfällt das Klappen.
|
||||
*/
|
||||
import { getTimeContext } from '$lib/stores/times.svelte'
|
||||
import { fullName, getRiderContext } from '$lib/stores/riders.svelte'
|
||||
import { getRunContext } from '$lib/stores/runs.svelte'
|
||||
import { ticker } from '$lib/stores/ticker.svelte'
|
||||
import StopTimeDialog from './StopTimeDialog.svelte'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ChevronDown, ChevronUp, Square } from 'lucide-svelte'
|
||||
|
||||
const times = getTimeContext()
|
||||
const riders = getRiderContext()
|
||||
const runs = getRunContext()
|
||||
const clock = ticker()
|
||||
|
||||
const running = $derived(times.allRunning(clock.now))
|
||||
const single = $derived(running.length === 1)
|
||||
|
||||
let expanded = $state(false)
|
||||
let stopTimeId = $state<string | null>(null)
|
||||
|
||||
// Eine einzelne Zeit steht immer offen da; bei mehreren entscheidet der
|
||||
// Nutzer. Fällt die Anzahl wieder auf eine, ist der Zustand egal.
|
||||
const open = $derived(single || expanded)
|
||||
|
||||
function riderLabel(riderId: string | undefined) {
|
||||
const r = riderId ? riders.getById(riderId) : undefined
|
||||
if (!r) return 'Unbekannter Fahrer'
|
||||
return `#${r.number || '—'} ${fullName(r)}`.trim()
|
||||
}
|
||||
|
||||
function runPath(runId: string | undefined) {
|
||||
const run = runId ? runs.getById(runId) : undefined
|
||||
if (!run?.event) return null
|
||||
return `/dashboard/events/${run.event}/runs/${run.id}`
|
||||
}
|
||||
|
||||
function runLabel(runId: string | undefined) {
|
||||
const run = runId ? runs.getById(runId) : undefined
|
||||
return run?.description || 'Run'
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if running.length > 0}
|
||||
<!-- fixed statt sticky: Die Leiste soll auch beim Scrollen stehen bleiben. -->
|
||||
<div class="fixed bottom-4 right-4 z-50 w-[min(24rem,calc(100vw-2rem))]">
|
||||
<div class="rounded-lg border bg-card text-card-foreground shadow-lg overflow-hidden">
|
||||
<div class="flex items-center gap-2 px-3 py-2 border-b bg-primary/5">
|
||||
<span class="relative flex size-2 shrink-0">
|
||||
<span class="absolute inline-flex h-full w-full rounded-full bg-primary opacity-60 animate-ping"></span>
|
||||
<span class="relative inline-flex size-2 rounded-full bg-primary"></span>
|
||||
</span>
|
||||
|
||||
<span class="text-sm font-medium">
|
||||
{running.length === 1 ? 'Zeit läuft' : `${running.length} Zeiten laufen`}
|
||||
</span>
|
||||
|
||||
{#if !open}
|
||||
<span class="text-sm tabular-nums font-semibold ml-auto">
|
||||
{running[0].formattedTime}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if !single}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="ml-auto size-7 shrink-0"
|
||||
title={expanded ? 'Einklappen' : 'Alle anzeigen'}
|
||||
onclick={() => (expanded = !expanded)}
|
||||
>
|
||||
{#if expanded}
|
||||
<ChevronDown class="size-4" />
|
||||
{:else}
|
||||
<ChevronUp class="size-4" />
|
||||
{/if}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if open}
|
||||
<ul class="divide-y max-h-[50vh] overflow-y-auto">
|
||||
{#each running as t (t.id)}
|
||||
{@const path = runPath(t.run)}
|
||||
<li class="flex items-center gap-2 px-3 py-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-medium truncate">{riderLabel(t.rider)}</p>
|
||||
<p class="text-xs text-muted-foreground truncate">
|
||||
{#if path}
|
||||
<a href={path} class="hover:underline">{runLabel(t.run)}</a>
|
||||
{:else}
|
||||
{runLabel(t.run)}
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<span class="text-lg font-semibold tabular-nums shrink-0">
|
||||
{t.formattedTime}
|
||||
</span>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="shrink-0"
|
||||
title="Zeit stoppen"
|
||||
onclick={() => (stopTimeId = t.id)}
|
||||
>
|
||||
<Square class="size-4 text-destructive" />
|
||||
</Button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StopTimeDialog bind:timeId={stopTimeId} />
|
||||
{/if}
|
||||
84
frontend/src/lib/components/StopTimeDialog.svelte
Normal file
84
frontend/src/lib/components/StopTimeDialog.svelte
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Bestätigung zum Stoppen einer laufenden Zeit. Eigene Komponente, weil
|
||||
* gestoppt wird, wo man gerade ist: auf der Run-Seite und aus der
|
||||
* Toast-Leiste heraus. Beide binden dieselbe `timeId`.
|
||||
*/
|
||||
import { getTimeContext, type TimeStatus } from '$lib/stores/times.svelte'
|
||||
import { fullName, getRiderContext } from '$lib/stores/riders.svelte'
|
||||
import { ticker } from '$lib/stores/ticker.svelte'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import * as Dialog from '@/components/ui/dialog'
|
||||
import { Square } from 'lucide-svelte'
|
||||
|
||||
let { timeId = $bindable(null) }: { timeId?: string | null } = $props()
|
||||
|
||||
const times = getTimeContext()
|
||||
const riders = getRiderContext()
|
||||
const clock = ticker()
|
||||
|
||||
// Aus der laufenden Liste gelesen statt kopiert: So zählt die Anzeige im
|
||||
// Dialog weiter und verschwindet, wenn jemand anders zuerst stoppt.
|
||||
const time = $derived(
|
||||
timeId ? times.allRunning(clock.now).find((t) => t.id === timeId) : undefined,
|
||||
)
|
||||
const rider = $derived(time?.rider ? riders.getById(time.rider) : undefined)
|
||||
|
||||
let error = $state<string | null>(null)
|
||||
|
||||
function close() {
|
||||
timeId = null
|
||||
error = null
|
||||
}
|
||||
|
||||
async function stop(status: 'finished' | 'dnf') {
|
||||
if (!timeId) return
|
||||
|
||||
error = null
|
||||
try {
|
||||
await times.stop(timeId, status as TimeStatus)
|
||||
close()
|
||||
} catch (e: any) {
|
||||
error = e.message ?? 'Die Zeit konnte nicht gestoppt werden.'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root
|
||||
open={timeId !== null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) close()
|
||||
}}
|
||||
>
|
||||
<Dialog.Content>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Zeit stoppen?</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
{#if time}
|
||||
#{rider?.number ?? '—'} {rider ? fullName(rider) : 'Unbekannter Fahrer'}
|
||||
{:else}
|
||||
Diese Zeit läuft nicht mehr.
|
||||
{/if}
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
{#if time}
|
||||
<p class="font-mono text-4xl font-semibold tabular-nums text-center py-4">
|
||||
{time.formattedTime}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
<p class="text-sm text-destructive">{error}</p>
|
||||
{/if}
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button variant="ghost" onclick={close}>Abbrechen</Button>
|
||||
<Button variant="outline" onclick={() => stop('dnf')} disabled={!time}>DNF</Button>
|
||||
<Button variant="destructive" onclick={() => stop('finished')} disabled={!time}>
|
||||
<Square class="h-4 w-4 mr-2" />
|
||||
Stoppen
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
<thead
|
||||
bind:this={ref}
|
||||
data-slot="table-header"
|
||||
class={cn("[&_tr]:border-b", className)}
|
||||
class={cn("[&_tr]:border-b [&_tr]:hover:bg-transparent", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
|
|
|
|||
|
|
@ -166,8 +166,18 @@ export class TimeStore {
|
|||
}
|
||||
|
||||
get running(): TimeWithDuration[] {
|
||||
const now = Date.now()
|
||||
return this.scoped.filter((r) => r.status === 'active').map((r) => enrich(r, now))
|
||||
return this.allRunning()
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle laufenden Zeiten des Teams, run-übergreifend — der am längsten
|
||||
* laufende zuerst. `now` wie bei `getByRun`, damit die Anzeige mitzählt.
|
||||
*/
|
||||
allRunning(now = Date.now()): TimeWithDuration[] {
|
||||
return this.scoped
|
||||
.filter((r) => r.status === 'active')
|
||||
.map((r) => enrich(r, now))
|
||||
.sort((a, b) => (a.start < b.start ? -1 : 1))
|
||||
}
|
||||
|
||||
get finished(): TimeWithDuration[] {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
import { Separator } from '@/components/ui/separator'
|
||||
import Logo from '$lib/components/Logo.svelte'
|
||||
import TeamSwitcher from '$lib/components/TeamSwitcher.svelte'
|
||||
import RunningTimesToast from '$lib/components/RunningTimesToast.svelte'
|
||||
import { Home, Calendar, LogOut, Menu, X, Moon, Sun, Route, Settings } from 'lucide-svelte'
|
||||
import { toggleMode, mode } from 'mode-watcher'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
|
|
@ -182,5 +183,8 @@
|
|||
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{@render children?.()}
|
||||
</main>
|
||||
|
||||
<!-- Laufende Zeiten bleiben auf jeder Dashboard-Seite im Blick. -->
|
||||
<RunningTimesToast />
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@
|
|||
{@const done = times.getByRider(rider.id).filter((t) => t.status === 'finished').length}
|
||||
<Table.Row class="cursor-pointer" onclick={() => goto(riderPath(rider.id))}>
|
||||
<Table.Cell>
|
||||
<Badge variant="outline" class="font-mono">{rider.number || '—'}</Badge>
|
||||
<Badge variant="outline">{rider.number || '—'}</Badge>
|
||||
</Table.Cell>
|
||||
<Table.Cell class="font-medium">
|
||||
<!-- Eigener Link, damit die Zeile auch per Tastatur erreichbar ist. -->
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@
|
|||
{event.name || 'Event'}
|
||||
</a>
|
||||
<div class="flex items-center gap-3">
|
||||
<Badge variant="outline" class="font-mono text-lg px-3 py-1">{rider.number || '—'}</Badge>
|
||||
<Badge variant="outline" class="text-lg px-3 py-1">{rider.number || '—'}</Badge>
|
||||
<h1 class="text-3xl font-bold tracking-tight">
|
||||
{fullName(rider) || 'Ohne Name'}
|
||||
</h1>
|
||||
|
|
@ -118,7 +118,7 @@
|
|||
<CardTitle class="text-sm font-medium text-muted-foreground">Bestzeit</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="text-3xl font-bold font-mono tabular-nums">
|
||||
<div class="text-3xl font-bold tabular-nums">
|
||||
{stats.best != null ? formatDuration(stats.best) : '—'}
|
||||
</div>
|
||||
</CardContent>
|
||||
|
|
@ -128,7 +128,7 @@
|
|||
<CardTitle class="text-sm font-medium text-muted-foreground">Gesamtzeit</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="text-3xl font-bold font-mono tabular-nums">
|
||||
<div class="text-3xl font-bold tabular-nums">
|
||||
{stats.done > 0 ? formatDuration(stats.total) : '—'}
|
||||
</div>
|
||||
</CardContent>
|
||||
|
|
@ -185,7 +185,7 @@
|
|||
<span class="text-muted-foreground text-sm">—</span>
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
<Table.Cell class="text-right font-mono tabular-nums">
|
||||
<Table.Cell class="text-right tabular-nums">
|
||||
{#if row.active}
|
||||
<span class="font-semibold">{row.active.formattedTime}</span>
|
||||
{:else if row.best}
|
||||
|
|
@ -196,7 +196,7 @@
|
|||
</Table.Cell>
|
||||
<Table.Cell class="text-right">
|
||||
{#if row.placement}
|
||||
<Badge variant={row.placement <= 3 ? 'default' : 'secondary'} class="font-mono w-7 justify-center">
|
||||
<Badge variant={row.placement <= 3 ? 'default' : 'secondary'} class="w-7 justify-center">
|
||||
{row.placement}
|
||||
</Badge>
|
||||
{:else}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,10 @@
|
|||
import { getEventContext } from '$lib/stores/events.svelte'
|
||||
import { getRunContext } from '$lib/stores/runs.svelte'
|
||||
import { fullName, getRiderContext } from '$lib/stores/riders.svelte'
|
||||
import { getTimeContext, type TimeStatus } from '$lib/stores/times.svelte'
|
||||
import { getTimeContext } from '$lib/stores/times.svelte'
|
||||
import { ticker } from '$lib/stores/ticker.svelte'
|
||||
import { TIME_STATUS_LABEL } from '$lib/time'
|
||||
import StopTimeDialog from '$lib/components/StopTimeDialog.svelte'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
|
@ -19,7 +20,7 @@
|
|||
import * as Dialog from '@/components/ui/dialog'
|
||||
import * as Table from '@/components/ui/table'
|
||||
import {
|
||||
ArrowLeft, Clock, Download, Flag, Play, Search, Square, Timer, Trophy,
|
||||
ArrowLeft, Download, Flag, Play, Search, Square, Timer, Trophy,
|
||||
} from 'lucide-svelte'
|
||||
|
||||
const events = getEventContext()
|
||||
|
|
@ -41,6 +42,16 @@
|
|||
const leaderboard = $derived(valid && run ? times.getLeaderboard(run.id) : [])
|
||||
const unranked = $derived(valid && run ? times.unrankedByRun(run.id) : [])
|
||||
|
||||
/**
|
||||
* Laufende und gewertete Zeiten in einer Liste: Wer gerade unterwegs ist,
|
||||
* steht oben ohne Platz, darunter die Rangliste. So sieht man beides auf
|
||||
* einen Blick, statt zwischen zwei Karten zu springen.
|
||||
*/
|
||||
const rows = $derived([
|
||||
...running.map((time) => ({ time, place: null as number | null })),
|
||||
...leaderboard.map((time, i) => ({ time, place: i + 1 })),
|
||||
])
|
||||
|
||||
function rider(id: string | undefined) {
|
||||
return id ? riders.getById(id) : undefined
|
||||
}
|
||||
|
|
@ -92,30 +103,12 @@
|
|||
}
|
||||
|
||||
// --- Zeitnahme stoppen -----------------------------------------------
|
||||
let stopDialog = $state(false)
|
||||
// Dialog und Fehlerbehandlung stecken in StopTimeDialog; hier steht nur,
|
||||
// welche Zeit gerade gestoppt werden soll.
|
||||
let stopTimeId = $state<string | null>(null)
|
||||
let stopError = $state<string | null>(null)
|
||||
|
||||
// Aus der laufenden Liste gelesen, damit die Anzeige im Dialog mitzählt.
|
||||
const stopTime = $derived(running.find((t) => t.id === stopTimeId))
|
||||
|
||||
function openStop(timeId: string) {
|
||||
stopTimeId = timeId
|
||||
stopError = null
|
||||
stopDialog = true
|
||||
}
|
||||
|
||||
async function doStop(status: 'finished' | 'dnf') {
|
||||
if (!stopTimeId) return
|
||||
|
||||
stopError = null
|
||||
try {
|
||||
await times.stop(stopTimeId, status as TimeStatus)
|
||||
stopDialog = false
|
||||
stopTimeId = null
|
||||
} catch (e: any) {
|
||||
stopError = e.message ?? 'Die Zeit konnte nicht gestoppt werden.'
|
||||
}
|
||||
}
|
||||
|
||||
// --- Export ------------------------------------------------------------
|
||||
|
|
@ -202,62 +195,18 @@
|
|||
</Card>
|
||||
{/if}
|
||||
|
||||
<!-- Laufende Zeiten -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2 text-base">
|
||||
<Clock class="h-4 w-4 text-primary" />
|
||||
Laufende Zeiten ({running.length})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="p-0">
|
||||
{#if running.length === 0}
|
||||
<p class="px-6 py-8 text-sm text-center text-muted-foreground">
|
||||
Keine laufende Zeit. Über „Zeitnahme starten" geht es los.
|
||||
</p>
|
||||
{:else}
|
||||
<ul class="divide-y">
|
||||
{#each running as t (t.id)}
|
||||
{@const r = rider(t.rider)}
|
||||
<li>
|
||||
<!-- Ganze Zeile als Ziel: an der Strecke wird mit Handschuhen getippt. -->
|
||||
<button
|
||||
type="button"
|
||||
class="w-full px-6 py-4 flex items-center justify-between gap-4 text-left hover:bg-accent/40"
|
||||
onclick={() => openStop(t.id)}
|
||||
>
|
||||
<div class="flex items-center gap-4 min-w-0">
|
||||
<Badge variant="outline" class="font-mono text-base px-3 py-1">
|
||||
{r?.number || '—'}
|
||||
</Badge>
|
||||
<span class="font-medium truncate">{r ? fullName(r) : 'Unbekannter Fahrer'}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 shrink-0">
|
||||
<span class="font-mono text-2xl font-semibold tabular-nums">
|
||||
{t.formattedTime}
|
||||
</span>
|
||||
<Square class="h-4 w-4 text-destructive" />
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Rangliste -->
|
||||
<!-- Zeiten: laufende oben, darunter die Wertung -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2 text-base">
|
||||
<Trophy class="h-4 w-4 text-primary" />
|
||||
Rangliste ({leaderboard.length})
|
||||
Zeiten ({leaderboard.length} gewertet{running.length ? `, ${running.length} laufend` : ''})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="p-0">
|
||||
{#if leaderboard.length === 0}
|
||||
{#if rows.length === 0}
|
||||
<p class="px-6 py-8 text-sm text-center text-muted-foreground">
|
||||
Für diesen Run wurden noch keine Zeiten gewertet.
|
||||
Noch keine Zeit. Über „Zeitnahme starten" geht es los.
|
||||
</p>
|
||||
{:else}
|
||||
<Table.Root>
|
||||
|
|
@ -268,19 +217,29 @@
|
|||
<Table.Head>Name</Table.Head>
|
||||
<Table.Head class="text-right">Zeit</Table.Head>
|
||||
<Table.Head class="text-right w-24">Korrektur</Table.Head>
|
||||
<Table.Head class="w-12"></Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each leaderboard as t, i (t.id)}
|
||||
{#each rows as row (row.time.id)}
|
||||
{@const t = row.time}
|
||||
{@const r = rider(t.rider)}
|
||||
<Table.Row>
|
||||
{@const live = row.place === null}
|
||||
<Table.Row
|
||||
class={live ? 'cursor-pointer bg-primary/5' : undefined}
|
||||
onclick={live ? () => openStop(t.id) : undefined}
|
||||
>
|
||||
<Table.Cell>
|
||||
<Badge variant={i < 3 ? 'default' : 'secondary'} class="font-mono w-7 justify-center">
|
||||
{i + 1}
|
||||
{#if live}
|
||||
<Badge variant="outline" class="text-primary border-primary">läuft</Badge>
|
||||
{:else}
|
||||
<Badge variant={row.place! <= 3 ? 'default' : 'secondary'} class="w-7 justify-center">
|
||||
{row.place}
|
||||
</Badge>
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Badge variant="outline" class="font-mono">{r?.number ?? '—'}</Badge>
|
||||
<Badge variant="outline">{r?.number ?? '—'}</Badge>
|
||||
</Table.Cell>
|
||||
<Table.Cell class="font-medium">
|
||||
{#if r}
|
||||
|
|
@ -289,10 +248,25 @@
|
|||
—
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
<Table.Cell class="text-right font-mono tabular-nums">{t.formattedTime}</Table.Cell>
|
||||
<Table.Cell class="text-right font-mono text-muted-foreground tabular-nums">
|
||||
<Table.Cell class="text-right tabular-nums {live ? 'font-semibold' : ''}">
|
||||
{t.formattedTime}
|
||||
</Table.Cell>
|
||||
<Table.Cell class="text-right text-muted-foreground tabular-nums">
|
||||
{t.correction ? `${t.correction > 0 ? '+' : ''}${t.correction.toFixed(1)}s` : '–'}
|
||||
</Table.Cell>
|
||||
<Table.Cell class="text-right" onclick={(e) => e.stopPropagation()}>
|
||||
{#if live}
|
||||
<!-- Eigener Knopf, damit die Zeile nicht nur mit der Maus stoppbar ist. -->
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
title="Zeit stoppen"
|
||||
onclick={() => openStop(t.id)}
|
||||
>
|
||||
<Square class="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Body>
|
||||
|
|
@ -315,7 +289,7 @@
|
|||
{@const r = rider(t.rider)}
|
||||
<li class="px-6 py-3 flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<Badge variant="outline" class="font-mono">{r?.number ?? '—'}</Badge>
|
||||
<Badge variant="outline">{r?.number ?? '—'}</Badge>
|
||||
<span class="truncate">
|
||||
{#if r}
|
||||
<a href={riderPath(r.id)} class="hover:underline">{fullName(r) || '—'}</a>
|
||||
|
|
@ -369,7 +343,7 @@
|
|||
onclick={() => startFor(c.rider.id)}
|
||||
>
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<Badge variant="outline" class="font-mono">{c.rider.number || '—'}</Badge>
|
||||
<Badge variant="outline">{c.rider.number || '—'}</Badge>
|
||||
<span class="truncate">{fullName(c.rider) || '—'}</span>
|
||||
</div>
|
||||
{#if c.active}
|
||||
|
|
@ -392,40 +366,4 @@
|
|||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
<!-- Stopp-Dialog -->
|
||||
<Dialog.Root bind:open={stopDialog}>
|
||||
<Dialog.Content>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Zeit stoppen?</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
{#if stopTime}
|
||||
{@const r = rider(stopTime.rider)}
|
||||
#{r?.number ?? '—'} {r ? fullName(r) : 'Unbekannter Fahrer'}
|
||||
{:else}
|
||||
Diese Zeit läuft nicht mehr.
|
||||
{/if}
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
{#if stopTime}
|
||||
<p class="font-mono text-4xl font-semibold tabular-nums text-center py-4">
|
||||
{stopTime.formattedTime}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if stopError}
|
||||
<p class="text-sm text-destructive">{stopError}</p>
|
||||
{/if}
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button variant="ghost" onclick={() => (stopDialog = false)}>Abbrechen</Button>
|
||||
<Button variant="outline" onclick={() => doStop('dnf')} disabled={!stopTime}>
|
||||
DNF
|
||||
</Button>
|
||||
<Button variant="destructive" onclick={() => doStop('finished')} disabled={!stopTime}>
|
||||
<Square class="h-4 w-4 mr-2" />
|
||||
Stoppen
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
<StopTimeDialog bind:timeId={stopTimeId} />
|
||||
|
|
|
|||
Loading…
Reference in a new issue