"use client"; import { useEffect, useState, useRef } from "react"; import { toast } from "@/components/ui/use-toast"; import { api } from "@/lib/trpc"; import { zodResolver } from "@hookform/resolvers/zod"; import { TRPCClientError } from "@trpc/client"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { ZMeasurement, zMeasurementSchema, ZMetric } from "@lifetracker/shared/types/metrics"; 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 { 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"; import spacetime from "spacetime"; interface CreateMeasurementSchema { type: string; dayId: string; hourId: string; metricId: string; } export default function HourMeasurementsDialog({ hour: initialHour, metrics, reload, children, }: { hour: ZHour, metrics: ZMetric[], reload: (newHour: ZHour) => void, children?: React.ReactNode; }) { 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) => { 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: decrement } = useDecrementCount({ onSuccess: (res, req) => { const oldMeasurementIndex = hour.measurements.findIndex(m => m.metricId === req.metricId); let newMeasurements; if (oldMeasurementIndex !== -1) { if (res === undefined) { // Remove the measurement if res is undefined newMeasurements = [ ...hour.measurements.slice(0, oldMeasurementIndex), ...hour.measurements.slice(oldMeasurementIndex + 1) ]; } else { // Update the measurement newMeasurements = [ ...hour.measurements.slice(0, oldMeasurementIndex), res, ...hour.measurements.slice(oldMeasurementIndex + 1) ]; } } else { // Add the new measurement newMeasurements = [...hour.measurements, res]; } const newHour = { ...hour, measurements: newMeasurements, }; setHour(newHour); reload(newHour); toast({ description: res === undefined ? "Measurement removed!" : "Measurement updated!", }); } }); let currentMeasurements = hour.measurements.map(measurement => measurement.metricName); // With useEffect, update currentMeasurements when hour changes useEffect(() => { 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 ( {children} Metrics for {hour.date} at {spacetime(hour.datetime).format("{hour} {ampm}")} {(hour.measurements && hour.measurements.length > 0) || pendingMeasurement ? (<> {/*
Measurements
*/}
{hour.measurements!.map(measurement => { const metric = metrics.find(m => m.id === measurement.metricId); 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! }); }} />
); } 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}
) }
) : <> } {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 : ))}
))}
); }