Replace JS hooks with LiveView patterns for map page styling, slideover, and theme switching
- Remove BodyClassHook: use CSS :has() selectors on data-map-page attribute
instead of JS toggling body.map-page class
- Remove ResponsiveSlideoverHook: pass viewport_width via LiveSocket connect
params and determine initial slideover state server-side
- Convert theme selector from data-set-theme attributes with DOMContentLoaded
listeners to JS.dispatch("phx:set-theme") with a single event handler
- Fix buggy reRenderAllCharts that incorrectly used `new MapAPRSMap()` as a
Map constructor; charts now re-render via themeChanged event directly
This commit is contained in:
parent
f1b363a2e2
commit
aca0571a3e
5 changed files with 65 additions and 183 deletions
152
assets/js/app.js
152
assets/js/app.js
|
|
@ -40,73 +40,6 @@ import { InfoMap } from "./hooks/info_map";
|
|||
// Import time ago hook
|
||||
import TimeAgoHook from "./hooks/time_ago_hook";
|
||||
|
||||
// Responsive Slideover Hook
|
||||
let ResponsiveSlideoverHook = {
|
||||
mounted() {
|
||||
this.isInitialized = false;
|
||||
|
||||
this.handleResize = () => {
|
||||
const isDesktop = window.innerWidth >= 1024;
|
||||
const isMobile = window.innerWidth < 1024;
|
||||
|
||||
// Set initial state based on screen size
|
||||
if (!this.isInitialized) {
|
||||
this.isInitialized = true;
|
||||
if (isDesktop) {
|
||||
this.pushEvent("set_slideover_state", { open: true });
|
||||
} else {
|
||||
this.pushEvent("set_slideover_state", { open: false });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Initial check after a brief delay to ensure LiveView is ready
|
||||
setTimeout(() => {
|
||||
this.handleResize();
|
||||
}, 100);
|
||||
|
||||
// Listen for resize events with debouncing
|
||||
let resizeTimer;
|
||||
this.debouncedResize = () => {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(this.handleResize, 150);
|
||||
};
|
||||
|
||||
window.addEventListener("resize", this.debouncedResize);
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
if (this.debouncedResize) {
|
||||
window.removeEventListener("resize", this.debouncedResize);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Body Class Hook - Updates body class based on map_page assign
|
||||
let BodyClassHook = {
|
||||
mounted() {
|
||||
this.updateBodyClass();
|
||||
},
|
||||
|
||||
updated() {
|
||||
this.updateBodyClass();
|
||||
},
|
||||
|
||||
updateBodyClass() {
|
||||
// Get the map_page value from the element's data attribute
|
||||
const mapPage = this.el?.dataset?.mapPage === "true";
|
||||
|
||||
// Update body class based on map_page value
|
||||
if (document.body && document.body.classList) {
|
||||
if (mapPage) {
|
||||
document.body.classList.add("map-page");
|
||||
} else {
|
||||
document.body.classList.remove("map-page");
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// APRS MapAPRSMap Hook
|
||||
let Hooks = {};
|
||||
|
||||
|
|
@ -206,29 +139,10 @@ Object.keys(WeatherChartHooks).forEach(hookName => {
|
|||
});
|
||||
|
||||
// Core hooks - no bundle loading needed
|
||||
Hooks.ResponsiveSlideoverHook = ResponsiveSlideoverHook;
|
||||
Hooks.BodyClassHook = BodyClassHook;
|
||||
Hooks.ErrorBoundary = ErrorBoundary;
|
||||
Hooks.TimeAgoHook = TimeAgoHook;
|
||||
|
||||
// Helper function to get theme-aware colors
|
||||
const getThemeColors = () => {
|
||||
const isDark = document.documentElement.getAttribute("data-theme") === "dark";
|
||||
return {
|
||||
text: isDark ? "#e5e7eb" : "#111827",
|
||||
grid: isDark ? "#374151" : "#9ca3af",
|
||||
background: isDark ? "rgba(0, 0, 0, 0.1)" : "rgba(255, 255, 255, 0.1)",
|
||||
};
|
||||
};
|
||||
|
||||
// Theme switching functionality
|
||||
const theme = (() => {
|
||||
if (typeof localStorage !== "undefined" && localStorage.getItem("theme")) {
|
||||
return localStorage.getItem("theme");
|
||||
}
|
||||
return "auto";
|
||||
})();
|
||||
|
||||
// Theme management
|
||||
const applyTheme = (theme) => {
|
||||
const element = document.documentElement;
|
||||
if (!element) return;
|
||||
|
|
@ -244,65 +158,29 @@ const applyTheme = (theme) => {
|
|||
}
|
||||
};
|
||||
|
||||
// Apply initial theme
|
||||
applyTheme(theme);
|
||||
window.localStorage.setItem("theme", theme);
|
||||
// Apply initial theme from localStorage
|
||||
applyTheme(localStorage.getItem("theme") || "auto");
|
||||
|
||||
// Global function to re-render all charts
|
||||
window.reRenderAllCharts = () => {
|
||||
// Store all chart instances globally so we can access them
|
||||
if (!window.chartInstances) {
|
||||
window.chartInstances = new MapAPRSMap();
|
||||
}
|
||||
|
||||
// Re-render all stored chart instances
|
||||
window.chartInstances.forEach((chartInstance, elementId) => {
|
||||
if (chartInstance && typeof chartInstance.renderChart === "function") {
|
||||
chartInstance.renderChart();
|
||||
}
|
||||
});
|
||||
|
||||
// Also dispatch a custom event that charts can listen to
|
||||
// Handle theme changes dispatched from LiveView via JS.dispatch
|
||||
window.addEventListener("phx:set-theme", (e) => {
|
||||
const theme = e.detail.theme;
|
||||
applyTheme(theme);
|
||||
localStorage.setItem("theme", theme);
|
||||
window.dispatchEvent(new CustomEvent("themeChanged"));
|
||||
};
|
||||
|
||||
const handleThemeClick = (selectedTheme) => {
|
||||
applyTheme(selectedTheme);
|
||||
localStorage.setItem("theme", selectedTheme);
|
||||
|
||||
// Re-render all charts with new theme colors
|
||||
setTimeout(() => {
|
||||
window.reRenderAllCharts();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// Listen for system theme changes when auto is selected
|
||||
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
|
||||
if (localStorage.getItem("theme") === "auto") {
|
||||
applyTheme("auto");
|
||||
|
||||
// Re-render all charts with new theme colors
|
||||
setTimeout(() => {
|
||||
window.reRenderAllCharts();
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
// Add event listeners for theme switching
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const themeButtons = document.querySelectorAll("[data-set-theme]");
|
||||
themeButtons.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const theme = button.getAttribute("data-set-theme");
|
||||
handleThemeClick(theme);
|
||||
});
|
||||
});
|
||||
// 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"));
|
||||
}
|
||||
});
|
||||
|
||||
console.log("Creating LiveSocket with hooks:", Object.keys(Hooks));
|
||||
let liveSocket = new LiveSocket("/live", Socket, {
|
||||
longPollFallbackMs: 2500,
|
||||
params: { _csrf_token: csrfToken },
|
||||
params: { _csrf_token: csrfToken, viewport_width: window.innerWidth },
|
||||
hooks: Hooks,
|
||||
timeout: 60000, // 60 second timeout for slow initial loads
|
||||
});
|
||||
|
|
|
|||
|
|
@ -475,7 +475,7 @@ defmodule AprsmeWeb.CoreComponents do
|
|||
</div>
|
||||
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
|
||||
<li>
|
||||
<button data-set-theme="light" data-act-class="ACTIVECLASS" class="flex items-center gap-2">
|
||||
<button phx-click={JS.dispatch("phx:set-theme", detail: %{theme: "light"})} class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
|
|
@ -487,7 +487,7 @@ defmodule AprsmeWeb.CoreComponents do
|
|||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button data-set-theme="dark" data-act-class="ACTIVECLASS" class="flex items-center gap-2">
|
||||
<button phx-click={JS.dispatch("phx:set-theme", detail: %{theme: "dark"})} class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
|
||||
</svg>
|
||||
|
|
@ -495,7 +495,7 @@ defmodule AprsmeWeb.CoreComponents do
|
|||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button data-set-theme="auto" data-act-class="ACTIVECLASS" class="flex items-center gap-2">
|
||||
<button phx-click={JS.dispatch("phx:set-theme", detail: %{theme: "auto"})} class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
|
|
|
|||
|
|
@ -4,13 +4,7 @@ defmodule AprsmeWeb.Layouts do
|
|||
|
||||
embed_templates "layouts/*"
|
||||
|
||||
def body_class(assigns) do
|
||||
base_classes = ["bg-base-100 antialiased"]
|
||||
|
||||
if Map.get(assigns, :map_page, false) do
|
||||
base_classes ++ ["map-page"]
|
||||
else
|
||||
base_classes
|
||||
end
|
||||
def body_class(_assigns) do
|
||||
["bg-base-100 antialiased"]
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@
|
|||
}
|
||||
|
||||
/* Reset constraints for non-map pages */
|
||||
body:not(.map-page) main,
|
||||
body:not(.map-page) .phx-main,
|
||||
body:not(.map-page) div[data-phx-main="true"] {
|
||||
body:not(:has(#main-content[data-map-page="true"])) main,
|
||||
body:not(:has(#main-content[data-map-page="true"])) .phx-main,
|
||||
body:not(:has(#main-content[data-map-page="true"])) div[data-phx-main="true"] {
|
||||
overflow: visible !important;
|
||||
height: auto !important;
|
||||
max-height: none !important;
|
||||
|
|
@ -18,48 +18,47 @@
|
|||
}
|
||||
|
||||
/* Ensure proper scrolling for non-map pages */
|
||||
body:not(.map-page) {
|
||||
body:not(:has(#main-content[data-map-page="true"])) {
|
||||
overflow-y: auto !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
/* Only apply map constraints to map pages */
|
||||
body.map-page,
|
||||
body.map-page html {
|
||||
body:has(#main-content[data-map-page="true"]) {
|
||||
overflow: hidden !important;
|
||||
height: 100vh !important;
|
||||
}
|
||||
|
||||
/* Map page specific styles */
|
||||
body.map-page header {
|
||||
/* Map page specific styles - hide navbar header */
|
||||
body:has(#main-content[data-map-page="true"]) .navbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.map-page main {
|
||||
body:has(#main-content[data-map-page="true"]) main {
|
||||
padding: 0;
|
||||
max-width: none;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body.map-page main > div {
|
||||
body:has(#main-content[data-map-page="true"]) main > div {
|
||||
max-width: none;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body.map-page .phx-main {
|
||||
body:has(#main-content[data-map-page="true"]) .phx-main {
|
||||
position: relative;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body.map-page div[data-phx-main="true"] {
|
||||
body:has(#main-content[data-map-page="true"]) div[data-phx-main="true"] {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Full page map for APRS home page and callsign pages */
|
||||
body.map-page #aprs-map {
|
||||
body:has(#main-content[data-map-page="true"]) #aprs-map {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
|
|
@ -72,24 +71,24 @@
|
|||
|
||||
/* Desktop slideover behavior for map */
|
||||
@media (min-width: 1024px) {
|
||||
body.map-page #aprs-map.slideover-open {
|
||||
body:has(#main-content[data-map-page="true"]) #aprs-map.slideover-open {
|
||||
right: 352px !important;
|
||||
}
|
||||
|
||||
body.map-page #aprs-map.slideover-closed {
|
||||
|
||||
body:has(#main-content[data-map-page="true"]) #aprs-map.slideover-closed {
|
||||
right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile slideover behavior for map */
|
||||
@media (max-width: 1023px) {
|
||||
body.map-page #aprs-map {
|
||||
body:has(#main-content[data-map-page="true"]) #aprs-map {
|
||||
right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Slideover panel responsive styles */
|
||||
body.map-page .slideover-panel {
|
||||
body:has(#main-content[data-map-page="true"]) .slideover-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
|
@ -104,35 +103,35 @@
|
|||
|
||||
/* Desktop slideover behavior */
|
||||
@media (min-width: 1024px) {
|
||||
body.map-page .slideover-panel.slideover-open {
|
||||
body:has(#main-content[data-map-page="true"]) .slideover-panel.slideover-open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
body.map-page .slideover-panel.slideover-closed {
|
||||
|
||||
body:has(#main-content[data-map-page="true"]) .slideover-panel.slideover-closed {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile slideover behavior */
|
||||
@media (max-width: 1023px) {
|
||||
body.map-page .slideover-panel {
|
||||
body:has(#main-content[data-map-page="true"]) .slideover-panel {
|
||||
width: 100vw;
|
||||
max-width: 400px;
|
||||
box-shadow: -8px 0 32px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
body.map-page .slideover-panel.slideover-open {
|
||||
|
||||
body:has(#main-content[data-map-page="true"]) .slideover-panel.slideover-open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
body.map-page .slideover-panel.slideover-closed {
|
||||
|
||||
body:has(#main-content[data-map-page="true"]) .slideover-panel.slideover-closed {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile panel improvements */
|
||||
@media (max-width: 1023px) {
|
||||
body.map-page .slideover-panel {
|
||||
body:has(#main-content[data-map-page="true"]) .slideover-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
|
@ -140,12 +139,12 @@
|
|||
|
||||
/* Improve scrolling on mobile */
|
||||
@media (max-width: 1023px) {
|
||||
body.map-page .slideover-panel {
|
||||
body:has(#main-content[data-map-page="true"]) .slideover-panel {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
body.map-page .slideover-panel .flex-1 {
|
||||
|
||||
body:has(#main-content[data-map-page="true"]) .slideover-panel .flex-1 {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -155,7 +154,6 @@
|
|||
<main
|
||||
id="main-content"
|
||||
class={if assigns[:map_page], do: "min-h-screen bg-base-100", else: "bg-base-100"}
|
||||
phx-hook="BodyClassHook"
|
||||
data-map-page={to_string(assigns[:map_page] || false)}
|
||||
>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
import AprsmeWeb.Live.Shared.PacketUtils, only: [get_callsign_key: 1]
|
||||
import AprsmeWeb.MapLive.Components
|
||||
import AprsmeWeb.TimeHelpers, only: [time_ago_in_words: 1]
|
||||
import Phoenix.LiveView, only: [connected?: 1, push_event: 3, push_patch: 2, put_flash: 3]
|
||||
|
||||
import Phoenix.LiveView,
|
||||
only: [connected?: 1, get_connect_params: 1, push_event: 3, push_patch: 2, put_flash: 3]
|
||||
|
||||
# Import the new components module
|
||||
alias Aprsme.Packets
|
||||
|
|
@ -198,6 +200,9 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
# Start packet batcher for efficient updates
|
||||
{:ok, batcher_pid} = PacketBatcher.start_link(self())
|
||||
|
||||
# Determine initial slideover state from client viewport width
|
||||
slideover_open = initial_slideover_open?(socket)
|
||||
|
||||
assign(socket,
|
||||
map_ready: false,
|
||||
map_bounds: initial_bounds,
|
||||
|
|
@ -212,7 +217,7 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
trail_duration: trail_duration,
|
||||
historical_hours: historical_hours,
|
||||
packet_age_threshold: packet_age_threshold,
|
||||
slideover_open: true,
|
||||
slideover_open: slideover_open,
|
||||
deployed_at: deployed_at,
|
||||
map_page: true,
|
||||
packet_buffer: [],
|
||||
|
|
@ -239,6 +244,15 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
if connection_disabled, do: "disconnected", else: "connected"
|
||||
end
|
||||
|
||||
defp initial_slideover_open?(socket) do
|
||||
if connected?(socket) do
|
||||
viewport_width = get_connect_params(socket)["viewport_width"] || 1024
|
||||
viewport_width >= 1024
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
@spec assign_defaults(Socket.t(), DateTime.t()) :: Socket.t()
|
||||
defp assign_defaults(socket, one_hour_ago) do
|
||||
assign(socket,
|
||||
|
|
@ -269,7 +283,6 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
pending_batch_tasks: [],
|
||||
# Overlay controls
|
||||
overlay_callsign: "",
|
||||
# Slideover state - will be set based on screen size
|
||||
slideover_open: true,
|
||||
# Track when last update occurred for real-time display in map sidebar
|
||||
# Updated when packets are processed or map bounds change
|
||||
|
|
@ -1431,7 +1444,6 @@ defmodule AprsmeWeb.MapLive.Index do
|
|||
"slideover-panel",
|
||||
if(@slideover_open, do: "slideover-open", else: "slideover-closed")
|
||||
]}
|
||||
phx-hook="ResponsiveSlideoverHook"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between p-6 border-b border-slate-200 dark:border-slate-700 bg-gradient-to-r from-indigo-600 to-purple-600 text-white">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue