From 2708e065febbdf9e3225e7fca4a9f2122e83afe7 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sun, 22 Feb 2026 22:48:48 -0600 Subject: [PATCH] feat(web): add profile page with user info and preferences Refs #468 --- .../src/app/(authenticated)/profile/page.tsx | 467 ++++++++++++++++++ 1 file changed, 467 insertions(+) create mode 100644 apps/web/src/app/(authenticated)/profile/page.tsx diff --git a/apps/web/src/app/(authenticated)/profile/page.tsx b/apps/web/src/app/(authenticated)/profile/page.tsx new file mode 100644 index 0000000..52bef2c --- /dev/null +++ b/apps/web/src/app/(authenticated)/profile/page.tsx @@ -0,0 +1,467 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import type { ReactElement } from "react"; +import Link from "next/link"; +import { useAuth } from "@/lib/auth/auth-context"; +import { apiGet } from "@/lib/api/client"; + +// ─── Types ──────────────────────────────────────────────────────────── + +interface UserPreferences { + id: string; + userId: string; + theme: string; + locale: string; + timezone: string | null; + settings: Record; + updatedAt: string; +} + +// ─── Sub-components ─────────────────────────────────────────────────── + +interface PreferenceRowProps { + label: string; + value: string; +} + +function PreferenceRow({ label, value }: PreferenceRowProps): ReactElement { + return ( +
+ {label} + + {value} + +
+ ); +} + +function PreferencesSkeleton(): ReactElement { + return ( +
+ {Array.from({ length: 3 }).map((_, i) => ( +
+
+
+
+ ))} +
+ ); +} + +// ─── Main Page Component ────────────────────────────────────────────── + +export default function ProfilePage(): ReactElement { + const { user, signOut } = useAuth(); + const [preferences, setPreferences] = useState(null); + const [prefsLoading, setPrefsLoading] = useState(true); + const [prefsError, setPrefsError] = useState(null); + const [signOutHovered, setSignOutHovered] = useState(false); + const [settingsHovered, setSettingsHovered] = useState(false); + + const loadPreferences = useCallback(async (): Promise => { + setPrefsLoading(true); + setPrefsError(null); + + try { + const data = await apiGet("/users/me/preferences"); + setPreferences(data); + } catch (err: unknown) { + const message = err instanceof Error ? err.message : "Could not load preferences"; + setPrefsError(message); + } finally { + setPrefsLoading(false); + } + }, []); + + useEffect(() => { + void loadPreferences(); + }, [loadPreferences]); + + // User initials for avatar fallback + const initials = user?.name + ? user.name + .split(" ") + .slice(0, 2) + .map((part) => part[0]) + .join("") + .toUpperCase() + : user?.email + ? (user.email[0] ?? "?").toUpperCase() + : "?"; + + return ( +
+ {/* ── Page Header ── */} +
+

+ Profile +

+

+ Your account information and preferences +

+
+ + {/* ── User Info Card ── */} +
+
+ {/* Avatar (64px) */} +
+ {user?.image ? ( + {user.name + ) : ( + + {initials} + + )} +
+ + {/* Name, email, role, status */} +
+
+

+ {user?.name ?? "User"} +

+ + {/* Online indicator */} +
+ +
+ + {user?.email && ( +

+ {user.email} +

+ )} + + {user?.workspaceRole && ( + + {user.workspaceRole} + + )} +
+
+
+ + {/* ── Preferences Section ── */} +
+

+ Preferences +

+ + {prefsLoading ? ( + + ) : prefsError ? ( +
+ Preferences unavailable + — {prefsError} +
+ ) : preferences ? ( +
+ + + + {Object.keys(preferences.settings).length > 0 && ( + <> +
+ Custom Settings +
+ {Object.entries(preferences.settings).map(([key, value]) => ( + + ))} + + )} +
+ ) : ( +

+ No preferences configured yet. +

+ )} +
+ + {/* ── Account Actions ── */} +
+

+ Account +

+ +
+ {/* Settings link */} + { + setSettingsHovered(true); + }} + onMouseLeave={() => { + setSettingsHovered(false); + }} + > + + Settings + + + {/* Sign Out button */} + +
+
+
+ ); +}