Skip to content

Commit

Permalink
feat: add interactive stop to map for create shuttle stop and edit sh…
Browse files Browse the repository at this point in the history
…uttle stop (#999)

* feat: add map to stop page

* fix: add png files from leaflet dist to fix incorrect leaflet asset path

* feat: add stop marker on stop edit page

* feat: live stop_form with React map updates using phoenix_live_react

* fix: update stop_controller to use get for index page

* tests: add tests for stop_live

* fix: add @types packages for phoenix and phoenix_live_view

* fix: fix wss connect by adding check_origin config

* fix: fix markerIcon size on mobile sizes by adding iconSize and iconAnchor

* feat: fallback to longpoll in live socket if websocket has issues

* feat: add _header and _footer shared templates for app and live views
  • Loading branch information
meagharty authored Aug 19, 2024
1 parent d28f34b commit 270e17f
Show file tree
Hide file tree
Showing 37 changed files with 624 additions and 285 deletions.
1 change: 1 addition & 0 deletions assets/.eslintignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
webpack.config.js
src/ReactPhoenix.js
src/LiveReactPhoenix.js
src/socket.js
38 changes: 38 additions & 0 deletions assets/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
"leaflet": "^1.9.4",
"phoenix": "file:../deps/phoenix",
"phoenix_html": "file:../deps/phoenix_html",
"phoenix_live_react": "file:../deps/phoenix_live_react",
"phoenix_live_view": "file:../deps/phoenix_live_view",
"react": "^18.3.0",
"react-datepicker": "^6.2.0",
"react-dom": "^18.3.0",
Expand All @@ -31,6 +33,8 @@
"@testing-library/user-event": "^14.5.2",
"@types/jest": "^29.5.12",
"@types/leaflet": "^1.9.12",
"@types/phoenix": "^1.6.5",
"@types/phoenix_live_view": "^0.18.5",
"@types/react": "^18.3.0",
"@types/react-datepicker": "^6.2.0",
"@types/react-select": "^5.0.1",
Expand Down
82 changes: 82 additions & 0 deletions assets/src/LiveReactPhoenix.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React from "react"
import ReactDOM from "react-dom"

const render = function (
el,
target,
componentClass,
additionalProps = {},
previousProps = {}
) {
let props = el.dataset.liveReactProps
? JSON.parse(el.dataset.liveReactProps)
: {}
if (el.dataset.liveReactMerge) {
props = { ...previousProps, ...props, ...additionalProps }
} else {
props = { ...props, ...additionalProps }
}
const reactElement = React.createElement(componentClass, props)
ReactDOM.render(reactElement, target)
return props
}

const initLiveReactElement = function (el, additionalProps) {
const target = el.nextElementSibling
const componentClass = Array.prototype.reduce.call(
el.dataset.liveReactClass.split("."),
(acc, el) => {
return acc[el]
},
window
)
render(el, target, componentClass, additionalProps)
return { target: target, componentClass: componentClass }
}

const initLiveReact = function () {
const elements = document.querySelectorAll("[data-live-react-class]")
Array.prototype.forEach.call(elements, (el) => {
initLiveReactElement(el)
})
}

const LiveReact = {
mounted() {
const { el } = this
const pushEvent = this.pushEvent.bind(this)
const pushEventTo = this.pushEventTo && this.pushEventTo.bind(this)
const handleEvent = this.handleEvent && this.handleEvent.bind(this)
const { target, componentClass } = initLiveReactElement(el, { pushEvent })
const props = render(el, target, componentClass, {
pushEvent,
pushEventTo,
handleEvent,
})
if (el.dataset.liveReactMerge) this.props = props
Object.assign(this, { target, componentClass })
},

updated() {
const { el, target, componentClass } = this
const pushEvent = this.pushEvent.bind(this)
const pushEventTo = this.pushEventTo && this.pushEventTo.bind(this)
const handleEvent = this.handleEvent
const previousProps = this.props
const props = render(
el,
target,
componentClass,
{ pushEvent, pushEventTo },
previousProps
)
if (el.dataset.liveReactMerge) this.props = props
},

destroyed() {
const { target } = this
ReactDOM.unmountComponentAtNode(target)
},
}

export { LiveReact as default, initLiveReact, initLiveReactElement }
35 changes: 34 additions & 1 deletion assets/src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,51 @@
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
import LiveReact from "./LiveReactPhoenix"
// Establish Phoenix Socket and LiveView configuration.
import { Socket } from "phoenix"
import { LiveSocket } from "phoenix_live_view"

import ReactPhoenix from "./ReactPhoenix"
import DisruptionCalendar from "./components/DisruptionCalendar"
import DisruptionForm from "./components/DisruptionForm"
import ShapeViewMap from "./components/ShapeViewMap"
import StopViewMap from "./components/StopViewMap"

declare global {
interface Window {
liveSocket: LiveSocket
Components: {
[name: string]: (props: any) => JSX.Element
}
}
}

window.Components = { DisruptionCalendar, DisruptionForm, ShapeViewMap }
// https://github.com/fidr/phoenix_live_react
const hooks = { LiveReact }

const csrfToken = document
.querySelector("meta[name='csrf-token']")!
.getAttribute("content")
const liveSocket = new LiveSocket("/live", Socket, {
hooks,
longPollFallbackMs: location.host.startsWith("localhost") ? undefined : 2500,
params: { _csrf_token: csrfToken },
})

// connect if there are any LiveViews on the page
liveSocket.connect()

// expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
// >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket

window.Components = {
DisruptionCalendar,
DisruptionForm,
ShapeViewMap,
StopViewMap,
}

ReactPhoenix.init()
2 changes: 1 addition & 1 deletion assets/src/components/ShapeViewMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
TileLayer,
} from "react-leaflet"

