peak search, denser catalog, profiles, filename parsing, themed map controls
This commit is contained in:
+63
-3
@@ -72,6 +72,14 @@ db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_ascents_user ON ascents(user_id);
|
||||
`);
|
||||
|
||||
// lightweight migrations for columns added after the initial schema
|
||||
const userCols = (db.prepare('PRAGMA table_info(users)').all() as { name: string }[]).map(
|
||||
(c) => c.name
|
||||
);
|
||||
for (const col of ['bio', 'location', 'avatar']) {
|
||||
if (!userCols.includes(col)) db.exec(`ALTER TABLE users ADD COLUMN ${col} TEXT`);
|
||||
}
|
||||
|
||||
// Seed the catalog with the curated list while no OSM import has run.
|
||||
// Seed rows use osm_id "seed:<name>"; scripts/import-peaks.js upgrades them
|
||||
// to real OSM nodes (preserving ascents) and fills in the rest of the Alps.
|
||||
@@ -181,6 +189,58 @@ export function listAllActivities(): (Omit<ActivityRow, 'points'> & { username:
|
||||
.all() as (Omit<ActivityRow, 'points'> & { username: string })[];
|
||||
}
|
||||
|
||||
export interface ProfileRow {
|
||||
id: number;
|
||||
username: string;
|
||||
created_at: string;
|
||||
bio: string | null;
|
||||
location: string | null;
|
||||
avatar: string | null;
|
||||
}
|
||||
|
||||
export function getProfile(username: string): ProfileRow | undefined {
|
||||
return db
|
||||
.prepare('SELECT id, username, created_at, bio, location, avatar FROM users WHERE username = ?')
|
||||
.get(username) as ProfileRow | undefined;
|
||||
}
|
||||
|
||||
export function updateProfile(
|
||||
userId: number,
|
||||
fields: { bio: string | null; location: string | null; avatar?: string }
|
||||
): void {
|
||||
if (fields.avatar !== undefined) {
|
||||
db.prepare('UPDATE users SET bio = ?, location = ?, avatar = ? WHERE id = ?').run(
|
||||
fields.bio,
|
||||
fields.location,
|
||||
fields.avatar,
|
||||
userId
|
||||
);
|
||||
} else {
|
||||
db.prepare('UPDATE users SET bio = ?, location = ? WHERE id = ?').run(
|
||||
fields.bio,
|
||||
fields.location,
|
||||
userId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function userStats(userId: number): {
|
||||
activities: number;
|
||||
distance_m: number;
|
||||
elev_gain_m: number;
|
||||
moving_s: number;
|
||||
} {
|
||||
return db
|
||||
.prepare(
|
||||
`SELECT count(*) activities,
|
||||
coalesce(sum(distance_m), 0) distance_m,
|
||||
coalesce(sum(elev_gain_m), 0) elev_gain_m,
|
||||
coalesce(sum(coalesce(moving_s, duration_s, 0)), 0) moving_s
|
||||
FROM activities WHERE user_id = ?`
|
||||
)
|
||||
.get(userId) as { activities: number; distance_m: number; elev_gain_m: number; moving_s: number };
|
||||
}
|
||||
|
||||
export function communityTotals(): {
|
||||
users: number;
|
||||
activities: number;
|
||||
@@ -207,17 +267,17 @@ export function peaksInView(
|
||||
userId: number,
|
||||
bbox: { minLat: number; minLon: number; maxLat: number; maxLon: number },
|
||||
zoom: number,
|
||||
limit = 300
|
||||
limit = 1000
|
||||
): (PeakRow & { climbed_at: string | null; ascent_activity_id: number | null })[] {
|
||||
return db
|
||||
.prepare(
|
||||
`SELECT p.*, a.climbed_at, a.activity_id ascent_activity_id
|
||||
FROM peaks p
|
||||
LEFT JOIN ascents a ON a.peak_id = p.id AND a.user_id = @userId
|
||||
WHERE p.minzoom <= @zoom
|
||||
WHERE (p.minzoom <= @zoom OR a.climbed_at IS NOT NULL)
|
||||
AND p.lat BETWEEN @minLat AND @maxLat
|
||||
AND p.lon BETWEEN @minLon AND @maxLon
|
||||
ORDER BY p.score DESC
|
||||
ORDER BY (a.climbed_at IS NOT NULL) DESC, p.score DESC
|
||||
LIMIT @limit`
|
||||
)
|
||||
.all({ userId, zoom: Math.floor(zoom), ...bbox, limit }) as (PeakRow & {
|
||||
|
||||
Reference in New Issue
Block a user