- CategoryLive.Index: browse categories with business counts - CategoryLive.Show: view businesses filtered by category - BusinessLive.Index: search/filter businesses with geolocation - BusinessLive.Show: detailed business profile with hours - BusinessLive.New: submission form for new businesses - BusinessLive.Map: interactive Leaflet.js map view - Seed data with sample Columbus, OH businesses 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
118 lines
3.6 KiB
Elixir
118 lines
3.6 KiB
Elixir
defmodule LocalspotWeb.BusinessLive.Map do
|
|
use LocalspotWeb, :live_view
|
|
|
|
alias Localspot.Businesses
|
|
|
|
@impl true
|
|
def mount(_params, _session, socket) do
|
|
businesses = Businesses.list_businesses()
|
|
|
|
{:ok,
|
|
socket
|
|
|> assign(:page_title, "Business Map")
|
|
|> assign(:businesses, businesses)
|
|
|> assign(:selected_business, nil)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("select_business", %{"slug" => slug}, socket) do
|
|
business = Enum.find(socket.assigns.businesses, &(&1.slug == slug))
|
|
{:noreply, assign(socket, :selected_business, business)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("close_popup", _params, socket) do
|
|
{:noreply, assign(socket, :selected_business, nil)}
|
|
end
|
|
|
|
@impl true
|
|
def render(assigns) do
|
|
~H"""
|
|
<Layouts.app flash={@flash}>
|
|
<div class="flex items-center justify-between mb-4">
|
|
<.header>
|
|
Business Map
|
|
<:subtitle>View all businesses on an interactive map</:subtitle>
|
|
</.header>
|
|
|
|
<.link navigate={~p"/businesses"} class="btn btn-ghost btn-sm">
|
|
<.icon name="hero-list-bullet" class="w-4 h-4" /> List View
|
|
</.link>
|
|
</div>
|
|
|
|
<div
|
|
id="map-container"
|
|
class="w-full h-[600px] rounded-lg overflow-hidden relative"
|
|
phx-hook="LeafletMap"
|
|
data-businesses={Jason.encode!(map_businesses(@businesses))}
|
|
data-center={Jason.encode!(%{lat: 39.9612, lng: -82.9988})}
|
|
data-zoom="12"
|
|
>
|
|
<div class="absolute inset-0 bg-base-300 flex items-center justify-center">
|
|
<span class="loading loading-spinner loading-lg"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div :if={@selected_business} class="mt-4">
|
|
<div class="card bg-base-200">
|
|
<div class="card-body">
|
|
<div class="flex items-start justify-between">
|
|
<div>
|
|
<h2 class="card-title">
|
|
{@selected_business.name}
|
|
<span :if={@selected_business.locally_owned} class="badge badge-success badge-sm">
|
|
Local
|
|
</span>
|
|
</h2>
|
|
<p class="text-base-content/60">{@selected_business.category.name}</p>
|
|
</div>
|
|
<button class="btn btn-ghost btn-sm" phx-click="close_popup">
|
|
<.icon name="hero-x-mark" class="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
|
|
<p :if={@selected_business.description} class="mt-2">
|
|
{@selected_business.description}
|
|
</p>
|
|
|
|
<div class="flex items-center gap-2 text-sm text-base-content/60 mt-2">
|
|
<.icon name="hero-map-pin" class="w-4 h-4" />
|
|
<span>
|
|
{@selected_business.street_address}, {@selected_business.city}, {@selected_business.state}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="card-actions justify-end mt-4">
|
|
<.link
|
|
navigate={~p"/businesses/#{@selected_business.slug}"}
|
|
class="btn btn-primary btn-sm"
|
|
>
|
|
View Details
|
|
</.link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4 text-sm text-base-content/60">
|
|
<p>Showing {length(@businesses)} businesses on the map.</p>
|
|
</div>
|
|
</Layouts.app>
|
|
"""
|
|
end
|
|
|
|
defp map_businesses(businesses) do
|
|
businesses
|
|
|> Enum.filter(&(&1.latitude && &1.longitude))
|
|
|> Enum.map(fn b ->
|
|
%{
|
|
slug: b.slug,
|
|
name: b.name,
|
|
lat: Decimal.to_float(b.latitude),
|
|
lng: Decimal.to_float(b.longitude),
|
|
category: b.category.name,
|
|
locally_owned: b.locally_owned
|
|
}
|
|
end)
|
|
end
|
|
end
|