From c4a6f6b3f9f7211319e6017313504a3a5fec7867 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sun, 9 Mar 2025 15:53:51 +0100 Subject: [PATCH] Add per account overridable timezone setting To make it possible to render the timetable in the user's local time we need to know the timezone to render it in on the server. Otherwise there will be hydration errors and paint flashing as the client renders a different timezone. Add a server global default timezone that can be overriden on a per-account bases to prepare for timezone handling the timetable. --- nuxt.config.ts | 1 + package.json | 2 ++ pages/account/settings.vue | 25 +++++++++++++++++++++ pnpm-lock.yaml | 17 ++++++++++++++ server/api/account.patch.ts | 44 +++++++++++++++++++++++++++++++------ shared/types/account.d.ts | 1 + 6 files changed, 83 insertions(+), 7 deletions(-) diff --git a/nuxt.config.ts b/nuxt.config.ts index f7de71c..69fc68d 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -6,6 +6,7 @@ export default defineNuxtConfig({ cookieSecretKey: "", vapidPrivateKey: "", public: { + defaultTimezone: "Europe/Oslo", vapidPublicKey: "", } } diff --git a/package.json b/package.json index 503e65c..bd7dbc8 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "postinstall": "nuxt prepare" }, "dependencies": { + "luxon": "^3.5.0", "nuxt": "^3.15.4", "vue": "latest", "vue-router": "latest", @@ -23,6 +24,7 @@ ] }, "devDependencies": { + "@types/luxon": "^3.4.2", "@types/web-push": "^3.6.4" } } diff --git a/pages/account/settings.vue b/pages/account/settings.vue index e1283d4..41dcd69 100644 --- a/pages/account/settings.vue +++ b/pages/account/settings.vue @@ -5,6 +5,15 @@ Name: {{ session?.account.name }}

Access: {{ session?.account.type }}

+
+ + +

@@ -26,6 +35,22 @@ definePageMeta({ const { data: session } = useAccountSession(); const { refresh: sessionRefresh } = useAccountSession(); +const timezone = ref(session.value?.account.timezone ?? ""); + +async function changeSettings() { + try { + await $fetch("/api/account", { + method: "patch", + body: { + timezone: timezone.value, + } + }); + await sessionRefresh(); + } catch (err: any) { + alert(err.data.message); + } +} + async function deleteAccount() { try { await $fetch.raw("/api/account", { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 361b39c..2108bdd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + luxon: + specifier: ^3.5.0 + version: 3.5.0 nuxt: specifier: ^3.15.4 version: 3.15.4(@parcel/watcher@2.5.1)(@types/node@22.13.8)(db0@0.2.4)(ioredis@5.5.0)(magicast@0.3.5)(rollup@4.34.9)(terser@5.39.0)(typescript@5.8.2)(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0) @@ -21,6 +24,9 @@ importers: specifier: ^3.6.7 version: 3.6.7 devDependencies: + '@types/luxon': + specifier: ^3.4.2 + version: 3.4.2 '@types/web-push': specifier: ^3.6.4 version: 3.6.4 @@ -873,6 +879,9 @@ packages: '@types/http-proxy@1.17.16': resolution: {integrity: sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==} + '@types/luxon@3.4.2': + resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} + '@types/node@22.13.8': resolution: {integrity: sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ==} @@ -1943,6 +1952,10 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + luxon@3.5.0: + resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==} + engines: {node: '>=12'} + magic-string-ast@0.7.0: resolution: {integrity: sha512-686fgAHaJY7wLTFEq7nnKqeQrhqmXB19d1HnqT35Ci7BN6hbAYLZUezTQ062uUHM7ggZEQlqJ94Ftls+KDXU8Q==} engines: {node: '>=16.14.0'} @@ -4036,6 +4049,8 @@ snapshots: dependencies: '@types/node': 22.13.8 + '@types/luxon@3.4.2': {} + '@types/node@22.13.8': dependencies: undici-types: 6.20.0 @@ -5210,6 +5225,8 @@ snapshots: dependencies: yallist: 3.1.1 + luxon@3.5.0: {} + magic-string-ast@0.7.0: dependencies: magic-string: 0.30.17 diff --git a/server/api/account.patch.ts b/server/api/account.patch.ts index 3b27cd8..9059d6f 100644 --- a/server/api/account.patch.ts +++ b/server/api/account.patch.ts @@ -1,12 +1,16 @@ import { Account } from "~/shared/types/account"; import { readAccounts, writeAccounts } from "~/server/database"; +import { DateTime } from "luxon"; export default defineEventHandler(async (event) => { const session = await requireAccountSession(event); - const body: Pick = await readBody(event); + const body: Pick = await readBody(event); if ( - !(body.interestedIds instanceof Array) - || !body.interestedIds.every(id => typeof id === "string") + body.interestedIds !== undefined + && !( + !(body.interestedIds instanceof Array) + || !body.interestedIds.every(id => typeof id === "string") + ) ) { throw createError({ status: 400, @@ -14,16 +18,42 @@ export default defineEventHandler(async (event) => { }); } + if (body.timezone !== undefined) { + if (typeof body.timezone !== "string") { + throw createError({ + status: 400, + message: "Invalid timezone", + }); + } + if (body.timezone.length) { + const zonedTime = DateTime.local().setZone(body.timezone); + if (!zonedTime.isValid) { + throw createError({ + status: 400, + message: "Invalid timezone: " + zonedTime.invalidExplanation, + }); + } + } + } + const accounts = await readAccounts(); const sessionAccount = accounts.find(account => account.id === session.accountId); if (!sessionAccount) { throw Error("Account does not exist"); } - if (body.interestedIds.length) { - sessionAccount.interestedIds = body.interestedIds; - } else { - delete sessionAccount.interestedIds; + if (body.interestedIds !== undefined) { + if (body.interestedIds.length) { + sessionAccount.interestedIds = body.interestedIds; + } else { + delete sessionAccount.interestedIds; + } + } + if (body.timezone !== undefined) { + if (body.timezone) + sessionAccount.timezone = body.timezone; + else + delete sessionAccount.timezone; } await writeAccounts(accounts); diff --git a/shared/types/account.d.ts b/shared/types/account.d.ts index 9a3780a..8662628 100644 --- a/shared/types/account.d.ts +++ b/shared/types/account.d.ts @@ -4,6 +4,7 @@ export interface Account { /** Name of the account. Not present on anonymous accounts */ name?: string, interestedIds?: string[], + timezone?: string, } export interface Subscription {