From 3fb913842e38187cb04ff8aec445ed56a544f103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aron=20Sch=C3=BCler?= Date: Sat, 8 Jun 2024 12:00:10 +0200 Subject: [PATCH] feat: introduce dialog for goals, improve ux --- src/hooks/useGoals.ts | 61 +++++++++++ src/hooks/useIsEnglish.ts | 8 ++ src/router.tsx | 12 +-- src/types/Goals.ts | 19 ++++ src/types/GolfSwingData.ts | 50 ++++++--- src/views/Goal.tsx | 23 ++++ src/views/GoalForm.tsx | 95 +++++++++++++++++ src/views/GoalList.tsx | 15 +++ src/views/Goals.tsx | 211 +++++-------------------------------- src/views/ProgressBar.tsx | 24 +++++ 10 files changed, 311 insertions(+), 207 deletions(-) create mode 100644 src/hooks/useGoals.ts create mode 100644 src/hooks/useIsEnglish.ts create mode 100644 src/types/Goals.ts create mode 100644 src/views/Goal.tsx create mode 100644 src/views/GoalForm.tsx create mode 100644 src/views/GoalList.tsx create mode 100644 src/views/ProgressBar.tsx diff --git a/src/hooks/useGoals.ts b/src/hooks/useGoals.ts new file mode 100644 index 0000000..a41f9f7 --- /dev/null +++ b/src/hooks/useGoals.ts @@ -0,0 +1,61 @@ +import { atom, useAtom } from "jotai"; +import { useEffect } from "react"; +import { Goal, PartialGoal } from "../types/Goals"; +import { + golfSwingDataKeysInDegrees, + golfSwingDataKeysInMeters, +} from "../types/GolfSwingData"; +import { useAveragedSwings } from "../utils/calculateAverages"; +import { useIsEnglish } from "./useIsEnglish"; + +export const goalAtom = atom([]); +export const useGoals: () => Goal[] = () => { + const isEnglish = useIsEnglish(); + const [goals, setGoals] = useAtom(goalAtom); + useEffect( + () => + setGoals( + isEnglish + ? [ + { + id: "1", + title: "Driving distance", + target: 200, + metric: "Carry Distance", + }, + ] + : [ + { + id: "1", + title: "Driving distance", + target: 200, + metric: "Gesamtstrecke", + }, + ], + ), + [isEnglish, setGoals], + ); + const averages = useAveragedSwings(); + + const calculateGoalProgress = (partialGoal: PartialGoal) => { + const current = + averages.find((average) => average.name === "Driver")?.[ + "Carry Distance" + ] || + averages.find((average) => average.name === "Driver")?.["Gesamtstrecke"]; + + const progress = current ? (current / partialGoal.target) * 100 : 0; + const progressText = `${(current ? (current / partialGoal.target) * 100 : 0).toFixed(2)}%`; + const unit = golfSwingDataKeysInMeters.includes(partialGoal.metric) + ? "m" + : golfSwingDataKeysInDegrees.includes(partialGoal.metric) + ? "°" + : ""; + return { progress, progressText, current, unit }; + }; + + return goals.map((goal) => ({ + ...goal, + ...calculateGoalProgress(goal), + })); +}; diff --git a/src/hooks/useIsEnglish.ts b/src/hooks/useIsEnglish.ts new file mode 100644 index 0000000..633e7d6 --- /dev/null +++ b/src/hooks/useIsEnglish.ts @@ -0,0 +1,8 @@ +import { useSelectedSessions } from "./useSelectedSessions"; + +export const useIsEnglish = () => { + const sessions = useSelectedSessions(); + return Object.values(sessions).some( + (session) => session.results.length && !!session.results[0]?.["Ball Speed"], + ); +}; diff --git a/src/router.tsx b/src/router.tsx index ebfcde2..0458e16 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -1,18 +1,18 @@ import { Navigate, Outlet, createBrowserRouter } from "react-router-dom"; -import { Layout } from "./views/Layout"; -import { Visualization } from "./views/Visualization"; -import { Authentication } from "./views/Authentication"; +import { routes } from "./routes"; import { RedirectIfNotLoggedIn, RedirectIfSignedIn, } from "./utils/AuthRedirects"; +import { Authentication } from "./views/Authentication"; +import { Dashboard } from "./views/Dashboard"; import { Goals } from "./views/Goals"; +import { Layout } from "./views/Layout"; import { NewLayout } from "./views/NewLayout"; -import { routes } from "./routes"; -import { Sessions } from "./views/Sessions"; import { Reports } from "./views/Reports"; +import { Sessions } from "./views/Sessions"; import { Settings } from "./views/Settings"; -import { Dashboard } from "./views/Dashboard"; +import { Visualization } from "./views/Visualization"; export const router = createBrowserRouter([ { diff --git a/src/types/Goals.ts b/src/types/Goals.ts new file mode 100644 index 0000000..077c077 --- /dev/null +++ b/src/types/Goals.ts @@ -0,0 +1,19 @@ +import { GolfSwingDataDE, GolfSwingDataEN } from "./GolfSwingData"; + +export type Goal = { + id: string; + title: string; + current: string | number | null | undefined; + target: number; + progressText: string; + progress: number; + unit: string; +}; + +export type PartialGoal = { + id: string; + title: string; + target: number; + club?: string; + metric: keyof GolfSwingDataDE | keyof GolfSwingDataEN; +}; diff --git a/src/types/GolfSwingData.ts b/src/types/GolfSwingData.ts index a89b5b2..3031f6f 100644 --- a/src/types/GolfSwingData.ts +++ b/src/types/GolfSwingData.ts @@ -68,30 +68,52 @@ export type GolfSwingDataDE = { export type GolfSwingData = GolfSwingDataEN & GolfSwingDataDE; -export const golfSwingDataKeysInMeters: Array< - keyof GolfSwingDataEN | keyof GolfSwingDataDE -> = [ +export const englishMetersMetrics: (keyof GolfSwingDataEN)[] = [ "Carry Distance", - "Carry-Distanz", - "Carry Deviation Distance", - "Carry-Abweichungsdistanz", "Total Distance", - "Gesamtstrecke", "Total Deviation Distance", - "Gesamtabweichungsdistanz", + "Carry Deviation Distance", "Apex Height", +]; + +export const germanMetersMetrics: (keyof GolfSwingDataDE)[] = [ + "Carry-Distanz", + "Gesamtstrecke", + "Gesamtabweichungsdistanz", + "Carry-Abweichungsdistanz", "Höhe des Scheitelpunkts", ]; -export const golfSwingDataKeysInDegrees: Array< +export const golfSwingDataKeysInMeters: Array< keyof GolfSwingDataEN | keyof GolfSwingDataDE -> = [ +> = [...englishMetersMetrics, ...germanMetersMetrics]; + +export const englishDegreeMetrics = [ "Carry Deviation Angle", - "Carry-Abweichungswinkel", "Total Deviation Angle", - "Gesamtabweichungswinkel", "Launch Direction", - "Abflugrichtung", "Club Face", + "Spin Axis", + "Launch Angle", + "Attack Angle", + "Face to Path", + "Club Path", +]; + +export const germanDegreeMetrics: (keyof GolfSwingDataDE)[] = [ + "Carry-Abweichungswinkel", + "Gesamtabweichungswinkel", + "Abflugrichtung", "Abflugwinkel", -] as unknown as Array; + "Schlagflächenstellung", + "Schlagfläche", + "Drehachse", + "Schwungbahn", + "Anstellwinkel", +]; + +export const golfSwingDataKeysInDegrees: Array< + keyof GolfSwingDataEN | keyof GolfSwingDataDE +> = [...englishDegreeMetrics, ...germanDegreeMetrics] as unknown as Array< + keyof GolfSwingData +>; diff --git a/src/views/Goal.tsx b/src/views/Goal.tsx new file mode 100644 index 0000000..8a223f7 --- /dev/null +++ b/src/views/Goal.tsx @@ -0,0 +1,23 @@ +import type { Goal as GoalType } from "../types/Goals"; +import { ProgressBar } from "./ProgressBar"; + +export const Goal = ({ goal }: { goal: GoalType }) => { + return ( +
+

{goal.title}

+
+
+

+ Current: {goal.current + goal.unit} +

+

+ Target: {goal.target + goal.unit} +

+

+ Progress: {goal.progressText} +

+
+ +
+ ); +}; diff --git a/src/views/GoalForm.tsx b/src/views/GoalForm.tsx new file mode 100644 index 0000000..147c3fe --- /dev/null +++ b/src/views/GoalForm.tsx @@ -0,0 +1,95 @@ +import { useAtom } from "jotai"; +import { useForm } from "react-hook-form"; +import { useClubsPerSession } from "../hooks/useClubsPerSesssion"; +import { goalAtom } from "../hooks/useGoals"; +import { useIsEnglish } from "../hooks/useIsEnglish"; +import { + GolfSwingDataDE, + GolfSwingDataEN, + englishDegreeMetrics, + englishMetersMetrics, + germanDegreeMetrics, + germanMetersMetrics, + golfSwingDataKeysInMeters, +} from "../types/GolfSwingData"; + +export const GoalForm = ({ closeAction }: { closeAction: () => void }) => { + const formMethods = useForm<{ + title: string; + target: number; + club?: string; + metric: keyof GolfSwingDataDE | keyof GolfSwingDataEN; + }>(); + + const isEnglish = useIsEnglish(); + const metricOptions = isEnglish + ? [...englishDegreeMetrics, ...englishMetersMetrics] + : [...germanDegreeMetrics, ...germanMetersMetrics]; + const [, setGoals] = useAtom(goalAtom); + const clubs = useClubsPerSession(); + return ( +
+
{ + setGoals((goals) => [ + ...goals, + { + id: (goals.length + 1).toString(), + title: data.title, + target: data.target, + metric: data.metric, + }, + ]); + closeAction(); + })} + > +
+ + + + + + + +
+ +
+
+ ); +}; diff --git a/src/views/GoalList.tsx b/src/views/GoalList.tsx new file mode 100644 index 0000000..2e6cbcc --- /dev/null +++ b/src/views/GoalList.tsx @@ -0,0 +1,15 @@ +import { useGoals } from "../hooks/useGoals"; +import { Goal } from "./Goal"; + +export const GoalList = () => { + const goals = useGoals(); + + return ( +
+

Your goals

+ {goals.map((goal) => ( + + ))} +
+ ); +}; diff --git a/src/views/Goals.tsx b/src/views/Goals.tsx index be5b358..2e06f38 100644 --- a/src/views/Goals.tsx +++ b/src/views/Goals.tsx @@ -1,197 +1,34 @@ -import { atom, useAtom } from "jotai"; -import { useForm } from "react-hook-form"; +import { useState } from "react"; +import { BaseDialog } from "../components/base/BaseDialog"; import { BasePageLayout } from "../components/base/BasePageLayout"; -import { - GolfSwingDataDE, - GolfSwingDataEN, - golfSwingDataKeysInDegrees, - golfSwingDataKeysInMeters, -} from "../types/GolfSwingData"; -import { useAveragedSwings } from "../utils/calculateAverages"; +import { GoalForm } from "./GoalForm"; +import { GoalList } from "./GoalList"; -export const Goals = () => ( - -

Goals

- - -
-); - -const GoalList = () => { - const goals = useGoals(); - - return ( -
-

Your goals

- {goals.map((goal) => ( - - ))} - -
- ); -}; - -const Goal = ({ goal }: { goal: Goal }) => { +export const Goals = () => { return ( -
-

{goal.title}

-
-
-

- Current: {goal.current + goal.unit} -

-

- Target: {goal.target + goal.unit} -

-

- Progress: {goal.progressText} -

-
- -
+ +

Goals

+ + +
); }; -type Goal = { - id: string; - title: string; - current: string | number | null | undefined; - target: number; - progressText: string; - progress: number; - unit: string; -}; - -type PartialGoal = { - id: string; - title: string; - target: number; - metric: keyof GolfSwingDataDE | keyof GolfSwingDataEN; -}; - -const useGoals: () => Goal[] = () => { - const averages = useAveragedSwings(); - const [goals] = useAtom(goalAtom); - - const calculateGoalProgress = (partialGoal: PartialGoal) => { - const current = - averages.find((average) => average.name === "Driver")?.[ - "Carry Distance" - ] || - averages.find((average) => average.name === "Driver")?.["Gesamtstrecke"]; - - const progress = current ? (current / partialGoal.target) * 100 : 0; - const progressText = `${(current ? (current / partialGoal.target) * 100 : 0).toFixed(2)}%`; - const unit = golfSwingDataKeysInMeters.includes(partialGoal.metric) - ? "m" - : golfSwingDataKeysInDegrees.includes(partialGoal.metric) - ? "°" - : ""; - return { progress, progressText, current, unit }; - }; - - return goals.map((goal) => ({ - ...goal, - ...calculateGoalProgress(goal), - })); -}; - -const goalAtom = atom([ - { - id: "1", - title: "Driving distance", - target: 200, - metric: "Gesamtstrecke", - }, -]); - -const GoalForm = () => { - const formMethods = useForm<{ - title: string; - target: number; - metric: keyof GolfSwingDataDE | keyof GolfSwingDataEN; - }>(); - - const metricOptions = [ - ...golfSwingDataKeysInDegrees, - ...golfSwingDataKeysInMeters, - ]; - const [, setGoals] = useAtom(goalAtom); +const GoalDialog = () => { + const [showDialog, setShowDialog] = useState(false); return ( -
-

Add a new goal

-
- setGoals((goals) => [ - ...goals, - { - id: (goals.length + 1).toString(), - title: data.title, - target: data.target, - metric: data.metric, - }, - ]), - )} + <> + + + setShowDialog(false)} + title="Add a new goal" > -
- - - - - -
- - -
- ); -}; - -const ProgressBar = ({ progress }: { progress: number }) => { - return ( -
-
-
- - {progress.toFixed(2)}% - -
-
- - 100% - -
-
-
-
-
-
+ setShowDialog(false)} /> + + ); }; diff --git a/src/views/ProgressBar.tsx b/src/views/ProgressBar.tsx new file mode 100644 index 0000000..958c2e1 --- /dev/null +++ b/src/views/ProgressBar.tsx @@ -0,0 +1,24 @@ +export const ProgressBar = ({ progress }: { progress: number }) => { + return ( +
+
+
+ + {progress.toFixed(2)}% + +
+
+ + 100% + +
+
+
+
+
+
+ ); +};