Skip to content

Commit

Permalink
server action for race results associated with location in use
Browse files Browse the repository at this point in the history
  • Loading branch information
philipstubbs13 committed Sep 25, 2024
1 parent 36c1573 commit 253e6dc
Show file tree
Hide file tree
Showing 8 changed files with 275 additions and 35 deletions.
46 changes: 46 additions & 0 deletions app/actions/editRaceLocation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"use server";
import { db } from "@/lib/db";
import { getSessionUser } from "@/utils/getSessionUser";
import { Routes } from "@/utils/router/Routes.constants";
import { revalidatePath } from "next/cache";

async function editRaceLocation(
city: string,
state: string,
locationId: string
): Promise<void> {
const sessionUser = await getSessionUser();

if (!sessionUser || !sessionUser.userId) {
throw new Error("User ID is required");
}

const { userId } = sessionUser;

const existingRaceLocation = await db.raceLocation.findFirst({
where: { id: locationId },
});

if (existingRaceLocation?.userId.toString() !== userId) {
throw new Error("Current user does not own this race location");
}

await db.raceLocation.update({
where: { id: locationId },
data: {
city,
state,
},
});
await db.raceResult.updateMany({
where: { raceLocationId: locationId, userId },
data: {
city,
state,
},
});

revalidatePath(Routes.ManageRaceLocations, "layout");
}

export default editRaceLocation;
33 changes: 33 additions & 0 deletions app/actions/getRaceResultsAssociatedWithLocation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"use server";
import { IPersonalResult } from "@/components/personal-results/PersonalResults.types";
import { db } from "@/lib/db";
import { getSessionUser } from "@/utils/getSessionUser";
import { Routes } from "@/utils/router/Routes.constants";
import { revalidatePath } from "next/cache";

async function getRaceResultsAssociatedWithLocation(
locationId: string
): Promise<{
resultsAssociatedWithLocation: IPersonalResult[];
}> {
const sessionUser = await getSessionUser();

if (!sessionUser || !sessionUser.userId) {
throw new Error("User ID is required");
}

const { userId } = sessionUser;

const resultsAssociatedWithLocation: IPersonalResult[] =
await db.raceResult.findMany({
where: { raceLocationId: locationId, userId },
});

revalidatePath(Routes.ManageRaceLocations, "layout");

return {
resultsAssociatedWithLocation,
};
}

export default getRaceResultsAssociatedWithLocation;
9 changes: 5 additions & 4 deletions components/dialog/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ interface IProps extends ComponentProps<typeof UiDialog> {
/**
* The text displayed in the dialog's header. This is the main title of the dialog box.
*/
title: string;
title: string | ReactNode;
/**
* Determines the color of the dialog title. Accepts either "pink" or "blue".
* Determines the color of the dialog title.
*/
titleColor: "pink" | "blue";
titleColor: "pink" | "blue" | "orange";
/**
* A React element that serves as the trigger to open the dialog.
*/
trigger: ReactNode;
trigger?: ReactNode;
}

