From 26f2d61582d15427fc4372ba00e22c66dbd91f29 Mon Sep 17 00:00:00 2001 From: Sanne de Vries Date: Tue, 28 Jul 2026 16:22:52 +0200 Subject: [PATCH 1/3] Improve onboarding pages - Give the add site and installation pages a plain white layout with a heading, subtitle and a progress indicator at the bottom of the page - Remove the timezone field from the add site form, and detect it automatically - Rebuild the installation instructions as numbered steps with click-to-copy code, and hide the extra options during onboarding - Offer a way out of every step with a secondary button, either skipping ahead or heading back to where you came from - Drop the separate verification step from the progress indicator, as verification now happens on the dashboard - Tidy up shared form and UI details along the way: help text under fields, read-only inputs, smaller toggles and an improved loading spinner --- assets/js/app.js | 2 + assets/js/liveview/copy-snippet.js | 34 ++ e2e/tests/fixtures.ts | 13 +- lib/plausible/site.ex | 11 +- lib/plausible_web/components/flow_progress.ex | 63 ++-- lib/plausible_web/components/generic.ex | 83 ++--- lib/plausible_web/components/icons.ex | 19 + .../components/site/toggle_live.ex | 6 +- .../controllers/auth_controller.ex | 56 ++- lib/plausible_web/controllers/helpers.ex | 16 + .../controllers/site_controller.ex | 56 ++- lib/plausible_web/flows.ex | 12 +- lib/plausible_web/live/components/form.ex | 40 +-- .../live/components/verification_banner.ex | 7 +- lib/plausible_web/live/installation.ex | 160 ++++++--- lib/plausible_web/live/installation/icons.ex | 8 +- .../live/installation/instructions.ex | 333 +++++++++--------- .../live/onboarding_layout_context.ex | 19 + lib/plausible_web/router.ex | 14 +- .../templates/layout/_header.html.heex | 3 +- .../templates/layout/app.html.heex | 8 +- .../templates/layout/onboarding.html.heex | 28 ++ .../templates/site/new.html.heex | 82 ++--- test/plausible/site/domain_test.exs | 8 +- .../components/flow_progress_test.exs | 43 ++- ...l_sites_controller_sites_crud_api_test.exs | 2 +- .../controllers/site_controller_test.exs | 12 +- .../plausible_web/live/change_domain_test.exs | 6 +- .../components/verification_banner_test.exs | 4 +- test/plausible_web/live/installation_test.exs | 164 ++++++--- 30 files changed, 759 insertions(+), 553 deletions(-) create mode 100644 assets/js/liveview/copy-snippet.js create mode 100644 lib/plausible_web/live/onboarding_layout_context.ex create mode 100644 lib/plausible_web/templates/layout/onboarding.html.heex diff --git a/assets/js/app.js b/assets/js/app.js index f73084a96689..ca1143490f4a 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -3,11 +3,13 @@ import 'abortcontroller-polyfill/dist/polyfill-patch-fetch' import Alpine from 'alpinejs' import './liveview/live_socket' import comboBox from './liveview/combo-box' +import copySnippet from './liveview/copy-snippet' import dropdown from './liveview/dropdown' import './liveview/phx_events' Alpine.data('dropdown', dropdown) Alpine.data('comboBox', comboBox) +Alpine.data('copySnippet', copySnippet) Alpine.start() if (document.querySelectorAll('[data-modal]').length > 0) { diff --git a/assets/js/liveview/copy-snippet.js b/assets/js/liveview/copy-snippet.js new file mode 100644 index 000000000000..e3f6fdb3e7e8 --- /dev/null +++ b/assets/js/liveview/copy-snippet.js @@ -0,0 +1,34 @@ +// Copy-to-clipboard behaviour for a readonly textarea holding a code snippet. +// Expects an `x-ref="snippet"` textarea within the same `x-data` scope. + +const RESET_DELAY = 2000 + +const hasSelection = (el) => el.selectionStart !== el.selectionEnd + +const isFullySelected = (el) => + el.selectionStart === 0 && el.selectionEnd === el.value.length + +export default () => ({ + copied: false, + copyAll() { + const el = this.$refs.snippet + el.focus() + el.select() + document.execCommand('copy') + this.copied = true + + setTimeout(() => { + this.copied = false + + // Leave it alone if the user has highlighted their own text since. + if (isFullySelected(el)) { + el.setSelectionRange(0, 0) + el.blur() + } + }, RESET_DELAY) + }, + copyIfNoSelection() { + if (hasSelection(this.$refs.snippet)) return + this.copyAll() + } +}) diff --git a/e2e/tests/fixtures.ts b/e2e/tests/fixtures.ts index 34eeabc2c59c..f3022996ee37 100644 --- a/e2e/tests/fixtures.ts +++ b/e2e/tests/fixtures.ts @@ -115,9 +115,7 @@ export async function register({ await page.getByRole('button', { name: 'Activate' }).click() - await expect( - page.getByRole('button', { name: 'Install Plausible' }) - ).toBeVisible() + await expect(page.getByRole('button', { name: 'Add site' })).toBeVisible() } export async function login({ page, user }: { page: Page; user: User }) { @@ -149,19 +147,16 @@ export async function addSite({ }) { await page.goto('/sites/new', { waitUntil: 'commit' }) - await expect( - page.getByRole('button', { name: 'Install Plausible' }) - ).toBeVisible() + await expect(page.getByRole('button', { name: 'Add site' })).toBeVisible() await page.getByLabel('Domain').fill(domain) - await page.getByLabel('Reporting timezone').selectOption('Etc/UTC') - await page.getByRole('button', { name: 'Install Plausible' }).click() + await page.getByRole('button', { name: 'Add site' }).click() await expect(page).toHaveURL(/\/installation/) await expect( - page.getByRole('button', { name: /Verify .* installation/ }) + page.getByRole('button', { name: "I've installed it" }) ).toBeVisible() } diff --git a/lib/plausible/site.ex b/lib/plausible/site.ex index b4c5aaa2daca..33da11e57036 100644 --- a/lib/plausible/site.ex +++ b/lib/plausible/site.ex @@ -97,13 +97,9 @@ defmodule Plausible.Site do end on_ee do - @domain_unique_error """ - This domain cannot be registered. Perhaps one of your colleagues registered it? If that's not the case, please contact support@plausible.io - """ + @domain_unique_error "This domain is already registered. Ask the owner for access, or contact support@plausible.io" else - @domain_unique_error """ - This domain cannot be registered. Perhaps one of your colleagues registered it? - """ + @domain_unique_error "This domain is already registered. Ask the owner for access." end on_ee do @@ -116,7 +112,8 @@ defmodule Plausible.Site do site |> cast(attrs, @changeset_cast_fields) |> clean_domain() - |> validate_required([:domain, :timezone]) + |> validate_required([:domain], message: "Please enter a domain or subdomain") + |> validate_required([:timezone]) |> validate_timezone() |> validate_domain_format() |> validate_domain_reserved_characters() diff --git a/lib/plausible_web/components/flow_progress.ex b/lib/plausible_web/components/flow_progress.ex index 53288065fedd..705a00a2c725 100644 --- a/lib/plausible_web/components/flow_progress.ex +++ b/lib/plausible_web/components/flow_progress.ex @@ -1,8 +1,8 @@ defmodule PlausibleWeb.Components.FlowProgress do @moduledoc """ - Component for provisioning/registration flows displaying - progress status. See `PlausibleWeb.Flows` for the list of - flow definitions. + Dotted progress indicator shown during the registration flow. + One small dot per step in `PlausibleWeb.Flows.steps/1`, with completed + and current steps highlighted. """ use Phoenix.Component @@ -20,45 +20,24 @@ defmodule PlausibleWeb.Components.FlowProgress do ) ~H""" -