- {availableParticipants.map((p) => {
- const inQueue = queuedParticipantIds.has(p.id);
- return (
-
-
-
{p.name}
-
{p.sport.name}
+ const allPlayersPanel = useMemo(
+ () => (
+
+ {availableParticipants.map((p) => {
+ const inQueue = queuedParticipantIds.has(p.id);
+ return (
+
+
+
{p.name}
+
{p.sport.name}
+
+ {p.vorpValue != null && (
+
+ {Number(p.vorpValue).toFixed(1)}
+
+ )}
+ {inQueue ? (
+
+ ) : (
+
+ )}
- {p.vorpValue != null && (
-
- {Number(p.vorpValue).toFixed(1)}
-
- )}
- {inQueue ? (
-
- ) : (
-
- )}
-
- );
- })}
- {availableParticipants.length === 0 && (
-
- No participants have been added to this season yet.
-
- )}
-
+ );
+ })}
+ {availableParticipants.length === 0 && (
+
+ No participants have been added to this season yet.
+
+ )}
+
+ ),
+ [availableParticipants, queuedParticipantIds, handleAdd, handleRemoveByParticipantId]
);
- const rankingsPanel = (
- <>
-
-
-
- {localQueue.length > 0 && (
-
- Drag to reorder · Autopick follows this order
-
- )}
- >
+ const rankingsPanel = useMemo(
+ () => (
+ <>
+
+
+
+ {localQueue.length > 0 && (
+
+ Drag to reorder · Autopick follows this order
+
+ )}
+ >
+ ),
+ [localQueue, availableParticipants, handleRemove, handleReorder]
);
// ── Render ──────────────────────────────────────────────────────────────────
diff --git a/app/routes/leagues/$leagueId.tsx b/app/routes/leagues/$leagueId.tsx
index 0a17ef5..d527196 100644
--- a/app/routes/leagues/$leagueId.tsx
+++ b/app/routes/leagues/$leagueId.tsx
@@ -210,7 +210,11 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
draftTimezone={currentUserTimezone ?? season.overnightPauseTimezone}
draftBoardHref={`/leagues/${league.id}/draft-board/${season.id}`}
draftRoomHref={`/leagues/${league.id}/draft/${season.id}`}
- queueBuilderHref={myTeam && season.status === "pre_draft" ? `/leagues/${league.id}/draft-queue/${season.id}` : undefined}
+ queueBuilderHref={
+ myTeam && season.status === "pre_draft"
+ ? `/leagues/${league.id}/draft-queue/${season.id}`
+ : undefined
+ }
userDraftPosition={userDraftPosition}
overnightPauseMode={season.overnightPauseMode}
overnightPauseStart={season.overnightPauseStart}
diff --git a/app/routes/leagues/__tests__/draft-queue-access.test.ts b/app/routes/leagues/__tests__/draft-queue-access.test.ts
new file mode 100644
index 0000000..c5969d5
--- /dev/null
+++ b/app/routes/leagues/__tests__/draft-queue-access.test.ts
@@ -0,0 +1,213 @@
+import { describe, it, expect } from 'vitest';
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+const createMockSeason = (overrides: Partial<{
+ id: string;
+ leagueId: string;
+ status: 'pre_draft' | 'draft' | 'active' | 'completed';
+}> = {}) => ({
+ id: 'season-1',
+ leagueId: 'league-1',
+ year: 2025,
+ status: 'pre_draft' as const,
+ templateId: null,
+ draftRounds: 20,
+ flexSpots: 0,
+ draftDateTime: null,
+ draftInitialTime: 120,
+ draftIncrementTime: 15,
+ currentPickNumber: null,
+ draftStartedAt: null,
+ draftPaused: false,
+ inviteCode: 'TEST123',
+ pointsFor1st: 100,
+ pointsFor2nd: 70,
+ pointsFor3rd: 50,
+ pointsFor4th: 40,
+ pointsFor5th: 25,
+ pointsFor6th: 25,
+ pointsFor7th: 15,
+ pointsFor8th: 15,
+ createdAt: new Date('2025-01-01'),
+ updatedAt: new Date('2025-01-01'),
+ ...overrides,
+});
+
+type MockSeason = ReturnType
;
+
+// Mirrors the access-control and redirect logic from the pre-draft rankings loader.
+// Returns null for a successful load, a redirect URL, or an error object.
+function checkPreDraftRankingsAccess(opts: {
+ leagueId: string;
+ season: MockSeason;
+ userId: string | null;
+ hasTeam: boolean;
+}): { redirect: string } | { status: number; message: string } | null {
+ const { leagueId, season, userId, hasTeam } = opts;
+ const { id: seasonId } = season;
+
+ if (season.status === 'draft') {
+ return { redirect: `/leagues/${leagueId}/draft/${seasonId}` };
+ }
+ if (season.status === 'active' || season.status === 'completed') {
+ return { redirect: `/leagues/${leagueId}/draft-board/${seasonId}` };
+ }
+
+ if (!userId) {
+ return { status: 401, message: 'You must be logged in to set pre-draft rankings' };
+ }
+
+ if (!hasTeam) {
+ return { status: 403, message: 'You do not have a team in this season' };
+ }
+
+ return null;
+}
+
+// ---------------------------------------------------------------------------
+// Redirect behaviour based on season status
+// ---------------------------------------------------------------------------
+
+describe('Pre-Draft Rankings Access - Season Status Redirects', () => {
+ it('redirects to the draft room when status is draft', () => {
+ const season = createMockSeason({ status: 'draft' });
+ const result = checkPreDraftRankingsAccess({
+ leagueId: 'league-1',
+ season,
+ userId: 'user-1',
+ hasTeam: true,
+ });
+ expect(result).toEqual({ redirect: '/leagues/league-1/draft/season-1' });
+ });
+
+ it('redirects to the draft board when status is active', () => {
+ const season = createMockSeason({ status: 'active' });
+ const result = checkPreDraftRankingsAccess({
+ leagueId: 'league-1',
+ season,
+ userId: 'user-1',
+ hasTeam: true,
+ });
+ expect(result).toEqual({ redirect: '/leagues/league-1/draft-board/season-1' });
+ });
+
+ it('redirects to the draft board when status is completed', () => {
+ const season = createMockSeason({ status: 'completed' });
+ const result = checkPreDraftRankingsAccess({
+ leagueId: 'league-1',
+ season,
+ userId: 'user-1',
+ hasTeam: true,
+ });
+ expect(result).toEqual({ redirect: '/leagues/league-1/draft-board/season-1' });
+ });
+
+ it('does not redirect when status is pre_draft', () => {
+ const season = createMockSeason({ status: 'pre_draft' });
+ const result = checkPreDraftRankingsAccess({
+ leagueId: 'league-1',
+ season,
+ userId: 'user-1',
+ hasTeam: true,
+ });
+ expect(result).toBeNull();
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Authentication
+// ---------------------------------------------------------------------------
+
+describe('Pre-Draft Rankings Access - Authentication', () => {
+ it('returns 401 when no user is logged in', () => {
+ const season = createMockSeason({ status: 'pre_draft' });
+ const result = checkPreDraftRankingsAccess({
+ leagueId: 'league-1',
+ season,
+ userId: null,
+ hasTeam: false,
+ });
+ expect(result).toMatchObject({ status: 401 });
+ });
+
+ it('uses a descriptive 401 message', () => {
+ const season = createMockSeason({ status: 'pre_draft' });
+ const result = checkPreDraftRankingsAccess({
+ leagueId: 'league-1',
+ season,
+ userId: null,
+ hasTeam: false,
+ });
+ expect(result).toMatchObject({ message: 'You must be logged in to set pre-draft rankings' });
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Team membership
+// ---------------------------------------------------------------------------
+
+describe('Pre-Draft Rankings Access - Team Membership', () => {
+ it('returns 403 for a logged-in user with no team in the season', () => {
+ const season = createMockSeason({ status: 'pre_draft' });
+ const result = checkPreDraftRankingsAccess({
+ leagueId: 'league-1',
+ season,
+ userId: 'outsider',
+ hasTeam: false,
+ });
+ expect(result).toMatchObject({ status: 403 });
+ });
+
+ it('uses a descriptive 403 message', () => {
+ const season = createMockSeason({ status: 'pre_draft' });
+ const result = checkPreDraftRankingsAccess({
+ leagueId: 'league-1',
+ season,
+ userId: 'outsider',
+ hasTeam: false,
+ });
+ expect(result).toMatchObject({ message: 'You do not have a team in this season' });
+ });
+
+ it('grants access to a logged-in user who owns a team', () => {
+ const season = createMockSeason({ status: 'pre_draft' });
+ const result = checkPreDraftRankingsAccess({
+ leagueId: 'league-1',
+ season,
+ userId: 'owner-user',
+ hasTeam: true,
+ });
+ expect(result).toBeNull();
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Redirect targets use the correct leagueId and seasonId
+// ---------------------------------------------------------------------------
+
+describe('Pre-Draft Rankings Access - Redirect URL Correctness', () => {
+ it('uses the leagueId from params in the redirect URL', () => {
+ const season = createMockSeason({ id: 'season-99', status: 'draft' });
+ const result = checkPreDraftRankingsAccess({
+ leagueId: 'league-42',
+ season,
+ userId: 'user-1',
+ hasTeam: true,
+ });
+ expect(result).toEqual({ redirect: '/leagues/league-42/draft/season-99' });
+ });
+
+ it('uses the seasonId in the redirect URL', () => {
+ const season = createMockSeason({ id: 'season-77', status: 'active' });
+ const result = checkPreDraftRankingsAccess({
+ leagueId: 'league-1',
+ season,
+ userId: 'user-1',
+ hasTeam: true,
+ });
+ expect(result).toEqual({ redirect: '/leagues/league-1/draft-board/season-77' });
+ });
+});