claude/sports-templates-conversion-ILWAM (#71)
Co-authored-by: Claude <noreply@anthropic.com> Reviewed-on: #71
This commit is contained in:
parent
a528aecc86
commit
f657293430
15 changed files with 12555 additions and 165 deletions
|
|
@ -1,6 +1,8 @@
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import { database } from "~/database/context";
|
import { database } from "~/database/context";
|
||||||
import * as schema from "~/database/schema";
|
import * as schema from "~/database/schema";
|
||||||
|
import { findDraftableSportsSeasonBySportId } from "~/models/sports-season";
|
||||||
|
import { logger } from "~/lib/logger";
|
||||||
|
|
||||||
export type SeasonSport = typeof schema.seasonSports.$inferSelect;
|
export type SeasonSport = typeof schema.seasonSports.$inferSelect;
|
||||||
export type NewSeasonSport = typeof schema.seasonSports.$inferInsert;
|
export type NewSeasonSport = typeof schema.seasonSports.$inferInsert;
|
||||||
|
|
@ -89,7 +91,6 @@ export async function applySportsFromTemplate(
|
||||||
): Promise<SeasonSport[]> {
|
): Promise<SeasonSport[]> {
|
||||||
const db = database();
|
const db = database();
|
||||||
|
|
||||||
// Get all sports from the template
|
|
||||||
const templateSports = await db.query.seasonTemplateSports.findMany({
|
const templateSports = await db.query.seasonTemplateSports.findMany({
|
||||||
where: eq(schema.seasonTemplateSports.templateId, templateId),
|
where: eq(schema.seasonTemplateSports.templateId, templateId),
|
||||||
});
|
});
|
||||||
|
|
@ -98,12 +99,26 @@ export async function applySportsFromTemplate(
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Link them to the season
|
const resolved = await Promise.all(
|
||||||
|
templateSports.map(async (ts) => {
|
||||||
|
const season = await findDraftableSportsSeasonBySportId(ts.sportId);
|
||||||
|
if (!season) {
|
||||||
|
logger.warn(`No draftable season found for sport ${ts.sportId} in template ${templateId}`);
|
||||||
|
}
|
||||||
|
return season ?? null;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const sportsSeasonIds = resolved
|
||||||
|
.filter((s): s is NonNullable<typeof s> => s !== null)
|
||||||
|
.map((s) => s.id);
|
||||||
|
|
||||||
|
if (sportsSeasonIds.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return await linkMultipleSportsToSeason(
|
return await linkMultipleSportsToSeason(
|
||||||
templateSports.map((ts) => ({
|
sportsSeasonIds.map((sportsSeasonId) => ({ seasonId, sportsSeasonId }))
|
||||||
seasonId,
|
|
||||||
sportsSeasonId: ts.sportsSeasonId,
|
|
||||||
}))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,14 @@ export type NewSeasonTemplateSport = typeof schema.seasonTemplateSports.$inferIn
|
||||||
|
|
||||||
export async function addSportToTemplate(
|
export async function addSportToTemplate(
|
||||||
templateId: string,
|
templateId: string,
|
||||||
sportsSeasonId: string
|
sportId: string
|
||||||
): Promise<SeasonTemplateSport> {
|
): Promise<SeasonTemplateSport> {
|
||||||
const db = database();
|
const db = database();
|
||||||
const [link] = await db
|
const [link] = await db
|
||||||
.insert(schema.seasonTemplateSports)
|
.insert(schema.seasonTemplateSports)
|
||||||
.values({
|
.values({
|
||||||
templateId,
|
templateId,
|
||||||
sportsSeasonId,
|
sportId,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
return link;
|
return link;
|
||||||
|
|
@ -32,7 +32,7 @@ export async function addMultipleSportsToTemplate(
|
||||||
|
|
||||||
export async function removeSportFromTemplate(
|
export async function removeSportFromTemplate(
|
||||||
templateId: string,
|
templateId: string,
|
||||||
sportsSeasonId: string
|
sportId: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const db = database();
|
const db = database();
|
||||||
await db
|
await db
|
||||||
|
|
@ -40,7 +40,7 @@ export async function removeSportFromTemplate(
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(schema.seasonTemplateSports.templateId, templateId),
|
eq(schema.seasonTemplateSports.templateId, templateId),
|
||||||
eq(schema.seasonTemplateSports.sportsSeasonId, sportsSeasonId)
|
eq(schema.seasonTemplateSports.sportId, sportId)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -52,28 +52,23 @@ export async function findSeasonTemplateSportsByTemplateId(
|
||||||
return await db.query.seasonTemplateSports.findMany({
|
return await db.query.seasonTemplateSports.findMany({
|
||||||
where: eq(schema.seasonTemplateSports.templateId, templateId),
|
where: eq(schema.seasonTemplateSports.templateId, templateId),
|
||||||
with: {
|
with: {
|
||||||
sportsSeason: {
|
sport: true,
|
||||||
with: {
|
|
||||||
sport: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function findSeasonTemplateSportsBySportsSeasonId(
|
export async function findSeasonTemplateSportsBySportId(
|
||||||
sportsSeasonId: string
|
sportId: string
|
||||||
): Promise<SeasonTemplateSport[]> {
|
): Promise<SeasonTemplateSport[]> {
|
||||||
const db = database();
|
const db = database();
|
||||||
return await db.query.seasonTemplateSports.findMany({
|
return await db.query.seasonTemplateSports.findMany({
|
||||||
where: eq(schema.seasonTemplateSports.sportsSeasonId, sportsSeasonId),
|
where: eq(schema.seasonTemplateSports.sportId, sportId),
|
||||||
with: {
|
with: {
|
||||||
template: true,
|
template: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function deleteSeasonTemplateSport(id: string): Promise<void> {
|
export async function deleteSeasonTemplateSport(id: string): Promise<void> {
|
||||||
const db = database();
|
const db = database();
|
||||||
await db.delete(schema.seasonTemplateSports).where(eq(schema.seasonTemplateSports.id, id));
|
await db.delete(schema.seasonTemplateSports).where(eq(schema.seasonTemplateSports.id, id));
|
||||||
|
|
|
||||||
|
|
@ -29,18 +29,14 @@ export async function findSeasonTemplateById(id: string): Promise<SeasonTemplate
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function findSeasonTemplateWithSportsSeasons(id: string) {
|
export async function findSeasonTemplateWithSports(id: string) {
|
||||||
const db = database();
|
const db = database();
|
||||||
return await db.query.seasonTemplates.findFirst({
|
return await db.query.seasonTemplates.findFirst({
|
||||||
where: eq(schema.seasonTemplates.id, id),
|
where: eq(schema.seasonTemplates.id, id),
|
||||||
with: {
|
with: {
|
||||||
seasonTemplateSports: {
|
seasonTemplateSports: {
|
||||||
with: {
|
with: {
|
||||||
sportsSeason: {
|
sport: true,
|
||||||
with: {
|
|
||||||
sport: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -51,14 +47,6 @@ export async function findActiveSeasonTemplates(): Promise<SeasonTemplate[]> {
|
||||||
const db = database();
|
const db = database();
|
||||||
return await db.query.seasonTemplates.findMany({
|
return await db.query.seasonTemplates.findMany({
|
||||||
where: eq(schema.seasonTemplates.isActive, true),
|
where: eq(schema.seasonTemplates.isActive, true),
|
||||||
orderBy: (templates, { desc }) => [desc(templates.year)],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function findSeasonTemplatesByYear(year: number): Promise<SeasonTemplate[]> {
|
|
||||||
const db = database();
|
|
||||||
return await db.query.seasonTemplates.findMany({
|
|
||||||
where: eq(schema.seasonTemplates.year, year),
|
|
||||||
orderBy: (templates, { asc }) => [asc(templates.name)],
|
orderBy: (templates, { asc }) => [asc(templates.name)],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -66,7 +54,7 @@ export async function findSeasonTemplatesByYear(year: number): Promise<SeasonTem
|
||||||
export async function findAllSeasonTemplates(): Promise<SeasonTemplate[]> {
|
export async function findAllSeasonTemplates(): Promise<SeasonTemplate[]> {
|
||||||
const db = database();
|
const db = database();
|
||||||
return await db.query.seasonTemplates.findMany({
|
return await db.query.seasonTemplates.findMany({
|
||||||
orderBy: (templates, { desc }) => [desc(templates.year)],
|
orderBy: (templates, { asc }) => [asc(templates.name)],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,24 @@ export async function findDraftableSportsSeasons() {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function findDraftableSportsSeasonBySportId(sportId: string) {
|
||||||
|
const db = database();
|
||||||
|
const today = sql`CURRENT_DATE`;
|
||||||
|
return await db.query.sportsSeasons.findFirst({
|
||||||
|
where: (ss, ops) =>
|
||||||
|
ops.and(
|
||||||
|
ops.eq(ss.sportId, sportId),
|
||||||
|
lte(ss.draftOn, today),
|
||||||
|
gte(ss.draftOff, today),
|
||||||
|
isNull(ss.fantasySeasonId)
|
||||||
|
),
|
||||||
|
orderBy: (ss, { desc }) => [desc(ss.year), desc(ss.draftOn)],
|
||||||
|
with: {
|
||||||
|
sport: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function updateSportsSeason(
|
export async function updateSportsSeason(
|
||||||
id: string,
|
id: string,
|
||||||
data: Partial<NewSportsSeason>
|
data: Partial<NewSportsSeason>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import type { Route } from "./+types/admin.templates.$id";
|
||||||
|
|
||||||
import { logger } from "~/lib/logger";
|
import { logger } from "~/lib/logger";
|
||||||
import {
|
import {
|
||||||
findSeasonTemplateWithSportsSeasons,
|
findSeasonTemplateWithSports,
|
||||||
updateSeasonTemplate,
|
updateSeasonTemplate,
|
||||||
deleteSeasonTemplate,
|
deleteSeasonTemplate,
|
||||||
setSeasonTemplateActive
|
setSeasonTemplateActive
|
||||||
|
|
@ -13,7 +13,7 @@ import {
|
||||||
addSportToTemplate,
|
addSportToTemplate,
|
||||||
removeSportFromTemplate
|
removeSportFromTemplate
|
||||||
} from "~/models/season-template-sport";
|
} from "~/models/season-template-sport";
|
||||||
import { findAllSportsSeasons } from "~/models/sports-season";
|
import { findAllSports } from "~/models/sport";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Input } from "~/components/ui/input";
|
import { Input } from "~/components/ui/input";
|
||||||
import { Label } from "~/components/ui/label";
|
import { Label } from "~/components/ui/label";
|
||||||
|
|
@ -51,19 +51,15 @@ export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loader({ params }: Route.LoaderArgs) {
|
export async function loader({ params }: Route.LoaderArgs) {
|
||||||
const template = await findSeasonTemplateWithSportsSeasons(params.id);
|
const template = await findSeasonTemplateWithSports(params.id);
|
||||||
|
|
||||||
if (!template) {
|
if (!template) {
|
||||||
throw new Response("Template not found", { status: 404 });
|
throw new Response("Template not found", { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const allSportsSeasons = await findAllSportsSeasons();
|
const allSports = await findAllSports();
|
||||||
|
|
||||||
// Type assertion since we know the sport relation is included
|
return { template, allSports };
|
||||||
return {
|
|
||||||
template,
|
|
||||||
allSportsSeasons: allSportsSeasons as Array<typeof allSportsSeasons[0] & { sport: { id: string; name: string; type: string; slug: string } }>
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function action({ request, params }: Route.ActionArgs) {
|
export async function action({ request, params }: Route.ActionArgs) {
|
||||||
|
|
@ -82,45 +78,34 @@ export async function action({ request, params }: Route.ActionArgs) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (intent === "add-sport") {
|
if (intent === "add-sport") {
|
||||||
const sportsSeasonId = formData.get("sportsSeasonId");
|
const sportId = formData.get("sportId");
|
||||||
|
|
||||||
if (typeof sportsSeasonId === "string") {
|
if (typeof sportId === "string") {
|
||||||
await addSportToTemplate(params.id, sportsSeasonId);
|
await addSportToTemplate(params.id, sportId);
|
||||||
}
|
}
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (intent === "remove-sport") {
|
if (intent === "remove-sport") {
|
||||||
const sportsSeasonId = formData.get("sportsSeasonId");
|
const sportId = formData.get("sportId");
|
||||||
|
|
||||||
if (typeof sportsSeasonId === "string") {
|
if (typeof sportId === "string") {
|
||||||
await removeSportFromTemplate(params.id, sportsSeasonId);
|
await removeSportFromTemplate(params.id, sportId);
|
||||||
}
|
}
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update template
|
// Update template
|
||||||
const name = formData.get("name");
|
const name = formData.get("name");
|
||||||
const year = formData.get("year");
|
|
||||||
const description = formData.get("description");
|
const description = formData.get("description");
|
||||||
|
|
||||||
if (typeof name !== "string" || !name.trim()) {
|
if (typeof name !== "string" || !name.trim()) {
|
||||||
return { error: "Template name is required" };
|
return { error: "Template name is required" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof year !== "string") {
|
|
||||||
return { error: "Year is required" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const yearNum = parseInt(year, 10);
|
|
||||||
if (isNaN(yearNum) || yearNum < 2000 || yearNum > 2100) {
|
|
||||||
return { error: "Year must be between 2000 and 2100" };
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateSeasonTemplate(params.id, {
|
await updateSeasonTemplate(params.id, {
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
year: yearNum,
|
|
||||||
description: typeof description === "string" && description.trim() ? description.trim() : null,
|
description: typeof description === "string" && description.trim() ? description.trim() : null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -132,15 +117,15 @@ export async function action({ request, params }: Route.ActionArgs) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EditTemplate({ loaderData, actionData }: Route.ComponentProps) {
|
export default function EditTemplate({ loaderData, actionData }: Route.ComponentProps) {
|
||||||
const { template, allSportsSeasons } = loaderData;
|
const { template, allSports } = loaderData;
|
||||||
const [selectedSportId, setSelectedSportId] = useState("");
|
const [selectedSportId, setSelectedSportId] = useState("");
|
||||||
|
|
||||||
const includedSeasonIds = new Set(
|
const includedSportIds = new Set(
|
||||||
template.seasonTemplateSports.map((s) => s.sportsSeasonId)
|
template.seasonTemplateSports.map((s) => s.sportId)
|
||||||
);
|
);
|
||||||
|
|
||||||
const availableSportsSeasons = allSportsSeasons.filter(
|
const availableSports = allSports.filter(
|
||||||
(s) => !includedSeasonIds.has(s.id)
|
(s) => !includedSportIds.has(s.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -170,19 +155,6 @@ export default function EditTemplate({ loaderData, actionData }: Route.Component
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="year">Year</Label>
|
|
||||||
<Input
|
|
||||||
id="year"
|
|
||||||
name="year"
|
|
||||||
type="number"
|
|
||||||
min="2000"
|
|
||||||
max="2100"
|
|
||||||
defaultValue={template.year}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="description">Description (Optional)</Label>
|
<Label htmlFor="description">Description (Optional)</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
|
|
@ -236,9 +208,9 @@ export default function EditTemplate({ loaderData, actionData }: Route.Component
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Sports Seasons</CardTitle>
|
<CardTitle>Sports</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Add sports seasons to this template
|
Add sports to this template. The active season for each sport will be used automatically when a league is created.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
|
|
@ -249,17 +221,12 @@ export default function EditTemplate({ loaderData, actionData }: Route.Component
|
||||||
key={templateSport.id}
|
key={templateSport.id}
|
||||||
className="flex items-center justify-between p-3 border rounded-lg"
|
className="flex items-center justify-between p-3 border rounded-lg"
|
||||||
>
|
>
|
||||||
<div>
|
<p className="font-medium">
|
||||||
<p className="font-medium">
|
{templateSport.sport.name}
|
||||||
{templateSport.sportsSeason.sport.name} - {templateSport.sportsSeason.name}
|
</p>
|
||||||
</p>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{templateSport.sportsSeason.year} • {templateSport.sportsSeason.scoringType.replace("_", " ")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Form method="post">
|
<Form method="post">
|
||||||
<input type="hidden" name="intent" value="remove-sport" />
|
<input type="hidden" name="intent" value="remove-sport" />
|
||||||
<input type="hidden" name="sportsSeasonId" value={templateSport.sportsSeasonId} />
|
<input type="hidden" name="sportId" value={templateSport.sportId} />
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
@ -274,22 +241,22 @@ export default function EditTemplate({ loaderData, actionData }: Route.Component
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{availableSportsSeasons.length > 0 ? (
|
{availableSports.length > 0 ? (
|
||||||
<Form method="post" className="flex gap-2">
|
<Form method="post" className="flex gap-2">
|
||||||
<input type="hidden" name="intent" value="add-sport" />
|
<input type="hidden" name="intent" value="add-sport" />
|
||||||
<Select
|
<Select
|
||||||
name="sportsSeasonId"
|
name="sportId"
|
||||||
value={selectedSportId}
|
value={selectedSportId}
|
||||||
onValueChange={setSelectedSportId}
|
onValueChange={setSelectedSportId}
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<SelectTrigger className="flex-1">
|
<SelectTrigger className="flex-1">
|
||||||
<SelectValue placeholder="Select a sports season" />
|
<SelectValue placeholder="Select a sport" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{availableSportsSeasons.map((season) => (
|
{availableSports.map((sport) => (
|
||||||
<SelectItem key={season.id} value={season.id}>
|
<SelectItem key={sport.id} value={sport.id}>
|
||||||
{season.sport.name} - {season.name} ({season.year})
|
{sport.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|
@ -300,7 +267,7 @@ export default function EditTemplate({ loaderData, actionData }: Route.Component
|
||||||
</Form>
|
</Form>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-muted-foreground text-center py-4">
|
<p className="text-sm text-muted-foreground text-center py-4">
|
||||||
All available sports seasons have been added
|
All available sports have been added
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
|
||||||
|
|
@ -22,27 +22,15 @@ export function meta(): Route.MetaDescriptors {
|
||||||
export async function action({ request }: Route.ActionArgs) {
|
export async function action({ request }: Route.ActionArgs) {
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const name = formData.get("name");
|
const name = formData.get("name");
|
||||||
const year = formData.get("year");
|
|
||||||
const description = formData.get("description");
|
const description = formData.get("description");
|
||||||
|
|
||||||
// Validation
|
|
||||||
if (typeof name !== "string" || !name.trim()) {
|
if (typeof name !== "string" || !name.trim()) {
|
||||||
return { error: "Template name is required" };
|
return { error: "Template name is required" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof year !== "string") {
|
|
||||||
return { error: "Year is required" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const yearNum = parseInt(year, 10);
|
|
||||||
if (isNaN(yearNum) || yearNum < 2000 || yearNum > 2100) {
|
|
||||||
return { error: "Year must be between 2000 and 2100" };
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const template = await createSeasonTemplate({
|
const template = await createSeasonTemplate({
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
year: yearNum,
|
|
||||||
description: typeof description === "string" && description.trim() ? description.trim() : null,
|
description: typeof description === "string" && description.trim() ? description.trim() : null,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
});
|
});
|
||||||
|
|
@ -55,15 +43,13 @@ export async function action({ request }: Route.ActionArgs) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NewTemplate({ actionData }: Route.ComponentProps) {
|
export default function NewTemplate({ actionData }: Route.ComponentProps) {
|
||||||
const currentYear = new Date().getFullYear();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8">
|
<div className="p-8">
|
||||||
<div className="max-w-2xl">
|
<div className="max-w-2xl">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h1 className="text-3xl font-bold">Create Season Template</h1>
|
<h1 className="text-3xl font-bold">Create Season Template</h1>
|
||||||
<p className="text-muted-foreground mt-1">
|
<p className="text-muted-foreground mt-1">
|
||||||
Bundle sports seasons together for league commissioners
|
Bundle sports together for league commissioners
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -82,7 +68,7 @@ export default function NewTemplate({ actionData }: Route.ComponentProps) {
|
||||||
id="name"
|
id="name"
|
||||||
name="name"
|
name="name"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="e.g., 2025 Full Season, 2025 Playoffs Only"
|
placeholder="e.g., Full Season, Playoffs Only"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
|
|
@ -90,25 +76,12 @@ export default function NewTemplate({ actionData }: Route.ComponentProps) {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="year">Year</Label>
|
|
||||||
<Input
|
|
||||||
id="year"
|
|
||||||
name="year"
|
|
||||||
type="number"
|
|
||||||
min="2000"
|
|
||||||
max="2100"
|
|
||||||
defaultValue={currentYear}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="description">Description (Optional)</Label>
|
<Label htmlFor="description">Description (Optional)</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="description"
|
id="description"
|
||||||
name="description"
|
name="description"
|
||||||
placeholder="Describe what sports seasons are included in this template"
|
placeholder="Describe what sports are included in this template"
|
||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -138,7 +111,7 @@ export default function NewTemplate({ actionData }: Route.ComponentProps) {
|
||||||
<CardContent className="text-sm text-foreground/80">
|
<CardContent className="text-sm text-foreground/80">
|
||||||
<p>After creating the template, you'll be able to:</p>
|
<p>After creating the template, you'll be able to:</p>
|
||||||
<ul className="list-disc list-inside mt-2 space-y-1">
|
<ul className="list-disc list-inside mt-2 space-y-1">
|
||||||
<li>Add sports seasons to the template</li>
|
<li>Add sports to the template</li>
|
||||||
<li>Mark which sports are required vs optional</li>
|
<li>Mark which sports are required vs optional</li>
|
||||||
<li>Set the number of flex spots</li>
|
<li>Set the number of flex spots</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,6 @@ export default function AdminTemplates({ loaderData }: Route.ComponentProps) {
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Name</TableHead>
|
<TableHead>Name</TableHead>
|
||||||
<TableHead>Year</TableHead>
|
|
||||||
<TableHead>Status</TableHead>
|
<TableHead>Status</TableHead>
|
||||||
<TableHead>Description</TableHead>
|
<TableHead>Description</TableHead>
|
||||||
<TableHead className="text-right">Actions</TableHead>
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
|
|
@ -87,7 +86,6 @@ export default function AdminTemplates({ loaderData }: Route.ComponentProps) {
|
||||||
{templates.map((template) => (
|
{templates.map((template) => (
|
||||||
<TableRow key={template.id}>
|
<TableRow key={template.id}>
|
||||||
<TableCell className="font-medium">{template.name}</TableCell>
|
<TableCell className="font-medium">{template.name}</TableCell>
|
||||||
<TableCell>{template.year}</TableCell>
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant={template.isActive ? "default" : "secondary"}>
|
<Badge variant={template.isActive ? "default" : "secondary"}>
|
||||||
{template.isActive ? "Active" : "Inactive"}
|
{template.isActive ? "Active" : "Inactive"}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { createCommissioner } from "~/models/commissioner";
|
||||||
import { createSeason } from "~/models/season";
|
import { createSeason } from "~/models/season";
|
||||||
import { linkMultipleSportsToSeason } from "~/models/season-sport";
|
import { linkMultipleSportsToSeason } from "~/models/season-sport";
|
||||||
import { createManyTeams } from "~/models/team";
|
import { createManyTeams } from "~/models/team";
|
||||||
import { findActiveSeasonTemplates, findSeasonTemplateWithSportsSeasons } from "~/models/season-template";
|
import { findActiveSeasonTemplates, findSeasonTemplateWithSports } from "~/models/season-template";
|
||||||
import { findDraftableSportsSeasons } from "~/models/sports-season";
|
import { findDraftableSportsSeasons } from "~/models/sports-season";
|
||||||
import { findUserById, updateUser } from "~/models/user";
|
import { findUserById, updateUser } from "~/models/user";
|
||||||
import { generateUniqueTeamNames, prependOwnerToTeamName } from "~/utils/team-names";
|
import { generateUniqueTeamNames, prependOwnerToTeamName } from "~/utils/team-names";
|
||||||
|
|
@ -57,7 +57,6 @@ type SportSeason = {
|
||||||
type Template = {
|
type Template = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
year: number;
|
|
||||||
description: string | null;
|
description: string | null;
|
||||||
sportsSeasonIds: string[];
|
sportsSeasonIds: string[];
|
||||||
};
|
};
|
||||||
|
|
@ -104,14 +103,18 @@ export async function loader(args: Route.LoaderArgs) {
|
||||||
const templates = await findActiveSeasonTemplates();
|
const templates = await findActiveSeasonTemplates();
|
||||||
const allSportsSeasons = await findDraftableSportsSeasons();
|
const allSportsSeasons = await findDraftableSportsSeasons();
|
||||||
|
|
||||||
|
const draftableSeasonBySportId = new Map(
|
||||||
|
allSportsSeasons.map((s) => [s.sportId, s])
|
||||||
|
);
|
||||||
|
|
||||||
const templatesWithSports = await Promise.all(
|
const templatesWithSports = await Promise.all(
|
||||||
templates.map(async (template) => {
|
templates.map(async (template) => {
|
||||||
const t = await findSeasonTemplateWithSportsSeasons(template.id);
|
const t = await findSeasonTemplateWithSports(template.id);
|
||||||
return {
|
return {
|
||||||
...template,
|
...template,
|
||||||
sportsSeasonIds: t?.seasonTemplateSports?.map(
|
sportsSeasonIds: (t?.seasonTemplateSports ?? [])
|
||||||
(ts: { sportsSeason: { id: string } }) => ts.sportsSeason.id
|
.map((ts: { sportId: string }) => draftableSeasonBySportId.get(ts.sportId)?.id)
|
||||||
) ?? [],
|
.filter((id): id is string => id !== null && id !== undefined),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -80,14 +80,11 @@ const ImportDataSchema = z.object({
|
||||||
seasonTemplates: z.array(
|
seasonTemplates: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
year: z.number(),
|
|
||||||
description: z.string().nullable(),
|
description: z.string().nullable(),
|
||||||
isActive: z.boolean(),
|
isActive: z.boolean(),
|
||||||
sports: z.array(
|
sports: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
sportSlug: z.string(),
|
sportSlug: z.string(),
|
||||||
seasonName: z.string(),
|
|
||||||
seasonYear: z.number(),
|
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
@ -133,10 +130,10 @@ export async function exportSportsDataToJSON(): Promise<ExportData> {
|
||||||
db.query.seasonTemplates.findMany({
|
db.query.seasonTemplates.findMany({
|
||||||
with: {
|
with: {
|
||||||
seasonTemplateSports: {
|
seasonTemplateSports: {
|
||||||
with: { sportsSeason: { with: { sport: true } } },
|
with: { sport: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
orderBy: (t, { desc }) => [desc(t.year)],
|
orderBy: (t, { asc }) => [asc(t.name)],
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -197,13 +194,10 @@ export async function exportSportsDataToJSON(): Promise<ExportData> {
|
||||||
})),
|
})),
|
||||||
seasonTemplates: seasonTemplates.map((t) => ({
|
seasonTemplates: seasonTemplates.map((t) => ({
|
||||||
name: t.name,
|
name: t.name,
|
||||||
year: t.year,
|
|
||||||
description: t.description,
|
description: t.description,
|
||||||
isActive: t.isActive,
|
isActive: t.isActive,
|
||||||
sports: t.seasonTemplateSports.map((ts) => ({
|
sports: t.seasonTemplateSports.map((ts) => ({
|
||||||
sportSlug: ts.sportsSeason.sport.slug,
|
sportSlug: ts.sport.slug,
|
||||||
seasonName: ts.sportsSeason.name,
|
|
||||||
seasonYear: ts.sportsSeason.year,
|
|
||||||
})),
|
})),
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
@ -529,10 +523,7 @@ export async function importSportsDataFromJSON(
|
||||||
|
|
||||||
for (const template of importData.seasonTemplates) {
|
for (const template of importData.seasonTemplates) {
|
||||||
const existing = await tx.query.seasonTemplates.findFirst({
|
const existing = await tx.query.seasonTemplates.findFirst({
|
||||||
where: and(
|
where: eq(schema.seasonTemplates.name, template.name),
|
||||||
eq(schema.seasonTemplates.name, template.name),
|
|
||||||
eq(schema.seasonTemplates.year, template.year)
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let templateId: string;
|
let templateId: string;
|
||||||
|
|
@ -561,7 +552,6 @@ export async function importSportsDataFromJSON(
|
||||||
.insert(schema.seasonTemplates)
|
.insert(schema.seasonTemplates)
|
||||||
.values({
|
.values({
|
||||||
name: template.name,
|
name: template.name,
|
||||||
year: template.year,
|
|
||||||
description: template.description,
|
description: template.description,
|
||||||
isActive: template.isActive,
|
isActive: template.isActive,
|
||||||
})
|
})
|
||||||
|
|
@ -571,12 +561,11 @@ export async function importSportsDataFromJSON(
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const templateSport of template.sports) {
|
for (const templateSport of template.sports) {
|
||||||
const seasonKey = `${templateSport.sportSlug}:${templateSport.seasonName}:${templateSport.seasonYear}`;
|
const sportId = sportIdMap.get(templateSport.sportSlug);
|
||||||
const sportsSeasonId = sportsSeasonIdMap.get(seasonKey);
|
if (sportId) {
|
||||||
if (sportsSeasonId) {
|
|
||||||
await tx.insert(schema.seasonTemplateSports).values({
|
await tx.insert(schema.seasonTemplateSports).values({
|
||||||
templateId,
|
templateId,
|
||||||
sportsSeasonId,
|
sportId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -190,11 +190,12 @@ export const seasonTemplates = pgTable("season_templates", {
|
||||||
id: uuid("id").primaryKey().defaultRandom(),
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
name: varchar("name", { length: 255 }).notNull(),
|
name: varchar("name", { length: 255 }).notNull(),
|
||||||
description: text("description"),
|
description: text("description"),
|
||||||
year: integer("year").notNull(),
|
|
||||||
isActive: boolean("is_active").notNull().default(true),
|
isActive: boolean("is_active").notNull().default(true),
|
||||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||||
});
|
}, (t) => ({
|
||||||
|
uniqueName: uniqueIndex("season_templates_name_unique").on(t.name),
|
||||||
|
}));
|
||||||
|
|
||||||
export const seasons = pgTable("seasons", {
|
export const seasons = pgTable("seasons", {
|
||||||
id: uuid("id").primaryKey().defaultRandom(),
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
@ -567,11 +568,13 @@ export const seasonTemplateSports = pgTable("season_template_sports", {
|
||||||
templateId: uuid("template_id")
|
templateId: uuid("template_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => seasonTemplates.id, { onDelete: "cascade" }),
|
.references(() => seasonTemplates.id, { onDelete: "cascade" }),
|
||||||
sportsSeasonId: uuid("sports_season_id")
|
sportId: uuid("sport_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
.references(() => sports.id, { onDelete: "cascade" }),
|
||||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||||
});
|
}, (t) => ({
|
||||||
|
uniqueTemplateSport: uniqueIndex("season_template_sports_template_sport_unique").on(t.templateId, t.sportId),
|
||||||
|
}));
|
||||||
|
|
||||||
export const seasonSports = pgTable("season_sports", {
|
export const seasonSports = pgTable("season_sports", {
|
||||||
id: uuid("id").primaryKey().defaultRandom(),
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
@ -974,6 +977,7 @@ export const sportsRelations = relations(sports, ({ many }) => ({
|
||||||
sportsSeasons: many(sportsSeasons),
|
sportsSeasons: many(sportsSeasons),
|
||||||
tournaments: many(tournaments),
|
tournaments: many(tournaments),
|
||||||
participants: many(participants),
|
participants: many(participants),
|
||||||
|
seasonTemplateSports: many(seasonTemplateSports),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const sportsSeasonsRelations = relations(sportsSeasons, ({ one, many }) => ({
|
export const sportsSeasonsRelations = relations(sportsSeasons, ({ one, many }) => ({
|
||||||
|
|
@ -988,7 +992,6 @@ export const sportsSeasonsRelations = relations(sportsSeasons, ({ one, many }) =
|
||||||
participants: many(seasonParticipants),
|
participants: many(seasonParticipants),
|
||||||
simulatorConfig: one(sportsSeasonSimulatorConfigs),
|
simulatorConfig: one(sportsSeasonSimulatorConfigs),
|
||||||
simulatorInputs: many(seasonParticipantSimulatorInputs),
|
simulatorInputs: many(seasonParticipantSimulatorInputs),
|
||||||
seasonTemplateSports: many(seasonTemplateSports),
|
|
||||||
seasonSports: many(seasonSports),
|
seasonSports: many(seasonSports),
|
||||||
participantResults: many(seasonParticipantResults),
|
participantResults: many(seasonParticipantResults),
|
||||||
regularSeasonStandings: many(regularSeasonStandings),
|
regularSeasonStandings: many(regularSeasonStandings),
|
||||||
|
|
@ -1092,9 +1095,9 @@ export const seasonTemplateSportsRelations = relations(seasonTemplateSports, ({
|
||||||
fields: [seasonTemplateSports.templateId],
|
fields: [seasonTemplateSports.templateId],
|
||||||
references: [seasonTemplates.id],
|
references: [seasonTemplates.id],
|
||||||
}),
|
}),
|
||||||
sportsSeason: one(sportsSeasons, {
|
sport: one(sports, {
|
||||||
fields: [seasonTemplateSports.sportsSeasonId],
|
fields: [seasonTemplateSports.sportId],
|
||||||
references: [sportsSeasons.id],
|
references: [sports.id],
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
||||||
13
drizzle/0114_warm_swarm.sql
Normal file
13
drizzle/0114_warm_swarm.sql
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
TRUNCATE TABLE "season_template_sports";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "season_template_sports" DROP CONSTRAINT "season_template_sports_sports_season_id_sports_seasons_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "season_template_sports" ADD COLUMN "sport_id" uuid NOT NULL;--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "season_template_sports" ADD CONSTRAINT "season_template_sports_sport_id_sports_id_fk" FOREIGN KEY ("sport_id") REFERENCES "public"."sports"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "season_template_sports" DROP COLUMN IF EXISTS "sports_season_id";--> statement-breakpoint
|
||||||
|
ALTER TABLE "season_templates" DROP COLUMN IF EXISTS "year";
|
||||||
2
drizzle/0115_spooky_spot.sql
Normal file
2
drizzle/0115_spooky_spot.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "season_template_sports_template_sport_unique" ON "season_template_sports" USING btree ("template_id","sport_id");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "season_templates_name_unique" ON "season_templates" USING btree ("name");
|
||||||
6187
drizzle/meta/0114_snapshot.json
Normal file
6187
drizzle/meta/0114_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
6225
drizzle/meta/0115_snapshot.json
Normal file
6225
drizzle/meta/0115_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -799,6 +799,20 @@
|
||||||
"when": 1779579877169,
|
"when": 1779579877169,
|
||||||
"tag": "0113_bent_banshee",
|
"tag": "0113_bent_banshee",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 114,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1780595525976,
|
||||||
|
"tag": "0114_warm_swarm",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 115,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1780596702982,
|
||||||
|
"tag": "0115_spooky_spot",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Loading…
Add table
Reference in a new issue