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.
This commit is contained in:
Hornwitser 2025-03-09 15:53:51 +01:00
parent 264c97b586
commit c4a6f6b3f9
6 changed files with 83 additions and 7 deletions

View file

@ -6,6 +6,7 @@ export default defineNuxtConfig({
cookieSecretKey: "",
vapidPrivateKey: "",
public: {
defaultTimezone: "Europe/Oslo",
vapidPublicKey: "",
}
}

View file

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

View file

@ -5,6 +5,15 @@
Name: {{ session?.account.name }}
</p>
<p>Access: {{ session?.account.type }}</p>
<form @submit.prevent="changeSettings">
<label>
Timezone
<input type="text" v-model="timezone">
</label>
<button type="submit">
Save
</button>
</form>
<p>
<PushNotification />
</p>
@ -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", {

17
pnpm-lock.yaml generated
View file

@ -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

View file

@ -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<Account, "interestedIds"> = await readBody(event);
const body: Pick<Account, "interestedIds" | "timezone"> = 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);

View file

@ -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 {