From 28576f6e564e7762ca5ba2b962fdcaf00e0ea338 Mon Sep 17 00:00:00 2001 From: Andor Kesselman Date: Mon, 29 Apr 2024 12:58:04 +0530 Subject: [PATCH 1/4] adding configure update --- README.md | 6 +- src/app/page.tsx | 2 +- src/app/workoutselectionview/index.tsx | 35 ++- .../configureRoutine/ConfigureRoutine.tsx | 214 ++++++++++++++++++ src/lib/protocols/routine.ts | 5 +- src/models/workout.ts | 7 + src/pages/configreRoutine/index.tsx | 0 src/pages/workoutselectionview/index.tsx | 79 ------- 8 files changed, 261 insertions(+), 87 deletions(-) create mode 100644 src/components/configureRoutine/ConfigureRoutine.tsx create mode 100644 src/pages/configreRoutine/index.tsx delete mode 100644 src/pages/workoutselectionview/index.tsx diff --git a/README.md b/README.md index 4ea1a9f..aa1403d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ Tabata timers are used for HIT excrcises. They give you short increments of exercies and rest. I use it quite a bit, but the one I use has a paywall after 2 -routines. This is a web5 based tabata timer. +routines and has a ton of ads. I don't want any of that. + +This is a web5 based tabata timer. What does web5 mean? Well, you'll own your data. Entirely. This means that you don't need to worry about us looking at it ever. This was mostly a fun project @@ -38,3 +40,5 @@ bun dev - [ ] Record Sessions - [ ] Sync - [ ] Share sessions +- [ ] Launch on play store +- [ ] diff --git a/src/app/page.tsx b/src/app/page.tsx index c22ca2a..cf9a0a3 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,5 +1,5 @@ import Image from "next/image"; -import WorkoutSelectionView from "@/pages/workoutselectionview"; +import WorkoutSelectionView from "@/app/workoutselectionview"; export default function Home() { return ( diff --git a/src/app/workoutselectionview/index.tsx b/src/app/workoutselectionview/index.tsx index d3aee26..3c80ddf 100644 --- a/src/app/workoutselectionview/index.tsx +++ b/src/app/workoutselectionview/index.tsx @@ -5,6 +5,7 @@ import { Routine } from "@/models/workout"; import { storeRoutine, getRoutines } from "@/lib/store/dwn/routines"; import { useWeb5 } from "@/context/Web5Context"; import { useState, useEffect } from "react"; +import RoutineConfigurationForm from "@/components/configureRoutine/ConfigureRoutine"; const mockRoutines: Routine[] = [ { @@ -34,10 +35,14 @@ const mockRoutines: Routine[] = [ export default function WorkoutSelectionView() { const { web5, did } = useWeb5(); const [routines, setRoutines] = useState([]); + const [showModal, setShowModal] = useState(false); // State to control the modal visibility - const handleAddWorkout = async () => { - console.log("handling workout"); - storeRoutine(mockRoutines[0], web5); + const handleAddWorkout = () => { + setShowModal(true); + }; + + const handleCloseModal = () => { + setShowModal(false); }; const handleGetRoutines = async (web5) => { @@ -68,12 +73,34 @@ export default function WorkoutSelectionView() {
DID: {did}

Web5 Workouts

- +
{routines.map((routine) => ( ))}
+ {showModal && ( +
+
+
+
+ + +
+
+
+ )}
); } diff --git a/src/components/configureRoutine/ConfigureRoutine.tsx b/src/components/configureRoutine/ConfigureRoutine.tsx new file mode 100644 index 0000000..a1dbe16 --- /dev/null +++ b/src/components/configureRoutine/ConfigureRoutine.tsx @@ -0,0 +1,214 @@ +import React, { useState } from "react"; +import { RoutineConfiguration } from "@/models/workout"; // Import your types from the correct location +import { useWeb5 } from "@/context/Web5Context"; +import { storeRoutine } from "@/lib/store/dwn/routines"; + +// Validation function +export const validateRoutineConfiguration = ( + r: RoutineConfiguration, +): boolean => { + const routine = r.routine; + return ( + routine.Prepare.duration >= 0 && + routine.Work.duration >= 0 && + routine.Rest.duration >= 0 && + routine.Cycles.value >= 1 && + routine.Sets.value >= 1 && + routine.RestBetweenSteps.duration >= 0 && + routine.CoolDown.duration >= 0 + ); +}; + +const RoutineConfigurationForm: React.FC = () => { + const { web5, did } = useWeb5(); + + const [routineConfig, setRoutineConfig] = useState({ + name: "", + title: "", + description: "", + routine: { + Prepare: { duration: 300, name: "Preparation", value: 60 }, + Work: { duration: 50, name: "Work", value: 30 }, + Rest: { duration: 60, name: "Rest", value: 10 }, + Cycles: { value: 6, name: "Cycles" }, + Sets: { value: 4, name: "Sets" }, + RestBetweenSteps: { + duration: 120, + name: "Rest Between Cycles", + value: 30, + }, + CoolDown: { duration: 300, name: "Cool Down", value: 60 }, + }, + }); + const [errors, setErrors] = useState([]); + + const handleInputChange = (event, key) => { + const value = event.target.value; + if (key === "Cycles" || key === "Sets") { + setRoutineConfig((prevConfig) => ({ + ...prevConfig, + [key]: { value: parseInt(value) }, + })); + } else { + setRoutineConfig((prevConfig) => ({ + ...prevConfig, + [key]: value, + })); + } + }; + + // Function to handle form submission + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + const isValid = validateRoutineConfiguration(routineConfig); + if (isValid) { + setErrors([]); + storeRoutine(routineConfig, web5); + console.log("got routine config", routineConfig); + } else { + // Display validation errors + setErrors(["Invalid routine configuration. Please check your inputs."]); + } + }; + + return ( +
+
+
+

Routine Configuration

+
+
+ {/* Form inputs */} + {errors.length > 0 && ( +
+ {errors.map((error, index) => ( +

{error}

+ ))} +
+ )} +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+
+ ); +}; + +export default RoutineConfigurationForm; diff --git a/src/lib/protocols/routine.ts b/src/lib/protocols/routine.ts index 9e957f4..2262267 100644 --- a/src/lib/protocols/routine.ts +++ b/src/lib/protocols/routine.ts @@ -23,11 +23,12 @@ export const routineProtocol = { $actions: [ { who: "anyone", - can: "create", + can: ["create"], }, { who: "author", - can: "update", + of: "session", + can: ["create", "update"], }, ], }, diff --git a/src/models/workout.ts b/src/models/workout.ts index 125fec3..f29514e 100644 --- a/src/models/workout.ts +++ b/src/models/workout.ts @@ -38,3 +38,10 @@ export type Session = { routine: Routine; completed: boolean; }; + +export const validateRoutineConfiguration = (routine: RoutineConfiguration) : boolean { + if ((routine.Prepare.duration < 0) || (routine.Work.duration < 0) || (routine.Rest.duration < 0) || (routine.Cycles.value < 1) || (routine.Sets.value < 1) || (routine.RestBetweenSteps.duration < 0) || (routine.CoolDown.duration < 0)){ + return false + } return true + +} diff --git a/src/pages/configreRoutine/index.tsx b/src/pages/configreRoutine/index.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/workoutselectionview/index.tsx b/src/pages/workoutselectionview/index.tsx deleted file mode 100644 index 80b8f89..0000000 --- a/src/pages/workoutselectionview/index.tsx +++ /dev/null @@ -1,79 +0,0 @@ -// pages/workouts.tsx -"use client"; -import RoutineCard from "@/components/RoutineCard"; // Make sure the path is correct -import { Routine } from "@/models/workout"; -import { storeRoutine, getRoutines } from "@/lib/store/dwn/routines"; -import { useWeb5 } from "@/context/Web5Context"; -import { useState, useEffect } from "react"; - -const mockRoutines: Routine[] = [ - { - name: "Morning Boost", - title: "Quick Morning Routine", - description: "A quick, energetic start to your morning.", - lastUpdated: new Date().toISOString(), - createdOn: new Date().toISOString(), - createdBy: "user123", - id: "routine1", - routine: { - Prepare: { duration: 60, name: "Preparation", value: 60 }, - Work: { duration: 30, name: "Work", value: 30 }, - Rest: { duration: 10, name: "Rest", value: 10 }, - Cycles: { value: 3, name: "Cycles" }, - Sets: { value: 2, name: "Sets" }, - RestBetweenSteps: { - duration: 30, - name: "Rest Between Cycles", - value: 30, - }, - CoolDown: { duration: 60, name: "Cool Down", value: 60 }, - }, - }, -]; - -export default function WorkoutSelectionView() { - const { web5, did } = useWeb5(); - const [routines, setRoutines] = useState([]); - - const handleAddWorkout = async () => { - console.log("handling workout"); - storeRoutine(mockRoutines[0], web5); - }; - - const handleGetRoutines = async (web5) => { - try { - const r = await getRoutines(web5); - const routinesData = await Promise.all( - r?.records?.map(async (v, _) => { - console.log(v); - const vv = await v.data.json(); - vv.id = v.id; - return vv; // Return vv to include it in the array of routinesData - }), - ); - setRoutines(routinesData); // Update state with the fetched routinesData - console.log(routinesData); - } catch (error) { - console.error("Error fetching routines:", error); - } - }; - - useEffect(() => { - if (web5) { - handleGetRoutines(web5); - } - }, [web5]); - - return ( -
- DID: {did} -

Workouts

- -
- {routines.map((routine) => ( - - ))} -
-
- ); -} From 2d9c1357bd82994e47089b13c3a2f195b671fbb2 Mon Sep 17 00:00:00 2001 From: Andor Kesselman Date: Mon, 29 Apr 2024 16:10:53 +0530 Subject: [PATCH 2/4] updated --- src/components/configureRoutine/AddWorkoutModal.tsx | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/components/configureRoutine/AddWorkoutModal.tsx diff --git a/src/components/configureRoutine/AddWorkoutModal.tsx b/src/components/configureRoutine/AddWorkoutModal.tsx new file mode 100644 index 0000000..e69de29 From b6b025d3c48219a3e6cf95c9535c9f7065d76e8a Mon Sep 17 00:00:00 2001 From: Andor Kesselman Date: Mon, 29 Apr 2024 16:10:59 +0530 Subject: [PATCH 3/4] adding workout works --- src/app/page.tsx | 2 +- src/app/playview/[id]/page.tsx | 8 +- src/app/workoutselectionview/index.tsx | 20 ++++- src/components/RoutineCard.tsx | 9 +-- .../configureRoutine/ConfigureRoutine.tsx | 74 ++++++++++--------- src/context/TimerContext.tsx | 24 ++++-- 6 files changed, 87 insertions(+), 50 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index cf9a0a3..0cca8cd 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -4,7 +4,7 @@ import WorkoutSelectionView from "@/app/workoutselectionview"; export default function Home() { return (
-
+
diff --git a/src/app/playview/[id]/page.tsx b/src/app/playview/[id]/page.tsx index e2a3521..78c0fe8 100644 --- a/src/app/playview/[id]/page.tsx +++ b/src/app/playview/[id]/page.tsx @@ -110,12 +110,13 @@ export default function PlayView({ params }: { params: { routerId: string } }) { startStepTimer, setTimeElapsed, setTotalTime, + setIsPaused, } = useTimer(); const [routine, setRoutine] = useState(null); const [steps, setSteps] = useState([]); const [currentStep, setCurrentStep] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); + const [isPlaying, setIsPlaying] = useState(true); const [isDone, setIsDone] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false); @@ -130,6 +131,11 @@ export default function PlayView({ params }: { params: { routerId: string } }) { const totalCycles = 3; const currentCycle = 1; + useEffect(() => { + console.log("setting to ", !isPlaying); + setIsPaused(!isPlaying); + }, [isPlaying]); + const computeTimeElapsed = (steps: Step[]): number => { const time = steps.reduce((total, step) => { return total + step.duration; diff --git a/src/app/workoutselectionview/index.tsx b/src/app/workoutselectionview/index.tsx index 3c80ddf..40b5fd4 100644 --- a/src/app/workoutselectionview/index.tsx +++ b/src/app/workoutselectionview/index.tsx @@ -5,6 +5,8 @@ import { Routine } from "@/models/workout"; import { storeRoutine, getRoutines } from "@/lib/store/dwn/routines"; import { useWeb5 } from "@/context/Web5Context"; import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; + import RoutineConfigurationForm from "@/components/configureRoutine/ConfigureRoutine"; const mockRoutines: Routine[] = [ @@ -36,6 +38,7 @@ export default function WorkoutSelectionView() { const { web5, did } = useWeb5(); const [routines, setRoutines] = useState([]); const [showModal, setShowModal] = useState(false); // State to control the modal visibility + const router = useRouter(); const handleAddWorkout = () => { setShowModal(true); @@ -50,7 +53,6 @@ export default function WorkoutSelectionView() { const r = await getRoutines(web5); const routinesData = await Promise.all( r?.records?.map(async (v, _) => { - console.log(v); const vv = await v.data.json(); vv.id = v.id; return vv; // Return vv to include it in the array of routinesData @@ -63,6 +65,22 @@ export default function WorkoutSelectionView() { } }; + useEffect(() => { + if (web5) { + console.log("GOT Dispatched Event"); + const handleFormSubmitted = async () => { + setShowModal(false); + console.log("testing dispatch"); + await handleGetRoutines(web5); + console.log("passed dispatch"); + }; + document.addEventListener("routineSubmitted", handleFormSubmitted); + return () => { + document.removeEventListener("routineSubmitted", handleFormSubmitted); + }; + } + }, [router, web5]); + useEffect(() => { if (web5) { handleGetRoutines(web5); diff --git a/src/components/RoutineCard.tsx b/src/components/RoutineCard.tsx index 75ad903..be92a4c 100644 --- a/src/components/RoutineCard.tsx +++ b/src/components/RoutineCard.tsx @@ -11,7 +11,6 @@ interface RoutineCardProps { } const RoutineCard: React.FC = ({ routine }) => { - console.log(routine.id); return (
@@ -20,13 +19,11 @@ const RoutineCard: React.FC = ({ routine }) => { ▶️ - + */}
-

- {routine.title} ({routine.name}) -

+

{routine.name}

{routine.description}

{routine.id.slice(-6)}

diff --git a/src/components/configureRoutine/ConfigureRoutine.tsx b/src/components/configureRoutine/ConfigureRoutine.tsx index a1dbe16..4445cef 100644 --- a/src/components/configureRoutine/ConfigureRoutine.tsx +++ b/src/components/configureRoutine/ConfigureRoutine.tsx @@ -24,7 +24,6 @@ const RoutineConfigurationForm: React.FC = () => { const [routineConfig, setRoutineConfig] = useState({ name: "", - title: "", description: "", routine: { Prepare: { duration: 300, name: "Preparation", value: 60 }, @@ -40,31 +39,43 @@ const RoutineConfigurationForm: React.FC = () => { CoolDown: { duration: 300, name: "Cool Down", value: 60 }, }, }); + const [errors, setErrors] = useState([]); const handleInputChange = (event, key) => { - const value = event.target.value; - if (key === "Cycles" || key === "Sets") { - setRoutineConfig((prevConfig) => ({ - ...prevConfig, - [key]: { value: parseInt(value) }, - })); - } else { - setRoutineConfig((prevConfig) => ({ - ...prevConfig, - [key]: value, - })); + let value = event.target.value; + + if (event.target.type === "number") { + value = parseInt(value); } + + const keys = key.split("."); + const updatedRoutineConfig = { ...routineConfig }; + let currentObj = updatedRoutineConfig; + for (let i = 0; i < keys.length; i++) { + const currentKey = keys[i]; + if (i === keys.length - 1) { + currentObj[currentKey] = value; + } else { + currentObj = currentObj[currentKey]; + } + } + setRoutineConfig(updatedRoutineConfig); }; + const storeRoutineWrapper = async (routineConfig, web5) => { + await storeRoutine(routineConfig, web5); + console.log("got routine config", routineConfig); + document.dispatchEvent(new CustomEvent("routineSubmitted")); + console.log("dispatched Event"); + }; // Function to handle form submission const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); const isValid = validateRoutineConfiguration(routineConfig); if (isValid) { setErrors([]); - storeRoutine(routineConfig, web5); - console.log("got routine config", routineConfig); + storeRoutineWrapper(routineConfig, web5); } else { // Display validation errors setErrors(["Invalid routine configuration. Please check your inputs."]); @@ -95,18 +106,7 @@ const RoutineConfigurationForm: React.FC = () => { handleInputChange(e, "Name")} - className="py-2 px-3 border border-gray-300 rounded-md" - /> - -
-
- @@ -117,7 +117,7 @@ const RoutineConfigurationForm: React.FC = () => { handleInputChange(e, "Description")} + onChange={(e) => handleInputChange(e, "description")} className="py-2 px-3 border border-gray-300 rounded-md" /> @@ -128,7 +128,9 @@ const RoutineConfigurationForm: React.FC = () => { handleInputChange(e, "Prepare")} + onChange={(e) => + handleInputChange(e, "routine.Prepare.duration") + } className="py-2 px-3 border border-gray-300 rounded-md" /> @@ -139,7 +141,7 @@ const RoutineConfigurationForm: React.FC = () => { handleInputChange(e, "Cycles")} + onChange={(e) => handleInputChange(e, "routine.Cycles")} className="py-2 px-3 border border-gray-300 rounded-md" /> @@ -150,7 +152,7 @@ const RoutineConfigurationForm: React.FC = () => { handleInputChange(e, "Sets")} + onChange={(e) => handleInputChange(e, "routine.Sets.value")} className="py-2 px-3 border border-gray-300 rounded-md" /> @@ -161,7 +163,7 @@ const RoutineConfigurationForm: React.FC = () => { handleInputChange(e, "Work")} + onChange={(e) => handleInputChange(e, "routine.Work.duration")} className="py-2 px-3 border border-gray-300 rounded-md" /> @@ -172,7 +174,7 @@ const RoutineConfigurationForm: React.FC = () => { handleInputChange(e, "Rest")} + onChange={(e) => handleInputChange(e, "routine.Rest.duration")} className="py-2 px-3 border border-gray-300 rounded-md" /> @@ -183,7 +185,9 @@ const RoutineConfigurationForm: React.FC = () => { handleInputChange(e, "RestBetweenSteps")} + onChange={(e) => + handleInputChange(e, "routine.RestBetweenSteps.duration") + } className="py-2 px-3 border border-gray-300 rounded-md" /> @@ -194,7 +198,9 @@ const RoutineConfigurationForm: React.FC = () => { handleInputChange(e, "CoolDown")} + onChange={(e) => + handleInputChange(e, "routine.CoolDown.duration") + } className="py-2 px-3 border border-gray-300 rounded-md" /> diff --git a/src/context/TimerContext.tsx b/src/context/TimerContext.tsx index bd76814..7d1374c 100644 --- a/src/context/TimerContext.tsx +++ b/src/context/TimerContext.tsx @@ -19,14 +19,19 @@ export const TimerProvider = ({ children }) => { }); const { elapsedTime, totalTime } = state; const [timeLeft, setTimeLeft] = useState(0); + const [isPaused, setIsPaused] = useState(false); useEffect(() => { const stepInterval = setInterval(() => { - setStepTime((prevStepTime) => Math.max(prevStepTime - 1, 0)); - setState((prevState) => ({ - ...prevState, - elapsedTime: prevState.elapsedTime + 1, - })); + if (!isPaused) { + setStepTime((prevStepTime) => Math.max(prevStepTime - 1, 0)); + setState((prevState) => ({ + ...prevState, + elapsedTime: prevState.elapsedTime + 1, + })); + } else { + console.log("is paused"); + } }, 1000); return () => { @@ -40,6 +45,7 @@ export const TimerProvider = ({ children }) => { const startTimers = (totalSeconds) => { setState({ elapsedTime: 0, totalTime: totalSeconds }); + setIsPaused(false); }; const startStepTimer = (seconds) => { @@ -54,7 +60,11 @@ export const TimerProvider = ({ children }) => { state.totalTime = seconds; }; - const endTimer = () => {}; + useEffect(() => { + if (timeLeft === 0) { + console.log("Timer ended"); + } + }, [timeLeft]); return ( { elapsedTime, totalTime, timeLeft, - endTimer, startTimers, startStepTimer, setTimeElapsed, setTotalTime, + setIsPaused, }} > {children} From 722fed5718fb7acb169b77316b105e627541ea8e Mon Sep 17 00:00:00 2001 From: Andor Kesselman Date: Mon, 29 Apr 2024 16:12:43 +0530 Subject: [PATCH 4/4] added dockerfile --- Dockerfile | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3d1b852 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,67 @@ +FROM node:18-alpine AS base + +# Install dependencies only when needed +FROM base AS deps +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Install dependencies based on the preferred package manager +COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ +RUN \ + if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ + elif [ -f package-lock.json ]; then npm ci; \ + elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \ + else echo "Lockfile not found." && exit 1; \ + fi + + +# Rebuild the source code only when needed +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Next.js collects completely anonymous telemetry data about general usage. +# Learn more here: https://nextjs.org/telemetry +# Uncomment the following line in case you want to disable telemetry during the build. +# ENV NEXT_TELEMETRY_DISABLED 1 + +RUN \ + if [ -f yarn.lock ]; then yarn run build; \ + elif [ -f package-lock.json ]; then npm run build; \ + elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \ + else echo "Lockfile not found." && exit 1; \ + fi + +# Production image, copy all the files and run next +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV production +# Uncomment the following line in case you want to disable telemetry during runtime. +# ENV NEXT_TELEMETRY_DISABLED 1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public + +# Set the correct permission for prerender cache +RUN mkdir .next +RUN chown nextjs:nodejs .next + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT 3000 + +# server.js is created by next build from the standalone output +# https://nextjs.org/docs/pages/api-reference/next-config-js/output +CMD HOSTNAME="0.0.0.0" node server.js