feat: add sports selection and team management to league settings
This commit is contained in:
parent
94586564e2
commit
0999e1c980
6 changed files with 490 additions and 67 deletions
42
app/components/ui/radio-group.tsx
Normal file
42
app/components/ui/radio-group.tsx
Normal 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 }
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
123
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue