Skip to content

Commit 0997804

Browse files
authored
Trim the profile-completion checklist to three steps (#2001)
* Die Profil-Checkliste zeigt drei Schritte statt fünf Der Tag-Schritt war für jeden neu Angemeldeten schon abgehakt, weil die Registrierung drei Tags verlangt, und lehrte deshalb nichts; den Follow-Schritt ersetzt die daneben stehende Karte „Wem folgen", die echte Gesichter zeigt, statt bis fünf zu zählen. Der LinkedIn-Import rückt dafür von der Fußzeile in die Liste: er füllt als einziger Eintrag mehrere Profilabschnitte auf einmal und gilt als erledigt, sobald ein Beruf oder eine Ausbildung eingetragen ist, von Hand genauso wie aus dem Archiv. Den Link zu den LinkedIn-Kontakten trägt weiterhin die Karte „Wem folgen". Die Liste wird jetzt nur noch für den Eigentümer innerhalb des Onboarding- Fensters gebaut, nicht mehr bei jedem fremden Profilaufruf. Diesen Text hat ein KI-Agent in meinem Namen geschrieben. Ich weiß, dass das problematisch ist. Claude-Session: https://claude.ai/code/session_01U7NgwBLboWUBQ5U6g3GJEk * Die Checkliste springt zum Feld, nicht an den Seitenanfang Die Einstellungsseite ist mehrere Bildschirme lang, und die Kurzbeschreibung liegt weit unter dem Namen: „Kurzbeschreibung hinzufügen" landete deshalb oben und ließ den Leser selbst suchen. Das Feld bekommt eine Sprungmarke wie das Profilbild und das Titelbild sie längst haben (#avatar, #cover), und beide Schritte der Checkliste tragen jetzt die Marke ihres eigenen Feldes. Der Test prüft mit, dass die Marken auf dem Formular wirklich existieren, damit kein Sprung ins Leere geht. Diesen Text hat ein KI-Agent in meinem Namen geschrieben. Ich weiß, dass das problematisch ist. Claude-Session: https://claude.ai/code/session_01U7NgwBLboWUBQ5U6g3GJEk * Kataloge über den vereinten Stand neu erzeugt Der Rebase auf main traf in allen vier Katalogen auf Konflikte, weil #2000 gleichzeitig elf msgids für die Feed-Filter hinzugefügt hat. Statt die Hunks von Hand aufzulösen steht hier mains Seite und darüber ein Lauf von `mix gettext.extract --merge`, der die zwei msgids des Follow-Schritts entfernt. Keine neuen fuzzy-Einträge. Diesen Text hat ein KI-Agent in meinem Namen geschrieben. Ich weiß, dass das problematisch ist. Claude-Session: https://claude.ai/code/session_01U7NgwBLboWUBQ5U6g3GJEk
1 parent ebdf5ae commit 0997804

11 files changed

Lines changed: 1848 additions & 1998 deletions

File tree

docs/architecture/settings-and-account.md

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -349,13 +349,19 @@ parsing and case-insensitive de-duplication the tag creation uses).
349349

350350
After the confirmation PIN a fresh member lands on their own profile, where the
351351
**"Complete your profile" checklist** (owner-only, first hour after sign-up —
352-
`UserProfileLive.@onboarding_window_seconds`) opens with the tag step already
353-
checked — 1/5 done — and leads through photo → tagline (Kurzbeschreibung) →
354-
**first post** (suggesting a topic from the member's own tags, "Zum Beispiel
355-
ein Gedanke zu #elixir") → **"Follow 5 members"**. The follow step shows the
356-
running count as its hint, ticks off live when the fifth follow happens on the
357-
page itself, and links to the "Who to follow" card — or, on an installation
358-
with nobody to suggest, to the member directory.
352+
`UserProfileLive.@onboarding_window_seconds`) names three steps: photo →
353+
tagline (Kurzbeschreibung) → **"Import from LinkedIn"**. The import is the one
354+
step that fills several profile sections at once, which is why it is a step and
355+
not a link under the card; it counts as done once the profile carries a work
356+
experience or an education entry, so a member who types one in by hand is not
357+
left with a step they cannot finish.
358+
359+
Three things are deliberately *not* on the list. A **tag** step, because
360+
sign-up already requires three tags, so it only ever arrived pre-checked. A
361+
**first post**, because it asks the one thing a member cannot do well in their
362+
first minute here, with nobody yet reading. And a **follow** step, because the
363+
promoted "Who to follow" card sits right beside the checklist with real faces
364+
on it, and a checkbox counting to five read as a quota.
359365

