From 81f446d4e5ae23f45872ff4aefab11b726767185 Mon Sep 17 00:00:00 2001 From: Ian Jones Date: Sat, 6 Jun 2026 22:38:51 -0600 Subject: [PATCH 1/8] Move competition index to root --- .../src/components/cohost-sidebar.tsx | 6 +- .../src/components/compete-mobile-nav.tsx | 6 +- .../src/components/compete-nav-brand.tsx | 2 +- .../src/components/compete-nav.tsx | 8 +- .../src/components/competition-sidebar.tsx | 6 +- .../src/components/landing/final-cta.tsx | 2 +- .../src/components/landing/two-audiences.tsx | 2 +- .../src/components/nav/main-nav.tsx | 6 +- .../src/components/nav/mobile-nav.tsx | 4 +- .../src/components/series-sidebar.tsx | 6 +- apps/wodsmith-start/src/routes/__root.tsx | 2 + .../admin/teams/programming/index.tsx | 2 +- .../_protected/admin/teams/scaling/index.tsx | 2 +- .../routes/_protected/calculator/index.tsx | 2 +- .../src/routes/_protected/dashboard.tsx | 2 +- .../src/routes/_protected/log/index.tsx | 2 +- .../src/routes/_protected/movements/index.tsx | 2 +- .../routes/_protected/programming/index.tsx | 2 +- .../_protected/settings/overview/index.tsx | 2 +- .../_protected/settings/programming/index.tsx | 2 +- .../src/routes/_protected/team/index.tsx | 2 +- .../src/routes/_protected/workouts/index.tsx | 2 +- apps/wodsmith-start/src/routes/api/sitemap.ts | 1 - .../routes/compete/$slug/register/success.tsx | 2 +- .../src/routes/compete/$slug/registered.tsx | 2 +- .../routes/compete/cohost-invite/$token.tsx | 8 +- .../routes/compete/cohost/$competitionId.tsx | 4 +- .../src/routes/compete/index.tsx | 610 +---------------- .../src/routes/compete/invite/$token.tsx | 8 +- .../-components/accept-invite-button.tsx | 2 +- .../accept-volunteer-invite-form.tsx | 2 +- .../compete/organizer/$competitionId.tsx | 2 +- .../compete/organizer/onboard/index.tsx | 2 +- .../compete/organizer/series/$groupId.tsx | 2 +- apps/wodsmith-start/src/routes/index.tsx | 635 ++++++++++++++++-- .../src/routes/transfer/$transferId.tsx | 6 +- lat.md/architecture.md | 3 +- 37 files changed, 626 insertions(+), 735 deletions(-) diff --git a/apps/wodsmith-start/src/components/cohost-sidebar.tsx b/apps/wodsmith-start/src/components/cohost-sidebar.tsx index 896cb7b47..e3c06fa3d 100644 --- a/apps/wodsmith-start/src/components/cohost-sidebar.tsx +++ b/apps/wodsmith-start/src/components/cohost-sidebar.tsx @@ -216,7 +216,7 @@ function CohostSidebarHeader({ competitionName }: { competitionName: string }) { return (
- + wodsmith compete {!shouldHideBrand(pathname) && ( @@ -126,7 +126,7 @@ export default function CompeteMobileNav({ {session?.user ? ( <> @@ -200,7 +200,7 @@ export default function CompeteMobileNav({ ) : ( <> diff --git a/apps/wodsmith-start/src/components/compete-nav-brand.tsx b/apps/wodsmith-start/src/components/compete-nav-brand.tsx index 82122c483..5053daef2 100644 --- a/apps/wodsmith-start/src/components/compete-nav-brand.tsx +++ b/apps/wodsmith-start/src/components/compete-nav-brand.tsx @@ -4,7 +4,7 @@ import { Link } from "@tanstack/react-router" export function CompeteNavBrand() { return ( - + wodsmith compete Events @@ -55,21 +55,21 @@ export default function CompeteNav({ session, canOrganize }: CompeteNavProps) { ) : (
Events Login Sign Up diff --git a/apps/wodsmith-start/src/components/competition-sidebar.tsx b/apps/wodsmith-start/src/components/competition-sidebar.tsx index d6d9d5aca..e8c5a5f40 100644 --- a/apps/wodsmith-start/src/components/competition-sidebar.tsx +++ b/apps/wodsmith-start/src/components/competition-sidebar.tsx @@ -208,7 +208,7 @@ function CompetitionSidebarHeader() { return ( - + wodsmith compete - Browse Competitions + Browse Competitions
diff --git a/apps/wodsmith-start/src/components/landing/two-audiences.tsx b/apps/wodsmith-start/src/components/landing/two-audiences.tsx index ea889e96b..c99aeeea0 100644 --- a/apps/wodsmith-start/src/components/landing/two-audiences.tsx +++ b/apps/wodsmith-start/src/components/landing/two-audiences.tsx @@ -137,7 +137,7 @@ export function TwoAudiences({ session }: TwoAudiencesProps) {
diff --git a/apps/wodsmith-start/src/routes/_protected/settings/programming/index.tsx b/apps/wodsmith-start/src/routes/_protected/settings/programming/index.tsx index 71f3acc18..701cf7534 100644 --- a/apps/wodsmith-start/src/routes/_protected/settings/programming/index.tsx +++ b/apps/wodsmith-start/src/routes/_protected/settings/programming/index.tsx @@ -12,7 +12,7 @@ export const Route = createFileRoute("/_protected/settings/programming/")({ component: ProgrammingTracksPage, beforeLoad: async ({ context }) => { if (!context.hasWorkoutTracking) { - throw redirect({ to: "/compete" }) + throw redirect({ to: "/" }) } }, loader: async ({ context }) => { diff --git a/apps/wodsmith-start/src/routes/_protected/team/index.tsx b/apps/wodsmith-start/src/routes/_protected/team/index.tsx index 7d6c9a989..67677d8b8 100644 --- a/apps/wodsmith-start/src/routes/_protected/team/index.tsx +++ b/apps/wodsmith-start/src/routes/_protected/team/index.tsx @@ -5,7 +5,7 @@ export const Route = createFileRoute("/_protected/team/")({ component: TeamPage, beforeLoad: async ({ context }) => { if (!context.hasWorkoutTracking) { - throw redirect({ to: "/compete" }) + throw redirect({ to: "/" }) } }, }) diff --git a/apps/wodsmith-start/src/routes/_protected/workouts/index.tsx b/apps/wodsmith-start/src/routes/_protected/workouts/index.tsx index 1879bdd0e..0592697e7 100644 --- a/apps/wodsmith-start/src/routes/_protected/workouts/index.tsx +++ b/apps/wodsmith-start/src/routes/_protected/workouts/index.tsx @@ -73,7 +73,7 @@ export const Route = createFileRoute("/_protected/workouts/")({ component: WorkoutsPage, beforeLoad: async ({ context }) => { if (!context.hasWorkoutTracking) { - throw redirect({ to: "/compete" }) + throw redirect({ to: "/" }) } }, validateSearch: (search: Record): WorkoutsSearch => { diff --git a/apps/wodsmith-start/src/routes/api/sitemap.ts b/apps/wodsmith-start/src/routes/api/sitemap.ts index b7ff71345..a94069a41 100644 --- a/apps/wodsmith-start/src/routes/api/sitemap.ts +++ b/apps/wodsmith-start/src/routes/api/sitemap.ts @@ -37,7 +37,6 @@ export const Route = createFileRoute("/api/sitemap")({ const staticPages = [ { url: "/", priority: "1.0", changefreq: "weekly" }, - { url: "/compete", priority: "0.9", changefreq: "daily" }, { url: "/terms", priority: "0.2", changefreq: "yearly" }, { url: "/privacy", priority: "0.2", changefreq: "yearly" }, ] diff --git a/apps/wodsmith-start/src/routes/compete/$slug/register/success.tsx b/apps/wodsmith-start/src/routes/compete/$slug/register/success.tsx index a9b31d732..766bbadbc 100644 --- a/apps/wodsmith-start/src/routes/compete/$slug/register/success.tsx +++ b/apps/wodsmith-start/src/routes/compete/$slug/register/success.tsx @@ -82,7 +82,7 @@ export const Route = createFileRoute("/compete/$slug/register/success")({ const parentMatch = await parentMatchPromise const competition = parentMatch.loaderData?.competition if (!competition) { - throw redirect({ to: "/compete" }) + throw redirect({ to: "/" }) } // Check for registration diff --git a/apps/wodsmith-start/src/routes/compete/$slug/registered.tsx b/apps/wodsmith-start/src/routes/compete/$slug/registered.tsx index bcdaa086b..44caa3c15 100644 --- a/apps/wodsmith-start/src/routes/compete/$slug/registered.tsx +++ b/apps/wodsmith-start/src/routes/compete/$slug/registered.tsx @@ -44,7 +44,7 @@ export const Route = createFileRoute("/compete/$slug/registered")({ const competition = parentMatch.loaderData?.competition const divisions = parentMatch.loaderData?.divisions ?? [] if (!competition) { - throw redirect({ to: "/compete" }) + throw redirect({ to: "/" }) } const [{ registrations }, affiliateResult] = await Promise.all([ diff --git a/apps/wodsmith-start/src/routes/compete/cohost-invite/$token.tsx b/apps/wodsmith-start/src/routes/compete/cohost-invite/$token.tsx index 448be3e2c..fb834d847 100644 --- a/apps/wodsmith-start/src/routes/compete/cohost-invite/$token.tsx +++ b/apps/wodsmith-start/src/routes/compete/cohost-invite/$token.tsx @@ -93,7 +93,7 @@ function CohostInvitePage() { This invitation link is invalid or has expired.

@@ -143,7 +143,7 @@ function CohostInvitePage() { new invite.

@@ -215,7 +215,7 @@ function CohostInvitePage() { replace: true, }) } else { - await router.navigate({ to: "/compete", replace: true }) + await router.navigate({ to: "/", replace: true }) } } catch (error) { toast.dismiss() @@ -305,7 +305,7 @@ function CohostInvitePage() { diff --git a/apps/wodsmith-start/src/routes/compete/cohost/$competitionId.tsx b/apps/wodsmith-start/src/routes/compete/cohost/$competitionId.tsx index 3e36d7e0f..68d93556f 100644 --- a/apps/wodsmith-start/src/routes/compete/cohost/$competitionId.tsx +++ b/apps/wodsmith-start/src/routes/compete/cohost/$competitionId.tsx @@ -59,7 +59,7 @@ export const Route = createFileRoute("/compete/cohost/$competitionId")({ const competitionTeamId = competition.competitionTeamId if (!competitionTeamId) { throw redirect({ - to: "/compete", + to: "/", search: {}, }) } @@ -81,7 +81,7 @@ export const Route = createFileRoute("/compete/cohost/$competitionId")({ if (!permissions) { throw redirect({ - to: "/compete", + to: "/", search: {}, }) } diff --git a/apps/wodsmith-start/src/routes/compete/index.tsx b/apps/wodsmith-start/src/routes/compete/index.tsx index dcb9639a9..de258836b 100644 --- a/apps/wodsmith-start/src/routes/compete/index.tsx +++ b/apps/wodsmith-start/src/routes/compete/index.tsx @@ -1,611 +1,7 @@ -import { - useEffect, - useDeferredValue, - useMemo, - useState, - useTransition, -} from "react" -import { createFileRoute, useNavigate } from "@tanstack/react-router" -import { FilterIcon, SearchIcon, X } from "lucide-react" -import { CompetitionCard } from "@/components/competition-card" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" -import { getSessionFn } from "@/server-fns/auth-fns" -import { - type CompetitionWithOrganizingTeam, - getPublicCompetitionsFn, -} from "@/server-fns/competition-fns" -import { cn } from "@/utils/cn" -import { getTodayInTimezone } from "@/utils/date-utils" - -// --------------------------------------------------------------------------- -// Route -// --------------------------------------------------------------------------- - -type StatusFilter = "all" | "registration-open" | "active" | "upcoming" | "past" - -type CompeteSearch = { - q?: string - filter?: StatusFilter - location?: string - organizer?: string - type?: "in-person" | "online" -} +import { createFileRoute, redirect } from "@tanstack/react-router" export const Route = createFileRoute("/compete/")({ - head: () => ({ - meta: [ - { - title: "Find Fitness Competitions | WODsmith", - }, - { - name: "description", - content: - "Browse and register for functional fitness competitions. Find upcoming Functional Fitness throwdowns, see leaderboards, and sign up on WODsmith.", - }, - { property: "og:type", content: "website" }, - { property: "og:url", content: "https://wodsmith.com/compete" }, - { - property: "og:title", - content: "Find Fitness Competitions | WODsmith", - }, - { - property: "og:description", - content: - "Browse and register for functional fitness competitions. Find upcoming Functional Fitness throwdowns, see leaderboards, and sign up.", - }, - { property: "og:site_name", content: "WODsmith" }, - { name: "twitter:card", content: "summary" }, - { - name: "twitter:title", - content: "Find Fitness Competitions | WODsmith", - }, - { - name: "twitter:description", - content: - "Browse and register for functional fitness competitions on WODsmith.", - }, - ], - links: [{ rel: "canonical", href: "https://wodsmith.com/compete" }], - }), - component: CompetePage, - staleTime: 30_000, - validateSearch: (search: Record): CompeteSearch => ({ - q: typeof search.q === "string" ? search.q : undefined, - filter: isValidStatusFilter(search.filter) ? search.filter : undefined, - location: typeof search.location === "string" ? search.location : undefined, - organizer: - typeof search.organizer === "string" ? search.organizer : undefined, - type: - search.type === "in-person" || search.type === "online" - ? search.type - : undefined, - }), - loader: async () => { - const [result, session] = await Promise.all([ - getPublicCompetitionsFn({ data: {} }), - getSessionFn(), - ]) - return { - competitions: result.competitions, - isAuthenticated: session !== null, - } + beforeLoad: () => { + throw redirect({ to: "/" }) }, }) - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -function isValidStatusFilter(value: unknown): value is StatusFilter { - return ( - typeof value === "string" && - ["all", "registration-open", "active", "upcoming", "past"].includes(value) - ) -} - -type CompetitionStatus = - | "registration-open" - | "active" - | "coming-soon" - | "registration-closed" - | "past" - -function getCompetitionStatus( - comp: CompetitionWithOrganizingTeam, -): CompetitionStatus { - const { startDate, endDate, registrationOpensAt, registrationClosesAt } = comp - const today = getTodayInTimezone(comp.timezone ?? "America/Denver") - - if (endDate < today) return "past" - if (startDate <= today && endDate >= today) return "active" - if ( - startDate > today && - registrationOpensAt && - registrationClosesAt && - registrationOpensAt <= today && - registrationClosesAt >= today - ) - return "registration-open" - if (startDate > today && registrationClosesAt && registrationClosesAt < today) - return "registration-closed" - return "coming-soon" -} - -const STATUS_TABS: { value: StatusFilter; label: string }[] = [ - { value: "all", label: "All" }, - { value: "registration-open", label: "Open" }, - { value: "active", label: "Live" }, - { value: "upcoming", label: "Upcoming" }, - { value: "past", label: "Past" }, -] - -function matchesStatusFilter( - status: CompetitionStatus, - filter: StatusFilter, -): boolean { - if (filter === "all") return status !== "past" - if (filter === "past") return status === "past" - if (filter === "active") return status === "active" - if (filter === "registration-open") return status === "registration-open" - if (filter === "upcoming") - return status === "coming-soon" || status === "registration-closed" - return true -} - -function getLocationLabel(comp: CompetitionWithOrganizingTeam): string | null { - const city = comp.address?.city?.trim() - const state = comp.address?.stateProvince?.trim() - if (city && state) return `${city}, ${state}` - if (city) return city - if (state) return state - return null -} - -// --------------------------------------------------------------------------- -// Page -// --------------------------------------------------------------------------- - -function CompetePage() { - const { competitions } = Route.useLoaderData() - const search = Route.useSearch() - const navigate = useNavigate({ from: Route.fullPath }) - const activeStatus = search.filter ?? "all" - const [, startTransition] = useTransition() - const [localSearch, setLocalSearch] = useState(search.q ?? "") - const deferredSearchQuery = useDeferredValue(search.q) - const [showAdvanced, setShowAdvanced] = useState( - Boolean(search.location || search.organizer || search.type), - ) - - // Prevent card-enter animation from firing twice on SSR + hydration. - // Only enable animation after the client has hydrated. - const [hasMounted, setHasMounted] = useState(false) - useEffect(() => setHasMounted(true), []) - - const updateSearch = (updates: Partial) => { - startTransition(() => { - navigate({ - search: (prev) => { - const next = { ...prev, ...updates } - // Clean undefined values - for (const key of Object.keys(next) as (keyof CompeteSearch)[]) { - if (next[key] === undefined) delete next[key] - } - return next - }, - }) - }) - } - - const handleSearchChange = (value: string) => { - setLocalSearch(value) - updateSearch({ q: value || undefined }) - } - - // Derive statuses - const competitionsWithStatus = useMemo( - () => - competitions.map((comp) => ({ - ...comp, - _status: getCompetitionStatus(comp), - _location: getLocationLabel(comp), - })), - [competitions], - ) - - // Build filter option lists from data - const filterOptions = useMemo(() => { - const locations = new Set() - const organizers = new Set() - - for (const comp of competitionsWithStatus) { - if (comp._location) locations.add(comp._location) - if (comp.organizingTeam?.name) organizers.add(comp.organizingTeam.name) - } - - return { - locations: [...locations].sort(), - organizers: [...organizers].sort(), - } - }, [competitionsWithStatus]) - - // Counts per status tab - const counts = useMemo(() => { - const c = { - all: 0, - "registration-open": 0, - active: 0, - upcoming: 0, - past: 0, - } - for (const comp of competitionsWithStatus) { - if (comp._status !== "past") c.all++ - if (comp._status === "registration-open") c["registration-open"]++ - if (comp._status === "active") c.active++ - if ( - comp._status === "coming-soon" || - comp._status === "registration-closed" - ) - c.upcoming++ - if (comp._status === "past") c.past++ - } - return c - }, [competitionsWithStatus]) - - // Check if any advanced filter is active - const hasAdvancedFilters = Boolean( - search.location || search.organizer || search.type, - ) - - const clearAdvancedFilters = () => { - updateSearch({ - location: undefined, - organizer: undefined, - type: undefined, - }) - } - - // Filter + search + sort - const sorted = useMemo(() => { - let list = competitionsWithStatus.filter((c) => - matchesStatusFilter(c._status, activeStatus), - ) - - // Advanced filters - if (search.location) { - list = list.filter((c) => c._location === search.location) - } - if (search.organizer) { - list = list.filter((c) => c.organizingTeam?.name === search.organizer) - } - if (search.type) { - list = list.filter((c) => c.competitionType === search.type) - } - - // Text search - if (deferredSearchQuery) { - const q = deferredSearchQuery.toLowerCase() - list = list.filter( - (c) => - c.name.toLowerCase().includes(q) || - c.description?.toLowerCase().includes(q) || - c.organizingTeam?.name.toLowerCase().includes(q), - ) - } - - if (activeStatus === "all") { - return [...list].sort( - (a, b) => - new Date(a.startDate).getTime() - new Date(b.startDate).getTime(), - ) - } - - const priority: Record = { - active: 0, - "registration-open": 1, - "coming-soon": 2, - "registration-closed": 2, - past: 4, - } - return [...list].sort((a, b) => { - const p = priority[a._status] - priority[b._status] - if (p !== 0) return p - return new Date(a.startDate).getTime() - new Date(b.startDate).getTime() - }) - }, [ - competitionsWithStatus, - activeStatus, - search.location, - search.organizer, - search.type, - deferredSearchQuery, - ]) - - return ( -
- {/* ── Header ─────────────────────────────────────────── */} -
-

- Competitions -

-

- Discover events, register to compete, and track your performance. -

-
- - {/* ── Toolbar ────────────────────────────────────────── */} -
- {/* Status tabs */} -
- {STATUS_TABS.map((tab) => { - const isActive = activeStatus === tab.value - const count = counts[tab.value] - return ( - - ) - })} -
- - {/* Search + filter toggle */} -
-
-
- -
-
- - {/* ── Advanced filters ───────────────────────────────── */} - {showAdvanced && ( -
- {/* Location */} -
- - -
- - {/* Organizer */} -
- - -
- - {/* Competition type */} -
- - -
- - {/* Clear */} - {hasAdvancedFilters && ( - - )} -
- )} - - {/* ── Divider ────────────────────────────────────────── */} -
- - {/* ── Grid ───────────────────────────────────────────── */} - {sorted.length === 0 ? ( - - ) : ( -
- {sorted.map((comp, i) => ( - - ))} -
- )} -
- ) -} - -// --------------------------------------------------------------------------- -// Empty state -// --------------------------------------------------------------------------- - -function EmptyState({ - searchQuery, - hasFilters, -}: { - searchQuery?: string - hasFilters: boolean -}) { - const navigate = useNavigate({ from: Route.fullPath }) - - return ( -
-
-

- {searchQuery ? "No results" : "Nothing here yet"} -

-

- {searchQuery - ? `Nothing matches "${searchQuery}".` - : hasFilters - ? "Try adjusting your filters." - : "Check back soon — new competitions are added regularly."} -

- {(searchQuery || hasFilters) && ( - - )} -
- ) -} diff --git a/apps/wodsmith-start/src/routes/compete/invite/$token.tsx b/apps/wodsmith-start/src/routes/compete/invite/$token.tsx index 76d193fdc..257c68062 100644 --- a/apps/wodsmith-start/src/routes/compete/invite/$token.tsx +++ b/apps/wodsmith-start/src/routes/compete/invite/$token.tsx @@ -223,7 +223,7 @@ function InvitePage() { This invitation link is invalid or has expired.

@@ -271,7 +271,7 @@ function InvitePage() { a new invite.

@@ -630,7 +630,7 @@ function VolunteerApplicationStatus({ invite }: { invite: VolunteerInvite }) { )} @@ -697,7 +697,7 @@ function DirectVolunteerInvite({ organizer for a new invite.

diff --git a/apps/wodsmith-start/src/routes/compete/invite/-components/accept-invite-button.tsx b/apps/wodsmith-start/src/routes/compete/invite/-components/accept-invite-button.tsx index 7446c5655..ed829c6e6 100644 --- a/apps/wodsmith-start/src/routes/compete/invite/-components/accept-invite-button.tsx +++ b/apps/wodsmith-start/src/routes/compete/invite/-components/accept-invite-button.tsx @@ -80,7 +80,7 @@ export function AcceptInviteButton({ params: { slug: result.competitionSlug }, }) } else { - navigate({ to: "/compete" }) + navigate({ to: "/" }) } } catch (err) { const message = diff --git a/apps/wodsmith-start/src/routes/compete/invite/-components/accept-volunteer-invite-form.tsx b/apps/wodsmith-start/src/routes/compete/invite/-components/accept-volunteer-invite-form.tsx index c102cd89b..43fb6f040 100644 --- a/apps/wodsmith-start/src/routes/compete/invite/-components/accept-volunteer-invite-form.tsx +++ b/apps/wodsmith-start/src/routes/compete/invite/-components/accept-volunteer-invite-form.tsx @@ -114,7 +114,7 @@ export function AcceptVolunteerInviteForm({ if (competitionSlug) { navigate({ to: "/compete/$slug", params: { slug: competitionSlug } }) } else { - navigate({ to: "/compete" }) + navigate({ to: "/" }) } } catch (err) { const message = diff --git a/apps/wodsmith-start/src/routes/compete/organizer/$competitionId.tsx b/apps/wodsmith-start/src/routes/compete/organizer/$competitionId.tsx index 91e277acf..62b22c0e4 100644 --- a/apps/wodsmith-start/src/routes/compete/organizer/$competitionId.tsx +++ b/apps/wodsmith-start/src/routes/compete/organizer/$competitionId.tsx @@ -53,7 +53,7 @@ export const Route = createFileRoute("/compete/organizer/$competitionId")({ if (!canManage) { throw redirect({ - to: "/compete", + to: "/", search: {}, }) } diff --git a/apps/wodsmith-start/src/routes/compete/organizer/onboard/index.tsx b/apps/wodsmith-start/src/routes/compete/organizer/onboard/index.tsx index 8bbacb2aa..b468efc93 100644 --- a/apps/wodsmith-start/src/routes/compete/organizer/onboard/index.tsx +++ b/apps/wodsmith-start/src/routes/compete/organizer/onboard/index.tsx @@ -481,7 +481,7 @@ function OrganizerRequestForm({ teams }: { teams: TeamInfo[] }) { + ) + })} +
+ + {/* Search + filter toggle */} +
+
+
+ +
+
+ + {/* ── Advanced filters ───────────────────────────────── */} + {showAdvanced && ( +
+ {/* Location */} +
+ + +
+ + {/* Organizer */} +
+ + +
+ + {/* Competition type */} +
+ + +
+ + {/* Clear */} + {hasAdvancedFilters && ( + + )} +
+ )} + + {/* ── Divider ────────────────────────────────────────── */} +
+ + {/* ── Grid ───────────────────────────────────────────── */} + {sorted.length === 0 ? ( + + ) : ( +
+ {sorted.map((comp, i) => ( + + ))} +
+ )} +
+ ) +} + +// --------------------------------------------------------------------------- +// Empty state +// --------------------------------------------------------------------------- + +function EmptyState({ + searchQuery, + hasFilters, +}: { + searchQuery?: string + hasFilters: boolean +}) { + const navigate = useNavigate({ from: Route.fullPath }) + + return ( +
+
+

+ {searchQuery ? "No results" : "Nothing here yet"} +

+

+ {searchQuery + ? `Nothing matches "${searchQuery}".` + : hasFilters + ? "Try adjusting your filters." + : "Check back soon — new competitions are added regularly."} +

+ {(searchQuery || hasFilters) && ( + + )} +
) } diff --git a/apps/wodsmith-start/src/routes/transfer/$transferId.tsx b/apps/wodsmith-start/src/routes/transfer/$transferId.tsx index 1ee564485..df788daa6 100644 --- a/apps/wodsmith-start/src/routes/transfer/$transferId.tsx +++ b/apps/wodsmith-start/src/routes/transfer/$transferId.tsx @@ -121,7 +121,7 @@ function TransferAcceptPage() { This transfer link is invalid or the transfer no longer exists.

@@ -171,7 +171,7 @@ function TransferAcceptPage() { This transfer has expired. Contact the organizer to resend.

@@ -191,7 +191,7 @@ function TransferAcceptPage() { This transfer was cancelled by the organizer.

diff --git a/lat.md/architecture.md b/lat.md/architecture.md index a6978c0e3..b04e94e5f 100644 --- a/lat.md/architecture.md +++ b/lat.md/architecture.md @@ -84,7 +84,8 @@ Authenticated routes requiring a valid session. Contains the main app dashboard, ### compete -Public-facing competition pages. Athletes browse, register for, and view results of competitions at `/compete/{slug}`. +Public-facing competition pages. +The competition discovery index moved to `/`; competition detail pages remain at `/compete/{slug}`. The event details view ([[apps/wodsmith-start/src/components/event-details-content.tsx]]) groups divisions by price tier, each tier collapsible (default open) with a chevron toggle. From 0c59fcedee4693eb14d764e3051d17e3c0e6a372 Mon Sep 17 00:00:00 2001 From: Ian Jones Date: Sun, 7 Jun 2026 22:13:33 -0600 Subject: [PATCH 2/8] Fix competition index PR feedback --- .../src/components/cohost-sidebar.tsx | 52 +- .../src/components/compete-breadcrumb.tsx | 9 +- .../src/components/compete-mobile-nav.tsx | 34 +- .../src/components/compete-nav-brand.tsx | 1 + .../src/components/compete-nav.tsx | 45 +- .../src/components/competition-sidebar.tsx | 8 +- .../src/components/landing/final-cta.tsx | 1 + .../src/components/landing/two-audiences.tsx | 1 + .../src/components/nav/main-nav.tsx | 8 +- .../src/components/nav/mobile-nav.tsx | 2 + .../src/components/series-sidebar.tsx | 154 +++--- .../admin/teams/programming/index.tsx | 1 + .../_protected/admin/teams/scaling/index.tsx | 1 + .../routes/_protected/calculator/index.tsx | 1 + .../src/routes/_protected/dashboard.tsx | 1 + .../src/routes/_protected/log/index.tsx | 1 + .../src/routes/_protected/movements/index.tsx | 1 + .../routes/_protected/programming/index.tsx | 1 + .../_protected/settings/overview/index.tsx | 1 + .../_protected/settings/programming/index.tsx | 1 + .../src/routes/_protected/team/index.tsx | 1 + .../src/routes/_protected/workouts/index.tsx | 1 + .../src/routes/compete/index.tsx | 36 +- .../src/routes/compete/invite/$token.tsx | 1 + .../-components/accept-invite-button.tsx | 1 + .../compete/organizer/series/$groupId.tsx | 26 +- apps/wodsmith-start/src/routes/index.tsx | 487 +++++++++--------- lat.md/architecture.md | 8 + 28 files changed, 507 insertions(+), 378 deletions(-) diff --git a/apps/wodsmith-start/src/components/cohost-sidebar.tsx b/apps/wodsmith-start/src/components/cohost-sidebar.tsx index e3c06fa3d..b484343dd 100644 --- a/apps/wodsmith-start/src/components/cohost-sidebar.tsx +++ b/apps/wodsmith-start/src/components/cohost-sidebar.tsx @@ -81,7 +81,13 @@ const getNavigation = ( label: "Competition Setup", items: [ ...(permissions?.divisions - ? [{ label: "Divisions", href: `${basePath}/divisions`, icon: Layers }] + ? [ + { + label: "Divisions", + href: `${basePath}/divisions`, + icon: Layers, + }, + ] : []), ...(permissions?.editEvents ? [ @@ -98,13 +104,31 @@ const getNavigation = ( ] : []), ...(permissions?.locations - ? [{ label: "Locations", href: `${basePath}/locations`, icon: MapPin }] + ? [ + { + label: "Locations", + href: `${basePath}/locations`, + icon: MapPin, + }, + ] : []), ...(permissions?.scoringConfig - ? [{ label: "Scoring", href: `${basePath}/scoring`, icon: Calculator }] + ? [ + { + label: "Scoring", + href: `${basePath}/scoring`, + icon: Calculator, + }, + ] : []), ...(permissions?.viewRegistrations - ? [{ label: "Registrations", href: `${basePath}/athletes`, icon: Users }] + ? [ + { + label: "Registrations", + href: `${basePath}/athletes`, + icon: Users, + }, + ] : []), ...(permissions?.waivers ? [ @@ -189,7 +213,13 @@ const getNavigation = ( ] : []), ...(permissions?.sponsors - ? [{ label: "Sponsors", href: `${basePath}/sponsors`, icon: Sparkles }] + ? [ + { + label: "Sponsors", + href: `${basePath}/sponsors`, + icon: Sparkles, + }, + ] : []), ], }, @@ -215,6 +245,7 @@ function NavMenuItem({ item, isActive }: { item: NavItem; isActive: boolean }) { function CohostSidebarHeader({ competitionName }: { competitionName: string }) { return ( + {/* @lat: [[architecture#Route Groups#compete]] */}
- + {/* @lat: [[architecture#Route Groups#compete]] */} + wodsmith compete
+ {/* @lat: [[architecture#Route Groups#compete]] */} - {competitionName} + + {competitionName} +
diff --git a/apps/wodsmith-start/src/components/compete-breadcrumb.tsx b/apps/wodsmith-start/src/components/compete-breadcrumb.tsx index 24d4aaddd..01c19a3fe 100644 --- a/apps/wodsmith-start/src/components/compete-breadcrumb.tsx +++ b/apps/wodsmith-start/src/components/compete-breadcrumb.tsx @@ -14,6 +14,9 @@ interface BreadcrumbSegment { href?: string } +// @lat: [[architecture#Route Groups#compete]] +const COMPETITION_DISCOVERY_PATH = "/" + /** * Route segment to human-readable label mapping */ @@ -128,9 +131,13 @@ export function CompeteBreadcrumb({ // Don't add href for the last segment (current page) const isLast = i === pathSegments.length - 1 + const href = + segment === "compete" && currentPath === "/compete" + ? COMPETITION_DISCOVERY_PATH + : currentPath segments.push({ label, - href: isLast ? undefined : currentPath, + href: isLast ? undefined : href, }) } diff --git a/apps/wodsmith-start/src/components/compete-mobile-nav.tsx b/apps/wodsmith-start/src/components/compete-mobile-nav.tsx index d78e38fe9..9fffc7758 100644 --- a/apps/wodsmith-start/src/components/compete-mobile-nav.tsx +++ b/apps/wodsmith-start/src/components/compete-mobile-nav.tsx @@ -69,6 +69,7 @@ export default function CompeteMobileNav({ const [open, setOpen] = useState(false) const router = useRouterState() const pathname = router.location.pathname + const showCompetitionsLink = pathname !== "/" const isProfileIncomplete = missingProfileFields && @@ -95,6 +96,7 @@ export default function CompeteMobileNav({ Navigation Menu