fix: harden frontend browser compatibility

This commit is contained in:
Graham McIntire 2026-03-23 13:41:16 -05:00
parent 6e47d3b15c
commit d2fd0d181f
No known key found for this signature in database
5 changed files with 73 additions and 42 deletions

View file

@ -217,6 +217,10 @@ const applyTheme = (theme: string | null) => {
// Apply initial theme from localStorage
applyTheme(localStorage.getItem("theme") || "auto");
const colorSchemeQuery = window.matchMedia
? window.matchMedia("(prefers-color-scheme: dark)")
: null;
// Handle theme changes dispatched from LiveView via JS.dispatch
window.addEventListener("phx:set-theme", ((e: CustomEvent<{ theme: string }>) => {
const theme = e.detail.theme;
@ -226,14 +230,20 @@ window.addEventListener("phx:set-theme", ((e: CustomEvent<{ theme: string }>) =>
}) as EventListener);
// Listen for system theme changes when auto is selected
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", () => {
if (localStorage.getItem("theme") === "auto") {
applyTheme("auto");
window.dispatchEvent(new CustomEvent("themeChanged"));
}
});
const handleSystemThemeChange = () => {
if (localStorage.getItem("theme") === "auto") {
applyTheme("auto");
window.dispatchEvent(new CustomEvent("themeChanged"));
}
};
if (colorSchemeQuery) {
if (typeof colorSchemeQuery.addEventListener === "function") {
colorSchemeQuery.addEventListener("change", handleSystemThemeChange);
} else if (typeof (colorSchemeQuery as MediaQueryList).addListener === "function") {
(colorSchemeQuery as MediaQueryList).addListener(handleSystemThemeChange);
}
}
const liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 5000,

View file

@ -2,7 +2,7 @@
interface TimeAgoHookContext {
el: HTMLElement;
timer: ReturnType<typeof setInterval> | null;
timer: ReturnType<typeof setTimeout> | null;
}
const TimeAgoHook = {
@ -22,14 +22,19 @@ const TimeAgoHook = {
};
function startTimer(this: TimeAgoHookContext) {
const updateTimeAgo = () => {
const timestampStr = this.el.dataset.timestamp;
if (!timestampStr) return;
const timestampStr = this.el.dataset.timestamp;
if (!timestampStr) {
return;
}
const timestamp = new Date(timestampStr);
const now = new Date();
const diffMs = now.getTime() - timestamp.getTime();
const diffSeconds = Math.floor(diffMs / 1000);
const timestampMs = new Date(timestampStr).getTime();
if (Number.isNaN(timestampMs)) {
return;
}
const updateTimeAgo = () => {
const nowMs = Date.now();
const diffSeconds = Math.max(0, Math.floor((nowMs - timestampMs) / 1000));
if (diffSeconds < 60) {
this.el.textContent = `${diffSeconds} second${diffSeconds !== 1 ? "s" : ""} ago`;
@ -43,33 +48,35 @@ function startTimer(this: TimeAgoHookContext) {
const days = Math.floor(diffSeconds / 86400);
this.el.textContent = `${days} day${days !== 1 ? "s" : ""} ago`;
}
const nextDelay = getNextUpdateDelay(diffSeconds);
this.timer = setTimeout(updateTimeAgo, nextDelay);
};
updateTimeAgo();
const timestampStr = this.el.dataset.timestamp;
if (timestampStr) {
const timestamp = new Date(timestampStr);
const age = Date.now() - timestamp.getTime();
let interval: number;
if (age < 60000) {
interval = 1000;
} else if (age < 3600000) {
interval = 60000;
} else {
interval = 300000;
}
this.timer = setInterval(updateTimeAgo, interval);
}
}
function stopTimer(this: TimeAgoHookContext) {
if (this.timer) {
clearInterval(this.timer);
clearTimeout(this.timer);
this.timer = null;
}
}
function getNextUpdateDelay(diffSeconds: number): number {
if (diffSeconds < 60) {
return 1000;
}
if (diffSeconds < 3600) {
return (60 - (diffSeconds % 60)) * 1000;
}
if (diffSeconds < 86400) {
return (3600 - (diffSeconds % 3600)) * 1000;
}
return (86400 - (diffSeconds % 86400)) * 1000;
}
export default TimeAgoHook;

View file

@ -847,11 +847,15 @@ let MapAPRSMap = {
// Use Phoenix LiveView's built-in navigation
const liveSocket = getLiveSocket();
if (liveSocket) {
liveSocket.pushHistoryPatch(navLink.href, "push", navLink);
} else {
// Fallback to regular navigation if LiveView socket not available
window.location.href = navLink.href;
try {
liveSocket.pushHistoryPatch(navLink.href, "push", navLink);
return;
} catch (error) {
console.warn("LiveView navigation failed, falling back to full navigation", error);
}
}
// Fallback to regular navigation if LiveView socket is not available or patching fails
window.location.href = navLink.href;
}
};

View file

@ -127,7 +127,16 @@ export function safePushEvent<T extends BaseEventPayload = BaseEventPayload>(pus
* Check if LiveView socket is available
*/
export function isLiveViewConnected(): boolean {
return typeof window !== 'undefined' && !!window.liveSocket;
const liveSocket = getLiveSocket();
if (!liveSocket) {
return false;
}
if (typeof liveSocket.isConnected === "function") {
return liveSocket.isConnected();
}
return liveSocket.connected !== false;
}
/**
@ -135,4 +144,4 @@ export function isLiveViewConnected(): boolean {
*/
export function getLiveSocket(): LiveSocket | null {
return typeof window !== 'undefined' ? window.liveSocket || null : null;
}
}

View file

@ -110,7 +110,8 @@ export interface MapState {
}
export interface LiveSocket {
connected: boolean;
connected?: boolean;
isConnected?: () => boolean;
pushHistoryPatch: (href: string, state: string, target: HTMLElement) => void;
}
@ -118,4 +119,4 @@ declare global {
interface Window {
liveSocket?: LiveSocket;
}
}
}