improve setup UX: password field, setup hook, checklist banners, theme tweaks
All checks were successful
deploy / deploy (push) Successful in 1m31s

- add password field and required shop name to setup wizard
- extract SetupHook for DRY redirect to /setup when no admin exists
- add ?from=checklist param to checklist hrefs with contextual banner on
  email settings and theme pages for easy return to dashboard
- remove email warning banner from admin layout (checklist covers it)
- make email a required checklist item (no longer optional)
- add DevReset module for wiping dev data without restart
- rename "Theme Studio" to "Theme", drop subtitle
- lower theme editor side-by-side breakpoint from 64em to 48em
- clean up login/registration pages (remove dead registration_open code)
- fix settings.put_secret to invalidate cache after write

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-03-03 17:41:08 +00:00
parent 0853b6f528
commit 64f083d271
23 changed files with 309 additions and 118 deletions

View File

@@ -154,10 +154,8 @@ defmodule BerrypodWeb.Admin.Dashboard do
defp launch_checklist(assigns) do
items = checklist_items(assigns.setup)
# Email is optional — exclude from progress count
required_items = Enum.reject(items, &(&1.key == :email_configured))
done_count = Enum.count(required_items, & &1.done)
total = length(required_items)
done_count = Enum.count(items, & &1.done)
total = length(items)
progress_pct = round(done_count / total * 100)
assigns =
@@ -248,33 +246,36 @@ defmodule BerrypodWeb.Admin.Dashboard do
%{
key: :provider_connected,
label: "Connect a print provider",
href: "/admin/providers"
href: "/admin/providers?from=checklist"
},
%{
key: :stripe_connected,
label: "Connect Stripe",
href: "/admin/settings"
href: "/admin/settings?from=checklist"
},
# Post-setup items
%{
key: :products_synced,
label: "Sync your products",
href: if(setup.provider_connected, do: "/admin/products", else: "/admin/providers"),
href:
if(setup.provider_connected,
do: "/admin/products?from=checklist",
else: "/admin/providers?from=checklist"
),
hint: "Import products from your print provider."
},
%{
key: :theme_customised,
label: "Customise your theme",
href: "/admin/theme",
hint: "Upload your logo, pick your colours, and choose a font that matches your brand."
},
%{
key: :email_configured,
label: "Set up email",
href: "/admin/settings",
optional: true,
href: "/admin/settings/email?from=checklist",
hint: "Needed for order confirmations, abandoned cart emails, and the contact form."
},
%{
key: :theme_customised,
label: "Customise your theme",
href: "/admin/theme?from=checklist",
hint: "Upload your logo, pick your colours, and choose a font that matches your brand."
},
%{
key: :has_orders,
label: "Place a test order",

View File

@@ -27,9 +27,15 @@ defmodule BerrypodWeb.Admin.EmailSettings do
Settings.get_setting("email_from_address") || socket.assigns.current_scope.user.email
)
|> assign(:sending_test, false)
|> assign(:from_checklist, false)
|> assign(:form, to_form(%{}, as: :email))}
end
@impl true
def handle_params(params, _uri, socket) do
{:noreply, assign(socket, :from_checklist, params["from"] == "checklist")}
end
defp load_adapter_values(adapter_key) do
case Adapters.get(adapter_key) do
nil ->
@@ -201,6 +207,15 @@ defmodule BerrypodWeb.Admin.EmailSettings do
def render(assigns) do
~H"""
<div class="admin-content-medium">
<div :if={@from_checklist} class="admin-checklist-banner">
<.icon name="hero-clipboard-document-check" class="size-5 admin-checklist-banner-icon" />
<span class="admin-checklist-banner-text">
You're setting up email for your shop.
</span>
<.link navigate={~p"/admin"} class="admin-link admin-checklist-banner-link">
&larr; Back to checklist
</.link>
</div>
<.header>
Email settings
<:subtitle>

View File

@@ -61,7 +61,12 @@ defmodule BerrypodWeb.Admin.Theme.Index do
progress: &handle_progress/3
)
{:ok, socket}
{:ok, assign(socket, :from_checklist, false)}
end
@impl true
def handle_params(params, _uri, socket) do
{:noreply, assign(socket, :from_checklist, params["from"] == "checklist")}
end
defp handle_progress(:logo_upload, entry, socket) do

View File

@@ -37,14 +37,21 @@
<.link href={~p"/admin"} class="theme-back-link">
<.icon name="hero-arrow-left-mini" class="size-4" /> Admin
</.link>
<div :if={@from_checklist} class="admin-checklist-banner">
<.icon name="hero-clipboard-document-check" class="size-5 admin-checklist-banner-icon" />
<span class="admin-checklist-banner-text">
You're customising your theme.
</span>
<.link navigate={~p"/admin"} class="admin-link admin-checklist-banner-link">
&larr; Back to checklist
</.link>
</div>
<!-- Header -->
<div class="theme-header">
<div class="admin-fill">
<h1 class="theme-title">Theme Studio</h1>
<p class="theme-subtitle">
One theme, infinite possibilities. Every combination is designed to work beautifully.
</p>
<h1 class="theme-title">Theme</h1>
</div>
<button
type="button"

View File

@@ -12,17 +12,10 @@ defmodule BerrypodWeb.Auth.Login do
<.header>
<p>Log in</p>
<:subtitle>
<%= cond do %>
<% @current_scope -> %>
You need to reauthenticate to perform sensitive actions on your account.
<% @registration_open -> %>
Don't have an account? <.link
navigate={~p"/setup"}
class="font-semibold text-brand hover:underline"
phx-no-format
>Set up your shop</.link> to get started.
<% true -> %>
Log in with your admin credentials.
<%= if @current_scope do %>
You need to reauthenticate to perform sensitive actions on your account.
<% else %>
Log in with your admin credentials.
<% end %>
</:subtitle>
</.header>
@@ -116,7 +109,6 @@ defmodule BerrypodWeb.Auth.Login do
assign(socket,
form: form,
trigger_submit: false,
registration_open: !Accounts.has_admin?(),
email_configured: Mailer.email_verified?()
)}
end

View File

@@ -48,15 +48,11 @@ defmodule BerrypodWeb.Auth.Registration do
end
def mount(_params, _session, socket) do
if Accounts.has_admin?() do
{:ok,
socket
|> put_flash(:error, "Registration is closed")
|> redirect(to: ~p"/users/log-in")}
else
# Fresh install — account creation happens on the setup page
{:ok, redirect(socket, to: ~p"/setup")}
end
# Admin exists (hook handles no-admin), registration is single-user only
{:ok,
socket
|> put_flash(:error, "Registration is closed")
|> redirect(to: ~p"/users/log-in")}
end
@impl true

View File

@@ -54,7 +54,10 @@ defmodule BerrypodWeb.Setup.Onboarding do
|> assign(:secret_verified, false)
|> assign(:secret_form, to_form(%{"secret" => ""}, as: :secret))
# Account (card 1)
|> assign(:account_form, to_form(%{"email" => "", "shop_name" => ""}, as: :account))
|> assign(
:account_form,
to_form(%{"email" => "", "password" => "", "shop_name" => ""}, as: :account)
)
# Provider (card 2)
|> assign(:providers, Provider.all())
|> assign(:selected_provider, nil)
@@ -81,32 +84,38 @@ defmodule BerrypodWeb.Setup.Onboarding do
def handle_event("create_account", %{"account" => params}, socket) do
email = params["email"]
password = params["password"]
shop_name = String.trim(params["shop_name"] || "")
if email == "" do
{:noreply, put_flash(socket, :error, "Please enter your email address")}
else
if shop_name != "" do
cond do
shop_name == "" ->
{:noreply, put_flash(socket, :error, "Please enter a shop name")}
email == "" ->
{:noreply, put_flash(socket, :error, "Please enter your email address")}
true ->
Settings.put_setting("site_name", shop_name, "string")
end
case Accounts.register_and_confirm_admin(%{email: email}) do
{:ok, user} ->
token = Accounts.generate_login_token(user)
{:noreply, redirect(socket, to: ~p"/setup/login/#{token}")}
case Accounts.register_and_confirm_admin(%{email: email, password: password}) do
{:ok, user} ->
token = Accounts.generate_login_token(user)
{:noreply, redirect(socket, to: ~p"/setup/login/#{token}")}
{:error, :admin_already_exists} ->
{:noreply,
socket
|> put_flash(:error, "An admin account already exists")
|> push_navigate(to: ~p"/setup")}
{:error, :admin_already_exists} ->
{:noreply,
socket
|> put_flash(:error, "An admin account already exists")
|> push_navigate(to: ~p"/setup")}
{:error, changeset} ->
{:noreply,
socket
|> assign(:account_form, to_form(changeset, as: :account))
|> put_flash(:error, "Could not create account")}
end
{:error, changeset} ->
form = to_form(params, as: :account, errors: changeset.errors, action: :validate)
{:noreply,
socket
|> assign(:account_form, form)
|> put_flash(:error, "Could not create account")}
end
end
end
@@ -255,6 +264,7 @@ defmodule BerrypodWeb.Setup.Onboarding do
label="Shop name"
placeholder="e.g. Acme Prints"
autocomplete="off"
required
phx-mounted={@current_step == 1 && JS.focus()}
/>
<.input
@@ -264,6 +274,14 @@ defmodule BerrypodWeb.Setup.Onboarding do
autocomplete="email"
required
/>
<.input
field={@account_form[:password]}
type="password"
label="Password"
placeholder="12 characters minimum"
autocomplete="new-password"
required
/>
<div class="setup-actions">
<.button phx-disable-with="Creating account...">Create account</.button>
</div>