diff --git a/package.json b/package.json index 3ee1e6f1..31d9ef03 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "cssnano": "7.0.6", "eslint": "9.16.0", "globals": "15.13.0", + "howler": "2.2.4", "htmx.org": "1.9.12", "pino-princess": "1.0.0", "postcss-cli": "11.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fbc23a86..f48dc91b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -177,6 +177,9 @@ importers: globals: specifier: 15.13.0 version: 15.13.0 + howler: + specifier: 2.2.4 + version: 2.2.4 htmx.org: specifier: 1.9.12 version: 1.9.12 @@ -1644,6 +1647,9 @@ packages: highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + howler@2.2.4: + resolution: {integrity: sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w==} + htmx.org@1.9.12: resolution: {integrity: sha512-VZAohXyF7xPGS52IM8d1T1283y+X4D+Owf3qY1NZ9RuBypyu9l8cGsxUMAG5fEAb/DhT7rDoJ9Hpu5/HxFD3cw==} @@ -4447,6 +4453,8 @@ snapshots: highlight.js@10.7.3: {} + howler@2.2.4: {} + htmx.org@1.9.12: {} http-errors@2.0.0: diff --git a/public/js/main.js b/public/js/main.js index 38c6df3e..ed9306d3 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -7,3 +7,4 @@ import './countdown.js' import './fade-scroll.js' import './flash-message.js' import './map-thumbnail.js' +import './notification.js' diff --git a/public/js/notification.js b/public/js/notification.js new file mode 100644 index 00000000..e18887c5 --- /dev/null +++ b/public/js/notification.js @@ -0,0 +1,56 @@ +import htmx from './htmx.js' +import { Howl } from 'https://cdn.jsdelivr.net/npm/howler@2.2.4/+esm' + +function init(/** @type {HTMLElement} */ element) { + const title = element.getAttribute('data-notification-title') + if (!title) { + return + } + + const body = element.getAttribute('data-notification-body') ?? '' + const icon = element.getAttribute('data-notification-icon') + const notification = new Notification(title, { body, ...(icon ? { icon } : {}) }) + + let /** @type {Howl} */ howl + const sound = element.getAttribute('data-notification-sound') + if (sound) { + let volume = 1.0 + + const v = element.getAttribute('data-notification-sound-volume') + if (v) { + volume = parseFloat(v) + } + + howl = new Howl({ + src: sound, + autoplay: true, + volume, + }) + } + + function afterSwap() { + if (document.body.contains(element)) { + return + } + + notification.close() + howl.stop() + htmx.off('htmx:afterSwap', afterSwap) + } + + htmx.on('htmx:afterSwap', afterSwap) +} + +htmx.onLoad(element => { + if (!(element instanceof HTMLElement)) return + + if (element.hasAttribute('data-notification-title')) { + init(element) + } + + element.querySelectorAll(`[data-notification-title]`).forEach(element => { + if (element instanceof HTMLElement) { + init(element) + } + }) +}) diff --git a/public/js/types.d.ts b/public/js/types.d.ts index 5b8d89d5..51f7a707 100644 --- a/public/js/types.d.ts +++ b/public/js/types.d.ts @@ -6,6 +6,10 @@ declare module 'https://esm.sh/chart.js@4.4.6?bundle-deps&exports=Chart,PieContr export * from 'chart.js' } +declare module 'https://cdn.jsdelivr.net/npm/howler@2.2.4/+esm' { + export { Howl } from 'howler' +} + interface Window { htmx: typeof import('htmx.org').default } diff --git a/src/html/layout.tsx b/src/html/layout.tsx index 87e252c6..814313c7 100644 --- a/src/html/layout.tsx +++ b/src/html/layout.tsx @@ -18,7 +18,7 @@ export function Layout( const title = {props?.title ?? environment.WEBSITE_NAME} const body = ( <> - {props?.embedStyle && } + {props?.embedStyle ? : <>}
{props?.children}
@@ -44,6 +44,7 @@ export function Layout( + {title} diff --git a/src/queue/views/html/ready-up-dialog.tsx b/src/queue/views/html/ready-up-dialog.tsx index 5eb02f12..52ee7242 100644 --- a/src/queue/views/html/ready-up-dialog.tsx +++ b/src/queue/views/html/ready-up-dialog.tsx @@ -4,60 +4,76 @@ const dialogId = 'ready-up-dialog' export function ReadyUpDialog() { return ( - + then me.showModal() end on close me.close() end `} - > -
`} > -
- Game is starting! - Are you ready to play? -
+ `} + > +
+ Game is starting! + Are you ready to play? +
-
- - -
-
-
+
+ + +
+ +
+ + ) } ReadyUpDialog.show = () => { const id = nanoid() return ( -
-
-
+ <> +
+
+
+
+
+
+ ) } ReadyUpDialog.close = () => { const id = nanoid() return ( -
-
-
+ <> +
+
+
+
+ ) }