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 {