From 26d3bd782a739e72665a8ce3cfc6cd3b232ed241 Mon Sep 17 00:00:00 2001 From: jamey Date: Thu, 12 Feb 2026 08:35:22 +0000 Subject: [PATCH] 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 --- lib/simpleshop_theme_web/admin_layout_hook.ex | 19 ++ .../components/layouts.ex | 36 +-- .../components/layouts/admin.html.heex | 102 ++++++ .../components/layouts/admin_root.html.heex | 35 ++ .../components/layouts/root.html.heex | 36 --- .../components/layouts/shop.html.heex | 11 + .../controllers/admin_controller.ex | 2 +- .../live/admin/order_show.ex | 298 +++++++++--------- lib/simpleshop_theme_web/live/admin/orders.ex | 114 ++++--- .../live/admin/providers/form.html.heex | 196 ++++++------ .../live/admin/providers/index.html.heex | 136 ++++---- .../live/admin/settings.ex | 156 +++++---- .../live/admin/theme/index.html.heex | 9 +- lib/simpleshop_theme_web/router.ex | 42 ++- .../user_session_controller_test.exs | 12 +- .../live/admin/layout_test.exs | 91 ++++++ .../live/auth/login_test.exs | 2 +- 17 files changed, 756 insertions(+), 541 deletions(-) create mode 100644 lib/simpleshop_theme_web/admin_layout_hook.ex create mode 100644 lib/simpleshop_theme_web/components/layouts/admin.html.heex create mode 100644 lib/simpleshop_theme_web/components/layouts/admin_root.html.heex create mode 100644 test/simpleshop_theme_web/live/admin/layout_test.exs diff --git a/lib/simpleshop_theme_web/admin_layout_hook.ex b/lib/simpleshop_theme_web/admin_layout_hook.ex new file mode 100644 index 0000000..bedd7f0 --- /dev/null +++ b/lib/simpleshop_theme_web/admin_layout_hook.ex @@ -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 diff --git a/lib/simpleshop_theme_web/components/layouts.ex b/lib/simpleshop_theme_web/components/layouts.ex index 9359016..e350040 100644 --- a/lib/simpleshop_theme_web/components/layouts.ex +++ b/lib/simpleshop_theme_web/components/layouts.ex @@ -35,35 +35,8 @@ defmodule SimpleshopThemeWeb.Layouts do def app(assigns) do ~H""" - - -
-
+
+
{render_slot(@inner_block)}
@@ -72,6 +45,11 @@ defmodule SimpleshopThemeWeb.Layouts do """ 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 """ Shows the flash group with standard titles and content. diff --git a/lib/simpleshop_theme_web/components/layouts/admin.html.heex b/lib/simpleshop_theme_web/components/layouts/admin.html.heex new file mode 100644 index 0000000..f59a5eb --- /dev/null +++ b/lib/simpleshop_theme_web/components/layouts/admin.html.heex @@ -0,0 +1,102 @@ +
+ + + <%!-- main content area --%> +
+ <%!-- mobile header --%> + + + <%!-- page content --%> +
+
+ {@inner_content} +
+
+
+ + <%!-- sidebar --%> +
+ + +
+
+ +<.flash_group flash={@flash} /> diff --git a/lib/simpleshop_theme_web/components/layouts/admin_root.html.heex b/lib/simpleshop_theme_web/components/layouts/admin_root.html.heex new file mode 100644 index 0000000..0f602d7 --- /dev/null +++ b/lib/simpleshop_theme_web/components/layouts/admin_root.html.heex @@ -0,0 +1,35 @@ + + + + + + + <.live_title default="Admin" suffix=" · SimpleShop"> + {assigns[:page_title]} + + + + + + + {@inner_content} + + diff --git a/lib/simpleshop_theme_web/components/layouts/root.html.heex b/lib/simpleshop_theme_web/components/layouts/root.html.heex index e5040ed..cc5b99b 100644 --- a/lib/simpleshop_theme_web/components/layouts/root.html.heex +++ b/lib/simpleshop_theme_web/components/layouts/root.html.heex @@ -31,42 +31,6 @@ - {@inner_content} diff --git a/lib/simpleshop_theme_web/components/layouts/shop.html.heex b/lib/simpleshop_theme_web/components/layouts/shop.html.heex index 86b3e32..1422873 100644 --- a/lib/simpleshop_theme_web/components/layouts/shop.html.heex +++ b/lib/simpleshop_theme_web/components/layouts/shop.html.heex @@ -1,2 +1,13 @@ +
+ <.link + href={~p"/admin/orders"} + style="color: var(--t-text-secondary, #666); text-decoration: none;" + > + Admin + +
<.shop_flash_group flash={@flash} /> {@inner_content} diff --git a/lib/simpleshop_theme_web/controllers/admin_controller.ex b/lib/simpleshop_theme_web/controllers/admin_controller.ex index 7b56584..943532b 100644 --- a/lib/simpleshop_theme_web/controllers/admin_controller.ex +++ b/lib/simpleshop_theme_web/controllers/admin_controller.ex @@ -2,6 +2,6 @@ defmodule SimpleshopThemeWeb.AdminController do use SimpleshopThemeWeb, :controller def index(conn, _params) do - redirect(conn, to: ~p"/admin/theme") + redirect(conn, to: ~p"/admin/orders") end end diff --git a/lib/simpleshop_theme_web/live/admin/order_show.ex b/lib/simpleshop_theme_web/live/admin/order_show.ex index 02b9d9d..ca9ab8d 100644 --- a/lib/simpleshop_theme_web/live/admin/order_show.ex +++ b/lib/simpleshop_theme_web/live/admin/order_show.ex @@ -28,169 +28,167 @@ defmodule SimpleshopThemeWeb.Admin.OrderShow do @impl true def render(assigns) do ~H""" - - <.header> - <.link - navigate={~p"/admin/orders"} - class="text-sm font-normal text-base-content/60 hover:underline" - > - ← Orders - -
- {@order.order_number} - <.status_badge status={@order.payment_status} /> -
- - -
- <%!-- order info --%> -
-
-

