activity-sport-type: schema, contract, write/read paths, federation
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>
This commit is contained in:
parent
ee76945f20
commit
2cb32cd2d3
25 changed files with 328 additions and 18 deletions
39
apps/journal/app/components/SportBadge.tsx
Normal file
39
apps/journal/app/components/SportBadge.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { useTranslation } from "react-i18next";
|
||||
import type { SportType } from "@trails-cool/db/schema/journal";
|
||||
|
||||
// Emoji glyphs as lightweight, dependency-free sport icons. The localized
|
||||
// label carries the meaning; the glyph is decorative (aria-hidden).
|
||||
const SPORT_EMOJI: Record<SportType, string> = {
|
||||
hike: "🥾",
|
||||
walk: "🚶",
|
||||
run: "🏃",
|
||||
ride: "🚴",
|
||||
gravel: "🚲",
|
||||
mtb: "🚵",
|
||||
ski: "⛷️",
|
||||
other: "📍",
|
||||
};
|
||||
|
||||
/**
|
||||
* Small pill (glyph + localized label) shown next to an activity title on the
|
||||
* detail page, feed cards, and the profile list. Renders nothing when the
|
||||
* sport type is unset.
|
||||
*/
|
||||
export function SportBadge({
|
||||
sportType,
|
||||
className,
|
||||
}: {
|
||||
sportType: SportType | null | undefined;
|
||||
className?: string;
|
||||
}) {
|
||||
const { t } = useTranslation("journal");
|
||||
if (!sportType) return null;
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center gap-1 rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-700${className ? ` ${className}` : ""}`}
|
||||
>
|
||||
<span aria-hidden>{SPORT_EMOJI[sportType]}</span>
|
||||
{t(`activities.sport.${sportType}`)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue