defmodule BerrypodWeb.Admin.Navigation do use BerrypodWeb, :live_view alias Berrypod.{Pages, Products, Settings} alias BerrypodWeb.ThemeHook # System pages that are always available @system_pages [ %{href: "/", label: "Home"}, %{href: "/about", label: "About"}, %{href: "/contact", label: "Contact"}, %{href: "/cart", label: "Cart"}, %{href: "/search", label: "Search"}, %{href: "/delivery", label: "Delivery"}, %{href: "/privacy", label: "Privacy policy"}, %{href: "/terms", label: "Terms & conditions"} ] @impl true def mount(_params, _session, socket) do header_items = load_nav("header_nav", ThemeHook.default_header_nav()) footer_items = load_nav("footer_nav", ThemeHook.default_footer_nav()) {:ok, socket |> assign(:page_title, "Navigation") |> assign(:header_items, header_items) |> assign(:footer_items, footer_items) |> assign(:dirty, false) |> assign(:save_status, :idle) |> assign(:save_error, nil) |> assign_link_options()} end defp assign_link_options(socket) do custom_pages = Pages.list_custom_pages() categories = Products.list_categories() # Build grouped link options link_options = [ {"Pages", Enum.map(@system_pages, &{&1.label, &1.href})}, {"Custom pages", Enum.map(custom_pages, fn p -> {p.title, "/#{p.slug}"} end)}, {"Collections", Enum.map(categories, fn c -> {c.name, "/collections/#{c.slug}"} end)}, {"Other", [{"External URL", "external"}]} ] # Filter out empty groups link_options = Enum.reject(link_options, fn {_label, opts} -> opts == [] end) socket |> assign(:link_options, link_options) |> assign(:custom_pages, custom_pages) |> assign(:categories, categories) end # ── Item manipulation ────────────────────────────────────────── @impl true def handle_event("nav_item_change", params, socket) do section = params["section"] index = String.to_integer(params["index"]) label = params["label"] dest = params["dest"] external_url = params["external_url"] items = get_items(socket, section) item = Enum.at(items, index) if item do updated = cond do # User selected "External URL" dest == "external" -> item |> Map.put("label", label) |> Map.put("href", external_url || "") |> Map.put("external", true) # User selected an internal link dest != nil && dest != "" -> # Auto-fill label if it was empty and user just selected a destination auto_label = if label == "" || label == item["label"], do: label_for_href(dest, socket), else: label item |> Map.put("label", if(label == "", do: auto_label, else: label)) |> Map.put("href", dest) |> Map.delete("external") # Just updating the label true -> Map.put(item, "label", label) end items = List.replace_at(items, index, updated) {:noreply, put_items(socket, section, items) |> clear_save_status()} else {:noreply, socket} end end def handle_event("remove_item", %{"section" => section, "index" => index_str}, socket) do index = String.to_integer(index_str) items = get_items(socket, section) |> List.delete_at(index) {:noreply, put_items(socket, section, items) |> clear_save_status()} end def handle_event( "move_item", %{"section" => section, "index" => index_str, "dir" => dir}, socket ) do index = String.to_integer(index_str) items = get_items(socket, section) target = if dir == "up", do: index - 1, else: index + 1 if target >= 0 and target < length(items) do item = Enum.at(items, index) items = items |> List.delete_at(index) |> List.insert_at(target, item) {:noreply, put_items(socket, section, items) |> clear_save_status()} else {:noreply, socket} end end def handle_event("add_item", %{"section" => section}, socket) do # Add a new empty item - user will pick destination from dropdown items = get_items(socket, section) ++ [%{"label" => "", "href" => ""}] {:noreply, put_items(socket, section, items) |> clear_save_status()} end def handle_event("save", _params, socket) do all_items = socket.assigns.header_items ++ socket.assigns.footer_items errors = validate_nav_items(all_items) if errors == [] do Settings.put_setting("header_nav", socket.assigns.header_items, "json") Settings.put_setting("footer_nav", socket.assigns.footer_items, "json") {:noreply, socket |> assign(:dirty, false) |> assign(:save_status, :saved) |> assign(:save_error, nil)} else {:noreply, socket |> assign(:save_status, :error) |> assign(:save_error, Enum.join(errors, ". "))} end end def handle_event("reset_defaults", _params, socket) do {:noreply, socket |> assign(:header_items, ThemeHook.default_header_nav()) |> assign(:footer_items, ThemeHook.default_footer_nav()) |> assign(:dirty, true) |> clear_save_status()} end # ── Private helpers for events ─────────────────────────────────── defp label_for_href(href, socket) do # Check system pages case Enum.find(@system_pages, &(&1.href == href)) do %{label: label} -> label nil -> # Check custom pages case Enum.find(socket.assigns.custom_pages, &("/#{&1.slug}" == href)) do %{title: title} -> title nil -> # Check collections case Enum.find(socket.assigns.categories, &("/collections/#{&1.slug}" == href)) do %{name: name} -> name nil -> "" end end end end defp validate_nav_items(items) do items |> Enum.with_index() |> Enum.flat_map(fn {item, _idx} -> cond do # Empty label item["label"] == "" || item["label"] == nil -> ["All links need a label"] # Empty href item["href"] == "" || item["href"] == nil -> ["\"#{item["label"]}\" needs a destination"] # External URL validation item["external"] == true || is_external_url?(item["href"]) -> if valid_url?(item["href"]) do [] else ["\"#{item["label"]}\" has an invalid URL"] end true -> [] end end) |> Enum.uniq() end defp valid_url?(url) when is_binary(url) do case URI.parse(url) do %URI{scheme: scheme, host: host} when scheme in ["http", "https"] and is_binary(host) and host != "" -> true _ -> false end end defp valid_url?(_), do: false defp clear_save_status(socket) do socket |> assign(:save_status, :idle) |> assign(:save_error, nil) end # ── Render ───────────────────────────────────────────────────── @impl true def render(assigns) do ~H""" """ end defp nav_section(assigns) do ~H"""

{@title}

<.nav_item :for={{item, idx} <- Enum.with_index(@items)} item={item} idx={idx} section={@section} total={length(@items)} link_options={@link_options} />
No items yet.
""" end defp nav_item(assigns) do # Determine if this is an external link is_external = assigns.item["external"] == true || is_external_url?(assigns.item["href"]) selected_value = if is_external, do: "external", else: assigns.item["href"] # Check if external URL is valid (only if there's a value) url_invalid = is_external && assigns.item["href"] != "" && assigns.item["href"] != nil && !valid_url?(assigns.item["href"]) assigns = assigns |> assign(:is_external, is_external) |> assign(:selected_value, selected_value) |> assign(:url_invalid, url_invalid) ~H""" """ end defp is_external_url?(nil), do: false defp is_external_url?(""), do: false defp is_external_url?(href) do String.starts_with?(href, "http://") || String.starts_with?(href, "https://") end # ── Helpers ──────────────────────────────────────────────────── defp get_items(socket, "header"), do: socket.assigns.header_items defp get_items(socket, "footer"), do: socket.assigns.footer_items defp put_items(socket, "header", items), do: socket |> assign(:header_items, items) |> assign(:dirty, true) defp put_items(socket, "footer", items), do: socket |> assign(:footer_items, items) |> assign(:dirty, true) defp load_nav(key, default) do case Settings.get_setting(key) do items when is_list(items) -> items _ -> default end end end