Order details

- <.list> - <:item title="Date">{format_date(@order.inserted_at)} - <:item title="Customer">{@order.customer_email || "—"} - <:item title="Payment status"> - <.status_badge status={@order.payment_status} /> - - <:item :if={@order.stripe_payment_intent_id} title="Stripe payment"> - {@order.stripe_payment_intent_id} - - <:item title="Currency">{String.upcase(@order.currency)} - -
-
- - <%!-- shipping address --%> -
-
-

Shipping address

- <%= if @order.shipping_address != %{} do %> - <.list> - <:item :if={@order.shipping_address["name"]} title="Name"> - {@order.shipping_address["name"]} - - <:item :if={@order.shipping_address["line1"]} title="Address"> - {@order.shipping_address["line1"]} - -
{@order.shipping_address["line2"]} -
- - <:item :if={@order.shipping_address["city"]} title="City"> - {@order.shipping_address["city"]} - - <:item :if={@order.shipping_address["state"] not in [nil, ""]} title="State"> - {@order.shipping_address["state"]} - - <:item :if={@order.shipping_address["postal_code"]} title="Postcode"> - {@order.shipping_address["postal_code"]} - - <:item :if={@order.shipping_address["country"]} title="Country"> - {@order.shipping_address["country"]} - - - <% else %> -

No shipping address provided

- <% end %> -
-
+ <.header> + <.link + navigate={~p"/admin/orders"} + class="text-sm font-normal text-base-content/60 hover:underline" + > + ← Orders + +
+ {@order.order_number} + <.status_badge status={@order.payment_status} />
+ - <%!-- fulfilment --%> -
+
+ <%!-- order info --%> +
-
-

Fulfilment

- <.fulfilment_badge status={@order.fulfilment_status} /> -
+

Order details

<.list> - <:item :if={@order.provider_order_id} title="Provider order ID"> - {@order.provider_order_id} + <:item title="Date">{format_date(@order.inserted_at)} + <:item title="Customer">{@order.customer_email || "—"} + <:item title="Payment status"> + <.status_badge status={@order.payment_status} /> - <:item :if={@order.provider_status} title="Provider status"> - {@order.provider_status} - - <:item :if={@order.submitted_at} title="Submitted"> - {format_date(@order.submitted_at)} - - <:item :if={@order.tracking_number} title="Tracking"> - <%= if @order.tracking_url do %> - - {@order.tracking_number} - - <% else %> - {@order.tracking_number} - <% end %> - - <:item :if={@order.shipped_at} title="Shipped"> - {format_date(@order.shipped_at)} - - <:item :if={@order.delivered_at} title="Delivered"> - {format_date(@order.delivered_at)} - - <:item :if={@order.fulfilment_error} title="Error"> - {@order.fulfilment_error} + <:item :if={@order.stripe_payment_intent_id} title="Stripe payment"> + {@order.stripe_payment_intent_id} + <:item title="Currency">{String.upcase(@order.currency)} -
- - -
- <%!-- line items --%> -
+ <%!-- shipping address --%> +
-

