Mobile Draft fixes.
This commit is contained in:
parent
26cfb04990
commit
272110a934
5 changed files with 229 additions and 111 deletions
|
|
@ -117,6 +117,14 @@ html {
|
|||
animation: pulse-gradient 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes rpf-slide-in {
|
||||
from { transform: translateY(-32px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
.rpf-animate {
|
||||
animation: rpf-slide-in 0.35s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
}
|
||||
|
||||
/* NProgress theme override */
|
||||
#nprogress .bar {
|
||||
background: var(--electric) !important;
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS
|
|||
? "All Sports"
|
||||
: sportFilters.length === 1
|
||||
? sportFilters[0]
|
||||
: `${sportFilters.length} sports`,
|
||||
: `${sportFilters.length} Sports`,
|
||||
[sportFilters]
|
||||
);
|
||||
|
||||
|
|
@ -268,111 +268,115 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS
|
|||
</div>
|
||||
)}
|
||||
<div className="px-4 pt-4 pb-2 flex-shrink-0">
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search participants..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="flex-1 min-w-0 px-3 py-2 border rounded-md text-base md:text-sm bg-background text-foreground"
|
||||
/>
|
||||
|
||||
<div className="md:hidden shrink-0">
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
aria-label={triggerAriaLabel}
|
||||
className="justify-between text-base font-normal"
|
||||
>
|
||||
<span className="truncate">{triggerText}</span>
|
||||
<ChevronDown className="h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="bottom" className="max-h-[70vh] pt-12" aria-describedby={undefined}>
|
||||
<SheetTitle className="sr-only">Filter by sport</SheetTitle>
|
||||
<SportFilterContent
|
||||
sportsForDropdown={sportsForDropdown}
|
||||
sportFilterSet={sportFilterSet}
|
||||
hideCompletedSports={hideCompletedSports}
|
||||
hasTeam={hasTeam}
|
||||
variant="sheet"
|
||||
onToggleSport={handleToggleSport}
|
||||
onHideCompletedSportsChange={onHideCompletedSportsChange}
|
||||
/>
|
||||
<SheetFooter className="px-4 pb-8 flex-row gap-2">
|
||||
{hasActiveFilters && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={handleReset}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
)}
|
||||
<SheetClose asChild>
|
||||
<Button className="flex-1">Done</Button>
|
||||
</SheetClose>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
|
||||
<div className="hidden md:block shrink-0">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
aria-label={triggerAriaLabel}
|
||||
className="w-[160px] justify-between text-sm font-normal"
|
||||
>
|
||||
<span className="truncate">{triggerText}</span>
|
||||
<ChevronDown className="h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-72 p-0" align="start">
|
||||
{hasActiveFilters && (
|
||||
<div className="flex justify-end px-2 pt-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-auto px-2 py-1 text-xs"
|
||||
onClick={handleReset}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<SportFilterContent
|
||||
sportsForDropdown={sportsForDropdown}
|
||||
sportFilterSet={sportFilterSet}
|
||||
hideCompletedSports={hideCompletedSports}
|
||||
hasTeam={hasTeam}
|
||||
variant="popover"
|
||||
onToggleSport={handleToggleSport}
|
||||
onHideCompletedSportsChange={onHideCompletedSportsChange}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer whitespace-nowrap px-3 py-2 border rounded-md shrink-0">
|
||||
<Checkbox
|
||||
checked={!hideDrafted}
|
||||
onCheckedChange={(checked) => onHideDraftedChange(checked === false)}
|
||||
<div className="flex flex-col gap-2 md:flex-row md:flex-wrap md:items-center">
|
||||
<div className="flex gap-2 md:contents">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search participants..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="flex-1 min-w-0 px-3 py-2 h-9 border rounded-md text-base md:text-sm bg-background text-foreground"
|
||||
/>
|
||||
<span>Show Drafted</span>
|
||||
</label>
|
||||
|
||||
{hasTeam && eligibility && (
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer whitespace-nowrap px-3 py-2 border rounded-md shrink-0">
|
||||
<div className="md:hidden shrink-0">
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
aria-label={triggerAriaLabel}
|
||||
className="justify-between text-base font-normal"
|
||||
>
|
||||
<span className="truncate">{triggerText}</span>
|
||||
<ChevronDown className="h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="bottom" className="max-h-[70vh] pt-12" aria-describedby={undefined}>
|
||||
<SheetTitle className="sr-only">Filter by sport</SheetTitle>
|
||||
<SportFilterContent
|
||||
sportsForDropdown={sportsForDropdown}
|
||||
sportFilterSet={sportFilterSet}
|
||||
hideCompletedSports={hideCompletedSports}
|
||||
hasTeam={hasTeam}
|
||||
variant="sheet"
|
||||
onToggleSport={handleToggleSport}
|
||||
onHideCompletedSportsChange={onHideCompletedSportsChange}
|
||||
/>
|
||||
<SheetFooter className="px-4 pb-8 flex-row gap-2">
|
||||
{hasActiveFilters && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={handleReset}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
)}
|
||||
<SheetClose asChild>
|
||||
<Button className="flex-1">Done</Button>
|
||||
</SheetClose>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
|
||||
<div className="hidden md:block shrink-0">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
aria-label={triggerAriaLabel}
|
||||
className="w-[160px] justify-between text-sm font-normal"
|
||||
>
|
||||
<span className="truncate">{triggerText}</span>
|
||||
<ChevronDown className="h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-72 p-0" align="start">
|
||||
{hasActiveFilters && (
|
||||
<div className="flex justify-end px-2 pt-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-auto px-2 py-1 text-xs"
|
||||
onClick={handleReset}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<SportFilterContent
|
||||
sportsForDropdown={sportsForDropdown}
|
||||
sportFilterSet={sportFilterSet}
|
||||
hideCompletedSports={hideCompletedSports}
|
||||
hasTeam={hasTeam}
|
||||
variant="popover"
|
||||
onToggleSport={handleToggleSport}
|
||||
onHideCompletedSportsChange={onHideCompletedSportsChange}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 flex-wrap md:contents">
|
||||
<label className="flex flex-1 items-center gap-2 text-sm cursor-pointer px-3 py-2 border rounded-md md:shrink-0 md:flex-none">
|
||||
<Checkbox
|
||||
checked={!hideIneligible}
|
||||
onCheckedChange={(checked) => onHideIneligibleChange(checked === false)}
|
||||
checked={!hideDrafted}
|
||||
onCheckedChange={(checked) => onHideDraftedChange(checked === false)}
|
||||
/>
|
||||
<span>Show Ineligible</span>
|
||||
<span>Show Drafted</span>
|
||||
</label>
|
||||
)}
|
||||
|
||||
{hasTeam && eligibility && (
|
||||
<label className="flex flex-1 items-center gap-2 text-sm cursor-pointer px-3 py-2 border rounded-md md:shrink-0 md:flex-none">
|
||||
<Checkbox
|
||||
checked={!hideIneligible}
|
||||
onCheckedChange={(checked) => onHideIneligibleChange(checked === false)}
|
||||
/>
|
||||
<span>Show Ineligible</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -78,6 +78,8 @@ export const MiniDraftGrid = memo(function MiniDraftGrid({
|
|||
const prevRoundRef = useRef(currentRound);
|
||||
const firstRowRef = useRef<HTMLDivElement>(null);
|
||||
const [rowHeight, setRowHeight] = useState(0);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const currentCellRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (firstRowRef.current) {
|
||||
|
|
@ -85,6 +87,16 @@ export const MiniDraftGrid = memo(function MiniDraftGrid({
|
|||
}
|
||||
}, [draftGrid]);
|
||||
|
||||
useEffect(() => {
|
||||
const container = scrollContainerRef.current;
|
||||
const cell = currentCellRef.current;
|
||||
if (!container || !cell) return;
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const cellRect = cell.getBoundingClientRect();
|
||||
const scrollLeft = container.scrollLeft + cellRect.left - containerRect.left - (containerRect.width - cellRect.width) / 2;
|
||||
container.scrollTo({ left: Math.max(0, scrollLeft), behavior: "smooth" });
|
||||
}, [currentPick]);
|
||||
|
||||
useEffect(() => {
|
||||
const prev = prevRoundRef.current;
|
||||
prevRoundRef.current = currentRound;
|
||||
|
|
@ -132,7 +144,7 @@ export const MiniDraftGrid = memo(function MiniDraftGrid({
|
|||
: [displayedIndices[0], displayedIndices[1]];
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<div ref={scrollContainerRef} className="overflow-x-auto">
|
||||
<div className="inline-block min-w-full">
|
||||
{/* Team header */}
|
||||
<div className="flex gap-1.5 mb-1.5">
|
||||
|
|
@ -191,6 +203,7 @@ export const MiniDraftGrid = memo(function MiniDraftGrid({
|
|||
<ContextMenu key={cell.pickNumber}>
|
||||
<ContextMenuTrigger asChild>
|
||||
<DraftPickCell
|
||||
ref={isCurrent ? currentCellRef : undefined}
|
||||
pickNumber={cell.pickNumber}
|
||||
round={cell.round}
|
||||
pickInRound={cell.pickInRound}
|
||||
|
|
@ -254,6 +267,7 @@ export const MiniDraftGrid = memo(function MiniDraftGrid({
|
|||
return (
|
||||
<DraftPickCell
|
||||
key={cell.pickNumber}
|
||||
ref={isCurrent ? currentCellRef : undefined}
|
||||
pickNumber={cell.pickNumber}
|
||||
round={cell.round}
|
||||
pickInRound={cell.pickInRound}
|
||||
|
|
|
|||
56
app/components/draft/RecentPicksFeed.tsx
Normal file
56
app/components/draft/RecentPicksFeed.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { memo, useEffect, useRef, useState } from "react";
|
||||
|
||||
type Pick = {
|
||||
id: string;
|
||||
pickNumber: number;
|
||||
participant: { name: string };
|
||||
sport: { name: string };
|
||||
team: { name: string };
|
||||
};
|
||||
|
||||
export const RecentPicksFeed = memo(function RecentPicksFeed({ picks }: { picks: Pick[] }) {
|
||||
const recentPicks = picks.slice(-3).reverse();
|
||||
const prevNewestIdRef = useRef<string | undefined>(picks.at(-1)?.id);
|
||||
const [animatingId, setAnimatingId] = useState<string | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const newest = recentPicks[0];
|
||||
if (newest && newest.id !== prevNewestIdRef.current) {
|
||||
prevNewestIdRef.current = newest.id;
|
||||
setAnimatingId(newest.id);
|
||||
const t = setTimeout(() => setAnimatingId(undefined), 450);
|
||||
return () => clearTimeout(t);
|
||||
}
|
||||
}, [recentPicks]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1 px-3 pt-2 pb-1 overflow-hidden">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/60 mb-0.5">Latest Picks</p>
|
||||
{picks.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground py-1">No picks yet</p>
|
||||
) : (
|
||||
recentPicks.map((pick, index) => (
|
||||
<div
|
||||
key={pick.id}
|
||||
className={`w-full ${index === 0 && animatingId === pick.id ? "rpf-animate" : ""} bg-muted rounded-lg px-3 py-1.5 flex items-center gap-2`}
|
||||
>
|
||||
<span className="text-xs font-bold text-muted-foreground shrink-0 tabular-nums">
|
||||
#{pick.pickNumber}
|
||||
</span>
|
||||
<div className="flex flex-col flex-1 min-w-0">
|
||||
<span className="text-sm font-medium truncate">
|
||||
{pick.participant.name}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground/70 truncate">
|
||||
{pick.sport.name}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground truncate max-w-[90px] text-right shrink-0">
|
||||
{pick.team.name}
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -18,6 +18,7 @@ import { DraftSummaryView } from "~/components/draft/DraftSummaryView";
|
|||
import { QueueSection } from "~/components/draft/QueueSection";
|
||||
import { AvailableParticipantsSection } from "~/components/draft/AvailableParticipantsSection";
|
||||
import { SidebarRecentPicks } from "~/components/draft/SidebarRecentPicks";
|
||||
import { RecentPicksFeed } from "~/components/draft/RecentPicksFeed";
|
||||
import { DraftGridSection } from "~/components/draft/DraftGridSection";
|
||||
import { ConnectionOverlay } from "~/components/draft/ConnectionOverlay";
|
||||
import { AuthRecoveryOverlay } from "~/components/draft/AuthRecoveryOverlay";
|
||||
|
|
@ -46,7 +47,7 @@ type AutodraftStatusEntry = {
|
|||
};
|
||||
|
||||
const MOBILE_TABS_BASE = [
|
||||
{ id: "available" as const, label: "Available", Icon: Users },
|
||||
{ id: "available" as const, label: "Participants", Icon: Users },
|
||||
{ id: "board" as const, label: "Board", Icon: LayoutGrid },
|
||||
{ id: "teams" as const, label: "Teams", Icon: ListChecks },
|
||||
{ id: "controls" as const, label: "Controls", Icon: Settings },
|
||||
|
|
@ -544,6 +545,7 @@ export default function DraftRoom() {
|
|||
!userTeam && isCommissioner ? "board" : "available"
|
||||
);
|
||||
const [teamsSubTab, setTeamsSubTab] = useState<"rosters" | "summary">("rosters");
|
||||
const [boardSubTab, setBoardSubTab] = useState<"board" | "pick-list">("board");
|
||||
const isMobile = useMediaQuery("(max-width: 767px)");
|
||||
|
||||
// Track autodraft status for all teams
|
||||
|
|
@ -1658,7 +1660,7 @@ export default function DraftRoom() {
|
|||
<img src="/logomark.svg" alt="Brackt" className="h-8" />
|
||||
</Link>
|
||||
<h1 className="text-lg md:text-xl font-bold">
|
||||
{season.league.name} Draft Room
|
||||
Draft Room
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
|
|
@ -1765,7 +1767,14 @@ export default function DraftRoom() {
|
|||
{isMobile ? (
|
||||
<div className="flex h-full overflow-hidden flex-col">
|
||||
{mobileTab === "available" && (
|
||||
<AvailableParticipantsSection {...availableParticipantsSectionProps} />
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
<div className="flex-shrink-0">
|
||||
<RecentPicksFeed picks={picks} />
|
||||
</div>
|
||||
<div className="flex-1 min-h-0 overflow-hidden">
|
||||
<AvailableParticipantsSection {...availableParticipantsSectionProps} miniDraftGrid={undefined} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{mobileTab === "queue" && (
|
||||
<div className="h-full overflow-y-auto">
|
||||
|
|
@ -1779,7 +1788,39 @@ export default function DraftRoom() {
|
|||
</div>
|
||||
)}
|
||||
{mobileTab === "board" && (
|
||||
<DraftGridSection {...draftGridSectionProps} />
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
<div className="flex-shrink-0 flex gap-1 p-2 border-b">
|
||||
<button
|
||||
onClick={() => setBoardSubTab("board")}
|
||||
className={`flex-1 py-1.5 text-sm font-medium rounded transition-colors ${
|
||||
boardSubTab === "board"
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
Board
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setBoardSubTab("pick-list")}
|
||||
className={`flex-1 py-1.5 text-sm font-medium rounded transition-colors ${
|
||||
boardSubTab === "pick-list"
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
Pick List
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{boardSubTab !== "pick-list" ? (
|
||||
<DraftGridSection {...draftGridSectionProps} />
|
||||
) : (
|
||||
<div className="h-full overflow-y-auto p-4">
|
||||
<SidebarRecentPicks picks={picks} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{mobileTab === "teams" && (
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
|
|
@ -1833,11 +1874,6 @@ export default function DraftRoom() {
|
|||
)
|
||||
)}
|
||||
|
||||
<section>
|
||||
<h2 className="font-semibold text-sm mb-2">Recent Picks</h2>
|
||||
<SidebarRecentPicks picks={picks} />
|
||||
</section>
|
||||
|
||||
<Button variant="outline" asChild className="w-full min-h-[48px]">
|
||||
<Link to={`/leagues/${season.leagueId}`}>Exit Draft Room</Link>
|
||||
</Button>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue