defmodule LocalspotWeb.AdminLive.Import do use LocalspotWeb, :live_view alias Localspot.Businesses.Import @json_example """ { "businesses": [ { "name": "Business Name", "category": "category-slug", "street_address": "123 Main St", "city": "Columbus", "state": "OH", "zip_code": "43215", "hours": [{"day": 1, "opens_at": "09:00", "closes_at": "17:00"}], "photos": [{"url": "https://...", "primary": true}] } ] } """ @impl true def mount(_params, _session, socket) do {:ok, socket |> assign(:page_title, "Import Businesses") |> assign(:uploaded_files, []) |> assign(:import_result, nil) |> assign(:importing, false) |> assign(:json_example, @json_example) |> allow_upload(:import_file, accept: ~w(.json), max_entries: 1, max_file_size: 10_000_000 )} end @impl true def handle_event("validate", _params, socket) do {:noreply, socket} end @impl true def handle_event("import", _params, socket) do socket = assign(socket, :importing, true) result = consume_uploaded_entries(socket, :import_file, fn %{path: path}, _entry -> {:ok, Import.from_file(path)} end) import_result = case result do [{:ok, result}] -> result [{:error, reason}] -> {:error, reason} [] -> {:error, :no_file} end {:noreply, socket |> assign(:import_result, import_result) |> assign(:importing, false)} end @impl true def handle_event("clear_result", _params, socket) do {:noreply, assign(socket, :import_result, nil)} end @impl true def render(assigns) do ~H""" <.header> Import Businesses <:subtitle>Upload a JSON file to bulk import businesses
<.icon name="hero-information-circle" class="w-5 h-5" />

Upload a JSON file with the following structure:

{@json_example}
<.form for={%{}} phx-change="validate" phx-submit="import" class="space-y-4">
<.live_file_input upload={@uploads.import_file} class="hidden" />
<.icon name="hero-document-arrow-up" class="w-12 h-12 mx-auto text-base-content/40" />

Drag and drop a JSON file here, or

Max file size: 10MB

<.icon name="hero-document" class="w-6 h-6 text-primary" /> {entry.client_name} ({format_bytes(entry.client_size)})
{error_to_string(err)}
<.icon name="hero-check-circle" class="w-5 h-5" /> Successfully imported {@import_result.imported} business(es)
0} class="alert alert-info"> <.icon name="hero-information-circle" class="w-5 h-5" /> Skipped {@import_result.skipped} duplicate(s)
0} class="alert alert-warning"> <.icon name="hero-exclamation-triangle" class="w-5 h-5" />

{length(@import_result.errors)} error(s) occurred:

  • Row {index}: {inspect(reason)}
<.link navigate={~p"/businesses"} class="btn btn-primary"> View Businesses
<.icon name="hero-x-circle" class="w-5 h-5" /> Import failed: {format_error(@import_result)}
""" end defp format_bytes(bytes) when bytes < 1024, do: "#{bytes} B" defp format_bytes(bytes) when bytes < 1_048_576, do: "#{Float.round(bytes / 1024, 1)} KB" defp format_bytes(bytes), do: "#{Float.round(bytes / 1_048_576, 1)} MB" defp error_to_string(:too_large), do: "File is too large (max 10MB)" defp error_to_string(:not_accepted), do: "Only JSON files are accepted" defp error_to_string(:too_many_files), do: "Only one file at a time" defp error_to_string(err), do: "Error: #{inspect(err)}" defp format_error({:error, {:file_error, reason}}), do: "Could not read file: #{inspect(reason)}" defp format_error({:error, {:json_error, _}}), do: "Invalid JSON format" defp format_error({:error, {:invalid_format, msg}}), do: msg defp format_error({:error, :no_file}), do: "No file selected" defp format_error({:error, reason}), do: inspect(reason) end