Items

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ProductVariantQtyUnit priceTotal
{item.product_name}{item.variant_title}{item.quantity}{Cart.format_price(item.unit_price)}{Cart.format_price(item.unit_price * item.quantity)}
Subtotal{Cart.format_price(@order.subtotal)}
Total{Cart.format_price(@order.total)}
+

Shipping address

+ <%= if @order.shipping_address != %{} do %> + <.list> + <:item :if={@order.shipping_address["name"]} title="Name"> + {@order.shipping_address["name"]} + + <:item :if={@order.shipping_address["line1"]} title="Address"> + {@order.shipping_address["line1"]} + +
{@order.shipping_address["line2"]} +
+ + <:item :if={@order.shipping_address["city"]} title="City"> + {@order.shipping_address["city"]} + + <:item :if={@order.shipping_address["state"] not in [nil, ""]} title="State"> + {@order.shipping_address["state"]} + + <:item :if={@order.shipping_address["postal_code"]} title="Postcode"> + {@order.shipping_address["postal_code"]} + + <:item :if={@order.shipping_address["country"]} title="Country"> + {@order.shipping_address["country"]} + + + <% else %> +

No shipping address provided

+ <% end %>
- +
+ + <%!-- fulfilment --%> +
+
+
+

Fulfilment

+ <.fulfilment_badge status={@order.fulfilment_status} /> +
+ <.list> + <:item :if={@order.provider_order_id} title="Provider order ID"> + {@order.provider_order_id} + + <:item :if={@order.provider_status} title="Provider status"> + {@order.provider_status} + + <:item :if={@order.submitted_at} title="Submitted"> + {format_date(@order.submitted_at)} + + <:item :if={@order.tracking_number} title="Tracking"> + <%= if @order.tracking_url do %> + + {@order.tracking_number} + + <% else %> + {@order.tracking_number} + <% end %> + + <:item :if={@order.shipped_at} title="Shipped"> + {format_date(@order.shipped_at)} + + <:item :if={@order.delivered_at} title="Delivered"> + {format_date(@order.delivered_at)} + + <:item :if={@order.fulfilment_error} title="Error"> + {@order.fulfilment_error} + + +
+ + +
+
+
+ + <%!-- line items --%> +
+
+

Items

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ProductVariantQtyUnit priceTotal
{item.product_name}{item.variant_title}{item.quantity}{Cart.format_price(item.unit_price)}{Cart.format_price(item.unit_price * item.quantity)}
Subtotal{Cart.format_price(@order.subtotal)}
Total{Cart.format_price(@order.total)}
+
+
""" end diff --git a/lib/simpleshop_theme_web/live/admin/orders.ex b/lib/simpleshop_theme_web/live/admin/orders.ex index abccb11..75161bb 100644 --- a/lib/simpleshop_theme_web/live/admin/orders.ex +++ b/lib/simpleshop_theme_web/live/admin/orders.ex @@ -36,67 +36,65 @@ defmodule SimpleshopThemeWeb.Admin.Orders do @impl true def render(assigns) do ~H""" - - <.header> - Orders - + <.header> + Orders + -
- <.filter_tab - status="all" - label="All" - count={total_count(@status_counts)} - active={@status_filter} - /> - <.filter_tab - status="paid" - label="Paid" - count={@status_counts["paid"]} - active={@status_filter} - /> - <.filter_tab - status="pending" - label="Pending" - count={@status_counts["pending"]} - active={@status_filter} - /> - <.filter_tab - status="failed" - label="Failed" - count={@status_counts["failed"]} - active={@status_filter} - /> - <.filter_tab - status="refunded" - label="Refunded" - count={@status_counts["refunded"]} - active={@status_filter} - /> -
+
+ <.filter_tab + status="all" + label="All" + count={total_count(@status_counts)} + active={@status_filter} + /> + <.filter_tab + status="paid" + label="Paid" + count={@status_counts["paid"]} + active={@status_filter} + /> + <.filter_tab + status="pending" + label="Pending" + count={@status_counts["pending"]} + active={@status_filter} + /> + <.filter_tab + status="failed" + label="Failed" + count={@status_counts["failed"]} + active={@status_filter} + /> + <.filter_tab + status="refunded" + label="Refunded" + count={@status_counts["refunded"]} + active={@status_filter} + /> +
- <.table - :if={@order_count > 0} - id="orders" - rows={@streams.orders} - row_item={fn {_id, order} -> order end} - row_click={fn {_id, order} -> JS.navigate(~p"/admin/orders/#{order}") end} - > - <:col :let={order} label="Order">{order.order_number} - <:col :let={order} label="Date">{format_date(order.inserted_at)} - <:col :let={order} label="Customer">{order.customer_email || "—"} - <:col :let={order} label="Total">{Cart.format_price(order.total)} - <:col :let={order} label="Status"><.status_badge status={order.payment_status} /> - <:col :let={order} label="Fulfilment"> - <.fulfilment_badge status={order.fulfilment_status} /> - - + <.table + :if={@order_count > 0} + id="orders" + rows={@streams.orders} + row_item={fn {_id, order} -> order end} + row_click={fn {_id, order} -> JS.navigate(~p"/admin/orders/#{order}") end} + > + <:col :let={order} label="Order">{order.order_number} + <:col :let={order} label="Date">{format_date(order.inserted_at)} + <:col :let={order} label="Customer">{order.customer_email || "—"} + <:col :let={order} label="Total">{Cart.format_price(order.total)} + <:col :let={order} label="Status"><.status_badge status={order.payment_status} /> + <:col :let={order} label="Fulfilment"> + <.fulfilment_badge status={order.fulfilment_status} /> + + -
- <.icon name="hero-inbox" class="size-12 mx-auto mb-4" /> -

