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 { Switch } from "~/components/ui/switch";
import { RadioGroup, RadioGroupItem } from "~/components/ui/radio-group"; import { RadioGroup, RadioGroupItem } from "~/components/ui/radio-group";
import { Label } from "~/components/ui/label"; import { Label } from "~/components/ui/label";
@ -24,6 +24,15 @@ export function AutodraftSettings({
const [localMode, setLocalMode] = useState<"next_pick" | "while_on">(mode); const [localMode, setLocalMode] = useState<"next_pick" | "while_on">(mode);
const [isUpdating, setIsUpdating] = useState(false); 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) => { const handleToggle = async (checked: boolean) => {
setLocalEnabled(checked); setLocalEnabled(checked);
setIsUpdating(true); setIsUpdating(true);

View file

@ -1,7 +1,12 @@
import { useState, useEffect } from "react";
import type { ReactNode } 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 { Button } from "~/components/ui/button";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "~/components/ui/accordion";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
interface DraftSidebarProps { interface DraftSidebarProps {
@ -19,59 +24,6 @@ export function DraftSidebar({
participantsSection, participantsSection,
className, className,
}: DraftSidebarProps) { }: 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) { if (collapsed) {
return ( return (
<div <div
@ -112,62 +64,48 @@ export function DraftSidebar({
className className
)} )}
> >
{/* Collapse button */} <Accordion
<div className="absolute top-4 right-2 z-20"> type="multiple"
<Button defaultValue={["queue", "participants"]}
variant="ghost" className="flex-1 overflow-hidden"
size="icon"
onClick={() => onCollapsedChange(true)}
aria-label="Collapse sidebar"
> >
<ChevronLeft className="h-4 w-4" /> {/* Queue Section */}
</Button> <AccordionItem value="queue" className="border-b">
</div> <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 */} {/* Available Participants Section */}
<div <AccordionItem value="participants" className="border-0">
className="flex flex-col border-b border-border overflow-hidden" <AccordionTrigger className="px-4 py-3 hover:no-underline bg-muted/50 hover:bg-muted">
style={{ flex: queueFlexSize }} <h2 className="font-semibold text-sm">Available Participants</h2>
> </AccordionTrigger>
<button <AccordionContent className="pb-0">
onClick={toggleQueueSection} <div className="h-[calc(100vh-400px)] overflow-y-auto">
className="flex items-center justify-between px-4 py-3 pr-12 bg-muted/50 hover:bg-muted transition-colors border-b border-border" {participantsSection}
disabled={queueExpanded && !participantsExpanded} </div>
> </AccordionContent>
<h2 className="font-semibold text-sm">My Queue</h2> </AccordionItem>
{queueExpanded ? ( </Accordion>
<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 */} {/* Collapse button at bottom */}
<div <div className="border-t border-border p-2 flex-shrink-0">
className="flex flex-col overflow-hidden" <Button
style={{ flex: participantsFlexSize }} variant="ghost"
> size="sm"
<button onClick={() => onCollapsedChange(true)}
onClick={toggleParticipantsSection} className="w-full flex items-center justify-center gap-2"
className="flex items-center justify-between px-4 py-3 bg-muted/50 hover:bg-muted transition-colors border-b border-border" aria-label="Collapse sidebar"
disabled={participantsExpanded && !queueExpanded} >
> <ChevronLeft className="h-4 w-4" />
<h2 className="font-semibold text-sm">Available Participants</h2> <span className="text-sm">Hide Sidebar</span>
{participantsExpanded ? ( </Button>
<ChevronUp className="h-4 w-4" /> </div>
) : (
<ChevronDown className="h-4 w-4" />
)}
</button>
{participantsExpanded && (
<div className="flex-1 overflow-y-auto">{participantsSection}</div>
)}
</div> </div>
</div>
</> </>
); );
} }

View file

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

View file

@ -46,7 +46,7 @@ export function QueueSection({
onAutodraftUpdate, onAutodraftUpdate,
}: QueueSectionProps) { }: QueueSectionProps) {
return ( return (
<> <div className="p-4">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">Queue ({queue.length})</h3> <h3 className="text-lg font-semibold">Queue ({queue.length})</h3>
{queue.length > 0 && ( {queue.length > 0 && (
@ -159,6 +159,6 @@ export function QueueSection({
isMyTurn={isMyTurn} isMyTurn={isMyTurn}
onUpdate={onAutodraftUpdate} 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/resume", "routes/api/draft.resume.ts"),
route("api/draft/force-autopick", "routes/api/draft.force-autopick.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/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("user-profile", "routes/user-profile.tsx"),
route("how-to-play", "routes/how-to-play.tsx"), route("how-to-play", "routes/how-to-play.tsx"),
route("test-socket", "routes/test-socket.tsx"), route("test-socket", "routes/test-socket.tsx"),

32
package-lock.json generated
View file

@ -7,6 +7,7 @@
"name": "brackt.com", "name": "brackt.com",
"dependencies": { "dependencies": {
"@clerk/react-router": "^2.1.0", "@clerk/react-router": "^2.1.0",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-collapsible": "^1.1.12",
@ -2600,6 +2601,37 @@
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT" "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": { "node_modules/@radix-ui/react-alert-dialog": {
"version": "1.1.15", "version": "1.1.15",
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz",

View file

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