Compare commits
No commits in common. "39f250c0eceff3aeebbf80c828435013ee47a9e2" and "2c3a2b520fc6993b1b4eddf2530971abc3dfb582" have entirely different histories.
39f250c0ec
...
2c3a2b520f
@ -1,5 +1,5 @@
|
|||||||
import Header from "@/components/dashboard/header/Header";
|
import Header from "@/components/dashboard/header/Header";
|
||||||
import MobileSidebar from "@/components/dashboard/sidebar/MobileSidebar";
|
import MobileSidebar from "@/components/dashboard/sidebar/ModileSidebar";
|
||||||
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";
|
||||||
|
|||||||
@ -7,7 +7,7 @@ async function fetchDays(view: string, dateQuery: string) {
|
|||||||
const timezone = await api.users.getTimezone();
|
const timezone = await api.users.getTimezone();
|
||||||
|
|
||||||
const today = spacetime(dateQuery ?? "today");
|
const today = spacetime(dateQuery ?? "today");
|
||||||
const firstDay = today.subtract(2, "week").startOf("week");
|
const firstDay = today.subtract(3, "day").last("week").startOf("week");
|
||||||
|
|
||||||
const days = [];
|
const days = [];
|
||||||
for (let i = 0; i < 20; i++) {
|
for (let i = 0; i < 20; i++) {
|
||||||
|
|||||||
@ -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/MobileSidebar";
|
import MobileSidebar from "@/components/settings/sidebar/ModileSidebar";
|
||||||
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";
|
||||||
|
|||||||
@ -1,13 +1,17 @@
|
|||||||
"use client"; // Mark as client component
|
"use client"; // Mark as client component
|
||||||
|
|
||||||
import { Separator } from "@radix-ui/react-dropdown-menu";
|
import { Separator } from "@radix-ui/react-dropdown-menu";
|
||||||
import { Dispatch, useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export default function DatabaseSettings() {
|
export default function DatabaseSettings() {
|
||||||
const [uploadStatus, setUploadStatus] = useState<string | null>(null);
|
const [uploadStatus, setUploadStatus] = useState<string | null>(null);
|
||||||
const [remoteCopyStatus, setRemoteCopyStatus] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const uploadDB = async (formData: FormData, setStatus: Dispatch<string>) => {
|
// Handle form submission
|
||||||
|
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault(); // Prevent default form submission
|
||||||
|
|
||||||
|
const formData = new FormData(event.currentTarget);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/db/upload", {
|
const response = await fetch("/api/db/upload", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -15,57 +19,15 @@ export default function DatabaseSettings() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
setStatus("Database uploaded successfully!");
|
setUploadStatus("Database uploaded successfully!");
|
||||||
} else {
|
} else {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
setStatus(error.message || "An error occurred during upload.");
|
setUploadStatus(error.message || "An error occurred during upload.");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error uploading the database:", error);
|
console.error("Error uploading the database:", error);
|
||||||
setStatus("An error occurred during upload.");
|
setUploadStatus("An error occurred during upload.");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Handle form submission
|
|
||||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
|
||||||
event.preventDefault(); // Prevent default form submission
|
|
||||||
|
|
||||||
const formData = new FormData(event.currentTarget);
|
|
||||||
uploadDB(formData, setUploadStatus);
|
|
||||||
};
|
|
||||||
|
|
||||||
const remoteCopy = async () => {
|
|
||||||
|
|
||||||
const remoteUrl = "https://lifetracker.ryanpandya.com/api/db/download";
|
|
||||||
|
|
||||||
setRemoteCopyStatus("Downloading...");
|
|
||||||
const download = await fetch(remoteUrl);
|
|
||||||
if (!download.ok) {
|
|
||||||
throw new Error(`Failed to fetch from ${remoteUrl}: ${download.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const dbData = await download.blob();
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('sqliteFile', dbData, 'remoteDB-file.db');
|
|
||||||
uploadDB(formData, setRemoteCopyStatus);
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// const response = await fetch("/api/db/upload", {
|
|
||||||
// method: "POST",
|
|
||||||
// body: formData,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (response.ok) {
|
|
||||||
// setUploadStatus("Database uploaded successfully!");
|
|
||||||
// } else {
|
|
||||||
// const error = await response.json();
|
|
||||||
// setUploadStatus(error.message || "An error occurred during upload.");
|
|
||||||
// }
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error("Error uploading the database:", error);
|
|
||||||
// setUploadStatus("An error occurred during upload.");
|
|
||||||
// }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -83,7 +45,7 @@ export default function DatabaseSettings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-2 flex w-full flex-col sm:flex-row">
|
<div className="mb-8 flex w-full flex-col sm:flex-row">
|
||||||
<div className="mb-4 w-full text-lg font-medium sm:w-1/3">
|
<div className="mb-4 w-full text-lg font-medium sm:w-1/3">
|
||||||
Download SQLite Database
|
Download SQLite Database
|
||||||
</div>
|
</div>
|
||||||
@ -95,19 +57,6 @@ export default function DatabaseSettings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{process.env.NODE_ENV === 'development' ?
|
|
||||||
<div className="mb-2 flex w-full flex-col sm:flex-row">
|
|
||||||
<div className="mb-4 w-full text-lg font-medium sm:w-1/3">
|
|
||||||
Copy from Remote URL
|
|
||||||
</div>
|
|
||||||
<div className="w-full">
|
|
||||||
<div className="mb-2">
|
|
||||||
<button onClick={remoteCopy}>Copy</button>
|
|
||||||
{remoteCopyStatus && <p>{remoteCopyStatus}</p>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
: ""}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -173,21 +173,17 @@ export default function EditableHour({
|
|||||||
{
|
{
|
||||||
|
|
||||||
hour.measurements?.map(m =>
|
hour.measurements?.map(m =>
|
||||||
m.metricType === "timeseries" ?
|
|
||||||
(<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}: ${m.value} ${m.unit}`}
|
decrementCount({ metricId: m.metricId, hourId: hour.id });
|
||||||
/>)
|
}}>
|
||||||
: 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}
|
||||||
}}>
|
/>
|
||||||
<Icon name={titleCase(m.icon)} size={24}
|
</div>
|
||||||
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">
|
||||||
@ -198,11 +194,7 @@ 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,17 +12,11 @@ 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 { Trash, X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { useDecrementCount, useIncrementCount, useSetValue, } from "@lifetracker/shared-react/hooks/measurements";
|
import { useDecrementCount, useIncrementCount, } 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";
|
|
||||||
|
|
||||||
interface CreateMeasurementSchema {
|
type CreateMeasurementSchema = z.infer<typeof zMeasurementSchema>;
|
||||||
type: string;
|
|
||||||
dayId: string;
|
|
||||||
hourId: string;
|
|
||||||
metricId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function HourMeasurementsDialog({
|
export default function HourMeasurementsDialog({
|
||||||
hour: initialHour,
|
hour: initialHour,
|
||||||
@ -37,51 +31,6 @@ 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) => {
|
||||||
@ -92,6 +41,7 @@ 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!",
|
||||||
});
|
});
|
||||||
@ -149,19 +99,6 @@ 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}>
|
||||||
@ -171,104 +108,72 @@ 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) || pendingMeasurement ?
|
{hour.measurements && hour.measurements.length > 0 ?
|
||||||
(<>
|
(<>
|
||||||
{/* <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);
|
||||||
if (metric!.type === "timeseries") {
|
return (
|
||||||
return (
|
<div key={measurement.id} className="flex items-center justify-between">
|
||||||
<div key={measurement.id} className="flex items-center justify-between">
|
<div className="gap-4 flex"><Icon name={titleCase(metric.icon)} size={24} />
|
||||||
<div className="gap-4 flex"><Icon name={titleCase(metric.icon)} size={24} />
|
<div>{metric.name}</div></div>
|
||||||
<div>{metric.name}</div></div>
|
<div className="flex gap-4 items-center">
|
||||||
<div className="flex gap-4 items-center">
|
<div className="hover:cursor-pointer" onClick={() => {
|
||||||
<EditableText originalText={measurement.value + " " + metric?.unit} onSave={(value) => {
|
decrement({ metricId: metric.id, hourId: hour.id, dayId: hour.dayId });
|
||||||
setValue({
|
}} ><Icon name={"Minus"} /></div>
|
||||||
metricId: metric.id,
|
<div>{measurement.value}</div>
|
||||||
hourId: hour.id,
|
<div className="hover:cursor-pointer" onClick={() => {
|
||||||
dayId: hour.dayId,
|
increment({ metricId: metric.id, hourId: hour.id, dayId: hour.dayId });
|
||||||
value: parseFloat(value),
|
}}><Icon name={"Plus"} /></div>
|
||||||
});
|
|
||||||
}} />
|
|
||||||
<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>
|
||||||
{Object.keys(metricsByType).map(type => (
|
<div className="grid" style={{ gridTemplateColumns: "repeat(6, 1fr)" }}>
|
||||||
<>
|
{metrics.map(metric => (
|
||||||
<div className="font-bold border-b border-white">{titleCase(type)}</div>
|
// If metric.name is in currentMeasurements, don't show it
|
||||||
<div className="grid" style={{ gridTemplateColumns: "repeat(4, 1fr)", rowGap: "1em" }}>
|
currentMeasurements.includes(metric.name) ? null :
|
||||||
{metricsByType[type].map(metric => (
|
<button style={{ aspectRatio: "1/1" }} key={metric.id} className="flex flex-col items-center justify-center hover:opacity-50" onClick={() => {
|
||||||
// If metric.name is in currentMeasurements, don't show it
|
increment({ metricId: metric.id, hourId: hour.id, dayId: hour.dayId });
|
||||||
currentMeasurements.includes(metric.name) ? null :
|
// onOpenChange(false);
|
||||||
<button style={{}} key={metric.id} className="flex flex-col items-center justify-center hover:opacity-50" onClick={() => {
|
}}>
|
||||||
addMeasurement({ ...metric, metricId: metric.id!, hourId: hour.id!, dayId: hour.dayId });
|
<Icon name={titleCase(metric.icon)} size={32} />
|
||||||
// onOpenChange(false);
|
<span className="text-sm">{metric.name}</span>
|
||||||
}}>
|
</button>
|
||||||
<Icon name={titleCase(metric.icon)} size={32} />
|
))}
|
||||||
<span className="text-sm">{metric.name}</span>
|
</div>
|
||||||
</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,12 +117,7 @@ export default function AddMetricDialog({
|
|||||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>Track a New {titleCase(form.watch("type"))} Metric</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))}>
|
||||||
@ -147,30 +142,6 @@ 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"
|
||||||
@ -182,12 +153,35 @@ 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,12 +54,20 @@ 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="white" />
|
<Pencil size={16} color="red" />
|
||||||
</ButtonWithTooltip>
|
</ButtonWithTooltip>
|
||||||
</AddMetricDialog>
|
</AddMetricDialog>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import MobileSidebarItem from "@/components/shared/sidebar/MobileSidebarItem";
|
import MobileSidebarItem from "@/components/shared/sidebar/ModileSidebarItem";
|
||||||
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/metrics" />
|
<MobileSidebarItem logo={<CheckCheck />} path="/dashboard/habits" />
|
||||||
<MobileSidebarItem logo={<GaugeCircleIcon />} path="/analytics" />
|
<MobileSidebarItem logo={<GaugeCircleIcon />} path="/analytics" />
|
||||||
</ul>
|
</ul>
|
||||||
</aside>
|
</aside>
|
||||||
@ -1,18 +1,5 @@
|
|||||||
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,8 +4,6 @@ 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(),
|
||||||
@ -18,7 +16,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", "drug", "diet", "workout", "sex"]),
|
type: z.enum(["timeseries", "count"]),
|
||||||
icon: z.string(),
|
icon: z.string(),
|
||||||
measurements: z.array(zMeasurementSchema).optional(),
|
measurements: z.array(zMeasurementSchema).optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -64,10 +64,8 @@ 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,47 +24,6 @@ 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