No orders yet

-

Orders will appear here once customers check out.

-
-
+
+ <.icon name="hero-inbox" class="size-12 mx-auto mb-4" /> +

No orders yet

+

Orders will appear here once customers check out.

+
""" end diff --git a/lib/simpleshop_theme_web/live/admin/providers/form.html.heex b/lib/simpleshop_theme_web/live/admin/providers/form.html.heex index 628885a..d163bcc 100644 --- a/lib/simpleshop_theme_web/live/admin/providers/form.html.heex +++ b/lib/simpleshop_theme_web/live/admin/providers/form.html.heex @@ -1,104 +1,102 @@ - - <.header> - {if @live_action == :new, do: "Connect to Printify", else: "Printify settings"} - +<.header> + {if @live_action == :new, do: "Connect to Printify", else: "Printify settings"} + -
- <%= if @live_action == :new do %> -
-

- 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. -

-
+
+ <%= if @live_action == :new do %> +
+

+ 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. +

+
-
-

Get your connection key from Printify:

-
    -
  1. - - Log in to Printify - - (or create a free account) -
  2. -
  3. Click Account (top right)
  4. -
  5. Select Connections from the dropdown
  6. -
  7. Find API tokens and click Generate
  8. -
  9. - Enter a name (e.g. "My Shop"), keep all scopes - selected, and click Generate token -
  10. -
  11. Click Copy to clipboard and paste it below
  12. -
+
+

Get your connection key from Printify:

+
    +
  1. + + Log in to Printify + + (or create a free account) +
  2. +
  3. Click Account (top right)
  4. +
  5. Select Connections from the dropdown
  6. +
  7. Find API tokens and click Generate
  8. +
  9. + Enter a name (e.g. "My Shop"), keep all scopes + selected, and click Generate token +
  10. +
  11. Click Copy to clipboard and paste it below
  12. +
+
+ <% end %> + + <.form for={@form} id="provider-form" phx-change="validate" phx-submit="save"> + + + <.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" + /> + +
+ + +
+ <%= case @test_result do %> + <% {:ok, info} -> %> + + <.icon name="hero-check-circle" class="size-4" /> Connected to {info.shop_name} + + <% {:error, reason} -> %> + + <.icon name="hero-x-circle" class="size-4" /> + {format_error(reason)} + + <% end %>
+
+ + <%= if @live_action == :edit do %> + <.input field={@form[:enabled]} type="checkbox" label="Connection enabled" /> <% end %> - <.form for={@form} id="provider-form" phx-change="validate" phx-submit="save"> - - - <.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" - /> - -
- - -
- <%= case @test_result do %> - <% {:ok, info} -> %> - - <.icon name="hero-check-circle" class="size-4" /> Connected to {info.shop_name} - - <% {:error, reason} -> %> - - <.icon name="hero-x-circle" class="size-4" /> - {format_error(reason)} - - <% end %> -
-
- - <%= if @live_action == :edit do %> - <.input field={@form[:enabled]} type="checkbox" label="Connection enabled" /> - <% end %> - -
- <.button type="submit" disabled={@testing}> - {if @live_action == :new, do: "Connect to Printify", else: "Save changes"} - - <.link navigate={~p"/admin/providers"} class="btn btn-ghost"> - Cancel - -
- -
- +
+ <.button type="submit" disabled={@testing}> + {if @live_action == :new, do: "Connect to Printify", else: "Save changes"} + + <.link navigate={~p"/admin/providers"} class="btn btn-ghost"> + Cancel + +
+ +
diff --git a/lib/simpleshop_theme_web/live/admin/providers/index.html.heex b/lib/simpleshop_theme_web/live/admin/providers/index.html.heex index 68f0a3f..441a2be 100644 --- a/lib/simpleshop_theme_web/live/admin/providers/index.html.heex +++ b/lib/simpleshop_theme_web/live/admin/providers/index.html.heex @@ -1,81 +1,79 @@ - - <.header> - Providers - <:actions> - <.button navigate={~p"/admin/providers/new"}> - <.icon name="hero-plus" class="size-4 mr-1" /> Connect Printify - - - +<.header> + Providers + <:actions> + <.button navigate={~p"/admin/providers/new"}> + <.icon name="hero-plus" class="size-4 mr-1" /> Connect Printify + + + -
- - -
-
-
-
-
- <.status_indicator status={connection.sync_status} enabled={connection.enabled} /> -

- {String.capitalize(connection.provider_type)} -

-
-

{connection.name}

-
- <.connection_info connection={connection} /> -
-
+
+ +
+
+
+
- <.link - navigate={~p"/admin/providers/#{connection.id}/edit"} - class="btn btn-ghost btn-sm" - > - Settings - - + <.status_indicator status={connection.sync_status} enabled={connection.enabled} /> +

+ {String.capitalize(connection.provider_type)} +

+
+

{connection.name}

+
+ <.connection_info connection={connection} />
-
-
+ +
+ +
- +
diff --git a/lib/simpleshop_theme_web/live/admin/settings.ex b/lib/simpleshop_theme_web/live/admin/settings.ex index f4c88fb..6b37012 100644 --- a/lib/simpleshop_theme_web/live/admin/settings.ex +++ b/lib/simpleshop_theme_web/live/admin/settings.ex @@ -119,88 +119,86 @@ defmodule SimpleshopThemeWeb.Admin.Settings do @impl true def render(assigns) do ~H""" - -
- <.header> - Settings - <:subtitle>Shop status, payment providers, and API keys - +
+ <.header> + Settings + <:subtitle>Shop status, payment providers, and API keys + -
-
-

