add admin sidebar layout with responsive drawer navigation

- New admin root + child layouts with daisyUI drawer sidebar
- AdminLayoutHook tracks current path for active nav highlighting
- Split router into :admin, :admin_theme, :user_settings live_sessions
- Theme editor stays full-screen with back link to admin
- Admin bar on shop pages for logged-in users (mount_current_scope)
- Strip Layouts.app wrapper from admin LiveViews
- Remove nav from root.html.heex (now only serves auth pages)
- 9 new layout tests covering sidebar, active state, theme editor, admin bar

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey 2026-02-12 08:35:22 +00:00
parent deea04885f
commit 26d3bd782a
17 changed files with 756 additions and 541 deletions

View File

@ -0,0 +1,19 @@
defmodule SimpleshopThemeWeb.AdminLayoutHook do
@moduledoc """
LiveView on_mount hook that assigns the current path for admin sidebar navigation.
"""
import Phoenix.Component
def on_mount(:assign_current_path, _params, _session, socket) do
socket =
socket
|> assign(:current_path, "")
|> Phoenix.LiveView.attach_hook(:set_current_path, :handle_params, fn _params,
uri,
socket ->
{:cont, assign(socket, :current_path, URI.parse(uri).path)}
end)
{:cont, socket}
end
end

View File

@ -35,35 +35,8 @@ defmodule SimpleshopThemeWeb.Layouts do
def app(assigns) do def app(assigns) do
~H""" ~H"""
<header class="navbar px-4 sm:px-6 lg:px-8"> <main class="px-4 py-12 sm:px-6 lg:px-8">
<div class="flex-1"> <div class="mx-auto max-w-lg flex flex-col gap-4">
<a href="/" class="flex-1 flex w-fit items-center gap-2">
<img src={~p"/images/logo.svg"} width="36" />
<span class="text-sm font-semibold">v{Application.spec(:phoenix, :vsn)}</span>
</a>
</div>
<div class="flex-none">
<ul class="flex flex-column px-1 space-x-4 items-center">
<li>
<a href="https://phoenixframework.org/" class="btn btn-ghost">Website</a>
</li>
<li>
<a href="https://github.com/phoenixframework/phoenix" class="btn btn-ghost">GitHub</a>
</li>
<li>
<.theme_toggle />
</li>
<li>
<a href="https://hexdocs.pm/phoenix/overview.html" class="btn btn-primary">
Get Started <span aria-hidden="true">&rarr;</span>
</a>
</li>
</ul>
</div>
</header>
<main class="px-4 py-20 sm:px-6 lg:px-8">
<div class="mx-auto max-w-2xl flex flex-col gap-4">
{render_slot(@inner_block)} {render_slot(@inner_block)}
</div> </div>
</main> </main>
@ -72,6 +45,11 @@ defmodule SimpleshopThemeWeb.Layouts do
""" """
end end
@doc false
def admin_nav_active?(current_path, link_path) do
if String.starts_with?(current_path, link_path), do: "active", else: nil
end
@doc """ @doc """
Shows the flash group with standard titles and content. Shows the flash group with standard titles and content.

View File

@ -0,0 +1,102 @@
<div class="drawer lg:drawer-open h-full">
<input id="admin-drawer" type="checkbox" class="drawer-toggle" />
<%!-- main content area --%>
<div class="drawer-content flex flex-col min-h-screen">
<%!-- mobile header --%>
<header class="navbar bg-base-100 border-b border-base-200 lg:hidden">
<div class="flex-none">
<label for="admin-drawer" class="btn btn-square btn-ghost" aria-label="Open navigation">
<.icon name="hero-bars-3" class="size-5" />
</label>
</div>
<div class="flex-1">
<span class="text-lg font-semibold">SimpleShop</span>
</div>
<div class="flex-none">
<.link href={~p"/"} class="btn btn-ghost btn-sm">
<.icon name="hero-arrow-top-right-on-square-mini" class="size-4" /> Shop
</.link>
</div>
</header>
<%!-- page content --%>
<main class="flex-1 p-4 sm:p-6 lg:p-8">
<div class="mx-auto max-w-5xl">
{@inner_content}
</div>
</main>
</div>
<%!-- sidebar --%>
<div class="drawer-side z-40">
<label for="admin-drawer" class="drawer-overlay" aria-label="Close navigation"></label>
<aside class="bg-base-200 w-64 min-h-full flex flex-col">
<%!-- sidebar header --%>
<div class="p-4 border-b border-base-300">
<.link navigate={~p"/admin/orders"} class="text-lg font-bold tracking-tight">
SimpleShop
</.link>
<p class="text-xs text-base-content/60 mt-0.5 truncate">
{@current_scope.user.email}
</p>
</div>
<%!-- nav links --%>
<nav class="flex-1 p-2" aria-label="Admin navigation">
<ul class="menu gap-0.5">
<li>
<.link
navigate={~p"/admin/orders"}
class={admin_nav_active?(@current_path, "/admin/orders")}
>
<.icon name="hero-shopping-bag" class="size-5" /> Orders
</.link>
</li>
<li>
<.link
href={~p"/admin/theme"}
class={admin_nav_active?(@current_path, "/admin/theme")}
>
<.icon name="hero-paint-brush" class="size-5" /> Theme
</.link>
</li>
<li>
<.link
navigate={~p"/admin/providers"}
class={admin_nav_active?(@current_path, "/admin/providers")}
>
<.icon name="hero-cube" class="size-5" /> Providers
</.link>
</li>
<li>
<.link
navigate={~p"/admin/settings"}
class={admin_nav_active?(@current_path, "/admin/settings")}
>
<.icon name="hero-cog-6-tooth" class="size-5" /> Settings
</.link>
</li>
</ul>
</nav>
<%!-- sidebar footer --%>
<div class="p-2 border-t border-base-300">
<ul class="menu gap-0.5">
<li>
<.link href={~p"/"}>
<.icon name="hero-arrow-top-right-on-square" class="size-5" /> View shop
</.link>
</li>
<li>
<.link href={~p"/users/log-out"} method="delete">
<.icon name="hero-arrow-right-start-on-rectangle" class="size-5" /> Log out
</.link>
</li>
</ul>
</div>
</aside>
</div>
</div>
<.flash_group flash={@flash} />

View File

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en" class="h-full">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content={get_csrf_token()} />
<.live_title default="Admin" suffix=" · SimpleShop">
{assigns[:page_title]}
</.live_title>
<link phx-track-static rel="stylesheet" href={~p"/assets/css/app.css"} />
<script defer phx-track-static src={~p"/assets/js/app.js"}>
</script>
<script>
(() => {
const setTheme = (theme) => {
if (theme === "system") {
localStorage.removeItem("phx:theme");
document.documentElement.removeAttribute("data-theme");
} else {
localStorage.setItem("phx:theme", theme);
document.documentElement.setAttribute("data-theme", theme);
}
};
if (!document.documentElement.hasAttribute("data-theme")) {
setTheme(localStorage.getItem("phx:theme") || "system");
}
window.addEventListener("storage", (e) => e.key === "phx:theme" && setTheme(e.newValue || "system"));
window.addEventListener("phx:set-theme", (e) => setTheme(e.target.dataset.phxTheme));
})();
</script>
</head>
<body class="h-full">
{@inner_content}
</body>
</html>

View File

@ -31,42 +31,6 @@
</script> </script>
</head> </head>
<body> <body>
<nav class="w-full relative z-10 flex items-center px-4 sm:px-6 lg:px-8 justify-between">
<div>
<%= if @current_scope && assigns[:conn] && @conn.request_path == "/admin/theme" do %>
<.link href={~p"/"} class="text-sm hover:underline">&larr; View Shop</.link>
<% end %>
</div>
<ul class="menu menu-horizontal flex items-center gap-4">
<%= if @current_scope do %>
<li>
{@current_scope.user.email}
</li>
<li>
<.link href={~p"/admin/theme"}>Theme</.link>
</li>
<li>
<.link href={~p"/admin/orders"}>Orders</.link>
</li>
<li>
<.link href={~p"/admin/settings"}>Credentials</.link>
</li>
<li>
<.link href={~p"/users/settings"}>Settings</.link>
</li>
<li>
<.link href={~p"/users/log-out"} method="delete">Log out</.link>
</li>
<% else %>
<li>
<.link href={~p"/users/register"}>Register</.link>
</li>
<li>
<.link href={~p"/users/log-in"}>Log in</.link>
</li>
<% end %>
</ul>
</nav>
{@inner_content} {@inner_content}
</body> </body>
</html> </html>

View File

@ -1,2 +1,13 @@
<div
:if={assigns[:current_scope]}
style="background-color: var(--t-surface-raised, #f5f5f5); border-bottom: 1px solid var(--t-border-default, #e5e5e5); padding: 0.25rem 1rem; font-size: 0.75rem; text-align: right;"
>
<.link
href={~p"/admin/orders"}
style="color: var(--t-text-secondary, #666); text-decoration: none;"
>
Admin
</.link>
</div>
<.shop_flash_group flash={@flash} /> <.shop_flash_group flash={@flash} />
{@inner_content} {@inner_content}

View File

@ -2,6 +2,6 @@ defmodule SimpleshopThemeWeb.AdminController do
use SimpleshopThemeWeb, :controller use SimpleshopThemeWeb, :controller
def index(conn, _params) do def index(conn, _params) do
redirect(conn, to: ~p"/admin/theme") redirect(conn, to: ~p"/admin/orders")
end end
end end

View File

@ -28,169 +28,167 @@ defmodule SimpleshopThemeWeb.Admin.OrderShow do
@impl true @impl true
def render(assigns) do def render(assigns) do
~H""" ~H"""
<Layouts.app flash={@flash} current_scope={@current_scope}> <.header>
<.header> <.link
<.link navigate={~p"/admin/orders"}
navigate={~p"/admin/orders"} class="text-sm font-normal text-base-content/60 hover:underline"
class="text-sm font-normal text-base-content/60 hover:underline" >
> &larr; Orders
&larr; Orders </.link>
</.link> <div class="flex items-center gap-3 mt-1">
<div class="flex items-center gap-3 mt-1"> <span class="text-2xl font-bold">{@order.order_number}</span>
<span class="text-2xl font-bold">{@order.order_number}</span> <.status_badge status={@order.payment_status} />
<.status_badge status={@order.payment_status} />
</div>
</.header>
<div class="grid gap-6 mt-6 lg:grid-cols-2">
<%!-- order info --%>
<div class="card bg-base-100 shadow-sm border border-base-200">
<div class="card-body">
<h3 class="card-title text-base">Order details</h3>
<.list>
<:item title="Date">{format_date(@order.inserted_at)}</:item>
<:item title="Customer">{@order.customer_email || ""}</:item>
<:item title="Payment status">
<.status_badge status={@order.payment_status} />
</:item>
<:item :if={@order.stripe_payment_intent_id} title="Stripe payment">
<code class="text-xs">{@order.stripe_payment_intent_id}</code>
</:item>
<:item title="Currency">{String.upcase(@order.currency)}</:item>
</.list>
</div>
</div>
<%!-- shipping address --%>
<div class="card bg-base-100 shadow-sm border border-base-200">
<div class="card-body">
<h3 class="card-title text-base">Shipping address</h3>
<%= if @order.shipping_address != %{} do %>
<.list>
<:item :if={@order.shipping_address["name"]} title="Name">
{@order.shipping_address["name"]}
</:item>
<:item :if={@order.shipping_address["line1"]} title="Address">
{@order.shipping_address["line1"]}
<span :if={@order.shipping_address["line2"]}>
<br />{@order.shipping_address["line2"]}
</span>
</:item>
<:item :if={@order.shipping_address["city"]} title="City">
{@order.shipping_address["city"]}
</:item>
<:item :if={@order.shipping_address["state"] not in [nil, ""]} title="State">
{@order.shipping_address["state"]}
</:item>
<:item :if={@order.shipping_address["postal_code"]} title="Postcode">
{@order.shipping_address["postal_code"]}
</:item>
<:item :if={@order.shipping_address["country"]} title="Country">
{@order.shipping_address["country"]}
</:item>
</.list>
<% else %>
<p class="text-base-content/60 text-sm">No shipping address provided</p>
<% end %>
</div>
</div>
</div> </div>
</.header>
<%!-- fulfilment --%> <div class="grid gap-6 mt-6 lg:grid-cols-2">
<div class="card bg-base-100 shadow-sm border border-base-200 mt-6"> <%!-- order info --%>
<div class="card bg-base-100 shadow-sm border border-base-200">
<div class="card-body"> <div class="card-body">
<div class="flex items-center justify-between"> <h3 class="card-title text-base">Order details</h3>
<h3 class="card-title text-base">Fulfilment</h3>
<.fulfilment_badge status={@order.fulfilment_status} />
</div>
<.list> <.list>
<:item :if={@order.provider_order_id} title="Provider order ID"> <:item title="Date">{format_date(@order.inserted_at)}</:item>
<code class="text-xs">{@order.provider_order_id}</code> <:item title="Customer">{@order.customer_email || ""}</:item>
<:item title="Payment status">
<.status_badge status={@order.payment_status} />
</:item> </:item>
<:item :if={@order.provider_status} title="Provider status"> <:item :if={@order.stripe_payment_intent_id} title="Stripe payment">
{@order.provider_status} <code class="text-xs">{@order.stripe_payment_intent_id}</code>
</:item>
<:item :if={@order.submitted_at} title="Submitted">
{format_date(@order.submitted_at)}
</:item>
<:item :if={@order.tracking_number} title="Tracking">
<%= if @order.tracking_url do %>
<a href={@order.tracking_url} target="_blank" class="link link-primary">
{@order.tracking_number}
</a>
<% else %>
{@order.tracking_number}
<% end %>
</:item>
<:item :if={@order.shipped_at} title="Shipped">
{format_date(@order.shipped_at)}
</:item>
<:item :if={@order.delivered_at} title="Delivered">
{format_date(@order.delivered_at)}
</:item>
<:item :if={@order.fulfilment_error} title="Error">
<span class="text-error text-sm">{@order.fulfilment_error}</span>
</:item> </:item>
<:item title="Currency">{String.upcase(@order.currency)}</:item>
</.list> </.list>
<div class="flex gap-2 mt-4">
<button
:if={can_submit?(@order)}
phx-click="submit_to_provider"
class="btn btn-primary btn-sm"
>
<.icon name="hero-paper-airplane-mini" class="size-4" />
{if @order.fulfilment_status == "failed",
do: "Retry submission",
else: "Submit to provider"}
</button>
<button
:if={can_refresh?(@order)}
phx-click="refresh_status"
class="btn btn-ghost btn-sm"
>
<.icon name="hero-arrow-path-mini" class="size-4" /> Refresh status
</button>
</div>
</div> </div>
</div> </div>
<%!-- line items --%> <%!-- shipping address --%>
<div class="card bg-base-100 shadow-sm border border-base-200 mt-6"> <div class="card bg-base-100 shadow-sm border border-base-200">
<div class="card-body"> <div class="card-body">
<h3 class="card-title text-base">Items</h3> <h3 class="card-title text-base">Shipping address</h3>
<table class="table table-zebra"> <%= if @order.shipping_address != %{} do %>
<thead> <.list>
<tr> <:item :if={@order.shipping_address["name"]} title="Name">
<th>Product</th> {@order.shipping_address["name"]}
<th>Variant</th> </:item>
<th class="text-right">Qty</th> <:item :if={@order.shipping_address["line1"]} title="Address">
<th class="text-right">Unit price</th> {@order.shipping_address["line1"]}
<th class="text-right">Total</th> <span :if={@order.shipping_address["line2"]}>
</tr> <br />{@order.shipping_address["line2"]}
</thead> </span>
<tbody> </:item>
<tr :for={item <- @order.items}> <:item :if={@order.shipping_address["city"]} title="City">
<td>{item.product_name}</td> {@order.shipping_address["city"]}
<td>{item.variant_title}</td> </:item>
<td class="text-right">{item.quantity}</td> <:item :if={@order.shipping_address["state"] not in [nil, ""]} title="State">
<td class="text-right">{Cart.format_price(item.unit_price)}</td> {@order.shipping_address["state"]}
<td class="text-right">{Cart.format_price(item.unit_price * item.quantity)}</td> </:item>
</tr> <:item :if={@order.shipping_address["postal_code"]} title="Postcode">
</tbody> {@order.shipping_address["postal_code"]}
<tfoot> </:item>
<tr> <:item :if={@order.shipping_address["country"]} title="Country">
<td colspan="4" class="text-right font-medium">Subtotal</td> {@order.shipping_address["country"]}
<td class="text-right font-medium">{Cart.format_price(@order.subtotal)}</td> </:item>
</tr> </.list>
<tr class="text-lg"> <% else %>
<td colspan="4" class="text-right font-bold">Total</td> <p class="text-base-content/60 text-sm">No shipping address provided</p>
<td class="text-right font-bold">{Cart.format_price(@order.total)}</td> <% end %>
</tr>
</tfoot>
</table>
</div> </div>
</div> </div>
</Layouts.app> </div>
<%!-- fulfilment --%>
<div class="card bg-base-100 shadow-sm border border-base-200 mt-6">
<div class="card-body">
<div class="flex items-center justify-between">
<h3 class="card-title text-base">Fulfilment</h3>
<.fulfilment_badge status={@order.fulfilment_status} />
</div>
<.list>
<:item :if={@order.provider_order_id} title="Provider order ID">
<code class="text-xs">{@order.provider_order_id}</code>
</:item>
<:item :if={@order.provider_status} title="Provider status">
{@order.provider_status}
</:item>
<:item :if={@order.submitted_at} title="Submitted">
{format_date(@order.submitted_at)}
</:item>
<:item :if={@order.tracking_number} title="Tracking">
<%= if @order.tracking_url do %>
<a href={@order.tracking_url} target="_blank" class="link link-primary">
{@order.tracking_number}
</a>
<% else %>
{@order.tracking_number}
<% end %>
</:item>
<:item :if={@order.shipped_at} title="Shipped">
{format_date(@order.shipped_at)}
</:item>
<:item :if={@order.delivered_at} title="Delivered">
{format_date(@order.delivered_at)}
</:item>
<:item :if={@order.fulfilment_error} title="Error">
<span class="text-error text-sm">{@order.fulfilment_error}</span>
</:item>
</.list>
<div class="flex gap-2 mt-4">
<button
:if={can_submit?(@order)}
phx-click="submit_to_provider"
class="btn btn-primary btn-sm"
>
<.icon name="hero-paper-airplane-mini" class="size-4" />
{if @order.fulfilment_status == "failed",
do: "Retry submission",
else: "Submit to provider"}
</button>
<button
:if={can_refresh?(@order)}
phx-click="refresh_status"
class="btn btn-ghost btn-sm"
>
<.icon name="hero-arrow-path-mini" class="size-4" /> Refresh status
</button>
</div>
</div>
</div>
<%!-- line items --%>
<div class="card bg-base-100 shadow-sm border border-base-200 mt-6">
<div class="card-body">
<h3 class="card-title text-base">Items</h3>
<table class="table table-zebra">
<thead>
<tr>
<th>Product</th>
<th>Variant</th>
<th class="text-right">Qty</th>
<th class="text-right">Unit price</th>
<th class="text-right">Total</th>
</tr>
</thead>
<tbody>
<tr :for={item <- @order.items}>
<td>{item.product_name}</td>
<td>{item.variant_title}</td>
<td class="text-right">{item.quantity}</td>
<td class="text-right">{Cart.format_price(item.unit_price)}</td>
<td class="text-right">{Cart.format_price(item.unit_price * item.quantity)}</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="4" class="text-right font-medium">Subtotal</td>
<td class="text-right font-medium">{Cart.format_price(@order.subtotal)}</td>
</tr>
<tr class="text-lg">
<td colspan="4" class="text-right font-bold">Total</td>
<td class="text-right font-bold">{Cart.format_price(@order.total)}</td>
</tr>
</tfoot>
</table>
</div>
</div>
""" """
end end

View File

@ -36,67 +36,65 @@ defmodule SimpleshopThemeWeb.Admin.Orders do
@impl true @impl true
def render(assigns) do def render(assigns) do
~H""" ~H"""
<Layouts.app flash={@flash} current_scope={@current_scope}> <.header>
<.header> Orders
Orders </.header>
</.header>
<div class="flex gap-2 mt-6 mb-4 flex-wrap"> <div class="flex gap-2 mt-6 mb-4 flex-wrap">
<.filter_tab <.filter_tab
status="all" status="all"
label="All" label="All"
count={total_count(@status_counts)} count={total_count(@status_counts)}
active={@status_filter} active={@status_filter}
/> />
<.filter_tab <.filter_tab
status="paid" status="paid"
label="Paid" label="Paid"
count={@status_counts["paid"]} count={@status_counts["paid"]}
active={@status_filter} active={@status_filter}
/> />
<.filter_tab <.filter_tab
status="pending" status="pending"
label="Pending" label="Pending"
count={@status_counts["pending"]} count={@status_counts["pending"]}
active={@status_filter} active={@status_filter}
/> />
<.filter_tab <.filter_tab
status="failed" status="failed"
label="Failed" label="Failed"
count={@status_counts["failed"]} count={@status_counts["failed"]}
active={@status_filter} active={@status_filter}
/> />
<.filter_tab <.filter_tab
status="refunded" status="refunded"
label="Refunded" label="Refunded"
count={@status_counts["refunded"]} count={@status_counts["refunded"]}
active={@status_filter} active={@status_filter}
/> />
</div> </div>
<.table <.table
:if={@order_count > 0} :if={@order_count > 0}
id="orders" id="orders"
rows={@streams.orders} rows={@streams.orders}
row_item={fn {_id, order} -> order end} row_item={fn {_id, order} -> order end}
row_click={fn {_id, order} -> JS.navigate(~p"/admin/orders/#{order}") end} row_click={fn {_id, order} -> JS.navigate(~p"/admin/orders/#{order}") end}
> >
<:col :let={order} label="Order">{order.order_number}</:col> <:col :let={order} label="Order">{order.order_number}</:col>
<:col :let={order} label="Date">{format_date(order.inserted_at)}</:col> <:col :let={order} label="Date">{format_date(order.inserted_at)}</:col>
<:col :let={order} label="Customer">{order.customer_email || ""}</:col> <:col :let={order} label="Customer">{order.customer_email || ""}</:col>
<:col :let={order} label="Total">{Cart.format_price(order.total)}</:col> <:col :let={order} label="Total">{Cart.format_price(order.total)}</:col>
<:col :let={order} label="Status"><.status_badge status={order.payment_status} /></:col> <:col :let={order} label="Status"><.status_badge status={order.payment_status} /></:col>
<:col :let={order} label="Fulfilment"> <:col :let={order} label="Fulfilment">
<.fulfilment_badge status={order.fulfilment_status} /> <.fulfilment_badge status={order.fulfilment_status} />
</:col> </:col>
</.table> </.table>
<div :if={@order_count == 0} class="text-center py-12 text-base-content/60"> <div :if={@order_count == 0} class="text-center py-12 text-base-content/60">
<.icon name="hero-inbox" class="size-12 mx-auto mb-4" /> <.icon name="hero-inbox" class="size-12 mx-auto mb-4" />
<p class="text-lg font-medium">No orders yet</p> <p class="text-lg font-medium">No orders yet</p>
<p class="text-sm mt-1">Orders will appear here once customers check out.</p> <p class="text-sm mt-1">Orders will appear here once customers check out.</p>
</div> </div>
</Layouts.app>
""" """
end end

View File

@ -1,104 +1,102 @@
<Layouts.app flash={@flash}> <.header>
<.header> {if @live_action == :new, do: "Connect to Printify", else: "Printify settings"}
{if @live_action == :new, do: "Connect to Printify", else: "Printify settings"} </.header>
</.header>
<div class="max-w-xl mt-6"> <div class="max-w-xl mt-6">
<%= if @live_action == :new do %> <%= if @live_action == :new do %>
<div class="prose prose-sm mb-6"> <div class="prose prose-sm mb-6">
<p> <p>
Printify is a print-on-demand service that prints and ships products for you. Printify is a print-on-demand service that prints and ships products for you.
Connect your account to automatically import your products into your shop. Connect your account to automatically import your products into your shop.
</p> </p>
</div> </div>
<div class="rounded-lg bg-base-200 p-4 mb-6 text-sm"> <div class="rounded-lg bg-base-200 p-4 mb-6 text-sm">
<p class="font-medium mb-2">Get your connection key from Printify:</p> <p class="font-medium mb-2">Get your connection key from Printify:</p>
<ol class="list-decimal list-inside space-y-1 text-base-content/80"> <ol class="list-decimal list-inside space-y-1 text-base-content/80">
<li> <li>
<a <a
href="https://printify.com/app/auth/login" href="https://printify.com/app/auth/login"
target="_blank" target="_blank"
rel="noopener" rel="noopener"
class="link" class="link"
> >
Log in to Printify Log in to Printify
</a> </a>
(or <a (or <a
href="https://printify.com/app/auth/register" href="https://printify.com/app/auth/register"
target="_blank" target="_blank"
rel="noopener" rel="noopener"
class="link" class="link"
>create a free account</a>) >create a free account</a>)
</li> </li>
<li>Click <strong>Account</strong> (top right)</li> <li>Click <strong>Account</strong> (top right)</li>
<li>Select <strong>Connections</strong> from the dropdown</li> <li>Select <strong>Connections</strong> from the dropdown</li>
<li>Find <strong>API tokens</strong> and click <strong>Generate</strong></li> <li>Find <strong>API tokens</strong> and click <strong>Generate</strong></li>
<li> <li>
Enter a name (e.g. "My Shop"), keep <strong>all scopes</strong> Enter a name (e.g. "My Shop"), keep <strong>all scopes</strong>
selected, and click <strong>Generate token</strong> selected, and click <strong>Generate token</strong>
</li> </li>
<li>Click <strong>Copy to clipboard</strong> and paste it below</li> <li>Click <strong>Copy to clipboard</strong> and paste it below</li>
</ol> </ol>
</div>
<% end %>
<.form for={@form} id="provider-form" phx-change="validate" phx-submit="save">
<input type="hidden" name="provider_connection[provider_type]" value="printify" />
<.input
field={@form[:api_key]}
type="password"
label="Printify connection key"
placeholder={
if @live_action == :edit,
do: "Leave blank to keep current key",
else: "Paste your key here"
}
autocomplete="off"
/>
<div class="flex items-center gap-3 mb-6">
<button
type="button"
class="btn btn-outline btn-sm"
phx-click="test_connection"
disabled={@testing}
>
<.icon
name={if @testing, do: "hero-arrow-path", else: "hero-signal"}
class={if @testing, do: "size-4 animate-spin", else: "size-4"}
/>
{if @testing, do: "Checking...", else: "Check connection"}
</button>
<div :if={@test_result} class="text-sm">
<%= case @test_result do %>
<% {:ok, info} -> %>
<span class="text-success flex items-center gap-1">
<.icon name="hero-check-circle" class="size-4" /> Connected to {info.shop_name}
</span>
<% {:error, reason} -> %>
<span class="text-error flex items-center gap-1">
<.icon name="hero-x-circle" class="size-4" />
{format_error(reason)}
</span>
<% end %>
</div> </div>
</div>
<%= if @live_action == :edit do %>
<.input field={@form[:enabled]} type="checkbox" label="Connection enabled" />
<% end %> <% end %>
<.form for={@form} id="provider-form" phx-change="validate" phx-submit="save"> <div class="flex gap-2 mt-6">
<input type="hidden" name="provider_connection[provider_type]" value="printify" /> <.button type="submit" disabled={@testing}>
{if @live_action == :new, do: "Connect to Printify", else: "Save changes"}
<.input </.button>
field={@form[:api_key]} <.link navigate={~p"/admin/providers"} class="btn btn-ghost">
type="password" Cancel
label="Printify connection key" </.link>
placeholder={ </div>
if @live_action == :edit, </.form>
do: "Leave blank to keep current key", </div>
else: "Paste your key here"
}
autocomplete="off"
/>
<div class="flex items-center gap-3 mb-6">
<button
type="button"
class="btn btn-outline btn-sm"
phx-click="test_connection"
disabled={@testing}
>
<.icon
name={if @testing, do: "hero-arrow-path", else: "hero-signal"}
class={if @testing, do: "size-4 animate-spin", else: "size-4"}
/>
{if @testing, do: "Checking...", else: "Check connection"}
</button>
<div :if={@test_result} class="text-sm">
<%= case @test_result do %>
<% {:ok, info} -> %>
<span class="text-success flex items-center gap-1">
<.icon name="hero-check-circle" class="size-4" /> Connected to {info.shop_name}
</span>
<% {:error, reason} -> %>
<span class="text-error flex items-center gap-1">
<.icon name="hero-x-circle" class="size-4" />
{format_error(reason)}
</span>
<% end %>
</div>
</div>
<%= if @live_action == :edit do %>
<.input field={@form[:enabled]} type="checkbox" label="Connection enabled" />
<% end %>
<div class="flex gap-2 mt-6">
<.button type="submit" disabled={@testing}>
{if @live_action == :new, do: "Connect to Printify", else: "Save changes"}
</.button>
<.link navigate={~p"/admin/providers"} class="btn btn-ghost">
Cancel
</.link>
</div>
</.form>
</div>
</Layouts.app>

View File

@ -1,81 +1,79 @@
<Layouts.app flash={@flash}> <.header>
<.header> Providers
Providers <:actions>
<:actions> <.button navigate={~p"/admin/providers/new"}>
<.button navigate={~p"/admin/providers/new"}> <.icon name="hero-plus" class="size-4 mr-1" /> Connect Printify
<.icon name="hero-plus" class="size-4 mr-1" /> Connect Printify </.button>
</.button> </:actions>
</:actions> </.header>
</.header>
<div id="connections" phx-update="stream" class="mt-6 space-y-4"> <div id="connections" phx-update="stream" class="mt-6 space-y-4">
<div class="hidden only:block text-center py-12"> <div class="hidden only:block text-center py-12">
<.icon name="hero-cube" class="size-16 mx-auto mb-4 text-base-content/30" /> <.icon name="hero-cube" class="size-16 mx-auto mb-4 text-base-content/30" />
<h2 class="text-xl font-medium">Connect your Printify account</h2> <h2 class="text-xl font-medium">Connect your Printify account</h2>
<p class="mt-2 text-base-content/60 max-w-md mx-auto"> <p class="mt-2 text-base-content/60 max-w-md mx-auto">
Printify handles printing and shipping for you. Connect your account Printify handles printing and shipping for you. Connect your account
to import your products and start selling. to import your products and start selling.
</p> </p>
<.button navigate={~p"/admin/providers/new"} class="mt-6"> <.button navigate={~p"/admin/providers/new"} class="mt-6">
Connect to Printify Connect to Printify
</.button> </.button>
</div> </div>
<div
:for={{dom_id, connection} <- @streams.connections}
id={dom_id}
class="card bg-base-100 shadow-sm border border-base-200"
>
<div class="card-body">
<div class="flex items-start justify-between gap-4">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<.status_indicator status={connection.sync_status} enabled={connection.enabled} />
<h3 class="font-semibold text-lg">
{String.capitalize(connection.provider_type)}
</h3>
</div>
<p class="text-base-content/70 mt-1">{connection.name}</p>
<div class="flex flex-wrap gap-x-4 gap-y-1 mt-2 text-sm text-base-content/60">
<.connection_info connection={connection} />
</div>
</div>
<div
:for={{dom_id, connection} <- @streams.connections}
id={dom_id}
class="card bg-base-100 shadow-sm border border-base-200"
>
<div class="card-body">
<div class="flex items-start justify-between gap-4">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<.link <.status_indicator status={connection.sync_status} enabled={connection.enabled} />
navigate={~p"/admin/providers/#{connection.id}/edit"} <h3 class="font-semibold text-lg">
class="btn btn-ghost btn-sm" {String.capitalize(connection.provider_type)}
> </h3>
Settings </div>
</.link> <p class="text-base-content/70 mt-1">{connection.name}</p>
<button <div class="flex flex-wrap gap-x-4 gap-y-1 mt-2 text-sm text-base-content/60">
phx-click="delete" <.connection_info connection={connection} />
phx-value-id={connection.id}
data-confirm="Disconnect from Printify? Your synced products will remain in your shop."
class="btn btn-ghost btn-sm text-error"
>
Disconnect
</button>
</div> </div>
</div> </div>
<div class="card-actions justify-end mt-4 pt-4 border-t border-base-200"> <div class="flex items-center gap-2">
<button <.link
phx-click="sync" navigate={~p"/admin/providers/#{connection.id}/edit"}
phx-value-id={connection.id} class="btn btn-ghost btn-sm"
disabled={connection.sync_status == "syncing"}
class="btn btn-outline btn-sm"
> >
<.icon Settings
name="hero-arrow-path" </.link>
class={ <button
if connection.sync_status == "syncing", do: "size-4 animate-spin", else: "size-4" phx-click="delete"
} phx-value-id={connection.id}
/> data-confirm="Disconnect from Printify? Your synced products will remain in your shop."
{if connection.sync_status == "syncing", do: "Syncing...", else: "Sync products"} class="btn btn-ghost btn-sm text-error"
>
Disconnect
</button> </button>
</div> </div>
</div> </div>
<div class="card-actions justify-end mt-4 pt-4 border-t border-base-200">
<button
phx-click="sync"
phx-value-id={connection.id}
disabled={connection.sync_status == "syncing"}
class="btn btn-outline btn-sm"
>
<.icon
name="hero-arrow-path"
class={
if connection.sync_status == "syncing", do: "size-4 animate-spin", else: "size-4"
}
/>
{if connection.sync_status == "syncing", do: "Syncing...", else: "Sync products"}
</button>
</div>
</div> </div>
</div> </div>
</Layouts.app> </div>

View File

@ -119,88 +119,86 @@ defmodule SimpleshopThemeWeb.Admin.Settings do
@impl true @impl true
def render(assigns) do def render(assigns) do
~H""" ~H"""
<Layouts.app flash={@flash} current_scope={@current_scope}> <div class="max-w-2xl">
<div class="max-w-2xl"> <.header>
<.header> Settings
Settings <:subtitle>Shop status, payment providers, and API keys</:subtitle>
<:subtitle>Shop status, payment providers, and API keys</:subtitle> </.header>
</.header>
<section class="mt-10"> <section class="mt-10">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<h2 class="text-lg font-semibold">Shop status</h2> <h2 class="text-lg font-semibold">Shop status</h2>
<%= if @site_live do %> <%= if @site_live do %>
<span class="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-green-600/20 ring-inset"> <span class="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-green-600/20 ring-inset">
<.icon name="hero-check-circle-mini" class="size-3" /> Live <.icon name="hero-check-circle-mini" class="size-3" /> Live
</span> </span>
<% else %>
<span class="inline-flex items-center gap-1 rounded-full bg-zinc-50 px-2 py-1 text-xs font-medium text-zinc-600 ring-1 ring-zinc-500/10 ring-inset">
Offline
</span>
<% end %>
</div>
<p class="mt-2 text-sm text-zinc-600">
<%= if @site_live do %>
Your shop is visible to the public.
<% else %>
Your shop is offline. Visitors see a "coming soon" page.
<% end %>
</p>
<div class="mt-4">
<button
phx-click="toggle_site_live"
class={[
"inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-semibold shadow-xs",
if(@site_live,
do: "bg-zinc-100 text-zinc-700 hover:bg-zinc-200 ring-1 ring-zinc-300 ring-inset",
else: "bg-green-600 text-white hover:bg-green-500"
)
]}
>
<%= if @site_live do %>
<.icon name="hero-eye-slash-mini" class="size-4" /> Take offline
<% else %>
<.icon name="hero-eye-mini" class="size-4" /> Go live
<% end %>
</button>
</div>
</section>
<section class="mt-10">
<div class="flex items-center gap-3">
<h2 class="text-lg font-semibold">Stripe</h2>
<%= case @stripe_status do %>
<% :connected -> %>
<span class="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-green-600/20 ring-inset">
<.icon name="hero-check-circle-mini" class="size-3" /> Connected
</span>
<% :connected_localhost -> %>
<span class="inline-flex items-center gap-1 rounded-full bg-amber-50 px-2 py-1 text-xs font-medium text-amber-700 ring-1 ring-amber-600/20 ring-inset">
<.icon name="hero-exclamation-triangle-mini" class="size-3" /> Dev mode
</span>
<% :not_configured -> %>
<span class="inline-flex items-center gap-1 rounded-full bg-zinc-50 px-2 py-1 text-xs font-medium text-zinc-600 ring-1 ring-zinc-500/10 ring-inset">
Not connected
</span>
<% end %>
</div>
<%= if @stripe_status == :not_configured do %>
<.stripe_setup_form connect_form={@connect_form} connecting={@connecting} />
<% else %> <% else %>
<.stripe_connected_view <span class="inline-flex items-center gap-1 rounded-full bg-zinc-50 px-2 py-1 text-xs font-medium text-zinc-600 ring-1 ring-zinc-500/10 ring-inset">
stripe_status={@stripe_status} Offline
stripe_api_key_hint={@stripe_api_key_hint} </span>
stripe_webhook_url={@stripe_webhook_url}
stripe_signing_secret_hint={@stripe_signing_secret_hint}
stripe_has_signing_secret={@stripe_has_signing_secret}
secret_form={@secret_form}
advanced_open={@advanced_open}
/>
<% end %> <% end %>
</section> </div>
</div> <p class="mt-2 text-sm text-zinc-600">
</Layouts.app> <%= if @site_live do %>
Your shop is visible to the public.
<% else %>
Your shop is offline. Visitors see a "coming soon" page.
<% end %>
</p>
<div class="mt-4">
<button
phx-click="toggle_site_live"
class={[
"inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-semibold shadow-xs",
if(@site_live,
do: "bg-zinc-100 text-zinc-700 hover:bg-zinc-200 ring-1 ring-zinc-300 ring-inset",
else: "bg-green-600 text-white hover:bg-green-500"
)
]}
>
<%= if @site_live do %>
<.icon name="hero-eye-slash-mini" class="size-4" /> Take offline
<% else %>
<.icon name="hero-eye-mini" class="size-4" /> Go live
<% end %>
</button>
</div>
</section>
<section class="mt-10">
<div class="flex items-center gap-3">
<h2 class="text-lg font-semibold">Stripe</h2>
<%= case @stripe_status do %>
<% :connected -> %>
<span class="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-green-600/20 ring-inset">
<.icon name="hero-check-circle-mini" class="size-3" /> Connected
</span>
<% :connected_localhost -> %>
<span class="inline-flex items-center gap-1 rounded-full bg-amber-50 px-2 py-1 text-xs font-medium text-amber-700 ring-1 ring-amber-600/20 ring-inset">
<.icon name="hero-exclamation-triangle-mini" class="size-3" /> Dev mode
</span>
<% :not_configured -> %>
<span class="inline-flex items-center gap-1 rounded-full bg-zinc-50 px-2 py-1 text-xs font-medium text-zinc-600 ring-1 ring-zinc-500/10 ring-inset">
Not connected
</span>
<% end %>
</div>
<%= if @stripe_status == :not_configured do %>
<.stripe_setup_form connect_form={@connect_form} connecting={@connecting} />
<% else %>
<.stripe_connected_view
stripe_status={@stripe_status}
stripe_api_key_hint={@stripe_api_key_hint}
stripe_webhook_url={@stripe_webhook_url}
stripe_signing_secret_hint={@stripe_signing_secret_hint}
stripe_has_signing_secret={@stripe_has_signing_secret}
secret_form={@secret_form}
advanced_open={@advanced_open}
/>
<% end %>
</section>
</div>
""" """
end end

