From 9b81a107b8a1f76451f30bd79828c032b99a1948 Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 25 Nov 2023 21:12:37 -0500 Subject: [PATCH 01/15] increase area for onclick --- src/components/Workouts/PlanCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) { From aa71d184aa5baa146b3e83a0b335f2088fc45cae Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 25 Nov 2023 21:37:31 -0500 Subject: [PATCH 02/15] add most recent weight --- src/components/CurrActiveSesh/index.tsx | 10 +++++-- src/hooks/useGetSetsByExerciseId.tsx | 5 ++++ .../sets/get-sets-by-exercise-id.ts | 28 +++++++++++++++++++ src/server/routers/_app.ts | 2 ++ src/server/routers/sets-router.ts | 7 +++++ 5 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 src/hooks/useGetSetsByExerciseId.tsx create mode 100644 src/server/procedures/sets/get-sets-by-exercise-id.ts create mode 100644 src/server/routers/sets-router.ts diff --git a/src/components/CurrActiveSesh/index.tsx b/src/components/CurrActiveSesh/index.tsx index 48193fc..1d452f4 100644 --- a/src/components/CurrActiveSesh/index.tsx +++ b/src/components/CurrActiveSesh/index.tsx @@ -14,6 +14,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 @@ -75,6 +76,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,6 +120,8 @@ export default function CurrActiveSeshContainer() { return list }, [exerciseHashmap, exercisesList]) + const mostRecentWeight = exerciseSetList?.[0].weight.toString() + const sets = useMemo(() => { if (!currActiveExercise?.targetSets) return [] const sets: ISet[] = [] @@ -123,7 +129,7 @@ export default function CurrActiveSeshContainer() { sets.push({ setNumber: i + 1, name: currActiveExercise.name, - weight: "0", // TODO: add last weight exercise was completed in + weight: mostRecentWeight ?? "0", reps: currActiveExercise.targetReps, unit: currActiveExercise.unit, exerciseId: currActiveExercise.exerciseId, @@ -132,7 +138,7 @@ export default function CurrActiveSeshContainer() { }) } return sets - }, [currActiveExercise, currActiveSeshId]) + }, [currActiveExercise, currActiveSeshId, mostRecentWeight]) const handleCompleteWorkout = async () => { // get saved things from db diff --git a/src/hooks/useGetSetsByExerciseId.tsx b/src/hooks/useGetSetsByExerciseId.tsx new file mode 100644 index 0000000..90f1760 --- /dev/null +++ b/src/hooks/useGetSetsByExerciseId.tsx @@ -0,0 +1,5 @@ +import { trpc } from "src/utils/trpc" + +export default function useGetSetsByExerciseId(exerciseId: string) { + return trpc.sets.getSetsByExerciseId.useQuery(exerciseId) +} 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..99a6ad1 --- /dev/null +++ b/src/server/procedures/sets/get-sets-by-exercise-id.ts @@ -0,0 +1,28 @@ +import { TRPCError } from "@trpc/server" +import { tProtectedProcedure } from "src/server/trpc" +import prisma from "src/utils/prisma" +import { z } from "zod" + +const getSetsByExerciseId = tProtectedProcedure + .input(z.string().uuid()) + .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/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 From abf954a12e4da289b6c33f810e5fdb0fd44490e2 Mon Sep 17 00:00:00 2001 From: Brandon Date: Sat, 25 Nov 2023 21:41:21 -0500 Subject: [PATCH 03/15] add comments --- src/server/procedures/sets/get-sets-by-exercise-id.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/server/procedures/sets/get-sets-by-exercise-id.ts b/src/server/procedures/sets/get-sets-by-exercise-id.ts index 99a6ad1..903d157 100644 --- a/src/server/procedures/sets/get-sets-by-exercise-id.ts +++ b/src/server/procedures/sets/get-sets-by-exercise-id.ts @@ -4,7 +4,8 @@ import prisma from "src/utils/prisma" import { z } from "zod" const getSetsByExerciseId = tProtectedProcedure - .input(z.string().uuid()) + // uuid exerciseId + .input(z.string()) .query(async ({ input }) => { try { const sets = await prisma.set.findMany({ From 95514633ea8e3e75bb06c572699ad54263470023 Mon Sep 17 00:00:00 2001 From: bcheung Date: Sat, 25 Nov 2023 23:42:21 -0500 Subject: [PATCH 04/15] add workout summary page --- src/components/CurrActiveSesh/Set.tsx | 3 + src/components/CurrActiveSesh/SetGrid.tsx | 1 + src/components/CurrActiveSesh/index.tsx | 15 +--- src/components/Summary/index.tsx | 80 +++++++++++++++++++ .../workouts/curr-active-workout/summary.tsx | 8 +- .../session-procedures/get-session.ts | 1 + src/types/curr-active-sesh.ts | 1 + 7 files changed, 94 insertions(+), 15 deletions(-) create mode 100644 src/components/Summary/index.tsx 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 1d452f4..aa67d88 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 @@ -44,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() @@ -128,7 +119,7 @@ export default function CurrActiveSeshContainer() { for (let i = 0; i < currActiveExercise.targetSets; i++) { sets.push({ setNumber: i + 1, - name: currActiveExercise.name, + exerciseName: currActiveExercise.name, weight: mostRecentWeight ?? "0", reps: currActiveExercise.targetReps, unit: currActiveExercise.unit, diff --git a/src/components/Summary/index.tsx b/src/components/Summary/index.tsx new file mode 100644 index 0000000..48e4bcc --- /dev/null +++ b/src/components/Summary/index.tsx @@ -0,0 +1,80 @@ +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) { + 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/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/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/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 } From 0f47585b01fff4940d50fc958ed30b4652e5c57a Mon Sep 17 00:00:00 2001 From: bcheung Date: Sat, 25 Nov 2023 23:59:31 -0500 Subject: [PATCH 05/15] add summary sorting, create delay when adding sets to table to get most recent weight --- src/components/CurrActiveSesh/index.tsx | 2 +- src/components/Summary/index.tsx | 12 +++++-- .../completed-workout.ts | 32 ++++++++++++++++--- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/components/CurrActiveSesh/index.tsx b/src/components/CurrActiveSesh/index.tsx index aa67d88..84398e5 100644 --- a/src/components/CurrActiveSesh/index.tsx +++ b/src/components/CurrActiveSesh/index.tsx @@ -111,7 +111,7 @@ export default function CurrActiveSeshContainer() { return list }, [exerciseHashmap, exercisesList]) - const mostRecentWeight = exerciseSetList?.[0].weight.toString() + const mostRecentWeight = exerciseSetList?.[0]?.weight?.toString() ?? "0" const sets = useMemo(() => { if (!currActiveExercise?.targetSets) return [] diff --git a/src/components/Summary/index.tsx b/src/components/Summary/index.tsx index 48e4bcc..516b26e 100644 --- a/src/components/Summary/index.tsx +++ b/src/components/Summary/index.tsx @@ -32,9 +32,17 @@ export default function Summary({ completedSets }: SummaryProps) { completedSetsExerciseHash.forEach((completedExercisesArr, exerciseId) => { const exercises: React.ReactNode[] = [] - for (const exercise of completedExercisesArr) { + 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}
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..03e63b7 100644 --- a/src/server/procedures/curr-active-sesh-procedures/completed-workout.ts +++ b/src/server/procedures/curr-active-sesh-procedures/completed-workout.ts @@ -18,8 +18,18 @@ const completedWorkout = tProtectedProcedure }, }) - // update Set table - const setsToInput = input.map((obj) => ({ + // 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 +37,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({ From 789a746c0bf3651c06b32c7475d4aebb04bfd49f Mon Sep 17 00:00:00 2001 From: bcheung Date: Sun, 26 Nov 2023 00:19:03 -0500 Subject: [PATCH 06/15] order exercise for summary page --- src/components/CurrActiveSesh/index.tsx | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/components/CurrActiveSesh/index.tsx b/src/components/CurrActiveSesh/index.tsx index 84398e5..45d7278 100644 --- a/src/components/CurrActiveSesh/index.tsx +++ b/src/components/CurrActiveSesh/index.tsx @@ -136,6 +136,13 @@ 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) + ) // calculate total sets to be completed let targetTotalSets = 0 @@ -147,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 @@ -161,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: () => { @@ -197,7 +211,7 @@ export default function CurrActiveSeshContainer() { router.push({ pathname: "/workouts/curr-active-workout/summary", query: { - data: JSON.stringify(completedSets), + data: JSON.stringify(sortedCompletedSets), }, }) } From 21604ac86cef40481b30f946bd8c5a3a2fc66ed3 Mon Sep 17 00:00:00 2001 From: bcheung Date: Sun, 26 Nov 2023 10:10:35 -0500 Subject: [PATCH 07/15] update prisma schema --- .../migration.sql | 20 +++++++++++++++ .../migration.sql | 22 ++++++++++++++++ .../migration.sql | 8 ++++++ .../migration.sql | 25 +++++++++++++++++++ prisma/schema.prisma | 11 +++++--- 5 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 prisma/migrations/20231126134440_add_activityid_column_optional/migration.sql create mode 100644 prisma/migrations/20231126134657_make_userid_and_activityid_required/migration.sql create mode 100644 prisma/migrations/20231126135005_change_userid_type/migration.sql create mode 100644 prisma/migrations/20231126140814_change_relation/migration.sql 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/schema.prisma b/prisma/schema.prisma index d7322e5..09ce731 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -55,6 +55,9 @@ model Activity { // relations runExercise RunExercise? + 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 } @@ -95,8 +98,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 +115,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]) From d017e7d5358b742e53d1f24368ca36877042d389 Mon Sep 17 00:00:00 2001 From: bcheung Date: Sun, 26 Nov 2023 11:08:21 -0500 Subject: [PATCH 08/15] add recent activity --- src/mocks/recent-activity.ts | 4 +- .../completed-workout.ts | 23 +++- .../get-curr-active-sesh.ts | 8 ++ src/server/procedures/stats/get-stats.ts | 110 ++++++++++++++++++ src/server/routers/stats-router.ts | 15 +-- src/types/home-page.ts | 4 +- 6 files changed, 147 insertions(+), 17 deletions(-) create mode 100644 src/server/procedures/stats/get-stats.ts diff --git a/src/mocks/recent-activity.ts b/src/mocks/recent-activity.ts index e56233d..a59cf2c 100644 --- a/src/mocks/recent-activity.ts +++ b/src/mocks/recent-activity.ts @@ -3,8 +3,8 @@ import { IRecentActivity, ITopStats, WeightLifted } from "src/types/home-page" const MOCK_RECENT_ACTIVITY: IRecentActivity[] = new Array(5) .fill(0) .map((_, index) => ({ - 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 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 03e63b7..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,17 +7,24 @@ 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, }, }) + 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) { @@ -60,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/stats/get-stats.ts b/src/server/procedures/stats/get-stats.ts new file mode 100644 index 0000000..9da5e68 --- /dev/null +++ b/src/server/procedures/stats/get-stats.ts @@ -0,0 +1,110 @@ +import { TRPCError } from "@trpc/server" +import { tProtectedProcedure } from "src/server/trpc" +import prisma from "src/utils/prisma" +// types +import { IRecentActivity } from "src/types/home-page" +// mocks +import { MOCK_TOP_STATS } from "src/mocks/recent-activity" + +const getStats = tProtectedProcedure.query(async ({ ctx: { user } }) => { + try { + const activities = await prisma.activity.findMany({ + where: { + userId: user.id, + }, + orderBy: { + createdAt: "desc", + }, + take: 5, + }) + + const sessions = await prisma.session.findMany({ + where: { + sessionId: { + in: activities.map((activity) => activity.sessionId), + }, + }, + }) + 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 + })() + + const workoutPlans = await prisma.workoutPlan.findMany({ + where: { + planId: { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + in: sessions.map((session) => session.planId!), + }, + }, + }) + 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 + })() + + const recentActivity = (() => { + const arr: IRecentActivity[] = [] + for (const activity of activities) { + const session = sessionsHash[activity.sessionId] + const duration = + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + session.endDuration!.getTime() - session.startDuration.getTime() + + arr.push({ + date: activity.createdAt.toISOString(), + id: activity.activityId, + workoutName: + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + workoutPlansHash[sessionsHash[activity.sessionId].planId!].name, + workoutDuration: duration, + }) + } + return arr + })() + + return { recentActivity: recentActivity, topStats: MOCK_TOP_STATS } + } catch (error) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "No activities found.", + cause: error, + }) + } +}) + +export default getStats 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/home-page.ts b/src/types/home-page.ts index ba95322..16b7f04 100644 --- a/src/types/home-page.ts +++ b/src/types/home-page.ts @@ -1,6 +1,6 @@ export interface IRecentActivity { - id: number - date: number // unix seconds + id: string + date: string // unix seconds workoutName: string workoutDuration: number // unix seconds } From 9f9200c9ac5ae7fb28335a3565c3450197353235 Mon Sep 17 00:00:00 2001 From: bcheung Date: Sun, 26 Nov 2023 11:43:37 -0500 Subject: [PATCH 09/15] update type and add run to stats --- .../migration.sql | 15 ++++ prisma/schema.prisma | 17 ++-- src/components/HomePage/RecentActivity.tsx | 6 +- src/server/procedures/stats/get-stats.ts | 84 ++++++++++++++++--- src/utils/unix-to-ISO-date.ts | 3 +- 5 files changed, 106 insertions(+), 19 deletions(-) create mode 100644 prisma/migrations/20231126161443_add_run_id_column_to_activity/migration.sql 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 09ce731..13fb8db 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -49,17 +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 session Session? @relation(fields: [sessionId], references: [sessionId], onDelete: Cascade) - sessionId String @unique @db.Uuid + sessionId String? @unique @db.Uuid - user User @relation(fields: [userId], references: [userId], onDelete: Cascade) - userId String + user User @relation(fields: [userId], references: [userId], onDelete: Cascade) + userId String + RunExercise RunExercise? } // Exercises Table @@ -185,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/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/server/procedures/stats/get-stats.ts b/src/server/procedures/stats/get-stats.ts index 9da5e68..f77fa7c 100644 --- a/src/server/procedures/stats/get-stats.ts +++ b/src/server/procedures/stats/get-stats.ts @@ -18,10 +18,53 @@ const getStats = tProtectedProcedure.query(async ({ ctx: { user } }) => { take: 5, }) + // hashmaps + const runIdsArr: string[] = [] + const sessionIdsArr: string[] = [] + for (const activity of activities) { + if (activity?.runId) { + runIdsArr.push(activity.runId) + } + if (activity?.sessionId) { + sessionIdsArr.push(activity.sessionId) + } + } + 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 + })() + const sessions = await prisma.session.findMany({ where: { sessionId: { - in: activities.map((activity) => activity.sessionId), + in: sessionIdsArr, }, }, }) @@ -46,11 +89,17 @@ const getStats = tProtectedProcedure.query(async ({ ctx: { user } }) => { return hash })() + const workoutPlanIdsArr = [] + for (const session of sessions) { + if (session?.planId) { + workoutPlanIdsArr.push(session.planId) + } + } + const workoutPlans = await prisma.workoutPlan.findMany({ where: { planId: { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - in: sessions.map((session) => session.planId!), + in: workoutPlanIdsArr, }, }, }) @@ -80,17 +129,32 @@ const getStats = tProtectedProcedure.query(async ({ ctx: { user } }) => { const recentActivity = (() => { const arr: IRecentActivity[] = [] for (const activity of activities) { - const session = sessionsHash[activity.sessionId] - const duration = - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - session.endDuration!.getTime() - session.startDuration.getTime() + 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: - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - workoutPlansHash[sessionsHash[activity.sessionId].planId!].name, + workoutName, workoutDuration: duration, }) } 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 From 638de79d17805929e6744747b0612ca0e743c6df Mon Sep 17 00:00:00 2001 From: bcheung Date: Mon, 27 Nov 2023 10:35:39 -0500 Subject: [PATCH 10/15] add total weight lifted this week --- src/server/procedures/stats/get-stats.ts | 74 ++++++++++++++++++++++-- src/utils/js-date-utils.ts | 22 +++++++ src/utils/unit-conversion.ts | 3 + 3 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 src/utils/js-date-utils.ts create mode 100644 src/utils/unit-conversion.ts diff --git a/src/server/procedures/stats/get-stats.ts b/src/server/procedures/stats/get-stats.ts index f77fa7c..cffa604 100644 --- a/src/server/procedures/stats/get-stats.ts +++ b/src/server/procedures/stats/get-stats.ts @@ -1,10 +1,14 @@ 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" // types -import { IRecentActivity } from "src/types/home-page" +import { IRecentActivity, ITopStats } from "src/types/home-page" // mocks import { MOCK_TOP_STATS } from "src/mocks/recent-activity" +import Units from "src/constants/units" const getStats = tProtectedProcedure.query(async ({ ctx: { user } }) => { try { @@ -15,13 +19,17 @@ const getStats = tProtectedProcedure.query(async ({ ctx: { user } }) => { orderBy: { createdAt: "desc", }, - take: 5, }) - // hashmaps + /* -------------------------------------------------------------------------- */ + /* processing */ + /* -------------------------------------------------------------------------- */ + const last5Activities = activities.slice(0, 5) + + // idsArr const runIdsArr: string[] = [] const sessionIdsArr: string[] = [] - for (const activity of activities) { + for (const activity of last5Activities) { if (activity?.runId) { runIdsArr.push(activity.runId) } @@ -29,6 +37,8 @@ const getStats = tProtectedProcedure.query(async ({ ctx: { user } }) => { sessionIdsArr.push(activity.sessionId) } } + + // runs const runs = await prisma.run.findMany({ where: { runId: { @@ -61,6 +71,7 @@ const getStats = tProtectedProcedure.query(async ({ ctx: { user } }) => { return hash })() + // sessions const sessions = await prisma.session.findMany({ where: { sessionId: { @@ -89,6 +100,7 @@ const getStats = tProtectedProcedure.query(async ({ ctx: { user } }) => { return hash })() + // workout plans const workoutPlanIdsArr = [] for (const session of sessions) { if (session?.planId) { @@ -126,9 +138,13 @@ const getStats = tProtectedProcedure.query(async ({ ctx: { user } }) => { return hash })() + /* -------------------------------------------------------------------------- */ + /* Output */ + /* -------------------------------------------------------------------------- */ + /* ----------------------------- recent activity ---------------------------- */ const recentActivity = (() => { const arr: IRecentActivity[] = [] - for (const activity of activities) { + for (const activity of last5Activities) { let session let duration = 0 let workoutName = "" @@ -161,7 +177,53 @@ const getStats = tProtectedProcedure.query(async ({ ctx: { user } }) => { return arr })() - return { recentActivity: recentActivity, topStats: MOCK_TOP_STATS } + /* -------------------------------- top stats ------------------------------- */ + // weight lifted total this week + const startOfThisWeek = new JsDateUtils( + new Date("2023-11-25") + ).getStartOfWeek() + const setsThisWeek = await prisma.set.findMany({ + where: { + AND: [ + { + unit: { + in: ["kg", "lbs"], + mode: "insensitive", + }, + }, + { + 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 + } + + // TODO: top 3 lifts? + + // TODO: random graph for random lift + + const topStatsRes: ITopStats = { + ...MOCK_TOP_STATS, + weightLiftedTotal: { + weight: totalKgLiftedThisWeek, + unit: Units.KG, + }, + } + + console.log(topStatsRes) + + return { recentActivity: recentActivity, topStats: topStatsRes } } catch (error) { throw new TRPCError({ code: "NOT_FOUND", 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)) +} From aa3eb7f68fdc7d86d76967b6a3c1ee19180e40ab Mon Sep 17 00:00:00 2001 From: bcheung Date: Mon, 27 Nov 2023 10:36:10 -0500 Subject: [PATCH 11/15] remove dummy date --- src/server/procedures/stats/get-stats.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/server/procedures/stats/get-stats.ts b/src/server/procedures/stats/get-stats.ts index cffa604..40dec78 100644 --- a/src/server/procedures/stats/get-stats.ts +++ b/src/server/procedures/stats/get-stats.ts @@ -179,9 +179,7 @@ const getStats = tProtectedProcedure.query(async ({ ctx: { user } }) => { /* -------------------------------- top stats ------------------------------- */ // weight lifted total this week - const startOfThisWeek = new JsDateUtils( - new Date("2023-11-25") - ).getStartOfWeek() + const startOfThisWeek = new JsDateUtils(new Date()).getStartOfWeek() const setsThisWeek = await prisma.set.findMany({ where: { AND: [ From f186459d426565d6aa7ee08f6ade5953ab0cd9d4 Mon Sep 17 00:00:00 2001 From: bcheung Date: Mon, 27 Nov 2023 12:59:26 -0500 Subject: [PATCH 12/15] add backend graph data --- src/mocks/recent-activity.ts | 7 +++- src/server/procedures/stats/get-stats.ts | 50 +++++++++++++++++++++--- src/types/home-page.ts | 11 +++++- 3 files changed, 61 insertions(+), 7 deletions(-) diff --git a/src/mocks/recent-activity.ts b/src/mocks/recent-activity.ts index a59cf2c..f90833e 100644 --- a/src/mocks/recent-activity.ts +++ b/src/mocks/recent-activity.ts @@ -1,4 +1,5 @@ import { IRecentActivity, ITopStats, WeightLifted } from "src/types/home-page" +import Units from "src/constants/units" const MOCK_RECENT_ACTIVITY: IRecentActivity[] = new Array(5) .fill(0) @@ -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/server/procedures/stats/get-stats.ts b/src/server/procedures/stats/get-stats.ts index 40dec78..296f92d 100644 --- a/src/server/procedures/stats/get-stats.ts +++ b/src/server/procedures/stats/get-stats.ts @@ -5,7 +5,7 @@ import prisma from "src/utils/prisma" import JsDateUtils from "src/utils/js-date-utils" import { lbsToKg } from "src/utils/unit-conversion" // types -import { IRecentActivity, ITopStats } from "src/types/home-page" +import { IRecentActivity, ITopStats, RechartsData } from "src/types/home-page" // mocks import { MOCK_TOP_STATS } from "src/mocks/recent-activity" import Units from "src/constants/units" @@ -185,8 +185,7 @@ const getStats = tProtectedProcedure.query(async ({ ctx: { user } }) => { AND: [ { unit: { - in: ["kg", "lbs"], - mode: "insensitive", + in: [Units.KG, Units.LB], }, }, { @@ -208,19 +207,60 @@ const getStats = tProtectedProcedure.query(async ({ ctx: { user } }) => { } // TODO: top 3 lifts? + const allSets = await prisma.set.findMany({ + where: { + unit: { + in: [Units.KG, Units.LB], + }, + }, + }) // TODO: random graph for random lift + 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 = allSets.filter( + (set) => set.exerciseId === ranSelected.exerciseId + ) + if (randExerciseSets.length > 0) { + const data = randExerciseSets.map( + (obj): RechartsData => ({ + name: obj.createdAt.toISOString(), + weight: obj.weight, + }) + ) + return { + data, + exerciseName: ranSelected.name, + unit: ranSelected.unit as Units, + } + } + return randomExercise() + } + + // response const topStatsRes: ITopStats = { ...MOCK_TOP_STATS, weightLiftedTotal: { weight: totalKgLiftedThisWeek, unit: Units.KG, }, + randomGraph: randomExercise(), } - console.log(topStatsRes) - return { recentActivity: recentActivity, topStats: topStatsRes } } catch (error) { throw new TRPCError({ diff --git a/src/types/home-page.ts b/src/types/home-page.ts index 16b7f04..759adb4 100644 --- a/src/types/home-page.ts +++ b/src/types/home-page.ts @@ -1,3 +1,5 @@ +import Units from "src/constants/units" + export interface IRecentActivity { id: string date: string // unix seconds @@ -10,6 +12,9 @@ export interface WeightLifted { unit: string } +type RechartsDataX = { name: string } +type RechartsDataY = { [value: string]: number | string } +export type RechartsData = RechartsDataX & RechartsDataY export interface ITopStats { big3: { squat: WeightLifted @@ -17,5 +22,9 @@ export interface ITopStats { deadlift: WeightLifted } weightLiftedTotal: WeightLifted - randomGraph: object // TODO: pending + randomGraph: { + exerciseName: string + unit: Units + data: RechartsData[] + } } From 14dd869a73f8c4aec606791f83e2ba4460718f4e Mon Sep 17 00:00:00 2001 From: bcheung Date: Mon, 27 Nov 2023 13:47:15 -0500 Subject: [PATCH 13/15] add graph of random lift --- package.json | 1 + src/components/HomePage/TopStats.tsx | 44 ++++- src/components/HomePage/index.tsx | 7 +- src/server/procedures/stats/get-stats.ts | 18 +- src/types/home-page.ts | 4 +- yarn.lock | 241 ++++++++++++++++++++++- 6 files changed, 303 insertions(+), 12 deletions(-) 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/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/server/procedures/stats/get-stats.ts b/src/server/procedures/stats/get-stats.ts index 296f92d..5b5cd09 100644 --- a/src/server/procedures/stats/get-stats.ts +++ b/src/server/procedures/stats/get-stats.ts @@ -4,6 +4,7 @@ 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" // mocks @@ -215,7 +216,16 @@ const getStats = tProtectedProcedure.query(async ({ ctx: { user } }) => { }, }) - // TODO: random graph for random lift + // 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, @@ -232,13 +242,13 @@ const getStats = tProtectedProcedure.query(async ({ ctx: { user } }) => { return { data: [], exerciseName: "", unit: Units.KG } const ranSelected = exercises[Math.floor(Math.random() * exercises.length)] - const randExerciseSets = allSets.filter( + const randExerciseSets = distinctSets.filter( (set) => set.exerciseId === ranSelected.exerciseId ) if (randExerciseSets.length > 0) { const data = randExerciseSets.map( (obj): RechartsData => ({ - name: obj.createdAt.toISOString(), + name: unixToIsoDate(obj.createdAt.getTime()), weight: obj.weight, }) ) @@ -255,7 +265,7 @@ const getStats = tProtectedProcedure.query(async ({ ctx: { user } }) => { const topStatsRes: ITopStats = { ...MOCK_TOP_STATS, weightLiftedTotal: { - weight: totalKgLiftedThisWeek, + weight: Number(totalKgLiftedThisWeek.toFixed(2)), unit: Units.KG, }, randomGraph: randomExercise(), diff --git a/src/types/home-page.ts b/src/types/home-page.ts index 759adb4..454aa2f 100644 --- a/src/types/home-page.ts +++ b/src/types/home-page.ts @@ -12,9 +12,7 @@ export interface WeightLifted { unit: string } -type RechartsDataX = { name: string } -type RechartsDataY = { [value: string]: number | string } -export type RechartsData = RechartsDataX & RechartsDataY +export type RechartsData = { [key: string]: string | number } export interface ITopStats { big3: { squat: WeightLifted 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" From 0644b43ee4bea22e019abba4d298b3c92c6753e5 Mon Sep 17 00:00:00 2001 From: bcheung Date: Mon, 27 Nov 2023 13:51:01 -0500 Subject: [PATCH 14/15] add sorting so no more random order --- src/components/Workouts/index.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) 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 }) => (
Date: Mon, 27 Nov 2023 14:36:51 -0500 Subject: [PATCH 15/15] finish home page --- src/server/procedures/stats/get-stats.ts | 66 +++++++++++++++++-- .../update-workout-plan.ts | 5 ++ src/types/home-page.ts | 4 +- 3 files changed, 68 insertions(+), 7 deletions(-) diff --git a/src/server/procedures/stats/get-stats.ts b/src/server/procedures/stats/get-stats.ts index 5b5cd09..fcb767a 100644 --- a/src/server/procedures/stats/get-stats.ts +++ b/src/server/procedures/stats/get-stats.ts @@ -7,8 +7,6 @@ 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" -// mocks -import { MOCK_TOP_STATS } from "src/mocks/recent-activity" import Units from "src/constants/units" const getStats = tProtectedProcedure.query(async ({ ctx: { user } }) => { @@ -207,14 +205,74 @@ const getStats = tProtectedProcedure.query(async ({ ctx: { user } }) => { totalKgLiftedThisWeek += set.weight } - // TODO: top 3 lifts? + // 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({ @@ -263,12 +321,12 @@ const getStats = tProtectedProcedure.query(async ({ ctx: { user } }) => { // response const topStatsRes: ITopStats = { - ...MOCK_TOP_STATS, weightLiftedTotal: { weight: Number(totalKgLiftedThisWeek.toFixed(2)), unit: Units.KG, }, randomGraph: randomExercise(), + big3, } return { recentActivity: recentActivity, topStats: topStatsRes } 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/types/home-page.ts b/src/types/home-page.ts index 454aa2f..e2c9630 100644 --- a/src/types/home-page.ts +++ b/src/types/home-page.ts @@ -15,9 +15,7 @@ export interface WeightLifted { export type RechartsData = { [key: string]: string | number } export interface ITopStats { big3: { - squat: WeightLifted - bench: WeightLifted - deadlift: WeightLifted + [key: string]: WeightLifted } weightLiftedTotal: WeightLifted randomGraph: {