diff --git a/app/actions/addRaceLocation.ts b/app/actions/addRaceLocation.ts new file mode 100644 index 0000000..ef235fd --- /dev/null +++ b/app/actions/addRaceLocation.ts @@ -0,0 +1,27 @@ +"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 addRaceLocation(formData: FormData): Promise { + const sessionUser = await getSessionUser(); + + if (!sessionUser || !sessionUser.userId) { + throw new Error("User ID is required"); + } + + const { userId } = sessionUser; + + await db.raceLocation.create({ + data: { + city: formData.get("city") as string, + state: formData.get("state") as string, + userId, + }, + }); + + revalidatePath(Routes.ManageRaceLocations, "layout"); +} + +export default addRaceLocation; diff --git a/app/actions/addRaceResult.ts b/app/actions/addRaceResult.ts index ed75bed..a34487e 100644 --- a/app/actions/addRaceResult.ts +++ b/app/actions/addRaceResult.ts @@ -24,13 +24,19 @@ async function addRaceResult(formData: FormData): Promise { time, }; + const locationId = formData.get("location") as string; + const location = await db.raceLocation.findFirst({ + where: { id: locationId }, + }); + await db.raceResult.create({ data: { - city: (formData.get("city") as string) || "", + city: location?.city || "", date: new Date(raceResultData.date).toISOString(), race: raceResultData.race, raceDistance: formData.get("distance") as string, - state: (formData.get("state") as string) || "", + raceLocationId: locationId, + state: location?.state || "", time: raceResultData.time, userId, }, diff --git a/app/actions/deleteRaceLocation.ts b/app/actions/deleteRaceLocation.ts new file mode 100644 index 0000000..1b90821 --- /dev/null +++ b/app/actions/deleteRaceLocation.ts @@ -0,0 +1,31 @@ +"use server"; +import { db } from "@/lib/db"; +import { getSessionUser } from "../../utils/getSessionUser"; +import { revalidatePath } from "next/cache"; +import { Routes } from "@/utils/router/Routes.constants"; + +async function deleteRaceLocation(raceLocationId: string): Promise { + const sessionUser = await getSessionUser(); + + if (!sessionUser || !sessionUser.userId) { + throw new Error("User ID is required"); + } + + const { userId } = sessionUser; + + const raceLocation = await db.raceLocation.findFirst({ + where: { id: raceLocationId }, + }); + + if (!raceLocation) throw new Error("Race Location Not Found"); + + if (raceLocation.userId.toString() !== userId) { + throw new Error("Unauthorized"); + } + + await db.raceLocation.delete({ where: { id: raceLocationId } }); + + revalidatePath(Routes.ManageRaceLocations, "layout"); +} + +export default deleteRaceLocation; diff --git a/app/actions/editRaceResult.ts b/app/actions/editRaceResult.ts index 2299070..5810f62 100644 --- a/app/actions/editRaceResult.ts +++ b/app/actions/editRaceResult.ts @@ -30,14 +30,20 @@ async function editRaceResult( const time = `${hours}:${minutes}:${seconds}`; const date = formData.get("date") as string; + const locationId = formData.get("location") as string; + const location = await db.raceLocation.findFirst({ + where: { id: locationId }, + }); + await db.raceResult.update({ where: { id: raceResultId }, data: { - city: (formData.get("city") as string) || "", + city: location?.city || "", date: new Date(date).toISOString(), race: formData.get("race") as string, raceDistance: formData.get("distance") as string, - state: (formData.get("state") as string) || "", + raceLocationId: locationId, + state: location?.state || "", time, }, }); diff --git a/app/settings/manage-locations/page.tsx b/app/settings/manage-locations/page.tsx new file mode 100644 index 0000000..bfb8c79 --- /dev/null +++ b/app/settings/manage-locations/page.tsx @@ -0,0 +1,35 @@ +import Link from "next/link"; +import { ArrowLeft } from "lucide-react"; +import { Routes } from "@/utils/router/Routes.constants"; +import { TabCard } from "@/components/tab-card/TabCard"; +import { Tab } from "@/components/tabs/Tabs.constants"; +import { db } from "@/lib/db"; +import { getSessionUser } from "@/utils/getSessionUser"; +import { LayoutContainer } from "@/components/layout-container/LayoutContainer"; +import { LocationsList } from "@/components/locations-list/LocationsList"; + +export default async function ManageLocationsPage() { + const session = await getSessionUser(); + const locations = await db.raceLocation.findMany({ + where: { userId: session?.userId }, + }); + + return ( + + + + Back to Dashboard + + +
+
+ +
+
+
+
+ ); +} diff --git a/components/charts/distance-comparison-chart/DistanceComparisonChart.tsx b/components/charts/distance-comparison-chart/DistanceComparisonChart.tsx index 327103a..fbb0e7f 100644 --- a/components/charts/distance-comparison-chart/DistanceComparisonChart.tsx +++ b/components/charts/distance-comparison-chart/DistanceComparisonChart.tsx @@ -1,13 +1,6 @@ "use client"; -import { - Bar, - BarChart, - CartesianGrid, - LabelList, - XAxis, - YAxis, -} from "recharts"; +import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts"; import { Card, @@ -67,15 +60,7 @@ export const DistanceComparisonChart = (props: IProps) => { formatter={(value) => minutesToTime(Number(value))} /> } /> - - minutesToTime(Number(value))} - /> - + diff --git a/components/footer/Footer.tsx b/components/footer/Footer.tsx index cb0e964..eb8e8c6 100644 --- a/components/footer/Footer.tsx +++ b/components/footer/Footer.tsx @@ -38,6 +38,12 @@ export const Footer = () => { label="Upcoming Races" /> +
  • + +
    • diff --git a/components/header/Header.tsx b/components/header/Header.tsx index 3e743f5..dc2121b 100644 --- a/components/header/Header.tsx +++ b/components/header/Header.tsx @@ -1,7 +1,7 @@ "use client"; import { Button } from "@/components/ui/button"; -import { LogOut, Route, Settings } from "lucide-react"; +import { LogOut, MapPinHouse, Route, Settings } from "lucide-react"; import { signOut, useSession } from "next-auth/react"; import { useGlobalContext } from "@/context/global-context/GlobalContext"; import { Tab } from "@/components/tabs/Tabs.constants"; @@ -36,7 +36,7 @@ export const Header = () => { }; return ( -
      +
      @@ -91,6 +91,14 @@ export const Header = () => { Manage Race Distances + + +
      + {props.locations.map((location, index) => ( +
      + + {location.city}, {location.state} + +
      + {/* */} + + setIsConfirmationDialogOpen(false)} + onConfirm={() => handleRemoveLocation(location.id)} + /> +
      +
      + ))} +
      + + ); +}; diff --git a/components/locations-list/LocationsList.types.ts b/components/locations-list/LocationsList.types.ts new file mode 100644 index 0000000..f32899a --- /dev/null +++ b/components/locations-list/LocationsList.types.ts @@ -0,0 +1,7 @@ +export interface IRaceLocation { + createdAt: Date; + city: string; + id: string; + state: string; + userId: string; +} diff --git a/components/personal-results/PersonalResults.tsx b/components/personal-results/PersonalResults.tsx index ce9fc7a..65848f0 100644 --- a/components/personal-results/PersonalResults.tsx +++ b/components/personal-results/PersonalResults.tsx @@ -19,12 +19,19 @@ export const PersonalResults = async () => { const distances = await db.raceDistance.findMany({ where: { userId: sessionUser?.userId }, }); + const locations = await db.raceLocation.findMany({ + where: { userId: sessionUser?.userId }, + }); const hasResults = results.length > 0; return (
      {hasResults && ( - + )} {!hasResults && ( { /> )}
      - + -
      - +
      + + +

      Finish Time

      diff --git a/components/personal-results/personal-results-list/PersonalResultsList.stories.tsx b/components/personal-results/personal-results-list/PersonalResultsList.stories.tsx index edb2e84..9f7d075 100644 --- a/components/personal-results/personal-results-list/PersonalResultsList.stories.tsx +++ b/components/personal-results/personal-results-list/PersonalResultsList.stories.tsx @@ -24,6 +24,7 @@ const resultsMock: IPersonalResult[] = [ id: "1", race: "Heart of the City", time: "01:05:38", + raceLocationId: "1", }, { date: new Date("09/14/2024"), @@ -31,6 +32,7 @@ const resultsMock: IPersonalResult[] = [ id: "2", race: "Circle the Lake", time: "00:42:15", + raceLocationId: "2", }, { date: new Date("10/01/2022"), @@ -38,12 +40,14 @@ const resultsMock: IPersonalResult[] = [ id: "3", race: "Twin Cities Marathon", time: "03:30:15", + raceLocationId: "3", }, ]; export const Basic: Story = { args: { distances: [], + locations: [], results: resultsMock, }, }; @@ -51,6 +55,7 @@ export const Basic: Story = { export const EmptyList: Story = { args: { distances: [], + locations: [], results: [], }, }; diff --git a/components/personal-results/personal-results-list/PersonalResultsList.tsx b/components/personal-results/personal-results-list/PersonalResultsList.tsx index a8aaee5..20a90fc 100644 --- a/components/personal-results/personal-results-list/PersonalResultsList.tsx +++ b/components/personal-results/personal-results-list/PersonalResultsList.tsx @@ -8,6 +8,7 @@ import { columns, } from "@/components/personal-results/personal-results-table/Columns"; import { DataTable } from "@/components/data-table/DataTable"; +import { IRaceLocation } from "@/components/locations-list/LocationsList.types"; interface IProps { /** @@ -18,6 +19,10 @@ interface IProps { * List of distances to populate distance dropdown in add result dialog form. */ distances: IRaceDistance[]; + /** + * List of locations to populate location dropdown in add result dialog form. + */ + locations: IRaceLocation[]; } export const PersonalResultsList = (props: IProps) => { @@ -30,6 +35,7 @@ export const PersonalResultsList = (props: IProps) => { time: result.time, result, distances: props.distances, + locations: props.locations, })); return ; diff --git a/components/personal-results/personal-results-table/Columns.tsx b/components/personal-results/personal-results-table/Columns.tsx index febc7cd..984540f 100644 --- a/components/personal-results/personal-results-table/Columns.tsx +++ b/components/personal-results/personal-results-table/Columns.tsx @@ -6,6 +6,7 @@ import { ArrowUpDown } from "lucide-react"; import { IPersonalResult } from "@/components/personal-results/PersonalResults.types"; import { IRaceDistance } from "@/components/race-distances/RaceDistances.types"; import { PersonalResultsTableActions } from "./personal-results-table-actions/PersonalResultsTableActions"; +import { IRaceLocation } from "@/components/locations-list/LocationsList.types"; export type PersonalResultTableData = { id: string; @@ -16,6 +17,7 @@ export type PersonalResultTableData = { time: string; result: IPersonalResult; distances: IRaceDistance[]; + locations: IRaceLocation[]; }; export const columns: ColumnDef[] = [ diff --git a/components/personal-results/personal-results-table/personal-results-table-actions/PersonalResultsTableActions.tsx b/components/personal-results/personal-results-table/personal-results-table-actions/PersonalResultsTableActions.tsx index f668573..bb9e685 100644 --- a/components/personal-results/personal-results-table/personal-results-table-actions/PersonalResultsTableActions.tsx +++ b/components/personal-results/personal-results-table/personal-results-table-actions/PersonalResultsTableActions.tsx @@ -43,6 +43,7 @@ export const PersonalResultsTableActions = (props: IProps) => { />
      diff --git a/components/tabs/Tabs.constants.ts b/components/tabs/Tabs.constants.ts index b9b4660..e971ecf 100644 --- a/components/tabs/Tabs.constants.ts +++ b/components/tabs/Tabs.constants.ts @@ -5,6 +5,7 @@ export enum Tab { Settings = "settings", Stats = "/dashboard/stats", RaceDistances = "race-distances", + ManageLocations = "/settings/manage-locations", } export const tabLabels: Record = { @@ -14,6 +15,7 @@ export const tabLabels: Record = { [Tab.Settings]: "Manage Photos", [Tab.Stats]: "Stats", [Tab.RaceDistances]: "Manage Race Distances", + [Tab.ManageLocations]: "Manage Locations", }; export const tabDescriptions: Record = { @@ -23,6 +25,7 @@ export const tabDescriptions: Record = { [Tab.Settings]: "Edit and delete your photos", [Tab.Stats]: "Analyze your running performance", [Tab.RaceDistances]: "Add or remove race distances for your results", + [Tab.ManageLocations]: "Add or remove race locations for your results", }; export const tabTextColors: Record = { @@ -32,6 +35,7 @@ export const tabTextColors: Record = { [Tab.Settings]: "text-blue-600", [Tab.Stats]: "text-purple-600", [Tab.RaceDistances]: "text-blue-600", + [Tab.ManageLocations]: "text-orange-600", }; export const tabDescriptionColors: Record = { @@ -41,6 +45,7 @@ export const tabDescriptionColors: Record = { [Tab.Settings]: "text-blue-100", [Tab.Stats]: "text-purple-100", [Tab.RaceDistances]: "text-blue-100", + [Tab.ManageLocations]: "text-orange-100", }; export const tabBorderColors: Record = { @@ -50,6 +55,7 @@ export const tabBorderColors: Record = { [Tab.Settings]: "border-blue-200", [Tab.Stats]: "border-purple-200", [Tab.RaceDistances]: "border-blue-200", + [Tab.ManageLocations]: "border-orange-200", }; export const tabBackgroundColors: Record = { @@ -59,6 +65,7 @@ export const tabBackgroundColors: Record = { [Tab.Settings]: "bg-blue-500", [Tab.Stats]: "bg-purple-500", [Tab.RaceDistances]: "bg-blue-500", + [Tab.ManageLocations]: "bg-orange-500", }; export const tabHoverBackgroundColors: Record = { @@ -68,4 +75,5 @@ export const tabHoverBackgroundColors: Record = { [Tab.Settings]: "hover:bg-blue-600", [Tab.Stats]: "hover:bg-purple-600", [Tab.RaceDistances]: "hover:bg-blue-600", + [Tab.ManageLocations]: "hover:bg-orange-600", }; diff --git a/components/tabs/Tabs.tsx b/components/tabs/Tabs.tsx index fe7a88f..34b5145 100644 --- a/components/tabs/Tabs.tsx +++ b/components/tabs/Tabs.tsx @@ -15,7 +15,7 @@ export const Tabs = (props: PropsWithChildren) => { onValueChange={(value: string) => updateActiveTab(value as Tab)} > {!shouldHideTabs && ( - +