diff --git a/app/author/[id]/page.tsx b/app/author/[id]/page.tsx index 871a3284d..139a6f1b8 100644 --- a/app/author/[id]/page.tsx +++ b/app/author/[id]/page.tsx @@ -25,8 +25,6 @@ import { type ActivityPillId, } from '@/components/profile/ProfileActivityTab'; import { OrcidSyncBanner } from '@/components/profile/OrcidSyncBanner'; -import { useAuthorPublications } from '@/hooks/usePublications'; -import { transformPublicationToFeedEntry } from '@/types/publication'; import PinnedFundraise from './components/PinnedFundraise'; import { useOrcidCallback } from '@/components/Orcid/lib/hooks/useOrcidCallback'; import { @@ -52,7 +50,6 @@ function AuthorProfileError({ error }: { error: string }) { const TAB_TO_CONTRIBUTION_TYPE: Record = { contributions: 'ALL', - publications: 'ARTICLE', 'peer-reviews': 'REVIEW', comments: 'CONVERSATION', bounties: 'BOUNTY', @@ -119,59 +116,6 @@ function AuthorTabContent({ ? allContributions.filter((contribution) => !contribution.item?.review?.score) : allContributions; - const { - publications, - isLoading: isPublicationsLoading, - error: publicationsError, - hasMore: hasMorePublications, - loadMore: loadMorePublications, - isLoadingMore: isLoadingMorePublications, - restoredFeedEntries: restoredPublicationsEntries, - restoredScrollPosition: restoredPublicationsScrollPosition, - lastClickedEntryId: lastClickedPublicationsEntryId, - } = useAuthorPublications({ - authorId, - activeTab: currentTab, - }); - - if (currentTab === 'publications') { - if (publicationsError) { - return
Error: {publicationsError.message}
; - } - - const entries = - restoredPublicationsEntries || - publications - .map((publication) => { - try { - return transformPublicationToFeedEntry(publication); - } catch (error) { - console.warn('[Publication] Could not parse publication', error); - return null; - } - }) - .filter((entry): entry is FeedEntry => !!entry); - - return ( - } - maxLength={150} - activeTab={currentTab} - restoredScrollPosition={restoredPublicationsScrollPosition} - lastClickedEntryId={lastClickedPublicationsEntryId ?? undefined} - shouldRenderBountyAsComment={true} - wideContent - /> - ); - } - if (contributionsError) { return
Error: {contributionsError.message}
; } @@ -359,7 +303,9 @@ export default function AuthorProfilePage({ params }: { params: Promise<{ id: st } if (activeGroup === 'activity') { - const activePill: ActivityPillId = isActivityPill(currentTab) ? currentTab : 'publications'; + const activePill: ActivityPillId = isActivityPill(currentTab) + ? currentTab + : ACTIVITY_PILLS[0].id; return ( { - onDoThisLater?.(); - toast.success('Visit the "Publications" tab on your profile to resume', { - duration: 5000, - }); - }; - const handleAddPublications = async () => { setStep('LOADING'); try { @@ -200,7 +192,7 @@ export function AddPublicationsForm({
{allowDoThisLater && ( - )} diff --git a/components/profile/ProfileActivityTab.tsx b/components/profile/ProfileActivityTab.tsx index 0055ddfcd..e16004485 100644 --- a/components/profile/ProfileActivityTab.tsx +++ b/components/profile/ProfileActivityTab.tsx @@ -5,7 +5,6 @@ import { PillTabs } from '@/components/ui/PillTabs'; import { useFeed } from '@/hooks/useFeed'; export const ACTIVITY_PILLS = [ - { id: 'publications', label: 'Publications' }, { id: 'proposals', label: 'Proposals' }, { id: 'peer-reviews', label: 'Peer Reviews' }, { id: 'comments', label: 'Comments' }, @@ -25,6 +24,14 @@ interface ProfileActivityTabProps { children: React.ReactNode; } +function ProposalsEmptyState() { + return ( +
+

No proposals yet

+
+ ); +} + function ProposalsContent({ userId }: { userId: number }) { const { entries, isLoading, hasMore, loadMore } = useFeed('all', { endpoint: 'funding_feed', @@ -45,11 +52,7 @@ function ProposalsContent({ userId }: { userId: number }) { hideActions skeletonVariant="fundraise" wideContent - noEntriesElement={ -
-

No proposals yet

-
- } + noEntriesElement={} /> ); } @@ -65,6 +68,11 @@ export function ProfileActivityTab({ userId, children, }: ProfileActivityTabProps) { + let activityContent = children; + if (activePill === 'proposals') { + activityContent = userId ? : ; + } + return (
@@ -75,7 +83,7 @@ export function ProfileActivityTab({ size="sm" />
- {activePill === 'proposals' && userId ? : children} + {activityContent}
); } diff --git a/hooks/useContributions.ts b/hooks/useContributions.ts index a4c5963ff..b174f180c 100644 --- a/hooks/useContributions.ts +++ b/hooks/useContributions.ts @@ -18,7 +18,6 @@ export const useContributions = (options: UseContributionsOptions = {}) => { useFeedStateRestoration({ activeTab: options.activeTab, shouldRestore: (isBackNavigation) => { - if (options.contribution_type === 'ARTICLE') return false; if (!isBackNavigation || !options.author_id) return false; return true; }, diff --git a/hooks/useFeedSource.ts b/hooks/useFeedSource.ts index 7cb2569f9..5f435b286 100644 --- a/hooks/useFeedSource.ts +++ b/hooks/useFeedSource.ts @@ -35,7 +35,7 @@ import { FeedSource } from '@/types/analytics'; * - /topic/ai/latest → source: 'topic', tab: 'latest' * - /fund/needs-funding → source: 'fund', tab: 'needs-funding' * - /author/153397 → source: 'author', tab: 'contributions' - * - /author/153397?tab=publications → source: 'author', tab: 'publications' + * - /author/153397?tab=peer-reviews → source: 'author', tab: 'peer-reviews' * - /list/123 → source: 'list', tab: '123' * - /search?q=ai → source: 'search', tab: 'search' */ diff --git a/hooks/usePublications.ts b/hooks/usePublications.ts index 7eb63c265..b6014f876 100644 --- a/hooks/usePublications.ts +++ b/hooks/usePublications.ts @@ -1,17 +1,13 @@ 'use client'; -import { useState, useCallback, useEffect } from 'react'; +import { useState, useCallback } from 'react'; import { PublicationService, PublicationSearchParams, PublicationError, AddPublicationsParams, - AuthorPublicationsResponse, } from '@/services/publication.service'; import { OpenAlexWork, OpenAlexAuthor, PublicationSearchResponse } from '@/types/publication'; -import { ID } from '@/types/root'; -import { useFeedStateRestoration } from './useFeedStateRestoration'; -import { FeedEntry } from '@/types/feed'; interface UsePublicationsSearchState { data: PublicationSearchResponse | null; @@ -103,101 +99,3 @@ export function useAddPublications(): UseAddPublicationsReturn { return [{ isLoading, error }, addPublications]; } - -interface UseAuthorPublicationsOptions { - authorId: ID; - initialData?: AuthorPublicationsResponse; - activeTab?: string; // Add activeTab for feed key generation -} - -export function useAuthorPublications(options: UseAuthorPublicationsOptions) { - const { restoredState, initialEntries, restoredScrollPosition, lastClickedEntryId } = - useFeedStateRestoration({ - activeTab: options.activeTab, - }); - - const initialHasRestoredEntries = restoredState !== null; - - const [publications, setPublications] = useState(options.initialData?.results || []); - const [isLoading, setIsLoading] = useState(!initialHasRestoredEntries && !options.initialData); - const [isLoadingMore, setIsLoadingMore] = useState(false); - const [currentResponse, setCurrentResponse] = useState( - options.initialData || null - ); - const [error, setError] = useState(null); - - const [restoredFeedEntries, setRestoredFeedEntries] = useState(initialEntries); - const [hasRestoredEntries, setHasRestoredEntries] = useState(initialHasRestoredEntries); - - const loadPublications = async () => { - setIsLoading(true); - setError(null); - - try { - const response = await PublicationService.getAuthorPublications({ - authorId: options.authorId, - }); - - setPublications(response.results); - setCurrentResponse(response); - } catch (err) { - setError(err instanceof Error ? err : new Error('Failed to load publications')); - console.error('Error loading publications:', err); - } finally { - setIsLoading(false); - } - }; - - useEffect(() => { - if (initialHasRestoredEntries && initialEntries.length > 0 && !hasRestoredEntries) { - setHasRestoredEntries(true); - setRestoredFeedEntries(initialEntries); - } - }, [initialHasRestoredEntries, initialEntries.length, hasRestoredEntries]); - - useEffect(() => { - if (hasRestoredEntries && restoredFeedEntries.length > 0) { - return; - } - - if (options.initialData) { - return; - } - - loadPublications(); - }, [options.authorId, options.initialData]); - - const loadMore = async () => { - if (!currentResponse?.next || isLoading || isLoadingMore) { - return; - } - - setIsLoadingMore(true); - setError(null); - - try { - const nextPage = await PublicationService.loadMoreAuthorPublications(currentResponse); - - setPublications((prev) => [...prev, ...nextPage.results]); - setCurrentResponse(nextPage); - } catch (err) { - setError(err instanceof Error ? err : new Error('Failed to load more publications')); - console.error('Error loading more publications:', err); - } finally { - setIsLoadingMore(false); - } - }; - - return { - publications, - isLoading, - error, - hasMore: !!currentResponse?.next, - loadMore, - refresh: loadPublications, - isLoadingMore, - restoredFeedEntries: hasRestoredEntries ? restoredFeedEntries : undefined, - restoredScrollPosition, - lastClickedEntryId, - }; -} diff --git a/services/contribution.service.ts b/services/contribution.service.ts index 9e4fdba0c..aa382082e 100644 --- a/services/contribution.service.ts +++ b/services/contribution.service.ts @@ -3,7 +3,7 @@ import { ApiError } from './types'; import type { Contribution, ContributionListResponse } from '@/types/contribution'; import { ID } from '@/types/root'; -export type ContributionType = 'CONVERSATION' | 'ARTICLE' | 'REVIEW' | 'BOUNTY' | 'ALL'; +export type ContributionType = 'CONVERSATION' | 'REVIEW' | 'BOUNTY' | 'ALL'; export interface GetContributionsParams { contribution_type?: ContributionType; diff --git a/services/publication.service.ts b/services/publication.service.ts index 85f06de32..155a3f2ed 100644 --- a/services/publication.service.ts +++ b/services/publication.service.ts @@ -1,6 +1,5 @@ import { ApiClient } from './client'; import { transformPublicationsResponse, PublicationSearchResponse } from '@/types/publication'; -import { ID } from '@/types/root'; import { ApiError } from './types'; export class PublicationError extends Error { @@ -24,18 +23,6 @@ export interface AddPublicationsParams { openAlexAuthorId: string; } -export interface GetAuthorPublicationsParams { - authorId: ID; - nextUrl?: string | null; -} - -export interface AuthorPublicationsResponse { - results: any[]; - count: number; - next: string | null; - previous: string | null; -} - export class PublicationService { private static readonly BASE_PATH = '/api/paper'; private static readonly AUTHOR_PATH = '/api/author'; @@ -102,74 +89,4 @@ export class PublicationService { ); } } - - /** - * Fetches publications for a specific author - * @param params - Parameters for fetching author publications - * @throws {PublicationError} When the request fails or parameters are invalid - */ - static async getAuthorPublications( - params: GetAuthorPublicationsParams - ): Promise { - try { - const url = params.nextUrl || `${this.AUTHOR_PATH}/${params.authorId}/publications`; - - const response = await ApiClient.get(url); - - if (!response || !Array.isArray(response.results)) { - throw new PublicationError('Invalid response format', 'INVALID_RESPONSE'); - } - - return { - results: response.results, - count: response.count || 0, - next: response.next || null, - previous: response.previous || null, - }; - } catch (error) { - if (error instanceof PublicationError) { - throw error; - } - - const { data = {} } = error instanceof ApiError ? JSON.parse(error.message) : {}; - const errorMsg = data?.detail || 'Failed to fetch author publications'; - throw new PublicationError(errorMsg); - } - } - - /** - * Loads the next page of author publications if available - * @param currentResponse - The current response containing the next page URL - * @throws {PublicationError} When the request fails or no next page is available - */ - static async loadMoreAuthorPublications( - currentResponse: AuthorPublicationsResponse - ): Promise { - if (!currentResponse.next) { - throw new PublicationError('No more publications available', 'NO_MORE_RESULTS'); - } - - try { - const response = await ApiClient.get(currentResponse.next); - - if (!response || !Array.isArray(response.results)) { - throw new PublicationError('Invalid response format', 'INVALID_RESPONSE'); - } - - return { - results: response.results, - count: response.count || 0, - next: response.next || null, - previous: response.previous || null, - }; - } catch (error) { - if (error instanceof PublicationError) { - throw error; - } - - const { data = {} } = error instanceof ApiError ? JSON.parse(error.message) : {}; - const errorMsg = data?.detail || 'Failed to load more publications'; - throw new PublicationError(errorMsg); - } - } } diff --git a/types/publication.ts b/types/publication.ts index ff2d8eab1..e066ab25d 100644 --- a/types/publication.ts +++ b/types/publication.ts @@ -1,5 +1,3 @@ -import { stripHtml } from '@/utils/stringUtils'; -import { FeedEntry } from './feed'; import { createTransformer } from './transformer'; // Transformed types for our application @@ -85,147 +83,3 @@ export const transformPublicationsResponse = createTransformer; - concepts: OpenAlexConcept[]; -} - -export interface AuthorPublicationsResponse { - count: number; - next: string | null; - previous: string | null; - results: Array<{ - id: number; - recommendation_id: string | null; - documents: { - id: number; - authors: Array<{ - id: number; - first_name: string; - last_name: string; - user: number | null; - authorship: { - position: string; - is_corresponding: boolean; - }; - }>; - title: string; - paper_title: string; - paper_publish_date: string; - abstract: string | null; - slug: string; - work_type: string; - external_source: string; - citations: number; - is_open_access: boolean; - oa_status: string; - created_date: string; - }; - hubs: Array<{ - id: number; - name: string; - slug: string; - hub_image: string; - }>; - created_date: string; - document_type: string; - }>; -} - -export const transformPublicationToFeedEntry = ( - publication: AuthorPublicationsResponse['results'][0] -): FeedEntry => { - const { documents, hubs, created_date } = publication; - - if (!documents) { - throw new Error('Publication documents field is missing'); - } - - const authors = documents.authors || []; - const firstAuthor = authors[0]; - - return { - id: documents.id?.toString() || '', - recommendationId: publication.recommendation_id, - timestamp: documents.paper_publish_date || created_date, - action: 'publish', - contentType: 'PAPER', - content: { - unifiedDocumentId: publication.id?.toString() || '', - id: documents.id, - contentType: 'PAPER', - createdDate: created_date, - textPreview: stripHtml(documents.abstract || ''), - slug: documents.slug || '', - title: documents.title || '', - authors: authors.map((author) => ({ - id: author.id, - profileImage: '', - firstName: author.first_name || '', - lastName: author.last_name || '', - fullName: `${author.first_name || ''} ${author.last_name || ''}`.trim() || 'Unknown Author', - profileUrl: '', - isClaimed: false, - isVerified: false, - })), - topics: (hubs || []) - .map((hub: any) => ({ - id: hub.id, - name: hub.name, - hub_image: hub.hub_image, - slug: hub.slug, - })) - .slice(0, 2), - createdBy: { - id: firstAuthor?.id || 0, - profileImage: '', - firstName: firstAuthor?.first_name || '', - lastName: firstAuthor?.last_name || '', - fullName: - `${firstAuthor?.first_name || ''} ${firstAuthor?.last_name || ''}`.trim() || - 'Unknown Author', - profileUrl: '', - isClaimed: false, - isVerified: false, - }, - journal: { - id: 0, - name: '', - slug: '', - description: '', - }, - }, - relatedWork: undefined, - metrics: undefined, - }; -}; - -export const transformAuthorPublicationsResponse = ( - response: AuthorPublicationsResponse -): { - entries: FeedEntry[]; - next: string | null; - previous: string | null; - count: number; -} => { - return { - entries: response.results.map(transformPublicationToFeedEntry), - next: response.next, - previous: response.previous, - count: response.count, - }; -};