prop/lib/microwaveprop_web/live/submit_live/csv_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

112 lines
4.5 KiB
Elixir

defmodule MicrowavepropWeb.SubmitLive.CsvUploadComponent do
@moduledoc false
use MicrowavepropWeb, :html
@doc false
@spec csv_upload_form(map()) :: Phoenix.LiveView.Rendered.t()
def csv_upload_form(assigns) do
~H"""
<div class="space-y-6">
<div class="alert text-sm">
<div>
<p class="mb-2">Upload a CSV file with multiple contacts. Columns:</p>
<code class="text-xs">
station1, station2, grid1, grid2, band, mode, qso_timestamp, notes
</code>
<ul class="list-disc list-outside pl-5 mt-2 space-y-1 text-base-content/60">
<li>
Timestamps in most formats accepted (e.g. <code>2024-06-15T14:30:00Z</code>, <code>6/15/2024 2:30 PM</code>, <code>2024-06-15 14:30</code>). All times assumed UTC.
</li>
<li>
Grid squares should be as detailed as possible (8 characters preferred, e.g. EM12kp37)
</li>
<li>Band in MHz (e.g. 10000, 24000)</li>
<li>
Mode is <strong>optional</strong> — omit the <code>mode</code> column entirely
(6 columns total) or leave its value blank.
</li>
<li>
Notes is <strong>optional</strong> — free-form operator commentary up to
2000 characters. Leave the cell blank to store NULL.
</li>
</ul>
<p class="mt-2">
<a href="/downloads/sample_contacts.csv" download class="link link-primary">
<.icon name="hero-arrow-down-tray" class="w-4 h-4" /> Download sample CSV
</a>
</p>
</div>
</div>
<form id="csv-upload-form" phx-submit="upload_csv" phx-change="validate_csv" class="space-y-4">
<div class="fieldset mb-2">
<label>
<span class="label mb-1">CSV File</span>
<.live_file_input upload={@uploads.csv} class="file-input file-input-bordered w-full" />
</label>
</div>
<div :for={entry <- @uploads.csv.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_csv_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.csv, entry)} class="text-xs text-error">
{error_to_string(err)}
</div>
</div>
<div :for={err <- upload_errors(@uploads.csv)} 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)
# An upload entry is actively streaming when its progress is partway
# between 0 and 100. With `auto_upload: false` entries sit at
# `progress: 0` after the user picks a file and only start streaming
# when the form is submitted, so gating the progress bar and percent
# text on the 0 < progress < 100 window keeps them hidden until the
# upload is actually in flight.
defp uploading?(%{progress: progress}), do: progress > 0 and progress < 100
defp uploading?(_), do: false
end