feat: Enhance league home page with sports seasons section and scoring pattern display

This commit is contained in:
Chris Parsons 2025-11-14 20:39:51 -08:00
parent f17699e4c5
commit 0eeb1b6245
3 changed files with 167 additions and 18 deletions

View file

@ -0,0 +1,104 @@
import { Link } from "react-router";
import { Badge } from "~/components/ui/badge";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { Trophy, Target, Flag } from "lucide-react";
interface SportSeasonCardProps {
leagueId: string;
sportSeasonId: string;
sportName: string;
seasonName: string;
status: "upcoming" | "active" | "completed";
scoringPattern?: string | null;
}
export function SportSeasonCard({
leagueId,
sportSeasonId,
sportName,
seasonName,
status,
scoringPattern,
}: SportSeasonCardProps) {
const getStatusBadge = () => {
switch (status) {
case "upcoming":
return (
<Badge variant="outline" className="bg-blue-50 text-blue-700 border-blue-200">
<Flag className="mr-1 h-3 w-3" />
Upcoming
</Badge>
);
case "active":
return (
<Badge variant="outline" className="bg-green-50 text-green-700 border-green-200">
<Target className="mr-1 h-3 w-3" />
Active
</Badge>
);
case "completed":
return (
<Badge variant="outline" className="bg-gray-50 text-gray-700 border-gray-200">
<Trophy className="mr-1 h-3 w-3" />
Completed
</Badge>
);
}
};
const getScoringPatternLabel = () => {
if (!scoringPattern) return null;
switch (scoringPattern) {
case "single_elimination_playoff":
return "Playoff Bracket";
case "page_playoff":
return "AFL Finals";
case "season_standings":
return "Season Standings";
case "qualifying_points":
return "Qualifying Points";
default:
return null;
}
};
const scoringLabel = getScoringPatternLabel();
const isPlayoffType = scoringPattern === "single_elimination_playoff" || scoringPattern === "page_playoff";
return (
<Link to={`/leagues/${leagueId}/sports-seasons/${sportSeasonId}`}>
<Card className="hover:shadow-lg transition-shadow cursor-pointer h-full">
<CardHeader>
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<CardTitle className="text-lg mb-1">{sportName}</CardTitle>
<CardDescription className="truncate">{seasonName}</CardDescription>
</div>
{getStatusBadge()}
</div>
</CardHeader>
<CardContent>
<div className="space-y-2">
{scoringLabel && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
{isPlayoffType ? (
<Trophy className="h-4 w-4" />
) : (
<Target className="h-4 w-4" />
)}
<span>{scoringLabel}</span>
</div>
)}
</div>
</CardContent>
</Card>
</Link>
);
}

View file

