2026-02-28 11:18:37 +00:00
|
|
|
defmodule BerrypodWeb.Admin.Navigation do
|
|
|
|
|
use BerrypodWeb, :live_view
|
|
|
|
|
|
|
|
|
|
alias Berrypod.{Pages, Settings}
|
|
|
|
|
alias BerrypodWeb.ThemeHook
|
|
|
|
|
|
|
|
|
|
@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())
|
|
|
|
|
custom_pages = Pages.list_custom_pages()
|
|
|
|
|
|
|
|
|
|
{:ok,
|
|
|
|
|
socket
|
|
|
|
|
|> assign(:page_title, "Navigation")
|
|
|
|
|
|> assign(:header_items, header_items)
|
|
|
|
|
|> assign(:footer_items, footer_items)
|
|
|
|
|
|> assign(:custom_pages, custom_pages)
|
|
|
|
|
|> assign(:dirty, false)}
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# ── Item manipulation ──────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
|
def handle_event("update_item", params, socket) do
|
|
|
|
|
section = params["section"]
|
|
|
|
|
index = String.to_integer(params["index"])
|
|
|
|
|
field = params["field"]
|
|
|
|
|
value = params["value"]
|
|
|
|
|
|
|
|
|
|
items = get_items(socket, section)
|
|
|
|
|
item = Enum.at(items, index)
|
|
|
|
|
|
|
|
|
|
if item do
|
|
|
|
|
updated = Map.put(item, field, value)
|
|
|
|
|
items = List.replace_at(items, index, updated)
|
|
|
|
|
{:noreply, put_items(socket, section, items)}
|
|
|
|
|
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)}
|
|
|
|
|
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)}
|
|
|
|
|
else
|
|
|
|
|
{:noreply, socket}
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def handle_event("add_item", %{"section" => section}, socket) do
|
|
|
|
|
items = get_items(socket, section) ++ [%{"label" => "", "href" => ""}]
|
|
|
|
|
{:noreply, put_items(socket, section, items)}
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def handle_event("add_page", %{"section" => section, "slug" => slug}, socket) do
|
|
|
|
|
page = Enum.find(socket.assigns.custom_pages, &(&1.slug == slug))
|
|
|
|
|
|
|
|
|
|
if page do
|
|
|
|
|
item = %{"label" => page.title, "href" => "/#{page.slug}", "slug" => page.slug}
|
|
|
|
|
items = get_items(socket, section) ++ [item]
|
|
|
|
|
{:noreply, put_items(socket, section, items)}
|
|
|
|
|
else
|
|
|
|
|
{:noreply, socket}
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# ── Save / reset ───────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
def handle_event("save", _params, socket) 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)
|
|
|
|
|
|> put_flash(:info, "Navigation saved")}
|
|
|
|
|
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)}
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# ── Render ─────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
|
def render(assigns) do
|
|
|
|
|
~H"""
|
|
|
|
|
<.header>
|
|
|
|
|
Navigation
|
|
|
|
|
<:subtitle>Configure the links in your shop header and footer.</:subtitle>
|
|
|
|
|
</.header>
|
|
|
|
|
|
complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones)
- Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid
primitives and gap variants (sm, md, lg, xl)
- Add transitions.css import and layout.css import to admin.css entry point
- Replace all Tailwind utility classes across 26 admin templates with
semantic admin-*/theme-*/page-specific CSS classes
- Replace all non-dynamic inline styles with semantic classes
- Add ~100 new semantic classes to components.css (analytics, dashboard,
order detail, settings, theme editor, generic utilities)
- Fix stray text-error → admin-text-error in media.ex
- Add missing .truncate definition to admin CSS
- Only remaining inline styles are dynamic data values (progress bars,
chart dimensions) and one JS.toggle target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:40:21 +00:00
|
|
|
<div class="admin-nav-layout">
|
|
|
|
|
<p :if={@dirty} class="admin-badge admin-badge-warning">
|
|
|
|
|
Unsaved changes
|
|
|
|
|
</p>
|
2026-02-28 11:18:37 +00:00
|
|
|
<.nav_section
|
|
|
|
|
title="Header navigation"
|
|
|
|
|
section="header"
|
|
|
|
|
items={@header_items}
|
|
|
|
|
custom_pages={@custom_pages}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<.nav_section
|
|
|
|
|
title="Footer navigation"
|
|
|
|
|
section="footer"
|
|
|
|
|
items={@footer_items}
|
|
|
|
|
custom_pages={@custom_pages}
|
|
|
|
|
/>
|
|
|
|
|
|
complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones)
- Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid
primitives and gap variants (sm, md, lg, xl)
- Add transitions.css import and layout.css import to admin.css entry point
- Replace all Tailwind utility classes across 26 admin templates with
semantic admin-*/theme-*/page-specific CSS classes
- Replace all non-dynamic inline styles with semantic classes
- Add ~100 new semantic classes to components.css (analytics, dashboard,
order detail, settings, theme editor, generic utilities)
- Fix stray text-error → admin-text-error in media.ex
- Add missing .truncate definition to admin CSS
- Only remaining inline styles are dynamic data values (progress bars,
chart dimensions) and one JS.toggle target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:40:21 +00:00
|
|
|
<div class="admin-row admin-row-lg">
|
2026-02-28 11:18:37 +00:00
|
|
|
<button
|
|
|
|
|
phx-click="save"
|
complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones)
- Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid
primitives and gap variants (sm, md, lg, xl)
- Add transitions.css import and layout.css import to admin.css entry point
- Replace all Tailwind utility classes across 26 admin templates with
semantic admin-*/theme-*/page-specific CSS classes
- Replace all non-dynamic inline styles with semantic classes
- Add ~100 new semantic classes to components.css (analytics, dashboard,
order detail, settings, theme editor, generic utilities)
- Fix stray text-error → admin-text-error in media.ex
- Add missing .truncate definition to admin CSS
- Only remaining inline styles are dynamic data values (progress bars,
chart dimensions) and one JS.toggle target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:40:21 +00:00
|
|
|
class="admin-btn admin-btn-primary"
|
2026-02-28 11:18:37 +00:00
|
|
|
disabled={!@dirty}
|
|
|
|
|
>
|
|
|
|
|
Save
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
phx-click="reset_defaults"
|
|
|
|
|
data-confirm="Reset navigation to defaults? Your changes will be lost."
|
|
|
|
|
class="admin-btn admin-btn-ghost"
|
|
|
|
|
>
|
|
|
|
|
Reset to defaults
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
"""
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
defp nav_section(assigns) do
|
|
|
|
|
~H"""
|
|
|
|
|
<section>
|
complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones)
- Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid
primitives and gap variants (sm, md, lg, xl)
- Add transitions.css import and layout.css import to admin.css entry point
- Replace all Tailwind utility classes across 26 admin templates with
semantic admin-*/theme-*/page-specific CSS classes
- Replace all non-dynamic inline styles with semantic classes
- Add ~100 new semantic classes to components.css (analytics, dashboard,
order detail, settings, theme editor, generic utilities)
- Fix stray text-error → admin-text-error in media.ex
- Add missing .truncate definition to admin CSS
- Only remaining inline styles are dynamic data values (progress bars,
chart dimensions) and one JS.toggle target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:40:21 +00:00
|
|
|
<h3 class="admin-nav-section-heading">
|
2026-02-28 11:18:37 +00:00
|
|
|
{@title}
|
|
|
|
|
</h3>
|
|
|
|
|
|
complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones)
- Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid
primitives and gap variants (sm, md, lg, xl)
- Add transitions.css import and layout.css import to admin.css entry point
- Replace all Tailwind utility classes across 26 admin templates with
semantic admin-*/theme-*/page-specific CSS classes
- Replace all non-dynamic inline styles with semantic classes
- Add ~100 new semantic classes to components.css (analytics, dashboard,
order detail, settings, theme editor, generic utilities)
- Fix stray text-error → admin-text-error in media.ex
- Add missing .truncate definition to admin CSS
- Only remaining inline styles are dynamic data values (progress bars,
chart dimensions) and one JS.toggle target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:40:21 +00:00
|
|
|
<div class="admin-stack admin-stack-sm">
|
2026-02-28 20:11:13 +00:00
|
|
|
<div :if={@items != []} class="nav-editor-labels" aria-hidden="true">
|
|
|
|
|
<span>Label</span>
|
|
|
|
|
<span>Path</span>
|
|
|
|
|
</div>
|
2026-02-28 11:18:37 +00:00
|
|
|
<div
|
|
|
|
|
:for={{item, idx} <- Enum.with_index(@items)}
|
|
|
|
|
class="nav-editor-item"
|
|
|
|
|
>
|
|
|
|
|
<div class="nav-editor-fields">
|
2026-02-28 20:11:13 +00:00
|
|
|
<label class="sr-only" for={"nav-#{@section}-#{idx}-label"}>Label</label>
|
2026-02-28 11:18:37 +00:00
|
|
|
<input
|
|
|
|
|
type="text"
|
2026-02-28 20:11:13 +00:00
|
|
|
id={"nav-#{@section}-#{idx}-label"}
|
2026-02-28 11:18:37 +00:00
|
|
|
value={item["label"]}
|
|
|
|
|
placeholder="Label"
|
|
|
|
|
phx-blur="update_item"
|
|
|
|
|
phx-value-section={@section}
|
|
|
|
|
phx-value-index={idx}
|
|
|
|
|
phx-value-field="label"
|
|
|
|
|
class="admin-input nav-editor-input"
|
|
|
|
|
/>
|
2026-02-28 20:11:13 +00:00
|
|
|
<label class="sr-only" for={"nav-#{@section}-#{idx}-href"}>Path</label>
|
2026-02-28 11:18:37 +00:00
|
|
|
<input
|
|
|
|
|
type="text"
|
2026-02-28 20:11:13 +00:00
|
|
|
id={"nav-#{@section}-#{idx}-href"}
|
2026-02-28 11:18:37 +00:00
|
|
|
value={item["href"]}
|
|
|
|
|
placeholder="/path"
|
|
|
|
|
phx-blur="update_item"
|
|
|
|
|
phx-value-section={@section}
|
|
|
|
|
phx-value-index={idx}
|
|
|
|
|
phx-value-field="href"
|
|
|
|
|
class="admin-input nav-editor-input"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="nav-editor-actions">
|
|
|
|
|
<button
|
|
|
|
|
phx-click="move_item"
|
|
|
|
|
phx-value-section={@section}
|
|
|
|
|
phx-value-index={idx}
|
|
|
|
|
phx-value-dir="up"
|
|
|
|
|
disabled={idx == 0}
|
|
|
|
|
class="admin-btn admin-btn-xs admin-btn-ghost"
|
|
|
|
|
aria-label="Move up"
|
|
|
|
|
>
|
|
|
|
|
<.icon name="hero-chevron-up" class="size-4" />
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
phx-click="move_item"
|
|
|
|
|
phx-value-section={@section}
|
|
|
|
|
phx-value-index={idx}
|
|
|
|
|
phx-value-dir="down"
|
|
|
|
|
disabled={idx == length(@items) - 1}
|
|
|
|
|
class="admin-btn admin-btn-xs admin-btn-ghost"
|
|
|
|
|
aria-label="Move down"
|
|
|
|
|
>
|
|
|
|
|
<.icon name="hero-chevron-down" class="size-4" />
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
phx-click="remove_item"
|
|
|
|
|
phx-value-section={@section}
|
|
|
|
|
phx-value-index={idx}
|
|
|
|
|
class="admin-btn admin-btn-xs admin-btn-ghost"
|
|
|
|
|
aria-label="Remove"
|
|
|
|
|
>
|
|
|
|
|
<.icon name="hero-x-mark" class="size-4" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones)
- Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid
primitives and gap variants (sm, md, lg, xl)
- Add transitions.css import and layout.css import to admin.css entry point
- Replace all Tailwind utility classes across 26 admin templates with
semantic admin-*/theme-*/page-specific CSS classes
- Replace all non-dynamic inline styles with semantic classes
- Add ~100 new semantic classes to components.css (analytics, dashboard,
order detail, settings, theme editor, generic utilities)
- Fix stray text-error → admin-text-error in media.ex
- Add missing .truncate definition to admin CSS
- Only remaining inline styles are dynamic data values (progress bars,
chart dimensions) and one JS.toggle target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:40:21 +00:00
|
|
|
<div
|
|
|
|
|
:if={@items == []}
|
|
|
|
|
class="admin-nav-empty"
|
|
|
|
|
>
|
2026-02-28 11:18:37 +00:00
|
|
|
No items yet.
|
|
|
|
|
</div>
|
|
|
|
|
|
complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones)
- Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid
primitives and gap variants (sm, md, lg, xl)
- Add transitions.css import and layout.css import to admin.css entry point
- Replace all Tailwind utility classes across 26 admin templates with
semantic admin-*/theme-*/page-specific CSS classes
- Replace all non-dynamic inline styles with semantic classes
- Add ~100 new semantic classes to components.css (analytics, dashboard,
order detail, settings, theme editor, generic utilities)
- Fix stray text-error → admin-text-error in media.ex
- Add missing .truncate definition to admin CSS
- Only remaining inline styles are dynamic data values (progress bars,
chart dimensions) and one JS.toggle target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:40:21 +00:00
|
|
|
<div class="admin-nav-actions">
|
2026-02-28 11:18:37 +00:00
|
|
|
<button
|
|
|
|
|
phx-click="add_item"
|
|
|
|
|
phx-value-section={@section}
|
|
|
|
|
class="admin-btn admin-btn-sm admin-btn-outline"
|
|
|
|
|
>
|
|
|
|
|
<.icon name="hero-plus" class="size-4" /> Add link
|
|
|
|
|
</button>
|
complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones)
- Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid
primitives and gap variants (sm, md, lg, xl)
- Add transitions.css import and layout.css import to admin.css entry point
- Replace all Tailwind utility classes across 26 admin templates with
semantic admin-*/theme-*/page-specific CSS classes
- Replace all non-dynamic inline styles with semantic classes
- Add ~100 new semantic classes to components.css (analytics, dashboard,
order detail, settings, theme editor, generic utilities)
- Fix stray text-error → admin-text-error in media.ex
- Add missing .truncate definition to admin CSS
- Only remaining inline styles are dynamic data values (progress bars,
chart dimensions) and one JS.toggle target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:40:21 +00:00
|
|
|
<div :if={@custom_pages != []} class="nav-editor-dropdown-wrap" id={"add-page-#{@section}"}>
|
2026-02-28 11:18:37 +00:00
|
|
|
<button
|
|
|
|
|
phx-click={Phoenix.LiveView.JS.toggle(to: "#page-menu-#{@section}")}
|
|
|
|
|
class="admin-btn admin-btn-sm admin-btn-outline"
|
|
|
|
|
>
|
|
|
|
|
<.icon name="hero-document-plus" class="size-4" /> Add page
|
|
|
|
|
</button>
|
|
|
|
|
<div
|
|
|
|
|
id={"page-menu-#{@section}"}
|
|
|
|
|
class="nav-editor-dropdown"
|
|
|
|
|
style="display: none;"
|
|
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
:for={page <- @custom_pages}
|
|
|
|
|
phx-click="add_page"
|
|
|
|
|
phx-value-section={@section}
|
|
|
|
|
phx-value-slug={page.slug}
|
|
|
|
|
class="nav-editor-dropdown-item"
|
|
|
|
|
>
|
|
|
|
|
{page.title}
|
complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones)
- Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid
primitives and gap variants (sm, md, lg, xl)
- Add transitions.css import and layout.css import to admin.css entry point
- Replace all Tailwind utility classes across 26 admin templates with
semantic admin-*/theme-*/page-specific CSS classes
- Replace all non-dynamic inline styles with semantic classes
- Add ~100 new semantic classes to components.css (analytics, dashboard,
order detail, settings, theme editor, generic utilities)
- Fix stray text-error → admin-text-error in media.ex
- Add missing .truncate definition to admin CSS
- Only remaining inline styles are dynamic data values (progress bars,
chart dimensions) and one JS.toggle target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:40:21 +00:00
|
|
|
<span class="nav-editor-dropdown-slug">
|
|
|
|
|
/{page.slug}
|
|
|
|
|
</span>
|
2026-02-28 11:18:37 +00:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
"""
|
|
|
|
|
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
|