360366
While the owner follows fewer than five members
361367
(`UserProfileLive.@discovery_follow_target`), the profile also renders the
@@ -373,11 +379,7 @@ then thinned by the per-viewer exclusions (owner, viewer, already-followed,
373379
blocked). Follower totals deliberately play no part — they reward the past,
374380
while the card's promise is a feed with something in it, which only current,
375381
liked output can keep. Deliberately strict, so a thin card beats a padded
376-
one; an installation with no recent posts renders no card, and the checklist
377-
step then links to the member directory instead.
378-
379-
Work experience is deliberately not on the checklist; its section card keeps its
380-
own add tile.
382+
one; an installation with no recent posts renders no card.
381383

382384
## Username (@handle) changes
383385

lib/vutuv_web/live/user_profile_live.ex

Lines changed: 41 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -612,14 +612,7 @@ defmodule VutuvWeb.UserProfileLive do
612612
force: true
613613
)
614614

615-
socket
616-
|> put_social_assigns(user, [])
617-
# The follow step is the one checklist item completable on this very page
618-
# (the promoted rail's follow buttons), so re-derive the steps and the tick
619-
# lands live. The checklist's visibility and the rail's promoted spot stay
620-
# as mounted; recomputing them here would yank the panel (or the card)
621-
# away under the member's cursor on the fifth follow.
622-
|> refresh_completion_steps()
615+
put_social_assigns(socket, user, [])
623616
end
624617

625618
# The follow-graph slice of the assigns, shared by the initial load and the
@@ -818,8 +811,8 @@ defmodule VutuvWeb.UserProfileLive do
818811
# ── Initial load (ports UserController.show_html) ──
819812

820813
# The discovery threshold: the owner's own profile leads the rail with the
821-
# promoted "Who to follow" card (and the checklist carries a follow step)
822-
# until they follow at least this many members. Below it their feed is too
814+
# promoted "Who to follow" card until they follow at least this many members.
815+
# Below it their feed is too
823816
# empty to be worth visiting (Home.path even keeps them on the profile), so
824817
# discovery outranks their own detail cards.
825818
@discovery_follow_target 5
@@ -946,6 +939,15 @@ defmodule VutuvWeb.UserProfileLive do
946939
fn -> owner? && Vutuv.Posts.get_draft(current_user) end
947940
])
948941

942+
# The onboarding checklist is the owner's alone and expires with the
943+
# onboarding window, so a visitor's view — nearly every view of this page —
944+
# skips building it. What is left of the gate (is anything still undone?)
945+
# is the list itself, below.
946+
checklist =
947+
if owner? and not user.onboarding_dismissed? and onboarding_window?(user),
948+
do: completion_steps(user, totals),
949+
else: []
950+
949951
socket
950952
|> assign(:as_owner?, owner?)
951953
|> ComposerPanel.open_for_draft(draft || nil)
@@ -999,20 +1001,13 @@ defmodule VutuvWeb.UserProfileLive do
9991001
relationship: relationship,
10001002
work_info: work_info
10011003
)
1002-
# The rail promotion and the checklist's follow step both read the followee
1003-
# count put_social_assigns just computed. The promotion is deliberately set
1004-
# only here, never on the social-graph refresh: the fifth follow, made from
1005-
# the promoted rail itself, must not teleport the card to the bottom of the
1006-
# page under the member's cursor. The next visit demotes it.
1007-
|> then(
1008-
&assign(
1009-
&1,
1010-
:promote_discovery?,
1011-
owner? and &1.assigns.followee_count < @discovery_follow_target
1012-
)
1013-
)
1014-
|> refresh_completion_steps()
1015-
|> then(&assign(&1, :show_completion?, show_completion?(&1.assigns)))
1004+
# The rail promotion is deliberately set only here, never on the
1005+
# social-graph refresh: the fifth follow, made from the promoted rail
1006+
# itself, must not teleport the card to the bottom of the page under the
1007+
# member's cursor. The next visit demotes it.
1008+
|> assign(:promote_discovery?, owner? and social_counts.followees < @discovery_follow_target)
1009+
|> assign(:completion_steps, checklist)
1010+
|> assign(:show_completion?, Enum.any?(checklist, &(not &1.done)))
10161011
|> put_social_feed_assigns(user)
10171012
|> put_code_stats_assigns(user)
10181013
|> put_job_search_assigns(user)
@@ -1454,86 +1449,56 @@ defmodule VutuvWeb.UserProfileLive do
14541449
)
14551450
end
14561451

