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>
39 lines
1.1 KiB
TypeScript
39 lines
1.1 KiB
TypeScript
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>
|
|
);
|
|
}
|