racecraft/src/views/TracksView.vue
Jan-Peter v. Hunnius 9e0b46596e feat: persistent sessions support, remove track_map, add characteristics + tracks views
- Remove obsolete track_map use case from types, views, and router
- Delete TrackMapView.vue (superseded by persistent TrackDetailView)
- Auto-analyze in handleBuildTrack before building track model
- Add CharacteristicsView: expandable car cards with aggregated shift-point
  profiles (gear RPM/speed ranges, recommended shift RPM)
- Add TracksView + TrackDetailView + tracks Pinia store (Canvas 2D geometry)
- Add characteristics API client functions (listCarProfiles)
- Add tracks and characteristics nav items and routes
- Update Game type values to match backend snake_case (project_cars_1/2)
- Update use_case types and labels (remove track_map, keep track_mapping)
2026-06-24 09:32:39 +02:00

116 lines
3.1 KiB
Vue

<script setup lang="ts">
import { onMounted } from 'vue'
import { RouterLink } from 'vue-router'
import { useTracksStore } from '@/stores/tracks'
const store = useTracksStore()
onMounted(() => {
store.fetchTracks()
})
function formatTime(iso: string): string {
return new Date(iso).toLocaleString()
}
function gameLabel(game: string): string {
const labels: Record<string, string> = {
assetto_corsa: 'Assetto Corsa',
assetto_corsa_competizione: 'ACC',
project_cars_1: 'Project CARS 1',
project_cars_2: 'Project CARS 2',
}
return labels[game] ?? game
}
async function handleDelete(game: string, trackName: string) {
if (confirm(`Delete track model "${trackName}" (${gameLabel(game)})?`)) {
await store.deleteTrack(game, trackName)
}
}
</script>
<template>
<div class="tracks-view">
<div class="flex items-center justify-between mb-3">
<h2 class="page-title">Tracks</h2>
<RouterLink to="/sessions" class="btn btn-secondary">
+ Map a Track
</RouterLink>
</div>
<div v-if="store.error" class="card mb-3 text-error">{{ store.error }}</div>
<div v-if="store.loading && store.tracks.length === 0" class="card text-secondary">Loading tracks...</div>
<div v-if="store.tracks.length === 0 && !store.loading" class="card text-secondary">
<p class="mb-2">No track models stored yet.</p>
<p style="font-size: 0.85rem; color: var(--color-text-muted)">
Create a Track Mapping session, record left and right boundary laps, then build the track model.
</p>
</div>
<div v-else class="track-cards">
<div v-for="track in store.tracks" :key="`${track.game}-${track.track_name}`" class="card track-card">
<div class="flex items-center justify-between mb-1">
<div class="flex items-center gap-2">
<span class="badge badge-primary">{{ gameLabel(track.game) }}</span>
<span class="track-name">{{ track.track_name }}</span>
</div>
<div class="flex gap-1">
<RouterLink
:to="`/tracks/${track.game}/${track.track_name}`"
class="btn btn-secondary btn-sm"
>
View
</RouterLink>
<button
class="btn btn-danger btn-sm"
@click="handleDelete(track.game, track.track_name)"
>
Delete
</button>
</div>
</div>
<div class="track-meta">
<span class="text-secondary">{{ track.corner_count }} corners</span>
<span class="text-muted">·</span>
<span class="text-muted" style="font-size: 0.8rem">{{ formatTime(track.created_at) }}</span>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.page-title {
font-size: 1.5rem;
margin: 0;
}
.track-cards {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.track-card {
transition: border-color 0.15s ease;
}
.track-card:hover {
border-color: var(--color-primary);
}
.track-name {
font-weight: 600;
font-size: 1rem;
}
.track-meta {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.85rem;
margin-top: 0.25rem;
}
</style>