diff --git a/apps/web/app/dashboard/layout.tsx b/apps/web/app/dashboard/layout.tsx index 4323119..477aba8 100644 --- a/apps/web/app/dashboard/layout.tsx +++ b/apps/web/app/dashboard/layout.tsx @@ -1,5 +1,5 @@ import Header from "@/components/dashboard/header/Header"; -import MobileSidebar from "@/components/dashboard/sidebar/ModileSidebar"; +import MobileSidebar from "@/components/dashboard/sidebar/MobileSidebar"; import Sidebar from "@/components/dashboard/sidebar/Sidebar"; import DemoModeBanner from "@/components/DemoModeBanner"; import { Separator } from "@/components/ui/separator"; diff --git a/apps/web/app/settings/layout.tsx b/apps/web/app/settings/layout.tsx index 3b90255..b30a89b 100644 --- a/apps/web/app/settings/layout.tsx +++ b/apps/web/app/settings/layout.tsx @@ -1,6 +1,6 @@ import Header from "@/components/dashboard/header/Header"; import DemoModeBanner from "@/components/DemoModeBanner"; -import MobileSidebar from "@/components/settings/sidebar/ModileSidebar"; +import MobileSidebar from "@/components/settings/sidebar/MobileSidebar"; import Sidebar from "@/components/settings/sidebar/Sidebar"; import { Separator } from "@/components/ui/separator"; import ValidAccountCheck from "@/components/utils/ValidAccountCheck"; diff --git a/apps/web/components/dashboard/hours/EditableHour.tsx b/apps/web/components/dashboard/hours/EditableHour.tsx index 3329632..aadd690 100644 --- a/apps/web/components/dashboard/hours/EditableHour.tsx +++ b/apps/web/components/dashboard/hours/EditableHour.tsx @@ -173,17 +173,21 @@ export default function EditableHour({ { hour.measurements?.map(m => - - Array.from({ length: m.value }).map((_, index) => -
{ - decrementCount({ metricId: m.metricId, hourId: hour.id }); - }}> - -
- ) + m.metricType === "timeseries" ? + () + : Array.from({ length: m.value }).map((_, index) => +
{ + decrementCount({ metricId: m.metricId, hourId: hour.id }); + }}> + +
+ ) )} {(hour.measurements.length > 0) ? ( @@ -194,7 +198,11 @@ export default function EditableHour({ } - + { + hour.measurements.length > 0 + ? + : + } : ""} diff --git a/apps/web/components/dashboard/hours/HourMeasurementsDialog.tsx b/apps/web/components/dashboard/hours/HourMeasurementsDialog.tsx index ede5411..78f9eea 100644 --- a/apps/web/components/dashboard/hours/HourMeasurementsDialog.tsx +++ b/apps/web/components/dashboard/hours/HourMeasurementsDialog.tsx @@ -12,11 +12,17 @@ import { ZHour } from "@lifetracker/shared/types/days"; import { Icon } from "@/components/ui/icon"; import { titleCase } from "title-case"; import { Dialog, DialogClose, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; -import { X } from "lucide-react"; -import { useDecrementCount, useIncrementCount, } from "@lifetracker/shared-react/hooks/measurements"; +import { Trash, X } from "lucide-react"; +import { useDecrementCount, useIncrementCount, useSetValue, } from "@lifetracker/shared-react/hooks/measurements"; import { Separator } from "@radix-ui/react-dropdown-menu"; +import { EditableText } from "../EditableText"; -type CreateMeasurementSchema = z.infer; +interface CreateMeasurementSchema { + type: string; + dayId: string; + hourId: string; + metricId: string; +} export default function HourMeasurementsDialog({ hour: initialHour, @@ -31,6 +37,51 @@ export default function HourMeasurementsDialog({ }) { const [hour, setHour] = useState(initialHour); const [isOpen, onOpenChange] = useState(false); + const [pendingMeasurement, setPendingMeasurement] = useState(false); + const pendingRef = useRef(null); + + useEffect(() => { + pendingRef.current?.focus(); + }, [pendingMeasurement]); + + const addMeasurement = (measurement: CreateMeasurementSchema) => { + const { type, ...rest } = measurement; + // Use a different function based on type of measurement. + // If it's a timeseries, open a sub dialog to get the value + // Use a switch statement to determine which function to call + + // Javascript case/switch: + switch (type) { + case "timeseries": + setPendingMeasurement(rest); + pendingRef.current?.focus(); + break; + default: + increment(rest); + } + } + + + const { mutate: setValue } = useSetValue({ + onSuccess: (res) => { + const oldMeasurement = hour.measurements.find(m => m.metricId === res.metricId); + const newHour = { + ...hour, + measurements: oldMeasurement ? hour.measurements.map(m => m.metricId === res.metricId ? res : m) : [...hour.measurements, res], + }; + setHour(newHour); + reload(newHour); + toast({ + description: "Measurement added!", + }); + }, + onError: (error) => { + toast({ + description: error.message, + + }); + }, + }); const { mutate: increment } = useIncrementCount({ onSuccess: (res) => { @@ -41,7 +92,6 @@ export default function HourMeasurementsDialog({ }; setHour(newHour); reload(newHour); - console.log("New hour's deets", newHour.measurements); toast({ description: "Measurement added!", }); @@ -99,6 +149,19 @@ export default function HourMeasurementsDialog({ currentMeasurements = hour.measurements.map(measurement => measurement.metricName); }, [hour]); + function groupMetricsByType(list: ZMetric[]): Record { + return list.reduce>((accumulator, item) => { + // Initialize the array for this type if it doesn't exist yet + if (!accumulator[item.type]) { + accumulator[item.type] = []; + } + accumulator[item.type].push(item); + return accumulator; + }, {}); + } + + // const metricsByType = groupMetricsByType(metrics); + const metricsByType = groupMetricsByType(metrics.filter(m => { return !currentMeasurements.includes(m.name) })); return ( @@ -108,72 +171,104 @@ export default function HourMeasurementsDialog({ Metrics for {hour.date} at {hour.datetime} - {hour.measurements && hour.measurements.length > 0 ? + {(hour.measurements && hour.measurements.length > 0) || pendingMeasurement ? (<> -
Measurements
+ {/*
Measurements
*/}
{hour.measurements.map(measurement => { const metric = metrics.find(m => m.id === measurement.metricId); - return ( -
-
-
{metric.name}
-
-
{ - decrement({ metricId: metric.id, hourId: hour.id, dayId: hour.dayId }); - }} >
-
{measurement.value}
-
{ - increment({ metricId: metric.id, hourId: hour.id, dayId: hour.dayId }); - }}>
+ if (metric!.type === "timeseries") { + return ( +
+
+
{metric.name}
+
+ { + setValue({ + metricId: metric.id, + hourId: hour.id, + dayId: hour.dayId, + value: parseFloat(value), + }); + }} /> + { + decrement({ metricId: metric.id, hourId: hour.id, dayId: hour.dayId }); + }} /> + +
+
+ ); + } + else { + return ( +
+
+
{metric.name}
+
+
{ + decrement({ metricId: metric.id, hourId: hour.id, dayId: hour.dayId }); + }} >
+
{measurement.value}
+
{ + increment({ metricId: metric.id, hourId: hour.id, dayId: hour.dayId }); + }}>
+
+
+ ); + } + })} + + {pendingMeasurement && ( +
+
+
{metrics.find(m => m.id === pendingMeasurement.metricId)?.name}
+
+
+ { + if (e.key === "Enter") { + setValue({ + ...pendingMeasurement, + value: parseFloat(pendingRef.current.value), + }); + setPendingMeasurement(false); + } + }} + /> +   + {pendingMeasurement.unit}
- ); - })} +
+ ) + }
) : <> } -
Add Measurement
-
- {metrics.map(metric => ( - // If metric.name is in currentMeasurements, don't show it - currentMeasurements.includes(metric.name) ? null : - - ))} -
+ + {Object.keys(metricsByType).map(type => ( + <> +
{titleCase(type)}
+
+ {metricsByType[type].map(metric => ( + // If metric.name is in currentMeasurements, don't show it + currentMeasurements.includes(metric.name) ? null : + + ))} +
+ ))}
- - - - - - - // // Show a dropdown menu with an option for each metric. When the user selects a metric, increment the count for that metric in the hour. - // // The dropdown should show the metric name and icon. - // // Reset the dropdown to the default value after the user selects a metric. - - // + + + + + + + Drug + Diet + Workout + Sex + Count + Timeseries + + + + + )} + /> )} /> - {/* Select field with "timeseries" and "count" */} - - ( - - - - - )} - /> {m.type} {m.description} - deleteMetric({ id: m.id! })} - loading={false} - > - - - + diff --git a/apps/web/components/dashboard/sidebar/ModileSidebar.tsx b/apps/web/components/dashboard/sidebar/MobileSidebar.tsx similarity index 89% rename from apps/web/components/dashboard/sidebar/ModileSidebar.tsx rename to apps/web/components/dashboard/sidebar/MobileSidebar.tsx index 35e69b7..5c4e5f3 100644 --- a/apps/web/components/dashboard/sidebar/ModileSidebar.tsx +++ b/apps/web/components/dashboard/sidebar/MobileSidebar.tsx @@ -1,4 +1,4 @@ -import MobileSidebarItem from "@/components/shared/sidebar/ModileSidebarItem"; +import MobileSidebarItem from "@/components/shared/sidebar/MobileSidebarItem"; import HoarderLogoIcon from "@/public/icons/logo-icon.svg"; import { CheckCheck, ClipboardList, GaugeCircleIcon, Home, HomeIcon, Search, Tag } from "lucide-react"; @@ -11,7 +11,7 @@ export default async function MobileSidebar() { path="/dashboard/day/today" /> } path="/dashboard/categories" /> - } path="/dashboard/habits" /> + } path="/dashboard/metrics" /> } path="/analytics" /> diff --git a/apps/web/components/shared/sidebar/ModileSidebarItem.tsx b/apps/web/components/shared/sidebar/MobileSidebarItem.tsx similarity index 100% rename from apps/web/components/shared/sidebar/ModileSidebarItem.tsx rename to apps/web/components/shared/sidebar/MobileSidebarItem.tsx diff --git a/packages/shared-react/hooks/measurements.ts b/packages/shared-react/hooks/measurements.ts index 9ebf75d..20440b4 100644 --- a/packages/shared-react/hooks/measurements.ts +++ b/packages/shared-react/hooks/measurements.ts @@ -1,5 +1,18 @@ import { api } from "../trpc"; + +export function useSetValue( + ...opts: Parameters +) { + const apiUtils = api.useUtils(); + return api.measurements.setValue.useMutation({ + ...opts[0], + onSuccess: (res, req, meta) => { + return opts[0]?.onSuccess?.(res, req, meta); + }, + }); +} + export function useIncrementCount( ...opts: Parameters ) { diff --git a/packages/shared/types/metrics.ts b/packages/shared/types/metrics.ts index 0b6f51f..832ae67 100644 --- a/packages/shared/types/metrics.ts +++ b/packages/shared/types/metrics.ts @@ -4,6 +4,8 @@ export const zMeasurementSchema = z.object({ id: z.string().optional(), metricId: z.string(), metricName: z.string().optional(), + metricType: z.string().optional(), + unit: z.string().nullish(), hourId: z.string().optional(), dayId: z.string().optional(), value: z.coerce.number(), @@ -16,7 +18,7 @@ export const zMetricSchema = z.object({ name: z.string(), description: z.string().optional(), unit: z.string().nullish(), - type: z.enum(["timeseries", "count"]), + type: z.enum(["timeseries", "count", "drug", "diet", "workout", "sex"]), icon: z.string(), measurements: z.array(zMeasurementSchema).optional(), }); diff --git a/packages/trpc/routers/hours.ts b/packages/trpc/routers/hours.ts index b0ee664..7939491 100644 --- a/packages/trpc/routers/hours.ts +++ b/packages/trpc/routers/hours.ts @@ -64,8 +64,10 @@ export async function hourJoinsQuery( id: measurements.id, metricId: measurements.metricId, value: measurements.value, + unit: metrics.unit, icon: metrics.icon, metricName: metrics.name, + metricType: metrics.type, }) .from(measurements) .leftJoin(metrics, eq(metrics.id, measurements.metricId)) diff --git a/packages/trpc/routers/measurements.ts b/packages/trpc/routers/measurements.ts index 68455db..2c10e92 100644 --- a/packages/trpc/routers/measurements.ts +++ b/packages/trpc/routers/measurements.ts @@ -24,6 +24,47 @@ export const measurementsAppRouter = router({ console.log(dbMeasurements.length); return dbMeasurements; }), + setValue: authedProcedure + .input(z.object({ metricId: z.string(), hourId: z.string(), dayId: z.string(), value: z.number() })) + .output(zMeasurementSchema) + .mutation(async ({ input, ctx }) => { + const metric = await ctx.db.select().from(metrics).where(eq(metrics.id, input.metricId)); + if (!metric[0]) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Metric not found", + }); + } + // Check if there is a measurement for this metric in this hour, if so, update it, if not, create it + const existingMeasurement = await ctx.db.select().from(measurements).where(and( + eq(measurements.metricId, input.metricId), + eq(measurements.hourId, input.hourId), + )); + if (existingMeasurement[0]) { + const updatedMeasurement = await ctx.db.update(measurements).set({ + value: input.value.toString(), + }).where(eq(measurements.id, existingMeasurement[0].id)).returning(); + + return { + ...updatedMeasurement[0], + icon: metric[0].icon, + metricName: metric[0].name, + }; + } else { + const newMeasurement = await ctx.db.insert(measurements).values({ + metricId: input.metricId, + hourId: input.hourId, + dayId: input.dayId, + value: input.value.toString(), + userId: ctx.user.id, + }).returning(); + return { + ...newMeasurement[0], + icon: metric[0].icon, + metricName: metric[0].name, + }; + } + }), incrementCount: authedProcedure .input(z.object({ metricId: z.string(), hourId: z.string(), dayId: z.string() })) .output(zMeasurementSchema)