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 139a6f1b8..5900898dc 100644 --- a/app/author/[id]/page.tsx +++ b/app/author/[id]/page.tsx @@ -6,26 +6,16 @@ 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 { 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 PinnedFundraise from './components/PinnedFundraise'; import { useOrcidCallback } from '@/components/Orcid/lib/hooks/useOrcidCallback'; import { ProfileHeroBanner, @@ -48,32 +38,9 @@ function AuthorProfileError({ error }: { error: string }) { ); } -const TAB_TO_CONTRIBUTION_TYPE: Record = { - contributions: 'ALL', - 'peer-reviews': 'REVIEW', - comments: 'CONVERSATION', - bounties: 'BOUNTY', -}; - -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', @@ -82,84 +49,47 @@ const MODERATION_TAB = { iconClassName: 'w-4 h-4', }; -function AuthorTabContent({ - authorId, - userId, - currentTab, - isPending, -}: { - authorId: number; - userId?: number; - currentTab: string; - isPending: boolean; -}) { - const contributionType = TAB_TO_CONTRIBUTION_TYPE[currentTab] || 'ALL'; +/** 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 { - 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 }); + + 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 = groupActivityRows(entries); return ( -
- {currentTab === 'contributions' && userId && ( -
- -
- )} - - } - maxLength={150} - showReadMoreCTA={true} - activeTab={currentTab} - restoredScrollPosition={restoredContributionsScrollPosition} - lastClickedEntryId={lastClickedContributionsEntryId ?? undefined} - shouldRenderBountyAsComment={true} - wideContent - /> -
+ + {rows.map((row) => ( + + ))} + ); } @@ -176,7 +106,7 @@ export default function AuthorProfilePage({ params }: { params: Promise<{ id: st // Tab state — lifted here so the tab bar can live in the hero banner const searchParams = useSearchParams(); const router = useRouter(); - const [isPending, startTransition] = useTransition(); + const [, startTransition] = useTransition(); const urlTab = searchParams.get('tab') || 'contributions'; const [pendingTab, setPendingTab] = useState(null); @@ -186,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 ( @@ -256,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 = (
@@ -288,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 || @@ -368,7 +240,7 @@ export default function AuthorProfilePage({ params }: { params: Promise<{ id: st
{sidebarContent}
)}
- {activeGroup === 'overview' && mobileOverviewHeader} + {activeTab === 'overview' && mobileOverviewHeader} {renderMain()}