diff --git a/package.json b/package.json index e4db637..500eae0 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@types/react-dom": "18.0.11", "autoprefixer": "^10.4.13", "date-fns": "^2.29.3", + "date-fns-tz": "^2.0.0", "discord.js": "^14.7.1", "ioredis": "^5.3.1", "life360-node-api": "^0.0.10", diff --git a/src/env/schema.mjs b/src/env/schema.mjs index 20bbb49..f957e4b 100644 --- a/src/env/schema.mjs +++ b/src/env/schema.mjs @@ -1,6 +1,7 @@ // @ts-check import { z } from 'zod'; +const TimezoneSchema = z.enum(Intl.supportedValuesOf("timeZone")); /** * Specify your server-side environment variables schema here. * This way you can ensure the app isn't built with invalid env vars. @@ -18,5 +19,6 @@ export const serverSchema = z.object({ EDGE_CACHE_TIME_SECONDS: z.coerce.number().int().nonnegative().default(60), REDIS_URL: z.string().url(), DISCORD_TOKEN: z.string(), - DISCORD_TARGET_USER_ID: z.string() + DISCORD_TARGET_USER_ID: z.string(), + TIMEZONE: TimezoneSchema }); diff --git a/src/env/server.mjs b/src/env/server.mjs index d5a44ae..c07f9f6 100644 --- a/src/env/server.mjs +++ b/src/env/server.mjs @@ -19,7 +19,8 @@ const _serverEnv = serverSchema.safeParse({ EDGE_CACHE_TIME_SECONDS: process.env.EDGE_CACHE_TIME_SECONDS, REDIS_URL: process.env.REDIS_URL, DISCORD_TOKEN: process.env.DISCORD_TOKEN, - DISCORD_TARGET_USER_ID: process.env.DISCORD_TARGET_USER_ID + DISCORD_TARGET_USER_ID: process.env.DISCORD_TARGET_USER_ID, + TIMEZONE: process.env.TIMEZONE }); if (_serverEnv.success === false) { diff --git a/src/pages/api/health.ts b/src/pages/api/health.ts index c66d1de..cb1cd2b 100644 --- a/src/pages/api/health.ts +++ b/src/pages/api/health.ts @@ -5,6 +5,7 @@ import { env } from "@/env/server.mjs"; type ResponseData = { now: number; status: string; + timezone: string; }; export default async function handler( @@ -14,5 +15,6 @@ export default async function handler( res.status(200).json({ now: new Date().getTime(), status: req.query.key == env.API_KEY ? "Authorized" : "Unauthorized", + timezone: env.TIMEZONE }); } diff --git a/src/utils/timezone.ts b/src/utils/timezone.ts new file mode 100644 index 0000000..8bb906d --- /dev/null +++ b/src/utils/timezone.ts @@ -0,0 +1,13 @@ +import { utcToZonedTime } from 'date-fns-tz'; +import { z } from 'zod'; +import { env } from '@/env/server.mjs'; + +// @ts-ignore TS2339 -- TODO: Figure out why Intl.supportedValuesOf isn't seen by Typescript +export const TimezoneSchema = z.enum(Intl.supportedValuesOf('timeZone')); +export type Timezone = z.infer; + +export function localNow(now?: Date, zone?: Timezone): Date { + zone = zone ?? env.TIMEZONE; + now = now ?? new Date(); + return utcToZonedTime(now, zone); +} diff --git a/tsconfig.json b/tsconfig.json index 828190b..ee94160 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "target": "ES2016", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": ["dom", "dom.iterable", "esnext", "es2021.intl"], "allowJs": true, "skipLibCheck": true, "strict": true, diff --git a/yarn.lock b/yarn.lock index 15cb34b..36e06c0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -357,6 +357,11 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9" integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== +date-fns-tz@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/date-fns-tz/-/date-fns-tz-2.0.0.tgz#1b14c386cb8bc16fc56fe333d4fc34ae1d1099d5" + integrity sha512-OAtcLdB9vxSXTWHdT8b398ARImVwQMyjfYGkKD2zaGpHseG2UPHbHjXELReErZFxWdSLph3c2zOaaTyHfOhERQ== + date-fns@^2.29.3: version "2.29.3" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8"