1457-
# Re-derive the checklist from the current assigns; called from the initial
1458-
# load and from the social-graph refresh (the follow step's count changes
1459-
# live). Reads :followee_count, so it must run after put_social_assigns.
1460-
defp refresh_completion_steps(socket) do
1461-
%{user: user, followee_count: followee_count} = socket.assigns
1462-
1463-
assign(
1464-
socket,
1465-
:completion_steps,
1466-
completion_steps(user, followee_count, socket.assigns.recommended_users != [])
1467-
)
1468-
end
1469-
1470-
defp show_completion?(%{as_owner?: owner?, user: user, completion_steps: steps}) do
1471-
owner? and not user.onboarding_dismissed? and
1472-
Enum.any?(steps, &(not &1.done)) and onboarding_window?(user)
1473-
end
1474-
1475-
defp completion_steps(user, followee_count, suggestions?) do
1452+
defp completion_steps(user, totals) do
14761453
[
1477-
# Sign-up requires three tags, so this step arrives already checked: the
1478-
# checklist opens with visible progress instead of a wall of zeros
1479-
# (people finish lists they have visibly started). It stays actionable
1480-
# for tag-less accounts from before the minimum, in their
1481-
# dormant-return window.
1482-
%{label: gettext("Add a tag"), done: user.user_tags != [], href: ~p"/settings/tags/new"},
14831454
# Both land on /settings/profile, which is where the two fields actually
14841455
# live. They used to point at /:slug/edit, the retired owner URL that only
14851456
# redirects there — one wasted round trip, and a link that shows the old
1486-
# address in the status bar.
1457+
# address in the status bar. Each carries the fragment of its own field
1458+
# (the ids are in the form), so the step lands on the input rather than at
1459+
# the top of a page whose fields are several screens apart.
14871460
%{
14881461
label: gettext("Add a profile photo"),
14891462
done: present?(user.avatar),
1490-
href: ~p"/settings/profile"
1463+
href: ~p"/settings/profile#avatar"
14911464
},
14921465
%{
14931466
label: gettext("Add a tagline"),
14941467
done: present?(user.headline),
1495-
href: ~p"/settings/profile"
1468+
href: ~p"/settings/profile#tagline"
14961469
},
1470+
# The importer is a step of its own, not a footer link under the card: it
1471+
# is the one entry here that fills several profile sections in a single
1472+
# go, so it belongs where the eye already is. Done once the profile
1473+
# carries a career entry, which is what the archive brings — typing one in
1474+
# by hand counts just as much, so nobody without a LinkedIn account is
1475+
# left with a step they cannot finish.
1476+
%{
1477+
label: gettext("Import from LinkedIn"),
1478+
done: totals.jobs > 0 or totals.educations > 0,
1479+
href: ~p"/settings/import/linkedin"
1480+
}
14971481
# There is deliberately NO "write your first post" step. It asked the one
14981482
# thing a member cannot do well on their first minute here — they have
14991483
# nobody reading yet and nothing to answer — and it is the step a new
15001484
# account is least likely to complete, so the list ended on a dead end.
15011485
# Posting has its own permanent invitation at the top of the Posts card
15021486
# and on the feed; it does not need a checkbox.
1503-
# vutuv runs on following: the feed stays empty until the member follows
1504-
# people, so the list closes with the social step. Its link jumps to the
1505-
# "Who to follow" card, which the under-threshold owner view promotes to
1506-
# the top of the rail; an installation with nobody to suggest falls back
1507-
# to the browsable member directory instead of a dead anchor.
1508-
%{
1509-
# Deliberately no number in the label. "Follow 5 members" reads as a
1510-
# quota to be served, and the figure is ours, not the member's. The
1511-
# threshold below still decides when the step is done; the hint under it
1512-
# reports real progress, which is the encouraging half of a count.
1513-
label: gettext("Follow other members"),
1514-
done: followee_count >= @discovery_follow_target,
1515-
href: if(suggestions?, do: "#profile-who-to-follow", else: ~p"/system/members"),
1516-
hint: follow_step_hint(followee_count)
1517-
}
1487+
# There is no tag step either (sign-up already requires three, so it
1488+
# arrived checked off and taught nothing) and no follow step: the
1489+
# promoted "Who to follow" card beside the checklist is the invitation,
1490+
# with real faces on it, and a checkbox counting to five read as a quota.
15181491
]
15191492
end
15201493

1521-
# Progress under the follow step ("You already follow 2 members."): visible
1522-
# momentum once the count has started, nothing at zero (the label alone
1523-
# reads cleaner) and nothing once the step is done.
1524-
defp follow_step_hint(n) when n < 1 or n >= @discovery_follow_target, do: nil
1525-
1526-
defp follow_step_hint(n),
1527-
do: ngettext("You already follow one member.", "You already follow %{count} members.", n)
1528-
15291494
defp present?(nil), do: false
15301495
defp present?(value) when is_binary(value), do: String.trim(value) != ""
15311496
defp present?(_), do: true
15321497

15331498
# The checklist is a brief, one-time post-registration nudge: it shows only
15341499
# during the first hour after sign-up, then never again. A member who wants it
15351500
# gone sooner closes it with the × (the dismiss_onboarding event sets
1536-
# users.onboarding_dismissed? for good — see the show_completion? gate above).
1501+
# users.onboarding_dismissed? for good — see the gate in load_profile/1).
15371502
@onboarding_window_seconds 60 * 60
15381503

15391504
defp onboarding_window?(user) do

lib/vutuv_web/templates/user/edit.html.heex

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,15 @@ lands here). --%>
241241
trimmed enough — the server still validates the cap (error_tag below).
242242
The number is language-neutral; only the static "characters" word is
243243
translated, and it never pluralizes in the "N/255" form. --%>
244-
<div data-char-counter data-max={Vutuv.Accounts.User.headline_max_length()}>
244+
<%!-- id + scroll-mt: the onboarding checklist's tagline step links to
245+
/settings/profile#tagline, so the member lands on the field instead of
246+
the top of a long settings page (same as #avatar and #cover above). --%>
247+
<div
248+
id="tagline"
249+
class="scroll-mt-24"
250+
data-char-counter
251+
data-max={Vutuv.Accounts.User.headline_max_length()}
252+
>
245253
<%= label f, :headline, gettext("Tagline"), class: "block text-sm font-medium text-slate-900 dark:text-white" %>
246254
<%= textarea f, :headline, rows: 3, class: input_class(), data: [char_count_input: true] %>
247255
<div class="mt-1 flex items-start justify-between gap-3">

lib/vutuv_web/templates/user/show.html.heex

Lines changed: 5 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -377,9 +377,7 @@ the resulting ~200px rail width. --%>
377377
the onboarding window (the first hour after sign-up) while anything is still
378378
undone, and gone once every step is done or the owner closes it with the ×
379379
(which persists, so it stays gone); @show_completion? carries that gate (see
380-
UserProfileLive). Each todo links straight to where it is done. The tag step
381-
is already checked for anyone registered since the three-tag sign-up minimum,
382-
so the list opens with visible progress. --%>
380+
UserProfileLive). Each todo links straight to where it is done. --%>
383381
<section
384382
:if={@show_completion?}
385383
id="profile-completion"
@@ -416,10 +414,7 @@ the resulting ~200px rail width. --%>
416414
<div class="h-full rounded-full bg-brand-600 transition-all" style={"width: #{round(done_count / total * 100)}%"}></div>
417415
</div>
418416
<ul class="mt-4 space-y-2">
419-
<%!-- items-start (not center): a step may carry a one-line hint under
420-
its label, and the glyph should stay on the label line. text-sm lines
421-
and the h-5 glyphs are both 20px, so the first lines still align. --%>
422-
<li :for={step <- @completion_steps} class="flex items-start gap-3">
417+
<li :for={step <- @completion_steps} class="flex items-center gap-3">
423418
<%= if step.done do %>
424419
<span class="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-brand-600 text-white" aria-hidden="true">
425420
<svg class="h-3 w-3" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24">
@@ -429,43 +424,12 @@ the resulting ~200px rail width. --%>
429424
<span class="text-sm text-brand-700/50 line-through dark:text-brand-200/40">{step.label}</span>
430425
<% else %>
431426
<span class="h-5 w-5 shrink-0 rounded-full border-2 border-brand-300 dark:border-brand-700" aria-hidden="true"></span>
432-
<span class="min-w-0">
433-
<.link href={step.href} class="text-sm font-medium text-brand-800 hover:underline dark:text-brand-100">
434-
{step.label}
435-
</.link>
436-
<%!-- The optional per-step nudge (the first-post topic borrowed
437-
from the member's own tags) — quiet, muted, not a second link. --%>
438-
<span :if={step[:hint]} class="block text-xs text-brand-700/70 dark:text-brand-200/60">
439-
{step.hint}
440-
</span>
441-
</span>
427+
<.link href={step.href} class="text-sm font-medium text-brand-800 hover:underline dark:text-brand-100">
428+
{step.label}
429+
</.link>
442430
<% end %>
443431
</li>
444432
</ul>
445-
446-
<%!-- A quiet link into the LinkedIn importer: the fastest way to fill
447-
several of the steps above at once. Owner-only (it lives inside this
448-
owner-only card) and part of the same onboarding push. --%>
449-
<div class="mt-4 space-y-1 border-t border-brand-100 pt-4 dark:border-brand-900/50">
450-
<.link
451-
navigate={~p"/settings/import/linkedin"}
452-
class="inline-flex items-center gap-1 text-sm font-semibold text-brand-800 hover:underline dark:text-brand-100"
453-
>
454-
{gettext("Import from LinkedIn")}
455-
<span aria-hidden="true"></span>
456-
</.link>
457-
<%!-- The other thing that same archive is good for (issue #1476), and
458-
the fastest way through the "follow other members" step above: the
459-
people you already know are better company than any suggestion we can
460-
make on a member's first minute here. --%>
461-
<.link
462-
navigate={~p"/settings/import/linkedin/connections"}
463-
class="flex items-center gap-1 text-sm font-semibold text-brand-800 hover:underline dark:text-brand-100"
464-
>
465-
{gettext("Find people you know from LinkedIn")}
466-
<span aria-hidden="true"></span>
467-
</.link>
468-
</div>
469433
</section>
470434

471435
<%!-- Posts (latest visible to this viewer; the owner writes them right here,

lib/vutuv_web/views/user_html.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,10 +226,13 @@ defmodule VutuvWeb.UserHTML do
226226

227227
def who_to_follow_card(assigns) do
228228
~H"""
229+
<%!-- The card's scroll-mt-24 went with the checklist's follow step: it
230+
existed so that step's #profile-who-to-follow jump did not land under the
231+
top bar, and nothing links to the anchor any more. --%>
229232
<.card
230233
:if={@recommended_users != []}
231234
id="profile-who-to-follow"
232-
class={if(@promoted, do: "scroll-mt-24", else: "scroll-mt-24 order-1 md:order-none")}
235+
class={if !@promoted, do: "order-1 md:order-none"}
233236
data-promoted={@promoted}
234237
>
235238
<.section_title class="mb-4">{gettext("Who to follow")}</.section_title>

0 commit comments

Comments
 (0)