diff --git a/assets/css/app.css b/assets/css/app.css index f5d96cd..ab5572e 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -167,6 +167,22 @@ html { text-underline-offset: 0.12em; } +.map-recenter-control { + display: grid !important; + place-items: center; + color: #1f2937; +} + +.map-recenter-icon { + width: 1.125rem; + height: 1.125rem; + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2; +} + .request-discovery-toolbar { display: flex; align-items: center; diff --git a/assets/js/hooks.js b/assets/js/hooks.js index a559be5..86de83b 100644 --- a/assets/js/hooks.js +++ b/assets/js/hooks.js @@ -147,6 +147,59 @@ const mapRetryButton = (element, retry) => { return button } +const mapRecenterControl = (element, recenter) => { + let container = null + let button = null + + const handleClick = event => { + event.preventDefault() + recenter() + } + + return { + onAdd() { + const label = element.dataset.mapRecenterLabel || "Show location on map" + const iconNamespace = "http://www.w3.org/2000/svg" + + container = document.createElement("div") + container.className = "maplibregl-ctrl maplibregl-ctrl-group" + + button = document.createElement("button") + button.type = "button" + button.className = "map-recenter-control" + button.dataset.mapRecenter = "true" + button.title = label + button.setAttribute("aria-label", label) + + const icon = document.createElementNS(iconNamespace, "svg") + icon.classList.add("map-recenter-icon") + icon.setAttribute("viewBox", "0 0 24 24") + icon.setAttribute("aria-hidden", "true") + + const circle = document.createElementNS(iconNamespace, "circle") + circle.setAttribute("cx", "12") + circle.setAttribute("cy", "12") + circle.setAttribute("r", "4.5") + + const crosshair = document.createElementNS(iconNamespace, "path") + crosshair.setAttribute("d", "M12 2v4m0 12v4M2 12h4m12 0h4") + + icon.append(circle, crosshair) + button.append(icon) + button.addEventListener("click", handleClick) + container.append(button) + + return container + }, + onRemove() { + button?.removeEventListener("click", handleClick) + container?.remove() + button = null + container = null + } + } +} + const reportMapUnavailable = (element, reason, error = null) => { element.dataset.mapUnavailable = "true" element.dataset.mapUnavailableReason = reason @@ -169,6 +222,8 @@ const createAidMap = element => { markers: [], markerElements: new Map(), initializedViewport: false, + locationBounds: null, + recenterControl: null, areaSourceId: `${element.id || "aid-map"}-areas`, areaFillId: `${element.id || "aid-map"}-area-fill`, areaLineId: `${element.id || "aid-map"}-area-line`, @@ -348,6 +403,18 @@ const createAidMap = element => { ) } + state.fitMarkers = (duration = 0) => { + if (!state.active || !state.map || !state.locationBounds) return false + + state.map.fitBounds(state.locationBounds, { + padding: 52, + maxZoom: 13, + duration + }) + + return true + } + state.renderMarkers = () => { if (!state.map) return @@ -361,6 +428,12 @@ const createAidMap = element => { ) const areaFeatures = areaFeatureCollection(points) const clusterFeatures = clusterFeatureCollection(points) + const bounds = new maplibregl.LngLatBounds() + + exactPoints.forEach(point => bounds.extend([point.longitude, point.latitude])) + areaFeatures.features.forEach(feature => extendBoundsWithFeature(bounds, feature)) + clusterFeatures.features.forEach(feature => bounds.extend(feature.geometry.coordinates)) + state.locationBounds = points.length > 0 ? bounds : null if (state.map.isStyleLoaded()) { const areaSource = state.map.getSource(state.areaSourceId) @@ -456,13 +529,7 @@ const createAidMap = element => { state.markers.push(marker) }) - if (points.length > 0 && state.element.dataset.autoFit !== "false") { - const bounds = new maplibregl.LngLatBounds() - exactPoints.forEach(point => bounds.extend([point.longitude, point.latitude])) - areaFeatures.features.forEach(feature => extendBoundsWithFeature(bounds, feature)) - clusterFeatures.features.forEach(feature => bounds.extend(feature.geometry.coordinates)) - state.map.fitBounds(bounds, {padding: 52, maxZoom: 13, duration: 0}) - } + if (state.element.dataset.autoFit !== "false") state.fitMarkers() } state.mount = () => { @@ -501,6 +568,11 @@ const createAidMap = element => { demoMap ? "bottom-right" : "top-right" ) + if (state.element.dataset.mapRecenterLabel) { + state.recenterControl = mapRecenterControl(state.element, () => state.fitMarkers(450)) + state.map.addControl(state.recenterControl, "top-right") + } + if (state.element.dataset.discoveryMap === "true" && navigator.geolocation) { state.map.addControl( new maplibregl.GeolocateControl({ @@ -572,6 +644,8 @@ const createAidMap = element => { window.removeEventListener("wnh:request-highlight", state.onRequestHighlight) state.map?.remove() state.map = null + state.locationBounds = null + state.recenterControl = null } return state diff --git a/lib/who_need_help_web/live/activity_live/index.ex b/lib/who_need_help_web/live/activity_live/index.ex index 87bcbaf..8c42afc 100644 --- a/lib/who_need_help_web/live/activity_live/index.ex +++ b/lib/who_need_help_web/live/activity_live/index.ex @@ -284,6 +284,7 @@ defmodule WhoNeedHelpWeb.ActivityLive.Index do gettext("The map is unavailable in this browser. Request details remain usable.") } data-map-retry-label={gettext("Try map again")} + data-map-recenter-label={gettext("Show location on map")} class="aid-map" /> diff --git a/lib/who_need_help_web/live/activity_live/show.ex b/lib/who_need_help_web/live/activity_live/show.ex index 86fc989..da97f55 100644 --- a/lib/who_need_help_web/live/activity_live/show.ex +++ b/lib/who_need_help_web/live/activity_live/show.ex @@ -540,6 +540,7 @@ defmodule WhoNeedHelpWeb.ActivityLive.Show do gettext("The map is unavailable in this browser. Request details remain usable.") } data-map-retry-label={gettext("Try map again")} + data-map-recenter-label={gettext("Show location on map")} class="aid-map" />
diff --git a/lib/who_need_help_web/live/request_live/show.ex b/lib/who_need_help_web/live/request_live/show.ex index 2a43023..c064636 100644 --- a/lib/who_need_help_web/live/request_live/show.ex +++ b/lib/who_need_help_web/live/request_live/show.ex @@ -1282,6 +1282,7 @@ defmodule WhoNeedHelpWeb.RequestLive.Show do gettext("The map is unavailable in this browser. Request details remain usable.") } data-map-retry-label={gettext("Try map again")} + data-map-recenter-label={gettext("Show location on map")} class="aid-map" />
Enum.find(&(&1.slug == "coffee-meetup")) {:ok, view, _html} = build_conn() |> log_in_user(viewer) |> live(~p"/activities") + assert has_element?(view, "#activity-map[data-map-recenter-label='Show location on map']") {:ok, _activity} = Activities.create_activity(Accounts.Scope.for_user(organizer), %{ @@ -661,6 +662,11 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do {:ok, activity} = Activities.create_activity(organizer_scope, attrs) {:ok, organizer_view, _html} = live(organizer_conn, ~p"/activities/#{activity.id}") {:ok, participant_view, html} = live(participant_conn, ~p"/activities/#{activity.id}") + assert has_element?( + participant_view, + "#activity-detail-map[data-map-recenter-label='Show location on map']" + ) + assert html =~ "Request to join" assert html =~ "Safety controls" assert html =~ "Block organizer" @@ -783,6 +789,7 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do {:ok, request} = Help.create_request(requester_scope, request_attrs(category)) {:ok, requester_view, _html} = live(requester_conn, ~p"/requests/#{request.id}") {:ok, helper_view, _html} = live(helper_conn, ~p"/requests/#{request.id}") + assert has_element?(requester_view, "#show-map[data-map-recenter-label='Show location on map']") helper_view |> element("button[phx-click='accept']")