diff --git a/assets/css/admin/components.css b/assets/css/admin/components.css index e687f61..c622057 100644 --- a/assets/css/admin/components.css +++ b/assets/css/admin/components.css @@ -1516,6 +1516,13 @@ margin-bottom: 0.75rem; } +.setup-field-note { + font-size: 0.75rem; + color: var(--admin-text-muted); + margin-top: -0.5rem; + margin-bottom: 0.75rem; +} + .setup-link { color: var(--t-text-primary, #171717); text-decoration: underline; @@ -1700,6 +1707,283 @@ margin: 0.25rem 0 1rem; } +/* ── Guided setup flow ── */ + +.setup-guided { + max-width: 600px; + margin: 0 auto; +} + +/* Progress bar */ +.setup-progress { + margin-bottom: 2rem; +} + +.setup-progress-list { + display: flex; + justify-content: center; + gap: 0.5rem; + list-style: none; + padding: 0; + margin: 0; +} + +.setup-progress-item { + flex: 1; + max-width: 140px; +} + +.setup-progress-button { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + width: 100%; + padding: 0.5rem; + border: none; + background: none; + cursor: pointer; + font: inherit; + color: var(--admin-text-muted); + transition: color 0.15s; +} + +.setup-progress-button:hover:not(:disabled) { + color: var(--t-text-primary, #171717); +} + +.setup-progress-button:disabled { + cursor: default; +} + +.setup-progress-indicator { + display: flex; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; + border-radius: 50%; + border: 2px solid var(--t-border-default, #d4d4d4); + background: var(--t-surface-base, #fff); + font-size: 0.875rem; + font-weight: 600; + transition: all 0.15s; +} + +.setup-progress-done .setup-progress-indicator { + background: var(--t-status-success, #22c55e); + border-color: var(--t-status-success, #22c55e); + color: #fff; +} + +.setup-progress-current .setup-progress-indicator { + border-color: var(--t-accent, #6366f1); + color: var(--t-accent, #6366f1); +} + +.setup-progress-label { + font-size: 0.75rem; + font-weight: 500; +} + +.setup-progress-current .setup-progress-label { + color: var(--t-text-primary, #171717); + font-weight: 600; +} + +/* Intro screen */ +.setup-intro { + text-align: center; + padding: 1.5rem; + border: 1px solid var(--t-border-default, #d4d4d4); + border-radius: 0.75rem; + background: var(--t-surface-base, #fff); +} + +.setup-intro-content { + text-align: left; + max-width: 480px; + margin: 0 auto; +} + +.setup-intro-lead { + font-size: 1rem; + line-height: 1.6; + margin-bottom: 1.5rem; +} + +.setup-intro-list { + list-style: none; + padding: 0; + margin: 0 0 1.5rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.setup-intro-list li { + display: flex; + align-items: flex-start; + gap: 0.75rem; +} + +.setup-intro-icon { + width: 1.25rem; + height: 1.25rem; + color: var(--t-accent, #6366f1); + flex-shrink: 0; + margin-top: 0.125rem; +} + +.setup-intro-list strong { + display: block; + font-weight: 600; + margin-bottom: 0.125rem; +} + +.setup-intro-list span { + font-size: 0.875rem; + color: var(--admin-text-muted); +} + +.setup-intro-note { + font-size: 0.875rem; + color: var(--admin-text-muted); + padding: 0.75rem 1rem; + background: var(--t-surface-sunken, #f5f5f5); + border-radius: 0.5rem; + margin-bottom: 1rem; +} + +.setup-actions-center { + justify-content: center; +} + +/* Step container */ +.setup-step { + border: 1px solid var(--t-border-default, #d4d4d4); + border-radius: 0.75rem; + background: var(--t-surface-base, #fff); + overflow: hidden; +} + +.setup-step-header { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 1rem 1.25rem; + border-bottom: 1px solid var(--t-border-default, #d4d4d4); + background: var(--t-surface-sunken, #f5f5f5); +} + +.setup-step-number { + display: flex; + align-items: center; + justify-content: center; + width: 1.75rem; + height: 1.75rem; + border-radius: 50%; + background: var(--t-accent, #6366f1); + color: #fff; + font-size: 0.875rem; + font-weight: 600; +} + +.setup-step-number-done { + background: var(--t-status-success, #22c55e); +} + +.setup-step-title { + font-size: 1rem; + font-weight: 600; + margin: 0; +} + +.setup-step-body { + padding: 1.25rem; +} + +.setup-step-done { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1rem; + background: color-mix(in oklab, var(--t-status-success, #22c55e) 10%, transparent); + border-radius: 0.5rem; + margin-bottom: 1rem; +} + +.setup-step-done-icon { + width: 1.25rem; + height: 1.25rem; + color: var(--t-status-success, #22c55e); +} + +.setup-step-done p { + margin: 0; + font-size: 0.875rem; +} + +/* Step navigation */ +.setup-step-nav { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: 1rem 1.25rem; + border-top: 1px solid var(--t-border-default, #d4d4d4); + background: var(--t-surface-sunken, #f5f5f5); +} + +.setup-step-nav-left, +.setup-step-nav-right { + display: flex; + align-items: center; +} + +.setup-step-back { + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 0.5rem 0.75rem; + border: none; + background: none; + font: inherit; + font-size: 0.875rem; + color: var(--admin-text-muted); + cursor: pointer; + transition: color 0.15s; +} + +.setup-step-back:hover { + color: var(--t-text-primary, #171717); +} + +.setup-step-skip { + text-align: right; +} + +.setup-step-skip-btn { + padding: 0.5rem 1rem; + border: 1px solid var(--t-border-default, #d4d4d4); + border-radius: 0.375rem; + background: var(--t-surface-base, #fff); + font: inherit; + font-size: 0.875rem; + cursor: pointer; + transition: all 0.15s; +} + +.setup-step-skip-btn:hover { + border-color: var(--t-border-input, #a3a3a3); +} + +.setup-step-skip-note { + font-size: 0.75rem; + color: var(--admin-text-muted); + margin-top: 0.5rem; + max-width: 200px; +} + /* ── Dashboard launch checklist ── */ .admin-checklist { @@ -1834,6 +2118,37 @@ margin-bottom: 0; } +.admin-checklist-help { + display: inline-flex; + margin-left: 0.25rem; + color: var(--admin-text-muted); + vertical-align: middle; + + &:hover { color: var(--t-accent); } +} + +/* ── Status banners ── */ + +.admin-status-banner { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1rem; + border-radius: 0.5rem; + font-size: 0.875rem; + font-weight: 500; +} + +.admin-status-banner-live { + background: oklch(0.95 0.05 145); + color: oklch(0.35 0.15 145); +} + +.admin-status-banner-setup { + background: oklch(0.95 0.05 250); + color: oklch(0.35 0.15 250); +} + /* ── Page editor ── */ .page-list { diff --git a/assets/css/shop/components.css b/assets/css/shop/components.css index de0ace7..4212cbb 100644 --- a/assets/css/shop/components.css +++ b/assets/css/shop/components.css @@ -2898,12 +2898,13 @@ } .coming-soon-logo { + display: flex; + justify-content: center; margin-bottom: 1.5rem; & img { max-height: 4rem; max-width: 12rem; - margin-inline: auto; object-fit: contain; } } @@ -2924,12 +2925,19 @@ } .coming-soon-admin-link { - font-size: 0.75rem; + position: fixed; + bottom: 1rem; + right: 1rem; + padding: 0.25rem 0.5rem; + font-size: 0.6875rem; color: var(--t-text-tertiary); text-decoration: none; + opacity: 0.5; + transition: opacity 0.15s ease; &:hover { color: var(--t-text-secondary); + opacity: 1; } } } diff --git a/berrypod_dev.db.backup-20260309-230545 b/berrypod_dev.db.backup-20260309-230545 new file mode 100644 index 0000000..0ee8469 Binary files /dev/null and b/berrypod_dev.db.backup-20260309-230545 differ diff --git a/berrypod_dev.db.backup-20260309-232703 b/berrypod_dev.db.backup-20260309-232703 new file mode 100644 index 0000000..0ee8469 Binary files /dev/null and b/berrypod_dev.db.backup-20260309-232703 differ diff --git a/docs/plans/onboarding-ux.md b/docs/plans/onboarding-ux.md index 2e4e100..e92835d 100644 --- a/docs/plans/onboarding-ux.md +++ b/docs/plans/onboarding-ux.md @@ -1,6 +1,6 @@ # Onboarding UX v2 -Status: In progress +Status: Complete Supersedes the original onboarding-ux plan. Based on usability testing session (March 2026) covering the setup wizard, launch checklist, email provider setup, and general onboarding flow. @@ -156,15 +156,15 @@ Increase input field border contrast to meet WCAG AA (3:1 minimum for UI compone | # | Task | Est | Status | |---|------|-----|--------| -| A | Simplify initial setup to account creation only | 1.5h | planned | -| B | Guided setup flow with progress bar | 4h | planned | -| C | Forgiving API key validation | 1.5h | planned | +| A | Simplify initial setup to account creation only | 1.5h | done | +| B | Guided setup flow with progress bar | 4h | done | +| C | Forgiving API key validation | 1.5h | done | | D | Email provider setup UX rework | 2h | done | | E | Contextual prompts for skipped steps | 2h | done | -| F | Dashboard checklist and messaging rework | 2h | planned | -| G | Coming soon page fixes (logo + admin link) | 30m | planned | -| H | External links UX (new tabs, icons, aria) | 1h | planned | -| I | Input styling — WCAG compliance | 1h | planned | +| F | Dashboard checklist and messaging rework | 2h | done | +| G | Coming soon page fixes (logo + admin link) | 30m | done | +| H | External links UX (new tabs, icons, aria) | 1h | done | +| I | Input styling — WCAG compliance | 1h | done | Total estimate: ~15.5h diff --git a/lib/berrypod/key_validation.ex b/lib/berrypod/key_validation.ex index ad3b727..78e447d 100644 --- a/lib/berrypod/key_validation.ex +++ b/lib/berrypod/key_validation.ex @@ -45,9 +45,11 @@ defmodule Berrypod.KeyValidation do @doc """ Validates a print provider API key (Printify, Printful, etc.). - Provider tokens are opaque, so we just check for empty/too-short values. + Checks known formats for each provider: + - Printify: 36-character UUID format + - Printful: typically 40+ characters """ - def validate_provider_key(key, _provider_type \\ nil) do + def validate_provider_key(key, provider_type \\ nil) do key = trim(key) cond do @@ -57,11 +59,58 @@ defmodule Berrypod.KeyValidation do String.length(key) < 10 -> {:error, "This looks too short for an API token. Check you copied the full value"} + true -> + validate_provider_format(key, provider_type) + end + end + + # Printify uses 36-character UUID format + defp validate_provider_format(key, "printify") do + # UUID format: 8-4-4-4-12 with hyphens = 36 chars + cond do + String.length(key) == 36 and uuid_format?(key) -> + {:ok, key} + + String.length(key) < 32 -> + {:error, + "This looks too short — Printify API keys are 36 characters. Find yours at Settings → Connections"} + + String.length(key) > 40 -> + {:error, + "This looks too long — Printify API keys are 36 characters. Make sure you're copying just the API key"} + + not uuid_format?(key) and String.length(key) in 32..40 -> + # Close enough in length but wrong format - might be missing hyphens + {:error, + "Printify API keys are in UUID format (like xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx). Check you copied the full key"} + true -> {:ok, key} end end + # Printful tokens are typically longer OAuth-style tokens + defp validate_provider_format(key, "printful") do + cond do + String.length(key) < 20 -> + {:error, "This looks too short for a Printful token. Find yours at Settings → API Access"} + + true -> + {:ok, key} + end + end + + # Unknown provider - basic validation + defp validate_provider_format(key, _provider_type), do: {:ok, key} + + # Check if string matches UUID format (8-4-4-4-12 hex with hyphens) + defp uuid_format?(key) do + String.match?( + key, + ~r/^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$/ + ) + end + @doc """ Validates an email provider API key or secret. diff --git a/lib/berrypod/theme/css_generator.ex b/lib/berrypod/theme/css_generator.ex index 93fdeef..79ece7d 100644 --- a/lib/berrypod/theme/css_generator.ex +++ b/lib/berrypod/theme/css_generator.ex @@ -72,7 +72,7 @@ defmodule Berrypod.Theme.CSSGenerator do --t-text-inverse: #ffffff; --t-border-default: #e5e5e5; --t-border-subtle: #f0f0f0; - --t-border-input: #8c8c8c; + --t-border-input: #767676; """ end @@ -89,7 +89,7 @@ defmodule Berrypod.Theme.CSSGenerator do --t-text-inverse: #ffffff; --t-border-default: #e7e0d8; --t-border-subtle: #f0ebe4; - --t-border-input: #8a827a; + --t-border-input: #706860; """ end @@ -106,7 +106,7 @@ defmodule Berrypod.Theme.CSSGenerator do --t-text-inverse: #ffffff; --t-border-default: #d4dce8; --t-border-subtle: #e8eff5; - --t-border-input: #7a8591; + --t-border-input: #606a7a; """ end @@ -123,7 +123,7 @@ defmodule Berrypod.Theme.CSSGenerator do --t-text-inverse: #171717; --t-border-default: #262626; --t-border-subtle: #1c1c1c; - --t-border-input: #707070; + --t-border-input: #808080; """ end diff --git a/lib/berrypod_web/live/admin/dashboard.ex b/lib/berrypod_web/live/admin/dashboard.ex index 98aed61..49b60c6 100644 --- a/lib/berrypod_web/live/admin/dashboard.ex +++ b/lib/berrypod_web/live/admin/dashboard.ex @@ -64,6 +64,24 @@ defmodule BerrypodWeb.Admin.Dashboard do + <%!-- Shop is live banner (not just now, but already live) --%> +
+ + <%!-- Setup in progress message --%> + + <%!-- Launch checklist --%> <.launch_checklist :if={@show_checklist and !@just_went_live} @@ -222,6 +240,16 @@ defmodule BerrypodWeb.Admin.Dashboard do{item.hint} + + <.icon name="hero-question-mark-circle-mini" class="size-4" /> + Help (opens in new window) +
- "You'll see the order in Orders when it works." + hint: "Use card 4242 4242 4242 4242 with any future expiry and CVC.", + optional: true }, %{key: :site_live, label: "Go live"} ] diff --git a/lib/berrypod_web/live/admin/providers/form.ex b/lib/berrypod_web/live/admin/providers/form.ex index b449d56..58f8210 100644 --- a/lib/berrypod_web/live/admin/providers/form.ex +++ b/lib/berrypod_web/live/admin/providers/form.ex @@ -40,14 +40,33 @@ defmodule BerrypodWeb.Admin.Providers.Form do @impl true def handle_event("validate", %{"provider_connection" => params}, socket) do - form = + api_key = params["api_key"] || "" + provider_type = socket.assigns.provider_type + + # Build base changeset + changeset = socket.assigns.connection |> ProviderConnection.changeset(params) |> Map.put(:action, :validate) - |> to_form() + + # Add key format validation error if key is present + form = + if api_key != "" do + case KeyValidation.validate_provider_key(api_key, provider_type) do + {:ok, _} -> + to_form(changeset) + + {:error, message} -> + changeset + |> Ecto.Changeset.add_error(:api_key, message) + |> to_form() + end + else + to_form(changeset) + end # Store api_key separately since changeset encrypts it immediately - {:noreply, assign(socket, form: form, pending_api_key: params["api_key"])} + {:noreply, assign(socket, form: form, pending_api_key: api_key)} end @impl true diff --git a/lib/berrypod_web/live/admin/providers/form.html.heex b/lib/berrypod_web/live/admin/providers/form.html.heex index d796664..399d449 100644 --- a/lib/berrypod_web/live/admin/providers/form.html.heex +++ b/lib/berrypod_web/live/admin/providers/form.html.heex @@ -51,6 +51,7 @@ else: "Paste your key here" } autocomplete="off" + phx-debounce="blur" /> <%= if @live_action == :edit do %> diff --git a/lib/berrypod_web/live/setup/onboarding.ex b/lib/berrypod_web/live/setup/onboarding.ex index 0c8eb24..5075e0b 100644 --- a/lib/berrypod_web/live/setup/onboarding.ex +++ b/lib/berrypod_web/live/setup/onboarding.ex @@ -5,6 +5,14 @@ defmodule BerrypodWeb.Setup.Onboarding do alias Berrypod.Providers.Provider alias Berrypod.Stripe.Setup, as: StripeSetup + # Steps in the guided flow (after account creation): + # :intro - explains what's needed + # :provider - connect print provider + # :stripe - connect Stripe payments + # :email - set up email provider + + @guided_steps [:intro, :provider, :stripe, :email] + # ── Mount ── @impl true @@ -37,18 +45,15 @@ defmodule BerrypodWeb.Setup.Onboarding do logged_in? = get_user(socket) != nil provider_conn = Products.get_first_provider_connection() - current_step = - cond do - not logged_in? -> 1 - not setup.provider_connected -> 2 - true -> 3 - end + # Determine which guided step to show (for logged-in users) + guided_step = determine_guided_step(setup) socket |> assign(:page_title, "Set up your shop") |> assign(:setup, setup) |> assign(:logged_in?, logged_in?) - |> assign(:current_step, current_step) + |> assign(:guided_step, guided_step) + |> assign(:guided_steps, @guided_steps) # Secret gate |> assign(:require_secret?, Setup.require_setup_secret?()) |> assign(:secret_verified, false) @@ -56,7 +61,10 @@ defmodule BerrypodWeb.Setup.Onboarding do # Account (card 1) |> assign( :account_form, - to_form(%{"email" => "", "password" => "", "shop_name" => ""}, as: :account) + to_form( + %{"email" => "", "password" => "", "password_confirmation" => "", "shop_name" => ""}, + as: :account + ) ) # Provider (card 2) |> assign(:providers, Provider.all()) @@ -69,6 +77,30 @@ defmodule BerrypodWeb.Setup.Onboarding do |> assign(:stripe_connecting, false) end + # Determine which guided step to show based on what's already configured + defp determine_guided_step(setup) do + cond do + # Show intro on first visit after account creation + not setup.provider_connected and not setup.stripe_connected and + not setup.email_configured -> + :intro + + # Resume at first incomplete step + not setup.provider_connected -> + :provider + + not setup.stripe_connected -> + :stripe + + not setup.email_configured -> + :email + + # All done - will redirect to dashboard + true -> + :email + end + end + # ── Events: Secret gate ── @impl true @@ -80,19 +112,69 @@ defmodule BerrypodWeb.Setup.Onboarding do end end + # ── Events: Guided flow navigation ── + + def handle_event("start_setup", _params, socket) do + {:noreply, assign(socket, :guided_step, :provider)} + end + + def handle_event("go_to_step", %{"step" => step}, socket) do + step = String.to_existing_atom(step) + + if step in @guided_steps do + {:noreply, assign(socket, :guided_step, step)} + else + {:noreply, socket} + end + end + + def handle_event("skip_step", _params, socket) do + next_step = next_guided_step(socket.assigns.guided_step) + handle_step_completion(socket, next_step) + end + + def handle_event("go_back", _params, socket) do + prev_step = prev_guided_step(socket.assigns.guided_step) + {:noreply, assign(socket, :guided_step, prev_step)} + end + # ── Events: Account ── + def handle_event("validate_account", %{"account" => params}, socket) do + errors = validate_account_fields(params) + + form = + to_form(params, + as: :account, + errors: errors, + action: if(errors != [], do: :validate) + ) + + {:noreply, assign(socket, :account_form, form)} + end + def handle_event("create_account", %{"account" => params}, socket) do email = params["email"] password = params["password"] + password_confirmation = params["password_confirmation"] shop_name = String.trim(params["shop_name"] || "") - cond do - shop_name == "" -> - {:noreply, put_flash(socket, :error, "Please enter a shop name")} + errors = validate_account_fields(params) - email == "" -> - {:noreply, put_flash(socket, :error, "Please enter your email address")} + cond do + errors != [] -> + form = to_form(params, as: :account, errors: errors, action: :validate) + {:noreply, assign(socket, :account_form, form)} + + password != password_confirmation -> + form = + to_form(params, + as: :account, + errors: [password_confirmation: {"Passwords don't match", []}], + action: :validate + ) + + {:noreply, assign(socket, :account_form, form)} true -> Settings.put_setting("site_name", shop_name, "string") @@ -149,19 +231,13 @@ defmodule BerrypodWeb.Setup.Onboarding do {:ok, connection} -> setup = Setup.setup_status() - if setup.setup_complete do - {:noreply, - socket - |> put_flash(:info, "You're in! Here's your launch checklist.") - |> push_navigate(to: ~p"/admin")} - else - {:noreply, - socket - |> assign(:provider_connecting, false) - |> assign(:provider_conn, connection) - |> assign(:setup, setup) - |> put_flash(:info, "Connected! Product sync started in the background.")} - end + {:noreply, + socket + |> assign(:provider_connecting, false) + |> assign(:provider_conn, connection) + |> assign(:setup, setup) + |> assign(:guided_step, :stripe) + |> put_flash(:info, "Connected! Product sync started in the background.")} {:error, :no_api_key} -> form = @@ -192,8 +268,46 @@ defmodule BerrypodWeb.Setup.Onboarding do end end + # Validate provider key on blur for fast feedback + def handle_event("validate_provider", %{"provider" => %{"api_key" => api_key}}, socket) do + type = socket.assigns.selected_provider + + form = + case KeyValidation.validate_provider_key(api_key, type) do + {:ok, _} -> + to_form(%{"api_key" => api_key}, as: :provider) + + {:error, message} -> + to_form(%{"api_key" => api_key}, + as: :provider, + errors: [api_key: {message, []}], + action: :validate + ) + end + + {:noreply, assign(socket, :provider_form, form)} + end + # ── Events: Stripe ── + # Validate Stripe key on blur for fast feedback + def handle_event("validate_stripe", %{"stripe" => %{"api_key" => api_key}}, socket) do + form = + case KeyValidation.validate_stripe_key(api_key) do + {:ok, _} -> + to_form(%{"api_key" => api_key}, as: :stripe) + + {:error, message} -> + to_form(%{"api_key" => api_key}, + as: :stripe, + errors: [api_key: {message, []}], + action: :validate + ) + end + + {:noreply, assign(socket, :stripe_form, form)} + end + def handle_event("connect_stripe", %{"stripe" => %{"api_key" => api_key}}, socket) do case KeyValidation.validate_stripe_key(api_key) do {:error, message} -> @@ -213,18 +327,12 @@ defmodule BerrypodWeb.Setup.Onboarding do {:ok, _result} -> setup = Setup.setup_status() - if setup.setup_complete do - {:noreply, - socket - |> put_flash(:info, "You're in! Here's your launch checklist.") - |> push_navigate(to: ~p"/admin")} - else - {:noreply, - socket - |> assign(:stripe_connecting, false) - |> assign(:setup, setup) - |> put_flash(:info, "Stripe connected")} - end + {:noreply, + socket + |> assign(:stripe_connecting, false) + |> assign(:setup, setup) + |> assign(:guided_step, :email) + |> put_flash(:info, "Stripe connected")} {:error, message} -> form = @@ -242,6 +350,37 @@ defmodule BerrypodWeb.Setup.Onboarding do end end + # ── Navigation helpers ── + + defp next_guided_step(current) do + case current do + :intro -> :provider + :provider -> :stripe + :stripe -> :email + :email -> :done + end + end + + defp prev_guided_step(current) do + case current do + :provider -> :intro + :stripe -> :provider + :email -> :stripe + _ -> current + end + end + + defp handle_step_completion(socket, :done) do + {:noreply, + socket + |> put_flash(:info, "Setup complete! Here's your launch checklist.") + |> push_navigate(to: ~p"/admin")} + end + + defp handle_step_completion(socket, next_step) do + {:noreply, assign(socket, :guided_step, next_step)} + end + # ── Render ── @impl true @@ -288,16 +427,16 @@ defmodule BerrypodWeb.Setup.Onboarding do <% else %> - <%!-- All three setup cards --%> -
Name your shop and create the admin account.
- <.form for={@account_form} phx-submit="create_account"> + <.form for={@account_form} phx-submit="create_account" phx-change="validate_account"> <.input field={@account_form[:shop_name]} type="text" @@ -305,8 +444,9 @@ defmodule BerrypodWeb.Setup.Onboarding do placeholder="e.g. Acme Prints" autocomplete="off" required - phx-mounted={@current_step == 1 && JS.focus()} + phx-mounted={JS.focus()} /> +You can change this later
<.input field={@account_form[:email]} type="email" @@ -322,48 +462,149 @@ defmodule BerrypodWeb.Setup.Onboarding do autocomplete="new-password" required /> + <.input + field={@account_form[:password_confirmation]} + type="password" + label="Confirm password" + autocomplete="new-password" + required + />Head to the dashboard to sync products, customise your theme, and go live.
- <.link navigate={~p"/admin"} class="admin-btn admin-btn-primary"> - Go to dashboard - + <%!-- Guided setup flow (after login) --%> ++ Berrypod connects your print-on-demand products to your own online shop. + To get fully set up, you'll need three things: +
++ Don't worry if you don't have all of these yet — you can skip any step and set it up later. +
++ Configure an email provider so your shop can send order confirmations, + shipping updates, and newsletters. +
+ <%= if @setup.email_configured do %> +Email is already configured.
++ <.link navigate={~p"/admin/settings/email"} class="setup-link"> + Set up email in settings + +
+ <% end %> +{@skip_note}
+ <% end %> +