- 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>
167 lines
5.7 KiB
Elixir
167 lines
5.7 KiB
Elixir
defmodule LocalspotWeb.BusinessLive.Show do
|
|
use LocalspotWeb, :live_view
|
|
|
|
alias Localspot.Businesses
|
|
|
|
@impl true
|
|
def mount(%{"slug" => slug}, _session, socket) do
|
|
case Businesses.get_business_by_slug(slug) do
|
|
nil ->
|
|
{:ok,
|
|
socket
|
|
|> put_flash(:error, "Business not found")
|
|
|> push_navigate(to: ~p"/businesses")}
|
|
|
|
business ->
|
|
{:ok,
|
|
socket
|
|
|> assign(:page_title, business.name)
|
|
|> assign(:business, business)
|
|
|> assign(:is_open, Businesses.currently_open?(business.hours))}
|
|
end
|
|
end
|
|
|
|
@impl true
|
|
def render(assigns) do
|
|
~H"""
|
|
<Layouts.app flash={@flash}>
|
|
<div class="max-w-4xl mx-auto">
|
|
<%!-- Back link --%>
|
|
<.link navigate={~p"/businesses"} class="btn btn-ghost btn-sm mb-4">
|
|
<.icon name="hero-arrow-left" class="w-4 h-4" /> Back to Directory
|
|
</.link>
|
|
|
|
<%!-- Photo gallery --%>
|
|
<div
|
|
:if={@business.photos != []}
|
|
class="carousel w-full rounded-lg mb-6 h-64 md:h-96 bg-base-300"
|
|
>
|
|
<div
|
|
:for={{photo, idx} <- Enum.with_index(@business.photos)}
|
|
id={"photo-#{idx}"}
|
|
class="carousel-item w-full"
|
|
>
|
|
<img
|
|
src={photo.url}
|
|
alt={photo.alt_text || "#{@business.name} photo #{idx + 1}"}
|
|
class="w-full object-cover"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<%!-- Business header --%>
|
|
<div class="flex flex-col md:flex-row md:items-start md:justify-between gap-4 mb-6">
|
|
<div>
|
|
<h1 class="text-3xl font-bold flex items-center gap-3 flex-wrap">
|
|
{@business.name}
|
|
<span
|
|
:if={@business.locally_owned}
|
|
class="badge badge-success"
|
|
title="Locally Owned"
|
|
>
|
|
Locally Owned
|
|
</span>
|
|
</h1>
|
|
<.link
|
|
navigate={~p"/categories/#{@business.category.slug}"}
|
|
class="text-base-content/60 hover:underline"
|
|
>
|
|
{@business.category.name}
|
|
</.link>
|
|
</div>
|
|
|
|
<div class={[
|
|
"badge badge-lg",
|
|
if(@is_open, do: "badge-success", else: "badge-error")
|
|
]}>
|
|
{if @is_open, do: "Open Now", else: "Closed"}
|
|
</div>
|
|
</div>
|
|
|
|
<%!-- Description --%>
|
|
<p :if={@business.description} class="text-lg mb-6">{@business.description}</p>
|
|
|
|
<%!-- Local ownership info --%>
|
|
<div :if={@business.locally_owned} class="alert alert-success mb-6">
|
|
<.icon name="hero-check-badge" class="w-6 h-6" />
|
|
<div>
|
|
<h3 class="font-bold">Locally Owned Business</h3>
|
|
<p class="text-sm">
|
|
This business has confirmed they are locally owned and operated, not a chain or franchise.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid md:grid-cols-2 gap-6">
|
|
<%!-- Contact info --%>
|
|
<div class="card bg-base-200">
|
|
<div class="card-body">
|
|
<h2 class="card-title">Contact Information</h2>
|
|
|
|
<div class="space-y-3">
|
|
<div class="flex items-start gap-3">
|
|
<.icon name="hero-map-pin" class="w-5 h-5 mt-0.5 text-primary" />
|
|
<div>
|
|
<p>{@business.street_address}</p>
|
|
<p>{@business.city}, {@business.state} {@business.zip_code}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div :if={@business.phone} class="flex items-center gap-3">
|
|
<.icon name="hero-phone" class="w-5 h-5 text-primary" />
|
|
<a href={"tel:#{@business.phone}"} class="link link-hover">
|
|
{Businesses.format_phone(@business.phone)}
|
|
</a>
|
|
</div>
|
|
|
|
<div :if={@business.email} class="flex items-center gap-3">
|
|
<.icon name="hero-envelope" class="w-5 h-5 text-primary" />
|
|
<a href={"mailto:#{@business.email}"} class="link link-hover">
|
|
{@business.email}
|
|
</a>
|
|
</div>
|
|
|
|
<div :if={@business.website} class="flex items-center gap-3">
|
|
<.icon name="hero-globe-alt" class="w-5 h-5 text-primary" />
|
|
<a href={@business.website} target="_blank" rel="noopener" class="link link-hover">
|
|
Visit Website
|
|
<.icon name="hero-arrow-top-right-on-square" class="w-4 h-4 inline" />
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<%!-- Hours --%>
|
|
<div class="card bg-base-200">
|
|
<div class="card-body">
|
|
<h2 class="card-title">Business Hours</h2>
|
|
|
|
<%= if @business.hours == [] do %>
|
|
<p class="text-base-content/60">Hours not available</p>
|
|
<% else %>
|
|
<div class="space-y-2">
|
|
<div
|
|
:for={hour <- Enum.sort_by(@business.hours, & &1.day_of_week)}
|
|
class="flex justify-between"
|
|
>
|
|
<span class="font-medium">{Businesses.day_name(hour.day_of_week)}</span>
|
|
<span :if={hour.closed} class="text-base-content/60">Closed</span>
|
|
<span :if={!hour.closed && hour.opens_at && hour.closes_at}>
|
|
{format_time(hour.opens_at)} - {format_time(hour.closes_at)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Layouts.app>
|
|
"""
|
|
end
|
|
|
|
defp format_time(time) do
|
|
Calendar.strftime(time, "%I:%M %p")
|
|
end
|
|
end
|