feat: add fantasy league schema and new league routes
This commit is contained in:
parent
6d998a5f85
commit
5c957249ad
19 changed files with 1459 additions and 4 deletions
60
app/components/ui/button.tsx
Normal file
60
app/components/ui/button.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "app/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
92
app/components/ui/card.tsx
Normal file
92
app/components/ui/card.tsx
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "app/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
21
app/components/ui/input.tsx
Normal file
21
app/components/ui/input.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "app/lib/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
||||
22
app/components/ui/label.tsx
Normal file
22
app/components/ui/label.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
|
||||
import { cn } from "app/lib/utils"
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Label }
|
||||
185
app/components/ui/select.tsx
Normal file
185
app/components/ui/select.tsx
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "app/lib/utils"
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default"
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "popper",
|
||||
align = "center",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
align={align}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
||||
4
app/models/index.ts
Normal file
4
app/models/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// Re-export all model functions and types
|
||||
export * from "./league";
|
||||
export * from "./season";
|
||||
export * from "./team";
|
||||
60
app/models/league.ts
Normal file
60
app/models/league.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { eq } from "drizzle-orm";
|
||||
import { database } from "~/database/context";
|
||||
import * as schema from "~/database/schema";
|
||||
|
||||
export type League = typeof schema.leagues.$inferSelect;
|
||||
export type NewLeague = typeof schema.leagues.$inferInsert;
|
||||
|
||||
export async function createLeague(data: NewLeague): Promise<League> {
|
||||
const db = database();
|
||||
const [league] = await db
|
||||
.insert(schema.leagues)
|
||||
.values(data)
|
||||
.returning();
|
||||
return league;
|
||||
}
|
||||
|
||||
export async function findLeagueById(id: string): Promise<League | undefined> {
|
||||
const db = database();
|
||||
return await db.query.leagues.findFirst({
|
||||
where: eq(schema.leagues.id, id),
|
||||
});
|
||||
}
|
||||
|
||||
export async function findLeaguesByCreator(userId: string): Promise<League[]> {
|
||||
const db = database();
|
||||
return await db.query.leagues.findMany({
|
||||
where: eq(schema.leagues.createdBy, userId),
|
||||
orderBy: (leagues, { desc }) => [desc(leagues.createdAt)],
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateLeague(
|
||||
id: string,
|
||||
data: Partial<NewLeague>
|
||||
): Promise<League> {
|
||||
const db = database();
|
||||
const [league] = await db
|
||||
.update(schema.leagues)
|
||||
.set({ ...data, updatedAt: new Date() })
|
||||
.where(eq(schema.leagues.id, id))
|
||||
.returning();
|
||||
return league;
|
||||
}
|
||||
|
||||
export async function deleteLeague(id: string): Promise<void> {
|
||||
const db = database();
|
||||
await db.delete(schema.leagues).where(eq(schema.leagues.id, id));
|
||||
}
|
||||
|
||||
export async function listLeagues(options?: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}): Promise<League[]> {
|
||||
const db = database();
|
||||
return await db.query.leagues.findMany({
|
||||
limit: options?.limit,
|
||||
offset: options?.offset,
|
||||
orderBy: (leagues, { desc }) => [desc(leagues.createdAt)],
|
||||
});
|
||||
}
|
||||
79
app/models/season.ts
Normal file
79
app/models/season.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { eq, and } from "drizzle-orm";
|
||||
import { database } from "~/database/context";
|
||||
import * as schema from "~/database/schema";
|
||||
|
||||
export type Season = typeof schema.seasons.$inferSelect;
|
||||
export type NewSeason = typeof schema.seasons.$inferInsert;
|
||||
export type SeasonStatus = "pre_draft" | "draft" | "active" | "completed";
|
||||
|
||||
export async function createSeason(data: NewSeason): Promise<Season> {
|
||||
const db = database();
|
||||
const [season] = await db
|
||||
.insert(schema.seasons)
|
||||
.values(data)
|
||||
.returning();
|
||||
return season;
|
||||
}
|
||||
|
||||
export async function findSeasonById(id: string): Promise<Season | undefined> {
|
||||
const db = database();
|
||||
return await db.query.seasons.findFirst({
|
||||
where: eq(schema.seasons.id, id),
|
||||
});
|
||||
}
|
||||
|
||||
export async function findSeasonsByLeagueId(leagueId: string): Promise<Season[]> {
|
||||
const db = database();
|
||||
return await db.query.seasons.findMany({
|
||||
where: eq(schema.seasons.leagueId, leagueId),
|
||||
orderBy: (seasons, { desc }) => [desc(seasons.year)],
|
||||
});
|
||||
}
|
||||
|
||||
export async function findCurrentSeason(
|
||||
leagueId: string
|
||||
): Promise<Season | undefined> {
|
||||
const db = database();
|
||||
return await db.query.seasons.findFirst({
|
||||
where: eq(schema.seasons.leagueId, leagueId),
|
||||
orderBy: (seasons, { desc }) => [desc(seasons.year)],
|
||||
});
|
||||
}
|
||||
|
||||
export async function findSeasonByLeagueAndYear(
|
||||
leagueId: string,
|
||||
year: number
|
||||
): Promise<Season | undefined> {
|
||||
const db = database();
|
||||
return await db.query.seasons.findFirst({
|
||||
where: and(
|
||||
eq(schema.seasons.leagueId, leagueId),
|
||||
eq(schema.seasons.year, year)
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateSeason(
|
||||
id: string,
|
||||
data: Partial<NewSeason>
|
||||
): Promise<Season> {
|
||||
const db = database();
|
||||
const [season] = await db
|
||||
.update(schema.seasons)
|
||||
.set({ ...data, updatedAt: new Date() })
|
||||
.where(eq(schema.seasons.id, id))
|
||||
.returning();
|
||||
return season;
|
||||
}
|
||||
|
||||
export async function updateSeasonStatus(
|
||||
id: string,
|
||||
status: SeasonStatus
|
||||
): Promise<Season> {
|
||||
return await updateSeason(id, { status });
|
||||
}
|
||||
|
||||
export async function deleteSeason(id: string): Promise<void> {
|
||||
const db = database();
|
||||
await db.delete(schema.seasons).where(eq(schema.seasons.id, id));
|
||||
}
|
||||
84
app/models/team.ts
Normal file
84
app/models/team.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { eq, and, isNull } from "drizzle-orm";
|
||||
import { database } from "~/database/context";
|
||||
import * as schema from "~/database/schema";
|
||||
|
||||
export type Team = typeof schema.teams.$inferSelect;
|
||||
export type NewTeam = typeof schema.teams.$inferInsert;
|
||||
|
||||
export async function createTeam(data: NewTeam): Promise<Team> {
|
||||
const db = database();
|
||||
const [team] = await db.insert(schema.teams).values(data).returning();
|
||||
return team;
|
||||
}
|
||||
|
||||
export async function createManyTeams(teams: NewTeam[]): Promise<Team[]> {
|
||||
const db = database();
|
||||
return await db.insert(schema.teams).values(teams).returning();
|
||||
}
|
||||
|
||||
export async function findTeamById(id: string): Promise<Team | undefined> {
|
||||
const db = database();
|
||||
return await db.query.teams.findFirst({
|
||||
where: eq(schema.teams.id, id),
|
||||
});
|
||||
}
|
||||
|
||||
export async function findTeamsBySeasonId(seasonId: string): Promise<Team[]> {
|
||||
const db = database();
|
||||
return await db.query.teams.findMany({
|
||||
where: eq(schema.teams.seasonId, seasonId),
|
||||
orderBy: (teams, { asc }) => [asc(teams.name)],
|
||||
});
|
||||
}
|
||||
|
||||
export async function findTeamsByOwnerId(ownerId: string): Promise<Team[]> {
|
||||
const db = database();
|
||||
return await db.query.teams.findMany({
|
||||
where: eq(schema.teams.ownerId, ownerId),
|
||||
orderBy: (teams, { asc }) => [asc(teams.name)],
|
||||
});
|
||||
}
|
||||
|
||||
export async function findAvailableTeams(seasonId: string): Promise<Team[]> {
|
||||
const db = database();
|
||||
return await db.query.teams.findMany({
|
||||
where: and(
|
||||
eq(schema.teams.seasonId, seasonId),
|
||||
isNull(schema.teams.ownerId)
|
||||
),
|
||||
orderBy: (teams, { asc }) => [asc(teams.name)],
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateTeam(
|
||||
id: string,
|
||||
data: Partial<NewTeam>
|
||||
): Promise<Team> {
|
||||
const db = database();
|
||||
const [team] = await db
|
||||
.update(schema.teams)
|
||||
.set({ ...data, updatedAt: new Date() })
|
||||
.where(eq(schema.teams.id, id))
|
||||
.returning();
|
||||
return team;
|
||||
}
|
||||
|
||||
export async function assignTeamOwner(
|
||||
id: string,
|
||||
ownerId: string
|
||||
): Promise<Team> {
|
||||
return await updateTeam(id, { ownerId });
|
||||
}
|
||||
|
||||
export async function removeTeamOwner(id: string): Promise<Team> {
|
||||
return await updateTeam(id, { ownerId: null });
|
||||
}
|
||||
|
||||
export async function renameTeam(id: string, name: string): Promise<Team> {
|
||||
return await updateTeam(id, { name });
|
||||
}
|
||||
|
||||
export async function deleteTeam(id: string): Promise<void> {
|
||||
const db = database();
|
||||
await db.delete(schema.teams).where(eq(schema.teams.id, id));
|
||||
}
|
||||
|
|
@ -1,3 +1,7 @@
|
|||
import { type RouteConfig, index } from "@react-router/dev/routes";
|
||||
import { type RouteConfig, index, route } from "@react-router/dev/routes";
|
||||
|
||||
export default [index("routes/home.tsx")] satisfies RouteConfig;
|
||||
export default [
|
||||
index("routes/home.tsx"),
|
||||
route("leagues/new", "routes/leagues/new.tsx"),
|
||||
route("leagues/:leagueId", "routes/leagues/$leagueId.tsx"),
|
||||
] satisfies RouteConfig;
|
||||
|
|
|
|||
130
app/routes/leagues/$leagueId.tsx
Normal file
130
app/routes/leagues/$leagueId.tsx
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import { Link } from "react-router";
|
||||
import { getAuth } from "@clerk/react-router/server";
|
||||
import { findLeagueById } from "~/models/league";
|
||||
import { findCurrentSeason } from "~/models/season";
|
||||
import { findTeamsBySeasonId } from "~/models/team";
|
||||
import type { Route } from "./+types/$leagueId";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card";
|
||||
import { Button } from "~/components/ui/button";
|
||||
|
||||
export async function loader(args: Route.LoaderArgs) {
|
||||
const { userId } = await getAuth(args);
|
||||
const { params } = args;
|
||||
const { leagueId } = params;
|
||||
|
||||
// Fetch league
|
||||
const league = await findLeagueById(leagueId);
|
||||
|
||||
if (!league) {
|
||||
throw new Response("League not found", { status: 404 });
|
||||
}
|
||||
|
||||
// Fetch current season
|
||||
const season = await findCurrentSeason(leagueId);
|
||||
|
||||
// Fetch teams for current season
|
||||
const teams = season ? await findTeamsBySeasonId(season.id) : [];
|
||||
|
||||
return {
|
||||
league,
|
||||
season,
|
||||
teams,
|
||||
currentUserId: userId,
|
||||
};
|
||||
}
|
||||
|
||||
export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
||||
const { league, season, teams, currentUserId } = loaderData;
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-8 px-4">
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h1 className="text-4xl font-bold">{league.name}</h1>
|
||||
<Button variant="outline" asChild>
|
||||
<Link to="/">Back to Home</Link>
|
||||
</Button>
|
||||
</div>
|
||||
{season && (
|
||||
<p className="text-muted-foreground">
|
||||
{season.year} Season • Status:{" "}
|
||||
<span className="capitalize">
|
||||
{season.status.replace("_", " ")}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card className="md:col-span-2 lg:col-span-3">
|
||||
<CardHeader>
|
||||
<CardTitle>Teams</CardTitle>
|
||||
<CardDescription>
|
||||
{teams.length} team{teams.length !== 1 ? "s" : ""} in this league
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{teams.map((team) => {
|
||||
const isOwned = team.ownerId === currentUserId;
|
||||
return (
|
||||
<Card
|
||||
key={team.id}
|
||||
className={isOwned ? "border-primary" : ""}
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{team.name}</CardTitle>
|
||||
<CardDescription>
|
||||
{team.ownerId ? (
|
||||
isOwned ? (
|
||||
<span className="text-primary font-medium">
|
||||
Your Team
|
||||
</span>
|
||||
) : (
|
||||
"Owned"
|
||||
)
|
||||
) : (
|
||||
<span className="text-muted-foreground">
|
||||
Available
|
||||
</span>
|
||||
)}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>League Info</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Created</p>
|
||||
<p className="font-medium">
|
||||
{new Date(league.createdAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
{season && (
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Season Status</p>
|
||||
<p className="font-medium capitalize">
|
||||
{season.status.replace("_", " ")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
152
app/routes/leagues/new.tsx
Normal file
152
app/routes/leagues/new.tsx
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import { Form, Link, redirect } from "react-router";
|
||||
import { getAuth } from "@clerk/react-router/server";
|
||||
import { createLeague } from "~/models/league";
|
||||
import { createSeason } from "~/models/season";
|
||||
import { createManyTeams } from "~/models/team";
|
||||
import type { Route } from "./+types/new";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Label } from "~/components/ui/label";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "~/components/ui/select";
|
||||
|
||||
export async function action(args: Route.ActionArgs) {
|
||||
const { userId } = await getAuth(args);
|
||||
const { request } = args;
|
||||
|
||||
if (!userId) {
|
||||
throw new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
const formData = await request.formData();
|
||||
const name = formData.get("name");
|
||||
const teamCount = formData.get("teamCount");
|
||||
|
||||
// Validation
|
||||
if (typeof name !== "string" || !name.trim()) {
|
||||
return { error: "League name is required" };
|
||||
}
|
||||
|
||||
if (typeof teamCount !== "string") {
|
||||
return { error: "Number of teams is required" };
|
||||
}
|
||||
|
||||
const teamCountNum = parseInt(teamCount, 10);
|
||||
if (isNaN(teamCountNum) || teamCountNum < 6 || teamCountNum > 16) {
|
||||
return { error: "Number of teams must be between 6 and 16" };
|
||||
}
|
||||
|
||||
try {
|
||||
// Create league
|
||||
const league = await createLeague({
|
||||
name: name.trim(),
|
||||
createdBy: userId,
|
||||
});
|
||||
|
||||
// Create first season
|
||||
const currentYear = new Date().getFullYear();
|
||||
const season = await createSeason({
|
||||
leagueId: league.id,
|
||||
year: currentYear,
|
||||
status: "pre_draft",
|
||||
});
|
||||
|
||||
// Create teams with placeholder names
|
||||
const teams = Array.from({ length: teamCountNum }, (_, i) => ({
|
||||
seasonId: season.id,
|
||||
name: `Team ${i + 1}`,
|
||||
ownerId: i === 0 ? userId : null, // Assign first team to creator
|
||||
}));
|
||||
|
||||
await createManyTeams(teams);
|
||||
|
||||
// Redirect to league homepage
|
||||
return redirect(`/leagues/${league.id}`);
|
||||
} catch (error) {
|
||||
console.error("Error creating league:", error);
|
||||
return { error: "Failed to create league. Please try again." };
|
||||
}
|
||||
}
|
||||
|
||||
export default function NewLeague({ actionData }: Route.ComponentProps) {
|
||||
return (
|
||||
<div className="container max-w-2xl mx-auto py-8 px-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-3xl">Start a New League</CardTitle>
|
||||
<CardDescription>
|
||||
Create your fantasy league and invite your friends to compete.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form method="post" className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">League Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="Enter league name"
|
||||
required
|
||||
minLength={3}
|
||||
maxLength={50}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="teamCount">Number of Teams</Label>
|
||||
<Select name="teamCount" defaultValue="12" required>
|
||||
<SelectTrigger id="teamCount">
|
||||
<SelectValue placeholder="Select number of teams" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="6">6 Teams</SelectItem>
|
||||
<SelectItem value="7">7 Teams</SelectItem>
|
||||
<SelectItem value="8">8 Teams</SelectItem>
|
||||
<SelectItem value="9">9 Teams</SelectItem>
|
||||
<SelectItem value="10">10 Teams</SelectItem>
|
||||
<SelectItem value="11">11 Teams</SelectItem>
|
||||
<SelectItem value="12">12 Teams</SelectItem>
|
||||
<SelectItem value="13">13 Teams</SelectItem>
|
||||
<SelectItem value="14">14 Teams</SelectItem>
|
||||
<SelectItem value="15">15 Teams</SelectItem>
|
||||
<SelectItem value="16">16 Teams</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Choose between 6 and 16 teams
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{actionData?.error && (
|
||||
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm">
|
||||
{actionData.error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-4">
|
||||
<Button type="submit" className="flex-1">
|
||||
Create League
|
||||
</Button>
|
||||
<Button type="button" variant="outline" asChild>
|
||||
<Link to="/">Cancel</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { Form, useNavigation } from "react-router";
|
||||
import { Form, Link, useNavigation } from "react-router";
|
||||
import { Button } from "~/components/ui/button";
|
||||
|
||||
import logoDark from "./logo-dark.svg";
|
||||
import logoLight from "./logo-light.svg";
|
||||
|
|
@ -36,6 +37,11 @@ export function Welcome({
|
|||
</div>
|
||||
</header>
|
||||
<div className="max-w-[300px] w-full space-y-6 px-4">
|
||||
<div className="flex justify-center">
|
||||
<Button size="lg" asChild className="w-full">
|
||||
<Link to="/leagues/new">Start a League</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<nav className="rounded-3xl border border-gray-200 p-6 dark:border-gray-700 space-y-4">
|
||||
<p className="leading-6 text-gray-700 dark:text-gray-200 text-center">
|
||||
What's next?
|
||||
|
|
|
|||
|
|
@ -1,7 +1,45 @@
|
|||
import { integer, pgTable, varchar } from "drizzle-orm/pg-core";
|
||||
import { integer, pgTable, varchar, uuid, timestamp, pgEnum } from "drizzle-orm/pg-core";
|
||||
|
||||
export const guestBook = pgTable("guestBook", {
|
||||
id: integer().primaryKey().generatedAlwaysAsIdentity(),
|
||||
name: varchar({ length: 255 }).notNull(),
|
||||
email: varchar({ length: 255 }).notNull().unique(),
|
||||
});
|
||||
|
||||
// Fantasy League Tables
|
||||
export const seasonStatusEnum = pgEnum("season_status", [
|
||||
"pre_draft",
|
||||
"draft",
|
||||
"active",
|
||||
"completed",
|
||||
]);
|
||||
|
||||
export const leagues = pgTable("leagues", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
name: varchar("name", { length: 255 }).notNull(),
|
||||
createdBy: varchar("created_by", { length: 255 }).notNull(), // Clerk user ID
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const seasons = pgTable("seasons", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
leagueId: uuid("league_id")
|
||||
.notNull()
|
||||
.references(() => leagues.id, { onDelete: "cascade" }),
|
||||
year: integer("year").notNull(),
|
||||
status: seasonStatusEnum("status").notNull().default("pre_draft"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const teams = pgTable("teams", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
seasonId: uuid("season_id")
|
||||
.notNull()
|
||||
.references(() => seasons.id, { onDelete: "cascade" }),
|
||||
name: varchar("name", { length: 255 }).notNull(),
|
||||
ownerId: varchar("owner_id", { length: 255 }), // Clerk user ID, nullable
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
});
|
||||
|
|
|
|||
38
drizzle/0001_serious_la_nuit.sql
Normal file
38
drizzle/0001_serious_la_nuit.sql
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
CREATE TYPE "public"."season_status" AS ENUM('pre_draft', 'draft', 'active', 'completed');--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "leagues" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"name" varchar(255) NOT NULL,
|
||||
"created_by" varchar(255) NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "seasons" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"league_id" uuid NOT NULL,
|
||||
"year" integer NOT NULL,
|
||||
"status" "season_status" DEFAULT 'pre_draft' NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "teams" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"season_id" uuid NOT NULL,
|
||||
"name" varchar(255) NOT NULL,
|
||||
"owner_id" varchar(255),
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "seasons" ADD CONSTRAINT "seasons_league_id_leagues_id_fk" FOREIGN KEY ("league_id") REFERENCES "public"."leagues"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "teams" ADD CONSTRAINT "teams_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 $$;
|
||||
260
drizzle/meta/0001_snapshot.json
Normal file
260
drizzle/meta/0001_snapshot.json
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
{
|
||||
"id": "dfca1306-7467-4d4e-8b9f-546611ed8b00",
|
||||
"prevId": "6bf145c1-851c-4a50-a085-9306f05abb25",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.guestBook": {
|
||||
"name": "guestBook",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"identity": {
|
||||
"type": "always",
|
||||
"name": "guestBook_id_seq",
|
||||
"schema": "public",
|
||||
"increment": "1",
|
||||
"startWith": "1",
|
||||
"minValue": "1",
|
||||
"maxValue": "2147483647",
|
||||
"cache": "1",
|
||||
"cycle": false
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"guestBook_email_unique": {
|
||||
"name": "guestBook_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.leagues": {
|
||||
"name": "leagues",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_by": {
|
||||
"name": "created_by",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.seasons": {
|
||||
"name": "seasons",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"league_id": {
|
||||
"name": "league_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"year": {
|
||||
"name": "year",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "season_status",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'pre_draft'"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"seasons_league_id_leagues_id_fk": {
|
||||
"name": "seasons_league_id_leagues_id_fk",
|
||||
"tableFrom": "seasons",
|
||||
"tableTo": "leagues",
|
||||
"columnsFrom": [
|
||||
"league_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.teams": {
|
||||
"name": "teams",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"season_id": {
|
||||
"name": "season_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"owner_id": {
|
||||
"name": "owner_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"teams_season_id_seasons_id_fk": {
|
||||
"name": "teams_season_id_seasons_id_fk",
|
||||
"tableFrom": "teams",
|
||||
"tableTo": "seasons",
|
||||
"columnsFrom": [
|
||||
"season_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {
|
||||
"public.season_status": {
|
||||
"name": "season_status",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"pre_draft",
|
||||
"draft",
|
||||
"active",
|
||||
"completed"
|
||||
]
|
||||
}
|
||||
},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,13 @@
|
|||
"when": 1732076135211,
|
||||
"tag": "0000_short_donald_blake",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1760165302772,
|
||||
"tag": "0001_serious_la_nuit",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
210
package-lock.json
generated
210
package-lock.json
generated
|
|
@ -8,7 +8,10 @@
|
|||
"dependencies": {
|
||||
"@clerk/react-router": "^2.1.0",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@react-router/express": "^7.7.1",
|
||||
"@react-router/node": "^7.7.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
|
|
@ -1501,6 +1504,44 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
|
||||
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
|
||||
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.3",
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react-dom": {
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
|
||||
"integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.7.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
|
||||
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
|
|
@ -1661,12 +1702,41 @@
|
|||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/number": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
||||
"integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-arrow": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
|
||||
"integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.1.3"
|
||||
},
|
||||
"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-collection": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
|
||||
|
|
@ -1859,6 +1929,29 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-label": {
|
||||
"version": "2.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz",
|
||||
"integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.1.3"
|
||||
},
|
||||
"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-navigation-menu": {
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz",
|
||||
|
|
@ -1895,6 +1988,38 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popper": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
|
||||
"integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.0.0",
|
||||
"@radix-ui/react-arrow": "1.1.7",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1",
|
||||
"@radix-ui/react-use-rect": "1.1.1",
|
||||
"@radix-ui/react-use-size": "1.1.1",
|
||||
"@radix-ui/rect": "1.1.1"
|
||||
},
|
||||
"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-portal": {
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
|
||||
|
|
@ -1966,6 +2091,49 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-select": {
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
|
||||
"integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/number": "1.1.1",
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@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-dismissable-layer": "1.1.11",
|
||||
"@radix-ui/react-focus-guards": "1.1.3",
|
||||
"@radix-ui/react-focus-scope": "1.1.7",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-popper": "1.2.8",
|
||||
"@radix-ui/react-portal": "1.1.9",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-slot": "1.2.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1",
|
||||
"@radix-ui/react-use-previous": "1.1.1",
|
||||
"@radix-ui/react-visually-hidden": "1.2.3",
|
||||
"aria-hidden": "^1.2.4",
|
||||
"react-remove-scroll": "^2.6.3"
|
||||
},
|
||||
"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-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
|
|
@ -2084,6 +2252,42 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-rect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
|
||||
"integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/rect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-size": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
|
||||
"integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-visually-hidden": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
|
||||
|
|
@ -2107,6 +2311,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/rect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
|
||||
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@react-router/dev": {
|
||||
"version": "7.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-7.9.4.tgz",
|
||||
|
|
|
|||
|
|
@ -13,7 +13,10 @@
|
|||
"dependencies": {
|
||||
"@clerk/react-router": "^2.1.0",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@react-router/express": "^7.7.1",
|
||||
"@react-router/node": "^7.7.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue