feat: Integrate accordion component for sidebar and update autodraft settings to sync with props

This commit is contained in:
Chris Parsons 2025-10-25 10:04:21 -07:00
parent 2cd4096e70
commit 32980428f2
8 changed files with 162 additions and 117 deletions

View file

@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import { Switch } from "~/components/ui/switch";
import { RadioGroup, RadioGroupItem } from "~/components/ui/radio-group";
import { Label } from "~/components/ui/label";
@ -24,6 +24,15 @@ export function AutodraftSettings({
const [localMode, setLocalMode] = useState<"next_pick" | "while_on">(mode);
const [isUpdating, setIsUpdating] = useState(false);
// Sync local state with props when they change (from socket events)
useEffect(() => {
setLocalEnabled(isEnabled);
}, [isEnabled]);
useEffect(() => {
setLocalMode(mode);
}, [mode]);
const handleToggle = async (checked: boolean) => {
setLocalEnabled(checked);
setIsUpdating(true);

View file

@ -1,7 +1,12 @@
import { useState, useEffect } from "react";
import type { ReactNode } from "react";
import { ChevronLeft, ChevronRight, ChevronDown, ChevronUp } from "lucide-react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { Button } from "~/components/ui/button";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "~/components/ui/accordion";
import { cn } from "~/lib/utils";
interface DraftSidebarProps {
@ -19,59 +24,6 @@ export function DraftSidebar({
participantsSection,
className,
}: DraftSidebarProps) {
// Track which sections are expanded
// At least one must be expanded at all times
const [queueExpanded, setQueueExpanded] = useState(() => {
if (typeof window === "undefined") return true;
const stored = localStorage.getItem("draftSidebarQueueExpanded");
return stored !== null ? JSON.parse(stored) : true;
});
const [participantsExpanded, setParticipantsExpanded] = useState(() => {
if (typeof window === "undefined") return true;
const stored = localStorage.getItem("draftSidebarParticipantsExpanded");
return stored !== null ? JSON.parse(stored) : true;
});
// Persist section states to localStorage
useEffect(() => {
if (typeof window !== "undefined") {
localStorage.setItem(
"draftSidebarQueueExpanded",
JSON.stringify(queueExpanded)
);
}
}, [queueExpanded]);
useEffect(() => {
if (typeof window !== "undefined") {
localStorage.setItem(
"draftSidebarParticipantsExpanded",
JSON.stringify(participantsExpanded)
);
}
}, [participantsExpanded]);
const toggleQueueSection = () => {
// Prevent collapsing if it's the only expanded section
if (queueExpanded && !participantsExpanded) {
return;
}
setQueueExpanded(!queueExpanded);
};
const toggleParticipantsSection = () => {
// Prevent collapsing if it's the only expanded section
if (participantsExpanded && !queueExpanded) {
return;
}
setParticipantsExpanded(!participantsExpanded);
};
// Calculate flex sizes for sections
const queueFlexSize = queueExpanded && participantsExpanded ? "1" : queueExpanded ? "1" : "0";
const participantsFlexSize = queueExpanded && participantsExpanded ? "1" : participantsExpanded ? "1" : "0";
if (collapsed) {
return (
<div
@ -112,62 +64,48 @@ export function DraftSidebar({
className
)}
>
{/* Collapse button */}
<div className="absolute top-4 right-2 z-20">
<Button
variant="ghost"
size="icon"
onClick={() => onCollapsedChange(true)}
aria-label="Collapse sidebar"
<Accordion
type="multiple"
defaultValue={["queue", "participants"]}
className="flex-1 overflow-hidden"
>
<ChevronLeft className="h-4 w-4" />
</Button>
</div>
{/* Queue Section */}
<AccordionItem value="queue" className="border-b">
<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">
{queueSection}
</AccordionContent>
</AccordionItem>
{/* Queue Section */}
<div
className="flex flex-col border-b border-border overflow-hidden"
style={{ flex: queueFlexSize }}
>
<button
onClick={toggleQueueSection}
className="flex items-center justify-between px-4 py-3 pr-12 bg-muted/50 hover:bg-muted transition-colors border-b border-border"
disabled={queueExpanded && !participantsExpanded}
>
<h2 className="font-semibold text-sm">My Queue</h2>
{queueExpanded ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</button>
{queueExpanded && (
<div className="flex-1 overflow-y-auto">{queueSection}</div>
)}
</div>
{/* 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>
</AccordionTrigger>
<AccordionContent className="pb-0">
<div className="h-[calc(100vh-400px)] overflow-y-auto">
{participantsSection}
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
{/* Available Participants Section */}
<div
className="flex flex-col overflow-hidden"
style={{ flex: participantsFlexSize }}
>
<button
onClick={toggleParticipantsSection}
className="flex items-center justify-between px-4 py-3 bg-muted/50 hover:bg-muted transition-colors border-b border-border"
disabled={participantsExpanded && !queueExpanded}
>
<h2 className="font-semibold text-sm">Available Participants</h2>
{participantsExpanded ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</button>
{participantsExpanded && (
<div className="flex-1 overflow-y-auto">{participantsSection}</div>
)}
{/* Collapse button at bottom */}
<div className="border-t border-border p-2 flex-shrink-0">
<Button
variant="ghost"
size="sm"
onClick={() => onCollapsedChange(true)}
className="w-full flex items-center justify-center gap-2"
aria-label="Collapse sidebar"
>
<ChevronLeft className="h-4 w-4" />
<span className="text-sm">Hide Sidebar</span>
</Button>
</div>
</div>
</div>
</>
);
}

View file

@ -50,10 +50,8 @@ export function AvailableParticipantsSection({
onRemoveFromQueue,
}: AvailableParticipantsSectionProps) {
return (
<div className="flex flex-col h-full">
<div className="mb-4 flex-shrink-0">
<h3 className="text-lg font-semibold mb-3">Available Participants</h3>
<div className="flex flex-col">
<div className="px-4 pt-4 pb-2 flex-shrink-0">
<div className="space-y-2">
{/* Search Input */}
<input
@ -93,9 +91,10 @@ export function AvailableParticipantsSection({
</div>
</div>
{/* Table */}
<div className="border rounded-lg overflow-hidden flex-1 flex flex-col min-h-0">
<div className="overflow-y-auto flex-1">
{/* 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]">
<table className="w-full text-sm">
<thead className="bg-muted sticky top-0">
<tr>
@ -270,6 +269,7 @@ export function AvailableParticipantsSection({
)}
</tbody>
</table>
</div>
</div>
</div>
</div>

View file

@ -46,7 +46,7 @@ export function QueueSection({
onAutodraftUpdate,
}: QueueSectionProps) {
return (
<>
<div className="p-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">Queue ({queue.length})</h3>
{queue.length > 0 && (
@ -159,6 +159,6 @@ export function QueueSection({
isMyTurn={isMyTurn}
onUpdate={onAutodraftUpdate}
/>
</>
</div>
);
}

View file

@ -0,0 +1,64 @@
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDownIcon } from "lucide-react"
import { cn } from "app/lib/utils"
function Accordion({
...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
}
function AccordionItem({
className,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn("border-b last:border-b-0", className)}
{...props}
/>
)
}
function AccordionTrigger({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
)
}
function AccordionContent({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...props}
>
<div className={cn("pt-0 pb-4", className)}>{children}</div>
</AccordionPrimitive.Content>
)
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

View file

@ -26,6 +26,7 @@ export default [
route("api/draft/resume", "routes/api/draft.resume.ts"),
route("api/draft/force-autopick", "routes/api/draft.force-autopick.ts"),
route("api/draft/force-manual-pick", "routes/api/draft.force-manual-pick.ts"),
route("api/autodraft/update", "routes/api/autodraft.update.ts"),
route("user-profile", "routes/user-profile.tsx"),
route("how-to-play", "routes/how-to-play.tsx"),
route("test-socket", "routes/test-socket.tsx"),

32
package-lock.json generated
View file

@ -7,6 +7,7 @@
"name": "brackt.com",
"dependencies": {
"@clerk/react-router": "^2.1.0",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12",
@ -2600,6 +2601,37 @@
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-accordion": {
"version": "1.2.12",
"resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz",
"integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-collapsible": "1.1.12",
"@radix-ui/react-collection": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-alert-dialog": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz",

View file

@ -22,6 +22,7 @@
},
"dependencies": {
"@clerk/react-router": "^2.1.0",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12",