add data-driven navigation with admin editor
All checks were successful
deploy / deploy (push) Successful in 1m34s
All checks were successful
deploy / deploy (push) Successful in 1m34s
Replace hardcoded header, footer and mobile nav with settings-driven loops. Nav items stored as JSON via Settings, loaded in ThemeHook with sensible defaults. New admin navigation editor at /admin/navigation for add/remove/reorder/save/reset. Mobile bottom nav also driven from header nav items with icon mapping by slug. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -102,6 +102,14 @@
|
||||
<.icon name="hero-document" class="size-5" /> Pages
|
||||
</.link>
|
||||
</li>
|
||||
<li>
|
||||
<.link
|
||||
navigate={~p"/admin/navigation"}
|
||||
class={admin_nav_active?(@current_path, "/admin/navigation")}
|
||||
>
|
||||
<.icon name="hero-bars-3" class="size-5" /> Navigation
|
||||
</.link>
|
||||
</li>
|
||||
<li>
|
||||
<.link
|
||||
navigate={~p"/admin/media"}
|
||||
|
||||
@@ -51,7 +51,8 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
@layout_keys ~w(theme_settings logo_image header_image mode cart_items cart_count
|
||||
cart_subtotal cart_total cart_drawer_open cart_status active_page error_page is_admin
|
||||
search_query search_results search_open categories shipping_estimate
|
||||
country_code available_countries editing editor_current_path editor_sidebar_open)a
|
||||
country_code available_countries editing editor_current_path editor_sidebar_open
|
||||
header_nav_items footer_nav_items)a
|
||||
|
||||
@doc """
|
||||
Extracts the assigns relevant to `shop_layout` from a full assigns map.
|
||||
@@ -95,6 +96,8 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
attr :shipping_estimate, :integer, default: nil
|
||||
attr :country_code, :string, default: "GB"
|
||||
attr :available_countries, :list, default: []
|
||||
attr :header_nav_items, :list, default: []
|
||||
attr :footer_nav_items, :list, default: []
|
||||
|
||||
slot :inner_block, required: true
|
||||
|
||||
@@ -123,6 +126,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
editing={@editing}
|
||||
editor_current_path={@editor_current_path}
|
||||
editor_sidebar_open={@editor_sidebar_open}
|
||||
header_nav_items={@header_nav_items}
|
||||
/>
|
||||
|
||||
{render_slot(@inner_block)}
|
||||
@@ -131,6 +135,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
theme_settings={@theme_settings}
|
||||
mode={@mode}
|
||||
categories={assigns[:categories] || []}
|
||||
footer_nav_items={@footer_nav_items}
|
||||
/>
|
||||
|
||||
<.cart_drawer
|
||||
@@ -153,7 +158,12 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
search_open={@search_open}
|
||||
/>
|
||||
|
||||
<.mobile_bottom_nav :if={!@error_page} active_page={@active_page} mode={@mode} />
|
||||
<.mobile_bottom_nav
|
||||
:if={!@error_page}
|
||||
active_page={@active_page}
|
||||
mode={@mode}
|
||||
items={@header_nav_items}
|
||||
/>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
@@ -180,6 +190,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
attr :active_page, :string, required: true
|
||||
attr :mode, :atom, default: :live
|
||||
attr :cart_count, :integer, default: 0
|
||||
attr :items, :list, default: []
|
||||
|
||||
def mobile_bottom_nav(assigns) do
|
||||
~H"""
|
||||
@@ -189,36 +200,13 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
>
|
||||
<ul>
|
||||
<.mobile_nav_item
|
||||
icon={:home}
|
||||
label="Home"
|
||||
page="home"
|
||||
href="/"
|
||||
active_page={@active_page}
|
||||
mode={@mode}
|
||||
/>
|
||||
<.mobile_nav_item
|
||||
icon={:shop}
|
||||
label="Shop"
|
||||
page="collection"
|
||||
href="/collections/all"
|
||||
active_page={@active_page}
|
||||
active_pages={["collection", "pdp"]}
|
||||
mode={@mode}
|
||||
/>
|
||||
<.mobile_nav_item
|
||||
icon={:about}
|
||||
label="About"
|
||||
page="about"
|
||||
href="/about"
|
||||
active_page={@active_page}
|
||||
mode={@mode}
|
||||
/>
|
||||
<.mobile_nav_item
|
||||
icon={:contact}
|
||||
label="Contact"
|
||||
page="contact"
|
||||
href="/contact"
|
||||
:for={item <- @items}
|
||||
icon={mobile_icon(item["slug"])}
|
||||
label={item["label"]}
|
||||
page={item["slug"] || ""}
|
||||
href={item["href"]}
|
||||
active_page={@active_page}
|
||||
active_pages={item["active_slugs"]}
|
||||
mode={@mode}
|
||||
/>
|
||||
</ul>
|
||||
@@ -266,6 +254,12 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
"""
|
||||
end
|
||||
|
||||
defp mobile_icon("home"), do: :home
|
||||
defp mobile_icon("collection"), do: :shop
|
||||
defp mobile_icon("about"), do: :about
|
||||
defp mobile_icon("contact"), do: :contact
|
||||
defp mobile_icon(_), do: :page
|
||||
|
||||
defp nav_icon(%{icon: :home} = assigns) do
|
||||
~H"""
|
||||
<svg
|
||||
@@ -341,6 +335,24 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
"""
|
||||
end
|
||||
|
||||
defp nav_icon(%{icon: :page} = assigns) do
|
||||
~H"""
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
</svg>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders the search modal overlay with live search results.
|
||||
|
||||
@@ -509,6 +521,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
attr :theme_settings, :map, required: true
|
||||
attr :mode, :atom, default: :live
|
||||
attr :categories, :list, default: []
|
||||
attr :footer_nav_items, :list, default: []
|
||||
|
||||
def shop_footer(assigns) do
|
||||
assigns = assign(assigns, :current_year, Date.utc_today().year)
|
||||
@@ -575,81 +588,22 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
Help
|
||||
</h4>
|
||||
<ul class="footer-nav">
|
||||
<%= if @mode == :preview do %>
|
||||
<li>
|
||||
<li :for={item <- @footer_nav_items}>
|
||||
<%= if @mode == :preview do %>
|
||||
<a
|
||||
href="#"
|
||||
phx-click="change_preview_page"
|
||||
phx-value-page="delivery"
|
||||
phx-value-page={item["slug"]}
|
||||
class="footer-link"
|
||||
>
|
||||
Delivery & returns
|
||||
{item["label"]}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
phx-click="change_preview_page"
|
||||
phx-value-page="privacy"
|
||||
class="footer-link"
|
||||
>
|
||||
Privacy policy
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
phx-click="change_preview_page"
|
||||
phx-value-page="terms"
|
||||
class="footer-link"
|
||||
>
|
||||
Terms of service
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
phx-click="change_preview_page"
|
||||
phx-value-page="contact"
|
||||
class="footer-link"
|
||||
>
|
||||
Contact
|
||||
</a>
|
||||
</li>
|
||||
<% else %>
|
||||
<li>
|
||||
<.link
|
||||
navigate="/delivery"
|
||||
class="footer-link"
|
||||
>
|
||||
Delivery & returns
|
||||
<% else %>
|
||||
<.link navigate={item["href"]} class="footer-link">
|
||||
{item["label"]}
|
||||
</.link>
|
||||
</li>
|
||||
<li>
|
||||
<.link
|
||||
navigate="/privacy"
|
||||
class="footer-link"
|
||||
>
|
||||
Privacy policy
|
||||
</.link>
|
||||
</li>
|
||||
<li>
|
||||
<.link
|
||||
navigate="/terms"
|
||||
class="footer-link"
|
||||
>
|
||||
Terms of service
|
||||
</.link>
|
||||
</li>
|
||||
<li>
|
||||
<.link
|
||||
navigate="/contact"
|
||||
class="footer-link"
|
||||
>
|
||||
Contact
|
||||
</.link>
|
||||
</li>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -694,6 +648,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
attr :editing, :boolean, default: false
|
||||
attr :editor_current_path, :string, default: nil
|
||||
attr :editor_sidebar_open, :boolean, default: true
|
||||
attr :header_nav_items, :list, default: []
|
||||
|
||||
def shop_header(assigns) do
|
||||
~H"""
|
||||
@@ -712,29 +667,15 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
||||
</div>
|
||||
|
||||
<nav class="shop-nav">
|
||||
<%= if @mode == :preview do %>
|
||||
<.nav_item label="Home" page="home" active_page={@active_page} mode={:preview} />
|
||||
<.nav_item
|
||||
label="Shop"
|
||||
page="collection"
|
||||
active_page={@active_page}
|
||||
mode={:preview}
|
||||
active_pages={["collection", "pdp"]}
|
||||
/>
|
||||
<.nav_item label="About" page="about" active_page={@active_page} mode={:preview} />
|
||||
<.nav_item label="Contact" page="contact" active_page={@active_page} mode={:preview} />
|
||||
<% else %>
|
||||
<.nav_item label="Home" href="/" active_page={@active_page} page="home" />
|
||||
<.nav_item
|
||||
label="Shop"
|
||||
href="/collections/all"
|
||||
active_page={@active_page}
|
||||
page="collection"
|
||||
active_pages={["collection", "pdp"]}
|
||||
/>
|
||||
<.nav_item label="About" href="/about" active_page={@active_page} page="about" />
|
||||
<.nav_item label="Contact" href="/contact" active_page={@active_page} page="contact" />
|
||||
<% end %>
|
||||
<.nav_item
|
||||
:for={item <- @header_nav_items}
|
||||
label={item["label"]}
|
||||
href={item["href"]}
|
||||
page={item["slug"] || ""}
|
||||
active_page={@active_page}
|
||||
active_pages={item["active_slugs"]}
|
||||
mode={@mode}
|
||||
/>
|
||||
</nav>
|
||||
|
||||
<div class="shop-actions">
|
||||
|
||||
Reference in New Issue
Block a user