feat: Integrate accordion component for sidebar and update autodraft settings to sync with props
This commit is contained in:
parent
2cd4096e70
commit
32980428f2
8 changed files with 162 additions and 117 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
64
app/components/ui/accordion.tsx
Normal file
64
app/components/ui/accordion.tsx
Normal 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 }
|
||||
|
|
@ -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
32
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue