From 64f083d271b1148fce616ddf2edae93e5c53e41e Mon Sep 17 00:00:00 2001 From: jamey Date: Tue, 3 Mar 2026 17:41:08 +0000 Subject: [PATCH] improve setup UX: password field, setup hook, checklist banners, theme tweaks - 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 --- assets/css/admin/components.css | 29 ++++++- assets/css/admin/icons.css | 12 +++ lib/berrypod/accounts.ex | 1 + lib/berrypod/dev_reset.ex | 78 +++++++++++++++++++ lib/berrypod/settings.ex | 38 +++++---- lib/berrypod_web/admin_layout_hook.ex | 1 - .../components/layouts/admin.html.heex | 11 --- lib/berrypod_web/live/admin/dashboard.ex | 31 ++++---- lib/berrypod_web/live/admin/email_settings.ex | 15 ++++ lib/berrypod_web/live/admin/theme/index.ex | 7 +- .../live/admin/theme/index.html.heex | 15 +++- lib/berrypod_web/live/auth/login.ex | 16 +--- lib/berrypod_web/live/auth/registration.ex | 14 ++-- lib/berrypod_web/live/setup/onboarding.ex | 60 +++++++++----- lib/berrypod_web/router.ex | 7 +- lib/berrypod_web/setup_hook.ex | 18 +++++ lib/berrypod_web/theme_hook.ex | 4 - test/berrypod/accounts_test.exs | 33 +++++++- .../live/admin/dashboard_test.exs | 3 +- test/berrypod_web/live/admin/theme_test.exs | 2 +- test/berrypod_web/live/auth/login_test.exs | 23 +++--- .../live/setup/onboarding_test.exs | 8 +- .../live/shop/coming_soon_test.exs | 1 + 23 files changed, 309 insertions(+), 118 deletions(-) create mode 100644 lib/berrypod/dev_reset.ex create mode 100644 lib/berrypod_web/setup_hook.ex diff --git a/assets/css/admin/components.css b/assets/css/admin/components.css index 8f8854f..0859d2b 100644 --- a/assets/css/admin/components.css +++ b/assets/css/admin/components.css @@ -934,6 +934,25 @@ border: 1px solid var(--t-border-default); } +/* ── Checklist banner (shown when arriving from the launch checklist) ── */ + +.admin-checklist-banner { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + margin-bottom: 1.5rem; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.5rem; + background-color: color-mix(in oklch, var(--t-status-info) 10%, var(--t-surface-base)); + border: 1px solid color-mix(in oklch, var(--t-status-info) 20%, var(--t-surface-base)); + + & .admin-checklist-banner-icon { color: var(--t-status-info); flex-shrink: 0; } + & .admin-checklist-banner-text { flex: 1; color: var(--t-text-secondary); } + & .admin-checklist-banner-link { font-weight: 500; white-space: nowrap; } +} + /* ── Dashboard stats grid ── */ @@ -3422,7 +3441,7 @@ min-height: 100vh; background: var(--t-surface-sunken); - @media (min-width: 64em) { + @media (min-width: 48em) { flex-direction: row; height: 100vh; } @@ -3434,7 +3453,7 @@ flex-shrink: 0; transition: width 0.3s, padding 0.3s; - @media (min-width: 64em) { + @media (min-width: 48em) { height: 100vh; } } @@ -3444,7 +3463,7 @@ overflow-y: auto; padding: 1.5rem; - @media (min-width: 64em) { + @media (min-width: 48em) { width: 380px; } } @@ -4168,6 +4187,10 @@ display: flex; flex-direction: column; gap: 1rem; + + &[hidden] { + display: none; + } } .admin-adapter-link { diff --git a/assets/css/admin/icons.css b/assets/css/admin/icons.css index 904b49d..a588df3 100644 --- a/assets/css/admin/icons.css +++ b/assets/css/admin/icons.css @@ -386,6 +386,18 @@ height: 1.5rem; } +.hero-clipboard-document-check { + --hero-clipboard-document-check: url('data:image/svg+xml;utf8,%20%20'); + -webkit-mask: var(--hero-clipboard-document-check); + mask: var(--hero-clipboard-document-check); + mask-repeat: no-repeat; + background-color: currentColor; + vertical-align: middle; + display: inline-block; + width: 1.5rem; + height: 1.5rem; +} + .hero-clipboard-document-list { --hero-clipboard-document-list: url('data:image/svg+xml;utf8,%20%20'); -webkit-mask: var(--hero-clipboard-document-list); diff --git a/lib/berrypod/accounts.ex b/lib/berrypod/accounts.ex index af06383..4f22d20 100644 --- a/lib/berrypod/accounts.ex +++ b/lib/berrypod/accounts.ex @@ -109,6 +109,7 @@ defmodule Berrypod.Accounts do else %User{} |> User.email_changeset(attrs) + |> User.password_changeset(attrs) |> Ecto.Changeset.put_change(:confirmed_at, DateTime.utc_now(:second)) |> Repo.insert() end diff --git a/lib/berrypod/dev_reset.ex b/lib/berrypod/dev_reset.ex new file mode 100644 index 0000000..cc46f09 --- /dev/null +++ b/lib/berrypod/dev_reset.ex @@ -0,0 +1,78 @@ +if Mix.env() == :dev do + defmodule Berrypod.DevReset do + @moduledoc """ + Dev-only helper to wipe all data and flush caches without restarting. + + Usage from IEx or Tidewave eval: + + Berrypod.DevReset.run() + """ + + alias Berrypod.Repo + + # Tables in deletion order (children before parents) + @tables ~w( + order_items + orders + abandoned_carts + product_images + product_variants + products + provider_connections + favicon_variants + images + users_tokens + users + pages + analytics_events + newsletter_subscribers + newsletter_campaigns + redirects + broken_urls + dead_links + activity_log + email_suppressions + settings + ) + + def run do + # Reconnect the Repo in case mix ecto.reset replaced the DB file + # while the server was running (old connections would point to a ghost file) + Supervisor.terminate_child(Berrypod.Supervisor, Repo) + Supervisor.restart_child(Berrypod.Supervisor, Repo) + Process.sleep(100) + + IO.puts("Wiping all data...") + + Repo.query!("PRAGMA foreign_keys = OFF") + + for table <- @tables do + Repo.query!("DELETE FROM \"#{table}\"") + end + + # Clear Oban jobs too + Repo.query!("DELETE FROM oban_jobs") + + Repo.query!("PRAGMA foreign_keys = ON") + + IO.puts("Flushing caches and runtime config...") + + # ETS caches (invalidate_all clears computed/cached values too) + Berrypod.Settings.SettingsCache.invalidate_all() + Berrypod.Theme.CSSCache.invalidate() + Berrypod.Pages.PageCache.invalidate_all() + + # Redirects ETS table + if :ets.whereis(:redirects_cache) != :undefined do + :ets.delete_all_objects(:redirects_cache) + end + + # Runtime Application env + Application.put_env(:berrypod, Berrypod.Mailer, adapter: Swoosh.Adapters.Local) + Application.put_env(:swoosh, :api_client, false) + + IO.puts("Done — visit /setup to start fresh") + :ok + end + end +end diff --git a/lib/berrypod/settings.ex b/lib/berrypod/settings.ex index ba3d3c0..8233d7d 100644 --- a/lib/berrypod/settings.ex +++ b/lib/berrypod/settings.ex @@ -202,23 +202,29 @@ defmodule Berrypod.Settings do The plaintext is encrypted via Vault before storage. """ def put_secret(key, plaintext) when is_binary(plaintext) do - case Vault.encrypt(plaintext) do - {:ok, encrypted} -> - %Setting{key: key} - |> Setting.changeset(%{ - key: key, - value: "[encrypted]", - value_type: "encrypted", - encrypted_value: encrypted - }) - |> Repo.insert( - on_conflict: {:replace, [:value, :encrypted_value, :value_type, :updated_at]}, - conflict_target: :key - ) + result = + case Vault.encrypt(plaintext) do + {:ok, encrypted} -> + %Setting{key: key} + |> Setting.changeset(%{ + key: key, + value: "[encrypted]", + value_type: "encrypted", + encrypted_value: encrypted + }) + |> Repo.insert( + on_conflict: {:replace, [:value, :encrypted_value, :value_type, :updated_at]}, + conflict_target: :key + ) - {:error, reason} -> - {:error, reason} - end + {:error, reason} -> + {:error, reason} + end + + SettingsCache.invalidate() + SettingsCache.warm() + + result end @doc """ diff --git a/lib/berrypod_web/admin_layout_hook.ex b/lib/berrypod_web/admin_layout_hook.ex index ce1926f..dc67d42 100644 --- a/lib/berrypod_web/admin_layout_hook.ex +++ b/lib/berrypod_web/admin_layout_hook.ex @@ -28,7 +28,6 @@ defmodule BerrypodWeb.AdminLayoutHook do socket |> assign(:current_path, "") |> assign(:site_live, Settings.site_live?()) - |> assign(:email_configured, Berrypod.Mailer.email_configured?()) |> assign(:theme_settings, theme_settings) |> assign(:site_name, Settings.site_name()) |> assign(:site_description, Settings.site_description()) diff --git a/lib/berrypod_web/components/layouts/admin.html.heex b/lib/berrypod_web/components/layouts/admin.html.heex index 0c20eac..3246914 100644 --- a/lib/berrypod_web/components/layouts/admin.html.heex +++ b/lib/berrypod_web/components/layouts/admin.html.heex @@ -18,17 +18,6 @@ - <%!-- email warning banner --%> -
- <.icon name="hero-exclamation-triangle" class="size-5 shrink-0" /> -

- Email delivery isn't set up yet — customers won't receive order confirmations or shipping updates. - <.link navigate={~p"/admin/settings/email"} class="admin-link"> - Configure email - -

-
- <%!-- page content --%>
diff --git a/lib/berrypod_web/live/admin/dashboard.ex b/lib/berrypod_web/live/admin/dashboard.ex index e880239..095e62a 100644 --- a/lib/berrypod_web/live/admin/dashboard.ex +++ b/lib/berrypod_web/live/admin/dashboard.ex @@ -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", diff --git a/lib/berrypod_web/live/admin/email_settings.ex b/lib/berrypod_web/live/admin/email_settings.ex index a08d6f2..2fd4dd9 100644 --- a/lib/berrypod_web/live/admin/email_settings.ex +++ b/lib/berrypod_web/live/admin/email_settings.ex @@ -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"""
+
+ <.icon name="hero-clipboard-document-check" class="size-5 admin-checklist-banner-icon" /> + + You're setting up email for your shop. + + <.link navigate={~p"/admin"} class="admin-link admin-checklist-banner-link"> + ← Back to checklist + +
<.header> Email settings <:subtitle> diff --git a/lib/berrypod_web/live/admin/theme/index.ex b/lib/berrypod_web/live/admin/theme/index.ex index 6d6f366..7f059e6 100644 --- a/lib/berrypod_web/live/admin/theme/index.ex +++ b/lib/berrypod_web/live/admin/theme/index.ex @@ -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 diff --git a/lib/berrypod_web/live/admin/theme/index.html.heex b/lib/berrypod_web/live/admin/theme/index.html.heex index a173103..e6cd93b 100644 --- a/lib/berrypod_web/live/admin/theme/index.html.heex +++ b/lib/berrypod_web/live/admin/theme/index.html.heex @@ -37,14 +37,21 @@ <.link href={~p"/admin"} class="theme-back-link"> <.icon name="hero-arrow-left-mini" class="size-4" /> Admin + +
+ <.icon name="hero-clipboard-document-check" class="size-5 admin-checklist-banner-icon" /> + + You're customising your theme. + + <.link navigate={~p"/admin"} class="admin-link admin-checklist-banner-link"> + ← Back to checklist + +
-

Theme Studio

-

- One theme, infinite possibilities. Every combination is designed to work beautifully. -

+

Theme