brackt/app/routes/leagues/$leagueId.settings.tsx

254 lines
8 KiB
TypeScript

import { useState } from "react";
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 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 {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "~/components/ui/alert-dialog";
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 to count teams
const season = await findCurrentSeason(leagueId);
const teams = season ? await findTeamsBySeasonId(season.id) : [];
return {
league,
teamCount: teams.length,
};
}
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");
if (intent === "delete") {
// Delete the league
await deleteLeague(leagueId);
// Redirect to home with success message
return redirect("/?deleted=true");
}
if (intent === "update") {
const name = formData.get("name");
// Validation
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" };
}
try {
await updateLeague(leagueId, {
name: name.trim(),
});
return redirect(`/leagues/${leagueId}?updated=true`);
} catch (error) {
console.error("Error updating league:", error);
return { error: "Failed to update league. Please try again." };
}
}
return { error: "Invalid action" };
}
export default function LeagueSettings({ loaderData, actionData }: Route.ComponentProps) {
const { league, teamCount } = loaderData;
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const handleDelete = () => {
setIsDeleteDialogOpen(false);
// The form submission will handle the actual deletion
};
return (
<div className="container max-w-2xl mx-auto py-8 px-4">
<div className="mb-8">
<div className="flex items-center justify-between mb-2">
<h1 className="text-4xl font-bold">League Settings</h1>
<Button variant="outline" asChild>
<Link to={`/leagues/${league.id}`}>Back to League</Link>
</Button>
</div>
<p className="text-muted-foreground">{league.name}</p>
</div>
<div className="space-y-6">
{/* Update League Form */}
<Card>
<CardHeader>
<CardTitle>General Settings</CardTitle>
<CardDescription>
Update your league's basic information
</CardDescription>
</CardHeader>
<CardContent>
<Form method="post" className="space-y-4">
<input type="hidden" name="intent" value="update" />
<div className="space-y-2">
<Label htmlFor="name">League Name</Label>
<Input
id="name"
name="name"
type="text"
defaultValue={league.name}
placeholder="Enter league name"
required
minLength={3}
maxLength={50}
/>
</div>
<div className="space-y-2">
<Label htmlFor="teamCount">Number of Teams</Label>
<Select name="teamCount" defaultValue={teamCount.toString()} disabled>
<SelectTrigger id="teamCount">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Array.from({ length: 11 }, (_, i) => i + 6).map((num) => (
<SelectItem key={num} value={num.toString()}>
{num} Teams
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
Team count cannot be changed after league creation
</p>
</div>
{actionData?.error && (
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm">
{actionData.error}
</div>
)}
<Button type="submit" className="w-full">
Save Changes
</Button>
</Form>
</CardContent>
</Card>
{/* Danger Zone */}
<Card className="border-destructive">
<CardHeader>
<CardTitle className="text-destructive">Danger Zone</CardTitle>
<CardDescription>
Irreversible and destructive actions
</CardDescription>
</CardHeader>
<CardContent>
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<AlertDialogTrigger asChild>
<Button variant="destructive" className="w-full">
Delete League
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This will permanently delete <strong>{league.name}</strong> and all
associated data including seasons, teams, and commissioners. This
action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<Form method="post" onSubmit={handleDelete}>
<input type="hidden" name="intent" value="delete" />
<AlertDialogAction type="submit" className="bg-destructive hover:bg-destructive/90">
Delete League
</AlertDialogAction>
</Form>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</CardContent>
</Card>
</div>
</div>
);
}