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:
parent
deea04885f
commit
26d3bd782a
19
lib/simpleshop_theme_web/admin_layout_hook.ex
Normal file
19
lib/simpleshop_theme_web/admin_layout_hook.ex
Normal 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
|
||||||
@ -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">→</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.
|
||||||
|
|
||||||
|
|||||||
102
lib/simpleshop_theme_web/components/layouts/admin.html.heex
Normal file
102
lib/simpleshop_theme_web/components/layouts/admin.html.heex
Normal 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} />
|
||||||
@ -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>
|
||||||
@ -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">← 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>
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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"
|
>
|
||||||
>
|
← Orders
|
||||||
← 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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
91
test/simpleshop_theme_web/live/admin/layout_test.exs
Normal file
91
test/simpleshop_theme_web/live/admin/layout_test.exs
Normal 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
|
||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user