feat: add search, filtering and EV sorting to draft participant list
This commit is contained in:
parent
d0ae694283
commit
83e40bd59f
1 changed files with 185 additions and 33 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { useLoaderData, Link } from "react-router";
|
||||
import { eq, and, asc, notInArray } from "drizzle-orm";
|
||||
import { eq, and, asc, desc, notInArray, inArray } from "drizzle-orm";
|
||||
import { database } from "~/database/context";
|
||||
import * as schema from "~/database/schema";
|
||||
import { useDraftSocket } from "~/hooks/useDraftSocket";
|
||||
|
|
@ -84,26 +84,41 @@ export async function loader(args: any) {
|
|||
.where(eq(schema.draftPicks.seasonId, seasonId))
|
||||
.orderBy(asc(schema.draftPicks.pickNumber));
|
||||
|
||||
// Get available participants (not yet picked)
|
||||
// Get sports seasons for this season
|
||||
const seasonSports = await db.query.seasonSports.findMany({
|
||||
where: eq(schema.seasonSports.seasonId, seasonId),
|
||||
});
|
||||
|
||||
const sportsSeasonIds = seasonSports.map((ss) => ss.sportsSeasonId);
|
||||
|
||||
// Get available participants (not yet picked) - sorted by EV desc, then name
|
||||
const pickedParticipantIds = draftPicks.map((p) => p.participant.id);
|
||||
const availableParticipants = await db
|
||||
.select({
|
||||
id: schema.participants.id,
|
||||
name: schema.participants.name,
|
||||
sport: schema.sports,
|
||||
})
|
||||
.from(schema.participants)
|
||||
.innerJoin(schema.sportsSeasons, eq(schema.participants.sportsSeasonId, schema.sportsSeasons.id))
|
||||
.innerJoin(schema.sports, eq(schema.sportsSeasons.sportId, schema.sports.id))
|
||||
.where(
|
||||
and(
|
||||
eq(schema.participants.sportsSeasonId, season.templateId!),
|
||||
pickedParticipantIds.length > 0
|
||||
? notInArray(schema.participants.id, pickedParticipantIds)
|
||||
: undefined
|
||||
|
||||
let availableParticipants: any[] = [];
|
||||
|
||||
if (sportsSeasonIds.length > 0) {
|
||||
availableParticipants = await db
|
||||
.select({
|
||||
id: schema.participants.id,
|
||||
name: schema.participants.name,
|
||||
expectedValue: schema.participants.expectedValue,
|
||||
sport: schema.sports,
|
||||
})
|
||||
.from(schema.participants)
|
||||
.innerJoin(schema.sportsSeasons, eq(schema.participants.sportsSeasonId, schema.sportsSeasons.id))
|
||||
.innerJoin(schema.sports, eq(schema.sportsSeasons.sportId, schema.sports.id))
|
||||
.where(
|
||||
and(
|
||||
sportsSeasonIds.length === 1
|
||||
? eq(schema.participants.sportsSeasonId, sportsSeasonIds[0])
|
||||
: inArray(schema.participants.sportsSeasonId, sportsSeasonIds),
|
||||
pickedParticipantIds.length > 0
|
||||
? notInArray(schema.participants.id, pickedParticipantIds)
|
||||
: undefined
|
||||
)
|
||||
)
|
||||
)
|
||||
.orderBy(asc(schema.participants.name));
|
||||
.orderBy(desc(schema.participants.expectedValue), asc(schema.participants.name));
|
||||
}
|
||||
|
||||
// Load user's team queue if they have a team
|
||||
const userQueue = userTeam ? await getTeamQueue(userTeam.id) : [];
|
||||
|
|
@ -126,6 +141,9 @@ export default function DraftRoom() {
|
|||
const { isConnected, on, off } = useDraftSocket(season.id);
|
||||
const [picks, setPicks] = useState(draftPicks);
|
||||
const [currentPick, setCurrentPick] = useState(season.currentPickNumber || 1);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [hideDrafted, setHideDrafted] = useState(true);
|
||||
const [sportFilter, setSportFilter] = useState<string>("all");
|
||||
|
||||
// Listen for new picks from other users
|
||||
useEffect(() => {
|
||||
|
|
@ -180,6 +198,51 @@ export default function DraftRoom() {
|
|||
|
||||
const draftGrid = generateDraftGrid();
|
||||
|
||||
// Get drafted participant IDs for filtering
|
||||
const draftedParticipantIds = new Set(picks.map((p: any) => p.participant.id));
|
||||
|
||||
// Get unique sports for filter dropdown
|
||||
const uniqueSports = Array.from(
|
||||
new Set(availableParticipants.map((p: any) => p.sport.name))
|
||||
).sort();
|
||||
|
||||
// Get all participants (including drafted) for the show/hide drafted toggle
|
||||
const allParticipants = [...availableParticipants];
|
||||
|
||||
// Add drafted participants if we're showing them
|
||||
if (!hideDrafted) {
|
||||
picks.forEach((pick: any) => {
|
||||
if (!allParticipants.find((p: any) => p.id === pick.participant.id)) {
|
||||
allParticipants.push({
|
||||
id: pick.participant.id,
|
||||
name: pick.participant.name,
|
||||
expectedValue: pick.participant.expectedValue || 0,
|
||||
sport: pick.sport,
|
||||
});
|
||||
}
|
||||
});
|
||||
// Re-sort after adding drafted players
|
||||
allParticipants.sort((a, b) => {
|
||||
if (b.expectedValue !== a.expectedValue) {
|
||||
return b.expectedValue - a.expectedValue;
|
||||
}
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
|
||||
// Filter participants based on search, sport, and drafted status
|
||||
const filteredParticipants = allParticipants.filter((participant: any) => {
|
||||
// Search filter
|
||||
if (searchQuery && !participant.name.toLowerCase().includes(searchQuery.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
// Sport filter
|
||||
if (sportFilter !== "all" && participant.sport.name !== sportFilter) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Standalone Header - No Main Nav */}
|
||||
|
|
@ -341,21 +404,110 @@ export default function DraftRoom() {
|
|||
{/* Right Column: Available Participants */}
|
||||
<div>
|
||||
<Card className="p-4">
|
||||
<h2 className="text-xl font-semibold mb-4">
|
||||
Available Participants ({availableParticipants.length})
|
||||
</h2>
|
||||
<div className="space-y-2 max-h-[600px] overflow-y-auto">
|
||||
{availableParticipants.map((participant: any) => (
|
||||
<div
|
||||
key={participant.id}
|
||||
className="p-3 bg-gray-50 rounded-lg hover:bg-gray-100 cursor-pointer transition-colors"
|
||||
>
|
||||
<p className="font-semibold">{participant.name}</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
{participant.sport.name}
|
||||
</p>
|
||||
<div className="mb-4">
|
||||
<h2 className="text-xl font-semibold mb-3">
|
||||
Available Participants ({filteredParticipants.length})
|
||||
</h2>
|
||||
|
||||
<div className="space-y-2">
|
||||
{/* Search Input */}
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search participants..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full px-3 py-2 border rounded-md text-sm"
|
||||
/>
|
||||
|
||||
<div className="flex gap-3">
|
||||
{/* Sport Filter Dropdown */}
|
||||
<select
|
||||
value={sportFilter}
|
||||
onChange={(e) => setSportFilter(e.target.value)}
|
||||
className="flex-1 px-3 py-2 border rounded-md text-sm"
|
||||
>
|
||||
<option value="all">All Sports</option>
|
||||
{uniqueSports.map((sport) => (
|
||||
<option key={sport} value={sport}>
|
||||
{sport}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{/* Show/Hide Drafted Toggle */}
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer whitespace-nowrap px-3 py-2 border rounded-md">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!hideDrafted}
|
||||
onChange={(e) => setHideDrafted(!e.target.checked)}
|
||||
className="rounded"
|
||||
/>
|
||||
<span>Show Drafted</span>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<div className="max-h-[500px] overflow-y-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-muted sticky top-0">
|
||||
<tr>
|
||||
<th className="text-left p-3 font-semibold">Participant</th>
|
||||
<th className="text-left p-3 font-semibold">Sport</th>
|
||||
<th className="text-right p-3 font-semibold">EV</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredParticipants.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={3} className="text-center py-8 text-muted-foreground">
|
||||
No participants found
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredParticipants.map((participant: any) => {
|
||||
const isDrafted = draftedParticipantIds.has(participant.id);
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={participant.id}
|
||||
className={`border-t transition-colors ${
|
||||
isDrafted
|
||||
? "bg-muted/50 opacity-60 cursor-not-allowed"
|
||||
: "hover:bg-muted/50 cursor-pointer"
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (!isDrafted) {
|
||||
// TODO: Add to queue functionality (Phase 7)
|
||||
console.log("Add to queue:", participant.name);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<td className="p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{participant.name}</span>
|
||||
{isDrafted && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
Drafted
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-3 text-muted-foreground">
|
||||
{participant.sport.name}
|
||||
</td>
|
||||
<td className="p-3 text-right font-mono font-semibold">
|
||||
{participant.expectedValue}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue