diff --git a/package.json b/package.json index 85997c4..6013916 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "react-dom": "18.2.0", "react-hook-form": "^7.44.1", "react-toastify": "^9.1.3", + "recharts": "^2.10.1", "swiper": "^11.0.4", "tailwind-merge": "^1.13.2", "tailwindcss": "3.3.2", diff --git a/prisma/migrations/20231126134440_add_activityid_column_optional/migration.sql b/prisma/migrations/20231126134440_add_activityid_column_optional/migration.sql new file mode 100644 index 0000000..a84fb12 --- /dev/null +++ b/prisma/migrations/20231126134440_add_activityid_column_optional/migration.sql @@ -0,0 +1,20 @@ +/* + Warnings: + + - A unique constraint covering the columns `[activityId]` on the table `Session` will be added. If there are existing duplicate values, this will fail. + +*/ +-- DropForeignKey +ALTER TABLE "Set" DROP CONSTRAINT "Set_sessionId_fkey"; + +-- AlterTable +ALTER TABLE "Session" ADD COLUMN "activityId" UUID; + +-- CreateIndex +CREATE UNIQUE INDEX "Session_activityId_key" ON "Session"("activityId"); + +-- AddForeignKey +ALTER TABLE "Session" ADD CONSTRAINT "Session_activityId_fkey" FOREIGN KEY ("activityId") REFERENCES "Activity"("activityId") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Set" ADD CONSTRAINT "Set_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "Session"("sessionId") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20231126134657_make_userid_and_activityid_required/migration.sql b/prisma/migrations/20231126134657_make_userid_and_activityid_required/migration.sql new file mode 100644 index 0000000..bd26143 --- /dev/null +++ b/prisma/migrations/20231126134657_make_userid_and_activityid_required/migration.sql @@ -0,0 +1,22 @@ +/* + Warnings: + + - Made the column `userId` on table `Session` required. This step will fail if there are existing NULL values in that column. + - Made the column `activityId` on table `Session` required. This step will fail if there are existing NULL values in that column. + +*/ +-- DropForeignKey +ALTER TABLE "Session" DROP CONSTRAINT "Session_activityId_fkey"; + +-- DropForeignKey +ALTER TABLE "Session" DROP CONSTRAINT "Session_userId_fkey"; + +-- AlterTable +ALTER TABLE "Session" ALTER COLUMN "userId" SET NOT NULL, +ALTER COLUMN "activityId" SET NOT NULL; + +-- AddForeignKey +ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Session" ADD CONSTRAINT "Session_activityId_fkey" FOREIGN KEY ("activityId") REFERENCES "Activity"("activityId") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20231126135005_change_userid_type/migration.sql b/prisma/migrations/20231126135005_change_userid_type/migration.sql new file mode 100644 index 0000000..f77a16b --- /dev/null +++ b/prisma/migrations/20231126135005_change_userid_type/migration.sql @@ -0,0 +1,8 @@ +-- DropForeignKey +ALTER TABLE "Session" DROP CONSTRAINT "Session_userId_fkey"; + +-- AlterTable +ALTER TABLE "Session" ALTER COLUMN "userId" SET DATA TYPE TEXT; + +-- AddForeignKey +ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20231126140814_change_relation/migration.sql b/prisma/migrations/20231126140814_change_relation/migration.sql new file mode 100644 index 0000000..0d8c4da --- /dev/null +++ b/prisma/migrations/20231126140814_change_relation/migration.sql @@ -0,0 +1,25 @@ +/* + Warnings: + + - You are about to drop the column `activityId` on the `Session` table. All the data in the column will be lost. + - A unique constraint covering the columns `[sessionId]` on the table `Activity` will be added. If there are existing duplicate values, this will fail. + - Added the required column `sessionId` to the `Activity` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "Session" DROP CONSTRAINT "Session_activityId_fkey"; + +-- DropIndex +DROP INDEX "Session_activityId_key"; + +-- AlterTable +ALTER TABLE "Activity" ADD COLUMN "sessionId" UUID NOT NULL; + +-- AlterTable +ALTER TABLE "Session" DROP COLUMN "activityId"; + +-- CreateIndex +CREATE UNIQUE INDEX "Activity_sessionId_key" ON "Activity"("sessionId"); + +-- AddForeignKey +ALTER TABLE "Activity" ADD CONSTRAINT "Activity_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "Session"("sessionId") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20231126161443_add_run_id_column_to_activity/migration.sql b/prisma/migrations/20231126161443_add_run_id_column_to_activity/migration.sql new file mode 100644 index 0000000..68fa08f --- /dev/null +++ b/prisma/migrations/20231126161443_add_run_id_column_to_activity/migration.sql @@ -0,0 +1,15 @@ +/* + Warnings: + + - A unique constraint covering the columns `[runId]` on the table `Activity` will be added. If there are existing duplicate values, this will fail. + +*/ +-- AlterTable +ALTER TABLE "Activity" ADD COLUMN "runId" UUID, +ALTER COLUMN "sessionId" DROP NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "Activity_runId_key" ON "Activity"("runId"); + +-- AddForeignKey +ALTER TABLE "Activity" ADD CONSTRAINT "Activity_runId_fkey" FOREIGN KEY ("runId") REFERENCES "Run"("runId") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d7322e5..13fb8db 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -49,14 +49,19 @@ model Profile { // Activity Table model Activity { - activityId String @id @unique @default(uuid()) @db.Uuid - updatedAt DateTime @default(now()) @updatedAt - createdAt DateTime @default(now()) + activityId String @id @unique @default(uuid()) @db.Uuid + updatedAt DateTime @default(now()) @updatedAt + createdAt DateTime @default(now()) // relations - runExercise RunExercise? + run Run? @relation(fields: [runId], references: [runId], onDelete: Cascade) + runId String? @unique @db.Uuid - user User @relation(fields: [userId], references: [userId], onDelete: Cascade) - userId String + session Session? @relation(fields: [sessionId], references: [sessionId], onDelete: Cascade) + sessionId String? @unique @db.Uuid + + user User @relation(fields: [userId], references: [userId], onDelete: Cascade) + userId String + RunExercise RunExercise? } // Exercises Table @@ -95,8 +100,10 @@ model Session { planId String? @db.Uuid currActiveSesh CurrActiveSesh? - User User? @relation(fields: [userId], references: [id]) - userId Int? + user User @relation(fields: [userId], references: [userId], onDelete: Cascade) + userId String + + activity Activity? } // Exercise History Table @@ -110,7 +117,7 @@ model Set { note String? // relations - session Session @relation(fields: [sessionId], references: [sessionId]) + session Session @relation(fields: [sessionId], references: [sessionId], onDelete: Cascade) sessionId String @db.Uuid exercise Exercise @relation(fields: [exerciseId], references: [exerciseId]) @@ -180,6 +187,7 @@ model Run { // relation runExercise RunExercise @relation(fields: [exerciseId], references: [exerciseId]) exerciseId String @db.Uuid + Activity Activity? } model GymLocation { diff --git a/src/components/CurrActiveSesh/Set.tsx b/src/components/CurrActiveSesh/Set.tsx index d4e05c5..776aa3f 100644 --- a/src/components/CurrActiveSesh/Set.tsx +++ b/src/components/CurrActiveSesh/Set.tsx @@ -19,6 +19,7 @@ interface SetProps { sessionId: string unit: string isDone?: boolean + exerciseName: string | undefined } export default function Set({ @@ -30,6 +31,7 @@ export default function Set({ sessionId, unit, isDone, + exerciseName, }: SetProps) { const { addToDb } = useCurrActiveSeshIndexDb() @@ -42,6 +44,7 @@ export default function Set({ sessionId, exerciseId, setNumber, + exerciseName: exerciseName ?? "", }) return ( diff --git a/src/components/CurrActiveSesh/SetGrid.tsx b/src/components/CurrActiveSesh/SetGrid.tsx index c0b13a0..fd18fd2 100644 --- a/src/components/CurrActiveSesh/SetGrid.tsx +++ b/src/components/CurrActiveSesh/SetGrid.tsx @@ -55,6 +55,7 @@ export default function SetGrid({ sets, currActiveExercise }: SetGridProps) { unit={unit} frontendSetId={frontendSetId} isDone={isDone} + exerciseName={currActiveExercise?.name} /> ) ) diff --git a/src/components/CurrActiveSesh/index.tsx b/src/components/CurrActiveSesh/index.tsx index 48193fc..45d7278 100644 --- a/src/components/CurrActiveSesh/index.tsx +++ b/src/components/CurrActiveSesh/index.tsx @@ -2,6 +2,8 @@ import React, { useState, useMemo } from "react" import { useRouter } from "next/router" import Pino from "pino" import { z } from "zod" +// types +import { ISet } from "src/types/curr-active-sesh" // utils import { trpc } from "src/utils/trpc" // validators @@ -14,6 +16,7 @@ import useCurrActiveSeshIndexDb, { IndexedDBStore, } from "src/hooks/useCurrActiveSeshIndexDb" import useToastMessage, { ToastMessage } from "src/hooks/useToastMessage" +import useGetSetsByExerciseId from "src/hooks/useGetSetsByExerciseId" // lodash import { capitalize } from "lodash" // components @@ -43,17 +46,6 @@ export interface Exercise { targetSets: number | null } -export interface ISet { - name: Exercise["name"] - weight: Exercise["unit"] - reps: Exercise["targetReps"] - setNumber: number - exerciseId: Exercise["exerciseId"] - sessionId: string - unit: Exercise["unit"] - isDone: boolean -} - export default function CurrActiveSeshContainer() { const utils = trpc.useContext() const { getAllFromDb, clearDb } = useCurrActiveSeshIndexDb() @@ -75,6 +67,9 @@ export default function CurrActiveSeshContainer() { const [isCompleteWorkout, setIsCompleteWorkout] = useState(false) const [isCompleteIncompleteWorkout, setIsCompleteIncompleteWorkout] = useState(false) + const { data: exerciseSetList } = useGetSetsByExerciseId( + currActiveExercise?.exerciseId ?? "" + ) const exerciseHashmap = useMemo(() => { const exercisesArr = sessionRes?.workoutPlan?.exercises @@ -116,14 +111,16 @@ export default function CurrActiveSeshContainer() { return list }, [exerciseHashmap, exercisesList]) + const mostRecentWeight = exerciseSetList?.[0]?.weight?.toString() ?? "0" + const sets = useMemo(() => { if (!currActiveExercise?.targetSets) return [] const sets: ISet[] = [] for (let i = 0; i < currActiveExercise.targetSets; i++) { sets.push({ setNumber: i + 1, - name: currActiveExercise.name, - weight: "0", // TODO: add last weight exercise was completed in + exerciseName: currActiveExercise.name, + weight: mostRecentWeight ?? "0", reps: currActiveExercise.targetReps, unit: currActiveExercise.unit, exerciseId: currActiveExercise.exerciseId, @@ -132,13 +129,20 @@ export default function CurrActiveSeshContainer() { }) } return sets - }, [currActiveExercise, currActiveSeshId]) + }, [currActiveExercise, currActiveSeshId, mostRecentWeight]) const handleCompleteWorkout = async () => { // get saved things from db const completedSets = (await getAllFromDb( IndexedDBStore.CurrActiveSesh )) as ISet[] + const exerciseOrderArr = sessionRes?.workoutPlan?.exerciseOrder + if (!exerciseOrderArr) return + const sortedCompletedSets = completedSets.sort( + (a, b) => + exerciseOrderArr.indexOf(a.exerciseId) - + exerciseOrderArr.indexOf(b.exerciseId) + ) // calculate total sets to be completed let targetTotalSets = 0 @@ -150,7 +154,7 @@ export default function CurrActiveSeshContainer() { } // if not all set fields are touched - if (completedSets.length !== targetTotalSets) { + if (sortedCompletedSets.length !== targetTotalSets) { setIsCompleteIncompleteWorkout(true) } // modal for complete workout @@ -164,10 +168,17 @@ export default function CurrActiveSeshContainer() { const completedSets = (await getAllFromDb( IndexedDBStore.CurrActiveSesh )) as ISet[] + const exerciseOrderArr = sessionRes?.workoutPlan?.exerciseOrder + if (!exerciseOrderArr) return + const sortedCompletedSets = completedSets.sort( + (a, b) => + exerciseOrderArr.indexOf(a.exerciseId) - + exerciseOrderArr.indexOf(b.exerciseId) + ) // validate try { - const validated = CompletedSetsSchema.parse(completedSets) + const validated = CompletedSetsSchema.parse(sortedCompletedSets) // mutate mutate(validated, { onSuccess: () => { @@ -200,7 +211,7 @@ export default function CurrActiveSeshContainer() { router.push({ pathname: "/workouts/curr-active-workout/summary", query: { - data: JSON.stringify(completedSets), + data: JSON.stringify(sortedCompletedSets), }, }) } diff --git a/src/components/HomePage/RecentActivity.tsx b/src/components/HomePage/RecentActivity.tsx index 6f7d9bc..2e57cf8 100644 --- a/src/components/HomePage/RecentActivity.tsx +++ b/src/components/HomePage/RecentActivity.tsx @@ -34,7 +34,11 @@ export default function RecentActivity({ return ( -
{recentActivitiesCards}
+
+ {recentActivitiesCards.length > 0 + ? recentActivitiesCards + : "Start a workout to get started :)"} +
) } diff --git a/src/components/HomePage/TopStats.tsx b/src/components/HomePage/TopStats.tsx index e605470..29e0628 100644 --- a/src/components/HomePage/TopStats.tsx +++ b/src/components/HomePage/TopStats.tsx @@ -1,8 +1,20 @@ import React from "react" +// recharts +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + Label, +} from "recharts" // components import ParentCard from "../UI/ParentCard" import { ITopStats } from "src/types/home-page" import SecondaryCard from "../UI/SecondaryCard" +import Text from "../UI/typography/Text" interface TopStatsProps { topStats: ITopStats @@ -39,7 +51,37 @@ export default function TopStats({ topStats }: TopStatsProps) {

