feat: add draft order management with UI for manual and random ordering
This commit is contained in:
parent
8c5a611b4b
commit
fdfce98d87
9 changed files with 1548 additions and 3 deletions
88
DRAFT_ORDER_IMPLEMENTATION.md
Normal file
88
DRAFT_ORDER_IMPLEMENTATION.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# Draft Order Implementation
|
||||
|
||||
## Overview
|
||||
Implemented a complete draft slot system that allows commissioners to manage draft order for their fantasy leagues.
|
||||
|
||||
## What Was Implemented
|
||||
|
||||
### 1. Database Schema
|
||||
- **New Table**: `draft_slots`
|
||||
- `id`: UUID primary key
|
||||
- `season_id`: Foreign key to seasons (cascade delete)
|
||||
- `team_id`: Foreign key to teams (cascade delete)
|
||||
- `draft_order`: Integer representing the draft position
|
||||
- `created_at`, `updated_at`: Timestamps
|
||||
|
||||
### 2. Model Layer (`app/models/draft-slot.ts`)
|
||||
- **CRUD Operations**:
|
||||
- `createDraftSlot()` - Create a single draft slot
|
||||
- `createManyDraftSlots()` - Create multiple draft slots
|
||||
- `findDraftSlotsBySeasonId()` - Get all draft slots for a season (with team info)
|
||||
- `findDraftSlotByTeamId()` - Find draft slot by team
|
||||
- `updateDraftSlotOrder()` - Update a slot's order
|
||||
- `deleteDraftSlotsBySeasonId()` - Delete all slots for a season
|
||||
- `deleteDraftSlot()` - Delete a specific slot
|
||||
|
||||
- **Special Functions**:
|
||||
- `setDraftOrder()` - Set the complete draft order for a season
|
||||
- `randomizeDraftOrder()` - Randomize draft order using Fisher-Yates shuffle
|
||||
|
||||
### 3. League Settings Page (`app/routes/leagues/$leagueId.settings.tsx`)
|
||||
- **New Draft Order Section**:
|
||||
- Visual list showing current draft order with numbered badges
|
||||
- Up/Down arrow buttons to manually reorder teams
|
||||
- "Save Draft Order" button to persist changes
|
||||
- "🎲 Randomize Draft Order" button for random order generation
|
||||
- Only editable during `pre_draft` status
|
||||
- Shows helpful note when draft order hasn't been set yet
|
||||
|
||||
- **Action Handlers**:
|
||||
- `set-draft-order` - Validates and saves manual draft order
|
||||
- `randomize-draft-order` - Generates and saves random order
|
||||
|
||||
### 4. League Page (`app/routes/leagues/$leagueId.tsx`)
|
||||
- **Draft Order Display Box**:
|
||||
- Shows in right column during `pre_draft` and `draft` statuses
|
||||
- Displays numbered list of teams in draft order
|
||||
- Shows team owner names when available
|
||||
- Automatically hidden once season moves to `active` or `completed`
|
||||
|
||||
### 5. Database Migration
|
||||
- Generated migration: `drizzle/0013_pink_the_order.sql`
|
||||
- Successfully applied to database
|
||||
|
||||
## Key Features
|
||||
|
||||
### Commissioner Controls
|
||||
- **Manual Ordering**: Commissioners can manually arrange teams using up/down buttons
|
||||
- **Randomization**: One-click randomization for fair draft order
|
||||
- **Validation**: Ensures all teams are included and valid
|
||||
- **Status Protection**: Draft order can only be modified during `pre_draft` status
|
||||
|
||||
### User Experience
|
||||
- **Visual Feedback**: Numbered badges clearly show draft positions
|
||||
- **Responsive Design**: Works on mobile and desktop
|
||||
- **Conditional Display**: Draft order only shown when relevant (pre_draft/draft states)
|
||||
- **Owner Information**: Shows which user owns each team in the draft order
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Data Flow
|
||||
1. Commissioner sets/randomizes order on settings page
|
||||
2. Action handler validates and calls model functions
|
||||
3. Model deletes old slots and creates new ones with correct order
|
||||
4. League page loader fetches draft slots for display
|
||||
5. Draft order box conditionally renders based on season status
|
||||
|
||||
### Snake Draft Ready
|
||||
The current implementation stores draft order as a simple integer sequence (1, 2, 3...). When implementing snake draft logic later, you can use this order to determine:
|
||||
- Round 1: Order 1→N
|
||||
- Round 2: Order N→1
|
||||
- Round 3: Order 1→N
|
||||
- etc.
|
||||
|
||||
## Future Enhancements
|
||||
- Drag-and-drop reordering (currently uses up/down buttons)
|
||||
- Draft order history/audit log
|
||||
- Automatic draft order setting when all teams are filled
|
||||
- Draft order lottery system with weighted probabilities
|
||||
144
app/models/draft-slot.ts
Normal file
144
app/models/draft-slot.ts
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import { eq } from "drizzle-orm";
|
||||
import { database } from "~/database/context";
|
||||
import * as schema from "~/database/schema";
|
||||
|
||||
export type DraftSlot = typeof schema.draftSlots.$inferSelect;
|
||||
export type NewDraftSlot = typeof schema.draftSlots.$inferInsert;
|
||||
|
||||
export interface DraftSlotWithTeam extends DraftSlot {
|
||||
team: {
|
||||
id: string;
|
||||
name: string;
|
||||
ownerId: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a single draft slot
|
||||
*/
|
||||
export async function createDraftSlot(data: NewDraftSlot): Promise<DraftSlot> {
|
||||
const db = database();
|
||||
const [draftSlot] = await db
|
||||
.insert(schema.draftSlots)
|
||||
.values(data)
|
||||
.returning();
|
||||
return draftSlot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create multiple draft slots at once
|
||||
*/
|
||||
export async function createManyDraftSlots(
|
||||
slots: NewDraftSlot[]
|
||||
): Promise<DraftSlot[]> {
|
||||
const db = database();
|
||||
return await db.insert(schema.draftSlots).values(slots).returning();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all draft slots for a season, ordered by draft order
|
||||
*/
|
||||
export async function findDraftSlotsBySeasonId(
|
||||
seasonId: string
|
||||
): Promise<DraftSlotWithTeam[]> {
|
||||
const db = database();
|
||||
return await db.query.draftSlots.findMany({
|
||||
where: eq(schema.draftSlots.seasonId, seasonId),
|
||||
orderBy: (draftSlots, { asc }) => [asc(draftSlots.draftOrder)],
|
||||
with: {
|
||||
team: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
ownerId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}) as DraftSlotWithTeam[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a draft slot by team ID
|
||||
*/
|
||||
export async function findDraftSlotByTeamId(
|
||||
teamId: string
|
||||
): Promise<DraftSlot | undefined> {
|
||||
const db = database();
|
||||
return await db.query.draftSlots.findFirst({
|
||||
where: eq(schema.draftSlots.teamId, teamId),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a draft slot's order
|
||||
*/
|
||||
export async function updateDraftSlotOrder(
|
||||
id: string,
|
||||
draftOrder: number
|
||||
): Promise<DraftSlot> {
|
||||
const db = database();
|
||||
const [draftSlot] = await db
|
||||
.update(schema.draftSlots)
|
||||
.set({ draftOrder, updatedAt: new Date() })
|
||||
.where(eq(schema.draftSlots.id, id))
|
||||
.returning();
|
||||
return draftSlot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all draft slots for a season
|
||||
*/
|
||||
export async function deleteDraftSlotsBySeasonId(
|
||||
seasonId: string
|
||||
): Promise<void> {
|
||||
const db = database();
|
||||
await db
|
||||
.delete(schema.draftSlots)
|
||||
.where(eq(schema.draftSlots.seasonId, seasonId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a specific draft slot
|
||||
*/
|
||||
export async function deleteDraftSlot(id: string): Promise<void> {
|
||||
const db = database();
|
||||
await db.delete(schema.draftSlots).where(eq(schema.draftSlots.id, id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the draft order for all teams in a season
|
||||
* This will delete existing slots and create new ones
|
||||
*/
|
||||
export async function setDraftOrder(
|
||||
seasonId: string,
|
||||
teamIds: string[]
|
||||
): Promise<DraftSlot[]> {
|
||||
// Delete existing draft slots for this season
|
||||
await deleteDraftSlotsBySeasonId(seasonId);
|
||||
|
||||
// Create new draft slots with the specified order
|
||||
const slots = teamIds.map((teamId, index) => ({
|
||||
seasonId,
|
||||
teamId,
|
||||
draftOrder: index + 1,
|
||||
}));
|
||||
|
||||
return await createManyDraftSlots(slots);
|
||||
}
|
||||
|
||||
/**
|
||||
* Randomize the draft order for a season
|
||||
*/
|
||||
export async function randomizeDraftOrder(
|
||||
seasonId: string,
|
||||
teamIds: string[]
|
||||
): Promise<DraftSlot[]> {
|
||||
// Shuffle the team IDs using Fisher-Yates algorithm
|
||||
const shuffled = [...teamIds];
|
||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
||||
}
|
||||
|
||||
return await setDraftOrder(seasonId, shuffled);
|
||||
}
|
||||
|
|
@ -11,3 +11,4 @@ export * from "./season-template";
|
|||
export * from "./season-template-sport";
|
||||
export * from "./season-sport";
|
||||
export * from "./participant-result";
|
||||
export * from "./draft-slot";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState } from "react";
|
||||
import { Form, Link, redirect } from "react-router";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Form, Link, redirect, useNavigation } from "react-router";
|
||||
import { getAuth } from "@clerk/react-router/server";
|
||||
import { findLeagueById, updateLeague, deleteLeague } from "~/models/league";
|
||||
import { isCommissioner } from "~/models/commissioner";
|
||||
|
|
@ -7,6 +7,7 @@ import { findCurrentSeasonWithSports } from "~/models/season";
|
|||
import { findTeamsBySeasonId, createManyTeams, deleteTeam } from "~/models/team";
|
||||
import { unlinkSportFromSeason, linkMultipleSportsToSeason } from "~/models/season-sport";
|
||||
import { findAllSportsSeasons } from "~/models/sports-season";
|
||||
import { findDraftSlotsBySeasonId, setDraftOrder, randomizeDraftOrder } from "~/models/draft-slot";
|
||||
import type { Route } from "./+types/$leagueId.settings";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Input } from "~/components/ui/input";
|
||||
|
|
@ -69,6 +70,7 @@ export async function loader(args: Route.LoaderArgs) {
|
|||
const season = await findCurrentSeasonWithSports(leagueId);
|
||||
const teams = season ? await findTeamsBySeasonId(season.id) : [];
|
||||
const allSportsSeasons = await findAllSportsSeasons();
|
||||
const draftSlots = season ? await findDraftSlotsBySeasonId(season.id) : [];
|
||||
|
||||
// Count teams with owners
|
||||
const teamsWithOwners = teams.filter(team => team.ownerId !== null).length;
|
||||
|
|
@ -80,6 +82,7 @@ export async function loader(args: Route.LoaderArgs) {
|
|||
teamCount: teams.length,
|
||||
teamsWithOwners,
|
||||
allSportsSeasons: allSportsSeasons as Array<typeof allSportsSeasons[0] & { sport: { id: string; name: string; type: string; slug: string } }>,
|
||||
draftSlots,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -150,6 +153,43 @@ export async function action(args: Route.ActionArgs) {
|
|||
return { success: true };
|
||||
}
|
||||
|
||||
if (intent === "set-draft-order") {
|
||||
if (season.status !== "pre_draft") {
|
||||
return { error: "Cannot modify draft order after draft has started" };
|
||||
}
|
||||
|
||||
const teams = await findTeamsBySeasonId(season.id);
|
||||
const teamIds = formData.getAll("teamOrder") as string[];
|
||||
|
||||
// Validate that all teams are included
|
||||
if (teamIds.length !== teams.length) {
|
||||
return { error: "All teams must be included in the draft order" };
|
||||
}
|
||||
|
||||
// Validate that all team IDs are valid
|
||||
const validTeamIds = new Set(teams.map(t => t.id));
|
||||
for (const teamId of teamIds) {
|
||||
if (!validTeamIds.has(teamId)) {
|
||||
return { error: "Invalid team ID in draft order" };
|
||||
}
|
||||
}
|
||||
|
||||
await setDraftOrder(season.id, teamIds);
|
||||
return { success: true, message: "Draft order updated successfully" };
|
||||
}
|
||||
|
||||
if (intent === "randomize-draft-order") {
|
||||
if (season.status !== "pre_draft") {
|
||||
return { error: "Cannot modify draft order after draft has started" };
|
||||
}
|
||||
|
||||
const teams = await findTeamsBySeasonId(season.id);
|
||||
const teamIds = teams.map(t => t.id);
|
||||
|
||||
await randomizeDraftOrder(season.id, teamIds);
|
||||
return { success: true, message: "Draft order randomized successfully" };
|
||||
}
|
||||
|
||||
if (intent === "delete") {
|
||||
// Delete the league
|
||||
await deleteLeague(leagueId);
|
||||
|
|
@ -230,14 +270,29 @@ export async function action(args: Route.ActionArgs) {
|
|||
}
|
||||
|
||||
export default function LeagueSettings({ loaderData, actionData }: Route.ComponentProps) {
|
||||
const { league, season, teamCount, teamsWithOwners, allSportsSeasons } = loaderData;
|
||||
const { league, season, teams, teamCount, teamsWithOwners, allSportsSeasons, draftSlots } = loaderData;
|
||||
const navigation = useNavigation();
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [selectedSports, setSelectedSports] = useState<Set<string>>(
|
||||
new Set(season?.seasonSports?.map((s: any) => s.sportsSeason.id) || [])
|
||||
);
|
||||
const [draftOrderTeams, setDraftOrderTeams] = useState<string[]>(
|
||||
draftSlots.length > 0
|
||||
? draftSlots.map((slot: any) => slot.teamId)
|
||||
: teams.map((team: any) => team.id)
|
||||
);
|
||||
|
||||
// Update draft order when loader data changes (after randomization)
|
||||
useEffect(() => {
|
||||
const newOrder = draftSlots.length > 0
|
||||
? draftSlots.map((slot: any) => slot.teamId)
|
||||
: teams.map((team: any) => team.id);
|
||||
setDraftOrderTeams(newOrder);
|
||||
}, [draftSlots, teams]);
|
||||
|
||||
const canEditSports = season && season.status === "pre_draft";
|
||||
const canEditTeamCount = season && season.status === "pre_draft";
|
||||
const canEditDraftOrder = season && season.status === "pre_draft";
|
||||
const minTeamCount = Math.max(6, teamsWithOwners);
|
||||
|
||||
const handleSportToggle = (sportId: string) => {
|
||||
|
|
@ -255,6 +310,17 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
|
|||
// The form submission will handle the actual deletion
|
||||
};
|
||||
|
||||
const moveDraftSlot = (fromIndex: number, toIndex: number) => {
|
||||
const newOrder = [...draftOrderTeams];
|
||||
const [movedTeam] = newOrder.splice(fromIndex, 1);
|
||||
newOrder.splice(toIndex, 0, movedTeam);
|
||||
setDraftOrderTeams(newOrder);
|
||||
};
|
||||
|
||||
const getTeamById = (teamId: string) => {
|
||||
return teams.find((t: any) => t.id === teamId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container max-w-2xl mx-auto py-8 px-4">
|
||||
<div className="mb-8">
|
||||
|
|
@ -415,6 +481,112 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
|
|||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Draft Order Management */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Draft Order</CardTitle>
|
||||
<CardDescription>
|
||||
{canEditDraftOrder
|
||||
? "Set the draft order for your league. Drag teams to reorder or randomize."
|
||||
: "Draft order cannot be changed after the draft has started"}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{draftSlots.length === 0 && canEditDraftOrder && (
|
||||
<div className="bg-muted/50 border border-muted-foreground/20 rounded-md p-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<strong>Note:</strong> Draft order has not been set yet. Use the form below to set the order manually or click "Randomize" to generate a random order.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Form method="post" className="space-y-4">
|
||||
<input type="hidden" name="intent" value="set-draft-order" />
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="border rounded-md divide-y">
|
||||
{draftOrderTeams.map((teamId, index) => {
|
||||
const team = getTeamById(teamId);
|
||||
if (!team) return null;
|
||||
|
||||
return (
|
||||
<div key={teamId} className="flex items-center gap-3 p-3">
|
||||
<input type="hidden" name="teamOrder" value={teamId} />
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-primary text-primary-foreground font-bold text-sm">
|
||||
{index + 1}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-medium">{team.name}</p>
|
||||
</div>
|
||||
{canEditDraftOrder && (
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => moveDraftSlot(index, Math.max(0, index - 1))}
|
||||
disabled={index === 0}
|
||||
>
|
||||
↑
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => moveDraftSlot(index, Math.min(draftOrderTeams.length - 1, index + 1))}
|
||||
disabled={index === draftOrderTeams.length - 1}
|
||||
>
|
||||
↓
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{canEditDraftOrder && (
|
||||
<div className="flex gap-2">
|
||||
<Button type="submit" className="flex-1">
|
||||
Save Draft Order
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{actionData?.success && actionData?.message && (
|
||||
<div className="bg-green-500/15 text-green-700 dark:text-green-400 px-4 py-3 rounded-md text-sm">
|
||||
{actionData.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{actionData?.error && (
|
||||
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm">
|
||||
{actionData.error}
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
|
||||
{canEditDraftOrder && (
|
||||
<Form method="post">
|
||||
<input type="hidden" name="intent" value="randomize-draft-order" />
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
disabled={navigation.state === "submitting"}
|
||||
>
|
||||
{navigation.state === "submitting" && navigation.formData?.get("intent") === "randomize-draft-order"
|
||||
? "Randomizing..."
|
||||
: "🎲 Randomize Draft Order"}
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Danger Zone */}
|
||||
<Card className="border-destructive">
|
||||
<CardHeader>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
isUserLeagueMember,
|
||||
isCommissioner,
|
||||
findUserByClerkId,
|
||||
findDraftSlotsBySeasonId,
|
||||
} from "~/models";
|
||||
import type { Route } from "./+types/$leagueId";
|
||||
import { Button } from "~/components/ui/button";
|
||||
|
|
@ -66,6 +67,11 @@ export async function loader(args: Route.LoaderArgs) {
|
|||
// Fetch teams for current season
|
||||
const teams = season ? await findTeamsBySeasonId(season.id) : [];
|
||||
|
||||
// Fetch draft slots if season is in pre_draft or draft status
|
||||
const draftSlots = season && (season.status === "pre_draft" || season.status === "draft")
|
||||
? await findDraftSlotsBySeasonId(season.id)
|
||||
: [];
|
||||
|
||||
// Fetch user data for team owners
|
||||
const ownerIds = teams
|
||||
.map((t) => t.ownerId)
|
||||
|
|
@ -114,6 +120,7 @@ export async function loader(args: Route.LoaderArgs) {
|
|||
ownerMap: Object.fromEntries(ownerMap),
|
||||
commissionerMap: Object.fromEntries(commissionerMap),
|
||||
availableTeamCount,
|
||||
draftSlots,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -128,6 +135,7 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
|||
ownerMap,
|
||||
commissionerMap,
|
||||
availableTeamCount,
|
||||
draftSlots,
|
||||
} = loaderData;
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
|
@ -280,6 +288,38 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
|||
|
||||
{/* Right Column - 1/3 width on desktop */}
|
||||
<div className="space-y-6">
|
||||
{/* Draft Order Display - Only show during pre_draft and draft */}
|
||||
{season && (season.status === "pre_draft" || season.status === "draft") && draftSlots.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Draft Order</CardTitle>
|
||||
<CardDescription>
|
||||
{season.status === "pre_draft" ? "Upcoming draft order" : "Current draft order"}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{draftSlots.map((slot: any, index: number) => {
|
||||
const ownerName = slot.team.ownerId ? ownerMap[slot.team.ownerId] : null;
|
||||
return (
|
||||
<div key={slot.id} className="flex items-center gap-3 py-2 border-b last:border-0">
|
||||
<div className="flex items-center justify-center w-7 h-7 rounded-full bg-primary text-primary-foreground font-bold text-xs">
|
||||
{index + 1}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-sm truncate">{slot.team.name}</p>
|
||||
{ownerName && (
|
||||
<p className="text-xs text-muted-foreground truncate">{ownerName}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>League Info</CardTitle>
|
||||
|
|
|
|||
|
|
@ -85,6 +85,19 @@ export const teams = pgTable("teams", {
|
|||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const draftSlots = pgTable("draft_slots", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
seasonId: uuid("season_id")
|
||||
.notNull()
|
||||
.references(() => seasons.id, { onDelete: "cascade" }),
|
||||
teamId: uuid("team_id")
|
||||
.notNull()
|
||||
.references(() => teams.id, { onDelete: "cascade" }),
|
||||
draftOrder: integer("draft_order").notNull(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const commissioners = pgTable("commissioners", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
leagueId: uuid("league_id")
|
||||
|
|
@ -245,3 +258,25 @@ export const participantResultsRelations = relations(participantResults, ({ one
|
|||
references: [sportsSeasons.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const teamsRelations = relations(teams, ({ one }) => ({
|
||||
season: one(seasons, {
|
||||
fields: [teams.seasonId],
|
||||
references: [seasons.id],
|
||||
}),
|
||||
draftSlot: one(draftSlots, {
|
||||
fields: [teams.id],
|
||||
references: [draftSlots.teamId],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const draftSlotsRelations = relations(draftSlots, ({ one }) => ({
|
||||
season: one(seasons, {
|
||||
fields: [draftSlots.seasonId],
|
||||
references: [seasons.id],
|
||||
}),
|
||||
team: one(teams, {
|
||||
fields: [draftSlots.teamId],
|
||||
references: [teams.id],
|
||||
}),
|
||||
}));
|
||||
|
|
|
|||
20
drizzle/0013_pink_the_order.sql
Normal file
20
drizzle/0013_pink_the_order.sql
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
CREATE TABLE IF NOT EXISTS "draft_slots" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"season_id" uuid NOT NULL,
|
||||
"team_id" uuid NOT NULL,
|
||||
"draft_order" integer NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "draft_slots" ADD CONSTRAINT "draft_slots_season_id_seasons_id_fk" FOREIGN KEY ("season_id") REFERENCES "public"."seasons"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "draft_slots" ADD CONSTRAINT "draft_slots_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."teams"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
1038
drizzle/meta/0013_snapshot.json
Normal file
1038
drizzle/meta/0013_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -92,6 +92,13 @@
|
|||
"when": 1760504208398,
|
||||
"tag": "0012_yielding_phil_sheldon",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 13,
|
||||
"version": "7",
|
||||
"when": 1760543654915,
|
||||
"tag": "0013_pink_the_order",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue