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:
parent
045be2ed7e
commit
3a243151af
@ -2152,4 +2152,71 @@
|
|||||||
to { opacity: 1; }
|
to { opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Navigation editor */
|
||||||
|
|
||||||
|
.nav-editor-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid var(--t-border-default);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background: var(--t-surface-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-editor-fields {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-editor-input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
padding: 0.375rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-editor-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.125rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-editor-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
z-index: 10;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
min-width: 12rem;
|
||||||
|
background: var(--t-surface-base);
|
||||||
|
border: 1px solid var(--t-border-default);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-editor-dropdown-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--t-text-primary);
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
&:hover {
|
||||||
|
background: var(--t-surface-sunken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} /* @layer admin */
|
} /* @layer admin */
|
||||||
|
|||||||
@ -102,6 +102,14 @@
|
|||||||
<.icon name="hero-document" class="size-5" /> Pages
|
<.icon name="hero-document" class="size-5" /> Pages
|
||||||
</.link>
|
</.link>
|
||||||
</li>
|
</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>
|
<li>
|
||||||
<.link
|
<.link
|
||||||
navigate={~p"/admin/media"}
|
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
|
@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
|
cart_subtotal cart_total cart_drawer_open cart_status active_page error_page is_admin
|
||||||
search_query search_results search_open categories shipping_estimate
|
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 """
|
@doc """
|
||||||
Extracts the assigns relevant to `shop_layout` from a full assigns map.
|
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 :shipping_estimate, :integer, default: nil
|
||||||
attr :country_code, :string, default: "GB"
|
attr :country_code, :string, default: "GB"
|
||||||
attr :available_countries, :list, default: []
|
attr :available_countries, :list, default: []
|
||||||
|
attr :header_nav_items, :list, default: []
|
||||||
|
attr :footer_nav_items, :list, default: []
|
||||||
|
|
||||||
slot :inner_block, required: true
|
slot :inner_block, required: true
|
||||||
|
|
||||||
@ -123,6 +126,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
|||||||
editing={@editing}
|
editing={@editing}
|
||||||
editor_current_path={@editor_current_path}
|
editor_current_path={@editor_current_path}
|
||||||
editor_sidebar_open={@editor_sidebar_open}
|
editor_sidebar_open={@editor_sidebar_open}
|
||||||
|
header_nav_items={@header_nav_items}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{render_slot(@inner_block)}
|
{render_slot(@inner_block)}
|
||||||
@ -131,6 +135,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
|||||||
theme_settings={@theme_settings}
|
theme_settings={@theme_settings}
|
||||||
mode={@mode}
|
mode={@mode}
|
||||||
categories={assigns[:categories] || []}
|
categories={assigns[:categories] || []}
|
||||||
|
footer_nav_items={@footer_nav_items}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<.cart_drawer
|
<.cart_drawer
|
||||||
@ -153,7 +158,12 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
|||||||
search_open={@search_open}
|
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>
|
</div>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
@ -180,6 +190,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
|||||||
attr :active_page, :string, required: true
|
attr :active_page, :string, required: true
|
||||||
attr :mode, :atom, default: :live
|
attr :mode, :atom, default: :live
|
||||||
attr :cart_count, :integer, default: 0
|
attr :cart_count, :integer, default: 0
|
||||||
|
attr :items, :list, default: []
|
||||||
|
|
||||||
def mobile_bottom_nav(assigns) do
|
def mobile_bottom_nav(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
@ -189,36 +200,13 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
|||||||
>
|
>
|
||||||
<ul>
|
<ul>
|
||||||
<.mobile_nav_item
|
<.mobile_nav_item
|
||||||
icon={:home}
|
:for={item <- @items}
|
||||||
label="Home"
|
icon={mobile_icon(item["slug"])}
|
||||||
page="home"
|
label={item["label"]}
|
||||||
href="/"
|
page={item["slug"] || ""}
|
||||||
active_page={@active_page}
|
href={item["href"]}
|
||||||
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"
|
|
||||||
active_page={@active_page}
|
active_page={@active_page}
|
||||||
|
active_pages={item["active_slugs"]}
|
||||||
mode={@mode}
|
mode={@mode}
|
||||||
/>
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
@ -266,6 +254,12 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
|||||||
"""
|
"""
|
||||||
end
|
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
|
defp nav_icon(%{icon: :home} = assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<svg
|
<svg
|
||||||
@ -341,6 +335,24 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
|||||||
"""
|
"""
|
||||||
end
|
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 """
|
@doc """
|
||||||
Renders the search modal overlay with live search results.
|
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 :theme_settings, :map, required: true
|
||||||
attr :mode, :atom, default: :live
|
attr :mode, :atom, default: :live
|
||||||
attr :categories, :list, default: []
|
attr :categories, :list, default: []
|
||||||
|
attr :footer_nav_items, :list, default: []
|
||||||
|
|
||||||
def shop_footer(assigns) do
|
def shop_footer(assigns) do
|
||||||
assigns = assign(assigns, :current_year, Date.utc_today().year)
|
assigns = assign(assigns, :current_year, Date.utc_today().year)
|
||||||
@ -575,81 +588,22 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
|||||||
Help
|
Help
|
||||||
</h4>
|
</h4>
|
||||||
<ul class="footer-nav">
|
<ul class="footer-nav">
|
||||||
<%= if @mode == :preview do %>
|
<li :for={item <- @footer_nav_items}>
|
||||||
<li>
|
<%= if @mode == :preview do %>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
phx-click="change_preview_page"
|
phx-click="change_preview_page"
|
||||||
phx-value-page="delivery"
|
phx-value-page={item["slug"]}
|
||||||
class="footer-link"
|
class="footer-link"
|
||||||
>
|
>
|
||||||
Delivery & returns
|
{item["label"]}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
<% else %>
|
||||||
<li>
|
<.link navigate={item["href"]} class="footer-link">
|
||||||
<a
|
{item["label"]}
|
||||||
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
|
|
||||||
</.link>
|
</.link>
|
||||||
</li>
|
<% end %>
|
||||||
<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 %>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -694,6 +648,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
|||||||
attr :editing, :boolean, default: false
|
attr :editing, :boolean, default: false
|
||||||
attr :editor_current_path, :string, default: nil
|
attr :editor_current_path, :string, default: nil
|
||||||
attr :editor_sidebar_open, :boolean, default: true
|
attr :editor_sidebar_open, :boolean, default: true
|
||||||
|
attr :header_nav_items, :list, default: []
|
||||||
|
|
||||||
def shop_header(assigns) do
|
def shop_header(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
@ -712,29 +667,15 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="shop-nav">
|
<nav class="shop-nav">
|
||||||
<%= if @mode == :preview do %>
|
<.nav_item
|
||||||
<.nav_item label="Home" page="home" active_page={@active_page} mode={:preview} />
|
:for={item <- @header_nav_items}
|
||||||
<.nav_item
|
label={item["label"]}
|
||||||
label="Shop"
|
href={item["href"]}
|
||||||
page="collection"
|
page={item["slug"] || ""}
|
||||||
active_page={@active_page}
|
active_page={@active_page}
|
||||||
mode={:preview}
|
active_pages={item["active_slugs"]}
|
||||||
active_pages={["collection", "pdp"]}
|
mode={@mode}
|
||||||
/>
|
/>
|
||||||
<.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>
|
</nav>
|
||||||
|
|
||||||
<div class="shop-actions">
|
<div class="shop-actions">
|
||||||
|
|||||||
@ -94,6 +94,14 @@ defmodule BerrypodWeb.ErrorHTML do
|
|||||||
|> Map.put(:cart_count, 0)
|
|> Map.put(:cart_count, 0)
|
||||||
|> Map.put(:cart_subtotal, "£0.00")
|
|> Map.put(:cart_subtotal, "£0.00")
|
||||||
|> Map.put(:page, page)
|
|> Map.put(:page, page)
|
||||||
|
|> Map.put(
|
||||||
|
:header_nav_items,
|
||||||
|
load_nav("header_nav", &BerrypodWeb.ThemeHook.default_header_nav/0)
|
||||||
|
)
|
||||||
|
|> Map.put(
|
||||||
|
:footer_nav_items,
|
||||||
|
load_nav("footer_nav", &BerrypodWeb.ThemeHook.default_footer_nav/0)
|
||||||
|
)
|
||||||
|
|
||||||
# Load block data (e.g. products for featured_products block)
|
# Load block data (e.g. products for featured_products block)
|
||||||
extra = safe_load(fn -> Pages.load_block_data(page.blocks, assigns) end) || %{}
|
extra = safe_load(fn -> Pages.load_block_data(page.blocks, assigns) end) || %{}
|
||||||
@ -159,4 +167,11 @@ defmodule BerrypodWeb.ErrorHTML do
|
|||||||
_ -> nil
|
_ -> nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp load_nav(key, default_fn) do
|
||||||
|
case safe_load(fn -> Settings.get_setting(key) end) do
|
||||||
|
items when is_list(items) -> items
|
||||||
|
_ -> default_fn.()
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
281
lib/berrypod_web/live/admin/navigation.ex
Normal file
281
lib/berrypod_web/live/admin/navigation.ex
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
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>
|
||||||
|
|
||||||
|
<p :if={@dirty} class="admin-badge admin-badge-warning mt-4">
|
||||||
|
Unsaved changes
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-6 space-y-8" style="max-width: 40rem;">
|
||||||
|
<.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}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button
|
||||||
|
phx-click="save"
|
||||||
|
class={["admin-btn admin-btn-primary", !@dirty && "opacity-50"]}
|
||||||
|
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>
|
||||||
|
<h3 class="text-sm font-semibold uppercase tracking-wider text-base-content/50 mb-3">
|
||||||
|
{@title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div
|
||||||
|
:for={{item, idx} <- Enum.with_index(@items)}
|
||||||
|
class="nav-editor-item"
|
||||||
|
>
|
||||||
|
<div class="nav-editor-fields">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
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>
|
||||||
|
|
||||||
|
<div :if={@items == []} class="text-sm text-base-content/50 py-4">
|
||||||
|
No items yet.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 flex gap-2">
|
||||||
|
<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>
|
||||||
|
<div :if={@custom_pages != []} class="relative" id={"add-page-#{@section}"}>
|
||||||
|
<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}
|
||||||
|
<span class="text-xs text-base-content/40">/{page.slug}</span>
|
||||||
|
</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
|
||||||
@ -425,6 +425,8 @@ defmodule BerrypodWeb.Admin.Pages.Editor do
|
|||||||
|> assign(:cart_count, 2)
|
|> assign(:cart_count, 2)
|
||||||
|> assign(:cart_subtotal, "£72.00")
|
|> assign(:cart_subtotal, "£72.00")
|
||||||
|> assign(:cart_drawer_open, false)
|
|> assign(:cart_drawer_open, false)
|
||||||
|
|> assign(:header_nav_items, BerrypodWeb.ThemeHook.default_header_nav())
|
||||||
|
|> assign(:footer_nav_items, BerrypodWeb.ThemeHook.default_footer_nav())
|
||||||
|> preview_page_context(assigns.slug)
|
|> preview_page_context(assigns.slug)
|
||||||
|
|
||||||
extra = Pages.load_block_data(page.blocks, preview)
|
extra = Pages.load_block_data(page.blocks, preview)
|
||||||
|
|||||||
@ -399,7 +399,9 @@ defmodule BerrypodWeb.Admin.Theme.Index do
|
|||||||
categories: assigns.preview_data.categories,
|
categories: assigns.preview_data.categories,
|
||||||
cart_items: PreviewData.cart_drawer_items(),
|
cart_items: PreviewData.cart_drawer_items(),
|
||||||
cart_count: 2,
|
cart_count: 2,
|
||||||
cart_subtotal: "£72.00"
|
cart_subtotal: "£72.00",
|
||||||
|
header_nav_items: BerrypodWeb.ThemeHook.default_header_nav(),
|
||||||
|
footer_nav_items: BerrypodWeb.ThemeHook.default_footer_nav()
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -158,6 +158,7 @@ defmodule BerrypodWeb.Router do
|
|||||||
live "/pages/new", Admin.Pages.CustomForm, :new
|
live "/pages/new", Admin.Pages.CustomForm, :new
|
||||||
live "/pages/:slug/settings", Admin.Pages.CustomForm, :edit
|
live "/pages/:slug/settings", Admin.Pages.CustomForm, :edit
|
||||||
live "/pages/:slug", Admin.Pages.Editor, :edit
|
live "/pages/:slug", Admin.Pages.Editor, :edit
|
||||||
|
live "/navigation", Admin.Navigation, :index
|
||||||
live "/media", Admin.Media, :index
|
live "/media", Admin.Media, :index
|
||||||
live "/redirects", Admin.Redirects, :index
|
live "/redirects", Admin.Redirects, :index
|
||||||
end
|
end
|
||||||
|
|||||||
@ -17,6 +17,28 @@ defmodule BerrypodWeb.ThemeHook do
|
|||||||
alias Berrypod.{Products, Settings, Media}
|
alias Berrypod.{Products, Settings, Media}
|
||||||
alias Berrypod.Theme.{CSSCache, CSSGenerator}
|
alias Berrypod.Theme.{CSSCache, CSSGenerator}
|
||||||
|
|
||||||
|
@default_header_nav [
|
||||||
|
%{"label" => "Home", "href" => "/", "slug" => "home"},
|
||||||
|
%{
|
||||||
|
"label" => "Shop",
|
||||||
|
"href" => "/collections/all",
|
||||||
|
"slug" => "collection",
|
||||||
|
"active_slugs" => ["collection", "pdp"]
|
||||||
|
},
|
||||||
|
%{"label" => "About", "href" => "/about", "slug" => "about"},
|
||||||
|
%{"label" => "Contact", "href" => "/contact", "slug" => "contact"}
|
||||||
|
]
|
||||||
|
|
||||||
|
@default_footer_nav [
|
||||||
|
%{"label" => "Delivery & returns", "href" => "/delivery", "slug" => "delivery"},
|
||||||
|
%{"label" => "Privacy policy", "href" => "/privacy", "slug" => "privacy"},
|
||||||
|
%{"label" => "Terms of service", "href" => "/terms", "slug" => "terms"},
|
||||||
|
%{"label" => "Contact", "href" => "/contact", "slug" => "contact"}
|
||||||
|
]
|
||||||
|
|
||||||
|
def default_header_nav, do: @default_header_nav
|
||||||
|
def default_footer_nav, do: @default_footer_nav
|
||||||
|
|
||||||
def on_mount(:mount_theme, _params, _session, socket) do
|
def on_mount(:mount_theme, _params, _session, socket) do
|
||||||
theme_settings = Settings.get_theme_settings()
|
theme_settings = Settings.get_theme_settings()
|
||||||
|
|
||||||
@ -43,6 +65,8 @@ defmodule BerrypodWeb.ThemeHook do
|
|||||||
:is_admin,
|
:is_admin,
|
||||||
!!(socket.assigns[:current_scope] && socket.assigns.current_scope.user)
|
!!(socket.assigns[:current_scope] && socket.assigns.current_scope.user)
|
||||||
)
|
)
|
||||||
|
|> assign(:header_nav_items, load_nav("header_nav", @default_header_nav))
|
||||||
|
|> assign(:footer_nav_items, load_nav("footer_nav", @default_footer_nav))
|
||||||
|
|
||||||
{:cont, socket}
|
{:cont, socket}
|
||||||
end
|
end
|
||||||
@ -64,4 +88,11 @@ defmodule BerrypodWeb.ThemeHook do
|
|||||||
{:halt, Phoenix.LiveView.redirect(socket, to: "/coming-soon")}
|
{:halt, Phoenix.LiveView.redirect(socket, to: "/coming-soon")}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp load_nav(key, default) do
|
||||||
|
case Settings.get_setting(key) do
|
||||||
|
items when is_list(items) -> items
|
||||||
|
_ -> default
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
163
test/berrypod_web/live/admin/navigation_test.exs
Normal file
163
test/berrypod_web/live/admin/navigation_test.exs
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
defmodule BerrypodWeb.Admin.NavigationTest do
|
||||||
|
use BerrypodWeb.ConnCase, async: false
|
||||||
|
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
import Berrypod.AccountsFixtures
|
||||||
|
|
||||||
|
alias Berrypod.{Pages, Settings}
|
||||||
|
alias Berrypod.Pages.PageCache
|
||||||
|
|
||||||
|
setup do
|
||||||
|
PageCache.invalidate_all()
|
||||||
|
user = user_fixture()
|
||||||
|
%{user: user}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "unauthenticated" do
|
||||||
|
test "redirects to login", %{conn: conn} do
|
||||||
|
{:error, redirect} = live(conn, ~p"/admin/navigation")
|
||||||
|
assert {:redirect, %{to: path}} = redirect
|
||||||
|
assert path == ~p"/users/log-in"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "navigation editor" do
|
||||||
|
setup %{conn: conn, user: user} do
|
||||||
|
%{conn: log_in_user(conn, user)}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders with header and footer sections", %{conn: conn} do
|
||||||
|
{:ok, _view, html} = live(conn, ~p"/admin/navigation")
|
||||||
|
|
||||||
|
assert html =~ "Navigation"
|
||||||
|
assert html =~ "Header navigation"
|
||||||
|
assert html =~ "Footer navigation"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "shows default header items", %{conn: conn} do
|
||||||
|
{:ok, _view, html} = live(conn, ~p"/admin/navigation")
|
||||||
|
|
||||||
|
assert html =~ "Home"
|
||||||
|
assert html =~ "Shop"
|
||||||
|
assert html =~ "About"
|
||||||
|
assert html =~ "Contact"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "shows default footer items", %{conn: conn} do
|
||||||
|
{:ok, _view, html} = live(conn, ~p"/admin/navigation")
|
||||||
|
|
||||||
|
assert html =~ "Delivery & returns"
|
||||||
|
assert html =~ "Privacy policy"
|
||||||
|
assert html =~ "Terms of service"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "adding an item appends to list", %{conn: conn} do
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/admin/navigation")
|
||||||
|
|
||||||
|
render_click(view, "add_item", %{"section" => "header"})
|
||||||
|
|
||||||
|
# Should have 5 items now (4 defaults + 1 new)
|
||||||
|
html = render(view)
|
||||||
|
# New empty item has empty placeholder inputs
|
||||||
|
assert Regex.scan(~r/phx-value-section="header".*?phx-value-field="label"/, html)
|
||||||
|
|> length() == 5
|
||||||
|
end
|
||||||
|
|
||||||
|
test "removing an item removes from list", %{conn: conn} do
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/admin/navigation")
|
||||||
|
|
||||||
|
render_click(view, "remove_item", %{"section" => "header", "index" => "0"})
|
||||||
|
|
||||||
|
html = render(view)
|
||||||
|
# "Home" should be gone (it was index 0)
|
||||||
|
refute html =~ ~s(value="Home")
|
||||||
|
# "Shop" should still be there
|
||||||
|
assert html =~ ~s(value="Shop")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "moving item up reorders", %{conn: conn} do
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/admin/navigation")
|
||||||
|
|
||||||
|
render_click(view, "move_item", %{
|
||||||
|
"section" => "header",
|
||||||
|
"index" => "1",
|
||||||
|
"dir" => "up"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Save and check order
|
||||||
|
render_click(view, "save")
|
||||||
|
|
||||||
|
items = Settings.get_setting("header_nav")
|
||||||
|
assert Enum.at(items, 0)["label"] == "Shop"
|
||||||
|
assert Enum.at(items, 1)["label"] == "Home"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "adding a custom page creates nav item", %{conn: conn} do
|
||||||
|
{:ok, _} = Pages.create_custom_page(%{slug: "faq", title: "FAQ"})
|
||||||
|
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/admin/navigation")
|
||||||
|
|
||||||
|
render_click(view, "add_page", %{"section" => "header", "slug" => "faq"})
|
||||||
|
|
||||||
|
html = render(view)
|
||||||
|
assert html =~ ~s(value="FAQ")
|
||||||
|
assert html =~ ~s(value="/faq")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "save persists to settings", %{conn: conn} do
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/admin/navigation")
|
||||||
|
|
||||||
|
render_click(view, "remove_item", %{"section" => "footer", "index" => "0"})
|
||||||
|
render_click(view, "save")
|
||||||
|
|
||||||
|
assert render(view) =~ "Navigation saved"
|
||||||
|
|
||||||
|
items = Settings.get_setting("footer_nav")
|
||||||
|
assert length(items) == 3
|
||||||
|
assert Enum.at(items, 0)["label"] == "Privacy policy"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "reset defaults restores original items", %{conn: conn} do
|
||||||
|
# Save custom nav
|
||||||
|
Settings.put_setting("header_nav", [%{"label" => "Only", "href" => "/only"}], "json")
|
||||||
|
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/admin/navigation")
|
||||||
|
|
||||||
|
# Should show the custom item
|
||||||
|
assert render(view) =~ ~s(value="Only")
|
||||||
|
|
||||||
|
render_click(view, "reset_defaults")
|
||||||
|
|
||||||
|
html = render(view)
|
||||||
|
assert html =~ ~s(value="Home")
|
||||||
|
assert html =~ ~s(value="Shop")
|
||||||
|
refute html =~ ~s(value="Only")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "dirty flag appears after changes", %{conn: conn} do
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/admin/navigation")
|
||||||
|
|
||||||
|
refute has_element?(view, ".admin-badge-warning")
|
||||||
|
|
||||||
|
render_click(view, "add_item", %{"section" => "header"})
|
||||||
|
|
||||||
|
assert has_element?(view, ".admin-badge-warning", "Unsaved changes")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "dirty flag clears after save", %{conn: conn} do
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/admin/navigation")
|
||||||
|
|
||||||
|
render_click(view, "add_item", %{"section" => "header"})
|
||||||
|
assert has_element?(view, ".admin-badge-warning")
|
||||||
|
|
||||||
|
render_click(view, "save")
|
||||||
|
refute has_element?(view, ".admin-badge-warning")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "save button disabled when not dirty", %{conn: conn} do
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/admin/navigation")
|
||||||
|
|
||||||
|
assert has_element?(view, "button[disabled]", "Save")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
91
test/berrypod_web/live/shop/navigation_test.exs
Normal file
91
test/berrypod_web/live/shop/navigation_test.exs
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
defmodule BerrypodWeb.Shop.NavigationTest do
|
||||||
|
use BerrypodWeb.ConnCase, async: false
|
||||||
|
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
import Berrypod.AccountsFixtures
|
||||||
|
|
||||||
|
alias Berrypod.{Pages, Settings}
|
||||||
|
alias Berrypod.Pages.PageCache
|
||||||
|
|
||||||
|
setup do
|
||||||
|
PageCache.invalidate_all()
|
||||||
|
user_fixture()
|
||||||
|
{:ok, _} = Settings.set_site_live(true)
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "header navigation" do
|
||||||
|
test "renders default items", %{conn: conn} do
|
||||||
|
{:ok, _view, html} = live(conn, ~p"/")
|
||||||
|
|
||||||
|
assert html =~ "Home"
|
||||||
|
assert html =~ "Shop"
|
||||||
|
assert html =~ "About"
|
||||||
|
assert html =~ "Contact"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders from saved settings", %{conn: conn} do
|
||||||
|
Settings.put_setting(
|
||||||
|
"header_nav",
|
||||||
|
[
|
||||||
|
%{"label" => "Blog", "href" => "/blog", "slug" => "blog"},
|
||||||
|
%{"label" => "FAQ", "href" => "/faq", "slug" => "faq"}
|
||||||
|
],
|
||||||
|
"json"
|
||||||
|
)
|
||||||
|
|
||||||
|
{:ok, _view, html} = live(conn, ~p"/")
|
||||||
|
|
||||||
|
assert html =~ "Blog"
|
||||||
|
assert html =~ "FAQ"
|
||||||
|
refute html =~ ~s(>Shop</a>)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "footer navigation" do
|
||||||
|
test "renders default items", %{conn: conn} do
|
||||||
|
{:ok, _view, html} = live(conn, ~p"/")
|
||||||
|
|
||||||
|
assert html =~ "Delivery & returns"
|
||||||
|
assert html =~ "Privacy policy"
|
||||||
|
assert html =~ "Terms of service"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders from saved settings", %{conn: conn} do
|
||||||
|
Settings.put_setting(
|
||||||
|
"footer_nav",
|
||||||
|
[
|
||||||
|
%{"label" => "Returns", "href" => "/returns", "slug" => "returns"},
|
||||||
|
%{"label" => "Shipping", "href" => "/shipping", "slug" => "shipping"}
|
||||||
|
],
|
||||||
|
"json"
|
||||||
|
)
|
||||||
|
|
||||||
|
{:ok, _view, html} = live(conn, ~p"/")
|
||||||
|
|
||||||
|
assert html =~ "Returns"
|
||||||
|
assert html =~ "Shipping"
|
||||||
|
refute html =~ "Privacy policy"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "custom page in navigation" do
|
||||||
|
test "renders when added to header nav", %{conn: conn} do
|
||||||
|
{:ok, _} = Pages.create_custom_page(%{slug: "faq", title: "FAQ"})
|
||||||
|
|
||||||
|
Settings.put_setting(
|
||||||
|
"header_nav",
|
||||||
|
[
|
||||||
|
%{"label" => "Home", "href" => "/", "slug" => "home"},
|
||||||
|
%{"label" => "FAQ", "href" => "/faq", "slug" => "faq"}
|
||||||
|
],
|
||||||
|
"json"
|
||||||
|
)
|
||||||
|
|
||||||
|
{:ok, _view, html} = live(conn, ~p"/")
|
||||||
|
|
||||||
|
assert html =~ ~s(href="/faq")
|
||||||
|
assert html =~ "FAQ"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in New Issue
Block a user