diff --git a/internal/templates/anime.templ b/internal/templates/anime.templ
index d602554..07b3f40 100644
--- a/internal/templates/anime.templ
+++ b/internal/templates/anime.templ
@@ -149,7 +149,7 @@ templ AnimeDetails(anime jikan.Anime, currentStatus string) {
if anime.Broadcast.String != "" {
}
if len(anime.Streaming) > 0 {
diff --git a/internal/templates/layout.templ b/internal/templates/layout.templ
index 28e6895..ab2a09f 100644
--- a/internal/templates/layout.templ
+++ b/internal/templates/layout.templ
@@ -14,6 +14,7 @@ templ Layout(title string) {
+
diff --git a/internal/templates/notifications.templ b/internal/templates/notifications.templ
index 60b4ed6..2d9ad51 100644
--- a/internal/templates/notifications.templ
+++ b/internal/templates/notifications.templ
@@ -124,7 +124,7 @@ templ NotificationCard(item WatchingAnimeWithDetails) {
if item.Anime.Broadcast.String != "" {
- { item.Anime.Broadcast.String }
+ { item.Anime.Broadcast.String }
}
if item.Anime.Episodes > 0 {
diff --git a/static/js/timezone.js b/static/js/timezone.js
new file mode 100644
index 0000000..8e7d01d
--- /dev/null
+++ b/static/js/timezone.js
@@ -0,0 +1,113 @@
+;(function () {
+ const jstOffsetMinutes = 9 * 60
+
+ const parseBroadcast = (text) => {
+ const match = text.match(/^(.*) at (\d{1,2}):(\d{2}) \(JST\)$/i)
+ if (!match) {
+ return null
+ }
+
+ const day = match[1].trim()
+ const hour = Number.parseInt(match[2], 10)
+ const minute = Number.parseInt(match[3], 10)
+
+ if (Number.isNaN(hour) || Number.isNaN(minute)) {
+ return null
+ }
+
+ return { day, hour, minute }
+ }
+
+ const normalizeDay = (day) => {
+ const key = day.trim().toLowerCase().replace(/s$/, '')
+ const days = {
+ mon: 1,
+ monday: 1,
+ tue: 2,
+ tues: 2,
+ tuesday: 2,
+ wed: 3,
+ wednesday: 3,
+ thu: 4,
+ thur: 4,
+ thurs: 4,
+ thursday: 4,
+ fri: 5,
+ friday: 5,
+ sat: 6,
+ saturday: 6,
+ sun: 0,
+ sunday: 0,
+ }
+
+ if (typeof days[key] !== 'number') {
+ return null
+ }
+
+ return days[key]
+ }
+
+ const convertToLocal = (parsed, localOffsetMinutes) => {
+ const sourceMinutes = parsed.hour * 60 + parsed.minute
+ const diff = jstOffsetMinutes - localOffsetMinutes
+ const localTotal = sourceMinutes - diff
+
+ const dayShift = Math.floor(localTotal / 1440)
+ const normalizedMinutes = ((localTotal % 1440) + 1440) % 1440
+ const localHour = Math.floor(normalizedMinutes / 60)
+ const localMinute = normalizedMinutes % 60
+
+ const sourceDayIndex = normalizeDay(parsed.day)
+ if (sourceDayIndex === null) {
+ return null
+ }
+
+ const localDayIndex = ((sourceDayIndex + dayShift) % 7 + 7) % 7
+ const localDay = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][localDayIndex]
+
+ const time = `${localHour.toString().padStart(2, '0')}:${localMinute.toString().padStart(2, '0')}`
+ return `${localDay} at ${time} (Local)`
+ }
+
+ const updateNode = (node, localOffsetMinutes) => {
+ const source = node.getAttribute('data-jst-text')
+ if (!source) {
+ return
+ }
+
+ const parsed = parseBroadcast(source)
+ if (!parsed) {
+ return
+ }
+
+ const converted = convertToLocal(parsed, localOffsetMinutes)
+ if (!converted) {
+ return
+ }
+
+ node.textContent = converted
+ }
+
+ const hideJstSuffix = () => {
+ document.querySelectorAll('.notification-broadcast, [data-jst-text]').forEach((node) => {
+ if (!(node instanceof HTMLElement)) {
+ return
+ }
+
+ const text = node.textContent || ''
+ if (text.includes('(JST)')) {
+ node.textContent = text.replace(/\s*\(JST\)/gi, ' (Local)')
+ }
+ })
+ }
+
+ const updateAll = () => {
+ const localOffsetMinutes = -new Date().getTimezoneOffset()
+ const nodes = document.querySelectorAll('[data-jst-text]')
+ nodes.forEach((node) => updateNode(node, localOffsetMinutes))
+ hideJstSuffix()
+ }
+
+ document.addEventListener('DOMContentLoaded', updateAll)
+ document.body.addEventListener('htmx:afterSwap', updateAll)
+})()