* Redesign home page with new layout and component system - Two-column layout (My Leagues 2/3, Upcoming Events 1/3) with mobile stack - LeagueRow: square avatar, gradient draft highlight, rank/points display, progress bar - MyLeaguesCard, CreateLeagueCard with shared SectionCardHeader - UpcomingEventsCard: vertical timeline with grouped multi-league events - Shared gradient system: BracktGradients SVG defs, GradientIcon wrapper, brand.ts constants - Button default variant updated to green→cyan gradient - Navbar: plain nav links with gradient hover, support/admin icon buttons - Accessibility fixes: semantic h2 headings, aria-label on LeagueAvatar and nav elements - Storybook stories for all new components Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Responsive league row layout and mobile polish - League rows stack avatar+name on top, stats full-width below on mobile - Stats spread to right side on sm+ screens with border separator on mobile - Tighter padding on mobile (px-3/py-3), full padding on sm+ - Card headers and content use px-3 sm:px-6 to reduce mobile gutters - Two-column home layout deferred to lg breakpoint (tablet gets stacked) - Active leagues sorted by completion percentage descending - Default rank 1 / 0 points for active leagues with no scoring events yet - Fix ordinal bug for 11th/12th/13th; add aria-labels to rank change indicators - Remove dead StatDivider className prop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Improve claude file. * Add StandingsPreview card component with podium row styling - New StandingsPreview component with gold/silver/bronze row tints for top 3, team avatar, and LeagueRow-style stat columns (Ranking + Points) with rank and 7-day point change indicators - Fix GradientIcon in Storybook by adding BracktGradients decorator to preview.tsx (renamed from .ts to support JSX) - Fix degenerate SVG gradient on horizontal strokes by switching BracktGradients to gradientUnits="userSpaceOnUse" with Lucide-space coordinates (0→24) - Revert erroneous fill: url(#gradient) from GradientIcon; stroke-only fix was sufficient once gradientUnits was corrected Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Update components on league homepage. * Finish up league page styling. * Work on standings page. * Add story for RecentScoresCard * Update Point Progression Chart. * Sort point progression legend by ranking and add team links to standings rows * Fix standings discrepancy on change. * Create draft cell component. * Update draft board page * Draft room improvements. * Update some draft room styling. * Fix context menu missing. * Move tab navigation and autodraft to header row, narrow sidebar * Virtualize available participants list, memoize draft room props Adds @tanstack/react-virtual to replace separate mobile/desktop lists with a single unified virtual scroll loop. Also memoizes miniDraftGrid and availableParticipantsSectionProps, and switches pick lookup from Array.find to a Map for O(1) access. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Update draft room UI. * More draft room fixes. * Draft room tweaks. * Fix Rosters page. * Queue Section fixes. * Mobile Draft fixes. * Fix draft board page. * Create bracket look. * Bracket work. * Finish bracket page. * Homepage initial styling * homepage copy * Add privacy policy. Fixes #88. * how to play copy * rules copy * Fix brackets on homepage. * Add footer to website. * Glow on dots. * Landing page copy. * Fix sidebar. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
194 lines
6.3 KiB
TypeScript
194 lines
6.3 KiB
TypeScript
import { memo, useEffect, useMemo, useState } from "react";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "~/components/ui/select";
|
|
import { DraftPickCell } from "./DraftPickCell";
|
|
import { type DraftPick, groupPicksByTeamAndSport } from "./picks-utils";
|
|
|
|
interface TeamRosterViewProps {
|
|
draftSlots: Array<{ id: string; team: { id: string; name: string } }>;
|
|
ownerMap?: Record<string, string>;
|
|
picks: DraftPick[];
|
|
sports: Array<{ id: string; name: string }>;
|
|
totalRounds: number;
|
|
}
|
|
|
|
export const TeamRosterView = memo(function TeamRosterView({
|
|
draftSlots,
|
|
ownerMap = {},
|
|
picks,
|
|
sports,
|
|
totalRounds,
|
|
}: TeamRosterViewProps) {
|
|
const [selectedTeamId, setSelectedTeamId] = useState(
|
|
draftSlots[0]?.team.id ?? ""
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (
|
|
draftSlots.length > 0 &&
|
|
!draftSlots.some((s) => s.team.id === selectedTeamId)
|
|
) {
|
|
setSelectedTeamId(draftSlots[0].team.id);
|
|
}
|
|
}, [draftSlots, selectedTeamId]);
|
|
|
|
const picksByTeamAndSport = useMemo(
|
|
() => groupPicksByTeamAndSport(picks),
|
|
[picks]
|
|
);
|
|
|
|
const teamPicks = useMemo(
|
|
() => picksByTeamAndSport.get(selectedTeamId),
|
|
[picksByTeamAndSport, selectedTeamId]
|
|
);
|
|
|
|
const flexPicksAvailable = useMemo(
|
|
() => Math.max(0, totalRounds - sports.length),
|
|
[totalRounds, sports.length]
|
|
);
|
|
|
|
const flexPicksUsed = useMemo(() => {
|
|
let used = 0;
|
|
teamPicks?.forEach((sportPicks) => {
|
|
if (sportPicks.length > 1) used += sportPicks.length - 1;
|
|
});
|
|
return used;
|
|
}, [teamPicks]);
|
|
|
|
const flexPicksRemaining = useMemo(
|
|
() => flexPicksAvailable - flexPicksUsed,
|
|
[flexPicksAvailable, flexPicksUsed]
|
|
);
|
|
|
|
const missingSports = useMemo(
|
|
() => sports.filter((s) => (teamPicks?.get(s.id)?.length ?? 0) === 0),
|
|
[sports, teamPicks]
|
|
);
|
|
|
|
const draftedSports = useMemo(
|
|
() => sports.filter((s) => (teamPicks?.get(s.id)?.length ?? 0) > 0),
|
|
[sports, teamPicks]
|
|
);
|
|
|
|
const flexColor =
|
|
flexPicksAvailable === 0
|
|
? "text-muted-foreground"
|
|
: flexPicksRemaining <= 0
|
|
? "text-destructive"
|
|
: flexPicksRemaining <= Math.ceil(flexPicksAvailable * 0.25)
|
|
? "text-amber-500 dark:text-amber-400"
|
|
: "text-emerald-500 dark:text-emerald-400";
|
|
|
|
if (draftSlots.length === 0) {
|
|
return (
|
|
<div className="flex items-center justify-center h-full p-8 text-muted-foreground">
|
|
<p>No teams in this draft.</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="h-full overflow-y-auto">
|
|
<div className="p-4 space-y-4">
|
|
{/* Team selector */}
|
|
<div className="space-y-1">
|
|
<label
|
|
htmlFor="roster-team-select"
|
|
className="text-xs font-medium text-muted-foreground"
|
|
>
|
|
Team
|
|
</label>
|
|
<Select value={selectedTeamId} onValueChange={setSelectedTeamId}>
|
|
<SelectTrigger id="roster-team-select" className="w-full">
|
|
<SelectValue placeholder="Select a team" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{draftSlots.map((slot) => (
|
|
<SelectItem key={slot.id} value={slot.team.id}>
|
|
{ownerMap[slot.team.id] || slot.team.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Mini dashboard */}
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="rounded-lg border bg-card px-3 py-2.5 space-y-0.5">
|
|
<p className="text-xs text-muted-foreground font-medium">Flex Picks</p>
|
|
{flexPicksAvailable === 0 ? (
|
|
<p className="text-sm font-semibold text-muted-foreground">N/A</p>
|
|
) : (
|
|
<>
|
|
<p className={`text-xl font-bold tabular-nums leading-none ${flexColor}`}>
|
|
{flexPicksRemaining}
|
|
<span className="text-sm font-normal text-muted-foreground ml-1">
|
|
/ {flexPicksAvailable}
|
|
</span>
|
|
</p>
|
|
<p className="text-xs text-muted-foreground">remaining</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
<div className="rounded-lg border bg-card px-3 py-2.5 space-y-0.5">
|
|
<p className="text-xs text-muted-foreground font-medium">Sports Remaining</p>
|
|
<p className={`text-xl font-bold tabular-nums leading-none ${missingSports.length > 0 ? "text-foreground" : "text-emerald-500 dark:text-emerald-400"}`}>
|
|
{missingSports.length}
|
|
<span className="text-sm font-normal text-muted-foreground ml-1">
|
|
/ {sports.length}
|
|
</span>
|
|
</p>
|
|
<p className="text-xs text-muted-foreground">to draft</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Missing sports list */}
|
|
{missingSports.length > 0 && (
|
|
<div>
|
|
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-1.5">Still Needed</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
{missingSports.map((s) => s.name).join(" · ")}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Per-sport pick cells */}
|
|
{draftedSports.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground text-center py-6">No picks yet</p>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{draftedSports.map((sport) => {
|
|
const sportPicks = teamPicks?.get(sport.id) ?? [];
|
|
return (
|
|
<div key={sport.id}>
|
|
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-1.5">
|
|
{sport.name}
|
|
</p>
|
|
<div className="flex flex-wrap gap-1">
|
|
{sportPicks.map((pick) => (
|
|
<DraftPickCell
|
|
key={pick.id}
|
|
pickNumber={pick.pickNumber}
|
|
round={pick.round}
|
|
pickInRound={pick.pickInRound}
|
|
state="picked"
|
|
pick={pick}
|
|
corona="gradient"
|
|
className="w-44 flex-none"
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
});
|