type Shape = {
interface Shape {
name: string
coordinates: number[][]
}
Expand Down
44 changes: 44 additions & 0 deletions assets/src/components/StopViewMap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from "react"
import { LatLngExpression, icon } from "leaflet"
import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet"

type Stop = {
stop_name: string
stop_desc: string
stop_lat: number
stop_lon: number
}

const defaultCenter: LatLngExpression = [42.360718, -71.05891]

const markerIcon = icon({
iconUrl: "/images/marker-icon.png",
iconRetinaUrl: "/images/marker-icon-2x.png",
shadowUrl: "/images/marker-shadow.png",
iconSize: [25, 41],
iconAnchor: [12, 41],
})

const StopViewMap = ({ stop }: { stop?: Stop }) => {
return (
<MapContainer
data-testid="stop-view-map-container"
style={{ height: "800px" }}
center={defaultCenter}
zoom={13}
scrollWheelZoom={true}
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{stop && stop.stop_lat && stop.stop_lon && (
<Marker position={[stop.stop_lat, stop.stop_lon]} icon={markerIcon}>
<Popup>{stop.stop_name}</Popup>
</Marker>
)}
</MapContainer>
)
}

export default StopViewMap
13 changes: 13 additions & 0 deletions assets/tests/components/StopViewMap.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from "react"
import { render } from "@testing-library/react"
import StopViewMap from "../../src/components/StopViewMap"

describe("StopViewMap", () => {
test("renders", () => {
const { container } = render(<StopViewMap />)
expect(container.getElementsByClassName("leaflet-map-pane").length).toBe(1)
expect(
container.getElementsByClassName("leaflet-control-container").length
).toBe(1)
})
})
11 changes: 9 additions & 2 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ config :arrow,
config :arrow, ArrowWeb.Endpoint,
url: [host: "localhost"],
render_errors: [view: ArrowWeb.ErrorView, accepts: ~w(html json)],
pubsub_server: Arrow.PubSub
pubsub_server: Arrow.PubSub,
live_view: [signing_salt: "35DDvOCJ"]

config :esbuild,
version: "0.17.11",
Expand All @@ -61,7 +62,13 @@ config :esbuild,
#{if(Mix.env() == :test, do: "--define:__REACT_DEVTOOLS_GLOBAL_HOOK__={'isDisabled':true}")}
),
cd: Path.expand("../assets", __DIR__),
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
env: %{
"NODE_PATH" =>
Enum.join(
[Path.expand("../deps", __DIR__)],
":"
)
}
]

# Configure tailwind (the version is required)
Expand Down
5 changes: 5 additions & 0 deletions config/prod.exs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ import Config
config :arrow,
shape_storage_enabled?: true

config :arrow, :websocket_check_origin, [
"https://*.mbta.com",
"https://*.mbtace.com"
]

config :arrow, ArrowWeb.Endpoint,
http: [:inet6, port: 4000],
url: [host: "example.com", port: 80],
Expand Down
2 changes: 2 additions & 0 deletions lib/arrow/shuttle/stop.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ defmodule Arrow.Shuttle.Stop do
use Ecto.Schema
import Ecto.Changeset

@derive {Jason.Encoder, only: [:stop_name, :stop_desc, :stop_lat, :stop_lon]}

@type id :: integer
@type t :: %__MODULE__{
id: id,
Expand Down
23 changes: 22 additions & 1 deletion lib/arrow_web.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ defmodule ArrowWeb do
def router do
quote do
use Phoenix.Router
import Phoenix.LiveView.Router
import Plug.Conn
import Phoenix.Controller
end
Expand Down Expand Up @@ -86,6 +87,26 @@ defmodule ArrowWeb do
end
end

def live_view do
quote do
use Phoenix.LiveView,
layout: {ArrowWeb.LayoutView, :live}

unquote(html_helpers())

# Import the `live_react_component` helper
import PhoenixLiveReact
end
end

def live_component do
quote do
use Phoenix.LiveComponent

unquote(html_helpers())
end
end

def verified_routes do
quote do
use Phoenix.VerifiedRoutes,
Expand All @@ -96,7 +117,7 @@ defmodule ArrowWeb do
end

@doc """
When used, dispatch to the appropriate controller/view/etc.
When used, dispatch to the appropriate controller/live_view/etc.
"""
defmacro __using__(which) when is_atom(which) do
apply(__MODULE__, which, [])
Expand Down
2 changes: 1 addition & 1 deletion lib/arrow_web/components/core_components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ defmodule ArrowWeb.CoreComponents do

def simple_form(assigns) do
~H"""
<.form :let={f} for={@for} as={@as} {@rest}>
<.form :let={f} for={@for} as={@as} phx-change="validate" {@rest}>
<%= render_slot(@inner_block, f) %>
<hr class="light-hr">
<div :for={action <- @actions} class="d-flex justify-content-center">
Expand Down
Loading

0 comments on commit 270e17f

Please sign in to comment.