From 3acfa9eb93e01e6d0ea25712595e6d937c28d3f8 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sun, 10 May 2026 12:40:50 -0400 Subject: [PATCH] feat(web): add profile sub-navigation with Sessions Wraps /profile in a left-sidebar layout. Existing profile page becomes the index; adds a placeholder /profile/sessions. Renames the identity rows from "IdP issuer"/"IdP subject" to "Issuer"/"Subject". Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/fabro-web/app/router.tsx | 9 +- .../fabro-web/app/routes/profile-overview.tsx | 69 ++++++++++ .../fabro-web/app/routes/profile-sessions.tsx | 3 + apps/fabro-web/app/routes/profile.tsx | 126 +++++++++--------- 4 files changed, 145 insertions(+), 62 deletions(-) create mode 100644 apps/fabro-web/app/routes/profile-overview.tsx create mode 100644 apps/fabro-web/app/routes/profile-sessions.tsx diff --git a/apps/fabro-web/app/router.tsx b/apps/fabro-web/app/router.tsx index e9382e032..e29d7b432 100644 --- a/apps/fabro-web/app/router.tsx +++ b/apps/fabro-web/app/router.tsx @@ -31,6 +31,8 @@ import * as Settings from "./routes/settings"; import * as SettingsOverview from "./routes/settings-overview"; import * as SettingsLiveEvents from "./routes/settings-live-events"; import * as Profile from "./routes/profile"; +import * as ProfileOverview from "./routes/profile-overview"; +import * as ProfileSessions from "./routes/profile-sessions"; import AppShellModule from "./layouts/app-shell"; type RouteModule = { @@ -121,7 +123,12 @@ export const routes: RouteObject[] = [ route("live-events", SettingsLiveEvents), ], }), - route("profile", Profile), + route("profile", Profile, { + children: [ + indexRoute(ProfileOverview), + route("sessions", ProfileSessions), + ], + }), ], }, ], diff --git a/apps/fabro-web/app/routes/profile-overview.tsx b/apps/fabro-web/app/routes/profile-overview.tsx new file mode 100644 index 000000000..9b6a27ff8 --- /dev/null +++ b/apps/fabro-web/app/routes/profile-overview.tsx @@ -0,0 +1,69 @@ +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 default function ProfileOverview() { + const { data: auth } = useAuthMe(); + + if (!auth) { + return ( +
+ + +
+ ); + } + + const { user } = auth; + + return ( +
+ + +
+ ); +} + +function BasicsPanel({ user }: { user: AuthSessionUser }) { + return ( + + +
+ + {user.name} +
+
+ + {user.login} + + {user.email} +
+ ); +} + +function IdentityPanel({ user }: { user: AuthSessionUser }) { + return ( + + + {user.idpIssuer ? {user.idpIssuer} : —} + + + {user.idpSubject ? {user.idpSubject} : —} + + + + + + ); +} diff --git a/apps/fabro-web/app/routes/profile-sessions.tsx b/apps/fabro-web/app/routes/profile-sessions.tsx new file mode 100644 index 000000000..9227b5c5b --- /dev/null +++ b/apps/fabro-web/app/routes/profile-sessions.tsx @@ -0,0 +1,3 @@ +export default function ProfileSessions() { + return
; +} diff --git a/apps/fabro-web/app/routes/profile.tsx b/apps/fabro-web/app/routes/profile.tsx index e9bbe2f78..27ca149ba 100644 --- a/apps/fabro-web/app/routes/profile.tsx +++ b/apps/fabro-web/app/routes/profile.tsx @@ -1,73 +1,77 @@ -import type { AuthSessionUser } from "@qltysh/fabro-api-client"; -import { useAuthMe } from "../lib/queries"; -import { - Mono, - Muted, - Panel, - PanelSkeleton, - Row, - UrlValue, -} from "../components/settings-panel"; +import { ComputerDesktopIcon, UserCircleIcon } from "@heroicons/react/24/outline"; +import { Link, Outlet, useLocation } from "react-router"; export function meta({}: any) { return [{ title: "Profile — Fabro" }]; } -export default function Profile() { - const { data: auth } = useAuthMe(); +export const handle = { hideHeader: true }; - if (!auth) { - return ( -
- - -
- ); - } +type NavItem = { + name: string; + href: string; + icon: typeof UserCircleIcon; + match: (pathname: string) => boolean; +}; - const { user } = auth; +const navItems: NavItem[] = [ + { + name: "Profile", + href: "/profile", + icon: UserCircleIcon, + match: (p) => p === "/profile", + }, + { + name: "Sessions", + href: "/profile/sessions", + icon: ComputerDesktopIcon, + match: (p) => p.startsWith("/profile/sessions"), + }, +]; + +function classNames(...classes: Array) { + return classes.filter(Boolean).join(" "); +} + +export default function ProfileLayout() { + const { pathname } = useLocation(); + const currentName = navItems.find((item) => item.match(pathname))?.name ?? "Profile"; return ( -
- - +
+ + +
+

+ {currentName} +

+ +
); } - -function BasicsPanel({ user }: { user: AuthSessionUser }) { - return ( - - -
- - {user.name} -
-
- - {user.login} - - {user.email} -
- ); -} - -function IdentityPanel({ user }: { user: AuthSessionUser }) { - return ( - - - {user.idpIssuer ? {user.idpIssuer} : —} - - - {user.idpSubject ? {user.idpSubject} : —} - - - - - - ); -}