From 402eebff63e1a05301958f038d6185a4612ddbf0 Mon Sep 17 00:00:00 2001 From: Chris Alfano Date: Fri, 26 Jun 2026 22:27:08 -0400 Subject: [PATCH] feat(web): add self "Manage account" link to PersonDetail (#113) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit person-detail.md's sidebar declares a self-only "Manage account" link to /account; the SPA didn't render it. Add it to the sidebar, gated on the viewer being the profile owner (useAuth slug match). Tests cover self (link present, href=/account) and anonymous (absent). Note: the other two items in this #113 batch were already implemented since the 2026-05-30 audit — PeopleIndex's sort dropdown (PeopleIndex.tsx) and HelpWantedIndex's "Post a role" button — so they need no change. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/web/src/screens/PersonDetail.tsx | 10 ++++++ apps/web/tests/PersonDetail.test.tsx | 50 +++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/apps/web/src/screens/PersonDetail.tsx b/apps/web/src/screens/PersonDetail.tsx index 484808b..8db13c4 100644 --- a/apps/web/src/screens/PersonDetail.tsx +++ b/apps/web/src/screens/PersonDetail.tsx @@ -5,12 +5,14 @@ import { MarkdownView } from '@/components/MarkdownView'; import { StageBadge } from '@/components/StageBadge'; import { TagChip } from '@/components/TagChip'; import { PersonAvatar } from '@/components/PersonAvatar'; +import { useAuth } from '@/hooks/useAuth'; import { api, ApiError } from '@/lib/api'; import { formatMonthYear, formatRelativeTime } from '@/lib/time'; export function PersonDetail() { const params = useParams(); const slug = params['slug']!; + const { person: viewer } = useAuth(); const personQ = useQuery({ queryKey: ['person', slug], @@ -37,6 +39,7 @@ export function PersonDetail() { } const person = personQ.data!.data; + const isSelf = viewer !== null && viewer.slug === person.slug; const allTags = [...person.tags.tech, ...person.tags.topic]; // Memberships sorted: maintainer desc, joinedAt desc @@ -193,6 +196,13 @@ export function PersonDetail() {

{formatMonthYear(person.createdAt)}

+ {isSelf && ( +
+ + Manage account + +
+ )} ); diff --git a/apps/web/tests/PersonDetail.test.tsx b/apps/web/tests/PersonDetail.test.tsx index 18f69ea..e5d2642 100644 --- a/apps/web/tests/PersonDetail.test.tsx +++ b/apps/web/tests/PersonDetail.test.tsx @@ -99,4 +99,54 @@ describe('PersonDetail Contact sidebar', () => { expect(mailto).toHaveAttribute('href', 'mailto:jane@example.com'); }); }); + + // #113 — "Manage account" link, self only + it('shows a "Manage account" link to /account when viewing your own profile', async () => { + vi.spyOn(globalThis, 'fetch').mockImplementation(((input: string) => { + if (input.startsWith('/api/auth/me')) { + return Promise.resolve( + new Response( + JSON.stringify( + mockOk({ + person: { id: BASE_PERSON.id, slug: 'jane-doe', fullName: 'Jane Doe', accountLevel: 'user', avatarUrl: null }, + accountLevel: 'user', + }), + ), + { status: 200, headers: { 'content-type': 'application/json' } }, + ), + ); + } + if (input.startsWith('/api/people/jane-doe')) { + return Promise.resolve(new Response(JSON.stringify(mockOk(BASE_PERSON)), { status: 200, headers: { 'content-type': 'application/json' } })); + } + return Promise.resolve(new Response(null, { status: 404 })); + }) as typeof fetch); + renderScreen( + + + } /> + + , + { initialEntries: ['/members/jane-doe'] }, + ); + await waitFor(() => { + expect(screen.getByRole('link', { name: /manage account/i })).toHaveAttribute('href', '/account'); + }); + }); + + it('does not show "Manage account" for anonymous viewers', async () => { + vi.spyOn(globalThis, 'fetch').mockImplementation(makeFetchMock(BASE_PERSON)); + renderScreen( + + + } /> + + , + { initialEntries: ['/members/jane-doe'] }, + ); + await waitFor(() => { + expect(screen.getByRole('heading', { name: 'Jane Doe', level: 1 })).toBeInTheDocument(); + }); + expect(screen.queryByRole('link', { name: /manage account/i })).not.toBeInTheDocument(); + }); });