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"