- 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
131 lines
4.7 KiB
Elixir
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
|