Day hours are timezone-adjusted and taken from adjacent days as needed

This commit is contained in:
Ryan Pandya 2024-11-28 00:12:31 -05:00
parent aaf351d328
commit b2e2770e91
22 changed files with 201 additions and 114 deletions

View File

@ -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:^",

View File

@ -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, {

View File

@ -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 ?
<LoadingSpinner />
:
<DayView
day={day}
/>
<DayView
day={day}
/>
);
}

View File

@ -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 (
<div className="flex flex-col gap-3">
<div className="flex justify-between">
<span className="text-2xl">
{format(day.date, "EEEE, MMMM do")}
</span>
<div className="flex">
<Link
href={`/dashboard/day/${prevDay}`}
className={cn(
"flex-0 items-center rounded-[inherit] px-3 py-2",
)}
>
<div className="flex w-full justify-between">
<ArrowLeftSquare size={18} />
</div>
</Link>
<span className="text-2xl flex-1">
{format(day.date, "EEEE, MMMM do", { in: utc })}
</span>
<Link
href={`/dashboard/day/${nextDay}`}
className={cn(
"flex-0 items-center rounded-[inherit] px-3 py-2",
)}
>
<div className="flex w-full justify-between">
<ArrowRightSquare size={18} />
</div>
</Link>
</div>
<div>
<MoodStars
day={day}
@ -45,7 +69,13 @@ export default async function DayView({
<Separator />
<ul>
{day.hours.map((hour) => (
<li key={hour.time}>
{hour.time}: {hour.categoryName} {hour.comment}
</li>
))}
</ul>
</div>
);

View File

@ -41,7 +41,6 @@ export default function EditableDayComment({
},
{
onError: (e) => {
console.log(e);
toast({
description: e.message,
variant: "destructive",

View File

@ -15,7 +15,7 @@ export default async function Header() {
return (
<header className="sticky left-0 right-0 top-0 z-50 flex h-16 items-center justify-between overflow-x-auto overflow-y-hidden bg-background p-4 shadow">
<div className="hidden items-center sm:flex">
<Link href={"/dashboard/bookmarks"} className="w-56">
<Link href={"/dashboard/day/today"} className="w-56">
<HoarderLogo height={20} gap="8px" />
</Link>
</div>

View File

@ -35,7 +35,7 @@ export default async function Sidebar() {
{
name: "Home",
icon: <Home size={18} />,
path: "/dashboard/today",
path: "/dashboard/day/today",
},
...searchItem,
{

View File

@ -16,7 +16,7 @@ export const settingsSidebarItems: {
{
name: "Back To App",
icon: <ArrowLeft size={18} />,
path: "/dashboard/today",
path: "/dashboard/day/today",
},
{
name: "User Info",

View File

@ -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",

View File

@ -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`);

View File

@ -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": [

View File

@ -5,8 +5,8 @@
{
"idx": 0,
"version": "6",
"when": 1732706352404,
"tag": "0000_exotic_dakota_north",
"when": 1732766704666,
"tag": "0000_cold_golden_guardian",
"breakpoints": true
}
]

View File

@ -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),
},

View File

@ -3,7 +3,6 @@ import { api } from "../trpc";
export function useUpdateUserTimezone(
...opts: Parameters<typeof api.users.changeTimezone.useMutation>
) {
const apiUtils = api.useUtils();
return api.users.changeTimezone.useMutation({
...opts[0],
onSuccess: (res, req, meta) => {

View File

@ -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"

View File

@ -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<typeof zHourSchema>;

View File

@ -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";
}
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)
);
}

View File

@ -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

View File

@ -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(

View File

@ -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]

View File

@ -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;
}),
});

14
pnpm-lock.yaml generated
View File

@ -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': {}