Checkpoint: Can actually log time (track life, if you will) using CLI and Web.

This commit is contained in:
Ryan Pandya 2024-11-30 23:36:01 -05:00
parent a85e8f294a
commit d0c07fd8b8
18 changed files with 348 additions and 112 deletions

View File

@ -66,41 +66,43 @@ export const daysCmd = new Command()
const day = (await api.days.get.query({ dateQuery: dateQuery, timezone: timezone }));
console.log(`Snagged day with id '${day.id}'`);
if (getGlobalOptions().json) {
printObject(day);
} else {
// if (getGlobalOptions().json) {
// printObject(day);
// } else {
const moodStr = moodToStars(day.mood);
const dateStr = format(day.date, "EEEE, MMMM do", { in: utc });
const data: string[][] = [[dateStr, '', moodStr], [day.comment ?? "", '', ''],
[`Time (${timezones[timezone]})`, "Category", "Comment"]
];
const moodStr = moodToStars(day.mood);
const dateStr = format(day.date, "EEEE, MMMM do", { in: utc });
const data: string[][] = [[dateStr, '', moodStr], [day.comment ?? "", '', ''],
[`Time (${timezones[timezone]})`, "Category", "Comment"]
];
day.hours.forEach((h, i) => {
data.push([
getHourFromTime(i, 'twelves', `(${timezones[timezone]})`),
h.categoryName ?? "--",
h.comment ?? ""
]);
})
console.log(day.hours.map((h) => h.date));
if (flags?.hour) {
doHour(day.hours, day, flags.hour, timezone);
}
else {
day.hours.forEach((h, i) => {
data.push([
getHourFromTime(i, 'twelves', `(${timezones[timezone]})`),
h.categoryName ?? "--",
h.comment ?? ""
]);
})
console.log(table(data, {
// border: getBorderCharacters("ramac"),
// singleLine: true,
spanningCells: [{ col: 0, row: 1, colSpan: 3 }, { col: 1, row: 2, colSpan: 2 }],
drawVerticalLine: (lineIndex, columnCount) => {
return lineIndex === 0 || lineIndex === columnCount || (lineIndex === 0 && columnCount === 2);
},
drawHorizontalLine: (lineIndex, rowCount) => {
return (lineIndex < 1 || lineIndex === 2 || lineIndex === 3 || lineIndex === rowCount);
},
}));
}
if (flags?.hour) {
doHour(day, flags.hour, timezone);
}
else {
console.log(table(data, {
// border: getBorderCharacters("ramac"),
// singleLine: true,
spanningCells: [{ col: 0, row: 1, colSpan: 3 }, { col: 1, row: 2, colSpan: 2 }],
drawVerticalLine: (lineIndex, columnCount) => {
return lineIndex === 0 || lineIndex === columnCount || (lineIndex === 0 && columnCount === 2);
},
drawHorizontalLine: (lineIndex, rowCount) => {
return (lineIndex < 1 || lineIndex === 2 || lineIndex === 3 || lineIndex === rowCount);
},
}));
// }
}
} catch (error) {
printErrorMessageWithReason("Failed", error as object);

View File

@ -26,11 +26,12 @@ export const hoursCmd = new Command()
return "Hello";
});
export async function doHour(hours: ZHour[], day: ZDay, hourFlags: string[], timezone = "UTC") {
export async function doHour(day: ZDay, hourFlags: string[], timezone = "UTC") {
const hourNum = getTimeFromHour(hourFlags.shift()!);
const hour = hours[hourNum];
const hour = day.hours[hourNum];
if (hourFlags.length == 0) {
console.log("No new values to set; printing hour.")
printHour(hour, hourNum, day, timezone);
}
else {
@ -48,6 +49,7 @@ export async function doHour(hours: ZHour[], day: ZDay, hourFlags: string[], tim
}
}
const props = { dateQuery: hour.date!, time: hour.time, code: hourFlags[0], comment: hourFlags[1] || null };
// console.log(props);
res = await api.hours.update.mutate({ ...props });
printHour(res as ZHour, hourNum, day, timezone);

View File

@ -2,6 +2,8 @@ import { notFound } from "next/navigation";
import { api } from "@/server/api/client";
import { TRPCError } from "@trpc/server";
import DayView from "@/components/dashboard/days/DayView";
import spacetime from "spacetime";
import { useTimezone } from "@lifetracker/shared-react/hooks/timezones";
export default async function DayPage({
params,
@ -9,9 +11,12 @@ export default async function DayPage({
params: { dateQuery: string };
}) {
let day;
const tzName = await api.users.getTimezone();
console.log(`Displaying ${spacetime.now(tzName).format()} in ${tzName}.`);
try {
day = await api.days.get({
dateQuery: params.dateQuery,
dateQuery: spacetime(params.dateQuery, tzName).format(),
timezone: tzName,
});
} catch (e) {
if (e instanceof TRPCError) {
@ -23,8 +28,10 @@ export default async function DayPage({
}
return (
<DayView
day={day}
/>
<>
<DayView
day={day}
/>
</>
);
}

View File

@ -1,7 +1,7 @@
export default function DemoModeBanner() {
return (
<div className="h-min w-full rounded bg-yellow-100 px-4 py-2 text-center text-black">
Demo mode is on. All modifications are disabled.
YO
</div>
);
}

View File

@ -12,6 +12,9 @@ import Link from "next/link";
import { cn } from "@/lib/utils";
import { ArrowLeftSquare, ArrowRightSquare } from "lucide-react";
import { UTCDate, utc } from "@date-fns/utc";
import spacetime from "spacetime";
import EditableHour from "@/components/hours/EditableHour";
export default async function DayView({
day,
}: {
@ -22,8 +25,8 @@ export default async function DayView({
redirect("/");
}
const prevDay = format(addDays(day.date, -1), "yyyy-MM-dd");
const nextDay = format(addDays(day.date, 1), "yyyy-MM-dd");
const prevDay = spacetime(day.date).subtract(1, "day").format("iso-short");
const nextDay = spacetime(day.date).add(1, "day").format("iso-short");
return (
<div className="flex flex-col gap-3">
@ -41,7 +44,7 @@ export default async function DayView({
</div>
</Link>
<span className="text-2xl flex-1">
{format(day.date, "EEEE, MMMM do", { in: utc })}
{spacetime(day.date).format("{day}, {month} {date}, {year}")}
</span>
<Link
href={`/dashboard/day/${nextDay}`}
@ -70,9 +73,29 @@ export default async function DayView({
<Separator />
<ul>
<li>
<div className={cn(
"p-4 grid justify-between",
)}
style={{
fontFamily: "inherit",
gridTemplateColumns: "100px 1fr 200px"
}}
>
<span className="text-right">
Time
</span>
<span className="text-center">
Category
</span>
<div className="text-right">
Actions
</div>
</div>
</li>
{day.hours.map((hour) => (
<li key={hour.time}>
{hour.time}: {hour.categoryName} {hour.comment}
<li key={hour.time} id={"hour-" + hour.time.toString()}>
<EditableHour hour={hour} />
</li>
))}
</ul>

View File

@ -8,50 +8,73 @@ import { useTimezone } from "@lifetracker/shared-react/hooks/timezones";
import LoadingSpinner from "@/components/ui/spinner";
import { db } from "@lifetracker/db";
import { Button } from "@/components/ui/button";
import spacetime from "spacetime";
export default function TimezoneDisplay() {
const dbTimezone = useTimezone();
const [timezone, setTimezone] = useState(dbTimezone);
// Update timezone state when dbTimezone changes
useEffect(() => {
if (dbTimezone !== undefined) {
setTimezone(dbTimezone);
}
}, [dbTimezone]);
useEffect(() => {
const handleTzChange = (event) => {
setTimezone(event.detail.timezone);
};
window.addEventListener('timezoneUpdated', handleTzChange);
return () => {
window.removeEventListener('timezoneUpdated', handleTzChange);
};
}, []);
if (timezone === undefined) {
return (
<div className="flex flex-col items-center w-full">
<LoadingSpinner />
</div>
);
}
const clientTime = spacetime.now();
return (
<div className="text-center w-full">
<Link href="/settings/app">
<b className="whitespace-nowrap">{format(TZDate.tz(timezone), 'MMM dd, hh:mm aa')}</b>
<b className="whitespace-nowrap">
{clientTime.format('nice')}
</b>
<br />
<span>in&nbsp;</span>
<span className="whitespace-nowrap">
<b>{timezones[timezone]}</b>
<b>
{timezones[clientTime.timezone().name] || clientTime.timezone().name}
</b>
</span>
</Link>
</div>
);
}
// export default function TimezoneDisplay() {
// const dbTimezone = useTimezone();
// const [timezone, setTimezone] = useState(dbTimezone);
// // Update timezone state when dbTimezone changes
// useEffect(() => {
// if (dbTimezone !== undefined) {
// setTimezone(dbTimezone);
// }
// }, [dbTimezone]);
// useEffect(() => {
// const handleTzChange = (event) => {
// setTimezone(event.detail.timezone);
// };
// window.addEventListener('timezoneUpdated', handleTzChange);
// return () => {
// window.removeEventListener('timezoneUpdated', handleTzChange);
// };
// }, []);
// if (timezone === undefined) {
// return (
// <div className="flex flex-col items-center w-full">
// <LoadingSpinner />
// </div>
// );
// }
// return (
// <div className="text-center w-full">
// <Link href="/settings/app">
// <b className="whitespace-nowrap">{format(TZDate.tz(timezone), 'MMM dd, hh:mm aa')}</b>
// <br />
// <span>in&nbsp;</span>
// <span className="whitespace-nowrap">
// <b>{timezones[timezone]}</b>
// </span>
// </Link>
// </div>
// );
// }

View File

@ -0,0 +1,137 @@
"use client";
import { usePathname, useRouter } from "next/navigation";
import { toast } from "@/components/ui/use-toast";
import { cn } from "@/lib/utils";
import { useUpdateHour } from "@lifetracker/shared-react/hooks/days";
import { EditableText } from "../dashboard/EditableText";
import { format } from "date-fns";
import { TZDate } from "@date-fns/tz";
import { ZHour } from "@lifetracker/shared/types/days";
import { useEffect, useRef } from "react";
function EditMode({
originalText,
hour,
onSubmit
}) {
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
if (ref.current) {
ref.current.value = originalText;
}
}, [ref]);
const submit = () => {
let newCode: string | null = ref.current?.value ?? null;
if (originalText == newCode) {
// Nothing to do here
return;
}
if (newCode == "") {
newCode = null;
}
// console.log(hour);
onSubmit({
dateQuery: hour.date,
time: hour.time,
code: newCode,
})
document.getElementById("hour-" + (hour.time + 1).toString())?.getElementsByClassName("edit-hour-code")[0].focus();
};
return (
<input
className="w-8 border-b-2 text-center edit-hour-code"
ref={ref}
value={originalText}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
submit();
}
}}
onClick={(e) => {
const range = document.createRange();
range.selectNodeContents(ref.current);
const selection = window.getSelection();
if (selection) {
selection.removeAllRanges();
selection.addRange(range);
}
}}
/>
// <input type="text"
// className="w-10 bg-inherit border-b-2 text-center"
// style={{ color: "inherit", borderColor: "inherit" }}
// onKeyDown={(e) => {
// if (e.key === "Enter") {
// e.preventDefault();
// onSave();
// }
// }}
// value={originalText.originalText ?? ""}
// />
);
}
export default function EditableHour({
hour,
className,
}: {
hour: ZHour,
className?: string;
}) {
const router = useRouter();
const currentPath = usePathname();
const { mutate: updateHour, isPending } = useUpdateHour({
onSuccess: () => {
toast({
description: "Hour updated!",
});
if (currentPath.includes("dashboard")) {
router.refresh();
}
},
});
function isActiveHour(hour: ZHour) {
const now = new TZDate();
return (hour.date == format(now, "yyyy-MM-dd")) && (((now.getHours()) + (now.getTimezoneOffset() / 60) - (parseInt(hour.time))) == 0)
}
return (
<div className={cn(
"p-4 grid justify-between",
)}
style={{
background: hour.background, color: hour.foreground, fontFamily: "inherit",
gridTemplateColumns: "100px 100px 1fr 200px"
}}
>
<span className="text-right">
{isActiveHour(hour) && "--> "}
{hour.datetime}
</span>
<div className="flex justify-center">
<EditMode
originalText={hour.categoryCode}
hour={hour}
onSubmit={updateHour}
/>
</div>
<span>
{hour.categoryDesc || " "}
</span>
<div className="text-right">
<EditableText
originalText={hour.comment || " "}
viewClassName="text-right"
/>
</div>
</div>
);
}

View File

@ -1,7 +1,7 @@
"use client";
import React, { useState, useEffect } from "react";
import { timezones } from '@/lib/timezones';
import { timezones } from "@lifetracker/shared/utils/timezones";
import TimezoneSelect, { type ITimezone } from 'react-timezone-select'
import { useUpdateUserTimezone } from "@lifetracker/shared-react/hooks/timezones";

View File

@ -77,6 +77,7 @@
"remark-gfm": "^4.0.0",
"request-ip": "^3.3.0",
"sharp": "^0.33.3",
"spacetime": "^7.6.2",
"superjson": "^2.2.1",
"tailwind-merge": "^2.2.1",
"title-case": "^4.3.2",

View File

@ -10,7 +10,7 @@ import dbConfig from "./drizzle.config";
const sqlite = new Database(dbConfig.dbCredentials.url);
export const db = drizzle(sqlite, {
schema,
logger: false
logger: false,
});
export function getInMemoryDB(runMigrations: boolean) {

View File

@ -4,6 +4,7 @@ export function useUpdateDay(
...opts: Parameters<typeof api.days.update.useMutation>
) {
const apiUtils = api.useUtils();
// console.log("UPDATING DAY");
return api.days.update.useMutation({
...opts[0],
onSuccess: (res, req, meta) => {
@ -13,30 +14,15 @@ export function useUpdateDay(
});
}
export function useDeleteLabel(
...opts: Parameters<typeof api.labels.delete.useMutation>
export function useUpdateHour(
...opts: Parameters<typeof api.hours.update.useMutation>
) {
const apiUtils = api.useUtils();
return api.labels.delete.useMutation({
// console.log(opts[0]);
return api.hours.update.useMutation({
...opts[0],
onSuccess: (res, req, meta) => {
apiUtils.labels.list.invalidate();
// apiUtils.bookmarks.getBookmark.invalidate();
return opts[0]?.onSuccess?.(res, req, meta);
},
});
}
export function useDeleteUnusedTags(
...opts: Parameters<typeof api.tags.deleteUnused.useMutation>
) {
const apiUtils = api.useUtils();
return api.tags.deleteUnused.useMutation({
...opts[0],
onSuccess: (res, req, meta) => {
apiUtils.tags.list.invalidate();
apiUtils.days.get.invalidate({ dateQuery: req.dateQuery });
return opts[0]?.onSuccess?.(res, req, meta);
},
});

View File

@ -12,6 +12,7 @@ export function useUpdateUserTimezone(
}
export function useTimezone() {
const res = api.users.getTimezone.useQuery().data;
const res = api.users.getTimezone.useQuery();
console.log("react hook useTimezone", res);
return res;
}

View File

@ -12,6 +12,8 @@ export const zHourSchema = z.object({
categoryName: z.string().nullish(),
categoryDesc: z.string().nullish(),
comment: z.string().nullish(),
background: z.string().nullish(),
foreground: z.string().nullish(),
});
export type ZHour = z.infer<typeof zHourSchema>;

View File

@ -18,6 +18,7 @@
"bcryptjs": "^2.4.3",
"date-fns": "^4.1.0",
"drizzle-orm": "^0.33.0",
"spacetime": "^7.6.2",
"superjson": "^2.2.1",
"tiny-invariant": "^1.3.3",
"title-case": "^4.3.2",

View File

@ -13,6 +13,9 @@ import { dateFromInput, hoursListInUTC } from "@lifetracker/shared/utils/days";
import { closestIndexTo, format } from "date-fns";
import { TZDate } from "@date-fns/tz";
import { hoursAppRouter } from "./hours";
import spacetime from "spacetime";
import { getHourFromTime, getTimeFromHour } from "@lifetracker/shared/utils/hours";
import { hourColors } from "./hours";
async function createDay(date: string, ctx: Context) {
return await ctx.db.transaction(async (trx) => {
@ -87,7 +90,9 @@ async function getDay(input: { dateQuery: string }, ctx: Context, date: string)
dayId: hours.dayId,
time: hours.time,
userId: hours.userId,
date: days.date
}).from(hours)
.leftJoin(days, eq(days.id, hours.dayId)) // Ensure days table is joined first
.where(and(
eq(hours.userId, ctx.user!.id),
eq(hours.dayId, day.id),
@ -132,7 +137,7 @@ export const daysAppRouter = router({
}
}));
const dayHours = await Promise.all(allHours.map(async function (map: { date, time }) {
const dayHours = await Promise.all(allHours.map(async function (map: { date, time }, i) {
const dayId = allDayIds.find((dayIds: { id, date }) => map.date == dayIds.date)!.id;
const hourMatch = await ctx.db.select({
@ -144,8 +149,10 @@ export const daysAppRouter = router({
categoryName: categories.name,
categoryDesc: categories.description,
comment: hours.comment,
date: days.date
}).from(hours)
.leftJoin(categories, eq(categories.id, hours.categoryId))
.leftJoin(days, eq(days.id, hours.dayId))
.where(and(
eq(hours.time, map.time),
eq(hours.dayId, dayId)));
@ -155,15 +162,20 @@ export const daysAppRouter = router({
// });
// console.log("Search values:: ", `allDayIds: ${allDayIds}, d: ${date}, t: ${time}, dayId: ${dayId}`)
// console.log("hourMatch", hourMatch[0]);
// console.log(hourMatch[0].categoryDesc);
const dayHour = {
...hourMatch[0],
...(await hourColors(hourMatch[0], ctx)),
};
const localDateTime = spacetime(date, timezone).add(i, "hour");
return {
...dayHour,
date: map.date,
datetime: `${localDateTime.format('{hour} {ampm}')}`,
// datetime: `${localDateTime.format('{nice}')} ${timezone} (${localDateTime.goto("UTC").format('{nice}')} UTC)`,
};
}));

View File

@ -3,7 +3,7 @@ import { and, desc, eq, inArray, notExists } from "drizzle-orm";
import { date, z } from "zod";
import { SqliteError } from "@lifetracker/db";
import { categories, days, hours, } from "@lifetracker/db/schema";
import { categories, days, hours, colors } from "@lifetracker/db/schema";
import {
zDaySchema, ZDay, ZHour, zHourSchema
} from "@lifetracker/shared/types/days";
@ -13,6 +13,32 @@ import { format } from "date-fns";
import { TZDate } from "@date-fns/tz";
import { dateFromInput } from "@lifetracker/shared/utils/days";
export async function hourColors(hour: ZHour, ctx: Context) {
const categoryColor = await ctx.db.select()
.from(colors)
.leftJoin(categories, eq(categories.id, hour.categoryId))
.where(and(
eq(colors.id, categories.colorId),
eq(colors.userId, ctx.user!.id)
))
// console.log(categoryColor);
if (!categoryColor[0]) {
return {
background: "inherit",
foreground: "inherit"
}
}
else {
return {
background: categoryColor[0].color.hexcode,
foreground: categoryColor[0].color.inverse,
}
}
}
export const hoursAppRouter = router({
get: authedProcedure
.input(z.object({
@ -30,6 +56,7 @@ export const hoursAppRouter = router({
categoryId: hours.categoryId,
categoryCode: categories.code,
categoryName: categories.name,
categoryDesc: categories.description,
comment: hours.comment,
})
.from(hours)
@ -44,6 +71,8 @@ export const hoursAppRouter = router({
update: authedProcedure
.input(
z.object({
hourId: z.string().optional(),
dayId: z.string().optional(),
dateQuery: z.string(),
time: z.number(),
code: z.string().optional(),
@ -53,7 +82,13 @@ export const hoursAppRouter = router({
.output(zHourSchema)
.mutation(async ({ input, ctx }) => {
const { dateQuery, time, code, ...updatedProps } = input;
var date = dateFromInput({ dateQuery: dateQuery });
let dateCondition;
if (input.dayId) {
dateCondition = eq(days.id, input.dayId)
}
else {
dateCondition = eq(days.date, dateFromInput({ dateQuery: dateQuery }));
}
const category =
code == "" ? [{
@ -64,6 +99,7 @@ export const hoursAppRouter = router({
{
id: categories.id,
name: categories.name,
description: categories.description
}
)
.from(categories)
@ -74,14 +110,11 @@ export const hoursAppRouter = router({
)
);
const day = await ctx.db.select(
{ id: days.id }
)
const day = await ctx.db.select()
.from(days)
.where(
and(
eq(days.date, date),
dateCondition,
eq(days.userId, ctx.user!.id),
)
);
@ -107,8 +140,9 @@ export const hoursAppRouter = router({
.returning();
return {
date: format(date, "yyyy-MM-dd"),
date: format(day[0].date, "yyyy-MM-dd"),
categoryName: category[0].name,
categoryDesc: category[0].description,
...hourRes[0]
}
}),

View File

@ -180,7 +180,6 @@ export const usersAppRouter = router({
)
.query(async ({ ctx }) => {
const res = await ctx.db.select({ timezone: users.timezone }).from(users).where(eq(users.id, ctx.user.id));
return res[0].timezone;
}),
changeTimezone: authedProcedure

6
pnpm-lock.yaml generated
View File

@ -337,6 +337,9 @@ importers:
sharp:
specifier: ^0.33.3
version: 0.33.5
spacetime:
specifier: ^7.6.2
version: 7.6.2
superjson:
specifier: ^2.2.1
version: 2.2.1
@ -616,6 +619,9 @@ importers:
drizzle-orm:
specifier: ^0.33.0
version: 0.33.0(@types/better-sqlite3@7.6.11)(@types/react@18.2.61)(better-sqlite3@11.5.0)(expo-sqlite@14.0.6(expo@51.0.36(@babel/core@7.26.0)(@babel/preset-env@7.25.7(@babel/core@7.26.0))(encoding@0.1.13)))(react@18.3.1)(sqlite3@5.1.7)
spacetime:
specifier: ^7.6.2
version: 7.6.2
superjson:
specifier: ^2.2.1
version: 2.2.1