- Recharts Graph Here for Random Lift + + + + + + + + + + + + + ) diff --git a/src/components/HomePage/index.tsx b/src/components/HomePage/index.tsx index a30d4c1..c9aabbf 100644 --- a/src/components/HomePage/index.tsx +++ b/src/components/HomePage/index.tsx @@ -8,6 +8,7 @@ import SecondaryButton from "../UI/SecondaryButton" // types import { StatsOutput, UserAttributesOutput } from "src/types/trpc/router-types" import { ITopStats, WeightLifted } from "src/types/home-page" +import Units from "src/constants/units" const defaultWeight: WeightLifted = { weight: 0, @@ -21,7 +22,11 @@ const defaultTopStats: ITopStats = { deadlift: defaultWeight, }, weightLiftedTotal: defaultWeight, - randomGraph: {}, + randomGraph: { + exerciseName: "", + data: [], + unit: Units.KG, + }, } interface HomePageProps { userAttributes: UserAttributesOutput | undefined diff --git a/src/components/Summary/index.tsx b/src/components/Summary/index.tsx new file mode 100644 index 0000000..516b26e --- /dev/null +++ b/src/components/Summary/index.tsx @@ -0,0 +1,88 @@ +import React from "react" +import { useRouter } from "next/router" +// types +import { ISet } from "src/types/curr-active-sesh" +// components +import ParentCard from "../UI/ParentCard" +import Text, { Typography } from "../UI/typography/Text" +import PrimaryButton from "../UI/PrimaryButton" + +interface SummaryProps { + completedSets: ISet[] +} + +export default function Summary({ completedSets }: SummaryProps) { + const router = useRouter() + + const completedSetsExerciseHash = (() => { + const hash: Map = new Map() + for (const set of completedSets) { + if (!hash.has(set.exerciseId)) { + hash.set(set.exerciseId, [set]) + } else { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + hash.set(set.exerciseId, [...hash.get(set.exerciseId)!, set]) + } + } + return hash + })() + + const displayExercises = (() => { + const displayExercisesArr: React.ReactNode[] = [] + + completedSetsExerciseHash.forEach((completedExercisesArr, exerciseId) => { + const exercises: React.ReactNode[] = [] + for (const exercise of completedExercisesArr.sort((a, b) => { + if (a.setNumber > b.setNumber) { + return 1 + } + if (a.setNumber < b.setNumber) { + return -1 + } + return 0 + })) { + exercises.push( +
+
+ {exercise.setNumber} +
+
{exercise.weight}
+
{exercise.unit}
+
{exercise.reps}
+
+ ) + } + return displayExercisesArr.push( + +
+
Set
+
Weight
+
Unit
+
Reps
+
+ {exercises} +
+ ) + }) + return displayExercisesArr + })() + + return ( +
+ +
{displayExercises}
+ router.push("/workouts")} + /> +
+ ) +} diff --git a/src/components/Workouts/PlanCard.tsx b/src/components/Workouts/PlanCard.tsx index a8aaf67..a916c4e 100644 --- a/src/components/Workouts/PlanCard.tsx +++ b/src/components/Workouts/PlanCard.tsx @@ -41,7 +41,7 @@ export default function PlanCard({ return (
{ e.stopPropagation() if (!isCurrActiveSeshPresent) { diff --git a/src/components/Workouts/index.tsx b/src/components/Workouts/index.tsx index 1c08dbe..74082e9 100644 --- a/src/components/Workouts/index.tsx +++ b/src/components/Workouts/index.tsx @@ -61,6 +61,15 @@ export default function Workouts({ plans, gymLocations }: WorkoutsProps) { plans.length > 0 && plans .filter((plan) => plan.gymId === selectedGymLocation) + .sort((a, b) => { + if (a.name < b.name) { + return -1 + } + if (a.name > b.name) { + return 1 + } + return 0 + }) .map(({ planId, name, lastWorkout, duration, gymLocation }) => (
({ - id: index, - date: 1688008833 * 1000, // unix milliseconds + id: index.toString(), + date: String(1688008833 * 1000), // unix milliseconds workoutName: `Deezed-${index}`, workoutDuration: Number( (new Date().getTime() - 1688008833 * 1000).toFixed(2) // unix milliseconds @@ -25,5 +26,9 @@ export const MOCK_TOP_STATS: ITopStats = { deadlift: defaultWeight, }, weightLiftedTotal: defaultWeight, - randomGraph: {}, + randomGraph: { + data: [], + exerciseName: "", + unit: Units.KG, + }, } diff --git a/src/pages/workouts/curr-active-workout/summary.tsx b/src/pages/workouts/curr-active-workout/summary.tsx index bd421d1..8a0d325 100644 --- a/src/pages/workouts/curr-active-workout/summary.tsx +++ b/src/pages/workouts/curr-active-workout/summary.tsx @@ -2,17 +2,19 @@ import React from "react" import { useRouter } from "next/router" // components import MainLayout from "src/components/MainLayout" +import Summary from "../../../components/Summary" +import { ISet } from "src/types/curr-active-sesh" export default function SummaryPage() { const router = useRouter() const { data } = router.query - const completedSets = data && JSON.parse(data as string) + const completedSets = data && (JSON.parse(data as string) as ISet[]) if (!completedSets || !completedSets.length) { - router.push("/workouts") + return router.push("/workouts") } - return
summary
+ return } SummaryPage.getLayout = function getLayout(page: React.ReactElement) { diff --git a/src/server/procedures/curr-active-sesh-procedures/completed-workout.ts b/src/server/procedures/curr-active-sesh-procedures/completed-workout.ts index 6cf7074..57f8294 100644 --- a/src/server/procedures/curr-active-sesh-procedures/completed-workout.ts +++ b/src/server/procedures/curr-active-sesh-procedures/completed-workout.ts @@ -7,19 +7,36 @@ const completedWorkout = tProtectedProcedure .input(CompletedSetsSchema) .mutation(async ({ input, ctx: { user } }) => { try { + const endDate = new Date() + const endDateISO = new Date().toISOString() const sessionId = input[0].sessionId + // close session (add end duration) - await prisma.session.update({ + const updatedSession = await prisma.session.update({ where: { sessionId, }, data: { - endDuration: new Date().toISOString(), + endDuration: endDateISO, }, }) - // update Set table - const setsToInput = input.map((obj) => ({ + const workoutPlanId = updatedSession.planId + const duration = + endDate.getTime() - updatedSession.startDuration.getTime() + + // update Set table, want to input in order for delay one at a time so that the latest set is the most recent weight + const sortedInput = input.sort((a, b) => { + if (a.setNumber > b.setNumber) { + return 1 + } + if (a.setNumber < b.setNumber) { + return -1 + } + return 0 + }) + + const setsToInput = sortedInput.map((obj) => ({ weight: Number(obj.weight), unit: obj.unit, reps: Number(obj.reps), @@ -27,9 +44,21 @@ const completedWorkout = tProtectedProcedure exerciseId: obj.exerciseId, })) - await prisma.set.createMany({ - data: setsToInput, - }) + const createSet = async (data: { + weight: number + unit: string + reps: number + sessionId: string + exerciseId: string + }) => { + await prisma.set.create({ + data, + }) + } + + for await (const set of setsToInput) { + await createSet(set) + } // close curractivesesh (delete by userid) await prisma.currActiveSesh.delete({ @@ -38,6 +67,18 @@ const completedWorkout = tProtectedProcedure }, }) + if (workoutPlanId) { + await prisma.workoutPlan.update({ + where: { + planId: workoutPlanId, + }, + data: { + lastWorkout: endDateISO, + duration, + }, + }) + } + return "OK" } catch (error) { throw new TRPCError({ diff --git a/src/server/procedures/curr-active-sesh-procedures/get-curr-active-sesh.ts b/src/server/procedures/curr-active-sesh-procedures/get-curr-active-sesh.ts index bb47f20..c9e8bd5 100644 --- a/src/server/procedures/curr-active-sesh-procedures/get-curr-active-sesh.ts +++ b/src/server/procedures/curr-active-sesh-procedures/get-curr-active-sesh.ts @@ -25,6 +25,7 @@ const getCurrActiveSesh = tProtectedProcedure const newSession = await prisma.session.create({ data: { planId: workoutPlanId, + userId: user.id, }, }) @@ -42,6 +43,13 @@ const getCurrActiveSesh = tProtectedProcedure }, }, }) + + await prisma.activity.create({ + data: { + userId: user.id, + sessionId: newSession.sessionId, + }, + }) } return currActiveSesh diff --git a/src/server/procedures/session-procedures/get-session.ts b/src/server/procedures/session-procedures/get-session.ts index ce0f118..fce23f5 100644 --- a/src/server/procedures/session-procedures/get-session.ts +++ b/src/server/procedures/session-procedures/get-session.ts @@ -24,6 +24,7 @@ const getSession = tProtectedProcedure throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Something went wrong", + cause: error, }) } }) diff --git a/src/server/procedures/sets/get-sets-by-exercise-id.ts b/src/server/procedures/sets/get-sets-by-exercise-id.ts new file mode 100644 index 0000000..903d157 --- /dev/null +++ b/src/server/procedures/sets/get-sets-by-exercise-id.ts @@ -0,0 +1,29 @@ +import { TRPCError } from "@trpc/server" +import { tProtectedProcedure } from "src/server/trpc" +import prisma from "src/utils/prisma" +import { z } from "zod" + +const getSetsByExerciseId = tProtectedProcedure + // uuid exerciseId + .input(z.string()) + .query(async ({ input }) => { + try { + const sets = await prisma.set.findMany({ + where: { + exerciseId: input, + }, + orderBy: { + createdAt: "desc", + }, + }) + return sets + } catch (error) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Something went wrong", + cause: error, + }) + } + }) + +export default getSetsByExerciseId diff --git a/src/server/procedures/stats/get-stats.ts b/src/server/procedures/stats/get-stats.ts new file mode 100644 index 0000000..fcb767a --- /dev/null +++ b/src/server/procedures/stats/get-stats.ts @@ -0,0 +1,342 @@ +import { TRPCError } from "@trpc/server" +import { tProtectedProcedure } from "src/server/trpc" +import prisma from "src/utils/prisma" +// utils +import JsDateUtils from "src/utils/js-date-utils" +import { lbsToKg } from "src/utils/unit-conversion" +import unixToIsoDate from "src/utils/unix-to-ISO-date" +// types +import { IRecentActivity, ITopStats, RechartsData } from "src/types/home-page" +import Units from "src/constants/units" + +const getStats = tProtectedProcedure.query(async ({ ctx: { user } }) => { + try { + const activities = await prisma.activity.findMany({ + where: { + userId: user.id, + }, + orderBy: { + createdAt: "desc", + }, + }) + + /* -------------------------------------------------------------------------- */ + /* processing */ + /* -------------------------------------------------------------------------- */ + const last5Activities = activities.slice(0, 5) + + // idsArr + const runIdsArr: string[] = [] + const sessionIdsArr: string[] = [] + for (const activity of last5Activities) { + if (activity?.runId) { + runIdsArr.push(activity.runId) + } + if (activity?.sessionId) { + sessionIdsArr.push(activity.sessionId) + } + } + + // runs + const runs = await prisma.run.findMany({ + where: { + runId: { + in: runIdsArr, + }, + }, + }) + const runsHash = (() => { + const hash: Record< + string, + { + runId: string + updatedAt: Date + createdAt: Date + duration: number + speed: number + speedUnits: string + speedPerTimeUnit: string + actualDistance: number + actualDistanceUnits: string + note: string | null + exerciseId: string + } + > = {} + for (const run of runs) { + if (!(run.runId in hash)) { + hash[run.runId] = run + } + } + return hash + })() + + // sessions + const sessions = await prisma.session.findMany({ + where: { + sessionId: { + in: sessionIdsArr, + }, + }, + }) + const sessionsHash = (() => { + const hash: Record< + string, + { + sessionId: string + updatedAt: Date + createdAt: Date + startDuration: Date + endDuration: Date | null + planId: string | null + userId: string + } + > = {} + for (const session of sessions) { + if (!(session.sessionId in hash)) { + hash[session.sessionId] = session + } + } + return hash + })() + + // workout plans + const workoutPlanIdsArr = [] + for (const session of sessions) { + if (session?.planId) { + workoutPlanIdsArr.push(session.planId) + } + } + + const workoutPlans = await prisma.workoutPlan.findMany({ + where: { + planId: { + in: workoutPlanIdsArr, + }, + }, + }) + const workoutPlansHash = (() => { + const hash: Record< + string, + { + planId: string + updatedAt: Date + createdAt: Date + name: string + exerciseOrder: string[] + lastWorkout: Date | null + duration: number | null + userId: string + gymId: string + } + > = {} + for (const workoutPlan of workoutPlans) { + if (!(workoutPlan.planId in hash)) { + hash[workoutPlan.planId] = workoutPlan + } + } + return hash + })() + + /* -------------------------------------------------------------------------- */ + /* Output */ + /* -------------------------------------------------------------------------- */ + /* ----------------------------- recent activity ---------------------------- */ + const recentActivity = (() => { + const arr: IRecentActivity[] = [] + for (const activity of last5Activities) { + let session + let duration = 0 + let workoutName = "" + if (activity?.sessionId) { + session = sessionsHash[activity.sessionId] + if (session?.endDuration) { + duration = + session.endDuration.getTime() - session.startDuration.getTime() + } + workoutName = + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + workoutPlansHash[sessionsHash[activity.sessionId].planId!].name + } else { + if (activity?.runId) { + session = runsHash[activity.runId] + duration = session.duration + workoutName = `Run ${ + workoutPlansHash[runsHash[activity.runId].actualDistance] + }${workoutPlansHash[runsHash[activity.runId].actualDistanceUnits]}` + } + } + + arr.push({ + date: activity.createdAt.toISOString(), + id: activity.activityId, + workoutName, + workoutDuration: duration, + }) + } + return arr + })() + + /* -------------------------------- top stats ------------------------------- */ + // weight lifted total this week + const startOfThisWeek = new JsDateUtils(new Date()).getStartOfWeek() + const setsThisWeek = await prisma.set.findMany({ + where: { + AND: [ + { + unit: { + in: [Units.KG, Units.LB], + }, + }, + { + createdAt: { + gte: startOfThisWeek.toISOString(), + }, + }, + ], + }, + }) + + let totalKgLiftedThisWeek = 0 + for (const set of setsThisWeek) { + if (set.unit === Units.LB) { + set.unit = Units.KG + set.weight = lbsToKg(set.weight) + } + totalKgLiftedThisWeek += set.weight + } + + // top 3 lifts? + const allExercises = await prisma.exercise.findMany({ + where: { + userId: user.id, + }, + }) + const exerciseHash = (() => { + const hash: Record< + string, + { + exerciseId: string + updatedAt: Date + createdAt: Date + name: string + description: string | null + url: string | null + unit: string + targetReps: number | null + targetSets: number | null + userId: string + gymId: string + } + > = {} + for (const exercise of allExercises) { + if (!(exercise.exerciseId in hash)) { + hash[exercise.exerciseId] = exercise + } + } + return hash + })() + + const allSets = await prisma.set.findMany({ + where: { + unit: { + in: [Units.KG, Units.LB], + }, + }, + orderBy: { + weight: "desc", + }, + }) + const convertedToKg = allSets.map((set) => { + if (set.unit === Units.LB) { + set.unit = Units.KG + set.weight = lbsToKg(set.weight) + } + return set + }) + const distinctExerciseId = new Set() + const top3Arr = convertedToKg + .filter((set) => { + if (!distinctExerciseId.has(set.exerciseId)) { + distinctExerciseId.add(set.exerciseId) + return true + } + return false + }) + .slice(0, 3) + + const big3: ITopStats["big3"] = {} + for (const set of top3Arr) { + if (set.exerciseId in exerciseHash) { + big3[exerciseHash[set.exerciseId].name] = { + weight: set.weight, + unit: set.unit as Units, + } + } + } + + // random graph for random lift + const distinctSets = await prisma.set.findMany({ + where: { + unit: { + in: [Units.KG, Units.LB], + }, + }, + distinct: ["weight"], + }) + + const exercises = await prisma.exercise.findMany({ + where: { + userId: user.id, + }, + select: { + exerciseId: true, + name: true, + unit: true, + }, + }) + + const randomExercise = (): ITopStats["randomGraph"] => { + if (exercises.length < 1) + return { data: [], exerciseName: "", unit: Units.KG } + const ranSelected = + exercises[Math.floor(Math.random() * exercises.length)] + const randExerciseSets = distinctSets.filter( + (set) => set.exerciseId === ranSelected.exerciseId + ) + if (randExerciseSets.length > 0) { + const data = randExerciseSets.map( + (obj): RechartsData => ({ + name: unixToIsoDate(obj.createdAt.getTime()), + weight: obj.weight, + }) + ) + return { + data, + exerciseName: ranSelected.name, + unit: ranSelected.unit as Units, + } + } + return randomExercise() + } + + // response + const topStatsRes: ITopStats = { + weightLiftedTotal: { + weight: Number(totalKgLiftedThisWeek.toFixed(2)), + unit: Units.KG, + }, + randomGraph: randomExercise(), + big3, + } + + return { recentActivity: recentActivity, topStats: topStatsRes } + } catch (error) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "No activities found.", + cause: error, + }) + } +}) + +export default getStats diff --git a/src/server/procedures/workouts-procedures/update-workout-plan.ts b/src/server/procedures/workouts-procedures/update-workout-plan.ts index 7b937c7..ca68bd6 100644 --- a/src/server/procedures/workouts-procedures/update-workout-plan.ts +++ b/src/server/procedures/workouts-procedures/update-workout-plan.ts @@ -26,6 +26,11 @@ const updateWorkoutPlan = tProtectedProcedure data: { targetReps: Number(exercise.reps), targetSets: Number(exercise.sets), + workoutPlans: { + connect: { + planId, + }, + }, }, }) } diff --git a/src/server/routers/_app.ts b/src/server/routers/_app.ts index 2939bbc..c94f5d2 100644 --- a/src/server/routers/_app.ts +++ b/src/server/routers/_app.ts @@ -6,6 +6,7 @@ import workoutsRouter from "./workouts-router" import gymLocationRouter from "./gym-location-router" import currActiveSeshRouter from "./curr-active-sesh-router" import sessionRouter from "./session-router" +import setsRouter from "./sets-router" export const appRouter = trouter({ health: tprocedure.query(() => "healthy"), @@ -15,6 +16,7 @@ export const appRouter = trouter({ gymLocations: gymLocationRouter, currActiveSesh: currActiveSeshRouter, session: sessionRouter, + sets: setsRouter, }) // export type definition of API diff --git a/src/server/routers/sets-router.ts b/src/server/routers/sets-router.ts new file mode 100644 index 0000000..e5f4029 --- /dev/null +++ b/src/server/routers/sets-router.ts @@ -0,0 +1,7 @@ +import { trouter } from "../trpc" +// procedures +import getSetsByExerciseId from "../procedures/sets/get-sets-by-exercise-id" + +const setsRouter = trouter({ getSetsByExerciseId }) + +export default setsRouter diff --git a/src/server/routers/stats-router.ts b/src/server/routers/stats-router.ts index d46117e..5e1fe45 100644 --- a/src/server/routers/stats-router.ts +++ b/src/server/routers/stats-router.ts @@ -1,16 +1,9 @@ -import { tProtectedProcedure, trouter } from "../trpc" -import MOCK_RECENT_ACTIVITY, { MOCK_TOP_STATS } from "src/mocks/recent-activity" +import { trouter } from "../trpc" +// procedures +import getStats from "../procedures/stats/get-stats" const statsRouter = trouter({ - getStats: tProtectedProcedure.query(async () => { - // const { - // ctx: { user }, - // } = opts - - // const userId = user.id - // TODO: setup postgres tables and finish backend - return { recentActivity: MOCK_RECENT_ACTIVITY, topStats: MOCK_TOP_STATS } - }), + getStats, }) export default statsRouter diff --git a/src/types/curr-active-sesh.ts b/src/types/curr-active-sesh.ts index dd5a4ca..50754dd 100644 --- a/src/types/curr-active-sesh.ts +++ b/src/types/curr-active-sesh.ts @@ -9,4 +9,5 @@ export interface ISet { sessionId: string exerciseId: string setNumber: number + exerciseName: string } diff --git a/src/types/home-page.ts b/src/types/home-page.ts index ba95322..e2c9630 100644 --- a/src/types/home-page.ts +++ b/src/types/home-page.ts @@ -1,6 +1,8 @@ +import Units from "src/constants/units" + export interface IRecentActivity { - id: number - date: number // unix seconds + id: string + date: string // unix seconds workoutName: string workoutDuration: number // unix seconds } @@ -10,12 +12,15 @@ export interface WeightLifted { unit: string } +export type RechartsData = { [key: string]: string | number } export interface ITopStats { big3: { - squat: WeightLifted - bench: WeightLifted - deadlift: WeightLifted + [key: string]: WeightLifted } weightLiftedTotal: WeightLifted - randomGraph: object // TODO: pending + randomGraph: { + exerciseName: string + unit: Units + data: RechartsData[] + } } diff --git a/src/utils/js-date-utils.ts b/src/utils/js-date-utils.ts new file mode 100644 index 0000000..77a1dde --- /dev/null +++ b/src/utils/js-date-utils.ts @@ -0,0 +1,22 @@ +class JsDateUtils { + private jsDate: Date + + constructor(date = new Date()) { + this.jsDate = date + } + + // monday = 0 a.k.a start of week + getStartOfWeek() { + const day = this.jsDate.getDay() // day of week + const diff = this.jsDate.getDate() - day + 1 + const newDate = new Date( + this.jsDate.getFullYear(), + this.jsDate.getMonth(), + diff + ) // create a new Date object + newDate.setHours(0, 0, 0, 0) // set the time to midnight + return newDate + } +} + +export default JsDateUtils diff --git a/src/utils/unit-conversion.ts b/src/utils/unit-conversion.ts new file mode 100644 index 0000000..1854795 --- /dev/null +++ b/src/utils/unit-conversion.ts @@ -0,0 +1,3 @@ +export const lbsToKg = (lbs: number, decimalPlaces = 2): number => { + return Number((lbs * 0.45359237).toFixed(decimalPlaces)) +} diff --git a/src/utils/unix-to-ISO-date.ts b/src/utils/unix-to-ISO-date.ts index ab607c3..b67fbb3 100644 --- a/src/utils/unix-to-ISO-date.ts +++ b/src/utils/unix-to-ISO-date.ts @@ -1,4 +1,5 @@ import dayjs from "dayjs" -const unixToIsoDate = (unix: number) => dayjs(unix).format("YYYY-MM-DD") +const unixToIsoDate = (unix: number | string) => + dayjs(unix).format("YYYY-MM-DD") export default unixToIsoDate diff --git a/yarn.lock b/yarn.lock index e829964..3db7830 100644 --- a/yarn.lock +++ b/yarn.lock @@ -270,6 +270,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" +"@babel/runtime@^7.1.2": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.4.tgz#36fa1d2b36db873d25ec631dcc4923fdc1cf2e2e" + integrity sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/runtime@^7.12.5", "@babel/runtime@^7.9.2": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.5.tgz#8564dd588182ce0047d55d7a75e93921107b57ec" @@ -1035,6 +1042,57 @@ dependencies: "@babel/types" "^7.20.7" +"@types/d3-array@^3.0.3": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5" + integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg== + +"@types/d3-color@*": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2" + integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A== + +"@types/d3-ease@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b" + integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA== + +"@types/d3-interpolate@^3.0.1": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c" + integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== + dependencies: + "@types/d3-color" "*" + +"@types/d3-path@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.0.2.tgz#4327f4a05d475cf9be46a93fc2e0f8d23380805a" + integrity sha512-WAIEVlOCdd/NKRYTsqCpOMHQHemKBEINf8YXMYOtXH0GA7SY0dqMB78P3Uhgfy+4X+/Mlw2wDtlETkN6kQUCMA== + +"@types/d3-scale@^4.0.2": + version "4.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.8.tgz#d409b5f9dcf63074464bf8ddfb8ee5a1f95945bb" + integrity sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ== + dependencies: + "@types/d3-time" "*" + +"@types/d3-shape@^3.1.0": + version "3.1.6" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.6.tgz#65d40d5a548f0a023821773e39012805e6e31a72" + integrity sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA== + dependencies: + "@types/d3-path" "*" + +"@types/d3-time@*", "@types/d3-time@^3.0.0": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.3.tgz#3c186bbd9d12b9d84253b6be6487ca56b54f88be" + integrity sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw== + +"@types/d3-timer@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70" + integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw== + "@types/graceful-fs@^4.1.3": version "4.1.6" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.6.tgz#e14b2576a1c25026b7f02ede1de3b84c3a1efeae" @@ -1845,6 +1903,11 @@ clsx@^1.1.1: resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== +clsx@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b" + integrity sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q== + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -1968,6 +2031,77 @@ csstype@^3.0.2: resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz" integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== +"d3-array@2 - 3", "d3-array@2.10.0 - 3", d3-array@^3.1.6: + version "3.2.4" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5" + integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg== + dependencies: + internmap "1 - 2" + +"d3-color@1 - 3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" + integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== + +d3-ease@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" + integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== + +"d3-format@1 - 3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641" + integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== + +"d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" + integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== + dependencies: + d3-color "1 - 3" + +d3-path@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526" + integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== + +d3-scale@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396" + integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ== + dependencies: + d3-array "2.10.0 - 3" + d3-format "1 - 3" + d3-interpolate "1.2.0 - 3" + d3-time "2.1.1 - 3" + d3-time-format "2 - 4" + +d3-shape@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5" + integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== + dependencies: + d3-path "^3.1.0" + +"d3-time-format@2 - 4": + version "4.1.0" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a" + integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg== + dependencies: + d3-time "1 - 3" + +"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7" + integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q== + dependencies: + d3-array "2 - 3" + +d3-timer@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" + integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== + d@1, d@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" @@ -2016,6 +2150,11 @@ debug@^3.2.6, debug@^3.2.7: dependencies: ms "^2.1.1" +decimal.js-light@^2.4.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" + integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg== + decimal.js@^10.4.2: version "10.4.3" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" @@ -2147,6 +2286,13 @@ dom-accessibility-api@^0.5.6, dom-accessibility-api@^0.5.9: resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== +dom-helpers@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8" + integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA== + dependencies: + "@babel/runtime" "^7.1.2" + domexception@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673" @@ -2581,6 +2727,11 @@ event-target-shim@^5.0.0: resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== +eventemitter3@^4.0.1: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + events@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" @@ -2644,6 +2795,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-equals@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-5.0.1.tgz#a4eefe3c5d1c0d021aeed0bc10ba5e0c12ee405d" + integrity sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ== + fast-glob@^3.2.11, fast-glob@^3.2.12, fast-glob@^3.2.9: version "3.2.12" resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz" @@ -3109,6 +3265,11 @@ internal-slot@^1.0.3, internal-slot@^1.0.4, internal-slot@^1.0.5: has "^1.0.3" side-channel "^1.0.4" +"internmap@1 - 2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" + integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== + is-arguments@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz" @@ -3955,7 +4116,7 @@ lodash.merge@^4.6.2: resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash@^4.17.15, lodash@^4.17.21: +lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -4644,7 +4805,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.8.1: +prop-types@^15.6.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -4701,7 +4862,7 @@ react-hook-form@^7.44.1: resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.44.1.tgz#5385819f399d6675f367fc78f555c8352ca11f04" integrity sha512-ZVmDuQQCq6agpVE2eoGjH3ZMDgo/aFV4JVobUQGOQ0/tgfcY/WY30mBSVnxmFOpzS+iSqD2ax7hw9Thg5F931A== -react-is@^16.13.1: +react-is@^16.10.2, react-is@^16.13.1: version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -4716,6 +4877,19 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== +react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + +react-smooth@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-2.0.5.tgz#d153b7dffc7143d0c99e82db1532f8cf93f20ecd" + integrity sha512-BMP2Ad42tD60h0JW6BFaib+RJuV5dsXJK9Baxiv/HlNFjvRLqA9xrNKxVWnUIZPQfzUwGXIlU/dSYLU+54YGQA== + dependencies: + fast-equals "^5.0.0" + react-transition-group "2.9.0" + react-ssr-prepass@^1.5.0: version "1.5.0" resolved "https://registry.npmjs.org/react-ssr-prepass/-/react-ssr-prepass-1.5.0.tgz" @@ -4728,6 +4902,16 @@ react-toastify@^9.1.3: dependencies: clsx "^1.1.1" +react-transition-group@2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d" + integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg== + dependencies: + dom-helpers "^3.4.0" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react-lifecycles-compat "^3.0.4" + react@18.2.0: version "18.2.0" resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz" @@ -4765,6 +4949,27 @@ real-require@^0.2.0: resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78" integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== +recharts-scale@^0.4.4: + version "0.4.5" + resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.5.tgz#0969271f14e732e642fcc5bd4ab270d6e87dd1d9" + integrity sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w== + dependencies: + decimal.js-light "^2.4.1" + +recharts@^2.10.1: + version "2.10.1" + resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.10.1.tgz#c28c7451ed83d31072013446104ec07b3062893c" + integrity sha512-9bi0jIzxOTfEda+oYqgimKuYfApmBr0zKnAX8r4Iw56k3Saz/IQyBD4zohZL0eyzfz0oGFRH7alpJBgH1eC57g== + dependencies: + clsx "^2.0.0" + eventemitter3 "^4.0.1" + lodash "^4.17.19" + react-is "^16.10.2" + react-smooth "^2.0.5" + recharts-scale "^0.4.4" + tiny-invariant "^1.3.1" + victory-vendor "^36.6.8" + redent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" @@ -4778,6 +4983,11 @@ regenerator-runtime@^0.13.11: resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== +regenerator-runtime@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" + integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== + regexp.prototype.flags@^1.4.3, regexp.prototype.flags@^1.5.0: version "1.5.0" resolved "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz" @@ -5321,6 +5531,11 @@ thread-stream@^2.0.0: dependencies: real-require "^0.2.0" +tiny-invariant@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" + integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== + titleize@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz" @@ -5565,6 +5780,26 @@ v8-to-istanbul@^9.0.1: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^1.6.0" +victory-vendor@^36.6.8: + version "36.6.12" + resolved "https://registry.yarnpkg.com/victory-vendor/-/victory-vendor-36.6.12.tgz#17fa4d79d266a6e2bde0291c60c5002c55008164" + integrity sha512-pJrTkNHln+D83vDCCSUf0ZfxBvIaVrFHmrBOsnnLAbdqfudRACAj51He2zU94/IWq9464oTADcPVkmWAfNMwgA== + dependencies: + "@types/d3-array" "^3.0.3" + "@types/d3-ease" "^3.0.0" + "@types/d3-interpolate" "^3.0.1" + "@types/d3-scale" "^4.0.2" + "@types/d3-shape" "^3.1.0" + "@types/d3-time" "^3.0.0" + "@types/d3-timer" "^3.0.0" + d3-array "^3.1.6" + d3-ease "^3.0.1" + d3-interpolate "^3.0.1" + d3-scale "^4.0.2" + d3-shape "^3.1.0" + d3-time "^3.0.0" + d3-timer "^3.0.1" + w3c-xmlserializer@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz#aebdc84920d806222936e3cdce408e32488a3073"