From b2e2770e9113b59ed3aeb6b5524ecad9bb2086aa Mon Sep 17 00:00:00 2001 From: Ryan Pandya Date: Thu, 28 Nov 2024 00:12:31 -0500 Subject: [PATCH] Day hours are timezone-adjusted and taken from adjacent days as needed --- apps/cli/package.json | 1 + apps/cli/src/commands/days.ts | 8 +- .../app/dashboard/day/[dateQuery]/page.tsx | 15 +- .../web/components/dashboard/days/DayView.tsx | 54 +++++-- .../dashboard/days/EditableDayComment.tsx | 1 - .../components/dashboard/header/Header.tsx | 2 +- .../components/dashboard/sidebar/Sidebar.tsx | 2 +- .../web/components/settings/sidebar/items.tsx | 2 +- apps/web/package.json | 1 + ...orth.sql => 0000_cold_golden_guardian.sql} | 1 - .../db/migrations/meta/0000_snapshot.json | 9 +- packages/db/migrations/meta/_journal.json | 4 +- packages/db/schema.ts | 3 +- packages/shared-react/hooks/timezones.ts | 1 - packages/shared/package.json | 1 + packages/shared/types/days.ts | 8 +- packages/shared/utils/days.ts | 31 +++- packages/trpc/routers/categories.ts | 4 - packages/trpc/routers/days.ts | 148 +++++++++++------- packages/trpc/routers/hours.ts | 3 - packages/trpc/routers/users.ts | 2 - pnpm-lock.yaml | 14 ++ 22 files changed, 201 insertions(+), 114 deletions(-) rename packages/db/migrations/{0000_exotic_dakota_north.sql => 0000_cold_golden_guardian.sql} (97%) diff --git a/apps/cli/package.json b/apps/cli/package.json index 4e04d9a..9ea7840 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -17,6 +17,7 @@ }, "dependencies": { "@date-fns/tz": "^1.2.0", + "@date-fns/utc": "^2.1.0", "@lifetracker/db": "workspace:*", "@lifetracker/shared": "workspace:^", "@lifetracker/trpc": "workspace:^", diff --git a/apps/cli/src/commands/days.ts b/apps/cli/src/commands/days.ts index 51b5a94..1545900 100644 --- a/apps/cli/src/commands/days.ts +++ b/apps/cli/src/commands/days.ts @@ -10,6 +10,7 @@ import { Command } from "@commander-js/extra-typings"; import { getBorderCharacters, table } from "table"; import { format } from "date-fns"; import { TZDate } from "@date-fns/tz"; +import { utc } from "@date-fns/utc"; import { getHourFromTime, getTimeFromHour } from "@lifetracker/shared/utils/hours"; function moodToStars(mood: number) { @@ -41,13 +42,14 @@ export const daysCmd = new Command() } else { const moodStr = moodToStars(day.mood); - const dateStr = format(day.date, "EEEE, MMMM do"); + const dateStr = format(day.date, "EEEE, MMMM do", { in: utc }); const data: string[][] = [[dateStr, '', moodStr], [day.comment ?? "No comment", '', ''], ["Time", "Category", "Comment"] ]; - day.hours.forEach((h) => { - data.push([getHourFromTime(h.time), h.categoryName ?? "--", h.comment ?? ""]); + day.hours.forEach((h, i) => { + data.push([getHourFromTime(h.time), + h.categoryName ?? "--", h.comment ?? ""]); }) console.log(table(data, { diff --git a/apps/web/app/dashboard/day/[dateQuery]/page.tsx b/apps/web/app/dashboard/day/[dateQuery]/page.tsx index accb234..7a53e2a 100644 --- a/apps/web/app/dashboard/day/[dateQuery]/page.tsx +++ b/apps/web/app/dashboard/day/[dateQuery]/page.tsx @@ -2,7 +2,6 @@ import { notFound } from "next/navigation"; import { api } from "@/server/api/client"; import { TRPCError } from "@trpc/server"; import DayView from "@/components/dashboard/days/DayView"; -import LoadingSpinner from "@/components/ui/spinner"; export default async function DayPage({ params, @@ -10,9 +9,10 @@ export default async function DayPage({ params: { dateQuery: string }; }) { let day; - try { - day = await api.days.get({ dateQuery: params.dateQuery }); + day = await api.days.get({ + dateQuery: params.dateQuery, + }); } catch (e) { if (e instanceof TRPCError) { if (e.code == "NOT_FOUND") { @@ -23,11 +23,8 @@ export default async function DayPage({ } return ( - params.dateQuery === undefined ? - - : - + ); } diff --git a/apps/web/components/dashboard/days/DayView.tsx b/apps/web/components/dashboard/days/DayView.tsx index 8d936ad..295d400 100644 --- a/apps/web/components/dashboard/days/DayView.tsx +++ b/apps/web/components/dashboard/days/DayView.tsx @@ -5,31 +5,55 @@ import { getServerAuthSession } from "@/server/auth"; import { ZDay } from "@lifetracker/shared/types/days"; import EditableDayComment from "./EditableDayComment"; import { MoodStars } from "./MoodStars"; -import { format } from "date-fns"; - +import { format, addDays } from "date-fns"; +import { ButtonWithTooltip } from "@/components/ui/button"; +import { router } from "next/navigation"; +import Link from "next/link"; +import { cn } from "@/lib/utils"; +import { ArrowLeftSquare, ArrowRightSquare } from "lucide-react"; +import { UTCDate, utc } from "@date-fns/utc"; export default async function DayView({ day, - header, - showDivider, - showEditorCard = false, }: { day: ZDay; - header?: React.ReactNode; - showDivider?: boolean; - showEditorCard?: boolean; }) { const session = await getServerAuthSession(); if (!session) { redirect("/"); } + const prevDay = format(addDays(day.date, -1), "yyyy-MM-dd"); + const nextDay = format(addDays(day.date, 1), "yyyy-MM-dd"); + return (
- - {format(day.date, "EEEE, MMMM do")} - +
+ +
+ +
+ + + {format(day.date, "EEEE, MMMM do", { in: utc })} + + +
+ +
+ +
- +
    + {day.hours.map((hour) => ( +
  • + {hour.time}: {hour.categoryName} {hour.comment} +
  • + ))} +
); diff --git a/apps/web/components/dashboard/days/EditableDayComment.tsx b/apps/web/components/dashboard/days/EditableDayComment.tsx index 3306261..33e2ea4 100644 --- a/apps/web/components/dashboard/days/EditableDayComment.tsx +++ b/apps/web/components/dashboard/days/EditableDayComment.tsx @@ -41,7 +41,6 @@ export default function EditableDayComment({ }, { onError: (e) => { - console.log(e); toast({ description: e.message, variant: "destructive", diff --git a/apps/web/components/dashboard/header/Header.tsx b/apps/web/components/dashboard/header/Header.tsx index ac1dcec..e254a2e 100644 --- a/apps/web/components/dashboard/header/Header.tsx +++ b/apps/web/components/dashboard/header/Header.tsx @@ -15,7 +15,7 @@ export default async function Header() { return (
- +
diff --git a/apps/web/components/dashboard/sidebar/Sidebar.tsx b/apps/web/components/dashboard/sidebar/Sidebar.tsx index 0133062..093d119 100644 --- a/apps/web/components/dashboard/sidebar/Sidebar.tsx +++ b/apps/web/components/dashboard/sidebar/Sidebar.tsx @@ -35,7 +35,7 @@ export default async function Sidebar() { { name: "Home", icon: , - path: "/dashboard/today", + path: "/dashboard/day/today", }, ...searchItem, { diff --git a/apps/web/components/settings/sidebar/items.tsx b/apps/web/components/settings/sidebar/items.tsx index d1b10e2..8335114 100644 --- a/apps/web/components/settings/sidebar/items.tsx +++ b/apps/web/components/settings/sidebar/items.tsx @@ -16,7 +16,7 @@ export const settingsSidebarItems: { { name: "Back To App", icon: , - path: "/dashboard/today", + path: "/dashboard/day/today", }, { name: "User Info", diff --git a/apps/web/package.json b/apps/web/package.json index fb26322..9662518 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -17,6 +17,7 @@ "dependencies": { "@auth/drizzle-adapter": "^1.4.2", "@date-fns/tz": "^1.2.0", + "@date-fns/utc": "^2.1.0", "@emoji-mart/data": "^1.1.2", "@emoji-mart/react": "^1.1.1", "@hookform/resolvers": "^3.3.4", diff --git a/packages/db/migrations/0000_exotic_dakota_north.sql b/packages/db/migrations/0000_cold_golden_guardian.sql similarity index 97% rename from packages/db/migrations/0000_exotic_dakota_north.sql rename to packages/db/migrations/0000_cold_golden_guardian.sql index 33821d6..9fa64ff 100644 --- a/packages/db/migrations/0000_exotic_dakota_north.sql +++ b/packages/db/migrations/0000_cold_golden_guardian.sql @@ -105,6 +105,5 @@ CREATE UNIQUE INDEX `apiKey_name_userId_unique` ON `apiKey` (`name`,`userId`);-- CREATE UNIQUE INDEX `category_userId_code_unique` ON `category` (`userId`,`code`);--> statement-breakpoint CREATE UNIQUE INDEX `color_userId_name_unique` ON `color` (`userId`,`name`);--> statement-breakpoint CREATE UNIQUE INDEX `day_date_unique` ON `day` (`date`);--> statement-breakpoint -CREATE UNIQUE INDEX `hour_time_unique` ON `hour` (`time`);--> statement-breakpoint CREATE UNIQUE INDEX `hour_dayId_time_unique` ON `hour` (`dayId`,`time`);--> statement-breakpoint CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`); \ No newline at end of file diff --git a/packages/db/migrations/meta/0000_snapshot.json b/packages/db/migrations/meta/0000_snapshot.json index 0b7fd3d..4067412 100644 --- a/packages/db/migrations/meta/0000_snapshot.json +++ b/packages/db/migrations/meta/0000_snapshot.json @@ -1,7 +1,7 @@ { "version": "6", "dialect": "sqlite", - "id": "170be9e2-c822-4d3a-a0b1-18c8468f1c5d", + "id": "ebffb4c7-5ecf-46d0-93c6-68f8e48a9fc4", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { "account": { @@ -524,13 +524,6 @@ } }, "indexes": { - "hour_time_unique": { - "name": "hour_time_unique", - "columns": [ - "time" - ], - "isUnique": true - }, "hour_dayId_time_unique": { "name": "hour_dayId_time_unique", "columns": [ diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index b23047c..288c140 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "6", - "when": 1732706352404, - "tag": "0000_exotic_dakota_north", + "when": 1732766704666, + "tag": "0000_cold_golden_guardian", "breakpoints": true } ] diff --git a/packages/db/schema.ts b/packages/db/schema.ts index dcb73b7..fc7e3a9 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -21,7 +21,6 @@ function createdAtField() { } export function calcInverseColor(hexcode: string): string { - console.log(hexcode); const hex = hexcode.replace("#", ""); const r = parseInt(hex.substr(0, 2), 16); const g = parseInt(hex.substr(2, 2), 16); @@ -148,7 +147,7 @@ export const hours = sqliteTable( .notNull() .references(() => users.id, { onDelete: "cascade" }), comment: text("comment"), - time: integer("time").unique(), + time: integer("time"), dayId: text("dayId").notNull().references(() => days.id, { onDelete: "cascade" }), categoryId: text("categoryId").references(() => categories.id), }, diff --git a/packages/shared-react/hooks/timezones.ts b/packages/shared-react/hooks/timezones.ts index 7bf9b56..c579414 100644 --- a/packages/shared-react/hooks/timezones.ts +++ b/packages/shared-react/hooks/timezones.ts @@ -3,7 +3,6 @@ import { api } from "../trpc"; export function useUpdateUserTimezone( ...opts: Parameters ) { - const apiUtils = api.useUtils(); return api.users.changeTimezone.useMutation({ ...opts[0], onSuccess: (res, req, meta) => { diff --git a/packages/shared/package.json b/packages/shared/package.json index a321c21..8052b6e 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -6,6 +6,7 @@ "type": "module", "dependencies": { "@date-fns/tz": "^1.2.0", + "@date-fns/utc": "^2.1.0", "date-fns": "^4.1.0", "winston": "^3.17.0", "zod": "^3.23.8" diff --git a/packages/shared/types/days.ts b/packages/shared/types/days.ts index 0e1b9e5..fb247d5 100644 --- a/packages/shared/types/days.ts +++ b/packages/shared/types/days.ts @@ -6,10 +6,10 @@ export const zHourSchema = z.object({ dayId: z.string(), date: z.string().optional(), time: z.number(), - categoryCode: z.coerce.number().nullable(), - categoryId: z.string().nullable(), - categoryName: z.string().nullable(), - comment: z.string().nullable(), + categoryCode: z.coerce.number().nullish(), + categoryId: z.string().nullish(), + categoryName: z.string().nullish(), + comment: z.string().nullish(), }); export type ZHour = z.infer; diff --git a/packages/shared/utils/days.ts b/packages/shared/utils/days.ts index b3a0a8c..a54d3fd 100644 --- a/packages/shared/utils/days.ts +++ b/packages/shared/utils/days.ts @@ -1,13 +1,32 @@ -import { format } from "date-fns"; +import { format, addHours } from "date-fns"; import { TZDate } from "@date-fns/tz"; +import { UTCDate, utc } from "@date-fns/utc"; export function dateFromInput(input: { dateQuery: string, timezone: string }) { - let t: TZDate; + let t: string; if (input.dateQuery == "today") { - t = TZDate.tz(input.timezone); + t = new Date(); + return format(t, "yyyy-MM-dd", { in: input.timezone }); } else { - t = new TZDate(input.dateQuery, input.timezone); + t = new UTCDate(input.dateQuery); + return format(t, "yyyy-MM-dd", { in: utc }); } - return format(t, "yyyy-MM-dd") + "T00:00:00"; -} \ No newline at end of file + +} + +function generateHour(d, t) { + const dt: TZDate = addHours(d, t); + return { + date: format(dt, 'yyyy-MM-dd'), + time: parseInt(format(dt, 'H')), + }; +} + +export function hoursListInTimezone(input: { dateQuery: string, timezone: string }) { + const dateStr = dateFromInput(input); + const d = new TZDate(dateStr, input.timezone); + return Array.from({ length: 24 }, (_, t) => + generateHour(d, t) + ); +} diff --git a/packages/trpc/routers/categories.ts b/packages/trpc/routers/categories.ts index 3bf1e32..0e95076 100644 --- a/packages/trpc/routers/categories.ts +++ b/packages/trpc/routers/categories.ts @@ -27,7 +27,6 @@ async function createCategory( ) { return ctx.db.transaction(async (trx) => { - console.log("Creating a category", input); try { const result = await trx .insert(categories) @@ -54,7 +53,6 @@ async function createCategory( }; } catch (e) { - console.log(e); if (e instanceof SqliteError) { if (e.code == "SQLITE_CONSTRAINT_UNIQUE") { throw new TRPCError({ @@ -196,8 +194,6 @@ export const categoriesAppRouter = router({ category.parentId = categoryId; } - console.log(category); - return category; }), create: authedProcedure diff --git a/packages/trpc/routers/days.ts b/packages/trpc/routers/days.ts index a9c5aca..43e8353 100644 --- a/packages/trpc/routers/days.ts +++ b/packages/trpc/routers/days.ts @@ -2,16 +2,17 @@ import { experimental_trpcMiddleware, TRPCError } from "@trpc/server"; import { and, desc, eq, inArray, notExists } from "drizzle-orm"; import { z } from "zod"; -import { SqliteError } from "@lifetracker/db"; -import { categories, days, hours, } from "@lifetracker/db/schema"; +import { db, SqliteError } from "@lifetracker/db"; +import { categories, days, hours, users } from "@lifetracker/db/schema"; import { zDaySchema, ZDay } from "@lifetracker/shared/types/days"; import type { Context } from "../index"; import { authedProcedure, router } from "../index"; -import { dateFromInput } from "@lifetracker/shared/utils/days"; -import { format } from "date-fns"; +import { dateFromInput, hoursListInTimezone } from "@lifetracker/shared/utils/days"; +import { closestIndexTo, format } from "date-fns"; import { TZDate } from "@date-fns/tz"; +import { hoursAppRouter } from "./hours"; async function createDay(date: string, ctx: Context) { return await ctx.db.transaction(async (trx) => { @@ -30,21 +31,8 @@ async function createDay(date: string, ctx: Context) { comment: days.comment, }); - const dayId = dayRes[0].id; - - // Generate 24 "hour" objects - const hoursData = Array.from({ length: 24 }, (_, hour) => ({ - userId: ctx.user!.id, - dayId: dayId, - time: hour - })); - - // Insert the "hour" objects - await trx.insert(hours).values(hoursData); - return dayRes; } catch (e) { - console.log(e); if (e instanceof SqliteError) { if (e.code == "SQLITE_CONSTRAINT_UNIQUE") { throw new TRPCError({ @@ -61,6 +49,59 @@ async function createDay(date: string, ctx: Context) { }); } +async function createHour(day, time, ctx,) { + const newHour = (await ctx.db.insert(hours).values({ + dayId: day.id, + time: time, + userId: ctx.user!.id, + }).returning()); + console.log(newHour); + return newHour[0]; +} + +async function getTimezone(ctx: Context) { + const dbTimezone = await ctx.db.select({ + timezone: users.timezone + }).from(users).where(eq(users.id, ctx.user!.id)); + + return dbTimezone[0].timezone as string; +} + +async function getDay(input: { dateQuery: string }, ctx: Context, date: string) { + const dayRes = await ctx.db.select({ + id: days.id, + date: days.date, + mood: days.mood, + comment: days.comment, + }) + .from(days) + .where(eq(days.date, date)); + + const day = dayRes.length == 0 + ? (await createDay(date, ctx))[0] + : (dayRes[0]); + + const dayHours = await Promise.all( + Array.from({ length: 24 }).map(async function (_, i) { + const existing = await ctx.db.select({ + dayId: hours.dayId, + time: hours.time, + userId: hours.userId, + }).from(hours) + .where(and( + eq(hours.userId, ctx.user!.id), + eq(hours.dayId, day.id), + eq(hours.time, i) + )) + return existing.length == 0 ? createHour(day, i, ctx) : existing[0]; + })); + + return { + hours: dayHours, + ...day + } +} + export const daysAppRouter = router({ get: authedProcedure .input(z.object({ @@ -68,47 +109,48 @@ export const daysAppRouter = router({ })) .output(zDaySchema) .query(async ({ input, ctx }) => { - const date = dateFromInput({ timezone: ctx.user.timezone, ...input }); - console.log(ctx.user.timezone); - // Fetch the day data - let dayRes; - dayRes = await ctx.db - .select({ - id: days.id, - date: days.date, - mood: days.mood, - comment: days.comment, - }) - .from(days) - .where(eq(days.date, date)); - if (dayRes.length === 0) { - dayRes = await createDay(date, ctx); - } + const timezone = await getTimezone(ctx); + const date = dateFromInput({ + dateQuery: input.dateQuery, + timezone: timezone + }); - const day = dayRes[0]; + const allHours = hoursListInTimezone({ + timezone, + ...input + }); - // Fetch the hours data for the corresponding dayId - const hoursRes = await ctx.db - .select({ - id: hours.id, - dayId: hours.dayId, - time: hours.time, - categoryId: hours.categoryId, - categoryCode: categories.code, - categoryName: categories.name, - comment: hours.comment, - }) - .from(hours) - .where(eq(hours.dayId, day.id)) - .leftJoin(categories, eq(categories.id, hours.categoryId)) + const dayRange = [...new Set(allHours.map(({ date: date, time: _time }) => date))]; - // Combine the day and hours data - const result = { - ...day, - hours: hoursRes, + const allDayIds = await Promise.all(dayRange.map(async function (date) { + const dayObj = await getDay(input, ctx, date); + return { + id: dayObj.id, + date: dayObj.date + } + })); + + const dayHours = await Promise.all(allHours.map(async function ({ date, time }) { + const dayId = allDayIds.find((_value: { id, date }) => date == date)!.id; + + + const hourMatch = await ctx.db.select().from(hours) + .where(and( + eq(hours.time, time), + eq(hours.dayId, dayId))); + + // console.log("Search values:: ", `d: ${date}, t: ${time}, dayId: ${dayId}`) + // console.log("hourMatch", hourMatch[0]); + + return hourMatch; + })); + + return { + ...await getDay(input, ctx, date), + hours: dayHours.flat(), }; - return result; + }), update: authedProcedure .input( diff --git a/packages/trpc/routers/hours.ts b/packages/trpc/routers/hours.ts index eeb4463..8991278 100644 --- a/packages/trpc/routers/hours.ts +++ b/packages/trpc/routers/hours.ts @@ -22,7 +22,6 @@ export const hoursAppRouter = router({ .output(zHourSchema) .query(async ({ input, ctx }) => { const date = dateFromInput(input); - console.log(input); const hourRes = await ctx.db .select({ id: hours.id, @@ -37,8 +36,6 @@ export const hoursAppRouter = router({ .leftJoin(days, eq(days.id, hours.dayId)) // Ensure days table is joined first .leftJoin(categories, eq(categories.id, hours.categoryId)) .where(and(eq(hours.time, input.time), eq(days.date, date))) // Use correct alias for days table - - console.log(hourRes); return { date: format(date, "yyyy-MM-dd"), ...hourRes[0] diff --git a/packages/trpc/routers/users.ts b/packages/trpc/routers/users.ts index 9082cfa..6a07b8e 100644 --- a/packages/trpc/routers/users.ts +++ b/packages/trpc/routers/users.ts @@ -198,8 +198,6 @@ export const usersAppRouter = router({ timezone: input.newTimezone, }) .where(eq(users.id, ctx.user.id)); - - console.log("changeTimezone input", input.newTimezone); return input.newTimezone; }), }); \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0aed72d..28a39da 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@date-fns/tz': specifier: ^1.2.0 version: 1.2.0 + '@date-fns/utc': + specifier: ^2.1.0 + version: 2.1.0 '@lifetracker/db': specifier: workspace:* version: link:../../packages/db @@ -148,6 +151,9 @@ importers: '@date-fns/tz': specifier: ^1.2.0 version: 1.2.0 + '@date-fns/utc': + specifier: ^2.1.0 + version: 2.1.0 '@emoji-mart/data': specifier: ^1.1.2 version: 1.2.1 @@ -478,6 +484,9 @@ importers: '@date-fns/tz': specifier: ^1.2.0 version: 1.2.0 + '@date-fns/utc': + specifier: ^2.1.0 + version: 2.1.0 date-fns: specifier: ^4.1.0 version: 4.1.0 @@ -1935,6 +1944,9 @@ packages: '@date-fns/tz@1.2.0': resolution: {integrity: sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==} + '@date-fns/utc@2.1.0': + resolution: {integrity: sha512-176grgAgU2U303rD2/vcOmNg0kGPbhzckuH1TEP2al7n0AQipZIy9P15usd2TKQCG1g+E1jX/ZVQSzs4sUDwgA==} + '@discoveryjs/json-ext@0.5.7': resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} @@ -14384,6 +14396,8 @@ snapshots: '@date-fns/tz@1.2.0': {} + '@date-fns/utc@2.1.0': {} + '@discoveryjs/json-ext@0.5.7': {} '@docsearch/css@3.6.2': {}