Finally working timezones
This commit is contained in:
parent
c925ae8811
commit
aaf351d328
@ -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`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
36
apps/cli/src/commands/settings.ts
Normal file
36
apps/cli/src/commands/settings.ts
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -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());
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
9
apps/web/app/settings/app/page.tsx
Normal file
9
apps/web/app/settings/app/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
<MoodStars
|
||||||
day={day}
|
day={day}
|
||||||
/></div>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
@ -45,7 +45,7 @@ export default async function DayView({
|
|||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
{day.hours}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
57
apps/web/components/dashboard/sidebar/TimezoneDisplay.tsx
Normal file
57
apps/web/components/dashboard/sidebar/TimezoneDisplay.tsx
Normal 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 </span>
|
||||||
|
<span className="whitespace-nowrap">
|
||||||
|
<b>{timezones[timezone]}</b>
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
apps/web/components/settings/AppSettings.tsx
Normal file
26
apps/web/components/settings/AppSettings.tsx
Normal 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>);
|
||||||
|
}
|
||||||
31
apps/web/components/settings/ChangeTimezone.tsx
Normal file
31
apps/web/components/settings/ChangeTimezone.tsx
Normal 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
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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
82
apps/web/lib/timezones.ts
Normal 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",
|
||||||
|
};
|
||||||
6
apps/web/lib/userLocalSettings/timezone.tsx
Normal file
6
apps/web/lib/userLocalSettings/timezone.tsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { z } from "zod";
|
||||||
|
import { createContext, useContext } from "react";
|
||||||
|
|
||||||
|
export const TimezoneCtx = createContext(undefined);
|
||||||
@ -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",
|
||||||
|
|||||||
@ -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` (
|
||||||
@ -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": {
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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) => ({
|
||||||
|
|||||||
18
packages/shared-react/hooks/timezones.ts
Normal file
18
packages/shared-react/hooks/timezones.ts
Normal 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;
|
||||||
|
}
|
||||||
@ -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";
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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 {
|
||||||
|
|||||||
@ -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 })));
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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
31
pnpm-lock.yaml
generated
@ -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: {}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user