import { useState, useEffect } from "react"; import { Form, Link, redirect, useNavigation } from "react-router"; import { getAuth } from "@clerk/react-router/server"; import { format } from "date-fns"; import { CalendarIcon } from "lucide-react"; import { findLeagueById, updateLeague, deleteLeague } from "~/models/league"; import { sendStandingsUpdateNotification } from "~/services/discord"; import { isCommissioner, findCommissionersByLeagueId, createCommissioner, countCommissionersByLeagueId, removeCommissionerByLeagueAndUser, } from "~/models/commissioner"; import { findCurrentSeasonWithSports, updateSeason } from "~/models/season"; import { findTeamsBySeasonId, deleteTeam, removeTeamOwner, assignTeamOwner } from "~/models/team"; import { findAllUsers, findUserByClerkId, isUserAdminByClerkId } from "~/models/user"; import { unlinkSportFromSeason, linkMultipleSportsToSeason } from "~/models/season-sport"; import { findAllSportsSeasons } from "~/models/sports-season"; import { findDraftSlotsBySeasonId, setDraftOrder, randomizeDraftOrder } from "~/models/draft-slot"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { deleteAllDraftPicks } from "~/models/draft-pick"; import { clearAllQueuesForSeason } from "~/models/draft-queue"; import { deleteSeasonTimers } from "~/models/draft-timer"; import type { Route } from "./+types/$leagueId.settings"; import { Button } from "~/components/ui/button"; import { Input } from "~/components/ui/input"; import { Label } from "~/components/ui/label"; import { Calendar } from "~/components/ui/calendar"; import { Popover, PopoverContent, PopoverTrigger, } from "~/components/ui/popover"; import { cn } from "~/lib/utils"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "~/components/ui/card"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "~/components/ui/select"; import { Badge } from "~/components/ui/badge"; import { Checkbox } from "~/components/ui/checkbox"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "~/components/ui/alert-dialog"; import { ScoringRulesEditor } from "~/components/scoring/ScoringRulesEditor"; import { toast } from "sonner"; export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors { return [{ title: `Settings — ${data?.league?.name ?? "League"} - Brackt` }]; } export async function loader(args: Route.LoaderArgs) { const { userId } = await getAuth(args); const { params } = args; const { leagueId } = params; if (!userId) { throw new Response("Unauthorized", { status: 401 }); } // Fetch league const league = await findLeagueById(leagueId); if (!league) { throw new Response("League not found", { status: 404 }); } // Check if user is authorized (creator or commissioner) const userIsCommissioner = await isCommissioner(leagueId, userId); const isCreator = league.createdBy === userId; if (!isCreator && !userIsCommissioner) { throw new Response("Forbidden - You must be a commissioner to access settings", { status: 403, }); } // Get current season with sports const season = await findCurrentSeasonWithSports(leagueId); const teams = season ? await findTeamsBySeasonId(season.id) : []; const allSportsSeasons = await findAllSportsSeasons(); const draftSlots = season ? await findDraftSlotsBySeasonId(season.id) : []; // Count teams with owners const teamsWithOwners = teams.filter(team => team.ownerId !== null).length; // Check if user is admin const isAdmin = await isUserAdminByClerkId(userId); // Get all users (only for admins - used in team assignment dropdown) const allUsers = isAdmin ? await findAllUsers() : []; // Get commissioners for this league with user info const commissioners = await findCommissionersByLeagueId(leagueId); const commissionerUserData = await Promise.all( commissioners.map(async (c) => { const user = await findUserByClerkId(c.userId); return { ...c, userName: user?.username || user?.displayName || "Unknown User", }; }) ); // Get owner details for teams const ownerIds = teams .map((t) => t.ownerId) .filter((id): id is string => id !== null); const uniqueOwnerIds = [...new Set(ownerIds)]; const owners = await Promise.all( uniqueOwnerIds.map(async (ownerId) => { const user = await findUserByClerkId(ownerId); return user ? { clerkId: ownerId, name: user.username || user.displayName, id: user.id } : null; }) ); const validOwners = owners.filter((o): o is NonNullable => o !== null); const ownerMap = new Map(validOwners.map((o) => [o.clerkId, o.name])); // League members (team owners) - available to all commissioners for adding co-commissioners const leagueMembers = validOwners.map((o) => ({ id: o.id, clerkId: o.clerkId, name: o.name, })); return { league, season, teams, teamCount: teams.length, teamsWithOwners, allSportsSeasons: allSportsSeasons as Array, draftSlots, isAdmin, allUsers, leagueMembers, ownerMap: Object.fromEntries(ownerMap), commissioners: commissionerUserData, currentUserId: userId, }; } export async function action(args: Route.ActionArgs) { const { userId } = await getAuth(args); const { params, request } = args; const { leagueId } = params; if (!userId) { throw new Response("Unauthorized", { status: 401 }); } // Verify authorization const league = await findLeagueById(leagueId); if (!league) { throw new Response("League not found", { status: 404 }); } const userIsCommissioner = await isCommissioner(leagueId, userId); const isCreator = league.createdBy === userId; if (!isCreator && !userIsCommissioner) { throw new Response("Forbidden", { status: 403 }); } const formData = await request.formData(); const intent = formData.get("intent"); // For league-level settings (name, public draft board), save immediately // before checking season, so they work even if no season exists if (intent === "update") { const name = formData.get("name"); const isPublicDraftBoard = formData.get("isPublicDraftBoard") === "on"; const discordWebhookUrl = formData.get("discordWebhookUrl"); if (typeof name !== "string" || !name.trim()) { return { error: "League name is required" }; } if (name.trim().length < 3 || name.trim().length > 50) { return { error: "League name must be between 3 and 50 characters" }; } const webhookUrl = typeof discordWebhookUrl === "string" ? discordWebhookUrl.trim() : ""; if (webhookUrl && !webhookUrl.startsWith("https://discord.com/api/webhooks/")) { return { error: "Discord webhook URL must start with https://discord.com/api/webhooks/" }; } try { await updateLeague(leagueId, { name: name.trim(), isPublicDraftBoard, discordWebhookUrl: webhookUrl || null, }); } catch (error) { console.error("Error updating league:", error); return { error: "Failed to update league. Please try again." }; } } if (intent === "test-discord-webhook") { const webhookUrl = formData.get("webhookUrl"); if (typeof webhookUrl !== "string" || !webhookUrl.startsWith("https://discord.com/api/webhooks/")) { return { error: "Invalid Discord webhook URL" }; } try { const testSeason = await findCurrentSeasonWithSports(leagueId); const seasonName = testSeason ? `${league.name} ${testSeason.year}` : league.name; await sendStandingsUpdateNotification({ webhookUrl, seasonName, standings: [ { teamId: "1", teamName: "Team Alpha", totalPoints: 150, rank: 1 }, { teamId: "2", teamName: "Team Beta", totalPoints: 125, rank: 2 }, { teamId: "3", teamName: "Team Gamma", totalPoints: 100, rank: 3 }, ], previousStandings: new Map([ ["1", 125], ["2", 125], ["3", 100], ]), eventName: "Test Notification", }); return { testSuccess: true }; } catch (err) { console.error("Discord test webhook failed:", err); return { error: "Failed to send test notification. Check your webhook URL." }; } } // Get current season to check status const season = await findCurrentSeasonWithSports(leagueId); if (!season) { if (intent === "update") { return redirect(`/leagues/${leagueId}?updated=true`); } 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 === "set-draft-order") { if (season.status !== "pre_draft") { return { error: "Cannot modify draft order after draft has started" }; } const teams = await findTeamsBySeasonId(season.id); const teamIds = formData.getAll("teamOrder") as string[]; // Validate that all teams are included if (teamIds.length !== teams.length) { return { error: "All teams must be included in the draft order" }; } // Validate that all team IDs are valid const validTeamIds = new Set(teams.map(t => t.id)); for (const teamId of teamIds) { if (!validTeamIds.has(teamId)) { return { error: "Invalid team ID in draft order" }; } } await setDraftOrder(season.id, teamIds); return { success: true, message: "Draft order updated successfully" }; } if (intent === "randomize-draft-order") { if (season.status !== "pre_draft") { return { error: "Cannot modify draft order after draft has started" }; } const teams = await findTeamsBySeasonId(season.id); const teamIds = teams.map(t => t.id); await randomizeDraftOrder(season.id, teamIds); return { success: true, message: "Draft order randomized successfully" }; } if (intent === "remove-team-owner") { const teamId = formData.get("teamId") as string; if (!teamId) { return { error: "Team ID is required" }; } try { await removeTeamOwner(teamId); return { success: true, message: "Owner removed successfully" }; } catch (error) { console.error("Error removing team owner:", error); return { error: "Failed to remove owner. Please try again." }; } } if (intent === "assign-team-owner") { const teamId = formData.get("teamId") as string; const userClerkId = formData.get("userClerkId") as string; if (!teamId || !userClerkId) { return { error: "Team ID and User ID are required" }; } // Check if user is admin (only admins can assign owners) const isAdmin = await isUserAdminByClerkId(userId); if (!isAdmin) { return { error: "Only admins can assign team owners" }; } // Check if user is already assigned to a team in this season const teams = await findTeamsBySeasonId(season.id); const userAlreadyHasTeam = teams.some(team => team.ownerId === userClerkId); if (userAlreadyHasTeam) { return { error: "This user is already assigned to a team in this league" }; } try { await assignTeamOwner(teamId, userClerkId); return { success: true, message: "Owner assigned successfully" }; } catch (error) { console.error("Error assigning team owner:", error); return { error: "Failed to assign owner. Please try again." }; } } if (intent === "reset-draft") { // Check if user is admin (only admins can reset draft) const isAdmin = await isUserAdminByClerkId(userId); if (!isAdmin) { return { error: "Only admins can reset the draft" }; } try { // Delete all draft picks await deleteAllDraftPicks(season.id); // Clear all draft queues await clearAllQueuesForSeason(season.id); // Delete all draft timers await deleteSeasonTimers(season.id); // Set season status back to pre_draft and reset draft state (keeps draft order intact) await updateSeason(season.id, { status: "pre_draft", currentPickNumber: null, draftStartedAt: null, draftPaused: false, }); return { success: true, message: "Draft has been reset successfully. Draft order preserved." }; } catch (error) { console.error("Error resetting draft:", error); return { error: "Failed to reset draft. Please try again." }; } } if (intent === "delete") { // Delete the league await deleteLeague(leagueId); // Redirect to home with success message return redirect("/?deleted=true"); } if (intent === "update") { const teamCount = formData.get("teamCount"); const draftDateTime = formData.get("draftDateTime"); const draftRounds = formData.get("draftRounds"); const draftSpeed = formData.get("draftSpeed"); // Map draft speed to time values let draftInitialTime: number; let draftIncrementTime: number; switch (draftSpeed) { case "fast": draftInitialTime = 60; draftIncrementTime = 10; break; case "standard": draftInitialTime = 120; draftIncrementTime = 15; break; case "slow": draftInitialTime = 28800; // 8 hours draftIncrementTime = 3600; // 1 hour break; case "very-slow": draftInitialTime = 43200; // 12 hours draftIncrementTime = 3600; // 1 hour break; default: draftInitialTime = 120; draftIncrementTime = 15; } // League-level fields (name, isPublicDraftBoard) are already saved above. // Now handle season-specific updates. try { // Update season settings const seasonUpdates: any = {}; // Handle draft rounds if (typeof draftRounds === "string") { const draftRoundsNum = parseInt(draftRounds, 10); if (!isNaN(draftRoundsNum)) { const sportsCount = season.seasonSports?.length || 0; if (draftRoundsNum < sportsCount) { return { error: `Draft rounds must be at least ${sportsCount} (number of sports selected)` }; } if (draftRoundsNum < 1 || draftRoundsNum > 50) { return { error: "Draft rounds must be between 1 and 50" }; } seasonUpdates.draftRounds = draftRoundsNum; } } // Handle draft date/time if (typeof draftDateTime === "string") { seasonUpdates.draftDateTime = draftDateTime ? new Date(draftDateTime) : null; } // Set draft times from speed preset (only if draftSpeed was submitted; // the select is disabled during an active draft so it won't be present) if (draftSpeed !== null) { seasonUpdates.draftInitialTime = draftInitialTime; seasonUpdates.draftIncrementTime = draftIncrementTime; } // Handle scoring rules (only if in pre_draft status) if (season.status === "pre_draft") { const scoringFields = [ "pointsFor1st", "pointsFor2nd", "pointsFor3rd", "pointsFor4th", "pointsFor5th", "pointsFor6th", "pointsFor7th", "pointsFor8th" ]; for (const field of scoringFields) { const value = formData.get(field); if (typeof value === "string") { const points = parseInt(value, 10); if (!isNaN(points)) { if (points < 0 || points > 1000) { return { error: `${field} must be between 0 and 1000 points` }; } seasonUpdates[field] = points; } } } } // Update season if there are changes if (Object.keys(seasonUpdates).length > 0) { await updateSeason(season.id, seasonUpdates); } // 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) { const teamsToAdd = Array.from( { length: newTeamCount - currentTeamCount }, (_, i) => ({ seasonId: season.id, name: `Team ${currentTeamCount + i + 1}`, ownerId: null as null, }) ); // Fetch existing slots before the transaction to determine append position const existingSlots = await findDraftSlotsBySeasonId(season.id); const maxOrder = existingSlots.reduce((max, s) => Math.max(max, s.draftOrder), 0); // Create teams and their draft slots atomically const db = database(); await db.transaction(async (tx) => { const newTeams = await tx .insert(schema.teams) .values(teamsToAdd) .returning(); await tx.insert(schema.draftSlots).values( newTeams.map((team, i) => ({ seasonId: season.id, teamId: team.id, draftOrder: maxOrder + i + 1, })) ); }); } 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 season settings:", error); return { error: "Failed to update season settings. Please try again." }; } } if (intent === "add-commissioner") { const userClerkId = formData.get("userClerkId") as string; if (!userClerkId) { return { error: "User is required" }; } // Check if user is already a commissioner const alreadyCommissioner = await isCommissioner(leagueId, userClerkId); if (alreadyCommissioner) { return { error: "This user is already a commissioner" }; } try { await createCommissioner({ leagueId, userId: userClerkId }); return { success: true, message: "Commissioner added successfully" }; } catch (error) { console.error("Error adding commissioner:", error); return { error: "Failed to add commissioner. Please try again." }; } } if (intent === "remove-commissioner") { const commissionerUserId = formData.get("commissionerUserId") as string; if (!commissionerUserId) { return { error: "User is required" }; } // Prevent self-removal if (commissionerUserId === userId) { return { error: "You cannot remove yourself as a commissioner" }; } // Prevent removing the last commissioner const count = await countCommissionersByLeagueId(leagueId); if (count <= 1) { return { error: "Cannot remove the last commissioner" }; } try { await removeCommissionerByLeagueAndUser(leagueId, commissionerUserId); return { success: true, message: "Commissioner removed successfully" }; } catch (error) { console.error("Error removing commissioner:", error); return { error: "Failed to remove commissioner. Please try again." }; } } return { error: "Invalid action" }; } export default function LeagueSettings({ loaderData, actionData }: Route.ComponentProps) { const { league, season, teams, teamCount, teamsWithOwners, allSportsSeasons, draftSlots, isAdmin, allUsers, leagueMembers, ownerMap, commissioners, currentUserId } = loaderData; const navigation = useNavigation(); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [selectedSports, setSelectedSports] = useState>( new Set(season?.seasonSports?.map((s: any) => s.sportsSeason.id) || []) ); const [draftOrderTeams, setDraftOrderTeams] = useState( draftSlots.length > 0 ? draftSlots.map((slot) => slot.teamId) : teams.map((team) => team.id) ); const [draftRounds, setDraftRounds] = useState(season?.draftRounds || 20); const [draftDate, setDraftDate] = useState( season?.draftDateTime ? new Date(season.draftDateTime) : undefined ); const [draftTime, setDraftTime] = useState( season?.draftDateTime ? format(new Date(season.draftDateTime), "HH:mm") : "" ); // Update draft order when loader data changes (after randomization) useEffect(() => { const newOrder = draftSlots.length > 0 ? draftSlots.map((slot) => slot.teamId) : teams.map((team) => team.id); setDraftOrderTeams(newOrder); }, [draftSlots, teams]); const canEditSports = season && season.status === "pre_draft"; const canEditTeamCount = season && season.status === "pre_draft"; const canEditDraftOrder = season && season.status === "pre_draft"; const canEditDraftRounds = season && season.status === "pre_draft"; const minTeamCount = Math.max(6, teamsWithOwners); // Calculate flex spots and minimum rounds const sportsCount = selectedSports.size; const minRounds = sportsCount; const recommendedRounds = Math.ceil(sportsCount * 1.25); const flexSpots = Math.max(0, draftRounds - sportsCount); 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); // The form submission will handle the actual deletion }; const moveDraftSlot = (fromIndex: number, toIndex: number) => { const newOrder = [...draftOrderTeams]; const [movedTeam] = newOrder.splice(fromIndex, 1); newOrder.splice(toIndex, 0, movedTeam); setDraftOrderTeams(newOrder); }; const getTeamById = (teamId: string) => { return teams.find((t) => t.id === teamId); }; const [copied, setCopied] = useState(false); const handleCopyInviteLink = async () => { if (!season?.inviteCode) return; const inviteUrl = `${window.location.origin}/i/${season.inviteCode}`; try { await navigator.clipboard.writeText(inviteUrl); setCopied(true); toast.success("Invite link copied to clipboard!"); setTimeout(() => setCopied(false), 2000); } catch { toast.error("Failed to copy invite link"); } }; return (

League Settings

{league.name}

{season?.inviteCode && ( Invite Link Share this link to invite people to join your league {teamCount - teamsWithOwners > 0 && ` (${teamCount - teamsWithOwners} spot${teamCount - teamsWithOwners !== 1 ? "s" : ""} available)`}
e.currentTarget.select()} />

Anyone with this link can join your league

)}
{/* General Settings */} General Settings Update your league's basic information

