Add more metric types, update dialog, fix typo "modile" to "mobile"
This commit is contained in:
parent
613d6f3f32
commit
39f250c0ec
@ -1,5 +1,5 @@
|
|||||||
import Header from "@/components/dashboard/header/Header";
|
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 Sidebar from "@/components/dashboard/sidebar/Sidebar";
|
||||||
import DemoModeBanner from "@/components/DemoModeBanner";
|
import DemoModeBanner from "@/components/DemoModeBanner";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import Header from "@/components/dashboard/header/Header";
|
import Header from "@/components/dashboard/header/Header";
|
||||||
import DemoModeBanner from "@/components/DemoModeBanner";
|
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 Sidebar from "@/components/settings/sidebar/Sidebar";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import ValidAccountCheck from "@/components/utils/ValidAccountCheck";
|
import ValidAccountCheck from "@/components/utils/ValidAccountCheck";
|
||||||
|
|||||||
@ -173,17 +173,21 @@ export default function EditableHour({
|
|||||||
{
|
{
|
||||||
|
|
||||||
hour.measurements?.map(m =>
|
hour.measurements?.map(m =>
|
||||||
|
m.metricType === "timeseries" ?
|
||||||
Array.from({ length: m.value }).map((_, index) =>
|
(<Icon name={titleCase(m.icon)} size={24}
|
||||||
<div key={`${m.id}-${index}`} className="hover:cursor-no-drop" onClick={(e) => {
|
color={hour.foreground}
|
||||||
decrementCount({ metricId: m.metricId, hourId: hour.id });
|
tooltip={`${m.metricName}: ${m.value} ${m.unit}`}
|
||||||
}}>
|
/>)
|
||||||
<Icon name={titleCase(m.icon)} size={24}
|
: Array.from({ length: m.value }).map((_, index) =>
|
||||||
color={hour.foreground}
|
<div key={`${m.id}-${index}`} className="hover:cursor-no-drop" onClick={(e) => {
|
||||||
tooltip={m.metricName}
|
decrementCount({ metricId: m.metricId, hourId: hour.id });
|
||||||
/>
|
}}>
|
||||||
</div>
|
<Icon name={titleCase(m.icon)} size={24}
|
||||||
)
|
color={hour.foreground}
|
||||||
|
tooltip={`${m.metricName} x${m.value}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
{(hour.measurements.length > 0) ?
|
{(hour.measurements.length > 0) ?
|
||||||
(<span className="mx-2 opacity-50">
|
(<span className="mx-2 opacity-50">
|
||||||
@ -194,7 +198,11 @@ export default function EditableHour({
|
|||||||
}
|
}
|
||||||
|
|
||||||
<HourMeasurementsDialog hour={hour} metrics={metrics} reload={reload}>
|
<HourMeasurementsDialog hour={hour} metrics={metrics} reload={reload}>
|
||||||
<Plus size={16} className="opacity-50 hover:cursor-pointer" />
|
{
|
||||||
|
hour.measurements.length > 0
|
||||||
|
? <Pencil size={16} className="opacity-50 hover:cursor-pointer" />
|
||||||
|
: <Plus size={16} className="opacity-50 hover:cursor-pointer" />
|
||||||
|
}
|
||||||
</HourMeasurementsDialog>
|
</HourMeasurementsDialog>
|
||||||
</div>
|
</div>
|
||||||
: ""}
|
: ""}
|
||||||
|
|||||||
@ -12,11 +12,17 @@ import { ZHour } from "@lifetracker/shared/types/days";
|
|||||||
import { Icon } from "@/components/ui/icon";
|
import { Icon } from "@/components/ui/icon";
|
||||||
import { titleCase } from "title-case";
|
import { titleCase } from "title-case";
|
||||||
import { Dialog, DialogClose, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
import { Dialog, DialogClose, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||||
import { X } from "lucide-react";
|
import { Trash, X } from "lucide-react";
|
||||||
import { useDecrementCount, useIncrementCount, } from "@lifetracker/shared-react/hooks/measurements";
|
import { useDecrementCount, useIncrementCount, useSetValue, } from "@lifetracker/shared-react/hooks/measurements";
|
||||||
import { Separator } from "@radix-ui/react-dropdown-menu";
|
import { Separator } from "@radix-ui/react-dropdown-menu";
|
||||||
|
import { EditableText } from "../EditableText";
|
||||||
|
|
||||||
type CreateMeasurementSchema = z.infer<typeof zMeasurementSchema>;
|
interface CreateMeasurementSchema {
|
||||||
|
type: string;
|
||||||
|
dayId: string;
|
||||||
|
hourId: string;
|
||||||
|
metricId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function HourMeasurementsDialog({
|
export default function HourMeasurementsDialog({
|
||||||
hour: initialHour,
|
hour: initialHour,
|
||||||
@ -31,6 +37,51 @@ export default function HourMeasurementsDialog({
|
|||||||
}) {
|
}) {
|
||||||
const [hour, setHour] = useState(initialHour);
|
const [hour, setHour] = useState(initialHour);
|
||||||
const [isOpen, onOpenChange] = useState(false);
|
const [isOpen, onOpenChange] = useState(false);
|
||||||
|
const [pendingMeasurement, setPendingMeasurement] = useState(false);
|
||||||
|
const pendingRef = useRef<HTMLInputElement>(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({
|
const { mutate: increment } = useIncrementCount({
|
||||||
onSuccess: (res) => {
|
onSuccess: (res) => {
|
||||||
@ -41,7 +92,6 @@ export default function HourMeasurementsDialog({
|
|||||||
};
|
};
|
||||||
setHour(newHour);
|
setHour(newHour);
|
||||||
reload(newHour);
|
reload(newHour);
|
||||||
console.log("New hour's deets", newHour.measurements);
|
|
||||||
toast({
|
toast({
|
||||||
description: "Measurement added!",
|
description: "Measurement added!",
|
||||||
});
|
});
|
||||||
@ -99,6 +149,19 @@ export default function HourMeasurementsDialog({
|
|||||||
currentMeasurements = hour.measurements.map(measurement => measurement.metricName);
|
currentMeasurements = hour.measurements.map(measurement => measurement.metricName);
|
||||||
}, [hour]);
|
}, [hour]);
|
||||||
|
|
||||||
|
function groupMetricsByType(list: ZMetric[]): Record<string, ZMetric[]> {
|
||||||
|
return list.reduce<Record<string, ZMetric[]>>((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 (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||||
@ -108,72 +171,104 @@ export default function HourMeasurementsDialog({
|
|||||||
<DialogTitle>Metrics for {hour.date} at {hour.datetime}</DialogTitle>
|
<DialogTitle>Metrics for {hour.date} at {hour.datetime}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<Separator />
|
<Separator />
|
||||||
{hour.measurements && hour.measurements.length > 0 ?
|
{(hour.measurements && hour.measurements.length > 0) || pendingMeasurement ?
|
||||||
(<>
|
(<>
|
||||||
<div className="font-bold">Measurements</div>
|
{/* <div className="font-bold">Measurements</div> */}
|
||||||
<div className="mx-4 mb-4">
|
<div className="mx-4 mb-4">
|
||||||
{hour.measurements.map(measurement => {
|
{hour.measurements.map(measurement => {
|
||||||
const metric = metrics.find(m => m.id === measurement.metricId);
|
const metric = metrics.find(m => m.id === measurement.metricId);
|
||||||
return (
|
if (metric!.type === "timeseries") {
|
||||||
<div key={measurement.id} className="flex items-center justify-between">
|
return (
|
||||||
<div className="gap-4 flex"><Icon name={titleCase(metric.icon)} size={24} />
|
<div key={measurement.id} className="flex items-center justify-between">
|
||||||
<div>{metric.name}</div></div>
|
<div className="gap-4 flex"><Icon name={titleCase(metric.icon)} size={24} />
|
||||||
<div className="flex gap-4 items-center">
|
<div>{metric.name}</div></div>
|
||||||
<div className="hover:cursor-pointer" onClick={() => {
|
<div className="flex gap-4 items-center">
|
||||||
decrement({ metricId: metric.id, hourId: hour.id, dayId: hour.dayId });
|
<EditableText originalText={measurement.value + " " + metric?.unit} onSave={(value) => {
|
||||||
}} ><Icon name={"Minus"} /></div>
|
setValue({
|
||||||
<div>{measurement.value}</div>
|
metricId: metric.id,
|
||||||
<div className="hover:cursor-pointer" onClick={() => {
|
hourId: hour.id,
|
||||||
increment({ metricId: metric.id, hourId: hour.id, dayId: hour.dayId });
|
dayId: hour.dayId,
|
||||||
}}><Icon name={"Plus"} /></div>
|
value: parseFloat(value),
|
||||||
|
});
|
||||||
|
}} />
|
||||||
|
<Trash size={16} color="red" onClick={() => {
|
||||||
|
decrement({ metricId: metric.id, hourId: hour.id, dayId: hour.dayId });
|
||||||
|
}} />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return (
|
||||||
|
<div key={measurement.id} className="flex items-center justify-between">
|
||||||
|
<div className="gap-4 flex"><Icon name={titleCase(metric.icon)} size={24} />
|
||||||
|
<div>{metric.name}</div></div>
|
||||||
|
<div className="flex gap-4 items-center">
|
||||||
|
<div className="hover:cursor-pointer" onClick={() => {
|
||||||
|
decrement({ metricId: metric.id, hourId: hour.id, dayId: hour.dayId });
|
||||||
|
}} ><Icon name={"Minus"} /></div>
|
||||||
|
<div>{measurement.value}</div>
|
||||||
|
<div className="hover:cursor-pointer" onClick={() => {
|
||||||
|
increment({ metricId: metric.id, hourId: hour.id, dayId: hour.dayId });
|
||||||
|
}}><Icon name={"Plus"} /></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
|
||||||
|
{pendingMeasurement && (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="gap-4 flex"><Icon name={titleCase(pendingMeasurement.icon)} size={24} />
|
||||||
|
<div>{metrics.find(m => m.id === pendingMeasurement.metricId)?.name}</div></div>
|
||||||
|
<div className="flex gap-4 items-center">
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
ref={pendingRef}
|
||||||
|
type="text" className="border border-gray-300 rounded-md p-1 mr-1 w-16 text-right"
|
||||||
|
value={pendingMeasurement.value}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
setValue({
|
||||||
|
...pendingMeasurement,
|
||||||
|
value: parseFloat(pendingRef.current.value),
|
||||||
|
});
|
||||||
|
setPendingMeasurement(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{pendingMeasurement.unit}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
})}
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</>)
|
</>)
|
||||||
: <>
|
: <>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
<div className="font-bold">Add Measurement</div>
|
|
||||||
<div className="grid" style={{ gridTemplateColumns: "repeat(6, 1fr)" }}>
|
{Object.keys(metricsByType).map(type => (
|
||||||
{metrics.map(metric => (
|
<>
|
||||||
// If metric.name is in currentMeasurements, don't show it
|
<div className="font-bold border-b border-white">{titleCase(type)}</div>
|
||||||
currentMeasurements.includes(metric.name) ? null :
|
<div className="grid" style={{ gridTemplateColumns: "repeat(4, 1fr)", rowGap: "1em" }}>
|
||||||
<button style={{ aspectRatio: "1/1" }} key={metric.id} className="flex flex-col items-center justify-center hover:opacity-50" onClick={() => {
|
{metricsByType[type].map(metric => (
|
||||||
increment({ metricId: metric.id, hourId: hour.id, dayId: hour.dayId });
|
// If metric.name is in currentMeasurements, don't show it
|
||||||
// onOpenChange(false);
|
currentMeasurements.includes(metric.name) ? null :
|
||||||
}}>
|
<button style={{}} key={metric.id} className="flex flex-col items-center justify-center hover:opacity-50" onClick={() => {
|
||||||
<Icon name={titleCase(metric.icon)} size={32} />
|
addMeasurement({ ...metric, metricId: metric.id!, hourId: hour.id!, dayId: hour.dayId });
|
||||||
<span className="text-sm">{metric.name}</span>
|
// onOpenChange(false);
|
||||||
</button>
|
}}>
|
||||||
))}
|
<Icon name={titleCase(metric.icon)} size={32} />
|
||||||
</div>
|
<span className="text-sm">{metric.name}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div></>
|
||||||
|
))}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// // 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.
|
|
||||||
|
|
||||||
// <Select
|
|
||||||
// className="w-full h-2"
|
|
||||||
// unstyled={true}
|
|
||||||
// isSearchable={false}
|
|
||||||
// placeholder="Add"
|
|
||||||
// options={metrics.map(metric =>
|
|
||||||
// ({ value: metric.id, label: metric.name, icon: metric.icon }))}
|
|
||||||
// onChange={(selectedOption) => incrementCount(selectedOption.value)}
|
|
||||||
// classNames={{
|
|
||||||
// menu: (state) =>
|
|
||||||
// "bg-card px-4 py-2 text-white rounded-md shadow-lg",
|
|
||||||
// }}
|
|
||||||
// />
|
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,8 +52,8 @@ export default function AddMetricDialog({
|
|||||||
const form = useForm<CreateMetricSchema>({
|
const form = useForm<CreateMetricSchema>({
|
||||||
resolver: zodResolver(zMetricSchema),
|
resolver: zodResolver(zMetricSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
type: "count",
|
||||||
...initialMetric,
|
...initialMetric,
|
||||||
type: "count"
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const handleSuccess = (message: string) => {
|
const handleSuccess = (message: string) => {
|
||||||
@ -117,7 +117,12 @@ export default function AddMetricDialog({
|
|||||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Track a New {titleCase(form.watch("type"))} Metric</DialogTitle>
|
<DialogTitle>
|
||||||
|
{initialMetric
|
||||||
|
? "Update " + initialMetric.name
|
||||||
|
: "New " + titleCase(form.watch('type')) + " Metric"
|
||||||
|
}
|
||||||
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit((val) => mutate(val))}>
|
<form onSubmit={form.handleSubmit((val) => mutate(val))}>
|
||||||
@ -142,6 +147,30 @@ export default function AddMetricDialog({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="type"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<Select onValueChange={field.onChange} defaultValue={initialMetric?.type ?? "count"}>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a type of metric to track" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="drug">Drug</SelectItem>
|
||||||
|
<SelectItem value="diet">Diet</SelectItem>
|
||||||
|
<SelectItem value="workout">Workout</SelectItem>
|
||||||
|
<SelectItem value="sex">Sex</SelectItem>
|
||||||
|
<SelectItem value="count">Count</SelectItem>
|
||||||
|
<SelectItem value="timeseries">Timeseries</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="unit"
|
name="unit"
|
||||||
@ -153,35 +182,12 @@ export default function AddMetricDialog({
|
|||||||
placeholder="Unit"
|
placeholder="Unit"
|
||||||
{...field}
|
{...field}
|
||||||
className="w-full rounded border p-2"
|
className="w-full rounded border p-2"
|
||||||
disabled={(form.watch("type") === "count")}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{/* Select field with "timeseries" and "count" */}
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="type"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<Select onValueChange={field.onChange} defaultValue={"count"}>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select a type of metric to track" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="timeseries">Timeseries</SelectItem>
|
|
||||||
<SelectItem value="count">Count</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<FormField
|
<FormField
|
||||||
|
|||||||
@ -54,20 +54,12 @@ export default function MetricsView() {
|
|||||||
<TableCell className="py-1">{m.type}</TableCell>
|
<TableCell className="py-1">{m.type}</TableCell>
|
||||||
<TableCell className="py-1">{m.description}</TableCell>
|
<TableCell className="py-1">{m.description}</TableCell>
|
||||||
<TableCell className="flex gap-1 py-1">
|
<TableCell className="flex gap-1 py-1">
|
||||||
<ActionButtonWithTooltip
|
|
||||||
tooltip="Delete category"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => deleteMetric({ id: m.id! })}
|
|
||||||
loading={false}
|
|
||||||
>
|
|
||||||
<Trash size={16} color="red" />
|
|
||||||
</ActionButtonWithTooltip>
|
|
||||||
<AddMetricDialog initialMetric={m} >
|
<AddMetricDialog initialMetric={m} >
|
||||||
<ButtonWithTooltip
|
<ButtonWithTooltip
|
||||||
tooltip="Edit"
|
tooltip="Edit"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
<Pencil size={16} color="red" />
|
<Pencil size={16} color="white" />
|
||||||
</ButtonWithTooltip>
|
</ButtonWithTooltip>
|
||||||
</AddMetricDialog>
|
</AddMetricDialog>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@ -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 HoarderLogoIcon from "@/public/icons/logo-icon.svg";
|
||||||
import { CheckCheck, ClipboardList, GaugeCircleIcon, Home, HomeIcon, Search, Tag } from "lucide-react";
|
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/day/today"
|
||||||
/>
|
/>
|
||||||
<MobileSidebarItem logo={<Tag />} path="/dashboard/categories" />
|
<MobileSidebarItem logo={<Tag />} path="/dashboard/categories" />
|
||||||
<MobileSidebarItem logo={<CheckCheck />} path="/dashboard/habits" />
|
<MobileSidebarItem logo={<CheckCheck />} path="/dashboard/metrics" />
|
||||||
<MobileSidebarItem logo={<GaugeCircleIcon />} path="/analytics" />
|
<MobileSidebarItem logo={<GaugeCircleIcon />} path="/analytics" />
|
||||||
</ul>
|
</ul>
|
||||||
</aside>
|
</aside>
|
||||||
@ -1,5 +1,18 @@
|
|||||||
import { api } from "../trpc";
|
import { api } from "../trpc";
|
||||||
|
|
||||||
|
|
||||||
|
export function useSetValue(
|
||||||
|
...opts: Parameters<typeof api.measurements.setValue.useMutation>
|
||||||
|
) {
|
||||||
|
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(
|
export function useIncrementCount(
|
||||||
...opts: Parameters<typeof api.measurements.incrementCount.useMutation>
|
...opts: Parameters<typeof api.measurements.incrementCount.useMutation>
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -4,6 +4,8 @@ export const zMeasurementSchema = z.object({
|
|||||||
id: z.string().optional(),
|
id: z.string().optional(),
|
||||||
metricId: z.string(),
|
metricId: z.string(),
|
||||||
metricName: z.string().optional(),
|
metricName: z.string().optional(),
|
||||||
|
metricType: z.string().optional(),
|
||||||
|
unit: z.string().nullish(),
|
||||||
hourId: z.string().optional(),
|
hourId: z.string().optional(),
|
||||||
dayId: z.string().optional(),
|
dayId: z.string().optional(),
|
||||||
value: z.coerce.number(),
|
value: z.coerce.number(),
|
||||||
@ -16,7 +18,7 @@ export const zMetricSchema = z.object({
|
|||||||
name: z.string(),
|
name: z.string(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
unit: z.string().nullish(),
|
unit: z.string().nullish(),
|
||||||
type: z.enum(["timeseries", "count"]),
|
type: z.enum(["timeseries", "count", "drug", "diet", "workout", "sex"]),
|
||||||
icon: z.string(),
|
icon: z.string(),
|
||||||
measurements: z.array(zMeasurementSchema).optional(),
|
measurements: z.array(zMeasurementSchema).optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -64,8 +64,10 @@ export async function hourJoinsQuery(
|
|||||||
id: measurements.id,
|
id: measurements.id,
|
||||||
metricId: measurements.metricId,
|
metricId: measurements.metricId,
|
||||||
value: measurements.value,
|
value: measurements.value,
|
||||||
|
unit: metrics.unit,
|
||||||
icon: metrics.icon,
|
icon: metrics.icon,
|
||||||
metricName: metrics.name,
|
metricName: metrics.name,
|
||||||
|
metricType: metrics.type,
|
||||||
})
|
})
|
||||||
.from(measurements)
|
.from(measurements)
|
||||||
.leftJoin(metrics, eq(metrics.id, measurements.metricId))
|
.leftJoin(metrics, eq(metrics.id, measurements.metricId))
|
||||||
|
|||||||
@ -24,6 +24,47 @@ export const measurementsAppRouter = router({
|
|||||||
console.log(dbMeasurements.length);
|
console.log(dbMeasurements.length);
|
||||||
return dbMeasurements;
|
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
|
incrementCount: authedProcedure
|
||||||
.input(z.object({ metricId: z.string(), hourId: z.string(), dayId: z.string() }))
|
.input(z.object({ metricId: z.string(), hourId: z.string(), dayId: z.string() }))
|
||||||
.output(zMeasurementSchema)
|
.output(zMeasurementSchema)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user