- 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
112 lines
4.5 KiB
Elixir
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
|