From 7e46f2c38d0701c65ea6a34c53cf6627b4a7f723 Mon Sep 17 00:00:00 2001 From: Ryan Pandya Date: Sat, 7 Dec 2024 15:19:32 -0800 Subject: [PATCH] Metrics and measurements work! --- .../dashboard/categories/EditMetricDialog.tsx | 183 ---- .../dashboard/categories/MetricsView.tsx | 14 +- .../web/components/dashboard/days/DayView.tsx | 6 +- .../dashboard/hours/EditableHour.tsx | 111 +- .../dashboard/hours/EditableHourComment.tsx | 119 ++- .../hours/HourMeasurementsDialog.tsx | 101 ++ .../AddMetricDialog.tsx | 60 +- apps/web/components/ui/icon.tsx | 14 + apps/web/next.config.mjs | 1 + ...nheart.sql => 0000_gorgeous_sasquatch.sql} | 29 + packages/db/migrations/0001_chemical_toro.sql | 28 - .../db/migrations/meta/0000_snapshot.json | 217 +++- .../db/migrations/meta/0001_snapshot.json | 955 ------------------ packages/db/migrations/meta/_journal.json | 11 +- packages/db/schema.ts | 1 + packages/shared-react/hooks/measurements.ts | 25 + packages/shared/types/days.ts | 2 + packages/shared/types/metrics.ts | 4 +- packages/trpc/routers/_app.ts | 2 + packages/trpc/routers/hours.ts | 13 +- packages/trpc/routers/measurements.ts | 103 ++ packages/trpc/routers/metrics.ts | 21 +- scripts/~~ | 0 23 files changed, 752 insertions(+), 1268 deletions(-) delete mode 100644 apps/web/components/dashboard/categories/EditMetricDialog.tsx create mode 100644 apps/web/components/dashboard/hours/HourMeasurementsDialog.tsx rename apps/web/components/dashboard/{categories => metrics}/AddMetricDialog.tsx (73%) create mode 100644 apps/web/components/ui/icon.tsx rename packages/db/migrations/{0000_sad_lionheart.sql => 0000_gorgeous_sasquatch.sql} (76%) delete mode 100644 packages/db/migrations/0001_chemical_toro.sql delete mode 100644 packages/db/migrations/meta/0001_snapshot.json create mode 100644 packages/shared-react/hooks/measurements.ts create mode 100644 packages/trpc/routers/measurements.ts create mode 100644 scripts/~~ diff --git a/apps/web/components/dashboard/categories/EditMetricDialog.tsx b/apps/web/components/dashboard/categories/EditMetricDialog.tsx deleted file mode 100644 index 65aafe9..0000000 --- a/apps/web/components/dashboard/categories/EditMetricDialog.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import { useEffect, useState } from "react"; -import { ActionButton } from "@/components/ui/action-button"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogClose, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -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 { zUpdateCategoryRequestSchema, ZUpdateCategoryRequest } from "@lifetracker/shared/types/categories"; - -export default function EditMetricDialog({ - category: initialCategory, - children, -}: { - category: ZUpdateCategoryRequest; - children?: React.ReactNode; -}) { - const apiUtils = api.useUtils(); - const [isOpen, onOpenChange] = useState(false); - - const form = useForm({ - resolver: zodResolver(zUpdateCategoryRequestSchema), - defaultValues: initialCategory, - }); - const { mutate, isPending } = api.categories.update.useMutation({ - onSuccess: () => { - apiUtils.categories.list.invalidate(); - toast({ - description: "Category updated successfully", - }); - onOpenChange(false); - }, - onError: (error) => { - if (error instanceof TRPCClientError) { - toast({ - variant: "destructive", - description: error.message, - }); - } else { - toast({ - variant: "destructive", - description: "Failed to update category", - }); - } - }, - }); - - useEffect(() => { - if (!isOpen) { - form.reset(); - } - }, [isOpen, form]); - - return ( - - {children} - - - Edit Category - -
- mutate(val))}> -
- ( - - Name - - - - - - )} - /> - ( - - Code - - - - - - )} - /> - ( - - Description - - - - - - )} - /> - {/* ( - - Color - - - - - - )} - /> */} - - - - - - Update - - -
-
- -
-
- ); -} diff --git a/apps/web/components/dashboard/categories/MetricsView.tsx b/apps/web/components/dashboard/categories/MetricsView.tsx index 6eea8b5..c1083ba 100644 --- a/apps/web/components/dashboard/categories/MetricsView.tsx +++ b/apps/web/components/dashboard/categories/MetricsView.tsx @@ -15,8 +15,7 @@ import { toast } from "@/components/ui/use-toast"; import { api } from "@/lib/trpc"; import { Pencil, Trash, FilePlus, Palette } from "lucide-react"; import { useSession } from "next-auth/react"; -import AddMetricDialog from "./AddMetricDialog"; -import EditMetricDialog from "./EditMetricDialog"; +import AddMetricDialog from "../metrics/AddMetricDialog"; import Link from "next/link"; import { ZMetric } from "@lifetracker/shared/types/metrics"; @@ -24,6 +23,15 @@ export default function MetricsView() { const { data: metrics } = api.metrics.list.useQuery(); const invalidateMetricsList = api.useUtils().metrics.list.invalidate; + const { mutate: deleteMetric } = api.metrics.delete.useMutation({ + onSettled: () => { + invalidateMetricsList(); + toast({ + description: "Metric deleted", + }); + }, + }); + const MetricsTable = ({ metrics }: { metrics: ZMetric[] }) => ( @@ -44,7 +52,7 @@ export default function MetricsView() { console.log({ metricId: m.id })} + onClick={() => deleteMetric({ id: m.id! })} loading={false} > diff --git a/apps/web/components/dashboard/days/DayView.tsx b/apps/web/components/dashboard/days/DayView.tsx index bf60dad..dbd26fd 100644 --- a/apps/web/components/dashboard/days/DayView.tsx +++ b/apps/web/components/dashboard/days/DayView.tsx @@ -10,6 +10,7 @@ import { ArrowLeftSquare, ArrowRightSquare } from "lucide-react"; import spacetime from "spacetime"; import EditableHour from "@/components/dashboard/hours/EditableHour"; import { DayMetrics } from "./DayMetrics"; +import { api } from "@/server/api/client"; export default async function DayView({ day, @@ -24,6 +25,9 @@ export default async function DayView({ const prevDay = spacetime(day.date).subtract(1, "day").format("iso-short"); const nextDay = spacetime(day.date).add(1, "day").format("iso-short"); + + const userMetrics = await api.metrics.list(); + return (
@@ -101,7 +105,7 @@ export default async function DayView({ {day.hours.map((hour, i) => (
  • - +
  • ))} diff --git a/apps/web/components/dashboard/hours/EditableHour.tsx b/apps/web/components/dashboard/hours/EditableHour.tsx index 14de2bf..ac32c0e 100644 --- a/apps/web/components/dashboard/hours/EditableHour.tsx +++ b/apps/web/components/dashboard/hours/EditableHour.tsx @@ -10,21 +10,29 @@ import { EditableText } from "@/components/dashboard/EditableText"; import { format } from "date-fns"; import { TZDate } from "@date-fns/tz"; import { ZHour } from "@lifetracker/shared/types/days"; -import { MessageCircle, Pencil } from "lucide-react"; +import { MessageCircle, Pencil, Plus } from "lucide-react"; import { ButtonWithTooltip } from "@/components/ui/button"; import { EditableHourCode } from "./EditableHourCode"; import { EditableHourComment } from "./EditableHourComment"; -import { api } from "@/lib/trpc"; import spacetime from 'spacetime'; import { eq, is } from "drizzle-orm"; +import HourMeasurementsDialog from "@/components/dashboard/hours/HourMeasurementsDialog"; +import { ActionButtonWithTooltip } from "@/components/ui/action-button"; +import { ZMetric } from "@lifetracker/shared/types/metrics"; +import { Badge } from "@/components/ui/badge"; +import { Icon } from "@/components/ui/icon"; +import { titleCase } from "title-case"; +import { useDecrementCount } from "@lifetracker/shared-react/hooks/measurements"; export default function EditableHour({ hour: initialHour, i, + metrics, className, }: { hour: ZHour, i: number, + metrics: ZMetric[], className?: string; }) { const [hour, setHour] = useState(initialHour); @@ -36,7 +44,6 @@ export default function EditableHour({ comment: oldComment, ...res, }; - console.log(res); setHour(newHour); // Only show toast if client screen is larger than mobile if (window.innerWidth > 640) { @@ -46,12 +53,48 @@ export default function EditableHour({ } }, }); + const { mutate: decrementCount } = 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); + toast({ + description: res === undefined ? "Measurement removed!" : "Measurement updated!", + }); + } + }); + + const tzOffset = spacetime().offset() / 60; const localDateTime = spacetime(hour.date).add(hour.time + tzOffset, "hour"); hour.datetime = `${localDateTime.format('{hour} {ampm}')}`; - - useEffect(() => { // console.log(hour.categoryDesc); }, [hour]); @@ -63,6 +106,11 @@ export default function EditableHour({ const isToday = (localDateTime.format("iso-short") == format(now, "yyyy-MM-dd")); return isToday && isCurrentHour; } + + function reload(newHour: ZHour) { + setHour(newHour); + } + return (
    640 ? "50px 100px 1fr 50px" : "50px 100px 1fr", // Known issue: This won't work if the screen is resized, only on reload + gridTemplateColumns: window.innerWidth > 640 ? "50px 100px 3fr 2fr" : "50px 100px 1fr", // Known issue: This won't work if the screen is resized, only on reload }} > @@ -88,12 +136,14 @@ export default function EditableHour({ />
    - + {hour.categoryCode != undefined ? + + : ""}
    -
    - { - console.log("Pushed edit") - }} - > - - +
    + + {hour.categoryCode != undefined ? + +
    + {hour.measurements?.map(m => + Array.from({ length: m.value }).map((_, index) => +
    { + decrementCount({ metricId: m.metricId, hourId: hour.id }); + }}> + +
    + ) + )} + {(hour.measurements.length > 0) ? + (|) + : "" + } + + + + +
    + : ""}
    ); diff --git a/apps/web/components/dashboard/hours/EditableHourComment.tsx b/apps/web/components/dashboard/hours/EditableHourComment.tsx index 1e52673..b89acd3 100644 --- a/apps/web/components/dashboard/hours/EditableHourComment.tsx +++ b/apps/web/components/dashboard/hours/EditableHourComment.tsx @@ -1,7 +1,8 @@ "use client"; -import { or } from "drizzle-orm"; -import { useEffect, useRef } from "react"; +import { set } from "date-fns"; +import { MessageCircle } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; function selectHourCode(time: number) { document.getElementById("hour-" + (time).toString())?.getElementsByClassName("edit-hour-code")[0].focus(); @@ -21,70 +22,80 @@ export function EditableHourComment({ const ref = useRef(null); + const [text, setText] = useState(originalText); + useEffect(() => { if (ref.current) { - ref.current.value = originalText; + ref.current.value = text; } }, [ref]); + // Update the text when Hour changes + useEffect(() => { + setText(hour.comment ?? hour.categoryName); + }, [hour]); + + // Update the ref element whenever the Text changes + useEffect(() => { + if (ref.current) { + ref.current.value = text; + } + }, [text]); + const submit = () => { - let newComment: string | null = ref.current?.value ?? null; - if (originalText == newComment) { - // Nothing to do here - selectHourComment(hour.time + 1); - return; - } - if (newComment == "") { - if (originalText == null) { - // Set value to previous hour's value - newComment = document.getElementById("hour-" + (i - 1).toString())?.getElementsByClassName("edit-hour-comment")[0].value; - } - else { - newComment = null; - } - } + setText(ref.current?.value ?? originalText); onSubmit({ date: hour.date, hourTime: hour.time, dayId: hour.dayId, - comment: newComment, + comment: ref.current?.value ?? "", code: hour.categoryCode.toString(), - }) - selectHourComment(hour.time + 1); + }); + selectHourComment(i + 1); }; return ( - { - if (e.key === "Enter") { - e.preventDefault(); - submit(); - } - if (e.key == "ArrowDown") { - e.preventDefault(); - selectHourComment(i + 1); - } - if (e.key == "ArrowUp") { - e.preventDefault(); - selectHourComment(i - 1); - } - if (e.key == "ArrowLeft") { - e.preventDefault(); - selectHourCode(i); - } - }} - onClick={(e) => { - e.target.select(); - }} - onFocus={(e) => { - e.target.select(); - }} - /> +
    + {hour.comment ? + + : ""} + { + if (e.key === "Enter") { + e.preventDefault(); + submit(); + } + if (e.key == "ArrowDown") { + e.preventDefault(); + selectHourComment(i + 1); + } + if (e.key == "ArrowUp") { + e.preventDefault(); + selectHourComment(i - 1); + } + if (e.key == "ArrowLeft") { + e.preventDefault(); + selectHourCode(i); + } + }} + onClick={(e) => { + e.target.select(); + }} + onFocus={(e) => { + e.target.select(); + }} + onBlur={(e) => { + if (!e.target.value) { + e.target.value = originalText ?? ""; + } + }} + /> +
    ); } diff --git a/apps/web/components/dashboard/hours/HourMeasurementsDialog.tsx b/apps/web/components/dashboard/hours/HourMeasurementsDialog.tsx new file mode 100644 index 0000000..f356b63 --- /dev/null +++ b/apps/web/components/dashboard/hours/HourMeasurementsDialog.tsx @@ -0,0 +1,101 @@ +"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 { 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 { X } from "lucide-react"; +import { useIncrementCount, } from "@lifetracker/shared-react/hooks/measurements"; + +type CreateMeasurementSchema = z.infer; + +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 { mutate: increment } = useIncrementCount({ + onSuccess: (res) => { + onOpenChange(false); + + 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], + }; + + reload(newHour); + toast({ + description: "Measurement added!", + }); + }, + onError: (error) => { + toast({ + description: error.message, + + }); + }, + }); + + return ( + + {children} + + + Add Measurement + +
    + {metrics.map(metric => ( + + ))} +
    +
    +
    + + + + + + + // // 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. + + // + { + searchIcons(e.target.value); + form.setValue("icon", e.target.value); + } + } + onBlur={() => { + if (form.getValues("icon") == "" && initialMetric?.icon) { + { + form.setValue("icon", initialMetric.icon); + searchIcons(initialMetric.icon); + } + } + }} + /> + + + + )} /> + Browse icons +