{canEditTeamCount ? `Can be changed before draft starts (minimum ${minTeamCount} based on registered teams)` : "Team count cannot be changed after draft has started"}

{/* Notifications */} Notifications Get Discord alerts when standings change after scoring events

Create a webhook in your Discord server under Server Settings → Integrations → Webhooks.

{league.discordWebhookUrl && ( )} {actionData && "testSuccess" in actionData && actionData.testSuccess && (

Test notification sent to Discord!

)}
{/* Draft Rounds */} Draft Rounds {canEditDraftRounds ? "Set the number of draft rounds for your league" : "Draft rounds cannot be modified after the draft has started"}

Minimum: {minRounds} (number of sports selected) {recommendedRounds > minRounds && ` • Recommended: ${recommendedRounds}`}

{sportsCount > 0 && draftRounds < sportsCount && (

⚠️ You need at least {sportsCount} rounds for the {sportsCount} sports selected

)} {sportsCount > 0 && draftRounds >= sportsCount && (

Flex Spots: {flexSpots}

)}
date < new Date(new Date().setHours(0, 0, 0, 0))} /> setDraftTime(e.target.value)} placeholder="Select time" disabled={!canEditDraftRounds} />
{draftDate && draftTime && ( )} {!draftDate && !draftTime && ( )}