export const Dialog = (props: PropsWithChildren<IProps>) => {
Expand All @@ -37,6 +37,7 @@ export const Dialog = (props: PropsWithChildren<IProps>) => {
className={clsx({
"text-pink-600": props.titleColor === "pink",
"text-blue-600": props.titleColor === "blue",
"text-orange-600": props.titleColor === "orange",
})}
>
{props.title}
Expand Down
87 changes: 57 additions & 30 deletions components/locations-list/LocationsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import { states } from "../personal-results/PersonalResults.constants";
import addRaceLocation from "@/app/actions/addRaceLocation";
import { IRaceLocation } from "./LocationsList.types";
import deleteRaceLocation from "@/app/actions/deleteRaceLocation";
import { EditRaceLocationDialog } from "./edit-race-location-dialog/EditRaceLocationDialog";
import getRaceResultsAssociatedWithLocation from "@/app/actions/getRaceResultsAssociatedWithLocation";
import { LocationInUseDialog } from "./location-in-use-dialog/LocationInUseDialog";

interface IProps {
locations: IRaceLocation[];
Expand All @@ -28,6 +31,8 @@ export const LocationsList = (props: IProps) => {
const { toast } = useToast();
const [isConfirmationDialogOpen, setIsConfirmationDialogOpen] =
useState<boolean>(false);
const [isLocationInUseDialogOpen, setIsLocationInUseDialogOpen] =
useState<boolean>(false);

const handleRemoveLocation = async (locationId: string) => {
await deleteRaceLocation(locationId);
Expand All @@ -49,6 +54,19 @@ export const LocationsList = (props: IProps) => {
}
};

const confirmDeleteLocation = async (locationId: string) => {
const { resultsAssociatedWithLocation } =
await getRaceResultsAssociatedWithLocation(locationId);

if (resultsAssociatedWithLocation.length) {
setIsLocationInUseDialogOpen(true);

return;
}

setIsConfirmationDialogOpen(true);
};

return (
<>
<form action={handleAddLocation} className="flex space-x-2" ref={formRef}>
Expand Down Expand Up @@ -83,37 +101,46 @@ export const LocationsList = (props: IProps) => {
</Button>
</form>
<div className="space-y-2">
{props.locations.map((location, index) => (
<div
key={index}
className="flex justify-between items-center bg-gray-100 p-2 rounded"
>
<span>
{location.city}, {location.state}
</span>
<div>
{/* <EditRaceDistanceDialog
distance={distance.distance}
id={distance.id}
/> */}
<Button
variant="ghost"
size="sm"
onClick={() => setIsConfirmationDialogOpen(true)}
className="text-red-500 hover:text-red-700"
>
<Trash2 className="h-4 w-4" />
</Button>
<ConfirmationDialog
description={`This will permanently delete ${location.city}, ${location.state}.`}
isOpen={isConfirmationDialogOpen}
title="Are you sure you want to delete?"
onClose={() => setIsConfirmationDialogOpen(false)}
onConfirm={() => handleRemoveLocation(location.id)}
/>
{props.locations.map((location) => {
const description = `This will permanently delete the location.`;

return (
<div
key={`${location.city}-${location.state}=${location.id}`}
className="flex justify-between items-center bg-gray-100 p-2 rounded"
>
<span>
{location.city}, {location.state}
</span>
<div>
<EditRaceLocationDialog
city={location.city}
state={location.state}
locationId={location.id}
/>
<Button
variant="ghost"
size="sm"
onClick={() => confirmDeleteLocation(location.id)}
className="text-red-500 hover:text-red-700"
>
<Trash2 className="h-4 w-4" />
</Button>
<ConfirmationDialog
description={description}
isOpen={isConfirmationDialogOpen}
title="Are you sure you want to delete?"
onClose={() => setIsConfirmationDialogOpen(false)}
onConfirm={() => handleRemoveLocation(location.id)}
/>
<LocationInUseDialog
isOpen={isLocationInUseDialogOpen}
onClose={() => setIsLocationInUseDialogOpen(false)}
/>
</div>
</div>
</div>
))}
);
})}
</div>
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"use client";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Dialog } from "@/components/dialog/Dialog";
import { useState } from "react";
import { Pencil } from "lucide-react";
import { SubmitButton } from "@/components/buttons/submit-button/SubmitButton";
import { Tab } from "@/components/tabs/Tabs.constants";
import { useToast } from "@/hooks/use-toast";
import editRaceLocation from "@/app/actions/editRaceLocation";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { states } from "@/components/personal-results/PersonalResults.constants";

interface IProps {
city: string;
state: string;
locationId: string;
}

export const EditRaceLocationDialog = (props: IProps) => {
const [open, setOpen] = useState<boolean>(false);
const { toast } = useToast();

const onSubmit = async (formData: FormData) => {
const city = formData.get("city") as string;
const state = formData.get("state") as string;

await editRaceLocation(city, state, props.locationId);
toast({
title: "Successfully updated race location",
});
setOpen(false);
};

return (
<Dialog
open={open}
onOpenChange={setOpen}
title={"Edit Race Location"}
titleColor={"orange"}
trigger={
<Button size="sm">
<Pencil className="h-4 w-4" />
</Button>
}
>
<form action={onSubmit} className="space-y-4">
<Input
className="border-blue-300 focus:border-blue-500"
defaultValue={props.city}
name="city"
placeholder="City"
required={true}
/>
<Select name="state" required={true} defaultValue={props.state}>
<SelectTrigger className="border-blue-300 focus:border-blue-500">
<SelectValue
placeholder="Select State"
defaultValue={props.state}
/>
</SelectTrigger>
<SelectContent className="bg-white">
{states.map((state) => (
<SelectItem key={state.value} value={state.value}>
{state.label}
</SelectItem>
))}
</SelectContent>
</Select>
<SubmitButton tab={Tab.ManageLocations} />
</form>
</Dialog>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"use client";

import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { AlertCircle } from "lucide-react";

interface IProps {
isOpen: boolean;
onClose: () => void;
}

export const LocationInUseDialog = ({ isOpen, onClose }: IProps) => {
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[425px] bg-white">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-amber-600">
<AlertCircle className="h-5 w-5" />
Location In Use
</DialogTitle>
<DialogDescription>
The location cannot be deleted because it is associated with
existing race results.
</DialogDescription>
</DialogHeader>
<div className="py-4">
<p className="text-sm text-gray-500">
This location is currently used in at least one race result. To
delete this location, you must first remove or update all associated
race results.
</p>
</div>
<DialogFooter className="sm:justify-start">
<Button type="button" onClick={onClose} variant="outline">
Understood
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
4 changes: 3 additions & 1 deletion components/race-distances/RaceDistances.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ export const RaceDistances = (props: IProps) => {
<Trash2 className="h-4 w-4" />
</Button>
<ConfirmationDialog
description={`This will permanently delete ${distance.distance}.`}
description={
"This will permanently delete the selected race distance"
}
isOpen={isConfirmationDialogOpen}
title="Are you sure you want to delete?"
onClose={() => setIsConfirmationDialogOpen(false)}
Expand Down
2 changes: 2 additions & 0 deletions context/global-context/GlobalContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
import { usePathname, useRouter } from "next/navigation";
import { Tab } from "@/components/tabs/Tabs.constants";
import { shouldHideDashboardTabs } from "@/components/tabs/Tabs.utils";
import { useSession } from "next-auth/react";
import { ISessionUser } from "@/components/auth/Auth.types";

export interface IGlobalContext {
activeTab: Tab | null;
Expand Down

0 comments on commit 253e6dc

Please sign in to comment.