View File

@ -35,7 +35,14 @@
</button> </button>
</div> </div>
<% else %> <% else %>
<!-- Header --> <.link
href={~p"/admin/orders"}
class="inline-flex items-center gap-1 text-sm text-base-content/60 hover:text-base-content mb-4"
>
<.icon name="hero-arrow-left-mini" class="size-4" /> Admin
</.link>
<!-- Header -->
<div class="mb-6 flex items-start justify-between gap-3"> <div class="mb-6 flex items-start justify-between gap-3">
<div class="flex-1"> <div class="flex-1">
<h1 class="text-xl font-semibold tracking-tight mb-2 text-base-content"> <h1 class="text-xl font-semibold tracking-tight mb-2 text-base-content">

View File

@ -28,6 +28,10 @@ defmodule SimpleshopThemeWeb.Router do
plug SimpleshopThemeWeb.Plugs.LoadTheme plug SimpleshopThemeWeb.Plugs.LoadTheme
end end
pipeline :admin do
plug :put_root_layout, html: {SimpleshopThemeWeb.Layouts, :admin_root}
end
# Public storefront (root level) # Public storefront (root level)
scope "/", SimpleshopThemeWeb do scope "/", SimpleshopThemeWeb do
pipe_through [:browser, :shop] pipe_through [:browser, :shop]
@ -43,6 +47,7 @@ defmodule SimpleshopThemeWeb.Router do
live_session :public_shop, live_session :public_shop,
layout: {SimpleshopThemeWeb.Layouts, :shop}, layout: {SimpleshopThemeWeb.Layouts, :shop},
on_mount: [ on_mount: [
{SimpleshopThemeWeb.UserAuth, :mount_current_scope},
{SimpleshopThemeWeb.ThemeHook, :mount_theme}, {SimpleshopThemeWeb.ThemeHook, :mount_theme},
{SimpleshopThemeWeb.ThemeHook, :require_site_live}, {SimpleshopThemeWeb.ThemeHook, :require_site_live},
{SimpleshopThemeWeb.CartHook, :mount_cart} {SimpleshopThemeWeb.CartHook, :mount_cart}
@ -123,27 +128,46 @@ defmodule SimpleshopThemeWeb.Router do
## Authentication routes ## Authentication routes
# /admin redirects to theme editor (requires auth, will redirect to login if needed) # /admin index redirect
scope "/admin", SimpleshopThemeWeb do scope "/admin", SimpleshopThemeWeb do
pipe_through [:browser, :require_authenticated_user] pipe_through [:browser, :require_authenticated_user]
get "/", AdminController, :index get "/", AdminController, :index
end end
# Admin pages with sidebar layout
scope "/admin", SimpleshopThemeWeb do
pipe_through [:browser, :require_authenticated_user, :admin]
live_session :admin,
layout: {SimpleshopThemeWeb.Layouts, :admin},
on_mount: [
{SimpleshopThemeWeb.UserAuth, :require_authenticated},
{SimpleshopThemeWeb.AdminLayoutHook, :assign_current_path}
] do
live "/orders", Admin.Orders, :index
live "/orders/:id", Admin.OrderShow, :show
live "/providers", Admin.Providers.Index, :index
live "/providers/new", Admin.Providers.Form, :new
live "/providers/:id/edit", Admin.Providers.Form, :edit
live "/settings", Admin.Settings, :index
end
# Theme editor: admin root layout but full-screen (no sidebar)
live_session :admin_theme,
on_mount: [{SimpleshopThemeWeb.UserAuth, :require_authenticated}] do
live "/theme", Admin.Theme.Index, :index
end
end
# User account settings
scope "/", SimpleshopThemeWeb do scope "/", SimpleshopThemeWeb do
pipe_through [:browser, :require_authenticated_user] pipe_through [:browser, :require_authenticated_user]
live_session :require_authenticated_user, live_session :user_settings,
on_mount: [{SimpleshopThemeWeb.UserAuth, :require_authenticated}] do on_mount: [{SimpleshopThemeWeb.UserAuth, :require_authenticated}] do
live "/users/settings", Auth.Settings, :edit live "/users/settings", Auth.Settings, :edit
live "/users/settings/confirm-email/:token", Auth.Settings, :confirm_email live "/users/settings/confirm-email/:token", Auth.Settings, :confirm_email
live "/admin/theme", Admin.Theme.Index, :index
live "/admin/providers", Admin.Providers.Index, :index
live "/admin/providers/new", Admin.Providers.Form, :new
live "/admin/providers/:id/edit", Admin.Providers.Form, :edit
live "/admin/orders", Admin.Orders, :index
live "/admin/orders/:id", Admin.OrderShow, :show
live "/admin/settings", Admin.Settings, :index
end end
post "/users/update-password", UserSessionController, :update_password post "/users/update-password", UserSessionController, :update_password

View File

@ -20,12 +20,10 @@ defmodule SimpleshopThemeWeb.UserSessionControllerTest do
assert get_session(conn, :user_token) assert get_session(conn, :user_token)
assert redirected_to(conn) == ~p"/" assert redirected_to(conn) == ~p"/"
# Now do a logged in request to an admin page and assert on the menu # Now do a logged in request and assert on the page content
conn = get(conn, ~p"/users/settings") conn = get(conn, ~p"/users/settings")
response = html_response(conn, 200) response = html_response(conn, 200)
assert response =~ user.email assert response =~ user.email
assert response =~ ~p"/users/settings"
assert response =~ ~p"/users/log-out"
end end
test "logs the user in with remember me", %{conn: conn, user: user} do test "logs the user in with remember me", %{conn: conn, user: user} do
@ -84,12 +82,10 @@ defmodule SimpleshopThemeWeb.UserSessionControllerTest do
assert get_session(conn, :user_token) assert get_session(conn, :user_token)
assert redirected_to(conn) == ~p"/" assert redirected_to(conn) == ~p"/"
# Now do a logged in request to an admin page and assert on the menu # Now do a logged in request and assert on the page content
conn = get(conn, ~p"/users/settings") conn = get(conn, ~p"/users/settings")
response = html_response(conn, 200) response = html_response(conn, 200)
assert response =~ user.email assert response =~ user.email
assert response =~ ~p"/users/settings"
assert response =~ ~p"/users/log-out"
end end
test "confirms unconfirmed user", %{conn: conn, unconfirmed_user: user} do test "confirms unconfirmed user", %{conn: conn, unconfirmed_user: user} do
@ -108,12 +104,10 @@ defmodule SimpleshopThemeWeb.UserSessionControllerTest do
assert Accounts.get_user!(user.id).confirmed_at assert Accounts.get_user!(user.id).confirmed_at
# Now do a logged in request to an admin page and assert on the menu # Now do a logged in request and assert on the page content
conn = get(conn, ~p"/users/settings") conn = get(conn, ~p"/users/settings")
response = html_response(conn, 200) response = html_response(conn, 200)
assert response =~ user.email assert response =~ user.email
assert response =~ ~p"/users/settings"
assert response =~ ~p"/users/log-out"
end end
test "redirects to login page when magic link is invalid", %{conn: conn} do test "redirects to login page when magic link is invalid", %{conn: conn} do

View File

@ -0,0 +1,91 @@
defmodule SimpleshopThemeWeb.Admin.LayoutTest do
use SimpleshopThemeWeb.ConnCase, async: false
import Phoenix.LiveViewTest
import SimpleshopTheme.AccountsFixtures
setup do
user = user_fixture()
%{user: user}
end
describe "admin sidebar" do
setup %{conn: conn, user: user} do
%{conn: log_in_user(conn, user)}
end
test "renders sidebar nav links on admin pages", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/orders")
assert has_element?(view, ~s(a[href="/admin/orders"]), "Orders")
assert has_element?(view, ~s(a[href="/admin/theme"]), "Theme")
assert has_element?(view, ~s(a[href="/admin/providers"]), "Providers")
assert has_element?(view, ~s(a[href="/admin/settings"]), "Settings")
end
test "highlights active nav link for current page", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/orders")
assert has_element?(view, ~s(a.active[href="/admin/orders"]))
refute has_element?(view, ~s(a.active[href="/admin/settings"]))
end
test "highlights correct link on different pages", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/settings")
assert has_element?(view, ~s(a.active[href="/admin/settings"]))
refute has_element?(view, ~s(a.active[href="/admin/orders"]))
end
test "shows user email in sidebar", %{conn: conn, user: user} do
{:ok, _view, html} = live(conn, ~p"/admin/orders")
assert html =~ user.email
end
test "shows view shop and log out links", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/orders")
assert has_element?(view, ~s(a[href="/"]), "View shop")
assert has_element?(view, ~s(a[href="/users/log-out"]), "Log out")
end
end
describe "theme editor layout" do
setup %{conn: conn, user: user} do
%{conn: log_in_user(conn, user)}
end
test "does not render sidebar", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/admin/theme")
refute html =~ "admin-drawer"
end
test "shows back link to admin", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/theme")
assert has_element?(view, ~s(a[href="/admin/orders"]), "Admin")
end
end
describe "admin bar on shop pages" do
setup do
{:ok, _} = SimpleshopTheme.Settings.set_site_live(true)
:ok
end
test "shows admin link when logged in", %{conn: conn, user: user} do
conn = log_in_user(conn, user)
{:ok, _view, html} = live(conn, ~p"/")
assert html =~ ~s(href="/admin/orders")
end
test "does not show admin link when logged out", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/")
refute html =~ ~s(href="/admin/orders")
end
end
end

View File

@ -9,7 +9,7 @@ defmodule SimpleshopThemeWeb.Auth.LoginTest do
{:ok, _lv, html} = live(conn, ~p"/users/log-in") {:ok, _lv, html} = live(conn, ~p"/users/log-in")
assert html =~ "Log in" assert html =~ "Log in"
assert html =~ "Register" assert html =~ "Sign up"
assert html =~ "Log in with email" assert html =~ "Log in with email"
end end
end end