@ -13,7 +13,7 @@ import {
findUserByClerkId,
findDraftSlotsBySeasonId,
} from "~/models";
import { findSeasonWithSportsSeasons } from "~/models/season";
import { findCurrentSeasonWithSports } from "~/models/season";
import type { Route } from "./+types/$leagueId";
import { Button } from "~/components/ui/button";
import {
@ -23,6 +23,7 @@ import {
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { SportSeasonCard } from "~/components/sports/SportSeasonCard";
export async function loader(args: Route.LoaderArgs) {
const { userId } = await getAuth(args);
@ -37,10 +38,8 @@ export async function loader(args: Route.LoaderArgs) {
}
// Fetch current season with sports
const season = await findCurrentSeason(leagueId);
const seasonWithSports = season
? await findSeasonWithSportsSeasons(season.id)
: null;
const seasonWithSports = await findCurrentSeasonWithSports(leagueId);
const season = seasonWithSports || null;
// Fetch commissioners
const commissioners = await findCommissionersByLeagueId(leagueId);
@ -119,8 +118,15 @@ export async function loader(args: Route.LoaderArgs) {
// Count teams with owners
const teamsWithOwners = teams.filter((t) => t.ownerId !== null).length;
// Count sports in season
const sportsCount = seasonWithSports?.seasonSports?.length || 0;
// Get sports seasons data
const sportsSeasons = seasonWithSports?.seasonSports?.map((ss) => ({
id: ss.sportsSeason.id,
name: ss.sportsSeason.name,
status: ss.sportsSeason.status,
scoringPattern: ss.sportsSeason.scoringPattern,
sport: ss.sportsSeason.sport,
})) || [];
const sportsCount = sportsSeasons.length;
// Check if draft order is set
const isDraftOrderSet = draftSlots.length > 0;
@ -139,6 +145,7 @@ export async function loader(args: Route.LoaderArgs) {
sportsCount,
teamsWithOwners,
isDraftOrderSet,
sportsSeasons,
};
}
@ -157,6 +164,7 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
sportsCount,
teamsWithOwners,
isDraftOrderSet,
sportsSeasons,
} = loaderData;
const [searchParams, setSearchParams] = useSearchParams();
const [copied, setCopied] = useState(false);
@ -227,6 +235,33 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
<div className="grid gap-6 md:grid-cols-3">
{/* Left Column - 2/3 width on desktop */}
<div className="md:col-span-2 space-y-6">
{/* Sports Seasons Section */}
{season && sportsSeasons.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Sports Seasons</CardTitle>
<CardDescription>
{sportsSeasons.length} sport{sportsSeasons.length !== 1 ? "s" : ""} in this season
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4 sm:grid-cols-2">
{sportsSeasons.map((sportSeason) => (
<SportSeasonCard
key={sportSeason.id}
leagueId={league.id}
sportSeasonId={sportSeason.id}
sportName={sportSeason.sport.name}
seasonName={sportSeason.name}
status={sportSeason.status as "upcoming" | "active" | "completed"}
scoringPattern={sportSeason.scoringPattern}
/>
))}
</div>
</CardContent>
</Card>
)}
{isUserCommissioner && season && availableTeamCount > 0 && (
<Card>
<CardHeader>

View file

@ -1358,18 +1358,24 @@ scoring_events {
**Note**: Route exists and displays correctly. Navigation cards on league home page pending (see 4.5)
- [ ] **4.5** League home page integration (partially complete)
- [x] **4.5** League home page integration ✅ *Completed*
- [x] Add navigation link to standings page from league home
- [ ] Display team standings table on league home page (or keep separate as currently implemented)
- [ ] Add "Sports Seasons" section showing all sports in current season
- [ ] Link each sport season card to detail page (Q11 - ownership hints on each sport's page)
- [ ] Show sport status badges (upcoming, active, completed)
- [ ] Quick access to brackets from league home
- [x] Display team standings table on league home page (kept separate as standalone page)
- [x] Add "Sports Seasons" section showing all sports in current season
- [x] Link each sport season card to detail page (Q11 - ownership hints on each sport's page)
- [x] Show sport status badges (upcoming, active, completed)
- [x] Quick access to brackets from league home (via sport season cards)
**Implementation Notes**:
- "View Standings" link added in League Info section (app/routes/leagues/$leagueId.tsx:433-440)
- Standalone standings page preferred over embedding in league home for cleaner UX
- Sport season detail pages already exist (Phase 3.4) - need navigation cards on league home
- "View Standings" link in League Info section (app/routes/leagues/$leagueId.tsx)
- Standalone standings page for cleaner UX
- Sport season detail pages exist from Phase 3.4
- Created `SportSeasonCard` component with status badges (app/components/sports/SportSeasonCard.tsx)
- Sports Seasons section displays 2-column grid on desktop, single column on mobile
- Each card shows: sport name, season name, status badge, scoring pattern icon
- Status badges use color coding: blue=upcoming, green=active, gray=completed
- Cards link directly to sport season detail pages where brackets/standings are displayed
- Updated loader to use `findCurrentSeasonWithSports` for efficient data fetching
- [ ] **4.6** Historical views
- [ ] Season completion detection
@ -1468,10 +1474,14 @@ scoring_events {
- ⏳ **Phase 4**: Standings & Display - IN PROGRESS
- ✅ **4.1** Enhanced Standings Table - COMPLETE (tiebreaker logic, placement breakdowns, standalone page, navigation)
- ✅ **4.2** Movement Tracking & Snapshots - COMPLETE (daily snapshots, 7-day comparison, admin interface)
- ⏳ **4.5** League Home Integration - PARTIAL (standings link added, sports season cards pending)
- ✅ **4.3** Team Breakdown Pages - COMPLETE (detailed score breakdown, sport grouping, navigation)
- ✅ **4.4** Sport Season Pages with Ownership - COMPLETE (pattern-specific displays, ownership hints, brackets)
- ✅ **4.5** League Home Integration - COMPLETE (sports season cards, status badges, direct navigation)
- ⏳ **Phase 5**: Expected Value - TODO
- ⏳ **Phase 6**: Polish & Optimization - TODO
**Current Focus**: Phase 4 - Standings & Display (4.1, 4.2, and 4.3 complete, 4.5 partially complete)
**Current Focus**: Phase 4 - Standings & Display (4.1-4.5 complete, only 4.6 Historical Views remaining)
**Total Test Count**: 404 tests passing ✅
**Next Task**: Phase 4.6 - Historical Views (season completion detection, point progression charts)