From 34301d7b70b3236b66abee34ac7039794b048b4d Mon Sep 17 00:00:00 2001 From: michaelcanova Date: Tue, 8 Sep 2026 18:03:24 -0400 Subject: [PATCH 1/4] [Author] Updating Author Profile Overview --- app/author/[id]/page.tsx | 119 +++++++++----------- components/Activity/ActivityPageContent.tsx | 17 +-- components/Activity/ActivityRow.tsx | 23 ++++ components/Activity/index.ts | 1 + components/profile/ProfileActivityTab.tsx | 3 +- hooks/useActivityFeed.ts | 41 ++++--- services/activity.service.ts | 29 +++-- services/funder.service.ts | 4 +- 8 files changed, 130 insertions(+), 107 deletions(-) create mode 100644 components/Activity/ActivityRow.tsx diff --git a/app/author/[id]/page.tsx b/app/author/[id]/page.tsx index 139a6f1b8..e1b63b9ca 100644 --- a/app/author/[id]/page.tsx +++ b/app/author/[id]/page.tsx @@ -6,12 +6,11 @@ import { useUser } from '@/contexts/UserContext'; import { useRouter, useSearchParams } from 'next/navigation'; import { Shield } from 'lucide-react'; import { Tabs } from '@/components/ui/Tabs'; -import { useContributions } from '@/hooks/useContributions'; -import { ContributionType } from '@/services/contribution.service'; -import { transformContributionToFeedEntry } from '@/types/contribution'; -import { FeedEntry } from '@/types/feed'; -import { FeedContent } from '@/components/Feed/FeedContent'; -import { SearchEmpty } from '@/components/ui/SearchEmpty'; +import { ActivityFeedList, ActivityRow } from '@/components/Activity'; +import { groupActivityRows } from '@/components/Activity/lib/activityGrouping.utils'; +import { useActivityFeed } from '@/hooks/useActivityFeed'; +import { useFeedScrollTracking } from '@/hooks/useFeedScrollTracking'; +import { ActivityCommentType } from '@/services/activity.service'; import { ModerationTab } from '@/components/profile/ModerationTab'; import { ModerationPreview } from '@/components/profile/ModerationPreview'; import { ProfileStatsCards } from '@/components/profile/ProfileStatsCards'; @@ -48,11 +47,19 @@ function AuthorProfileError({ error }: { error: string }) { ); } -const TAB_TO_CONTRIBUTION_TYPE: Record = { - contributions: 'ALL', - 'peer-reviews': 'REVIEW', - comments: 'CONVERSATION', - bounties: 'BOUNTY', +interface ActivityFilters { + contentType: string; + commentTypes: readonly ActivityCommentType[]; +} + +/** + * Narrows the author's activity to a single pill. Defined once at module scope so + * the filters keep a stable identity across renders and never restart the feed. + * The Overview tab has no entry: it shows everything the author did. + */ +const TAB_TO_ACTIVITY_FILTERS: Record = { + 'peer-reviews': { contentType: 'RHCOMMENTMODEL', commentTypes: ['REVIEW', 'PEER_REVIEW'] }, + comments: { contentType: 'RHCOMMENTMODEL', commentTypes: ['GENERIC_COMMENT', 'ANSWER'] }, }; type TabGroupId = 'overview' | 'funding' | 'activity' | 'moderation'; @@ -93,45 +100,33 @@ function AuthorTabContent({ currentTab: string; isPending: boolean; }) { - const contributionType = TAB_TO_CONTRIBUTION_TYPE[currentTab] || 'ALL'; - + const filters = TAB_TO_ACTIVITY_FILTERS[currentTab]; const { - contributions: allContributions, - isLoading: isContributionsLoading, - error: contributionsError, - hasMore: hasMoreContributions, - loadMore: loadMoreContributions, - isLoadingMore: isLoadingMoreContributions, - restoredFeedEntries: restoredContributionsEntries, - restoredScrollPosition: restoredContributionsScrollPosition, - lastClickedEntryId: lastClickedContributionsEntryId, - } = useContributions({ - contribution_type: contributionType, - author_id: authorId, - activeTab: currentTab, + entries, + isLoading, + isLoadingMore, + hasMore, + page, + loadMore, + feedKey, + restoredScrollPosition, + lastClickedEntryId, + } = useActivityFeed({ + authorId, + contentType: filters?.contentType, + commentTypes: filters?.commentTypes, + }); + + useFeedScrollTracking({ + feedKey, + entries, + hasMore, + page, + restoredScrollPosition, + lastClickedEntryId: lastClickedEntryId ?? undefined, }); - const contributions = - currentTab === 'comments' - ? allContributions.filter((contribution) => !contribution.item?.review?.score) - : allContributions; - - if (contributionsError) { - return
Error: {contributionsError.message}
; - } - - const entries = - restoredContributionsEntries || - contributions - .map((contribution) => { - try { - return transformContributionToFeedEntry({ contribution, contributionType }); - } catch (error) { - console.error('[Contribution] Could not transform contribution', error); - return null; - } - }) - .filter((entry): entry is FeedEntry => !!entry); + const rows = isPending ? [] : groupActivityRows(entries); return (
@@ -140,25 +135,17 @@ function AuthorTabContent({
)} - - } - maxLength={150} - showReadMoreCTA={true} - activeTab={currentTab} - restoredScrollPosition={restoredContributionsScrollPosition} - lastClickedEntryId={lastClickedContributionsEntryId ?? undefined} - shouldRenderBountyAsComment={true} - wideContent - /> + + {rows.map((row) => ( + + ))} + ); } diff --git a/components/Activity/ActivityPageContent.tsx b/components/Activity/ActivityPageContent.tsx index 2fb2cf286..c5fb9ec72 100644 --- a/components/Activity/ActivityPageContent.tsx +++ b/components/Activity/ActivityPageContent.tsx @@ -1,10 +1,8 @@ 'use client'; import { useEffect, useMemo } from 'react'; -import { ActivityCard } from './cards/ActivityCard'; -import { ActivityCommentGroupCard } from './cards/ActivityCommentGroupCard'; -import { ActivityFundingGroupCard } from './cards/ActivityFundingGroupCard'; import { ActivityFeedList } from './ActivityFeedList'; +import { ActivityRow } from './ActivityRow'; import { groupActivityRows } from './lib/activityGrouping.utils'; import { useActivityFeeds } from '@/contexts/ActivityFeedContext'; import { useScrollContainer } from '@/contexts/ScrollContainerContext'; @@ -58,16 +56,9 @@ export function ActivityPageContent() { loadMore={loadMore} isEmpty={entries.length === 0} > - {rows.map((row) => { - switch (row.kind) { - case 'funding-group': - return ; - case 'comment-group': - return ; - default: - return ; - } - })} + {rows.map((row) => ( + + ))} ); } diff --git a/components/Activity/ActivityRow.tsx b/components/Activity/ActivityRow.tsx new file mode 100644 index 000000000..83d4c4446 --- /dev/null +++ b/components/Activity/ActivityRow.tsx @@ -0,0 +1,23 @@ +'use client'; + +import { FC } from 'react'; +import { ActivityCard } from './cards/ActivityCard'; +import { ActivityCommentGroupCard } from './cards/ActivityCommentGroupCard'; +import { ActivityFundingGroupCard } from './cards/ActivityFundingGroupCard'; +import type { ActivityRow as ActivityRowType } from './lib/activityGrouping.utils'; + +interface ActivityRowProps { + row: ActivityRowType; +} + +/** Renders whichever card `groupActivityRows` decided a row should be. */ +export const ActivityRow: FC = ({ row }) => { + switch (row.kind) { + case 'funding-group': + return ; + case 'comment-group': + return ; + default: + return ; + } +}; diff --git a/components/Activity/index.ts b/components/Activity/index.ts index de22c8786..06b6c38fd 100644 --- a/components/Activity/index.ts +++ b/components/Activity/index.ts @@ -14,6 +14,7 @@ export { WorkPreviewCard } from './work/WorkPreviewCard'; // Feed export { ActivityFeedList } from './ActivityFeedList'; +export { ActivityRow } from './ActivityRow'; export { ActivityPageContent } from './ActivityPageContent'; export { ActivityCacheBypassControl } from './ActivityCacheBypassControl'; diff --git a/components/profile/ProfileActivityTab.tsx b/components/profile/ProfileActivityTab.tsx index e16004485..790c1e686 100644 --- a/components/profile/ProfileActivityTab.tsx +++ b/components/profile/ProfileActivityTab.tsx @@ -8,7 +8,6 @@ export const ACTIVITY_PILLS = [ { id: 'proposals', label: 'Proposals' }, { id: 'peer-reviews', label: 'Peer Reviews' }, { id: 'comments', label: 'Comments' }, - { id: 'bounties', label: 'Bounties' }, ]; export type ActivityPillId = (typeof ACTIVITY_PILLS)[number]['id']; @@ -60,7 +59,7 @@ function ProposalsContent({ userId }: { userId: number }) { /** * Activity tab — thin wrapper rendering the pill bar + whatever feed the * parent decided to render for the active pill. Proposals is rendered - * internally since it uses a different data source than the contributions feed. + * internally since it uses a different data source than the activity feed. */ export function ProfileActivityTab({ activePill, diff --git a/hooks/useActivityFeed.ts b/hooks/useActivityFeed.ts index c325fcb0d..6f19c97af 100644 --- a/hooks/useActivityFeed.ts +++ b/hooks/useActivityFeed.ts @@ -2,12 +2,17 @@ import { useState, useEffect, useCallback, useRef, useMemo } from 'react'; import { FeedEntry } from '@/types/feed'; -import { ActivityService, ActivityScope } from '@/services/activity.service'; +import { ActivityService, ActivityCommentType, ActivityScope } from '@/services/activity.service'; import { useFeedStateRestoration } from '@/hooks/useFeedStateRestoration'; export type ActivityTab = 'all' | 'peer_reviews' | 'financial'; interface UseActivityFeedOptions { + /** Author profile id, to read one author's activity instead of the site-wide feed. */ + authorId?: number; + contentType?: string; + /** Pass a stable reference: a new array on every render restarts the feed. */ + commentTypes?: readonly ActivityCommentType[]; scope?: ActivityScope; grantId?: number | string; disableCache?: boolean; @@ -15,6 +20,9 @@ interface UseActivityFeedOptions { } export function useActivityFeed({ + authorId, + contentType, + commentTypes, scope, grantId, disableCache = false, @@ -48,6 +56,19 @@ export function useActivityFeed({ // changes (scope / grantId) still refetch. const skipNextFetchRef = useRef(hasRestoredEntries); + const fetchPage = useCallback( + (pageNumber: number) => + authorId + ? ActivityService.getAuthorActivity(authorId, { + page: pageNumber, + contentType, + commentTypes, + scope, + }) + : ActivityService.getActivity({ page: pageNumber, scope, grantId, disableCache }), + [authorId, contentType, commentTypes, scope, grantId, disableCache] + ); + const fetchInitial = useCallback(async () => { setEntries([]); setIsLoading(true); @@ -55,12 +76,7 @@ export function useActivityFeed({ setPage(1); try { - const result = await ActivityService.getActivity({ - page: 1, - scope, - grantId, - disableCache, - }); + const result = await fetchPage(1); setEntries(result.entries); setHasMore(result.hasMore); setCount(result.count); @@ -69,7 +85,7 @@ export function useActivityFeed({ } finally { setIsLoading(false); } - }, [scope, grantId, disableCache]); + }, [fetchPage]); useEffect(() => { if (enabled === false) return; @@ -87,12 +103,7 @@ export function useActivityFeed({ const nextPage = pageRef.current + 1; try { - const result = await ActivityService.getActivity({ - page: nextPage, - scope, - grantId, - disableCache, - }); + const result = await fetchPage(nextPage); setEntries((prev) => { const next = [...prev, ...result.entries]; setCount(next.length); @@ -106,7 +117,7 @@ export function useActivityFeed({ } finally { setIsLoadingMore(false); } - }, [isLoading, isLoadingMore, hasMore, scope, grantId, disableCache]); + }, [isLoading, isLoadingMore, hasMore, fetchPage]); return { entries, diff --git a/services/activity.service.ts b/services/activity.service.ts index 8e1d14471..6ab8d23b4 100644 --- a/services/activity.service.ts +++ b/services/activity.service.ts @@ -23,7 +23,7 @@ export interface GetActivityParams { disableCache?: boolean; } -export interface GetUserActivityParams { +export interface GetActorActivityParams { page?: number; pageSize?: number; contentType?: string; @@ -82,20 +82,31 @@ export class ActivityService { return this.fetchActivity(url); } - static async getUserActivity( - userId: number, - params?: GetUserActivityParams + /** Activity of a single actor, identified either by user id or by author id. */ + private static fetchActorActivity( + endpoint: string, + actor: Record, + params?: GetActorActivityParams ): Promise { const pageSize = params?.pageSize ?? this.DEFAULT_PAGE_SIZE; - const queryParams = new URLSearchParams({ - user_id: userId.toString(), - page_size: pageSize.toString(), - }); + const queryParams = new URLSearchParams({ ...actor, page_size: pageSize.toString() }); if (params?.page) queryParams.append('page', params.page.toString()); if (params?.contentType) queryParams.append('content_type', params.contentType); params?.commentTypes?.forEach((commentType) => queryParams.append('comment_type', commentType)); if (params?.scope) queryParams.append('scope', params.scope); - return this.fetchActivity(`${this.BASE_PATH}/user_activity/?${queryParams.toString()}`); + return this.fetchActivity(`${this.BASE_PATH}/${endpoint}/?${queryParams.toString()}`); + } + + static getUserActivity(userId: number, params?: GetActorActivityParams): Promise { + return this.fetchActorActivity('user_activity', { user_id: userId.toString() }, params); + } + + /** `authorId` is an author profile id, not a user id. */ + static getAuthorActivity( + authorId: number, + params?: GetActorActivityParams + ): Promise { + return this.fetchActorActivity('author_activity', { author_id: authorId.toString() }, params); } } diff --git a/services/funder.service.ts b/services/funder.service.ts index 8f24af217..2b26da790 100644 --- a/services/funder.service.ts +++ b/services/funder.service.ts @@ -2,7 +2,7 @@ import { ApiClient } from './client'; import { ActivityService, type ActivityResult, - type GetUserActivityParams, + type GetActorActivityParams, } from './activity.service'; import { FunderOverview, transformFunderOverview } from '@/types/funder'; @@ -20,7 +20,7 @@ export class FunderService { */ static async getActivity( userId: number, - options?: GetUserActivityParams + options?: GetActorActivityParams ): Promise { return ActivityService.getUserActivity(userId, options); } From 9b54181520893aae240aeaff04c6e6f6e14773be Mon Sep 17 00:00:00 2001 From: michaelcanova Date: Wed, 9 Sep 2026 13:10:03 -0400 Subject: [PATCH 2/4] [Author] Removing extra feed pull for overview --- .../[id]/components/PinnedFundraise.tsx | 91 ------------------- app/author/[id]/page.tsx | 46 +++------- .../items/DeprecatedFeedItemFundraise.tsx | 10 +- 3 files changed, 14 insertions(+), 133 deletions(-) delete mode 100644 app/author/[id]/components/PinnedFundraise.tsx diff --git a/app/author/[id]/components/PinnedFundraise.tsx b/app/author/[id]/components/PinnedFundraise.tsx deleted file mode 100644 index 0a60e6425..000000000 --- a/app/author/[id]/components/PinnedFundraise.tsx +++ /dev/null @@ -1,91 +0,0 @@ -'use client'; - -import { FC } from 'react'; -import { useFeed } from '@/hooks/useFeed'; -import { ID } from '@/types/root'; -import { FundraiseSkeleton } from '@/components/Feed/skeletons/FundraiseSkeleton'; -import { FeedItemFundraise } from '@/components/Feed/items/DeprecatedFeedItemFundraise'; -import { useFeedItemAnalyticsTracking } from '@/hooks/useFeedItemAnalyticsTracking'; -import { cn } from '@/utils/styles'; - -export function PinnedFundraiseSkeleton({ className }: { className?: string }) { - return ( -
- -
- ); -} - -interface PinnedFundraiseProps { - userId: ID; - className?: string; - showTitle?: boolean; - showActions?: boolean; - compact?: boolean; -} - -/** - * Helper function to get the most funded pinned fundraise - * Sorting by amount raised (highest first) and returning the top one - */ -const getMostFundedFundraise = (entries: any[]) => { - if (!entries || entries.length === 0) return null; - - // Sort by amount raised (highest first) and return the first one - const sorted = entries.sort((a, b) => { - const aRaised = a.raw?.content_object?.fundraise?.amount_raised?.rsc || 0; - const bRaised = b.raw?.content_object?.fundraise?.amount_raised?.rsc || 0; - return bRaised - aRaised; - }); - - return sorted[0]; -}; - -const PinnedFundraise: FC = ({ - userId, - className, - showTitle = true, - showActions = true, - compact = false, -}) => { - const { entries, isLoading } = useFeed('open', { - endpoint: 'funding_feed', - contentType: 'PREREGISTRATION', - fundraiseStatus: 'OPEN', - createdBy: Number(userId), - }); - - const mostFundedFundraise = getMostFundedFundraise(entries); - - const { handleFeedItemClick } = useFeedItemAnalyticsTracking({ - entry: mostFundedFundraise, - }); - - if (isLoading) { - return ; - } - - if (!entries || entries.length === 0) { - return null; - } - - if (!mostFundedFundraise) { - return null; - } - - return ( -
- {/* Use FeedItemFundraise for rendering with pinned fundraise indicator */} - -
- ); -}; - -export default PinnedFundraise; diff --git a/app/author/[id]/page.tsx b/app/author/[id]/page.tsx index e1b63b9ca..789db1c6b 100644 --- a/app/author/[id]/page.tsx +++ b/app/author/[id]/page.tsx @@ -24,7 +24,6 @@ import { type ActivityPillId, } from '@/components/profile/ProfileActivityTab'; import { OrcidSyncBanner } from '@/components/profile/OrcidSyncBanner'; -import PinnedFundraise from './components/PinnedFundraise'; import { useOrcidCallback } from '@/components/Orcid/lib/hooks/useOrcidCallback'; import { ProfileHeroBanner, @@ -91,12 +90,10 @@ const MODERATION_TAB = { function AuthorTabContent({ authorId, - userId, currentTab, isPending, }: { authorId: number; - userId?: number; currentTab: string; isPending: boolean; }) { @@ -129,24 +126,17 @@ function AuthorTabContent({ const rows = isPending ? [] : groupActivityRows(entries); return ( -
- {currentTab === 'contributions' && userId && ( -
- -
- )} - - {rows.map((row) => ( - - ))} - -
+ + {rows.map((row) => ( + + ))} + ); } @@ -295,24 +285,14 @@ export default function AuthorProfilePage({ params }: { params: Promise<{ id: st : ACTIVITY_PILLS[0].id; return ( - + ); } // Overview (default) return ( - + ); }; diff --git a/components/Feed/items/DeprecatedFeedItemFundraise.tsx b/components/Feed/items/DeprecatedFeedItemFundraise.tsx index 948848449..35af35b2e 100644 --- a/components/Feed/items/DeprecatedFeedItemFundraise.tsx +++ b/components/Feed/items/DeprecatedFeedItemFundraise.tsx @@ -18,7 +18,7 @@ import { AuthorTooltip } from '@/components/ui/AuthorTooltip'; import { FeedItemFundingBadges } from '@/components/Feed/FeedItemFundingBadges'; import { PeerReviewTooltip } from '@/components/tooltips/PeerReviewTooltip'; import { Tooltip } from '@/components/ui/Tooltip'; -import { Pin, ArrowRight, Star } from 'lucide-react'; +import { ArrowRight, Star } from 'lucide-react'; import { useRouter } from 'next/navigation'; import { buildWorkUrl } from '@/utils/url'; import { useCurrencyPreference } from '@/contexts/CurrencyPreferenceContext'; @@ -36,7 +36,6 @@ interface FeedItemFundraiseProps { showHeader?: boolean; maxLength?: number; customActionText?: string; - isPinnedFundraise?: boolean; onFeedItemClick?: () => void; showBountyInfo?: boolean; } @@ -49,7 +48,6 @@ export const FeedItemFundraise: FC = ({ showHeader = true, maxLength, customActionText, - isPinnedFundraise = false, onFeedItemClick, showBountyInfo, }) => { @@ -137,12 +135,6 @@ export const FeedItemFundraise: FC = ({ ) : undefined } > - {isPinnedFundraise && ( -
- -
- )} - {/* Mobile image */} {imageUrl && (
From d6155d9d1289be4b84e353398881f09134198db8 Mon Sep 17 00:00:00 2001 From: michaelcanova Date: Thu, 10 Sep 2026 13:27:22 -0400 Subject: [PATCH 3/4] [Author] Cleanup of functionality no longer used --- app/author/[id]/page.tsx | 145 ++------ app/my-funding/components/FundsReceived.tsx | 49 +-- components/profile/ProfileActivityTab.tsx | 88 ----- components/profile/ProfileFundingTab.tsx | 55 --- hooks/useContributions.ts | 136 -------- hooks/useFeedSource.ts | 1 - services/contribution.service.ts | 112 ------ types/contribution.ts | 280 --------------- types/contributionTransformer.ts | 369 -------------------- 9 files changed, 39 insertions(+), 1196 deletions(-) delete mode 100644 components/profile/ProfileActivityTab.tsx delete mode 100644 components/profile/ProfileFundingTab.tsx delete mode 100644 hooks/useContributions.ts delete mode 100644 services/contribution.service.ts delete mode 100644 types/contribution.ts delete mode 100644 types/contributionTransformer.ts diff --git a/app/author/[id]/page.tsx b/app/author/[id]/page.tsx index 789db1c6b..5900898dc 100644 --- a/app/author/[id]/page.tsx +++ b/app/author/[id]/page.tsx @@ -10,19 +10,11 @@ import { ActivityFeedList, ActivityRow } from '@/components/Activity'; import { groupActivityRows } from '@/components/Activity/lib/activityGrouping.utils'; import { useActivityFeed } from '@/hooks/useActivityFeed'; import { useFeedScrollTracking } from '@/hooks/useFeedScrollTracking'; -import { ActivityCommentType } from '@/services/activity.service'; import { ModerationTab } from '@/components/profile/ModerationTab'; import { ModerationPreview } from '@/components/profile/ModerationPreview'; import { ProfileStatsCards } from '@/components/profile/ProfileStatsCards'; import { ProfileStatsStrip } from '@/components/profile/ProfileStatsStrip'; import ProfileAchievements from '@/components/profile/ProfileAchievements'; -import { ProfileFundingTab, isFundingPill } from '@/components/profile/ProfileFundingTab'; -import { - ProfileActivityTab, - ACTIVITY_PILLS, - isActivityPill, - type ActivityPillId, -} from '@/components/profile/ProfileActivityTab'; import { OrcidSyncBanner } from '@/components/profile/OrcidSyncBanner'; import { useOrcidCallback } from '@/components/Orcid/lib/hooks/useOrcidCallback'; import { @@ -46,40 +38,9 @@ function AuthorProfileError({ error }: { error: string }) { ); } -interface ActivityFilters { - contentType: string; - commentTypes: readonly ActivityCommentType[]; -} - -/** - * Narrows the author's activity to a single pill. Defined once at module scope so - * the filters keep a stable identity across renders and never restart the feed. - * The Overview tab has no entry: it shows everything the author did. - */ -const TAB_TO_ACTIVITY_FILTERS: Record = { - 'peer-reviews': { contentType: 'RHCOMMENTMODEL', commentTypes: ['REVIEW', 'PEER_REVIEW'] }, - comments: { contentType: 'RHCOMMENTMODEL', commentTypes: ['GENERIC_COMMENT', 'ANSWER'] }, -}; - -type TabGroupId = 'overview' | 'funding' | 'activity' | 'moderation'; +type AuthorTab = 'overview' | 'moderation'; -const TOP_LEVEL_TABS: Array<{ id: TabGroupId; label: string }> = [ - { id: 'overview', label: 'Overview' }, - { id: 'funding', label: 'Funding' }, - { id: 'activity', label: 'Activity' }, -]; - -/** - * Resolve a tab id (either a group id or a pill id from within a group) to its - * top-level group. The url stores a single `tab` param which may be either. - */ -function getTabGroup(tabId: string): TabGroupId { - if (tabId === 'overview' || tabId === 'contributions') return 'overview'; - if (tabId === 'funding' || isFundingPill(tabId)) return 'funding'; - if (tabId === 'activity' || isActivityPill(tabId)) return 'activity'; - if (tabId === 'moderation') return 'moderation'; - return 'overview'; -} +const OVERVIEW_TAB = { id: 'overview', label: 'Overview' }; const MODERATION_TAB = { id: 'moderation', @@ -88,16 +49,12 @@ const MODERATION_TAB = { iconClassName: 'w-4 h-4', }; -function AuthorTabContent({ - authorId, - currentTab, - isPending, -}: { - authorId: number; - currentTab: string; - isPending: boolean; -}) { - const filters = TAB_TO_ACTIVITY_FILTERS[currentTab]; +/** Overview answers to its legacy `contributions` token so existing links stay valid. */ +function resolveAuthorTab(tab: string): AuthorTab { + return tab === 'moderation' ? 'moderation' : 'overview'; +} + +function AuthorActivityFeed({ authorId }: { authorId: number }) { const { entries, isLoading, @@ -108,11 +65,7 @@ function AuthorTabContent({ feedKey, restoredScrollPosition, lastClickedEntryId, - } = useActivityFeed({ - authorId, - contentType: filters?.contentType, - commentTypes: filters?.commentTypes, - }); + } = useActivityFeed({ authorId }); useFeedScrollTracking({ feedKey, @@ -123,11 +76,11 @@ function AuthorTabContent({ lastClickedEntryId: lastClickedEntryId ?? undefined, }); - const rows = isPending ? [] : groupActivityRows(entries); + const rows = groupActivityRows(entries); return ( (null); @@ -163,63 +116,34 @@ export default function AuthorProfilePage({ params }: { params: Promise<{ id: st } }, [urlTab, pendingTab]); - const currentTab = pendingTab ?? urlTab; + const activeTab = resolveAuthorTab(pendingTab ?? urlTab); - const setTab = (tabId: string) => { - setPendingTab(tabId); + const changeTab = (tabId: string) => { + const nextTab = tabId === 'moderation' ? 'moderation' : 'contributions'; + setPendingTab(nextTab); startTransition(() => { const params = new URLSearchParams(searchParams); - params.set('tab', tabId); + params.set('tab', nextTab); router.replace(`/author/${authorId}?${params.toString()}`, { scroll: false }); }); }; - const activeGroup = getTabGroup(currentTab); - - /** - * Top-level tab clicks: jump to the group's landing state. For Activity this - * means its first pill; for Funding it's the group token (`funding`) which - * lets `ProfileFundingTab` decide the default pill based on fetched data. - */ - const handleTopTabChange = (groupId: string) => { - switch (groupId) { - case 'overview': - setTab('contributions'); - break; - case 'funding': - setTab('funding'); - break; - case 'activity': - setTab(ACTIVITY_PILLS[0].id); - break; - case 'moderation': - setTab('moderation'); - break; - } - }; - const canModerate = !!(currentUser?.moderator || isHubEditor) && !!user?.authorProfile?.userId; - const viewingOwnProfile = !!( + const isOwnProfile = !!( currentUser?.authorProfile?.id && user?.authorProfile?.id === currentUser.authorProfile.id ); - const canViewFunding = viewingOwnProfile || !!currentUser?.moderator; - const tabs = [ - TOP_LEVEL_TABS[0], - ...(canViewFunding ? [TOP_LEVEL_TABS[1]] : []), - TOP_LEVEL_TABS[2], - ...(canModerate ? [MODERATION_TAB] : []), - ]; + const tabs = canModerate ? [OVERVIEW_TAB, MODERATION_TAB] : [OVERVIEW_TAB]; const tabsReady = !isLoading && !isUserLoading && !!user?.authorProfile; const tabBar = tabsReady ? ( - + ) : undefined; const profileLoading = isLoading || isUserLoading; const topBanner = (() => { if (profileLoading) { - return ; + return ; } if (error || userError || !user?.authorProfile) return undefined; return ( @@ -233,7 +157,6 @@ export default function AuthorProfilePage({ params }: { params: Promise<{ id: st const author = user?.authorProfile; const profileError = error || userError; - const isOwnProfile = viewingOwnProfile; const sidebarContent = (
@@ -265,7 +188,7 @@ export default function AuthorProfilePage({ params }: { params: Promise<{ id: st } if (!author) return null; - if (activeGroup === 'moderation' && canModerate) { + if (activeTab === 'moderation' && canModerate) { return ( ; - } - - if (activeGroup === 'activity') { - const activePill: ActivityPillId = isActivityPill(currentTab) - ? currentTab - : ACTIVITY_PILLS[0].id; - return ( - - - - ); - } - - // Overview (default) - return ( - - ); + return ; }; // Compact mobile header shown inside the Overview tab only, to avoid filler - // space on other tabs at narrow widths. Tablet+ uses the full `sidebarContent`. + // space on the Moderation tab at narrow widths. Tablet+ uses the full `sidebarContent`. const hasAnyStats = !!summaryStats && (summaryStats.worksCount > 0 || @@ -335,7 +240,7 @@ export default function AuthorProfilePage({ params }: { params: Promise<{ id: st
{sidebarContent}
)}
- {activeGroup === 'overview' && mobileOverviewHeader} + {activeTab === 'overview' && mobileOverviewHeader} {renderMain()}