feat(web): add Profile page to user menu

Surfaces session info from useAuthMe (name, avatar, username, email,
IdP issuer/subject, profile URL) on a new /profile route, structured as
Basics + Identity panels matching the Settings page layout. Adds a
Profile link to the desktop dropdown and mobile menu.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-05-08 08:33:11 -07:00
parent da805fae0e
commit 025f41dddd
No known key found for this signature in database
3 changed files with 94 additions and 4 deletions

View file

@ -151,6 +151,14 @@ export default function AppShell() {
transition
className="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-panel py-1 outline-1 -outline-offset-1 outline-line-strong transition data-closed:scale-95 data-closed:transform data-closed:opacity-0 data-enter:duration-100 data-enter:ease-out data-leave:duration-75 data-leave:ease-in"
>
<MenuItem>
<Link
to="/profile"
className="block w-full px-4 py-2 text-left text-sm text-fg-3 data-focus:bg-overlay data-focus:outline-hidden"
>
Profile
</Link>
</MenuItem>
<MenuItem>
<form method="POST" action="/auth/logout">
<button
@ -237,8 +245,15 @@ export default function AppShell() {
</button>
</div>
</div>
{provider !== "tailscale" && (
<div className="mt-3 space-y-1 px-2">
<div className="mt-3 space-y-1 px-2">
<DisclosureButton
as={Link}
to="/profile"
className="block w-full rounded-md px-3 py-2 text-left text-base font-medium text-fg-muted hover:bg-overlay hover:text-fg"
>
Profile
</DisclosureButton>
{provider !== "tailscale" && (
<form method="POST" action="/auth/logout">
<DisclosureButton
as="button"
@ -248,8 +263,8 @@ export default function AppShell() {
Sign out
</DisclosureButton>
</form>
</div>
)}
)}
</div>
</div>
</DisclosurePanel>
</Disclosure>

View file

@ -26,6 +26,7 @@ import * as Insights from "./routes/insights";
import * as InsightsEditor from "./routes/insights-editor";
import * as InsightsNew from "./routes/insights-new";
import * as Settings from "./routes/settings";
import * as Profile from "./routes/profile";
import AppShellModule from "./layouts/app-shell";
type RouteModule = {
@ -109,6 +110,7 @@ export const routes: RouteObject[] = [
],
}),
route("settings", Settings),
route("profile", Profile),
],
},
],

View file

@ -0,0 +1,73 @@
import type { AuthSessionUser } from "@qltysh/fabro-api-client";
import { useAuthMe } from "../lib/queries";
import {
Mono,
Muted,
Panel,
PanelSkeleton,
Row,
UrlValue,
} from "../components/settings-panel";
export function meta({}: any) {
return [{ title: "Profile — Fabro" }];
}
export default function Profile() {
const { data: auth } = useAuthMe();
if (!auth) {
return (
<div className="space-y-6">
<PanelSkeleton />
<PanelSkeleton />
</div>
);
}
const { user } = auth;
return (
<div className="space-y-6">
<BasicsPanel user={user} />
<IdentityPanel user={user} />
</div>
);
}
function BasicsPanel({ user }: { user: AuthSessionUser }) {
return (
<Panel title="Basics">
<Row title="Name">
<div className="flex items-center gap-3">
<img
alt=""
src={user.avatarUrl}
className="size-10 rounded-full outline -outline-offset-1 outline-line-strong"
/>
<span>{user.name}</span>
</div>
</Row>
<Row title="Username">
<Mono>{user.login}</Mono>
</Row>
<Row title="Email">{user.email}</Row>
</Panel>
);
}
function IdentityPanel({ user }: { user: AuthSessionUser }) {
return (
<Panel title="Identity">
<Row title="IdP issuer">
{user.idpIssuer ? <Mono>{user.idpIssuer}</Mono> : <Muted></Muted>}
</Row>
<Row title="IdP subject">
{user.idpSubject ? <Mono>{user.idpSubject}</Mono> : <Muted></Muted>}
</Row>
<Row title="Profile URL">
<UrlValue url={user.userUrl} />
</Row>
</Panel>
);
}