- 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)
116 lines
3.1 KiB
Vue
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>
|