From aaf351d32826df5b1359dd99118d096c9f43e76a Mon Sep 17 00:00:00 2001 From: Ryan Pandya Date: Wed, 27 Nov 2024 11:31:17 -0500 Subject: [PATCH] Finally working timezones --- apps/cli/src/commands/reset.ts | 26 ------ apps/cli/src/commands/settings.ts | 36 ++++++++ apps/cli/src/index.ts | 2 + .../app/dashboard/day/[dateQuery]/page.tsx | 1 + apps/web/app/settings/app/page.tsx | 9 ++ .../web/components/dashboard/days/DayView.tsx | 12 +-- .../components/dashboard/sidebar/Sidebar.tsx | 8 +- .../dashboard/sidebar/TimezoneDisplay.tsx | 57 +++++++++++++ apps/web/components/settings/AppSettings.tsx | 26 ++++++ .../components/settings/ChangeTimezone.tsx | 31 +++++++ .../components/settings/sidebar/Sidebar.tsx | 4 +- .../web/components/settings/sidebar/items.tsx | 8 ++ apps/web/lib/timezones.ts | 82 +++++++++++++++++++ apps/web/lib/userLocalSettings/timezone.tsx | 6 ++ apps/web/package.json | 2 + ...terio.sql => 0000_exotic_dakota_north.sql} | 5 +- .../db/migrations/meta/0000_snapshot.json | 12 ++- packages/db/migrations/meta/_journal.json | 4 +- packages/db/schema.ts | 4 +- packages/shared-react/hooks/timezones.ts | 18 ++++ packages/shared/utils/days.ts | 8 +- packages/tailwind-config/globals.css | 13 ++- packages/trpc/index.ts | 1 + packages/trpc/routers/days.ts | 9 +- packages/trpc/routers/users.ts | 37 ++++++++- pnpm-lock.yaml | 31 +++++++ 26 files changed, 397 insertions(+), 55 deletions(-) delete mode 100644 apps/cli/src/commands/reset.ts create mode 100644 apps/cli/src/commands/settings.ts create mode 100644 apps/web/app/settings/app/page.tsx create mode 100644 apps/web/components/dashboard/sidebar/TimezoneDisplay.tsx create mode 100644 apps/web/components/settings/AppSettings.tsx create mode 100644 apps/web/components/settings/ChangeTimezone.tsx create mode 100644 apps/web/lib/timezones.ts create mode 100644 apps/web/lib/userLocalSettings/timezone.tsx rename packages/db/migrations/{0000_even_mysterio.sql => 0000_exotic_dakota_north.sql} (97%) create mode 100644 packages/shared-react/hooks/timezones.ts diff --git a/apps/cli/src/commands/reset.ts b/apps/cli/src/commands/reset.ts deleted file mode 100644 index dd53757..0000000 --- a/apps/cli/src/commands/reset.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { - printError, - printErrorMessageWithReason, - printObject, -} from "@/lib/output"; -import { getAPIClient } from "@/lib/trpc"; -import { Command } from "@commander-js/extra-typings"; - -export const resetCmd = new Command() - .name("reset") - .description("Initializes the database with default data") - .action(async () => { - await getAPIClient() - .users.create.mutate({ - email: "ryan@ryanpandya.com", - name: "Ryan Pandya", - password: "pleasework", - confirmPassword: "pleasework", - }) - .then(printObject) - .catch( - printError( - `Unable to create user for Ryan`, - ), - ); - }); diff --git a/apps/cli/src/commands/settings.ts b/apps/cli/src/commands/settings.ts new file mode 100644 index 0000000..3e2ca6c --- /dev/null +++ b/apps/cli/src/commands/settings.ts @@ -0,0 +1,36 @@ +import { getGlobalOptions } from "@/lib/globals"; +import { + printError, + printErrorMessageWithReason, + printObject, + printSuccess, +} from "@/lib/output"; +import { getAPIClient } from "@/lib/trpc"; +import { Command } from "@commander-js/extra-typings"; +import { getBorderCharacters, table } from "table"; + +export const settingsCmd = new Command() + .name("settings") + .description("Manipulate user settings"); + +settingsCmd + .command("timezone") + .description("get or update timezone") + .argument("[timezone]", "the timezone to set") + .action(async (timezone?) => { + const api = getAPIClient(); + + try { + if (timezone) { + const res = await api.users.changeTimezone.mutate({ newTimezone: timezone }); + console.log(`Updated to ${res}.`) + return; + } + else { + const res = await api.users.getTimezone.query(); + console.log(`Current timezone is ${res}.`); + } + } catch (error) { + printErrorMessageWithReason("Failed to do the thing", error as object); + } + }); diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 97f036d..eda0caf 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -6,6 +6,7 @@ import { colorsCmd } from "@/commands/colors"; import { categoriesCmd } from "@/commands/categories"; import { daysCmd } from "@/commands/days"; import { hoursCmd } from "./commands/hours"; +import { settingsCmd } from "./commands/settings"; import { config } from "dotenv"; @@ -42,6 +43,7 @@ program.addCommand(daysCmd); program.addCommand(colorsCmd); program.addCommand(categoriesCmd); program.addCommand(hoursCmd); +program.addCommand(settingsCmd); setGlobalOptions(program.opts()); diff --git a/apps/web/app/dashboard/day/[dateQuery]/page.tsx b/apps/web/app/dashboard/day/[dateQuery]/page.tsx index ac653af..accb234 100644 --- a/apps/web/app/dashboard/day/[dateQuery]/page.tsx +++ b/apps/web/app/dashboard/day/[dateQuery]/page.tsx @@ -10,6 +10,7 @@ export default async function DayPage({ params: { dateQuery: string }; }) { let day; + try { day = await api.days.get({ dateQuery: params.dateQuery }); } catch (e) { diff --git a/apps/web/app/settings/app/page.tsx b/apps/web/app/settings/app/page.tsx new file mode 100644 index 0000000..27f0965 --- /dev/null +++ b/apps/web/app/settings/app/page.tsx @@ -0,0 +1,9 @@ +import AppSettings from "@/components/settings/AppSettings"; + +export default async function AppSettingsPage() { + return ( +
+ +
+ ); +} diff --git a/apps/web/components/dashboard/days/DayView.tsx b/apps/web/components/dashboard/days/DayView.tsx index e975675..8d936ad 100644 --- a/apps/web/components/dashboard/days/DayView.tsx +++ b/apps/web/components/dashboard/days/DayView.tsx @@ -23,8 +23,6 @@ export default async function DayView({ redirect("/"); } - // const entries = await api.entries.get({ day: day }); - return (
@@ -32,9 +30,11 @@ export default async function DayView({ {format(day.date, "EEEE, MMMM do")} -
+
+ +
@@ -45,7 +45,7 @@ export default async function DayView({ - {day.hours} + ); diff --git a/apps/web/components/dashboard/sidebar/Sidebar.tsx b/apps/web/components/dashboard/sidebar/Sidebar.tsx index b2bdc4e..0133062 100644 --- a/apps/web/components/dashboard/sidebar/Sidebar.tsx +++ b/apps/web/components/dashboard/sidebar/Sidebar.tsx @@ -4,19 +4,19 @@ import { Separator } from "@/components/ui/separator"; import { api } from "@/server/api/client"; import { getServerAuthSession } from "@/server/auth"; import { Archive, Home, Search, Tag } from "lucide-react"; - import serverConfig from "@lifetracker/shared/config"; import AllLists from "./AllLists"; +import TimezoneDisplay from "./TimezoneDisplay"; export default async function Sidebar() { + + const session = await getServerAuthSession(); if (!session) { redirect("/"); } - const lists = await api.users.list(); - const searchItem = serverConfig.meilisearch ? [ { @@ -61,7 +61,7 @@ export default async function Sidebar() {
- Lifetracker v{serverConfig.serverVersion} +
); diff --git a/apps/web/components/dashboard/sidebar/TimezoneDisplay.tsx b/apps/web/components/dashboard/sidebar/TimezoneDisplay.tsx new file mode 100644 index 0000000..2138a31 --- /dev/null +++ b/apps/web/components/dashboard/sidebar/TimezoneDisplay.tsx @@ -0,0 +1,57 @@ +'use client'; +import { useState, useEffect, use, } from "react"; +import Link from "next/link"; +import { format } from "date-fns"; +import { TZDate } from "@date-fns/tz"; +import { timezones } from '@/lib/timezones'; +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"; + +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/settings/AppSettings.tsx b/apps/web/components/settings/AppSettings.tsx new file mode 100644 index 0000000..947b01e --- /dev/null +++ b/apps/web/components/settings/AppSettings.tsx @@ -0,0 +1,26 @@ +import { api } from "@/server/api/client"; +import ChangeTimezone from "@/components/settings/ChangeTimezone"; + +export default async function AppSettings() { + + const userTimezone: string = await api.users.getTimezone().then((res) => { + return res; + }); + + return ( +
+
+ Locale Settings +
+
+
+
Timezone
+
+ +
+
+
+
); +} diff --git a/apps/web/components/settings/ChangeTimezone.tsx b/apps/web/components/settings/ChangeTimezone.tsx new file mode 100644 index 0000000..6b7f454 --- /dev/null +++ b/apps/web/components/settings/ChangeTimezone.tsx @@ -0,0 +1,31 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { timezones } from '@/lib/timezones'; +import TimezoneSelect, { type ITimezone } from 'react-timezone-select' +import { useUpdateUserTimezone } from "@lifetracker/shared-react/hooks/timezones"; + +export default function ChangeTimezone({ userTimezone }) { + const [selectedTimezone, setSelectedTimezone] = useState(userTimezone); + const { mutate: updateUserTimezone, isPending } = useUpdateUserTimezone({ + onSuccess: () => { + toast({ + description: "User DB Timezone updated!", + }); + }, + }); + return ( + + { + setSelectedTimezone(tz); + updateUserTimezone({ newTimezone: tz.value }); + window.dispatchEvent(new CustomEvent('timezoneUpdated', { detail: { timezone: tz.value } })); + }} + timezones={{ + ...timezones + }} + /> + ); +} \ No newline at end of file diff --git a/apps/web/components/settings/sidebar/Sidebar.tsx b/apps/web/components/settings/sidebar/Sidebar.tsx index 10150e2..16ed895 100644 --- a/apps/web/components/settings/sidebar/Sidebar.tsx +++ b/apps/web/components/settings/sidebar/Sidebar.tsx @@ -5,6 +5,7 @@ import { getServerAuthSession } from "@/server/auth"; import serverConfig from "@lifetracker/shared/config"; import { settingsSidebarItems } from "./items"; +import TimezoneDisplay from "@/components/dashboard/sidebar/TimezoneDisplay"; export default async function Sidebar() { const session = await getServerAuthSession(); @@ -27,7 +28,8 @@ export default async function Sidebar() {
- Hoarder v{serverConfig.serverVersion} + +
); diff --git a/apps/web/components/settings/sidebar/items.tsx b/apps/web/components/settings/sidebar/items.tsx index 262775a..d1b10e2 100644 --- a/apps/web/components/settings/sidebar/items.tsx +++ b/apps/web/components/settings/sidebar/items.tsx @@ -5,6 +5,7 @@ import { KeyRound, User, Palette, + Settings } from "lucide-react"; export const settingsSidebarItems: { @@ -22,6 +23,13 @@ export const settingsSidebarItems: { icon: , path: "/settings/info", }, + + { + name: "App Configuration", + icon: , + path: "/settings/app", + }, + { name: "Color Settings", icon: , diff --git a/apps/web/lib/timezones.ts b/apps/web/lib/timezones.ts new file mode 100644 index 0000000..b26073f --- /dev/null +++ b/apps/web/lib/timezones.ts @@ -0,0 +1,82 @@ +export const timezones = { + // "Pacific/Midway": "Midway Island, Samoa", + "Pacific/Honolulu": "Hawaii", + // "America/Juneau": "Alaska", + "America/Boise": "Mountain Time", + // "America/Dawson": "Dawson, Yukon", + // "America/Chihuahua": "Chihuahua, La Paz, Mazatlan", + // "America/Phoenix": "Arizona", + "America/Chicago": "Central Time", + // "America/Regina": "Saskatchewan", + "America/Mexico_City": "Mexico", + // "America/Belize": "Central America", + "America/New_York": "Eastern Time", + // "America/Bogota": "Bogota, Lima, Quito", + // "America/Caracas": "Caracas, La Paz", + // "America/Santiago": "Santiago", + // "America/St_Johns": "Newfoundland and Labrador", + // "America/Sao_Paulo": "Brasilia", + // "America/Tijuana": "Mexico", + // "America/Montevideo": "Montevideo", + "America/Argentina/Buenos_Aires": "Argentina", + // "America/Godthab": "Greenland", + "America/Los_Angeles": "Pacific Time", + // "Atlantic/Azores": "Azores", + // "Atlantic/Cape_Verde": "Cape Verde Islands", + // GMT: "UTC", + "Europe/London": "The UK", + "Europe/Dublin": "Ireland", + // "Europe/Lisbon": "Lisbon", + // "Africa/Casablanca": "Casablanca, Monrovia", + // "Atlantic/Canary": "Canary Islands", + // "Europe/Belgrade": "Belgrade, Bratislava, Budapest, Ljubljana, Prague", + // "Europe/Sarajevo": "Sarajevo, Skopje, Warsaw, Zagreb", + // "Europe/Brussels": "Brussels, Copenhagen, Madrid, Paris", + "Europe/Amsterdam": "Western Europe", + // "Africa/Algiers": "West Central Africa", + // "Europe/Bucharest": "Bucharest", + // "Africa/Cairo": "Cairo", + "Europe/Helsinki": "Eastern Europe", + // "Europe/Athens": "Athens", + // "Asia/Jerusalem": "Jerusalem", + // "Africa/Harare": "Harare, Pretoria", + // "Europe/Moscow": "Istanbul, Minsk, Moscow, St. Petersburg, Volgograd", + // "Asia/Kuwait": "Kuwait, Riyadh", + "Africa/Nairobi": "Kenya", + "Africa/Johannesburg": "South Africa", + // "Asia/Baghdad": "Baghdad", + // "Asia/Tehran": "Tehran", + // "Asia/Dubai": "Abu Dhabi, Muscat", + // "Asia/Baku": "Baku, Tbilisi, Yerevan", + // "Asia/Kabul": "Kabul", + // "Asia/Yekaterinburg": "Ekaterinburg", + // "Asia/Karachi": "Islamabad, Karachi, Tashkent", + "Asia/Kolkata": "India", + // "Asia/Kathmandu": "Kathmandu", + // "Asia/Dhaka": "Astana, Dhaka", + // "Asia/Colombo": "Sri Jayawardenepura", + // "Asia/Almaty": "Almaty, Novosibirsk", + // "Asia/Rangoon": "Yangon Rangoon", + // "Asia/Bangkok": "Bangkok, Hanoi, Jakarta", + // "Asia/Krasnoyarsk": "Krasnoyarsk", + // "Asia/Shanghai": "Beijing, Chongqing, Hong Kong SAR, Urumqi", + // "Asia/Kuala_Lumpur": "Kuala Lumpur, Singapore", + // "Asia/Taipei": "Taipei", + // "Australia/Perth": "Perth", + // "Asia/Irkutsk": "Irkutsk, Ulaanbaatar", + // "Asia/Seoul": "Seoul", + "Asia/Tokyo": "Japan", + // "Asia/Yakutsk": "Yakutsk", + // "Australia/Darwin": "Darwin", + // "Australia/Adelaide": "Adelaide", + // "Australia/Sydney": "Canberra, Melbourne, Sydney", + // "Australia/Brisbane": "Brisbane", + // "Australia/Hobart": "Hobart", + // "Asia/Vladivostok": "Vladivostok", + // "Pacific/Guam": "Guam, Port Moresby", + // "Asia/Magadan": "Magadan, Solomon Islands, New Caledonia", + // "Asia/Kamchatka": "Kamchatka, Marshall Islands", + // "Pacific/Fiji": "Fiji Islands", + // "Pacific/Auckland": "Auckland, Wellington", + // "Pacific/Tongatapu": "Nuku'alofa", +}; \ No newline at end of file diff --git a/apps/web/lib/userLocalSettings/timezone.tsx b/apps/web/lib/userLocalSettings/timezone.tsx new file mode 100644 index 0000000..be8f84f --- /dev/null +++ b/apps/web/lib/userLocalSettings/timezone.tsx @@ -0,0 +1,6 @@ +"use client"; + +import type { z } from "zod"; +import { createContext, useContext } from "react"; + +export const TimezoneCtx = createContext(undefined); diff --git a/apps/web/package.json b/apps/web/package.json index 8aa5280..fb26322 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -16,6 +16,7 @@ }, "dependencies": { "@auth/drizzle-adapter": "^1.4.2", + "@date-fns/tz": "^1.2.0", "@emoji-mart/data": "^1.1.2", "@emoji-mart/react": "^1.1.1", "@hookform/resolvers": "^3.3.4", @@ -70,6 +71,7 @@ "react-masonry-css": "^1.0.16", "react-select": "^5.8.0", "react-syntax-highlighter": "^15.5.0", + "react-timezone-select": "^3.2.8", "remark-breaks": "^4.0.0", "remark-gfm": "^4.0.0", "request-ip": "^3.3.0", diff --git a/packages/db/migrations/0000_even_mysterio.sql b/packages/db/migrations/0000_exotic_dakota_north.sql similarity index 97% rename from packages/db/migrations/0000_even_mysterio.sql rename to packages/db/migrations/0000_exotic_dakota_north.sql index bdb439d..33821d6 100644 --- a/packages/db/migrations/0000_even_mysterio.sql +++ b/packages/db/migrations/0000_exotic_dakota_north.sql @@ -71,7 +71,7 @@ CREATE TABLE `hour` ( `dayId` text NOT NULL, `categoryId` text, FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade, - FOREIGN KEY (`dayId`) REFERENCES `day`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`dayId`) REFERENCES `day`(`id`) ON UPDATE no action ON DELETE cascade, FOREIGN KEY (`categoryId`) REFERENCES `category`(`id`) ON UPDATE no action ON DELETE no action ); --> statement-breakpoint @@ -89,7 +89,8 @@ CREATE TABLE `user` ( `emailVerified` integer, `image` text, `password` text, - `role` text DEFAULT 'user' + `role` text DEFAULT 'user', + `timezone` text DEFAULT 'America/Los_Angeles' NOT NULL ); --> statement-breakpoint CREATE TABLE `verificationToken` ( diff --git a/packages/db/migrations/meta/0000_snapshot.json b/packages/db/migrations/meta/0000_snapshot.json index 350f9a4..0b7fd3d 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": "ac3ad6ee-ccd2-4f91-9be7-83bd5b1d9ec8", + "id": "170be9e2-c822-4d3a-a0b1-18c8468f1c5d", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { "account": { @@ -564,7 +564,7 @@ "columnsTo": [ "id" ], - "onDelete": "no action", + "onDelete": "cascade", "onUpdate": "no action" }, "hour_categoryId_category_id_fk": { @@ -680,6 +680,14 @@ "notNull": false, "autoincrement": false, "default": "'user'" + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'America/Los_Angeles'" } }, "indexes": { diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index e09637e..b23047c 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "6", - "when": 1732648521848, - "tag": "0000_even_mysterio", + "when": 1732706352404, + "tag": "0000_exotic_dakota_north", "breakpoints": true } ] diff --git a/packages/db/schema.ts b/packages/db/schema.ts index c775588..dcb73b7 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -73,6 +73,7 @@ export const users = sqliteTable("user", { image: text("image"), password: text("password"), role: text("role", { enum: ["admin", "user"] }).default("user"), + timezone: text("timezone").notNull().default("America/Los_Angeles"), }); export const accounts = sqliteTable( @@ -133,7 +134,6 @@ export const days = sqliteTable("day", { userId: text("userId") .notNull() .references(() => users.id, { onDelete: "cascade" }), - }); export const hours = sqliteTable( @@ -149,7 +149,7 @@ export const hours = sqliteTable( .references(() => users.id, { onDelete: "cascade" }), comment: text("comment"), time: integer("time").unique(), - dayId: text("dayId").notNull().references(() => days.id), + dayId: text("dayId").notNull().references(() => days.id, { onDelete: "cascade" }), categoryId: text("categoryId").references(() => categories.id), }, (e) => ({ diff --git a/packages/shared-react/hooks/timezones.ts b/packages/shared-react/hooks/timezones.ts new file mode 100644 index 0000000..7bf9b56 --- /dev/null +++ b/packages/shared-react/hooks/timezones.ts @@ -0,0 +1,18 @@ +import { api } from "../trpc"; + +export function useUpdateUserTimezone( + ...opts: Parameters +) { + const apiUtils = api.useUtils(); + return api.users.changeTimezone.useMutation({ + ...opts[0], + onSuccess: (res, req, meta) => { + return opts[0]?.onSuccess?.(res, req, meta); + }, + }); +} + +export function useTimezone() { + const res = api.users.getTimezone.useQuery().data; + return res; +} \ No newline at end of file diff --git a/packages/shared/utils/days.ts b/packages/shared/utils/days.ts index 0ce3c1f..b3a0a8c 100644 --- a/packages/shared/utils/days.ts +++ b/packages/shared/utils/days.ts @@ -1,13 +1,13 @@ import { format } from "date-fns"; import { TZDate } from "@date-fns/tz"; -export function dateFromInput(input: { dateQuery: string }) { - let t: string; +export function dateFromInput(input: { dateQuery: string, timezone: string }) { + let t: TZDate; if (input.dateQuery == "today") { - t = TZDate.tz("America/Los_Angeles"); + t = TZDate.tz(input.timezone); } else { - t = new TZDate(input.dateQuery, "Etc/UTC"); + t = new TZDate(input.dateQuery, input.timezone); } return format(t, "yyyy-MM-dd") + "T00:00:00"; } \ No newline at end of file diff --git a/packages/tailwind-config/globals.css b/packages/tailwind-config/globals.css index 1ba64bc..3b3664e 100644 --- a/packages/tailwind-config/globals.css +++ b/packages/tailwind-config/globals.css @@ -70,7 +70,18 @@ * { @apply border-border; } + body { @apply bg-background text-foreground; + + } -} + + nextjs-portal { + display: none; + } + + .select-wrapper { + color: black !important; + } +} \ No newline at end of file diff --git a/packages/trpc/index.ts b/packages/trpc/index.ts index a19c922..35ae76f 100644 --- a/packages/trpc/index.ts +++ b/packages/trpc/index.ts @@ -10,6 +10,7 @@ interface User { name?: string | null | undefined; email?: string | null | undefined; role: "admin" | "user" | null; + timezone: string; } export interface Context { diff --git a/packages/trpc/routers/days.ts b/packages/trpc/routers/days.ts index 4c10a5a..a9c5aca 100644 --- a/packages/trpc/routers/days.ts +++ b/packages/trpc/routers/days.ts @@ -10,6 +10,8 @@ import { import type { Context } from "../index"; import { authedProcedure, router } from "../index"; import { dateFromInput } from "@lifetracker/shared/utils/days"; +import { format } from "date-fns"; +import { TZDate } from "@date-fns/tz"; async function createDay(date: string, ctx: Context) { return await ctx.db.transaction(async (trx) => { @@ -66,8 +68,8 @@ export const daysAppRouter = router({ })) .output(zDaySchema) .query(async ({ input, ctx }) => { - const date = dateFromInput(input); - + const date = dateFromInput({ timezone: ctx.user.timezone, ...input }); + console.log(ctx.user.timezone); // Fetch the day data let dayRes; dayRes = await ctx.db @@ -123,10 +125,9 @@ export const daysAppRouter = router({ if (updatedProps.mood) { updatedProps.mood = parseInt(updatedProps.mood); } - console.log(dateQuery, "::", dateFromInput({ dateQuery: dateQuery })); await ctx.db .update(days) .set(updatedProps) - .where(eq(days.date, dateFromInput({ dateQuery: dateQuery }))); + .where(eq(days.date, dateFromInput({ dateQuery: dateQuery, timezone: ctx.user.timezone }))); }), }); diff --git a/packages/trpc/routers/users.ts b/packages/trpc/routers/users.ts index 55d2a81..9082cfa 100644 --- a/packages/trpc/routers/users.ts +++ b/packages/trpc/routers/users.ts @@ -147,12 +147,14 @@ export const usersAppRouter = router({ }) .where(eq(users.id, ctx.user.id)); }), + whoami: authedProcedure .output( z.object({ id: z.string(), name: z.string().nullish(), email: z.string().nullish(), + timezone: z.string().nullish(), }), ) .query(async ({ ctx }) => { @@ -165,6 +167,39 @@ export const usersAppRouter = router({ if (!userDb) { throw new TRPCError({ code: "UNAUTHORIZED" }); } - return { id: ctx.user.id, name: ctx.user.name, email: ctx.user.email }; + return { + id: ctx.user.id, + name: ctx.user.name, + email: ctx.user.email, + timezone: ctx.user.timezone, + }; + }), + getTimezone: authedProcedure + .output( + z.string(), + ) + .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 + .input( + z.object({ + newTimezone: z.string(), + }), + ) + .output(z.string()) + .mutation(async ({ input, ctx }) => { + // invariant(ctx.user.timezone, "A user always has a timezone specified"); + await ctx.db + .update(users) + .set({ + 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 bf6ddd7..0aed72d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -145,6 +145,9 @@ importers: '@auth/drizzle-adapter': specifier: ^1.4.2 version: 1.7.3 + '@date-fns/tz': + specifier: ^1.2.0 + version: 1.2.0 '@emoji-mart/data': specifier: ^1.1.2 version: 1.2.1 @@ -307,6 +310,9 @@ importers: react-syntax-highlighter: specifier: ^15.5.0 version: 15.6.1(react@18.3.1) + react-timezone-select: + specifier: ^3.2.8 + version: 3.2.8(react-dom@18.3.1(react@18.3.1))(react-select@5.8.3(@types/react@18.2.61)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) remark-breaks: specifier: ^4.0.0 version: 4.0.0 @@ -10092,6 +10098,13 @@ packages: peerDependencies: react: '>= 0.14.0' + react-timezone-select@3.2.8: + resolution: {integrity: sha512-efEIVmYAHtm+oS+YlE/9DbieMka1Lop0v1LsW1TdLq0yCBnnAzROKDUY09CICY8TCijZlo0fk+wHZZkV5NpVNw==} + peerDependencies: + react: ^16 || ^17.0.1 || ^18 || ^19.0.0-0 + react-dom: ^16 || ^17.0.1 || ^18 || ^19.0.0-0 + react-select: ^5.8.0 + react-transition-group@4.4.5: resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} peerDependencies: @@ -10708,6 +10721,9 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + spacetime@7.6.2: + resolution: {integrity: sha512-x5Qr2yiG5wy/0IBeaNeYG9OLigePKTj1/liF9j5xWNbb1psi5Q2AuZTh9SpIT+QZqpKCWX0OCdvwJIm9FlJouA==} + spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} @@ -11083,6 +11099,9 @@ packages: thunky@1.1.0: resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==} + timezone-soft@1.5.2: + resolution: {integrity: sha512-BUr+CfBfeWXJwFAuEzPO9uF+v6sy3pL5SKLkDg4vdEhsyXgbBnpFoBCW8oEKSNTqNq9YHbVOjNb31xE7WyGmrA==} + tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -24844,6 +24863,14 @@ snapshots: react: 18.3.1 refractor: 3.6.0 + react-timezone-select@3.2.8(react-dom@18.3.1(react@18.3.1))(react-select@5.8.3(@types/react@18.2.61)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-select: 5.8.3(@types/react@18.2.61)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + spacetime: 7.6.2 + timezone-soft: 1.5.2 + react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.26.0 @@ -25645,6 +25672,8 @@ snapshots: space-separated-tokens@2.0.2: {} + spacetime@7.6.2: {} + spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1 @@ -26099,6 +26128,8 @@ snapshots: thunky@1.1.0: {} + timezone-soft@1.5.2: {} + tiny-invariant@1.3.3: {} tiny-queue@0.2.1: {}