Implements the activity-sport-type change (specs/activity-sport-type): - db: nullable `sport_type` column on journal.activities + SportType / SPORT_TYPES (text().$type<> convention). - api: optional sportType on the activity read + create schemas (mirrored SPORT_TYPES; @trails-cool/api stays zod-only). - write: ActivityInput + createActivity persist it; mapSportType() normalizes provider strings (Komoot bulk import passes tour.sport; Garmin unset); threaded through the unified importActivity. - read/display: sportType added to the detail/feed/profile loaders and the v1 REST endpoints; shared SportBadge (glyph + i18n label) on detail, feed, and profile; sport-aware feed verb; create-form <select>. - i18n: journal.activities.sport.* (labels + verbs) in en + de. - federation: `sport` PropertyValue on the Note when set. Tests: mapSportType unit table; federation asserts the sport attachment is present when set and omitted when unset. typecheck + lint + unit all green. E2E (create→badge) still to add. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
45 lines
1.5 KiB
TypeScript
45 lines
1.5 KiB
TypeScript
// Outbox listing queries (spec 5.1). Offset-paged, public-only —
|
|
// `unlisted` and `private` never federate. Separate from
|
|
// activities.server.ts because the outbox needs raw rows (no geojson
|
|
// batching) in a stable reverse-chronological order keyed on createdAt.
|
|
|
|
import { and, count, desc, eq } from "drizzle-orm";
|
|
import { activities } from "@trails-cool/db/schema/journal";
|
|
import { getDb } from "./db.ts";
|
|
import type { FederatableActivity } from "./federation-objects.server.ts";
|
|
|
|
export const OUTBOX_PAGE_SIZE = 20;
|
|
|
|
export async function listPublicActivitiesPage(
|
|
ownerId: string,
|
|
offset: number,
|
|
limit: number,
|
|
): Promise<FederatableActivity[]> {
|
|
const db = getDb();
|
|
return db
|
|
.select({
|
|
id: activities.id,
|
|
name: activities.name,
|
|
description: activities.description,
|
|
sportType: activities.sportType,
|
|
distance: activities.distance,
|
|
elevationGain: activities.elevationGain,
|
|
duration: activities.duration,
|
|
startedAt: activities.startedAt,
|
|
createdAt: activities.createdAt,
|
|
})
|
|
.from(activities)
|
|
.where(and(eq(activities.ownerId, ownerId), eq(activities.visibility, "public")))
|
|
.orderBy(desc(activities.createdAt))
|
|
.offset(offset)
|
|
.limit(limit);
|
|
}
|
|
|
|
export async function countPublicActivities(ownerId: string): Promise<number> {
|
|
const db = getDb();
|
|
const [row] = await db
|
|
.select({ n: count() })
|
|
.from(activities)
|
|
.where(and(eq(activities.ownerId, ownerId), eq(activities.visibility, "public")));
|
|
return row?.n ?? 0;
|
|
}
|