feat: Update DraftSidebar to include recent picks section and refactor layout; enhance AvailableParticipantsSection and QueueSection styling
This commit is contained in:
parent
714ab0f484
commit
c9d3ee73cd
7 changed files with 175 additions and 159 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
61
app/components/draft/SidebarRecentPicks.tsx
Normal file
61
app/components/draft/SidebarRecentPicks.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
14
app/root.tsx
14
app/root.tsx
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue