feat: add sports selection and team management to league settings

This commit is contained in:
Chris Parsons 2025-10-13 19:59:34 -07:00
parent 94586564e2
commit 0999e1c980
6 changed files with 490 additions and 67 deletions

View file

@ -0,0 +1,42 @@
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { Circle } from "lucide-react"
import { cn } from "~/lib/utils"
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn("grid gap-2", className)}
{...props}
ref={ref}
/>
)
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
export { RadioGroup, RadioGroupItem }

View file

@ -59,6 +59,27 @@ export async function findCurrentSeason(
});
}
export async function findCurrentSeasonWithSports(
leagueId: string
): Promise<SeasonWithSportsSeasons | undefined> {
const db = database();
return await db.query.seasons.findFirst({
where: eq(schema.seasons.leagueId, leagueId),
orderBy: (seasons, { desc }) => [desc(seasons.year)],
with: {
seasonSports: {
with: {
sportsSeason: {
with: {
sport: true,
},
},
},
},
},
}) as SeasonWithSportsSeasons | undefined;
}
export async function findSeasonByLeagueAndYear(
leagueId: string,
year: number

View file

@ -3,8 +3,10 @@ import { Form, Link, redirect } from "react-router";
import { getAuth } from "@clerk/react-router/server";
import { findLeagueById, updateLeague, deleteLeague } from "~/models/league";
import { isCommissioner } from "~/models/commissioner";
import { findCurrentSeason } from "~/models/season";
import { findTeamsBySeasonId } from "~/models/team";
import { findCurrentSeasonWithSports } from "~/models/season";
import { findTeamsBySeasonId, createManyTeams, deleteTeam } from "~/models/team";
import { unlinkSportFromSeason, linkMultipleSportsToSeason } from "~/models/season-sport";
import { findAllSportsSeasons } from "~/models/sports-season";
import type { Route } from "./+types/$leagueId.settings";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
@ -23,6 +25,8 @@ import {
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
import { Badge } from "~/components/ui/badge";
import { Checkbox } from "~/components/ui/checkbox";
import {
AlertDialog,
AlertDialogAction,
@ -61,13 +65,21 @@ export async function loader(args: Route.LoaderArgs) {
});
}
// Get current season to count teams
const season = await findCurrentSeason(leagueId);
// Get current season with sports
const season = await findCurrentSeasonWithSports(leagueId);
const teams = season ? await findTeamsBySeasonId(season.id) : [];
const allSportsSeasons = await findAllSportsSeasons();
// Count teams with owners
const teamsWithOwners = teams.filter(team => team.ownerId !== null).length;
return {
league,
season,
teams,
teamCount: teams.length,
teamsWithOwners,
allSportsSeasons: allSportsSeasons as Array<typeof allSportsSeasons[0] & { sport: { id: string; name: string; type: string; slug: string } }>,
};
}
@ -96,6 +108,48 @@ export async function action(args: Route.ActionArgs) {
const formData = await request.formData();
const intent = formData.get("intent");
// Get current season to check status
const season = await findCurrentSeasonWithSports(leagueId);
if (!season) {
return { error: "No active season found" };
}
if (intent === "update-sports") {
if (season.status !== "pre_draft") {
return { error: "Cannot modify sports after draft has started" };
}
const selectedSports = formData.getAll("sportsSeasons");
const currentSportIds = new Set(
season.seasonSports?.map((s: any) => s.sportsSeason.id) || []
);
const newSportIds = new Set(selectedSports as string[]);
// Remove sports that are no longer selected
for (const sportId of currentSportIds) {
if (!newSportIds.has(sportId)) {
await unlinkSportFromSeason(season.id, sportId);
}
}
// Add newly selected sports
const sportsToAdd = [];
for (const sportId of newSportIds) {
if (!currentSportIds.has(sportId)) {
sportsToAdd.push({
seasonId: season.id,
sportsSeasonId: sportId as string,
});
}
}
if (sportsToAdd.length > 0) {
await linkMultipleSportsToSeason(sportsToAdd);
}
return { success: true };
}
if (intent === "delete") {
// Delete the league
await deleteLeague(leagueId);
@ -106,6 +160,7 @@ export async function action(args: Route.ActionArgs) {
if (intent === "update") {
const name = formData.get("name");
const teamCount = formData.get("teamCount");
// Validation
if (typeof name !== "string" || !name.trim()) {
@ -121,6 +176,49 @@ export async function action(args: Route.ActionArgs) {
name: name.trim(),
});
// Handle team count changes if provided
if (typeof teamCount === "string") {
const newTeamCount = parseInt(teamCount, 10);
if (!isNaN(newTeamCount)) {
const teams = await findTeamsBySeasonId(season.id);
const currentTeamCount = teams.length;
const teamsWithOwners = teams.filter(t => t.ownerId !== null).length;
// Validate team count
if (newTeamCount < 6 || newTeamCount > 16) {
return { error: "Number of teams must be between 6 and 16" };
}
if (newTeamCount < teamsWithOwners) {
return { error: `Cannot reduce team count below ${teamsWithOwners} (number of teams with owners)` };
}
// Add or remove teams as needed
if (newTeamCount > currentTeamCount) {
// Add new teams
const teamsToAdd = Array.from(
{ length: newTeamCount - currentTeamCount },
(_, i) => ({
seasonId: season.id,
name: `Team ${currentTeamCount + i + 1}`,
ownerId: null,
})
);
await createManyTeams(teamsToAdd);
} else if (newTeamCount < currentTeamCount) {
// Remove teams without owners (from the end)
const teamsToRemove = teams
.filter(t => t.ownerId === null)
.slice(-(currentTeamCount - newTeamCount));
for (const team of teamsToRemove) {
await deleteTeam(team.id);
}
}
}
}
return redirect(`/leagues/${leagueId}?updated=true`);
} catch (error) {
console.error("Error updating league:", error);
@ -132,8 +230,25 @@ export async function action(args: Route.ActionArgs) {
}
export default function LeagueSettings({ loaderData, actionData }: Route.ComponentProps) {
const { league, teamCount } = loaderData;
const { league, season, teamCount, teamsWithOwners, allSportsSeasons } = loaderData;
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [selectedSports, setSelectedSports] = useState<Set<string>>(
new Set(season?.seasonSports?.map((s: any) => s.sportsSeason.id) || [])
);
const canEditSports = season && season.status === "pre_draft";
const canEditTeamCount = season && season.status === "pre_draft";
const minTeamCount = Math.max(6, teamsWithOwners);
const handleSportToggle = (sportId: string) => {
const newSelected = new Set(selectedSports);
if (newSelected.has(sportId)) {
newSelected.delete(sportId);
} else {
newSelected.add(sportId);
}
setSelectedSports(newSelected);
};
const handleDelete = () => {
setIsDeleteDialogOpen(false);
@ -181,20 +296,31 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
<div className="space-y-2">
<Label htmlFor="teamCount">Number of Teams</Label>
<Select name="teamCount" defaultValue={teamCount.toString()} disabled>
<Select
name="teamCount"
defaultValue={teamCount.toString()}
disabled={!canEditTeamCount}
>
<SelectTrigger id="teamCount">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Array.from({ length: 11 }, (_, i) => i + 6).map((num) => (
<SelectItem key={num} value={num.toString()}>
<SelectItem
key={num}
value={num.toString()}
disabled={num < minTeamCount}
>
{num} Teams
{num < minTeamCount && ` (${teamsWithOwners} teams have owners)`}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
Team count cannot be changed after league creation
{canEditTeamCount
? `Can be changed before draft starts (minimum ${minTeamCount} based on registered teams)`
: "Team count cannot be changed after draft has started"}
</p>
</div>
@ -211,6 +337,84 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
</CardContent>
</Card>
{/* Sports Seasons Management */}
<Card>
<CardHeader>
<CardTitle>Sports Seasons</CardTitle>
<CardDescription>
{canEditSports
? "Select the sports seasons that teams can draft from"
: "Sports cannot be modified after the draft has started"}
</CardDescription>
</CardHeader>
<CardContent>
<Form method="post" className="space-y-4">
<input type="hidden" name="intent" value="update-sports" />
<div className="space-y-3">
<div className="bg-muted/50 border border-muted-foreground/20 rounded-md p-3">
<p className="text-sm text-muted-foreground">
<strong>Recommendation:</strong> Select 16-20 sports seasons for optimal league balance and variety.
</p>
</div>
<div className="space-y-2">
<Label className="text-sm font-medium">
Sports Seasons ({selectedSports.size} selected)
</Label>
<div className="max-h-96 overflow-y-auto border rounded-md p-3 space-y-2">
{allSportsSeasons.length > 0 ? (
allSportsSeasons.map((season) => (
<div key={season.id} className="flex items-center space-x-2">
<Checkbox
id={season.id}
name="sportsSeasons"
value={season.id}
checked={selectedSports.has(season.id)}
onCheckedChange={() => handleSportToggle(season.id)}
disabled={!canEditSports}
/>
<Label
htmlFor={season.id}
className={`font-normal flex-1 ${canEditSports ? 'cursor-pointer' : 'cursor-not-allowed opacity-60'}`}
>
<span className="font-medium">{season.sport.name}</span> - {season.name} ({season.year})
<Badge variant="outline" className="ml-2 text-xs">
{season.scoringType.replace("_", " ")}
</Badge>
</Label>
</div>
))
) : (
<p className="text-sm text-muted-foreground">
No sports seasons available
</p>
)}
</div>
</div>
</div>
{canEditSports && (
<Button type="submit" className="w-full">
Save Sports Selection
</Button>
)}
{actionData?.success && (
<div className="bg-green-500/15 text-green-700 dark:text-green-400 px-4 py-3 rounded-md text-sm">
Sports updated successfully!
</div>
)}
{actionData?.error && (
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm">
{actionData.error}
</div>
)}
</Form>
</CardContent>
</Card>
{/* Danger Zone */}
<Card className="border-destructive">
<CardHeader>

View file

@ -4,10 +4,16 @@ import { createLeague, setCurrentSeason } from "~/models/league";
import { createSeason } from "~/models/season";
import { createManyTeams } from "~/models/team";
import { createCommissioner } from "~/models/commissioner";
import { linkMultipleSportsToSeason } from "~/models/season-sport";
import { findActiveSeasonTemplates, findSeasonTemplateWithSportsSeasons } from "~/models/season-template";
import { findAllSportsSeasons } from "~/models/sports-season";
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 { Checkbox } from "~/components/ui/checkbox";
import { Check } from "lucide-react";
import { useState } from "react";
import {
Card,
CardContent,
@ -22,6 +28,30 @@ import {
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
import { Badge } from "~/components/ui/badge";
export async function loader() {
const templates = await findActiveSeasonTemplates();
const allSportsSeasons = await findAllSportsSeasons();
// Fetch templates with their sports seasons for mapping
const templatesWithSports = await Promise.all(
templates.map(async (template) => {
const templateWithSports = await findSeasonTemplateWithSportsSeasons(template.id);
return {
...template,
sportsSeasonIds: templateWithSports?.seasonTemplateSports?.map(
(ts: any) => ts.sportsSeason.id
) || []
};
})
);
return {
templates: templatesWithSports,
allSportsSeasons: allSportsSeasons as Array<typeof allSportsSeasons[0] & { sport: { id: string; name: string; type: string; slug: string } }>
};
}
export async function action(args: Route.ActionArgs) {
const { userId } = await getAuth(args);
@ -34,6 +64,7 @@ export async function action(args: Route.ActionArgs) {
const formData = await request.formData();
const name = formData.get("name");
const teamCount = formData.get("teamCount");
const templateId = formData.get("templateId");
// Validation
if (typeof name !== "string" || !name.trim()) {
@ -68,11 +99,23 @@ export async function action(args: Route.ActionArgs) {
leagueId: league.id,
year: currentYear,
status: "pre_draft",
templateId: typeof templateId === "string" && templateId !== "" ? templateId : null,
});
// Set this as the current season for the league
await setCurrentSeason(league.id, season.id);
// Add selected sports to the season
const selectedSports = formData.getAll("sportsSeasons");
if (selectedSports.length > 0) {
await linkMultipleSportsToSeason(
selectedSports.map((id) => ({
seasonId: season.id,
sportsSeasonId: id as string,
}))
);
}
// Create teams with placeholder names
const teams = Array.from({ length: teamCountNum }, (_, i) => ({
seasonId: season.id,
@ -90,7 +133,25 @@ export async function action(args: Route.ActionArgs) {
}
}
export default function NewLeague({ actionData }: Route.ComponentProps) {
export default function NewLeague({ loaderData, actionData }: Route.ComponentProps) {
const { templates, allSportsSeasons } = loaderData;
const [selectedTemplate, setSelectedTemplate] = useState<string>("");
const [selectedSports, setSelectedSports] = useState<Set<string>>(new Set());
const handleSportToggle = (sportId: string) => {
const newSelected = new Set(selectedSports);
if (newSelected.has(sportId)) {
newSelected.delete(sportId);
} else {
newSelected.add(sportId);
}
setSelectedSports(newSelected);
};
const handleTemplateClick = (templateId: string, sportsSeasonIds: string[]) => {
setSelectedTemplate(templateId);
setSelectedSports(new Set(sportsSeasonIds));
};
return (
<div className="container max-w-2xl mx-auto py-8 px-4">
<Card>
@ -140,6 +201,93 @@ export default function NewLeague({ actionData }: Route.ComponentProps) {
</p>
</div>
<div className="space-y-4">
<Label>Sports Selection</Label>
<p className="text-sm text-muted-foreground">
Choose a template to auto-select sports, or manually select your own
</p>
<input type="hidden" name="templateId" value={selectedTemplate} />
{/* Template Cards */}
{templates.length > 0 && (
<div className="grid gap-3">
{templates.map((template) => (
<Card
key={template.id}
className={`cursor-pointer transition-all ${
selectedTemplate === template.id
? "ring-2 ring-primary"
: "hover:border-primary/50"
}`}
onClick={() => handleTemplateClick(template.id, template.sportsSeasonIds)}
>
<CardHeader className="pb-3">
<div className="flex items-start justify-between">
<div className="flex-1">
<CardTitle className="text-lg">{template.name}</CardTitle>
<CardDescription>{template.year}</CardDescription>
</div>
{selectedTemplate === template.id && (
<div className="rounded-full bg-primary text-primary-foreground p-1">
<Check className="h-4 w-4" />
</div>
)}
</div>
</CardHeader>
{template.description && (
<CardContent className="pt-0">
<p className="text-sm text-muted-foreground">{template.description}</p>
</CardContent>
)}
</Card>
))}
</div>
)}
{/* Sports Checkboxes */}
<div className="space-y-3">
<div className="bg-muted/50 border border-muted-foreground/20 rounded-md p-3">
<p className="text-sm text-muted-foreground">
<strong>Recommendation:</strong> Select 16-20 sports seasons for optimal league balance and variety.
</p>
</div>
<div className="space-y-2">
<Label className="text-sm font-medium">
Sports Seasons ({selectedSports.size} selected)
</Label>
<div className="max-h-64 overflow-y-auto border rounded-md p-3 space-y-2">
{allSportsSeasons.length > 0 ? (
allSportsSeasons.map((season) => (
<div key={season.id} className="flex items-center space-x-2">
<Checkbox
id={season.id}
name="sportsSeasons"
value={season.id}
checked={selectedSports.has(season.id)}
onCheckedChange={() => handleSportToggle(season.id)}
/>
<Label
htmlFor={season.id}
className="font-normal cursor-pointer flex-1"
>
<span className="font-medium">{season.sport.name}</span> - {season.name} ({season.year})
<Badge variant="outline" className="ml-2 text-xs">
{season.scoringType.replace("_", " ")}
</Badge>
</Label>
</div>
))
) : (
<p className="text-sm text-muted-foreground">
No sports seasons available
</p>
)}
</div>
</div>
</div>
</div>
{actionData?.error && (
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm">
{actionData.error}

123
package-lock.json generated
View file

@ -12,6 +12,7 @@
"@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-radio-group": "^1.3.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3",
"@react-router/express": "^7.7.1",
@ -19,6 +20,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"compression": "^1.8.0",
"drizzle-kit": "~0.28.1",
"drizzle-orm": "~0.36.3",
"express": "^5.1.0",
"isbot": "^5.1.27",
@ -45,7 +47,6 @@
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"dotenv-cli": "^8.0.0",
"drizzle-kit": "~0.28.1",
"tailwindcss": "^4.1.4",
"tsx": "^4.19.2",
"tw-animate-css": "^1.4.0",
@ -628,7 +629,6 @@
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz",
"integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@esbuild-kit/core-utils": {
@ -636,7 +636,6 @@
"resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz",
"integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==",
"deprecated": "Merged into tsx: https://tsx.is",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.18.20",
@ -650,7 +649,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -667,7 +665,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -684,7 +681,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -701,7 +697,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -718,7 +713,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -735,7 +729,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -752,7 +745,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -769,7 +761,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -786,7 +777,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -803,7 +793,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -820,7 +809,6 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -837,7 +825,6 @@
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -854,7 +841,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -871,7 +857,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -888,7 +873,6 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -905,7 +889,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -922,7 +905,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -939,7 +921,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -956,7 +937,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -973,7 +953,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -990,7 +969,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1007,7 +985,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1021,7 +998,6 @@
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
@ -1060,7 +1036,6 @@
"resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz",
"integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==",
"deprecated": "Merged into tsx: https://tsx.is",
"dev": true,
"license": "MIT",
"dependencies": {
"@esbuild-kit/core-utils": "^3.3.2",
@ -1074,7 +1049,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1091,7 +1065,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1108,7 +1081,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1125,7 +1097,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1142,7 +1113,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1159,7 +1129,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1176,7 +1145,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1193,7 +1161,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1210,7 +1177,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1227,7 +1193,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1244,7 +1209,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1261,7 +1225,6 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1278,7 +1241,6 @@
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1295,7 +1257,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1312,7 +1273,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1329,7 +1289,6 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1346,7 +1305,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1380,7 +1338,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1414,7 +1371,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1448,7 +1404,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1465,7 +1420,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1482,7 +1436,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -1499,7 +1452,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@ -2154,6 +2106,69 @@
}
}
},
"node_modules/@radix-ui/react-radio-group": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz",
"integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@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-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-roving-focus": "1.1.11",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-use-previous": "1.1.1",
"@radix-ui/react-use-size": "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-roving-focus": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
"integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
"license": "MIT",
"dependencies": {
"@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-id": "1.1.1",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"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-select": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
@ -3445,7 +3460,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true,
"license": "MIT"
},
"node_modules/bytes": {
@ -3821,7 +3835,6 @@
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.28.1.tgz",
"integrity": "sha512-JimOV+ystXTWMgZkLHYHf2w3oS28hxiH1FR0dkmJLc7GHzdGJoJAQtQS5DRppnabsRZwE2U1F6CuezVBgmsBBQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@drizzle-team/brocli": "^0.10.2",
@ -4070,7 +4083,6 @@
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
"integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
@ -4109,7 +4121,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz",
"integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.3.4"
@ -4358,7 +4369,6 @@
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.12.0.tgz",
"integrity": "sha512-LScr2aNr2FbjAjZh2C6X6BxRx1/x+aTDExct/xyq2XKbYOiG5c0aK7pMsSuyc0brz3ibr/lbQiHD9jzt4lccJw==",
"dev": true,
"license": "MIT",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
@ -5697,7 +5707,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
@ -5987,7 +5996,6 @@
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@ -6007,7 +6015,6 @@
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",

View file

@ -18,6 +18,7 @@
"@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-radio-group": "^1.3.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3",
"@react-router/express": "^7.7.1",