prop/lib/microwaveprop_web/live/submit_live/adif_upload_component.ex
Graham McIntire b4b8d4ec47
Some checks failed
Build base image / Build and push base image (push) Successful in 12s
Build and Push / Build CI test image (push) Successful in 14s
Build and Push / Build and Push Docker Image (push) Failing after 14m7s
simplify: DRY up shared changesets, context helpers, LiveView helpers, and structural extraction
- Create MaidenheadChangesetHelpers: consolidate grid validation, callsign
  normalization, lat/lon validation, grid/latlon derivation across 6 schemas
- Create ContextHelpers: shared fetch_owned with admin bypass, safe_enqueue
  for Oban workers, UUID casting to replace CastError rescues
- Extend LiveHelpers: add current_user/1 (removes 7 duplicate definitions),
  subscribe/2 (replaces 13 inline PubSub sites), assign_url_params/2
- Extract Propagation.ScoreStore (528 lines): separate file I/O and cache
  management from scoring logic, 13 defdelegate passthroughs
- Split SubmitLive (942->475 lines): extract CSV/ADIF upload rendering into
  3 function component modules (csv_upload, adif_upload, preview)
- Update 16 LiveViews to use shared helpers
2026-08-06 18:06:50 -05:00

131 lines
4.7 KiB
Elixir

defmodule MicrowavepropWeb.SubmitLive.AdifUploadComponent do
@moduledoc false
use MicrowavepropWeb, :html
alias Microwaveprop.Radio.AdifImport
@doc false
@spec adif_upload_form(map()) :: Phoenix.LiveView.Rendered.t()
def adif_upload_form(assigns) do
assigns = assign(assigns, mode_mapping: AdifImport.mode_mapping_reference())
~H"""
<div class="space-y-6">
<div class="alert text-sm">
<div>
<p class="mb-2">
Upload an ADIF (.adi / .adif) file exported from your logging program.
</p>
<ul class="list-disc list-outside pl-5 mt-2 space-y-1 text-base-content/60">
<li>
Required fields: <code>CALL</code>, <code>STATION_CALLSIGN</code>
(or <code>OPERATOR</code>), <code>GRIDSQUARE</code>, <code>MY_GRIDSQUARE</code>, <code>QSO_DATE</code>, <code>TIME_ON</code>,
and <code>FREQ</code>
or <code>BAND</code>
</li>
<li>Contacts on amateur bands from 50 MHz and up will be imported</li>
<li>HF contacts (below 50 MHz) are silently skipped</li>
<li>Frequencies are fuzzy-matched to the nearest amateur band</li>
<li>
Operator commentary from <code>NOTES</code>
is carried over (falling back to <code>COMMENT</code>)
</li>
</ul>
</div>
</div>
<div class="alert text-sm">
<div class="w-full">
<p class="font-semibold mb-2">Mode handling</p>
<p class="mb-2 text-base-content/70">
ADIF modes are normalized to the six modes we track. SUBMODE takes
precedence when present (e.g. <code>MODE=MFSK, SUBMODE=FT8</code> imports as FT8).
</p>
<table class="table table-xs mt-2">
<thead>
<tr>
<th>ADIF value</th>
<th>Stored as</th>
</tr>
</thead>
<tbody>
<tr :for={{adif, mapped} <- @mode_mapping}>
<td><code>{adif}</code></td>
<td><code>{mapped}</code></td>
</tr>
</tbody>
</table>
</div>
</div>
<form
id="adif-upload-form"
phx-submit="upload_adif"
phx-change="validate_adif"
class="space-y-4"
>
<div class="fieldset mb-2">
<label>
<span class="label mb-1">ADIF File</span>
<.live_file_input upload={@uploads.adif} class="file-input file-input-bordered w-full" />
</label>
</div>
<div :for={entry <- @uploads.adif.entries} class="space-y-1">
<div class="flex items-center justify-between text-xs tabular-nums">
<span class="truncate pr-2">{entry.client_name}</span>
<span class="flex items-center gap-2 shrink-0">
<span :if={uploading?(entry)}>{entry.progress}%</span>
<button
type="button"
class="btn btn-ghost btn-xs"
phx-click="cancel_adif_upload"
phx-value-ref={entry.ref}
aria-label="Cancel upload"
>
<.icon name="hero-x-mark" class="w-4 h-4" />
</button>
</span>
</div>
<progress
:if={uploading?(entry)}
class="progress progress-primary w-full"
value={entry.progress}
max="100"
></progress>
<div :for={err <- upload_errors(@uploads.adif, entry)} class="text-xs text-error">
{error_to_string(err)}
</div>
</div>
<div :for={err <- upload_errors(@uploads.adif)} class="text-xs text-error">
{error_to_string(err)}
</div>
<%= if @current_user do %>
<input type="hidden" name="submitter_email" value={@current_user.email} />
<% end %>
<label class="flex items-center gap-2 cursor-pointer mt-2">
<input type="checkbox" name="private" value="true" class="checkbox checkbox-sm" />
<span class="text-sm">Private — only visible to me and administrators</span>
</label>
<div class="mt-6">
<button type="submit" class="btn btn-primary btn-lg w-full sm:w-auto">
<.icon name="hero-arrow-up-tray" class="w-5 h-5" /> Upload Contacts
</button>
</div>
</form>
</div>
"""
end
defp error_to_string(:too_large), do: "File is too large"
defp error_to_string(:too_many_files), do: "Only one file allowed"
defp error_to_string(:not_accepted), do: "File type not accepted"
defp error_to_string(other), do: to_string(other)
defp uploading?(%{progress: progress}), do: progress > 0 and progress < 100
defp uploading?(_), do: false
end