From 111e1ddf669de1f50bf3f2435bb8c44bf0a7cf60 Mon Sep 17 00:00:00 2001 From: Alex Lion Date: Tue, 4 Aug 2026 14:42:28 +0700 Subject: [PATCH] Fix date reload on presentation upload --- assets/js/app.js | 50 +--------------------- assets/js/date_time_local.mjs | 52 +++++++++++++++++++++++ assets/package.json | 3 +- assets/test/date_time_local_hook_test.mjs | 39 +++++++++++++++++ 4 files changed, 95 insertions(+), 49 deletions(-) create mode 100644 assets/js/date_time_local.mjs create mode 100644 assets/test/date_time_local_hook_test.mjs diff --git a/assets/js/app.js b/assets/js/app.js index 779eee8..6a1d9fb 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -17,6 +17,7 @@ import QRCodeStyling from "qr-code-styling"; import { Presenter } from "./presenter"; import { Manager } from "./manager"; import { AudioCapture } from "./audio_capture"; +import DateTimeLocal from "./date_time_local.mjs"; import Split from "split-grid"; import CustomHooks from "./hooks"; import "./admin-charts.js"; @@ -418,54 +419,7 @@ Hooks.CalendarLocalDate = { this.el.innerHTML = moment.utc(this.el.dataset.date).local().calendar(); }, }; -Hooks.DateTimeLocal = { - mounted() { - this.localTime = this.el.querySelector("input[type=datetime-local]"); - this.utcTime = this.el.querySelector("input[type=hidden]"); - this.syncLocalTime(); - - this.handleInput = ({ target }) => { - if (target === this.localTime) this.syncUtcTime(); - }; - this.el.addEventListener("input", this.handleInput); - }, - updated() { - this.localTime = this.el.querySelector("input[type=datetime-local]"); - this.utcTime = this.el.querySelector("input[type=hidden]"); - this.syncLocalTime(); - }, - syncLocalTime() { - if (!this.utcTime.value) { - this.localTime.value = ""; - return; - } - - const value = this.utcTime.value.replace(" ", "T").replace(/Z$/, ""); - const date = new Date(`${value}Z`); - - if (!Number.isNaN(date.getTime())) { - const pad = (part) => String(part).padStart(2, "0"); - this.localTime.value = - `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` + - `T${pad(date.getHours())}:${pad(date.getMinutes())}`; - } - }, - syncUtcTime() { - if (!this.localTime.value) { - this.utcTime.value = ""; - return; - } - - const date = new Date(this.localTime.value); - - if (!Number.isNaN(date.getTime())) { - this.utcTime.value = date.toISOString().slice(0, 19); - } - }, - destroyed() { - this.el.removeEventListener("input", this.handleInput); - }, -}; +Hooks.DateTimeLocal = DateTimeLocal; Hooks.UpdateAttendees = { mounted() { this.handleEvent("update-attendees", ({ count }) => { diff --git a/assets/js/date_time_local.mjs b/assets/js/date_time_local.mjs new file mode 100644 index 0000000..521f98c --- /dev/null +++ b/assets/js/date_time_local.mjs @@ -0,0 +1,52 @@ +const DateTimeLocal = { + mounted() { + this.localTime = this.el.querySelector("input[type=datetime-local]"); + this.utcTime = this.el.querySelector("input[type=hidden]"); + this.syncLocalTime(); + + this.handleInput = ({ target }) => { + if (target === this.localTime) this.syncUtcTime(); + }; + this.el.addEventListener("input", this.handleInput); + this.el.addEventListener("change", this.handleInput); + }, + updated() { + this.localTime = this.el.querySelector("input[type=datetime-local]"); + this.utcTime = this.el.querySelector("input[type=hidden]"); + this.syncLocalTime(); + }, + syncLocalTime() { + if (!this.utcTime.value) { + this.localTime.value = ""; + return; + } + + const value = this.utcTime.value.replace(" ", "T").replace(/Z$/, ""); + const date = new Date(`${value}Z`); + + if (!Number.isNaN(date.getTime())) { + const pad = (part) => String(part).padStart(2, "0"); + this.localTime.value = + `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` + + `T${pad(date.getHours())}:${pad(date.getMinutes())}`; + } + }, + syncUtcTime() { + if (!this.localTime.value) { + this.utcTime.value = ""; + return; + } + + const date = new Date(this.localTime.value); + + if (!Number.isNaN(date.getTime())) { + this.utcTime.value = date.toISOString().slice(0, 19); + } + }, + destroyed() { + this.el.removeEventListener("input", this.handleInput); + this.el.removeEventListener("change", this.handleInput); + }, +}; + +export default DateTimeLocal; diff --git a/assets/package.json b/assets/package.json index 6e6976a..2670aef 100644 --- a/assets/package.json +++ b/assets/package.json @@ -1,6 +1,7 @@ { "scripts": { - "deploy": "NODE_ENV=production tailwindcss --postcss --minify --input=css/app.css --output=../priv/static/assets/app.css" + "deploy": "NODE_ENV=production tailwindcss --postcss --minify --input=css/app.css --output=../priv/static/assets/app.css", + "test": "node --test test/*.mjs" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.11", diff --git a/assets/test/date_time_local_hook_test.mjs b/assets/test/date_time_local_hook_test.mjs new file mode 100644 index 0000000..87d5f2c --- /dev/null +++ b/assets/test/date_time_local_hook_test.mjs @@ -0,0 +1,39 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import DateTimeLocal from "../js/date_time_local.mjs"; + +function mountHook() { + const localTime = { value: "" }; + const utcTime = { value: "2026-08-04T12:00:00" }; + const listeners = new Map(); + + const el = { + querySelector(selector) { + return selector === "input[type=datetime-local]" ? localTime : utcTime; + }, + addEventListener(type, listener) { + listeners.set(type, listener); + }, + removeEventListener(type) { + listeners.delete(type); + }, + dispatch(type, target) { + listeners.get(type)?.({ target }); + }, + }; + + const hook = { ...DateTimeLocal, el }; + hook.mounted(); + + return { el, hook, localTime, utcTime }; +} + +test("syncs the hidden UTC value when the datetime control only emits change", () => { + const { el, localTime, utcTime } = mountHook(); + localTime.value = "2030-01-02T10:30"; + + el.dispatch("change", localTime); + + assert.equal(utcTime.value, new Date(localTime.value).toISOString().slice(0, 19)); +});