segments with leaderboards, auto-matching, and popular-stretch suggestions

This commit is contained in:
Vincent van der Wal
2026-07-22 16:17:26 +02:00
parent 91775c4099
commit da9f7c6e1a
14 changed files with 930 additions and 4 deletions
+94
View File
@@ -66,6 +66,28 @@ db.exec(`
);
CREATE INDEX IF NOT EXISTS idx_peaks_lat_zoom ON peaks(minzoom, lat, lon);
CREATE TABLE IF NOT EXISTS segments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
creator_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
points TEXT NOT NULL,
bounds TEXT NOT NULL,
distance_m REAL NOT NULL,
elev_gain_m REAL NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS segment_efforts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
segment_id INTEGER NOT NULL REFERENCES segments(id) ON DELETE CASCADE,
activity_id INTEGER NOT NULL REFERENCES activities(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
elapsed_s REAL,
date TEXT,
UNIQUE (segment_id, activity_id)
);
CREATE INDEX IF NOT EXISTS idx_efforts_segment ON segment_efforts(segment_id);
CREATE TABLE IF NOT EXISTS ascents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
@@ -366,6 +388,78 @@ export function recordAscent(
return result.changes > 0;
}
export interface SegmentRow {
id: number;
name: string;
creator_id: number | null;
points: string;
bounds: string;
distance_m: number;
elev_gain_m: number;
created_at: string;
}
export function listSegments(): (Omit<SegmentRow, 'points'> & {
efforts: number;
athletes: number;
best_s: number | null;
})[] {
return db
.prepare(
`SELECT s.id, s.name, s.creator_id, s.bounds, s.distance_m, s.elev_gain_m, s.created_at,
count(e.id) efforts,
count(DISTINCT e.user_id) athletes,
min(e.elapsed_s) best_s
FROM segments s LEFT JOIN segment_efforts e ON e.segment_id = s.id
GROUP BY s.id ORDER BY efforts DESC, s.id DESC`
)
.all() as (Omit<SegmentRow, 'points'> & {
efforts: number;
athletes: number;
best_s: number | null;
})[];
}
export function getSegment(id: number): SegmentRow | undefined {
return db.prepare('SELECT * FROM segments WHERE id = ?').get(id) as SegmentRow | undefined;
}
export function segmentLeaderboard(segmentId: number): {
user_id: number;
username: string;
best_s: number;
attempts: number;
date: string | null;
}[] {
return db
.prepare(
`SELECT e.user_id, u.username, min(e.elapsed_s) best_s, count(*) attempts,
(SELECT date FROM segment_efforts b
WHERE b.segment_id = e.segment_id AND b.user_id = e.user_id
ORDER BY b.elapsed_s ASC LIMIT 1) date
FROM segment_efforts e JOIN users u ON u.id = e.user_id
WHERE e.segment_id = ? AND e.elapsed_s IS NOT NULL
GROUP BY e.user_id ORDER BY best_s ASC`
)
.all(segmentId) as {
user_id: number;
username: string;
best_s: number;
attempts: number;
date: string | null;
}[];
}
export function segmentEffortsForActivity(activityId: number): { segment_id: number; name: string; elapsed_s: number | null }[] {
return db
.prepare(
`SELECT e.segment_id, s.name, e.elapsed_s
FROM segment_efforts e JOIN segments s ON s.id = e.segment_id
WHERE e.activity_id = ? ORDER BY s.name`
)
.all(activityId) as { segment_id: number; name: string; elapsed_s: number | null }[];
}
export function reachedPeaks(activityId: number, userId: number): (PeakRow & { climbed_at: string | null })[] {
return db
.prepare(