-
- {comment.user}
-
+ {comment.userAccountId ? (
+
+ {comment.user}
+
+ ) : (
+ {comment.user}
+ )}
{comment.time}
diff --git a/web/frontend/app/features/breweries/utils/filler-brewery-detail.ts b/web/frontend/app/features/breweries/utils/filler-brewery-detail.ts
index 690aa184..d9298434 100644
--- a/web/frontend/app/features/breweries/utils/filler-brewery-detail.ts
+++ b/web/frontend/app/features/breweries/utils/filler-brewery-detail.ts
@@ -88,7 +88,7 @@ export const FILLER_BEERS: FillerBeer[] = [
export interface FillerComment {
id: string;
user: string;
- userAccountId: string;
+ userAccountId?: string;
initials: string;
rating: number;
time: string;
@@ -97,11 +97,12 @@ export interface FillerComment {
liked: boolean;
}
+// `userAccountId` is left unset below since these commenters aren't backed by real accounts
+// yet; CommentItem only links a commenter's name once a real account GUID is available.
export const FILLER_COMMENTS: FillerComment[] = [
{
id: '1',
user: 'malt_kettle',
- userAccountId: 'malt_kettle',
initials: 'MK',
rating: 5,
time: '2 days ago',
@@ -112,7 +113,6 @@ export const FILLER_COMMENTS: FillerComment[] = [
{
id: '2',
user: 'stoutfan_88',
- userAccountId: 'stoutfan_88',
initials: 'SB',
rating: 4,
time: '1 week ago',
@@ -123,7 +123,6 @@ export const FILLER_COMMENTS: FillerComment[] = [
{
id: '3',
user: 'ale_annie',
- userAccountId: 'ale_annie',
initials: 'AL',
rating: 4,
time: '3 weeks ago',
diff --git a/web/frontend/app/features/users/USER_PROFILE_HANDOFF.md b/web/frontend/app/features/users/USER_PROFILE_HANDOFF.md
index 2f94f86d..31cc65e0 100644
--- a/web/frontend/app/features/users/USER_PROFILE_HANDOFF.md
+++ b/web/frontend/app/features/users/USER_PROFILE_HANDOFF.md
@@ -13,7 +13,7 @@ Covers `/users/:id` (`routes/user-profile.tsx`). Mirrors the format of
| Field | Status | Source | Gap |
| ---------------------------------------- | ------ | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| Username, name, joined date | ✅ | `GET /api/user/{id}` (`UserAccount`) | |
+| Username, name, joined date | ✅ | `GET /api/user/{id}/profile` (`PublicUserProfileDto`) | |
| `isOwnProfile` | ✅ | Compares the session's `userAccountId` to the route param | |
| Bio | ⚠️ | `FILLER_PROFILE_META.bio` | `Social.UserProfile.Biography` exists (`IUserProfileRepository`, `UpdateBiographyHandler`) but there's no query/controller route to read another user's profile — only a write path for the authenticated user's own. |
| Location | ❌ | `FILLER_PROFILE_META.location` | No location field on `UserAccount` or `UserProfile`. |
diff --git a/web/frontend/app/features/users/components/profile/ProfileTabs.stories.tsx b/web/frontend/app/features/users/components/profile/ProfileTabs.stories.tsx
index 9ff96ec7..80c889ee 100644
--- a/web/frontend/app/features/users/components/profile/ProfileTabs.stories.tsx
+++ b/web/frontend/app/features/users/components/profile/ProfileTabs.stories.tsx
@@ -22,13 +22,19 @@ const meta = {
export default meta;
type Story = StoryObj
;
+const panels = {
+ activity: Activity panel
,
+ following: Following panel
,
+ liked: Liked panel
,
+};
+
const ControlledTabs = () => {
const [activeTab, setActiveTab] = useState('activity');
- return ;
+ return ;
};
export const Default: Story = {
- args: { activeTab: 'activity', onChange: fn() },
+ args: { activeTab: 'activity', onChange: fn(), panels },
render: () => ,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
@@ -36,5 +42,6 @@ export const Default: Story = {
await expect(followingTab).toHaveAttribute('aria-selected', 'false');
await userEvent.click(followingTab);
await expect(followingTab).toHaveAttribute('aria-selected', 'true');
+ await expect(canvas.getByText('Following panel')).toBeInTheDocument();
},
};
diff --git a/web/frontend/app/features/users/components/profile/ProfileTabs.tsx b/web/frontend/app/features/users/components/profile/ProfileTabs.tsx
index 174b7690..1713f021 100644
--- a/web/frontend/app/features/users/components/profile/ProfileTabs.tsx
+++ b/web/frontend/app/features/users/components/profile/ProfileTabs.tsx
@@ -1,4 +1,5 @@
-import { Tab, TabGroup, TabList } from '@headlessui/react';
+import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react';
+import type { ReactNode } from 'react';
export type ProfileTab = 'activity' | 'following' | 'liked';
@@ -11,9 +12,10 @@ const TABS: { id: ProfileTab; label: string }[] = [
interface ProfileTabsProps {
activeTab: ProfileTab;
onChange: (tab: ProfileTab) => void;
+ panels: Record;
}
-const ProfileTabs = ({ activeTab, onChange }: ProfileTabsProps) => {
+const ProfileTabs = ({ activeTab, onChange, panels }: ProfileTabsProps) => {
const selectedIndex = TABS.findIndex((tab) => tab.id === activeTab);
return (
@@ -28,6 +30,11 @@ const ProfileTabs = ({ activeTab, onChange }: ProfileTabsProps) => {
))}
+
+ {TABS.map((tab) => (
+ {panels[tab.id]}
+ ))}
+
);
};
diff --git a/web/frontend/app/features/users/routes/user-profile.tsx b/web/frontend/app/features/users/routes/user-profile.tsx
index 6948a62a..f2b7877f 100644
--- a/web/frontend/app/features/users/routes/user-profile.tsx
+++ b/web/frontend/app/features/users/routes/user-profile.tsx
@@ -2,7 +2,7 @@ import { useState } from 'react';
import { data } from 'react-router';
import RouteErrorState from '../../../components/ui/error/RouteErrorState';
import { getOptionalAuth } from '../../auth/auth.server';
-import { getPublicUserProfile } from '../users.server';
+import { getPublicUserProfile, type PublicUserProfile } from '../users.server';
import ProfileSidebarCard from '../components/profile/ProfileSidebarCard';
import ProfileTabs, { type ProfileTab } from '../components/profile/ProfileTabs';
import ActivityTab from '../components/profile/tabs/ActivityTab';
@@ -37,9 +37,17 @@ export const loader = async ({ request, params }: Route.LoaderArgs) => {
const joinedLabel = (isoDate: string) =>
`Joined ${new Intl.DateTimeFormat('en-US', { month: 'long', year: 'numeric' }).format(new Date(isoDate))}`;
-const UserProfile = ({ loaderData }: Route.ComponentProps) => {
- const { profile, isOwnProfile } = loaderData;
+interface ProfileContentProps {
+ profile: PublicUserProfile;
+ isOwnProfile: boolean;
+}
+/**
+ * Owns the profile-scoped interactive state. Keyed by `profile.userAccountId` in the
+ * parent so React remounts it (resetting follow/tab state) when the route's `:id` changes,
+ * since React Router reuses the route component itself across param-only navigations.
+ */
+const ProfileContent = ({ profile, isOwnProfile }: ProfileContentProps) => {
const [activeTab, setActiveTab] = useState('activity');
const [isFollowingProfile, setIsFollowingProfile] = useState(false);
const [following, setFollowing] = useState(() =>
@@ -91,17 +99,21 @@ const UserProfile = ({ loaderData }: Route.ComponentProps) => {
/>
-
-
-
- {activeTab === 'activity' &&
}
- {activeTab === 'following' && (
-
- )}
- {activeTab === 'liked' &&
}
+
+
,
+ following: (
+
+ ),
+ liked:
,
+ }}
+ />
@@ -109,6 +121,18 @@ const UserProfile = ({ loaderData }: Route.ComponentProps) => {
);
};
+const UserProfile = ({ loaderData }: Route.ComponentProps) => {
+ const { profile, isOwnProfile } = loaderData;
+
+ return (
+