Finally working timezones

This commit is contained in:
Ryan Pandya 2024-11-27 11:31:17 -05:00
parent c925ae8811
commit aaf351d328
26 changed files with 397 additions and 55 deletions

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import { colorsCmd } from "@/commands/colors";
import { categoriesCmd } from "@/commands/categories"; import { categoriesCmd } from "@/commands/categories";
import { daysCmd } from "@/commands/days"; import { daysCmd } from "@/commands/days";
import { hoursCmd } from "./commands/hours"; import { hoursCmd } from "./commands/hours";
import { settingsCmd } from "./commands/settings";
import { config } from "dotenv"; import { config } from "dotenv";
@ -42,6 +43,7 @@ program.addCommand(daysCmd);
program.addCommand(colorsCmd); program.addCommand(colorsCmd);
program.addCommand(categoriesCmd); program.addCommand(categoriesCmd);
program.addCommand(hoursCmd); program.addCommand(hoursCmd);
program.addCommand(settingsCmd);
setGlobalOptions(program.opts()); setGlobalOptions(program.opts());

View File

@ -10,6 +10,7 @@ export default async function DayPage({
params: { dateQuery: string }; params: { dateQuery: string };
}) { }) {
let day; let day;
try { try {
day = await api.days.get({ dateQuery: params.dateQuery }); day = await api.days.get({ dateQuery: params.dateQuery });
} catch (e) { } catch (e) {

View File

@ -0,0 +1,9 @@
import AppSettings from "@/components/settings/AppSettings";
export default async function AppSettingsPage() {
return (
<div className="rounded-md border bg-background p-4">
<AppSettings />
</div>
);
}

View File

@ -23,8 +23,6 @@ export default async function DayView({
redirect("/"); redirect("/");
} }
// const entries = await api.entries.get({ day: day });
return ( return (
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
@ -32,9 +30,11 @@ export default async function DayView({
<span className="text-2xl"> <span className="text-2xl">
{format(day.date, "EEEE, MMMM do")} {format(day.date, "EEEE, MMMM do")}
</span> </span>
<div><MoodStars <div>
day={day} <MoodStars
/></div> day={day}
/>
</div>
</div> </div>
<Separator /> <Separator />
@ -45,7 +45,7 @@ export default async function DayView({
<Separator /> <Separator />
{day.hours}
</div> </div>
); );

View File

@ -4,19 +4,19 @@ import { Separator } from "@/components/ui/separator";
import { api } from "@/server/api/client"; import { api } from "@/server/api/client";
import { getServerAuthSession } from "@/server/auth"; import { getServerAuthSession } from "@/server/auth";
import { Archive, Home, Search, Tag } from "lucide-react"; import { Archive, Home, Search, Tag } from "lucide-react";
import serverConfig from "@lifetracker/shared/config"; import serverConfig from "@lifetracker/shared/config";
import AllLists from "./AllLists"; import AllLists from "./AllLists";
import TimezoneDisplay from "./TimezoneDisplay";
export default async function Sidebar() { export default async function Sidebar() {
const session = await getServerAuthSession(); const session = await getServerAuthSession();
if (!session) { if (!session) {
redirect("/"); redirect("/");
} }
const lists = await api.users.list();
const searchItem = serverConfig.meilisearch const searchItem = serverConfig.meilisearch
? [ ? [
{ {
@ -61,7 +61,7 @@ export default async function Sidebar() {
</div> </div>
<Separator /> <Separator />
<div className="mt-auto flex items-center border-t pt-2 text-sm text-gray-400"> <div className="mt-auto flex items-center border-t pt-2 text-sm text-gray-400">
Lifetracker v{serverConfig.serverVersion} <TimezoneDisplay />
</div> </div>
</aside> </aside>
); );

View File

@ -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 (
<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,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 (
<div className="mb-8 flex w-full flex-col sm:flex-row">
<div className="mb-4 w-full text-lg font-medium sm:w-1/3">
Locale Settings
</div>
<div className="w-full">
<div className="mb-2" >
<div className="mb-2 text-sm font-medium">Timezone</div>
<div className="select-wrapper">
<ChangeTimezone
userTimezone={userTimezone}
/>
</div>
</div>
</div>
</div>);
}

View File

@ -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<ITimezone>(userTimezone);
const { mutate: updateUserTimezone, isPending } = useUpdateUserTimezone({
onSuccess: () => {
toast({
description: "User DB Timezone updated!",
});
},
});
return (
<TimezoneSelect
value={selectedTimezone}
onChange={(tz) => {
setSelectedTimezone(tz);
updateUserTimezone({ newTimezone: tz.value });
window.dispatchEvent(new CustomEvent('timezoneUpdated', { detail: { timezone: tz.value } }));
}}
timezones={{
...timezones
}}
/>
);
}

View File

@ -5,6 +5,7 @@ import { getServerAuthSession } from "@/server/auth";
import serverConfig from "@lifetracker/shared/config"; import serverConfig from "@lifetracker/shared/config";
import { settingsSidebarItems } from "./items"; import { settingsSidebarItems } from "./items";
import TimezoneDisplay from "@/components/dashboard/sidebar/TimezoneDisplay";
export default async function Sidebar() { export default async function Sidebar() {
const session = await getServerAuthSession(); const session = await getServerAuthSession();
@ -27,7 +28,8 @@ export default async function Sidebar() {
</ul> </ul>
</div> </div>
<div className="mt-auto flex items-center border-t pt-2 text-sm text-gray-400"> <div className="mt-auto flex items-center border-t pt-2 text-sm text-gray-400">
Hoarder v{serverConfig.serverVersion} <TimezoneDisplay />
</div> </div>
</aside> </aside>
); );

View File

@ -5,6 +5,7 @@ import {
KeyRound, KeyRound,
User, User,
Palette, Palette,
Settings
} from "lucide-react"; } from "lucide-react";
export const settingsSidebarItems: { export const settingsSidebarItems: {
@ -22,6 +23,13 @@ export const settingsSidebarItems: {
icon: <User size={18} />, icon: <User size={18} />,
path: "/settings/info", path: "/settings/info",
}, },
{
name: "App Configuration",
icon: <Settings size={18} />,
path: "/settings/app",
},
{ {
name: "Color Settings", name: "Color Settings",
icon: <Palette size={18} />, icon: <Palette size={18} />,

82
apps/web/lib/timezones.ts Normal file
View File

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

View File

@ -0,0 +1,6 @@
"use client";
import type { z } from "zod";
import { createContext, useContext } from "react";
export const TimezoneCtx = createContext(undefined);

View File

@ -16,6 +16,7 @@
}, },
"dependencies": { "dependencies": {
"@auth/drizzle-adapter": "^1.4.2", "@auth/drizzle-adapter": "^1.4.2",
"@date-fns/tz": "^1.2.0",
"@emoji-mart/data": "^1.1.2", "@emoji-mart/data": "^1.1.2",
"@emoji-mart/react": "^1.1.1", "@emoji-mart/react": "^1.1.1",
"@hookform/resolvers": "^3.3.4", "@hookform/resolvers": "^3.3.4",
@ -70,6 +71,7 @@
"react-masonry-css": "^1.0.16", "react-masonry-css": "^1.0.16",
"react-select": "^5.8.0", "react-select": "^5.8.0",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"react-timezone-select": "^3.2.8",
"remark-breaks": "^4.0.0", "remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
"request-ip": "^3.3.0", "request-ip": "^3.3.0",

View File

@ -71,7 +71,7 @@ CREATE TABLE `hour` (
`dayId` text NOT NULL, `dayId` text NOT NULL,
`categoryId` text, `categoryId` text,
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade, 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 FOREIGN KEY (`categoryId`) REFERENCES `category`(`id`) ON UPDATE no action ON DELETE no action
); );
--> statement-breakpoint --> statement-breakpoint
@ -89,7 +89,8 @@ CREATE TABLE `user` (
`emailVerified` integer, `emailVerified` integer,
`image` text, `image` text,
`password` text, `password` text,
`role` text DEFAULT 'user' `role` text DEFAULT 'user',
`timezone` text DEFAULT 'America/Los_Angeles' NOT NULL
); );
--> statement-breakpoint --> statement-breakpoint
CREATE TABLE `verificationToken` ( CREATE TABLE `verificationToken` (

View File

@ -1,7 +1,7 @@
{ {
"version": "6", "version": "6",
"dialect": "sqlite", "dialect": "sqlite",
"id": "ac3ad6ee-ccd2-4f91-9be7-83bd5b1d9ec8", "id": "170be9e2-c822-4d3a-a0b1-18c8468f1c5d",
"prevId": "00000000-0000-0000-0000-000000000000", "prevId": "00000000-0000-0000-0000-000000000000",
"tables": { "tables": {
"account": { "account": {
@ -564,7 +564,7 @@
"columnsTo": [ "columnsTo": [
"id" "id"
], ],
"onDelete": "no action", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
}, },
"hour_categoryId_category_id_fk": { "hour_categoryId_category_id_fk": {
@ -680,6 +680,14 @@
"notNull": false, "notNull": false,
"autoincrement": false, "autoincrement": false,
"default": "'user'" "default": "'user'"
},
"timezone": {
"name": "timezone",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'America/Los_Angeles'"
} }
}, },
"indexes": { "indexes": {

View File

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

View File

@ -73,6 +73,7 @@ export const users = sqliteTable("user", {
image: text("image"), image: text("image"),
password: text("password"), password: text("password"),
role: text("role", { enum: ["admin", "user"] }).default("user"), role: text("role", { enum: ["admin", "user"] }).default("user"),
timezone: text("timezone").notNull().default("America/Los_Angeles"),
}); });
export const accounts = sqliteTable( export const accounts = sqliteTable(
@ -133,7 +134,6 @@ export const days = sqliteTable("day", {
userId: text("userId") userId: text("userId")
.notNull() .notNull()
.references(() => users.id, { onDelete: "cascade" }), .references(() => users.id, { onDelete: "cascade" }),
}); });
export const hours = sqliteTable( export const hours = sqliteTable(
@ -149,7 +149,7 @@ export const hours = sqliteTable(
.references(() => users.id, { onDelete: "cascade" }), .references(() => users.id, { onDelete: "cascade" }),
comment: text("comment"), comment: text("comment"),
time: integer("time").unique(), 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), categoryId: text("categoryId").references(() => categories.id),
}, },
(e) => ({ (e) => ({

View File

@ -0,0 +1,18 @@
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) => {
return opts[0]?.onSuccess?.(res, req, meta);
},
});
}
export function useTimezone() {
const res = api.users.getTimezone.useQuery().data;
return res;
}

View File

@ -1,13 +1,13 @@
import { format } from "date-fns"; import { format } from "date-fns";
import { TZDate } from "@date-fns/tz"; import { TZDate } from "@date-fns/tz";
export function dateFromInput(input: { dateQuery: string }) { export function dateFromInput(input: { dateQuery: string, timezone: string }) {
let t: string; let t: TZDate;
if (input.dateQuery == "today") { if (input.dateQuery == "today") {
t = TZDate.tz("America/Los_Angeles"); t = TZDate.tz(input.timezone);
} }
else { else {
t = new TZDate(input.dateQuery, "Etc/UTC"); t = new TZDate(input.dateQuery, input.timezone);
} }
return format(t, "yyyy-MM-dd") + "T00:00:00"; return format(t, "yyyy-MM-dd") + "T00:00:00";
} }

View File

@ -70,7 +70,18 @@
* { * {
@apply border-border; @apply border-border;
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
}
nextjs-portal {
display: none;
}
.select-wrapper {
color: black !important;
}
}

View File

@ -10,6 +10,7 @@ interface User {
name?: string | null | undefined; name?: string | null | undefined;
email?: string | null | undefined; email?: string | null | undefined;
role: "admin" | "user" | null; role: "admin" | "user" | null;
timezone: string;
} }
export interface Context { export interface Context {

View File

@ -10,6 +10,8 @@ import {
import type { Context } from "../index"; import type { Context } from "../index";
import { authedProcedure, router } from "../index"; import { authedProcedure, router } from "../index";
import { dateFromInput } from "@lifetracker/shared/utils/days"; 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) { async function createDay(date: string, ctx: Context) {
return await ctx.db.transaction(async (trx) => { return await ctx.db.transaction(async (trx) => {
@ -66,8 +68,8 @@ export const daysAppRouter = router({
})) }))
.output(zDaySchema) .output(zDaySchema)
.query(async ({ input, ctx }) => { .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 // Fetch the day data
let dayRes; let dayRes;
dayRes = await ctx.db dayRes = await ctx.db
@ -123,10 +125,9 @@ export const daysAppRouter = router({
if (updatedProps.mood) { if (updatedProps.mood) {
updatedProps.mood = parseInt(updatedProps.mood); updatedProps.mood = parseInt(updatedProps.mood);
} }
console.log(dateQuery, "::", dateFromInput({ dateQuery: dateQuery }));
await ctx.db await ctx.db
.update(days) .update(days)
.set(updatedProps) .set(updatedProps)
.where(eq(days.date, dateFromInput({ dateQuery: dateQuery }))); .where(eq(days.date, dateFromInput({ dateQuery: dateQuery, timezone: ctx.user.timezone })));
}), }),
}); });

View File

@ -147,12 +147,14 @@ export const usersAppRouter = router({
}) })
.where(eq(users.id, ctx.user.id)); .where(eq(users.id, ctx.user.id));
}), }),
whoami: authedProcedure whoami: authedProcedure
.output( .output(
z.object({ z.object({
id: z.string(), id: z.string(),
name: z.string().nullish(), name: z.string().nullish(),
email: z.string().nullish(), email: z.string().nullish(),
timezone: z.string().nullish(),
}), }),
) )
.query(async ({ ctx }) => { .query(async ({ ctx }) => {
@ -165,6 +167,39 @@ export const usersAppRouter = router({
if (!userDb) { if (!userDb) {
throw new TRPCError({ code: "UNAUTHORIZED" }); 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;
}), }),
}); });

31
pnpm-lock.yaml generated
View File

@ -145,6 +145,9 @@ importers:
'@auth/drizzle-adapter': '@auth/drizzle-adapter':
specifier: ^1.4.2 specifier: ^1.4.2
version: 1.7.3 version: 1.7.3
'@date-fns/tz':
specifier: ^1.2.0
version: 1.2.0
'@emoji-mart/data': '@emoji-mart/data':
specifier: ^1.1.2 specifier: ^1.1.2
version: 1.2.1 version: 1.2.1
@ -307,6 +310,9 @@ importers:
react-syntax-highlighter: react-syntax-highlighter:
specifier: ^15.5.0 specifier: ^15.5.0
version: 15.6.1(react@18.3.1) 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: remark-breaks:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.0.0 version: 4.0.0
@ -10092,6 +10098,13 @@ packages:
peerDependencies: peerDependencies:
react: '>= 0.14.0' 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: react-transition-group@4.4.5:
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
peerDependencies: peerDependencies:
@ -10708,6 +10721,9 @@ packages:
space-separated-tokens@2.0.2: space-separated-tokens@2.0.2:
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
spacetime@7.6.2:
resolution: {integrity: sha512-x5Qr2yiG5wy/0IBeaNeYG9OLigePKTj1/liF9j5xWNbb1psi5Q2AuZTh9SpIT+QZqpKCWX0OCdvwJIm9FlJouA==}
spdx-correct@3.2.0: spdx-correct@3.2.0:
resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==}
@ -11083,6 +11099,9 @@ packages:
thunky@1.1.0: thunky@1.1.0:
resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==} resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==}
timezone-soft@1.5.2:
resolution: {integrity: sha512-BUr+CfBfeWXJwFAuEzPO9uF+v6sy3pL5SKLkDg4vdEhsyXgbBnpFoBCW8oEKSNTqNq9YHbVOjNb31xE7WyGmrA==}
tiny-invariant@1.3.3: tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
@ -24844,6 +24863,14 @@ snapshots:
react: 18.3.1 react: 18.3.1
refractor: 3.6.0 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): react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies: dependencies:
'@babel/runtime': 7.26.0 '@babel/runtime': 7.26.0
@ -25645,6 +25672,8 @@ snapshots:
space-separated-tokens@2.0.2: {} space-separated-tokens@2.0.2: {}
spacetime@7.6.2: {}
spdx-correct@3.2.0: spdx-correct@3.2.0:
dependencies: dependencies:
spdx-expression-parse: 3.0.1 spdx-expression-parse: 3.0.1
@ -26099,6 +26128,8 @@ snapshots:
thunky@1.1.0: {} thunky@1.1.0: {}
timezone-soft@1.5.2: {}
tiny-invariant@1.3.3: {} tiny-invariant@1.3.3: {}
tiny-queue@0.2.1: {} tiny-queue@0.2.1: {}