Shop status

- <%= if @site_live do %> - - <.icon name="hero-check-circle-mini" class="size-3" /> Live - - <% else %> - - Offline - - <% end %> -
-

- <%= if @site_live do %> - Your shop is visible to the public. - <% else %> - Your shop is offline. Visitors see a "coming soon" page. - <% end %> -

-
- -
-
- -
-
-

Stripe

- <%= case @stripe_status do %> - <% :connected -> %> - - <.icon name="hero-check-circle-mini" class="size-3" /> Connected - - <% :connected_localhost -> %> - - <.icon name="hero-exclamation-triangle-mini" class="size-3" /> Dev mode - - <% :not_configured -> %> - - Not connected - - <% end %> -
- - <%= if @stripe_status == :not_configured do %> - <.stripe_setup_form connect_form={@connect_form} connecting={@connecting} /> +
+
+

Shop status

+ <%= if @site_live do %> + + <.icon name="hero-check-circle-mini" class="size-3" /> Live + <% 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} - /> + + Offline + <% end %> -
-
- +
+

+ <%= if @site_live do %> + Your shop is visible to the public. + <% else %> + Your shop is offline. Visitors see a "coming soon" page. + <% end %> +

+
+ +
+ + +
+
+

Stripe

+ <%= case @stripe_status do %> + <% :connected -> %> + + <.icon name="hero-check-circle-mini" class="size-3" /> Connected + + <% :connected_localhost -> %> + + <.icon name="hero-exclamation-triangle-mini" class="size-3" /> Dev mode + + <% :not_configured -> %> + + Not connected + + <% end %> +
+ + <%= 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 %> +
+
""" end diff --git a/lib/simpleshop_theme_web/live/admin/theme/index.html.heex b/lib/simpleshop_theme_web/live/admin/theme/index.html.heex index 68280fe..0c55772 100644 --- a/lib/simpleshop_theme_web/live/admin/theme/index.html.heex +++ b/lib/simpleshop_theme_web/live/admin/theme/index.html.heex @@ -35,7 +35,14 @@
<% else %> - + <.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 + + +

diff --git a/lib/simpleshop_theme_web/router.ex b/lib/simpleshop_theme_web/router.ex index 396364a..3a77b25 100644 --- a/lib/simpleshop_theme_web/router.ex +++ b/lib/simpleshop_theme_web/router.ex @@ -28,6 +28,10 @@ defmodule SimpleshopThemeWeb.Router do plug SimpleshopThemeWeb.Plugs.LoadTheme end + pipeline :admin do + plug :put_root_layout, html: {SimpleshopThemeWeb.Layouts, :admin_root} + end + # Public storefront (root level) scope "/", SimpleshopThemeWeb do pipe_through [:browser, :shop] @@ -43,6 +47,7 @@ defmodule SimpleshopThemeWeb.Router do live_session :public_shop, layout: {SimpleshopThemeWeb.Layouts, :shop}, on_mount: [ + {SimpleshopThemeWeb.UserAuth, :mount_current_scope}, {SimpleshopThemeWeb.ThemeHook, :mount_theme}, {SimpleshopThemeWeb.ThemeHook, :require_site_live}, {SimpleshopThemeWeb.CartHook, :mount_cart} @@ -123,27 +128,46 @@ defmodule SimpleshopThemeWeb.Router do ## Authentication routes - # /admin redirects to theme editor (requires auth, will redirect to login if needed) + # /admin index redirect scope "/admin", SimpleshopThemeWeb do pipe_through [:browser, :require_authenticated_user] get "/", AdminController, :index 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 pipe_through [:browser, :require_authenticated_user] - live_session :require_authenticated_user, + live_session :user_settings, on_mount: [{SimpleshopThemeWeb.UserAuth, :require_authenticated}] do live "/users/settings", Auth.Settings, :edit 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 post "/users/update-password", UserSessionController, :update_password diff --git a/test/simpleshop_theme_web/controllers/user_session_controller_test.exs b/test/simpleshop_theme_web/controllers/user_session_controller_test.exs index f971ae0..d747da8 100644 --- a/test/simpleshop_theme_web/controllers/user_session_controller_test.exs +++ b/test/simpleshop_theme_web/controllers/user_session_controller_test.exs @@ -20,12 +20,10 @@ defmodule SimpleshopThemeWeb.UserSessionControllerTest do assert get_session(conn, :user_token) 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") response = html_response(conn, 200) assert response =~ user.email - assert response =~ ~p"/users/settings" - assert response =~ ~p"/users/log-out" end 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 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") response = html_response(conn, 200) assert response =~ user.email - assert response =~ ~p"/users/settings" - assert response =~ ~p"/users/log-out" end 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 - # 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") response = html_response(conn, 200) assert response =~ user.email - assert response =~ ~p"/users/settings" - assert response =~ ~p"/users/log-out" end test "redirects to login page when magic link is invalid", %{conn: conn} do diff --git a/test/simpleshop_theme_web/live/admin/layout_test.exs b/test/simpleshop_theme_web/live/admin/layout_test.exs new file mode 100644 index 0000000..dd99cc1 --- /dev/null +++ b/test/simpleshop_theme_web/live/admin/layout_test.exs @@ -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 diff --git a/test/simpleshop_theme_web/live/auth/login_test.exs b/test/simpleshop_theme_web/live/auth/login_test.exs index ac9c82b..05b0693 100644 --- a/test/simpleshop_theme_web/live/auth/login_test.exs +++ b/test/simpleshop_theme_web/live/auth/login_test.exs @@ -9,7 +9,7 @@ defmodule SimpleshopThemeWeb.Auth.LoginTest do {:ok, _lv, html} = live(conn, ~p"/users/log-in") assert html =~ "Log in" - assert html =~ "Register" + assert html =~ "Sign up" assert html =~ "Log in with email" end end