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:
@@ -28,169 +28,167 @@ defmodule SimpleshopThemeWeb.Admin.OrderShow do
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash} current_scope={@current_scope}>
|
||||
<.header>
|
||||
<.link
|
||||
navigate={~p"/admin/orders"}
|
||||
class="text-sm font-normal text-base-content/60 hover:underline"
|
||||
>
|
||||
← Orders
|
||||
</.link>
|
||||
<div class="flex items-center gap-3 mt-1">
|
||||
<span class="text-2xl font-bold">{@order.order_number}</span>
|
||||
<.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>
|
||||
<.header>
|
||||
<.link
|
||||
navigate={~p"/admin/orders"}
|
||||
class="text-sm font-normal text-base-content/60 hover:underline"
|
||||
>
|
||||
← Orders
|
||||
</.link>
|
||||
<div class="flex items-center gap-3 mt-1">
|
||||
<span class="text-2xl font-bold">{@order.order_number}</span>
|
||||
<.status_badge status={@order.payment_status} />
|
||||
</div>
|
||||
</.header>
|
||||
|
||||
<%!-- fulfilment --%>
|
||||
<div class="card bg-base-100 shadow-sm border border-base-200 mt-6">
|
||||
<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">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="card-title text-base">Fulfilment</h3>
|
||||
<.fulfilment_badge status={@order.fulfilment_status} />
|
||||
</div>
|
||||
<h3 class="card-title text-base">Order details</h3>
|
||||
<.list>
|
||||
<:item :if={@order.provider_order_id} title="Provider order ID">
|
||||
<code class="text-xs">{@order.provider_order_id}</code>
|
||||
<: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.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 :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 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">
|
||||
<%!-- shipping address --%>
|
||||
<div class="card bg-base-100 shadow-sm border border-base-200">
|
||||
<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>
|
||||
<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>
|
||||
</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
|
||||
|
||||
|
||||
@@ -36,67 +36,65 @@ defmodule SimpleshopThemeWeb.Admin.Orders do
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash} current_scope={@current_scope}>
|
||||
<.header>
|
||||
Orders
|
||||
</.header>
|
||||
<.header>
|
||||
Orders
|
||||
</.header>
|
||||
|
||||
<div class="flex gap-2 mt-6 mb-4 flex-wrap">
|
||||
<.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}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-2 mt-6 mb-4 flex-wrap">
|
||||
<.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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<.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>
|
||||
<:col :let={order} label="Date">{format_date(order.inserted_at)}</: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="Status"><.status_badge status={order.payment_status} /></:col>
|
||||
<:col :let={order} label="Fulfilment">
|
||||
<.fulfilment_badge status={order.fulfilment_status} />
|
||||
</:col>
|
||||
</.table>
|
||||
<.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>
|
||||
<:col :let={order} label="Date">{format_date(order.inserted_at)}</: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="Status"><.status_badge status={order.payment_status} /></:col>
|
||||
<:col :let={order} label="Fulfilment">
|
||||
<.fulfilment_badge status={order.fulfilment_status} />
|
||||
</:col>
|
||||
</.table>
|
||||
|
||||
<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" />
|
||||
<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>
|
||||
</div>
|
||||
</Layouts.app>
|
||||
<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" />
|
||||
<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>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
|
||||
@@ -1,104 +1,102 @@
|
||||
<Layouts.app flash={@flash}>
|
||||
<.header>
|
||||
{if @live_action == :new, do: "Connect to Printify", else: "Printify settings"}
|
||||
</.header>
|
||||
<.header>
|
||||
{if @live_action == :new, do: "Connect to Printify", else: "Printify settings"}
|
||||
</.header>
|
||||
|
||||
<div class="max-w-xl mt-6">
|
||||
<%= if @live_action == :new do %>
|
||||
<div class="prose prose-sm mb-6">
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<div class="max-w-xl mt-6">
|
||||
<%= if @live_action == :new do %>
|
||||
<div class="prose prose-sm mb-6">
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<ol class="list-decimal list-inside space-y-1 text-base-content/80">
|
||||
<li>
|
||||
<a
|
||||
href="https://printify.com/app/auth/login"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="link"
|
||||
>
|
||||
Log in to Printify
|
||||
</a>
|
||||
(or <a
|
||||
href="https://printify.com/app/auth/register"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="link"
|
||||
>create a free account</a>)
|
||||
</li>
|
||||
<li>Click <strong>Account</strong> (top right)</li>
|
||||
<li>Select <strong>Connections</strong> from the dropdown</li>
|
||||
<li>Find <strong>API tokens</strong> and click <strong>Generate</strong></li>
|
||||
<li>
|
||||
Enter a name (e.g. "My Shop"), keep <strong>all scopes</strong>
|
||||
selected, and click <strong>Generate token</strong>
|
||||
</li>
|
||||
<li>Click <strong>Copy to clipboard</strong> and paste it below</li>
|
||||
</ol>
|
||||
<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>
|
||||
<ol class="list-decimal list-inside space-y-1 text-base-content/80">
|
||||
<li>
|
||||
<a
|
||||
href="https://printify.com/app/auth/login"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="link"
|
||||
>
|
||||
Log in to Printify
|
||||
</a>
|
||||
(or <a
|
||||
href="https://printify.com/app/auth/register"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="link"
|
||||
>create a free account</a>)
|
||||
</li>
|
||||
<li>Click <strong>Account</strong> (top right)</li>
|
||||
<li>Select <strong>Connections</strong> from the dropdown</li>
|
||||
<li>Find <strong>API tokens</strong> and click <strong>Generate</strong></li>
|
||||
<li>
|
||||
Enter a name (e.g. "My Shop"), keep <strong>all scopes</strong>
|
||||
selected, and click <strong>Generate token</strong>
|
||||
</li>
|
||||
<li>Click <strong>Copy to clipboard</strong> and paste it below</li>
|
||||
</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>
|
||||
|
||||
<%= 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 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>
|
||||
|
||||
<%= 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>
|
||||
<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>
|
||||
|
||||
@@ -1,81 +1,79 @@
|
||||
<Layouts.app flash={@flash}>
|
||||
<.header>
|
||||
Providers
|
||||
<:actions>
|
||||
<.button navigate={~p"/admin/providers/new"}>
|
||||
<.icon name="hero-plus" class="size-4 mr-1" /> Connect Printify
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
<.header>
|
||||
Providers
|
||||
<:actions>
|
||||
<.button navigate={~p"/admin/providers/new"}>
|
||||
<.icon name="hero-plus" class="size-4 mr-1" /> Connect Printify
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<div id="connections" phx-update="stream" class="mt-6 space-y-4">
|
||||
<div class="hidden only:block text-center py-12">
|
||||
<.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>
|
||||
<p class="mt-2 text-base-content/60 max-w-md mx-auto">
|
||||
Printify handles printing and shipping for you. Connect your account
|
||||
to import your products and start selling.
|
||||
</p>
|
||||
<.button navigate={~p"/admin/providers/new"} class="mt-6">
|
||||
Connect to Printify
|
||||
</.button>
|
||||
</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 id="connections" phx-update="stream" class="mt-6 space-y-4">
|
||||
<div class="hidden only:block text-center py-12">
|
||||
<.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>
|
||||
<p class="mt-2 text-base-content/60 max-w-md mx-auto">
|
||||
Printify handles printing and shipping for you. Connect your account
|
||||
to import your products and start selling.
|
||||
</p>
|
||||
<.button navigate={~p"/admin/providers/new"} class="mt-6">
|
||||
Connect to Printify
|
||||
</.button>
|
||||
</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">
|
||||
<.link
|
||||
navigate={~p"/admin/providers/#{connection.id}/edit"}
|
||||
class="btn btn-ghost btn-sm"
|
||||
>
|
||||
Settings
|
||||
</.link>
|
||||
<button
|
||||
phx-click="delete"
|
||||
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>
|
||||
<.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 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"
|
||||
<div class="flex items-center gap-2">
|
||||
<.link
|
||||
navigate={~p"/admin/providers/#{connection.id}/edit"}
|
||||
class="btn btn-ghost 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"}
|
||||
Settings
|
||||
</.link>
|
||||
<button
|
||||
phx-click="delete"
|
||||
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 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>
|
||||
</Layouts.app>
|
||||
</div>
|
||||
|
||||
@@ -119,88 +119,86 @@ defmodule SimpleshopThemeWeb.Admin.Settings do
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash} current_scope={@current_scope}>
|
||||
<div class="max-w-2xl">
|
||||
<.header>
|
||||
Settings
|
||||
<:subtitle>Shop status, payment providers, and API keys</:subtitle>
|
||||
</.header>
|
||||
<div class="max-w-2xl">
|
||||
<.header>
|
||||
Settings
|
||||
<:subtitle>Shop status, payment providers, and API keys</:subtitle>
|
||||
</.header>
|
||||
|
||||
<section class="mt-10">
|
||||
<div class="flex items-center gap-3">
|
||||
<h2 class="text-lg font-semibold">Shop status</h2>
|
||||
<%= 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">
|
||||
<.icon name="hero-check-circle-mini" class="size-3" /> Live
|
||||
</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} />
|
||||
<section class="mt-10">
|
||||
<div class="flex items-center gap-3">
|
||||
<h2 class="text-lg font-semibold">Shop status</h2>
|
||||
<%= 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">
|
||||
<.icon name="hero-check-circle-mini" class="size-3" /> Live
|
||||
</span>
|
||||
<% 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}
|
||||
/>
|
||||
<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 %>
|
||||
</section>
|
||||
</div>
|
||||
</Layouts.app>
|
||||
</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 %>
|
||||
<.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
|
||||
|
||||
|
||||
@@ -35,7 +35,14 @@
|
||||
</button>
|
||||
</div>
|
||||
<% 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="flex-1">
|
||||
<h1 class="text-xl font-semibold tracking-tight mb-2 text-base-content">
|
||||
|
||||
Reference in New Issue
Block a user