neiam
/apollos_crib
It's Apollo's Crib
public · Issues · Pulls 1 · Labels · Forks · Compare · Actions failure · Badges · Packages
⭐
Log in to mark this repository.
theme picker, fix the 404 on logout bug :D
38dda48 · gmorell · 2025-09-16 01:58
Files changed
modified
apps/room_sanctum/assets/js/app.js
+67
−0
@@ -38,6 +38,73 @@ let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("
| 38 | 38 | |
| 39 | 39 | let Hooks = {}; |
| 40 | 40 | |
| 41 | +// Theme detection and UI updates | |
| 42 | +Hooks.ThemeDetector = { | |
| 43 | + mounted() { | |
| 44 | + this.updateThemeIndicators(); | |
| 45 | + | |
| 46 | + // Listen for theme changes | |
| 47 | + window.addEventListener("phx:set-theme", () => { | |
| 48 | + setTimeout(() => this.updateThemeIndicators(), 100); | |
| 49 | + }); | |
| 50 | + | |
| 51 | + // Listen for storage changes (cross-tab) | |
| 52 | + window.addEventListener("storage", (e) => { | |
| 53 | + if (e.key === "apollos:theme") { | |
| 54 | + setTimeout(() => this.updateThemeIndicators(), 100); | |
| 55 | + } | |
| 56 | + }); | |
| 57 | + | |
| 58 | + // Listen for system theme changes | |
| 59 | + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { | |
| 60 | + if (!localStorage.getItem("apollos:theme")) { | |
| 61 | + setTimeout(() => this.updateThemeIndicators(), 100); | |
| 62 | + } | |
| 63 | + }); | |
| 64 | + }, | |
| 65 | + | |
| 66 | + updateThemeIndicators() { | |
| 67 | + const currentTheme = this.getCurrentTheme(); | |
| 68 | + const themeCards = this.el.querySelectorAll('.theme-card'); | |
| 69 | + | |
| 70 | + themeCards.forEach(card => { | |
| 71 | + const themeName = card.getAttribute('data-theme-name'); | |
| 72 | + const indicator = card.querySelector('.theme-indicator'); | |
| 73 | + | |
| 74 | + if (themeName === currentTheme) { | |
| 75 | + // Show as current theme | |
| 76 | + card.classList.add('border-blue-500', 'ring-2', 'ring-blue-500'); | |
| 77 | + card.classList.remove('border-gray-200', 'dark:border-gray-700'); | |
| 78 | + indicator.classList.remove('hidden'); | |
| 79 | + } else { | |
| 80 | + // Show as selectable theme | |
| 81 | + card.classList.remove('border-blue-500', 'ring-2', 'ring-blue-500'); | |
| 82 | + card.classList.add('border-gray-200', 'dark:border-gray-700'); | |
| 83 | + indicator.classList.add('hidden'); | |
| 84 | + } | |
| 85 | + }); | |
| 86 | + }, | |
| 87 | + | |
| 88 | + getCurrentTheme() { | |
| 89 | + const savedTheme = localStorage.getItem("apollos:theme"); | |
| 90 | + if (savedTheme) { | |
| 91 | + return savedTheme; | |
| 92 | + } | |
| 93 | + | |
| 94 | + // Check if system theme is being used | |
| 95 | + const currentDataTheme = document.documentElement.getAttribute("data-theme"); | |
| 96 | + if (!savedTheme && currentDataTheme) { | |
| 97 | + // This means system theme is active, determine what it resolved to | |
| 98 | + const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; | |
| 99 | + if (currentDataTheme === (isDark ? 'dark' : 'lofi')) { | |
| 100 | + return 'system'; | |
| 101 | + } | |
| 102 | + } | |
| 103 | + | |
| 104 | + return currentDataTheme || 'system'; | |
| 105 | + } | |
| 106 | +}; | |
| 107 | + | |
| 41 | 108 | // Add our new LeafletMap hook |
| 42 | 109 | Hooks.LeafletMap = LeafletMap; |
| 43 | 110 | Hooks.MapPush = { |
modified
apps/room_sanctum/assets/js/hooks/leaflet_map.js
+47
−3
@@ -371,6 +371,21 @@ const LeafletMap = {
| 371 | 371 | const popupContent = this.createPopupContent(query); |
| 372 | 372 | marker.bindPopup(popupContent); |
| 373 | 373 | |
| 374 | + // Add event listener for the duplicate query button after popup opens | |
| 375 | + marker.on('popupopen', (e) => { | |
| 376 | + const popup = e.popup; | |
| 377 | + const duplicateQueryBtn = popup.getElement().querySelector('.duplicate-query-btn'); | |
| 378 | + if (duplicateQueryBtn) { | |
| 379 | + duplicateQueryBtn.addEventListener('click', () => { | |
| 380 | + this.pushEvent('add-query-from-map', { | |
| 381 | + station_id: query.id.toString(), | |
| 382 | + name: query.name, | |
| 383 | + type: 'query' | |
| 384 | + }); | |
| 385 | + }); | |
| 386 | + } | |
| 387 | + }); | |
| 388 | + | |
| 374 | 389 | // Add click handler for query navigation |
| 375 | 390 | marker.on('click', () => { |
| 376 | 391 | // You can emit events back to LiveView if needed |
@@ -391,6 +406,21 @@ const LeafletMap = {
| 391 | 406 | const popupContent = this.createVehiclePopupContent(vehicle); |
| 392 | 407 | marker.bindPopup(popupContent); |
| 393 | 408 | |
| 409 | + // Add event listener for vehicle query button after popup opens (if it's a GTFS vehicle) | |
| 410 | + marker.on('popupopen', (e) => { | |
| 411 | + const popup = e.popup; | |
| 412 | + const addVehicleQueryBtn = popup.getElement().querySelector('.add-vehicle-query-btn'); | |
| 413 | + if (addVehicleQueryBtn) { | |
| 414 | + addVehicleQueryBtn.addEventListener('click', () => { | |
| 415 | + this.pushEvent('add-query-from-map', { | |
| 416 | + station_id: vehicle.vehicle_id, | |
| 417 | + name: `Vehicle ${vehicle.vehicle_id} Position Query`, | |
| 418 | + type: 'vehicle' | |
| 419 | + }); | |
| 420 | + }); | |
| 421 | + } | |
| 422 | + }); | |
| 423 | + | |
| 394 | 424 | return marker; |
| 395 | 425 | }, |
| 396 | 426 |
@@ -535,10 +565,22 @@ const LeafletMap = {
| 535 | 565 | </div> |
| 536 | 566 | ` : ''} |
| 537 | 567 | |
| 538 | − <div class="text-xs text-gray-500"> | |
| 568 | + <div class="text-xs text-gray-500 mb-2"> | |
| 539 | 569 | <i class="fa-solid fa-location-dot mr-1"></i> |
| 540 | 570 | ${vehicle.lat.toFixed(4)}, ${vehicle.lng.toFixed(4)} |
| 541 | 571 | </div> |
| 572 | + | |
| 573 | + <div class="flex justify-center mt-2"> | |
| 574 | + <button | |
| 575 | + class="add-vehicle-query-btn btn btn-sm btn-primary text-xs px-3 py-1 rounded-md bg-orange-600 hover:bg-orange-700 text-white transition-colors" | |
| 576 | + data-vehicle-id="${vehicle.vehicle_id}" | |
| 577 | + data-query-type="vehicle" | |
| 578 | + title="Create a vehicle position query for this vehicle" | |
| 579 | + > | |
| 580 | + <i class="fa-solid fa-plus mr-1"></i> | |
| 581 | + Add Vehicle Query | |
| 582 | + </button> | |
| 583 | + </div> | |
| 542 | 584 | </div> |
| 543 | 585 | `; |
| 544 | 586 | }, |
@@ -850,8 +892,10 @@ const LeafletMap = {
| 850 | 892 | |
| 851 | 893 | <div class="flex justify-center mt-2"> |
| 852 | 894 | <button |
| 853 | − class="btn btn-sm btn-primary text-xs px-3 py-1 rounded-md bg-blue-600 hover:bg-blue-700 text-white transition-colors" | |
| 854 | − onclick="window.liveSocket.execJS(this, [['push', {event: 'add-query-from-map', value: {station_id: '${query.id}', name: '${query.name.replace(/'/g, "\\'")}', type: 'query'}}]])" | |
| 895 | + class="duplicate-query-btn btn btn-sm btn-primary text-xs px-3 py-1 rounded-md bg-blue-600 hover:bg-blue-700 text-white transition-colors" | |
| 896 | + data-query-id="${query.id}" | |
| 897 | + data-query-name="${query.name.replace(/"/g, '"')}" | |
| 898 | + data-query-type="query" | |
| 855 | 899 | > |
| 856 | 900 | <i class="fa-solid fa-plus mr-1"></i> |
| 857 | 901 | Duplicate Query |
modified
apps/room_sanctum/assets/tailwind.config.js
+45
−0
@@ -29,8 +29,53 @@ module.exports = {
| 29 | 29 | "warning": "#fef08a", |
| 30 | 30 | "error": "#fca5a5", |
| 31 | 31 | }, |
| 32 | + forest: { | |
| 33 | + "primary": "#4ade80", | |
| 34 | + "secondary": "#86efac", | |
| 35 | + "accent": "#fef3c7", | |
| 36 | + "neutral": "#166534", | |
| 37 | + "base-100": "#052e16", | |
| 38 | + "info": "#7dd3fc", | |
| 39 | + "success": "#a7f3d0", | |
| 40 | + "warning": "#fef08a", | |
| 41 | + "error": "#fca5a5", | |
| 42 | + }, | |
| 43 | + sky: { | |
| 44 | + "primary": "#38bdf8", | |
| 45 | + "secondary": "#7dd3fc", | |
| 46 | + "accent": "#fef3c7", | |
| 47 | + "neutral": "#0c4a6e", | |
| 48 | + "base-100": "#082f49", | |
| 49 | + "info": "#7dd3fc", | |
| 50 | + "success": "#a7f3d0", | |
| 51 | + "warning": "#fef08a", | |
| 52 | + "error": "#fca5a5", | |
| 53 | + }, | |
| 54 | + clays: { | |
| 55 | + "primary": "#d97706", | |
| 56 | + "secondary": "#f59e0b", | |
| 57 | + "accent": "#fef3c7", | |
| 58 | + "neutral": "#92400e", | |
| 59 | + "base-100": "#451a03", | |
| 60 | + "info": "#7dd3fc", | |
| 61 | + "success": "#a7f3d0", | |
| 62 | + "warning": "#fef08a", | |
| 63 | + "error": "#fca5a5", | |
| 64 | + }, | |
| 65 | + stones: { | |
| 66 | + "primary": "#6b7280", | |
| 67 | + "secondary": "#9ca3af", | |
| 68 | + "accent": "#fef3c7", | |
| 69 | + "neutral": "#57534e", | |
| 70 | + "base-100": "#292524", | |
| 71 | + "info": "#7dd3fc", | |
| 72 | + "success": "#a7f3d0", | |
| 73 | + "warning": "#fef08a", | |
| 74 | + "error": "#fca5a5", | |
| 75 | + }, | |
| 32 | 76 | }, |
| 33 | 77 | "lofi", |
| 78 | + "black" | |
| 34 | 79 | ] |
| 35 | 80 | }, |
| 36 | 81 | content: [ |
modified
apps/room_sanctum/lib/room_sanctum_web/components/layouts/root.html.heex
+42
−1
@@ -1,5 +1,5 @@
| 1 | 1 | <!DOCTYPE html> |
| 2 | −<html lang="en" data-theme="her"> | |
| 2 | +<html lang="en"> | |
| 3 | 3 | <head> |
| 4 | 4 | <meta charset="utf-8"/> |
| 5 | 5 | <meta http-equiv="X-UA-Compatible" content="IE=edge"/> |
@@ -10,6 +10,47 @@
| 10 | 10 | "/assets/app.css")}/> |
| 11 | 11 | <script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, |
| 12 | 12 | "/assets/app.js")}></script> |
| 13 | + <script> | |
| 14 | + (() => { | |
| 15 | + const setTheme = (theme) => { | |
| 16 | + if (theme === "system") { | |
| 17 | + localStorage.removeItem("apollos:theme"); | |
| 18 | + // Remove theme attribute to let system preference take over | |
| 19 | + document.documentElement.removeAttribute("data-theme"); | |
| 20 | + // Apply system theme based on media query | |
| 21 | + const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'lofi'; | |
| 22 | + document.documentElement.setAttribute("data-theme", systemTheme); | |
| 23 | + } else { | |
| 24 | + localStorage.setItem("apollos:theme", theme); | |
| 25 | + document.documentElement.setAttribute("data-theme", theme); | |
| 26 | + } | |
| 27 | + }; | |
| 28 | + | |
| 29 | + // Apply saved theme or system preference on page load | |
| 30 | + if (!document.documentElement.hasAttribute("data-theme")) { | |
| 31 | + setTheme(localStorage.getItem("apollos:theme") || "system"); | |
| 32 | + } | |
| 33 | + | |
| 34 | + // Listen for cross-tab theme changes | |
| 35 | + window.addEventListener("storage", (e) => { | |
| 36 | + if (e.key === "apollos:theme") { | |
| 37 | + setTheme(e.newValue || "system"); | |
| 38 | + } | |
| 39 | + }); | |
| 40 | + | |
| 41 | + // Listen for theme change events from components | |
| 42 | + window.addEventListener("phx:set-theme", (e) => { | |
| 43 | + setTheme(e.target.dataset.phxTheme || e.detail.theme); | |
| 44 | + }); | |
| 45 | + | |
| 46 | + // Listen for system theme changes | |
| 47 | + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { | |
| 48 | + if (!localStorage.getItem("apollos:theme")) { | |
| 49 | + setTheme("system"); | |
| 50 | + } | |
| 51 | + }); | |
| 52 | + })(); | |
| 53 | + </script> | |
| 13 | 54 | </head> |
| 14 | 55 | <body> |
| 15 | 56 | <div class="p-2"> |
modified
apps/room_sanctum/lib/room_sanctum_web/components/query_geospatial_map.ex
+2
−2
@@ -115,7 +115,7 @@ defmodule RoomSanctumWeb.Components.QueryGeospatialMap do
| 115 | 115 | has_query_point = extract_from_query_data(query) != false |
| 116 | 116 | |
| 117 | 117 | result = has_geom || has_source_point || has_query_point |
| 118 | − IO.inspect({query.id, query.name, has_geom, has_source_point, has_query_point, result}, label: "Query mappability check") | |
| 118 | +# IO.inspect({query.id, query.name, has_geom, has_source_point, has_query_point, result}, label: "Query mappability check") | |
| 119 | 119 | result |
| 120 | 120 | end) |
| 121 | 121 |
@@ -150,7 +150,7 @@ defmodule RoomSanctumWeb.Components.QueryGeospatialMap do
| 150 | 150 | {lat, lng} = extract_coordinates(query) |
| 151 | 151 | |
| 152 | 152 | # Debug log the coordinate extraction |
| 153 | − IO.inspect({query.id, query.name, lat, lng}, label: "Map coordinates for query") | |
| 153 | +# IO.inspect({query.id, query.name, lat, lng}, label: "Map coordinates for query") | |
| 154 | 154 | |
| 155 | 155 | tint = cond do |
| 156 | 156 | query.meta && query.meta.tint -> query.meta.tint |
added
apps/room_sanctum/lib/room_sanctum_web/components/theme_picker.ex
+122
−0
@@ -0,0 +1,122 @@
| 1 | +defmodule RoomSanctumWeb.ThemePicker do | |
| 2 | + use Phoenix.Component | |
| 3 | + alias Phoenix.LiveView.JS | |
| 4 | + | |
| 5 | + @themes [ | |
| 6 | + %{name: "system", title: "System", description: "Follow system preference", | |
| 7 | + colors: %{primary: "#6b7280", secondary: "#9ca3af", accent: "#fef3c7", neutral: "#374151", base: "#f9fafb"}}, | |
| 8 | + %{name: "afterdark", title: "After Dark", description: "Dark purple theme", | |
| 9 | + colors: %{primary: "#7B79B5", secondary: "#ACABD5", accent: "#fef3c7", neutral: "#38357F", base: "#201D65"}}, | |
| 10 | + %{name: "black", title: "Black", description: "True black theme", | |
| 11 | + colors: %{primary: "#373737", secondary: "#a6adbb", accent: "#66cc8a", neutral: "#2a2e37", base: "#1d232a"}}, | |
| 12 | + %{name: "clays", title: "Clays", description: "Warm earth tones", | |
| 13 | + colors: %{primary: "#d97706", secondary: "#f59e0b", accent: "#fef3c7", neutral: "#92400e", base: "#451a03"}}, | |
| 14 | + %{name: "forest", title: "Forest", description: "Green nature theme", | |
| 15 | + colors: %{primary: "#4ade80", secondary: "#86efac", accent: "#fef3c7", neutral: "#166534", base: "#052e16"}}, | |
| 16 | + %{name: "her", title: "Her", description: "Orange/warm theme", | |
| 17 | + colors: %{primary: "#b57979", secondary: "#d5abab", accent: "#fef3c7", neutral: "#7f3535", base: "#651d1d"}}, | |
| 18 | + %{name: "lofi", title: "Lo-Fi", description: "Neutral minimalist", | |
| 19 | + colors: %{primary: "#0f0f23", secondary: "#a6adbb", accent: "#66cc8a", neutral: "#2a2e37", base: "#1d232a"}}, | |
| 20 | + %{name: "sky", title: "Sky", description: "Blue sky theme", | |
| 21 | + colors: %{primary: "#38bdf8", secondary: "#7dd3fc", accent: "#fef3c7", neutral: "#0c4a6e", base: "#082f49"}}, | |
| 22 | + %{name: "stones", title: "Stones", description: "Gray/stone theme", | |
| 23 | + colors: %{primary: "#6b7280", secondary: "#9ca3af", accent: "#fef3c7", neutral: "#57534e", base: "#292524"}} | |
| 24 | + ] | |
| 25 | + | |
| 26 | + @doc """ | |
| 27 | + Renders a theme picker with preview cards for each theme. | |
| 28 | + | |
| 29 | + The current theme is determined client-side and doesn't need to be passed as an attribute. | |
| 30 | + The component will automatically detect which theme is currently active. | |
| 31 | + | |
| 32 | + ## Examples | |
| 33 | + | |
| 34 | + <.theme_picker /> | |
| 35 | + """ | |
| 36 | + attr :class, :string, default: nil | |
| 37 | + attr :id, :string, default: "theme-picker" | |
| 38 | + | |
| 39 | + def theme_picker(assigns) do | |
| 40 | + themes = [ | |
| 41 | + %{name: "system", title: "System", description: "Follow system preference", | |
| 42 | + colors: %{primary: "#6b7280", secondary: "#9ca3af", accent: "#fef3c7", neutral: "#374151", base: "#f9fafb"}}, | |
| 43 | + %{name: "afterdark", title: "After Dark", description: "Dark purple theme", | |
| 44 | + colors: %{primary: "#7B79B5", secondary: "#ACABD5", accent: "#fef3c7", neutral: "#38357F", base: "#201D65"}}, | |
| 45 | + %{name: "black", title: "Black", description: "True black theme", | |
| 46 | + colors: %{primary: "#373737", secondary: "#a6adbb", accent: "#66cc8a", neutral: "#2a2e37", base: "#1d232a"}}, | |
| 47 | + %{name: "clays", title: "Clays", description: "Warm earth tones", | |
| 48 | + colors: %{primary: "#d97706", secondary: "#f59e0b", accent: "#fef3c7", neutral: "#92400e", base: "#451a03"}}, | |
| 49 | + %{name: "forest", title: "Forest", description: "Green nature theme", | |
| 50 | + colors: %{primary: "#4ade80", secondary: "#86efac", accent: "#fef3c7", neutral: "#166534", base: "#052e16"}}, | |
| 51 | + %{name: "her", title: "Her", description: "Orange/warm theme", | |
| 52 | + colors: %{primary: "#b57979", secondary: "#d5abab", accent: "#fef3c7", neutral: "#7f3535", base: "#651d1d"}}, | |
| 53 | + %{name: "lofi", title: "Lo-Fi", description: "Neutral minimalist", | |
| 54 | + colors: %{primary: "#0f0f23", secondary: "#a6adbb", accent: "#66cc8a", neutral: "#2a2e37", base: "#1d232a"}}, | |
| 55 | + %{name: "sky", title: "Sky", description: "Blue sky theme", | |
| 56 | + colors: %{primary: "#38bdf8", secondary: "#7dd3fc", accent: "#fef3c7", neutral: "#0c4a6e", base: "#082f49"}}, | |
| 57 | + %{name: "stones", title: "Stones", description: "Gray/stone theme", | |
| 58 | + colors: %{primary: "#6b7280", secondary: "#9ca3af", accent: "#fef3c7", neutral: "#57534e", base: "#292524"}} | |
| 59 | + ] | |
| 60 | + | |
| 61 | + assigns = assign(assigns, :themes, themes) | |
| 62 | + | |
| 63 | + ~H""" | |
| 64 | + <div class={["theme-picker", @class]} id={@id} phx-hook="ThemeDetector"> | |
| 65 | + <h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4"> | |
| 66 | + Choose Theme | |
| 67 | + </h3> | |
| 68 | + | |
| 69 | + <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4"> | |
| 70 | + <.theme_card | |
| 71 | + :for={theme <- @themes} | |
| 72 | + theme={theme} | |
| 73 | + /> | |
| 74 | + </div> | |
| 75 | + </div> | |
| 76 | + """ | |
| 77 | + end | |
| 78 | + | |
| 79 | + @doc """ | |
| 80 | + Renders a single theme preview card. | |
| 81 | + """ | |
| 82 | + attr :theme, :map, required: true | |
| 83 | + | |
| 84 | + def theme_card(assigns) do | |
| 85 | + ~H""" | |
| 86 | + <div class="theme-card relative rounded-lg border-2 cursor-pointer transition-all hover:scale-105 border-gray-200 dark:border-gray-700 hover:border-gray-300 overflow-hidden" | |
| 87 | + data-theme-name={@theme.name}> | |
| 88 | + | |
| 89 | + <!-- Theme indicator --> | |
| 90 | + <div class="absolute top-2 right-2 theme-indicator hidden z-10"> | |
| 91 | + <div class="w-3 h-3 bg-blue-500 rounded-full shadow-lg"></div> | |
| 92 | + </div> | |
| 93 | + | |
| 94 | + <!-- Theme preview with name overlay --> | |
| 95 | + <button | |
| 96 | + type="button" | |
| 97 | + phx-click={JS.dispatch("phx:set-theme")} | |
| 98 | + data-phx-theme={@theme.name} | |
| 99 | + class="w-full h-24 flex items-center justify-center text-center transition-all hover:brightness-110" | |
| 100 | + style={ | |
| 101 | + if @theme.name == "system" do | |
| 102 | + "background: linear-gradient(135deg, #{@theme.colors.base} 0%, #{@theme.colors.primary} 100%); color: #{@theme.colors.neutral};" | |
| 103 | + else | |
| 104 | + "background-color: #{@theme.colors.base}; color: #{if String.starts_with?(@theme.colors.base, "#0") or String.starts_with?(@theme.colors.base, "#1") or String.starts_with?(@theme.colors.base, "#2"), do: @theme.colors.accent, else: @theme.colors.neutral};" | |
| 105 | + end | |
| 106 | + } | |
| 107 | + > | |
| 108 | + <%= if @theme.name == "system" do %> | |
| 109 | + <div class="flex flex-col items-center"> | |
| 110 | + <svg class="w-6 h-6 mb-1 opacity-80" fill="currentColor" viewBox="0 0 20 20"> | |
| 111 | + <path fill-rule="evenodd" d="M3 5a2 2 0 012-2h10a2 2 0 012 2v8a2 2 0 01-2 2h-2.22l.123.489.804.804A1 1 0 0113 18H7a1 1 0 01-.707-1.707l.804-.804L7.22 15H5a2 2 0 01-2-2V5zm5.771 7H5V5h10v7H8.771z" clip-rule="evenodd" /> | |
| 112 | + </svg> | |
| 113 | + <span class="text-sm font-medium">{@theme.title}</span> | |
| 114 | + </div> | |
| 115 | + <% else %> | |
| 116 | + <span class="text-sm font-medium">{@theme.title}</span> | |
| 117 | + <% end %> | |
| 118 | + </button> | |
| 119 | + </div> | |
| 120 | + """ | |
| 121 | + end | |
| 122 | +end |
modified
apps/room_sanctum/lib/room_sanctum_web/live/cfg_source_live/index.ex
+1
−1
@@ -105,7 +105,7 @@ defmodule RoomSanctumWeb.SourceLive.Index do
| 105 | 105 | end |
| 106 | 106 | |
| 107 | 107 | def handle_event("set-tint", %{"tint"=> tint}, socket) do |
| 108 | − IO.inspect({"set-tint", tint, socket.assigns.tint}) | |
| 108 | +# IO.inspect({"set-tint", tint, socket.assigns.tint}) | |
| 109 | 109 | case socket.assigns.tint == tint do |
| 110 | 110 | true -> {:noreply, socket |> assign(:tint, nil) |> stream(:cfg_sources, list_cfg_sources(socket.assigns.current_user.id), reset: true)} |
| 111 | 111 | false -> {:noreply, socket |> assign(:tint, tint) |> stream(:cfg_sources, list_cfg_sources(socket.assigns.current_user.id, tint), reset: true)} |
modified
apps/room_sanctum/lib/room_sanctum_web/live/cfg_source_live/show.ex
+13
−2
@@ -167,7 +167,7 @@ defmodule RoomSanctumWeb.SourceLive.Show do
| 167 | 167 | |
| 168 | 168 | @impl true |
| 169 | 169 | def handle_event("do-stats", %{"type" => type, "id" => id}, socket) do |
| 170 | − IO.inspect({type, id}) | |
| 170 | +# IO.inspect({type, id}) | |
| 171 | 171 | |
| 172 | 172 | stats = |
| 173 | 173 | case type do |
@@ -278,6 +278,17 @@ defmodule RoomSanctumWeb.SourceLive.Show do
| 278 | 278 | public: true |
| 279 | 279 | }) |> IO.inspect |
| 280 | 280 | |
| 281 | + {:gtfs, "vehicle"} -> | |
| 282 | + # For GTFS vehicle position queries, create a vehicle tracking query | |
| 283 | + Configuration.create_query( | |
| 284 | + %{ | |
| 285 | + user_id: socket.assigns.current_user.id, | |
| 286 | + source_id: socket.assigns.source.id, | |
| 287 | + name: "#{name}", | |
| 288 | + query: %{"vehicle_id" => station_id, "__type__" => "gtfs_vehicle_positions"}, | |
| 289 | + public: true | |
| 290 | + }) |> IO.inspect | |
| 291 | + | |
| 281 | 292 | {_, "query"} -> |
| 282 | 293 | # For duplicating existing queries, find the original and copy it |
| 283 | 294 | original_query = Enum.find(socket.assigns.queries, &(&1.id == String.to_integer(station_id))) |
@@ -330,7 +341,7 @@ defmodule RoomSanctumWeb.SourceLive.Show do
| 330 | 341 | end |
| 331 | 342 | |
| 332 | 343 | def handle_event("set-tint", %{"tint"=> tint}, socket) do |
| 333 | − IO.inspect({"set-tint", tint, socket.assigns.tint}) | |
| 344 | +# IO.inspect({"set-tint", tint, socket.assigns.tint}) | |
| 334 | 345 | case socket.assigns.tint == tint do |
| 335 | 346 | true -> {:noreply, socket |> assign(:tint, nil)} |
| 336 | 347 | false -> {:noreply, socket |> assign(:tint, tint)} |
modified
apps/room_sanctum/lib/room_sanctum_web/live/live_auth.ex
+1
−1
@@ -12,7 +12,7 @@ defmodule RoomSanctum.UserLiveAuth do
| 12 | 12 | ) |
| 13 | 13 | |
| 14 | 14 | if socket.assigns.current_user == nil do |
| 15 | − {:halt, redirect(socket, to: "/users/login")} | |
| 15 | + {:halt, redirect(socket, to: "/users/log_in")} | |
| 16 | 16 | else |
| 17 | 17 | {:cont, socket} |
| 18 | 18 | end |
modified
apps/room_sanctum/lib/room_sanctum_web/live/user_settings_live.ex
+184
−65
@@ -2,78 +2,182 @@ defmodule RoomSanctumWeb.UserSettingsLive do
| 2 | 2 | use RoomSanctumWeb, :live_view |
| 3 | 3 | |
| 4 | 4 | alias RoomSanctum.Accounts |
| 5 | + alias RoomSanctumWeb.ThemePicker | |
| 5 | 6 | |
| 6 | 7 | def render(assigns) do |
| 7 | 8 | ~H""" |
| 8 | − <.header class="text-center"> | |
| 9 | − Account Settings | |
| 10 | − <:subtitle>Manage your account email address and password settings</:subtitle> | |
| 11 | − </.header> | |
| 12 | − | |
| 13 | − <div class="space-y-12 divide-y"> | |
| 14 | − <div> | |
| 15 | − <.simple_form | |
| 16 | − for={@email_form} | |
| 17 | − id="email_form" | |
| 18 | − phx-submit="update_email" | |
| 19 | − phx-change="validate_email" | |
| 20 | − > | |
| 21 | − <.input field={@email_form[:email]} type="email" label="Email" required /> | |
| 22 | − <.input | |
| 23 | − field={@email_form[:current_password]} | |
| 24 | − name="current_password" | |
| 25 | − id="current_password_for_email" | |
| 26 | − type="password" | |
| 27 | − label="Current password" | |
| 28 | − value={@email_form_current_password} | |
| 29 | − required | |
| 30 | − /> | |
| 31 | − <:actions> | |
| 32 | − <.button phx-disable-with="Changing...">Change Email</.button> | |
| 33 | − </:actions> | |
| 34 | − </.simple_form> | |
| 35 | − </div> | |
| 36 | − <div> | |
| 37 | − <.simple_form | |
| 38 | − for={@password_form} | |
| 39 | − id="password_form" | |
| 40 | − action={~p"/users/log_in?_action=password_updated"} | |
| 41 | − method="post" | |
| 42 | − phx-change="validate_password" | |
| 43 | − phx-submit="update_password" | |
| 44 | − phx-trigger-action={@trigger_submit} | |
| 45 | − > | |
| 46 | − <.input | |
| 47 | − field={@password_form[:email]} | |
| 48 | − type="hidden" | |
| 49 | − id="hidden_user_email" | |
| 50 | − value={@current_email} | |
| 51 | − /> | |
| 52 | − <.input field={@password_form[:password]} type="password" label="New password" required /> | |
| 53 | − <.input | |
| 54 | − field={@password_form[:password_confirmation]} | |
| 55 | − type="password" | |
| 56 | − label="Confirm new password" | |
| 57 | − /> | |
| 58 | − <.input | |
| 59 | − field={@password_form[:current_password]} | |
| 60 | − name="current_password" | |
| 61 | − type="password" | |
| 62 | − label="Current password" | |
| 63 | − id="current_password_for_password" | |
| 64 | − value={@current_password} | |
| 65 | − required | |
| 66 | − /> | |
| 67 | − <:actions> | |
| 68 | − <.button phx-disable-with="Changing...">Change Password</.button> | |
| 69 | − </:actions> | |
| 70 | − </.simple_form> | |
| 9 | + <div class="hero min-h-screen bg-base-200"> | |
| 10 | + <div class="hero-content w-full max-w-4xl"> | |
| 11 | + <div class="w-full"> | |
| 12 | + <div class="text-center mb-8"> | |
| 13 | + <h1 class="text-4xl font-bold text-base-content">Account Settings</h1> | |
| 14 | + <p class="text-base-content/70 mt-2">Manage your account preferences and security</p> | |
| 15 | + </div> | |
| 16 | + | |
| 17 | + <div class="space-y-8"> | |
| 18 | + <!-- Theme Picker Section --> | |
| 19 | + <div class="card bg-base-100 shadow-xl"> | |
| 20 | + <div class="card-body"> | |
| 21 | + <h2 class="card-title text-primary mb-4"> | |
| 22 | + <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| 23 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zM21 5H9M21 9H9M21 13H9M21 17H9"></path> | |
| 24 | + </svg> | |
| 25 | + Theme Preferences | |
| 26 | + </h2> | |
| 27 | + <ThemePicker.theme_picker /> | |
| 28 | + </div> | |
| 29 | + </div> | |
| 30 | + | |
| 31 | + <!-- Email Settings --> | |
| 32 | + <div class="card bg-base-100 shadow-xl"> | |
| 33 | + <div class="card-body"> | |
| 34 | + <h2 class="card-title text-primary mb-4"> | |
| 35 | + <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| 36 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207"></path> | |
| 37 | + </svg> | |
| 38 | + Email Address | |
| 39 | + </h2> | |
| 40 | + <p class="text-base-content/70 mb-4">Update your email address. You'll need to verify the new email address.</p> | |
| 41 | + | |
| 42 | + <.simple_form | |
| 43 | + for={@email_form} | |
| 44 | + id="email_form" | |
| 45 | + phx-submit="update_email" | |
| 46 | + phx-change="validate_email" | |
| 47 | + class="space-y-4" | |
| 48 | + > | |
| 49 | + <div class="form-control w-full"> | |
| 50 | + <label class="label"> | |
| 51 | + <span class="label-text font-medium">Email Address</span> | |
| 52 | + </label> | |
| 53 | + <.input | |
| 54 | + field={@email_form[:email]} | |
| 55 | + type="email" | |
| 56 | + required | |
| 57 | + class="input input-bordered w-full focus:input-primary" | |
| 58 | + placeholder="Enter your email address" | |
| 59 | + /> | |
| 60 | + </div> | |
| 61 | + | |
| 62 | + <div class="form-control w-full"> | |
| 63 | + <label class="label"> | |
| 64 | + <span class="label-text font-medium">Current Password</span> | |
| 65 | + <span class="label-text-alt text-base-content/50">Required for security</span> | |
| 66 | + </label> | |
| 67 | + <.input | |
| 68 | + field={@email_form[:current_password]} | |
| 69 | + name="current_password" | |
| 70 | + id="current_password_for_email" | |
| 71 | + type="password" | |
| 72 | + value={@email_form_current_password} | |
| 73 | + required | |
| 74 | + class="input input-bordered w-full focus:input-primary" | |
| 75 | + placeholder="Enter your current password" | |
| 76 | + /> | |
| 77 | + </div> | |
| 78 | + | |
| 79 | + <div class="card-actions justify-end"> | |
| 80 | + <.button phx-disable-with="Updating..." class="btn btn-primary"> | |
| 81 | + <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| 82 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path> | |
| 83 | + </svg> | |
| 84 | + Update Email | |
| 85 | + </.button> | |
| 86 | + </div> | |
| 87 | + </.simple_form> | |
| 88 | + </div> | |
| 89 | + </div> | |
| 90 | + | |
| 91 | + <!-- Password Settings --> | |
| 92 | + <div class="card bg-base-100 shadow-xl"> | |
| 93 | + <div class="card-body"> | |
| 94 | + <h2 class="card-title text-primary mb-4"> | |
| 95 | + <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| 96 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"></path> | |
| 97 | + </svg> | |
| 98 | + Password Security | |
| 99 | + </h2> | |
| 100 | + <p class="text-base-content/70 mb-4">Change your password to keep your account secure.</p> | |
| 101 | + | |
| 102 | + <.simple_form | |
| 103 | + for={@password_form} | |
| 104 | + id="password_form" | |
| 105 | + action={~p"/users/log_in?_action=password_updated"} | |
| 106 | + method="post" | |
| 107 | + phx-change="validate_password" | |
| 108 | + phx-submit="update_password" | |
| 109 | + phx-trigger-action={@trigger_submit} | |
| 110 | + class="space-y-4" | |
| 111 | + > | |
| 112 | + <.input | |
| 113 | + field={@password_form[:email]} | |
| 114 | + type="hidden" | |
| 115 | + id="hidden_user_email" | |
| 116 | + value={@current_email} | |
| 117 | + /> | |
| 118 | + | |
| 119 | + <div class="form-control w-full"> | |
| 120 | + <label class="label"> | |
| 121 | + <span class="label-text font-medium">New Password</span> | |
| 122 | + <span class="label-text-alt text-base-content/50">At least 8 characters</span> | |
| 123 | + </label> | |
| 124 | + <.input | |
| 125 | + field={@password_form[:password]} | |
| 126 | + type="password" | |
| 127 | + required | |
| 128 | + class="input input-bordered w-full focus:input-primary" | |
| 129 | + placeholder="Enter your new password" | |
| 130 | + /> | |
| 131 | + </div> | |
| 132 | + | |
| 133 | + <div class="form-control w-full"> | |
| 134 | + <label class="label"> | |
| 135 | + <span class="label-text font-medium">Confirm New Password</span> | |
| 136 | + </label> | |
| 137 | + <.input | |
| 138 | + field={@password_form[:password_confirmation]} | |
| 139 | + type="password" | |
| 140 | + class="input input-bordered w-full focus:input-primary" | |
| 141 | + placeholder="Confirm your new password" | |
| 142 | + /> | |
| 143 | + </div> | |
| 144 | + | |
| 145 | + <div class="form-control w-full"> | |
| 146 | + <label class="label"> | |
| 147 | + <span class="label-text font-medium">Current Password</span> | |
| 148 | + <span class="label-text-alt text-base-content/50">Required for security</span> | |
| 149 | + </label> | |
| 150 | + <.input | |
| 151 | + field={@password_form[:current_password]} | |
| 152 | + name="current_password" | |
| 153 | + type="password" | |
| 154 | + id="current_password_for_password" | |
| 155 | + value={@current_password} | |
| 156 | + required | |
| 157 | + class="input input-bordered w-full focus:input-primary" | |
| 158 | + placeholder="Enter your current password" | |
| 159 | + /> | |
| 160 | + </div> | |
| 161 | + | |
| 162 | + <div class="card-actions justify-end"> | |
| 163 | + <.button phx-disable-with="Updating..." class="btn btn-primary"> | |
| 164 | + <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| 165 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"></path> | |
| 166 | + </svg> | |
| 167 | + Change Password | |
| 168 | + </.button> | |
| 169 | + </div> | |
| 170 | + </.simple_form> | |
| 171 | + </div> | |
| 172 | + </div> | |
| 173 | + </div> | |
| 174 | + </div> | |
| 71 | 175 | </div> |
| 72 | 176 | </div> |
| 73 | 177 | """ |
| 74 | 178 | end |
| 75 | 179 | |
| 76 | − def mount(%{"token" => token}, _session, socket) do | |
| 180 | + def mount(%{"token" => token}, session, socket) do | |
| 77 | 181 | socket = |
| 78 | 182 | case Accounts.update_user_email(socket.assigns.current_user, token) do |
| 79 | 183 | :ok -> |
@@ -86,10 +190,11 @@ defmodule RoomSanctumWeb.UserSettingsLive do
| 86 | 190 | {:ok, push_navigate(socket, to: ~p"/users/settings")} |
| 87 | 191 | end |
| 88 | 192 | |
| 89 | − def mount(_params, _session, socket) do | |
| 193 | + def mount(_params, session, socket) do | |
| 90 | 194 | user = socket.assigns.current_user |
| 91 | 195 | email_changeset = Accounts.change_user_email(user) |
| 92 | 196 | password_changeset = Accounts.change_user_password(user) |
| 197 | + current_theme = get_current_theme(session) | |
| 93 | 198 | |
| 94 | 199 | socket = |
| 95 | 200 | socket |
@@ -99,10 +204,15 @@ defmodule RoomSanctumWeb.UserSettingsLive do
| 99 | 204 | |> assign(:email_form, to_form(email_changeset)) |
| 100 | 205 | |> assign(:password_form, to_form(password_changeset)) |
| 101 | 206 | |> assign(:trigger_submit, false) |
| 207 | + |> assign(:current_theme, current_theme) | |
| 102 | 208 | |
| 103 | 209 | {:ok, socket} |
| 104 | 210 | end |
| 105 | 211 | |
| 212 | + defp get_current_theme(session) do | |
| 213 | + Map.get(session, "theme", "lofi") | |
| 214 | + end | |
| 215 | + | |
| 106 | 216 | def handle_event("validate_email", params, socket) do |
| 107 | 217 | %{"current_password" => password, "user" => user_params} = params |
| 108 | 218 |
@@ -164,4 +274,13 @@ defmodule RoomSanctumWeb.UserSettingsLive do
| 164 | 274 | {:noreply, assign(socket, password_form: to_form(changeset))} |
| 165 | 275 | end |
| 166 | 276 | end |
| 277 | + | |
| 278 | + def handle_event("change_theme", %{"theme" => theme}, socket) do | |
| 279 | + socket = | |
| 280 | + socket | |
| 281 | + |> assign(:current_theme, theme) | |
| 282 | + |> put_flash(:info, "Theme updated successfully!") | |
| 283 | + | |
| 284 | + {:noreply, socket} | |
| 285 | + end | |
| 167 | 286 | end |
Parents: c32f492