198 lines
6.5 KiB
Elixir
198 lines
6.5 KiB
Elixir
defmodule MicrowavepropWeb.ContactLive.Index do
|
|
@moduledoc false
|
|
use MicrowavepropWeb, :live_view
|
|
|
|
alias Microwaveprop.Radio
|
|
|
|
@sortable_fields ~w(station1 station2 band mode distance_km qso_timestamp inserted_at)
|
|
@default_sort_by "inserted_at"
|
|
@default_sort_order "desc"
|
|
|
|
@impl true
|
|
def mount(_params, _session, socket) do
|
|
{:ok, socket}
|
|
end
|
|
|
|
@impl true
|
|
def handle_params(params, _uri, socket) do
|
|
page =
|
|
case Integer.parse(Map.get(params, "page", "1")) do
|
|
{n, _} -> max(n, 1)
|
|
:error -> 1
|
|
end
|
|
|
|
sort_by = validate_sort_field(Map.get(params, "sort_by", @default_sort_by))
|
|
sort_order = validate_sort_order(Map.get(params, "sort_order", @default_sort_order))
|
|
search = Map.get(params, "search", "")
|
|
|
|
result =
|
|
Radio.list_contacts(
|
|
page: page,
|
|
sort_by: String.to_existing_atom(sort_by),
|
|
sort_order: String.to_existing_atom(sort_order),
|
|
search: search
|
|
)
|
|
|
|
{:noreply,
|
|
assign(socket,
|
|
page_title: "Contacts",
|
|
page: result.page,
|
|
total_pages: result.total_pages,
|
|
total_entries: result.total_entries,
|
|
contacts: result.entries,
|
|
grouped_contacts: result.grouped_entries,
|
|
sort_by: sort_by,
|
|
sort_order: sort_order,
|
|
search: search
|
|
)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("search", %{"search" => search}, socket) do
|
|
{:noreply, push_patch(socket, to: ~p"/contacts?search=#{search}")}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("sort", %{"field" => field}, socket) do
|
|
field = validate_sort_field(field)
|
|
|
|
new_order =
|
|
if socket.assigns.sort_by == field && socket.assigns.sort_order == "asc",
|
|
do: "desc",
|
|
else: "asc"
|
|
|
|
params = %{sort_by: field, sort_order: new_order}
|
|
params = if socket.assigns.search == "", do: params, else: Map.put(params, :search, socket.assigns.search)
|
|
{:noreply, push_patch(socket, to: ~p"/contacts?#{params}")}
|
|
end
|
|
|
|
defp validate_sort_field(field) when field in @sortable_fields, do: field
|
|
defp validate_sort_field(_), do: @default_sort_by
|
|
|
|
defp validate_sort_order(order) when order in ~w(asc desc), do: order
|
|
defp validate_sort_order(_), do: @default_sort_order
|
|
|
|
@impl true
|
|
def render(assigns) do
|
|
~H"""
|
|
<Layouts.app flash={@flash} max_width="max-w-7xl">
|
|
<.header>
|
|
Contacts
|
|
<:subtitle>{@total_entries} contacts</:subtitle>
|
|
<:actions>
|
|
<.link navigate={~p"/submit"} class="btn btn-primary">
|
|
<.icon name="hero-plus" class="w-5 h-5" /> Submit Contact
|
|
</.link>
|
|
</:actions>
|
|
</.header>
|
|
|
|
<form phx-submit="search" class="mb-4">
|
|
<div class="flex items-center gap-2">
|
|
<input
|
|
name="search"
|
|
value={@search}
|
|
placeholder="Search by callsign..."
|
|
type="text"
|
|
class="input input-bordered input-sm w-64"
|
|
/>
|
|
<button type="submit" class="btn btn-sm btn-outline">Search</button>
|
|
<.link :if={@search != ""} patch={~p"/contacts"} class="btn btn-sm btn-ghost">Clear</.link>
|
|
</div>
|
|
</form>
|
|
|
|
<table class="table table-sm w-full">
|
|
<thead>
|
|
<tr>
|
|
<th class="w-8"></th>
|
|
<th>Station 1</th>
|
|
<th>Grid 1</th>
|
|
<th>Station 2</th>
|
|
<th>Grid 2</th>
|
|
<th>Band</th>
|
|
<th>Mode</th>
|
|
<th>Distance (km)</th>
|
|
<th>Submitted</th>
|
|
<th>Timestamp</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<%= for {primary, reciprocals} <- @grouped_contacts do %>
|
|
<tr class="h-1">
|
|
<td colspan="10"></td>
|
|
</tr>
|
|
<tr
|
|
class={[
|
|
"hover:bg-base-200 cursor-pointer",
|
|
reciprocals != [] && "bg-primary/5"
|
|
]}
|
|
phx-click={JS.navigate(~p"/contacts/#{primary.id}")}
|
|
>
|
|
<td class="w-8 text-center">
|
|
<span
|
|
:if={reciprocals != []}
|
|
class="badge badge-primary badge-xs"
|
|
title="Has reciprocal contact"
|
|
>
|
|
{length(reciprocals) + 1}
|
|
</span>
|
|
</td>
|
|
<td class="font-mono font-semibold">{primary.station1}</td>
|
|
<td>{primary.grid1 || "—"}</td>
|
|
<td class="font-mono font-semibold">{primary.station2}</td>
|
|
<td>{primary.grid2 || "—"}</td>
|
|
<td>{primary.band}</td>
|
|
<td>{primary.mode}</td>
|
|
<td>{primary.distance_km}</td>
|
|
<td>{if primary.user_submitted, do: "Yes", else: "—"}</td>
|
|
<td>{Calendar.strftime(primary.qso_timestamp, "%Y-%m-%d %H:%M")}</td>
|
|
</tr>
|
|
<%= for recip <- reciprocals do %>
|
|
<tr
|
|
class="hover:bg-base-200 cursor-pointer bg-primary/5 opacity-70 border-t-0"
|
|
phx-click={JS.navigate(~p"/contacts/#{recip.id}")}
|
|
>
|
|
<td class="w-8 text-center text-primary">↳</td>
|
|
<td class="font-mono">{recip.station1}</td>
|
|
<td>{recip.grid1 || "—"}</td>
|
|
<td class="font-mono">{recip.station2}</td>
|
|
<td>{recip.grid2 || "—"}</td>
|
|
<td>{recip.band}</td>
|
|
<td>{recip.mode}</td>
|
|
<td>{recip.distance_km}</td>
|
|
<td>{if recip.user_submitted, do: "Yes", else: "—"}</td>
|
|
<td>{Calendar.strftime(recip.qso_timestamp, "%Y-%m-%d %H:%M")}</td>
|
|
</tr>
|
|
<% end %>
|
|
<% end %>
|
|
</tbody>
|
|
</table>
|
|
|
|
<div class="flex items-center justify-between pt-4">
|
|
<.link
|
|
:if={@page > 1}
|
|
patch={
|
|
~p"/contacts?page=#{@page - 1}&sort_by=#{@sort_by}&sort_order=#{@sort_order}&search=#{@search}"
|
|
}
|
|
class="btn btn-sm btn-outline"
|
|
>
|
|
<.icon name="hero-chevron-left" class="w-4 h-4" /> Previous
|
|
</.link>
|
|
<span :if={@page <= 1} />
|
|
|
|
<span class="text-sm text-base-content/70">Page {@page} of {@total_pages}</span>
|
|
|
|
<.link
|
|
:if={@page < @total_pages}
|
|
patch={
|
|
~p"/contacts?page=#{@page + 1}&sort_by=#{@sort_by}&sort_order=#{@sort_order}&search=#{@search}"
|
|
}
|
|
class="btn btn-sm btn-outline"
|
|
>
|
|
Next <.icon name="hero-chevron-right" class="w-4 h-4" />
|
|
</.link>
|
|
<span :if={@page >= @total_pages} />
|
|
</div>
|
|
</Layouts.app>
|
|
"""
|
|
end
|
|
end
|