initial version of streba 2
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import { error, redirect } from '@sveltejs/kit';
|
||||
import { db, deleteActivity, getActivity } from '$lib/server/db';
|
||||
import { haversine } from '$lib/server/gpx';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
import type { PeakRow } from '$lib/server/db';
|
||||
|
||||
export const load: PageServerLoad = ({ params }) => {
|
||||
const activity = getActivity(Number(params.id));
|
||||
if (!activity) error(404, 'Activity not found');
|
||||
|
||||
const points = JSON.parse(activity.points) as {
|
||||
lat: number;
|
||||
lon: number;
|
||||
ele: number | null;
|
||||
t: number | null;
|
||||
}[];
|
||||
|
||||
const latlngs = points.map((p) => [p.lat, p.lon] as [number, number]);
|
||||
|
||||
// cumulative-distance elevation profile
|
||||
const profile: { d: number; ele: number }[] = [];
|
||||
let dist = 0;
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
if (i > 0) {
|
||||
dist += haversine(points[i - 1].lat, points[i - 1].lon, points[i].lat, points[i].lon);
|
||||
}
|
||||
if (points[i].ele !== null) profile.push({ d: dist, ele: points[i].ele! });
|
||||
}
|
||||
|
||||
const bagged = db
|
||||
.prepare('SELECT * FROM peaks WHERE activity_id = ?')
|
||||
.all(activity.id) as PeakRow[];
|
||||
|
||||
return {
|
||||
activity: { ...activity, points: undefined },
|
||||
latlngs,
|
||||
profile,
|
||||
bagged: bagged.map((p) => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
elevation_m: p.elevation_m,
|
||||
lat: p.lat,
|
||||
lon: p.lon,
|
||||
climbed: true
|
||||
}))
|
||||
};
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
delete: async ({ params }) => {
|
||||
deleteActivity(Number(params.id));
|
||||
redirect(303, '/activities');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,106 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import Map from '$lib/components/Map.svelte';
|
||||
import ElevationChart from '$lib/components/ElevationChart.svelte';
|
||||
import StatTile from '$lib/components/StatTile.svelte';
|
||||
import { fmtDate, fmtDistance, fmtDuration, fmtElevation, fmtSpeed } from '$lib/format';
|
||||
|
||||
let { data } = $props();
|
||||
const a = $derived(data.activity);
|
||||
|
||||
const avgSpeed = $derived(
|
||||
a.moving_s && a.moving_s > 0 ? fmtSpeed(a.distance_m / a.moving_s) : null
|
||||
);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{a.name} · Streba</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="head">
|
||||
<div>
|
||||
<h1>{a.name}</h1>
|
||||
<p class="page-sub">{fmtDate(a.date)} · <span class="type">{a.type}</span></p>
|
||||
</div>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/delete"
|
||||
use:enhance={({ cancel }) => {
|
||||
if (!confirm('Delete this activity? Peaks it bagged stay climbed.')) cancel();
|
||||
}}
|
||||
>
|
||||
<button class="btn danger" type="submit">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="kpis">
|
||||
<StatTile label="Distance" value={fmtDistance(a.distance_m)} />
|
||||
<StatTile label="Ascent" value={fmtElevation(a.elev_gain_m)} detail="↓ {fmtElevation(a.elev_loss_m)}" />
|
||||
<StatTile
|
||||
label="Moving time"
|
||||
value={fmtDuration(a.moving_s ?? a.duration_s)}
|
||||
detail={a.duration_s ? `total ${fmtDuration(a.duration_s)}` : ''}
|
||||
/>
|
||||
{#if avgSpeed}
|
||||
<StatTile label="Avg moving speed" value={avgSpeed} />
|
||||
{:else}
|
||||
<StatTile label="Highest point" value={fmtElevation(a.elev_max_m)} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if data.bagged.length > 0}
|
||||
<div class="card bagged">
|
||||
⛰ This track bagged
|
||||
{#each data.bagged as peak, i (peak.id)}{i > 0 ? ', ' : ' '}<strong>{peak.name}</strong> ({peak.elevation_m} m){/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<h2>Map</h2>
|
||||
<Map tracks={[{ id: a.id, name: a.name, latlngs: data.latlngs }]} peaks={data.bagged} height="440px" />
|
||||
|
||||
{#if data.profile.length > 1}
|
||||
<h2>Elevation profile</h2>
|
||||
<div class="card chart">
|
||||
<ElevationChart profile={data.profile} />
|
||||
<div class="chart-meta">
|
||||
<span>Low {fmtElevation(a.elev_min_m)}</span>
|
||||
<span>High {fmtElevation(a.elev_max_m)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
.type {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.kpis {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.bagged {
|
||||
margin-top: 1rem;
|
||||
padding: 0.85rem 1.15rem;
|
||||
border-left: 3px solid var(--good);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.bagged strong {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.chart {
|
||||
padding: 1rem 1rem 0.5rem;
|
||||
}
|
||||
.chart-meta {
|
||||
display: flex;
|
||||
gap: 1.25rem;
|
||||
padding: 0.5rem 0.25rem 0.6rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user