feat: Update DraftSidebar to include recent picks section and refactor layout; enhance AvailableParticipantsSection and QueueSection styling

This commit is contained in:
Chris Parsons 2025-10-25 18:25:26 -07:00
parent 714ab0f484
commit c9d3ee73cd
7 changed files with 175 additions and 159 deletions

View file

@ -13,7 +13,7 @@ interface DraftSidebarProps {
collapsed: boolean;
onCollapsedChange: (collapsed: boolean) => void;
queueSection: ReactNode;
participantsSection: ReactNode;
recentPicksSection: ReactNode;
className?: string;
}
@ -21,27 +21,31 @@ export function DraftSidebar({
collapsed,
onCollapsedChange,
queueSection,
participantsSection,
recentPicksSection,
className,
}: DraftSidebarProps) {
if (collapsed) {
return (
<div
className={cn(
"relative flex-shrink-0 bg-card border-r border-border transition-all duration-300",
"relative flex-shrink-0 bg-card border-r border-border transition-all duration-300 flex flex-col",
"w-12 hidden lg:block", // Hide completely on mobile when collapsed
className
)}
>
<Button
variant="ghost"
size="icon"
onClick={() => onCollapsedChange(false)}
className="absolute top-4 left-1/2 -translate-x-1/2"
aria-label="Expand sidebar"
>
<ChevronRight className="h-4 w-4" />
</Button>
<div className="flex-1" />
{/* Expand button at bottom to match hide button position */}
<div className="border-t border-border p-2 flex-shrink-0">
<Button
variant="ghost"
size="icon"
onClick={() => onCollapsedChange(false)}
className="w-full"
aria-label="Expand sidebar"
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
);
}
@ -58,7 +62,7 @@ export function DraftSidebar({
<div
className={cn(
"relative flex-shrink-0 bg-card border-r border-border transition-all duration-300 flex flex-col",
"w-[350px]",
"w-[450px]",
// Mobile: fixed overlay, Desktop: normal sidebar
"fixed inset-y-0 left-0 z-50 lg:relative lg:z-auto",
className
@ -66,28 +70,26 @@ export function DraftSidebar({
>
<Accordion
type="multiple"
defaultValue={["queue", "participants"]}
className="flex-1 overflow-hidden"
defaultValue={["queue", "recent-picks"]}
className="flex-1 overflow-hidden flex flex-col"
>
{/* Queue Section */}
<AccordionItem value="queue" className="border-b">
<AccordionItem value="queue" className="border-b flex-shrink-0">
<AccordionTrigger className="px-4 py-3 hover:no-underline bg-muted/50 hover:bg-muted">
<h2 className="font-semibold text-sm">My Queue</h2>
</AccordionTrigger>
<AccordionContent className="max-h-[40vh] overflow-y-auto pb-0">
<AccordionContent className="max-h-[35vh] overflow-y-auto pb-0">
{queueSection}
</AccordionContent>
</AccordionItem>
{/* Available Participants Section */}
<AccordionItem value="participants" className="border-0">
<AccordionTrigger className="px-4 py-3 hover:no-underline bg-muted/50 hover:bg-muted">
<h2 className="font-semibold text-sm">Available Participants</h2>
{/* Recent Picks Section */}
<AccordionItem value="recent-picks" className="border-0 flex-1 overflow-hidden flex flex-col">
<AccordionTrigger className="px-4 py-3 hover:no-underline bg-muted/50 hover:bg-muted flex-shrink-0">
<h2 className="font-semibold text-sm">Recent Picks</h2>
</AccordionTrigger>
<AccordionContent className="pb-0">
<div className="h-[calc(100vh-400px)] overflow-y-auto">
{participantsSection}
</div>
<AccordionContent className="pb-0 flex-1 overflow-y-auto">
{recentPicksSection}
</AccordionContent>
</AccordionItem>
</Accordion>

View file

@ -50,7 +50,7 @@ export function AvailableParticipantsSection({
onRemoveFromQueue,
}: AvailableParticipantsSectionProps) {
return (
<div className="flex flex-col">
<div className="flex flex-col h-full">
<div className="px-4 pt-4 pb-2 flex-shrink-0">
<div className="space-y-2">
{/* Search Input */}
@ -92,9 +92,8 @@ export function AvailableParticipantsSection({
</div>
{/* Table - This will scroll independently */}
<div className="px-4 pb-4">
<div className="border rounded-lg overflow-hidden">
<div className="overflow-y-auto max-h-[60vh]">
<div className="flex-1 overflow-hidden">
<div className="h-full overflow-y-auto">
<table className="w-full text-sm">
<thead className="bg-muted sticky top-0">
<tr>
@ -269,7 +268,6 @@ export function AvailableParticipantsSection({
)}
</tbody>
</table>
</div>
</div>
</div>
</div>

View file

@ -56,80 +56,20 @@ export function QueueSection({
)}
</div>
{/* Eligibility Status Banner */}
{eligibility && (
<div className="mb-4 p-3 bg-muted/50 rounded-lg border">
<div className="text-sm font-semibold mb-2">Draft Status</div>
<div className="text-xs space-y-1">
<div className="flex justify-between">
<span>Flex Picks:</span>
<span
className={
eligibility.flexPicksUsed >= eligibility.flexPicksAvailable
? "text-red-600 dark:text-red-400 font-semibold"
: "text-green-600 dark:text-green-400"
}
>
{eligibility.flexPicksUsed} / {eligibility.flexPicksAvailable}
</span>
</div>
{Object.entries(eligibility.picksBySport).length > 0 && (
<>
<div className="font-semibold mt-2 mb-1">Picks by Sport:</div>
{Object.entries(eligibility.picksBySport).map(
([sportId, count]) => {
const sportInfo = eligibility.sportAvailability[sportId];
return (
<div
key={sportId}
className="flex justify-between items-center pl-2"
>
<span>{sportInfo?.sportName || sportId}:</span>
<span className="font-mono">{count}</span>
</div>
);
}
)}
</>
)}
{Object.keys(eligibility.ineligibleReasons).length > 0 && (
<>
<div className="font-semibold mt-2 mb-1 text-red-600 dark:text-red-400">
Restrictions:
</div>
{Object.entries(eligibility.ineligibleReasons)
.slice(0, 2)
.map(([sportId, reason]) => {
const sportInfo = eligibility.sportAvailability[sportId];
return (
<div
key={sportId}
className="text-xs text-red-600 dark:text-red-400 pl-2"
>
{sportInfo?.sportName}: {reason}
</div>
);
})}
</>
)}
</div>
</div>
)}
{/* Queue List */}
{queue.length === 0 ? (
<p className="text-muted-foreground text-sm text-center py-8">
Click participants to add to your queue
</p>
) : (
<div className="space-y-2 mb-4">
<div className="space-y-1.5 mb-4">
{queue.map((item, index) => (
<div
key={item.id}
className="flex items-center justify-between p-3 bg-muted rounded-lg"
className="flex items-center justify-between p-2 bg-muted rounded-lg"
>
<div className="flex items-center gap-3">
<Badge variant="default">{index + 1}</Badge>
<div className="flex items-center gap-2">
<Badge variant="default" className="text-xs">{index + 1}</Badge>
<div>
<p className="font-semibold text-sm">
{availableParticipants.find(
@ -139,11 +79,13 @@ export function QueueSection({
</div>
</div>
<Button
variant="destructive"
size="sm"
variant="ghost"
size="icon"
className="h-7 w-7 text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={() => onRemoveFromQueue(item.id)}
title="Remove from queue"
>
Remove
<span className="text-lg">×</span>
</Button>
</div>
))}

View file

@ -0,0 +1,61 @@
import { Badge } from "~/components/ui/badge";
interface SidebarRecentPicksProps {
picks: Array<{
id: string;
pickNumber: number;
round: number;
participant: {
name: string;
};
sport: {
name: string;
};
team: {
name: string;
};
}>;
}
export function SidebarRecentPicks({ picks }: SidebarRecentPicksProps) {
return (
<div className="p-4">
{picks.length === 0 ? (
<p className="text-muted-foreground text-sm text-center py-8">
No picks yet...
</p>
) : (
<div className="space-y-1.5">
{picks
.slice()
.reverse()
.slice(0, 10)
.map((pick) => (
<div
key={pick.id}
className="flex items-center justify-between p-2 bg-muted rounded-lg text-sm"
>
<div className="flex items-center gap-2 min-w-0 flex-1">
<Badge variant="outline" className="text-xs flex-shrink-0">
#{pick.pickNumber}
</Badge>
<div className="min-w-0 flex-1">
<p className="font-semibold truncate">{pick.participant.name}</p>
<p className="text-xs text-muted-foreground truncate">
{pick.sport.name}
</p>
</div>
</div>
<div className="text-right flex-shrink-0 ml-2">
<p className="font-medium text-xs truncate max-w-[100px]">{pick.team.name}</p>
<p className="text-xs text-muted-foreground">
R{pick.round}
</p>
</div>
</div>
))}
</div>
)}
</div>
);
}

View file

@ -123,19 +123,19 @@ export function TeamsDraftedGrid({
return (
<div className="w-full h-full overflow-auto">
<div className="inline-block min-w-full">
<table className="border-collapse border border-border">
<table className="w-full border-collapse">
<thead className="sticky top-0 bg-background z-10">
<tr>
<th className="border border-border p-2 text-left font-semibold min-w-[150px] bg-muted/50">
<th className="border-r border-b border-border p-2 text-left font-semibold min-w-[150px] bg-muted/50">
Sport
</th>
{draftSlots.map((slot) => {
{draftSlots.map((slot, index) => {
const flexUsed = flexPicksByTeam.get(slot.team.id) || 0;
const isLast = index === draftSlots.length - 1;
return (
<th
key={slot.id}
className="border border-border p-2 text-left font-semibold min-w-[180px] bg-muted/50"
className={`border-b border-border p-2 text-left font-semibold min-w-[180px] bg-muted/50 ${!isLast ? 'border-r' : ''}`}
>
<div className="flex flex-col gap-1">
<div className="font-semibold text-sm">
@ -150,28 +150,31 @@ export function TeamsDraftedGrid({
})}
</tr>
</thead>
<tbody>
{sports.map((sport) => (
<tr key={sport.id}>
<td className="border border-border p-2 font-medium bg-muted/30">
{sport.name}
</td>
{draftSlots.map((slot) => {
const teamSportPicks =
picksByTeamAndSport
.get(slot.team.id)
?.get(sport.id) || [];
const hasMultiplePicks = teamSportPicks.length > 1;
<tbody className="border-b border-border">
{sports.map((sport, sportIndex) => {
const isLastRow = sportIndex === sports.length - 1;
return (
<tr key={sport.id}>
<td className={`border-r border-border p-2 font-medium bg-muted/30 ${!isLastRow ? 'border-b' : ''}`}>
{sport.name}
</td>
{draftSlots.map((slot, slotIndex) => {
const teamSportPicks =
picksByTeamAndSport
.get(slot.team.id)
?.get(sport.id) || [];
const hasMultiplePicks = teamSportPicks.length > 1;
const isLastCol = slotIndex === draftSlots.length - 1;
return (
<td
key={`${slot.team.id}-${sport.id}`}
className={`border border-border p-2 ${
hasMultiplePicks
? "bg-accent/30 font-semibold"
: "bg-background"
}`}
>
return (
<td
key={`${slot.team.id}-${sport.id}`}
className={`border-border p-2 ${
hasMultiplePicks
? "bg-accent/30 font-semibold"
: "bg-background"
} ${!isLastRow ? 'border-b' : ''} ${!isLastCol ? 'border-r' : ''}`}
>
{teamSportPicks.length > 0 ? (
<div className="flex flex-col gap-1">
{teamSportPicks.map((pick) => (
@ -195,11 +198,11 @@ export function TeamsDraftedGrid({
</td>
);
})}
</tr>
))}
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
}

View file

@ -5,6 +5,7 @@ import {
Outlet,
Scripts,
ScrollRestoration,
useLocation,
} from "react-router";
import type { Route } from "./+types/root";
@ -62,12 +63,19 @@ export function Layout({ children }: { children: React.ReactNode }) {
}
export default function App({ loaderData }: Route.ComponentProps) {
const location = useLocation();
const isDraftRoute = location.pathname.includes('/draft');
return (
<ClerkProvider loaderData={loaderData}>
<Navbar isAdmin={loaderData?.isAdmin ?? false} />
<main>
{!isDraftRoute && <Navbar isAdmin={loaderData?.isAdmin ?? false} />}
{isDraftRoute ? (
<Outlet />
</main>
) : (
<main>
<Outlet />
</main>
)}
<Toaster />
</ClerkProvider>
);

View file

@ -14,7 +14,7 @@ import { DraftSidebar } from "~/components/DraftSidebar";
import { TeamsDraftedGrid } from "~/components/draft/TeamsDraftedGrid";
import { QueueSection } from "~/components/draft/QueueSection";
import { AvailableParticipantsSection } from "~/components/draft/AvailableParticipantsSection";
import { RecentPicksSection } from "~/components/draft/RecentPicksSection";
import { SidebarRecentPicks } from "~/components/draft/SidebarRecentPicks";
import { DraftGridSection } from "~/components/draft/DraftGridSection";
import { calculateDraftEligibility, getEligibilitySummary } from "~/lib/draft-eligibility";
@ -211,7 +211,7 @@ export default function DraftRoom() {
const stored = localStorage.getItem("draftSidebarCollapsed");
return stored ? JSON.parse(stored) : false;
});
const [activeTab, setActiveTab] = useState<"board" | "picks" | "teams">("board");
const [activeTab, setActiveTab] = useState<"participants" | "board" | "teams">("participants");
// Track autodraft status for all teams
const [autodraftStatus, setAutodraftStatus] = useState<Record<string, boolean>>(() => {
@ -727,10 +727,10 @@ export default function DraftRoom() {
);
return (
<div className="min-h-screen bg-background">
<div className="h-screen bg-background flex flex-col overflow-hidden">
{/* Draft Completion Banner */}
{isDraftComplete && (
<div className="bg-green-500 text-white px-4 py-3 text-center font-semibold">
<div className="bg-green-500 text-white px-4 py-3 text-center font-semibold flex-shrink-0">
🎉 Draft Complete! The season is now active.
{season.league.isPublicDraftBoard && (
<>
@ -851,25 +851,8 @@ export default function DraftRoom() {
}}
/>
}
participantsSection={
<AvailableParticipantsSection
participants={filteredParticipants}
searchQuery={searchQuery}
sportFilter={sportFilter}
hideDrafted={hideDrafted}
uniqueSports={uniqueSports}
draftedParticipantIds={draftedParticipantIds}
queue={queue}
eligibility={eligibility}
canPick={canPick}
hasTeam={!!userTeam}
onSearchChange={setSearchQuery}
onSportFilterChange={setSportFilter}
onHideDraftedChange={setHideDrafted}
onMakePick={handleMakePick}
onAddToQueue={handleAddToQueue}
onRemoveFromQueue={handleRemoveFromQueue}
/>
recentPicksSection={
<SidebarRecentPicks picks={picks} />
}
/>
)}
@ -879,16 +862,39 @@ export default function DraftRoom() {
<Tabs
value={activeTab}
onValueChange={(value) =>
setActiveTab(value as "board" | "picks" | "teams")
setActiveTab(value as "participants" | "board" | "teams")
}
className="h-full flex flex-col"
>
<TabsList className="mx-4 mt-4">
<TabsTrigger value="participants">Available Participants</TabsTrigger>
<TabsTrigger value="board">Draft Board</TabsTrigger>
<TabsTrigger value="picks">Recent Picks</TabsTrigger>
<TabsTrigger value="teams">Teams Drafted</TabsTrigger>
</TabsList>
<TabsContent value="participants" className="flex-1 overflow-hidden m-0">
<div className="h-full">
<AvailableParticipantsSection
participants={filteredParticipants}
searchQuery={searchQuery}
sportFilter={sportFilter}
hideDrafted={hideDrafted}
uniqueSports={uniqueSports}
draftedParticipantIds={draftedParticipantIds}
queue={queue}
eligibility={eligibility}
canPick={canPick}
hasTeam={!!userTeam}
onSearchChange={setSearchQuery}
onSportFilterChange={setSportFilter}
onHideDraftedChange={setHideDrafted}
onMakePick={handleMakePick}
onAddToQueue={handleAddToQueue}
onRemoveFromQueue={handleRemoveFromQueue}
/>
</div>
</TabsContent>
<TabsContent value="board" className="flex-1 overflow-hidden m-0">
<DraftGridSection
draftSlots={draftSlots}
@ -906,12 +912,8 @@ export default function DraftRoom() {
/>
</TabsContent>
<TabsContent value="picks" className="flex-1 overflow-hidden m-0">
<RecentPicksSection picks={picks} />
</TabsContent>
<TabsContent value="teams" className="flex-1 overflow-hidden m-0">
<div className="h-full p-4">
<div className="h-full">
<TeamsDraftedGrid
draftSlots={draftSlots}
picks={picks}