{canEditDraftRounds ? "You must set a draft date and time before starting the draft" : "Draft date cannot be changed after draft has started"}

Initial time + increment per pick

{/* Scoring Rules */} {/* Sports Seasons */} Sports Seasons {canEditSports ? "Select the sports seasons that teams can draft from" : "Sports cannot be modified after the draft has started"}

Recommendation: Select 16-20 sports seasons for optimal league balance and variety.

{allSportsSeasons.length > 0 ? ( allSportsSeasons.map((season) => (
handleSportToggle(season.id)} disabled={!canEditSports} />
)) ) : (

No sports seasons available

)}
{/* Save Button */} {actionData?.error && (
{actionData.error}
)} {actionData?.success && (
Settings updated successfully!
)}
{/* Draft Order Management */} Draft Order {canEditDraftOrder ? "Set the draft order for your league. Drag teams to reorder or randomize." : "Draft order cannot be changed after the draft has started"}
{draftSlots.length === 0 && canEditDraftOrder && (

Note: Draft order has not been set yet. Use the form below to set the order manually or click "Randomize" to generate a random order.

)}
{draftOrderTeams.map((teamId, index) => { const team = getTeamById(teamId); if (!team) return null; return (
{index + 1}

{team.name}

{canEditDraftOrder && (
)}
); })}
{canEditDraftOrder && (
)} {actionData?.success && actionData?.message && (
{actionData.message}
)} {actionData?.error && (
{actionData.error}
)}
{canEditDraftOrder && (
)}
{/* Team Management */} Team Management Manage team ownership for this league
{teams.map((team) => { const ownerName = team.ownerId ? ownerMap[team.ownerId] : null; return (

{team.name}

{team.ownerId ? ( Owner: {ownerName || "Unknown"} ) : ( No owner )}

{team.ownerId && (
)} {isAdmin && (
)}
); })}
{/* Commissioner Management */} Commissioner Management Commissioners can manage league settings and oversee the draft. They do not need to own a team. {actionData?.error && !actionData?.success && (
{actionData.error}
)} {actionData?.success && actionData?.message && (
{actionData.message}
)}
{commissioners.map((commissioner) => (

{commissioner.userName} {commissioner.userId === currentUserId && ( (you) )}

{!teams.some((t) => t.ownerId === commissioner.userId) && (

No team in current season

)}
{commissioners.length > 1 && commissioner.userId !== currentUserId && (
)}
))}

Add Commissioner

{/* Danger Zone */} Danger Zone Irreversible and destructive actions {/* Reset Draft - Admin Only */} {isAdmin && season && (

Reset Draft

Delete all draft picks, queues, and timers, and reset the season to pre-draft status. The draft order will be preserved.

Reset the draft? This will delete all draft picks, draft queues, and draft timers, and set the season back to pre-draft status. The draft order will remain intact. This action cannot be undone. Cancel
Reset Draft
)} {/* Delete League */}

Delete League

Permanently delete this league and all associated data.

Are you absolutely sure? This will permanently delete {league.name} and all associated data including seasons, teams, and commissioners. This action cannot be undone. Cancel
Delete League
); }