diff --git a/apps/cli/src/commands/days.ts b/apps/cli/src/commands/days.ts index 6489fba..a44cd75 100644 --- a/apps/cli/src/commands/days.ts +++ b/apps/cli/src/commands/days.ts @@ -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); diff --git a/apps/cli/src/commands/hours.ts b/apps/cli/src/commands/hours.ts index a5fef65..2575fe0 100644 --- a/apps/cli/src/commands/hours.ts +++ b/apps/cli/src/commands/hours.ts @@ -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); diff --git a/apps/web/app/dashboard/day/[dateQuery]/page.tsx b/apps/web/app/dashboard/day/[dateQuery]/page.tsx index 7a53e2a..801c5c8 100644 --- a/apps/web/app/dashboard/day/[dateQuery]/page.tsx +++ b/apps/web/app/dashboard/day/[dateQuery]/page.tsx @@ -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 ( - + <> + + ); } diff --git a/apps/web/components/DemoModeBanner.tsx b/apps/web/components/DemoModeBanner.tsx index 8ab4dd0..d69eba2 100644 --- a/apps/web/components/DemoModeBanner.tsx +++ b/apps/web/components/DemoModeBanner.tsx @@ -1,7 +1,7 @@ export default function DemoModeBanner() { return (
- Demo mode is on. All modifications are disabled. + YO
); } diff --git a/apps/web/components/dashboard/days/DayView.tsx b/apps/web/components/dashboard/days/DayView.tsx index 295d400..7bd5b9d 100644 --- a/apps/web/components/dashboard/days/DayView.tsx +++ b/apps/web/components/dashboard/days/DayView.tsx @@ -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 (
@@ -41,7 +44,7 @@ export default async function DayView({
- {format(day.date, "EEEE, MMMM do", { in: utc })} + {spacetime(day.date).format("{day}, {month} {date}, {year}")} diff --git a/apps/web/components/dashboard/sidebar/TimezoneDisplay.tsx b/apps/web/components/dashboard/sidebar/TimezoneDisplay.tsx index 9f21fcd..6b22be3 100644 --- a/apps/web/components/dashboard/sidebar/TimezoneDisplay.tsx +++ b/apps/web/components/dashboard/sidebar/TimezoneDisplay.tsx @@ -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 ( -
- -
- ); - } + const clientTime = spacetime.now(); return (
- {format(TZDate.tz(timezone), 'MMM dd, hh:mm aa')} + + {clientTime.format('nice')} +
in  - {timezones[timezone]} + + {timezones[clientTime.timezone().name] || clientTime.timezone().name} +
); } + +// 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 ( +//
+// +//
+// ); +// } +// return ( +//
+// +// {format(TZDate.tz(timezone), 'MMM dd, hh:mm aa')} +//
+// in  +// +// {timezones[timezone]} +// +// +//
+// ); +// } diff --git a/apps/web/components/hours/EditableHour.tsx b/apps/web/components/hours/EditableHour.tsx new file mode 100644 index 0000000..1b08bb7 --- /dev/null +++ b/apps/web/components/hours/EditableHour.tsx @@ -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(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 ( + { + 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); + } + }} + /> + // { + // 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 ( +
+ + {isActiveHour(hour) && "--> "} + {hour.datetime} + +
+ +
+ + {hour.categoryDesc || " "} + +
+ +
+
+ ); +} \ No newline at end of file diff --git a/apps/web/components/settings/ChangeTimezone.tsx b/apps/web/components/settings/ChangeTimezone.tsx index 6b7f454..dfec3d3 100644 --- a/apps/web/components/settings/ChangeTimezone.tsx +++ b/apps/web/components/settings/ChangeTimezone.tsx @@ -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"; diff --git a/apps/web/package.json b/apps/web/package.json index 9662518..7548e13 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -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", diff --git a/packages/db/drizzle.ts b/packages/db/drizzle.ts index 26bcfc0..f552037 100644 --- a/packages/db/drizzle.ts +++ b/packages/db/drizzle.ts @@ -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) { diff --git a/packages/shared-react/hooks/days.ts b/packages/shared-react/hooks/days.ts index 0a10eef..a8c2ea3 100644 --- a/packages/shared-react/hooks/days.ts +++ b/packages/shared-react/hooks/days.ts @@ -4,6 +4,7 @@ export function useUpdateDay( ...opts: Parameters ) { 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 +export function useUpdateHour( + ...opts: Parameters ) { 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 -) { - 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); }, }); diff --git a/packages/shared-react/hooks/timezones.ts b/packages/shared-react/hooks/timezones.ts index c579414..f8f2ee0 100644 --- a/packages/shared-react/hooks/timezones.ts +++ b/packages/shared-react/hooks/timezones.ts @@ -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; } \ No newline at end of file diff --git a/packages/shared/types/days.ts b/packages/shared/types/days.ts index 4b637a8..3fc2251 100644 --- a/packages/shared/types/days.ts +++ b/packages/shared/types/days.ts @@ -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; diff --git a/packages/trpc/package.json b/packages/trpc/package.json index deb3062..bf905e6 100644 --- a/packages/trpc/package.json +++ b/packages/trpc/package.json @@ -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", diff --git a/packages/trpc/routers/days.ts b/packages/trpc/routers/days.ts index a545c55..c741a21 100644 --- a/packages/trpc/routers/days.ts +++ b/packages/trpc/routers/days.ts @@ -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)`, }; })); diff --git a/packages/trpc/routers/hours.ts b/packages/trpc/routers/hours.ts index 9dfdf48..75ead39 100644 --- a/packages/trpc/routers/hours.ts +++ b/packages/trpc/routers/hours.ts @@ -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] } }), diff --git a/packages/trpc/routers/users.ts b/packages/trpc/routers/users.ts index 6a07b8e..8bc3760 100644 --- a/packages/trpc/routers/users.ts +++ b/packages/trpc/routers/users.ts @@ -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 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c91408a..2b6a7ca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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