brackt/app/components/draft/TeamRosterView.tsx
Chris Parsons 9ed0282fd0
New design (#309)
* 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>
2026-04-23 13:14:55 -07:00

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>
);
});