diff --git a/app/layouts/topbar/pageRoutes.tsx b/app/layouts/topbar/pageRoutes.tsx index c3b1f1527..3a64d109c 100644 --- a/app/layouts/topbar/pageRoutes.tsx +++ b/app/layouts/topbar/pageRoutes.tsx @@ -155,13 +155,6 @@ const ROUTE_RULES: RouteRule[] = [ icon: , }), }, - { - match: (p) => p.startsWith('/paper/'), - getInfo: () => ({ - title: 'Paper', - icon: , - }), - }, { match: (p) => p.startsWith('/post/'), getInfo: () => ({ diff --git a/app/paper/[id]/[slug]/bounties/page.tsx b/app/paper/[id]/[slug]/bounties/page.tsx deleted file mode 100644 index 873f8bab4..000000000 --- a/app/paper/[id]/[slug]/bounties/page.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { PaperService } from '@/services/paper.service'; -import { MetadataService } from '@/services/metadata.service'; -import { Metadata } from 'next'; -import { notFound } from 'next/navigation'; -import { WorkDocument } from '@/components/work/WorkDocument'; -import { SearchHistoryTracker } from '@/components/work/SearchHistoryTracker'; -import { WorkDocumentTracker } from '@/components/WorkDocumentTracker'; -import { getWorkMetadata } from '@/lib/metadata-helpers'; - -interface Props { - params: Promise<{ - id: string; - slug: string; - }>; -} - -async function getWork(id: string) { - if (!id.match(/^\d+$/)) { - notFound(); - } - - try { - return await PaperService.get(id); - } catch (error) { - notFound(); - } -} - -export async function generateMetadata({ params }: Props): Promise { - const resolvedParams = await params; - const work = await getWork(resolvedParams.id); - return getWorkMetadata({ - work: work, - url: `/paper/${resolvedParams.id}/${resolvedParams.slug}/bounties`, - }); -} - -export default async function WorkBountiesPage({ params }: Props) { - const resolvedParams = await params; - const work = await getWork(resolvedParams.id); - const metadata = await MetadataService.get(work.unifiedDocumentId?.toString() || ''); - - if (!work) { - notFound(); - } - - return ( - <> - - - - - ); -} diff --git a/app/paper/[id]/[slug]/conversation/page.tsx b/app/paper/[id]/[slug]/conversation/page.tsx deleted file mode 100644 index 20d0771fc..000000000 --- a/app/paper/[id]/[slug]/conversation/page.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { PaperService } from '@/services/paper.service'; -import { MetadataService } from '@/services/metadata.service'; -import { Metadata } from 'next'; -import { notFound } from 'next/navigation'; -import { WorkDocument } from '@/components/work/WorkDocument'; -import { SearchHistoryTracker } from '@/components/work/SearchHistoryTracker'; -import { WorkDocumentTracker } from '@/components/WorkDocumentTracker'; -import { getWorkMetadata } from '@/lib/metadata-helpers'; - -interface Props { - params: Promise<{ - id: string; - slug: string; - }>; -} - -async function getWork(id: string) { - if (!id.match(/^\d+$/)) { - notFound(); - } - - try { - return await PaperService.get(id); - } catch (error) { - notFound(); - } -} - -export async function generateMetadata({ params }: Props): Promise { - const resolvedParams = await params; - const work = await getWork(resolvedParams.id); - return getWorkMetadata({ - work: work, - url: `/paper/${resolvedParams.id}/${resolvedParams.slug}/conversation`, - }); -} - -export default async function WorkConversationPage({ params }: Props) { - const resolvedParams = await params; - const work = await getWork(resolvedParams.id); - const metadata = await MetadataService.get(work.unifiedDocumentId?.toString() || ''); - - if (!work) { - notFound(); - } - - return ( - <> - - - - - ); -} diff --git a/app/paper/[id]/[slug]/history/page.tsx b/app/paper/[id]/[slug]/history/page.tsx deleted file mode 100644 index 0da1befa3..000000000 --- a/app/paper/[id]/[slug]/history/page.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { PaperService } from '@/services/paper.service'; -import { MetadataService } from '@/services/metadata.service'; -import { Metadata } from 'next'; -import { notFound } from 'next/navigation'; -import { WorkDocument } from '@/components/work/WorkDocument'; -import { SearchHistoryTracker } from '@/components/work/SearchHistoryTracker'; -import { WorkDocumentTracker } from '@/components/WorkDocumentTracker'; -import { getWorkMetadata } from '@/lib/metadata-helpers'; - -interface Props { - params: Promise<{ - id: string; - slug: string; - }>; -} - -async function getWork(id: string) { - if (!id.match(/^\d+$/)) { - notFound(); - } - - try { - return await PaperService.get(id); - } catch (error) { - notFound(); - } -} - -export async function generateMetadata({ params }: Props): Promise { - const resolvedParams = await params; - const work = await getWork(resolvedParams.id); - return getWorkMetadata({ - work: work, - url: `/paper/${resolvedParams.id}/${resolvedParams.slug}/history`, - }); -} - -export default async function WorkHistoryPage({ params }: Props) { - const resolvedParams = await params; - const work = await getWork(resolvedParams.id); - const metadata = await MetadataService.get(work.unifiedDocumentId?.toString() || ''); - - if (!work) { - notFound(); - } - - return ( - <> - - - - - ); -} diff --git a/app/paper/[id]/[slug]/layout.tsx b/app/paper/[id]/[slug]/layout.tsx deleted file mode 100644 index 4a73c5bba..000000000 --- a/app/paper/[id]/[slug]/layout.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { Suspense } from 'react'; -import { Metadata } from 'next'; -import { notFound } from 'next/navigation'; -import { PaperService } from '@/services/paper.service'; -import { MetadataService } from '@/services/metadata.service'; -import { buildArticleMetadata } from '@/lib/metadata'; -import { stripHtml } from '@/utils/stringUtils'; -import { PageLayout } from '@/app/layouts/PageLayout'; -import { WorkHeader, WorkTabProvider } from '@/components/work/WorkHeader/index'; -import { WorkRightSidebar } from '@/components/work/WorkRightSidebar'; - -interface Props { - params: Promise<{ - id: string; - slug: string; - }>; - children: React.ReactNode; -} - -export async function generateMetadata({ params }: Props): Promise { - const { id, slug } = await params; - try { - const work = await PaperService.get(id); - const previewText = stripHtml(work.previewContent || '').substring(0, 155); - const description = work.abstract || previewText || 'Read this research paper on ResearchHub.'; - return buildArticleMetadata({ - title: work.title, - description, - url: `/paper/${id}/${slug}`, - image: work.image || work.figures?.[0]?.url, - publishedTime: work.publishedDate || work.createdDate, - modifiedTime: work.updatedDate, - authors: work.authors.map((a) => a.authorProfile.fullName), - section: work.topics?.[0]?.name, - tags: work.topics?.map((topic) => topic.name) ?? [], - }); - } catch { - return {}; - } -} - -export default async function PaperSlugLayout({ params, children }: Props) { - const { id } = await params; - - if (!id.match(/^\d+$/)) { - notFound(); - } - - let work; - try { - work = await PaperService.get(id); - } catch { - notFound(); - } - const metadata = await MetadataService.get(work.unifiedDocumentId?.toString() || ''); - - return ( - - } - rightSidebar={} - > - {children} - - - ); -} diff --git a/app/paper/[id]/[slug]/page.tsx b/app/paper/[id]/[slug]/page.tsx deleted file mode 100644 index 8bcc4844a..000000000 --- a/app/paper/[id]/[slug]/page.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { PaperService } from '@/services/paper.service'; -import { MetadataService } from '@/services/metadata.service'; -import { Metadata } from 'next'; -import { notFound } from 'next/navigation'; -import { WorkDocument } from '@/components/work/WorkDocument'; -import { SearchHistoryTracker } from '@/components/work/SearchHistoryTracker'; -import { WorkDocumentTracker } from '@/components/WorkDocumentTracker'; -import { getWorkMetadata } from '@/lib/metadata-helpers'; - -interface Props { - params: Promise<{ - id: string; - slug: string; - }>; -} - -async function getWork(id: string) { - if (!id.match(/^\d+$/)) { - notFound(); - } - - try { - return await PaperService.get(id); - } catch (error) { - notFound(); - } -} - -export async function generateMetadata({ params }: Props): Promise { - const resolvedParams = await params; - const work = await getWork(resolvedParams.id); - - return getWorkMetadata({ - work: work, - url: `/paper/${resolvedParams.id}/${resolvedParams.slug}`, - }); -} - -export default async function WorkPage({ params }: Props) { - const resolvedParams = await params; - const work = await getWork(resolvedParams.id); - const metadata = await MetadataService.get(work.unifiedDocumentId?.toString() || ''); - - if (!work) { - notFound(); - } - - return ( - <> - - - - - ); -} diff --git a/app/paper/[id]/[slug]/reviews/page.tsx b/app/paper/[id]/[slug]/reviews/page.tsx deleted file mode 100644 index 395ffbe95..000000000 --- a/app/paper/[id]/[slug]/reviews/page.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { PaperService } from '@/services/paper.service'; -import { MetadataService } from '@/services/metadata.service'; -import { Metadata } from 'next'; -import { notFound } from 'next/navigation'; -import { WorkDocument } from '@/components/work/WorkDocument'; -import { SearchHistoryTracker } from '@/components/work/SearchHistoryTracker'; -import { WorkDocumentTracker } from '@/components/WorkDocumentTracker'; -import { getWorkMetadata } from '@/lib/metadata-helpers'; - -interface Props { - params: Promise<{ - id: string; - slug: string; - }>; -} - -async function getWork(id: string) { - if (!id.match(/^\d+$/)) { - notFound(); - } - - try { - return await PaperService.get(id); - } catch (error) { - notFound(); - } -} - -export async function generateMetadata({ params }: Props): Promise { - const resolvedParams = await params; - const work = await getWork(resolvedParams.id); - return getWorkMetadata({ - work: work, - url: `/paper/${resolvedParams.id}/${resolvedParams.slug}/reviews`, - }); -} - -export default async function WorkReviewsPage({ params }: Props) { - const resolvedParams = await params; - const work = await getWork(resolvedParams.id); - const metadata = await MetadataService.get(work.unifiedDocumentId?.toString() || ''); - - if (!work) { - notFound(); - } - - return ( - <> - - - - - ); -} diff --git a/app/paper/[id]/page.tsx b/app/paper/[id]/page.tsx deleted file mode 100644 index 2aba55919..000000000 --- a/app/paper/[id]/page.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { notFound } from 'next/navigation'; -import { PaperService } from '@/services/paper.service'; -import { handleMissingSlugRedirect } from '@/utils/navigation'; - -interface Props { - params: Promise<{ - id: string; - }>; -} - -export default async function PaperRedirectPage({ params }: Props) { - const resolvedParams = await params; - const id = resolvedParams.id; - - if (!id.match(/^\d+$/)) { - notFound(); - } - - let paper; - try { - paper = await PaperService.get(id); - } catch (error) { - notFound(); - } - - // Redirect to the full URL with slug (outside try-catch) - handleMissingSlugRedirect(paper, id, 'paper'); -} diff --git a/app/paper/layout.tsx b/app/paper/layout.tsx deleted file mode 100644 index ce518aaf2..000000000 --- a/app/paper/layout.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function WorkLayout(props: { children: React.ReactNode }) { - return props.children; -} diff --git a/app/paper/loading.tsx b/app/paper/loading.tsx deleted file mode 100644 index 849d75755..000000000 --- a/app/paper/loading.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { PageLayout } from '@/app/layouts/PageLayout'; -import { WorkSkeleton } from '@/components/work/WorkSkeleton'; -import { PaperHeaderSkeleton } from '@/components/work/PaperHeaderSkeleton'; -import { WorkRightSidebarSkeleton } from '@/components/work/WorkRightSidebarSkeleton'; - -export default function PaperLoading() { - return ( - } rightSidebar={}> - - - ); -} diff --git a/app/paper/page.tsx b/app/paper/page.tsx deleted file mode 100644 index ab48fb34f..000000000 --- a/app/paper/page.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { PaperService } from '@/services/paper.service'; -import { redirect } from 'next/navigation'; -import { buildWorkUrl, generateSlug } from '@/utils/url'; - -type Props = { - params: Promise>; - searchParams: Promise<{ [key: string]: string | string[] | undefined }>; -}; - -export default async function WorkPage({ params, searchParams }: Props) { - const searchParamsData = await searchParams; - const doi = searchParamsData.doi; - if (!doi || typeof doi !== 'string') { - redirect('/404'); - } - - try { - // This will show loading state in @loading slot while fetching - const work = await PaperService.get(doi); - redirect( - buildWorkUrl({ - id: work.id, - contentType: 'paper', - slug: work.slug || generateSlug(work.title), - }) - ); - } catch (error: any) { - if (!error.digest?.startsWith('NEXT_REDIRECT')) { - console.error('Error importing work:', error); - redirect('/404'); - } - throw error; // Re-throw Next.js redirects - } -} diff --git a/app/sitemap.ts b/app/sitemap.ts index 131c47995..1ccb46c44 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -79,7 +79,7 @@ const STATIC_ROUTES: MetadataRoute.Sitemap = [ ]; export async function generateSitemaps() { - return [{ id: 0 }, { id: 1 }, { id: 2 }, { id: 3 }]; + return [{ id: 0 }, { id: 1 }, { id: 2 }]; } export default async function sitemap({ @@ -90,18 +90,11 @@ export default async function sitemap({ const sitemapId = Number(await id); if (sitemapId === 0) return STATIC_ROUTES; if (sitemapId === 1) - return buildSlugSitemap( - '/api/feed/?content_type=PAPER&page_size=1000', - '/paper/', - (item) => item.content_object - ); - - if (sitemapId === 2) return buildSlugSitemap( '/api/researchhubpost/?document_type=PREREGISTRATION&page_size=1000', '/proposal/' ); - if (sitemapId === 3) + if (sitemapId === 2) return buildSlugSitemap('/api/researchhubpost/?document_type=GRANT&page_size=100', '/grant/'); return []; diff --git a/components/Activity/lib/activityWork.utils.ts b/components/Activity/lib/activityWork.utils.ts index f85f67bd6..868ceedfb 100644 --- a/components/Activity/lib/activityWork.utils.ts +++ b/components/Activity/lib/activityWork.utils.ts @@ -18,7 +18,6 @@ import type { FeedBountyContent, FeedEntry, FeedGrantContent, - FeedPaperContent, FeedPostContent, } from '@/types/feed'; import type { Bounty } from '@/types/bounty'; @@ -546,29 +545,8 @@ function grantSummaryFromFeedGrant(content: FeedGrantContent): WorkGrantSummary function getWorkFromContent(entry: FeedEntry): ActivityWork | null { const bounty = getActivityBounty(entry); - if (entry.contentType === 'PAPER') { - const paper = entry.content as FeedPaperContent; - if (!paper.title) return null; - const documentType: ContentType = 'paper'; - const tab = resolveWorkTab(entry, documentType); - return { - id: paper.id, - slug: paper.slug, - title: paper.title, - href: buildWorkUrl({ - id: paper.id, - slug: paper.slug, - contentType: documentType, - tab, - }), - imageUrl: paper.previewImage || paper.previewThumbnail, - documentType, - unifiedDocumentId: toOptionalNumber(paper.unifiedDocumentId), - bounty, - authors: paper.authors, - tab, - }; - } + // Paper pages are retired, so a paper event has no work card to show. + if (entry.contentType === 'PAPER') return null; if (entry.contentType === 'GRANT') { const grantContent = entry.content as FeedGrantContent; diff --git a/components/Comment/lib/MentionExtension.tsx b/components/Comment/lib/MentionExtension.tsx index 9cf6a32eb..5a34a909d 100644 --- a/components/Comment/lib/MentionExtension.tsx +++ b/components/Comment/lib/MentionExtension.tsx @@ -193,7 +193,6 @@ export const MentionExtension = Mention.extend({ const url = buildWorkUrl({ id: node.attrs.id, contentType: 'paper', - doi: node.attrs.doi, }); // If we got a fallback URL, render as a span instead of a link diff --git a/components/Comment/lib/TipTapRenderer.tsx b/components/Comment/lib/TipTapRenderer.tsx index dce448329..06e50c7c8 100644 --- a/components/Comment/lib/TipTapRenderer.tsx +++ b/components/Comment/lib/TipTapRenderer.tsx @@ -622,7 +622,7 @@ const RenderNode: React.FC = ({ // For papers if (entityType === 'paper') { - const href = buildWorkUrl({ id, doi: node.attrs?.doi, contentType: 'paper' }); + const href = buildWorkUrl({ id, contentType: 'paper' }); if (!href || href === '#') { return {label}; } diff --git a/components/Feed/FeedEntryItem.tsx b/components/Feed/FeedEntryItem.tsx index 668599472..026c92d60 100644 --- a/components/Feed/FeedEntryItem.tsx +++ b/components/Feed/FeedEntryItem.tsx @@ -2,15 +2,8 @@ import { FC, useCallback } from 'react'; import { useInView } from 'react-intersection-observer'; -import { - FeedCommentContent, - FeedEntry, - FeedPostContent, - FeedPaperContent, - FeedGrantContent, -} from '@/types/feed'; +import { FeedCommentContent, FeedEntry, FeedPostContent, FeedGrantContent } from '@/types/feed'; import { FeedItemFundraise } from './items/DeprecatedFeedItemFundraise'; -import { FeedItemPaper } from './items/FeedItemPaper'; import { FeedItemComment } from './items/FeedItemComment'; import { FeedItemPost } from './items/FeedItemPost'; import { FeedItemGrant } from './items/FeedItemGrant'; @@ -97,6 +90,11 @@ export const FeedEntryItem: FC = ({ return null; } + // Paper pages are retired, so paper entries have nowhere to go. + if (entry.contentType === 'PAPER') { + return null; + } + // Apply appropriate spacing based on position const spacingClass = index !== 0 ? 'mt-8' : ''; @@ -120,15 +118,6 @@ export const FeedEntryItem: FC = ({ contentType: 'preregistration', }); } - case 'PAPER': { - const paperContent = entry.content as FeedPaperContent; - return buildWorkUrl({ - id: paperContent.id, - slug: paperContent.slug, - contentType: 'paper', - }); - } - case 'BOUNTY': if (entry.relatedWork) { let contentType = entry.relatedWork.contentType; @@ -202,9 +191,7 @@ export const FeedEntryItem: FC = ({ // When showBountyInfo is set and the entry has open bounties, render as a uniform bounty card if ( shouldRenderAsBountyCard && - (entry.contentType === 'POST' || - entry.contentType === 'PAPER' || - entry.contentType === 'PREREGISTRATION') + (entry.contentType === 'POST' || entry.contentType === 'PREREGISTRATION') ) { content = ( = ({ ); break; - case 'PAPER': - content = ( - - ); - break; - case 'BOUNTY': { if (shouldRenderBountyAsComment) { content = ( diff --git a/components/Feed/FeedItemJournalBadge.tsx b/components/Feed/FeedItemJournalBadge.tsx deleted file mode 100644 index 3df98688e..000000000 --- a/components/Feed/FeedItemJournalBadge.tsx +++ /dev/null @@ -1,23 +0,0 @@ -'use client'; - -import { FC } from 'react'; -import { HashtagBadge } from '@/components/ui/badges/HashtagBadge'; -import { Journal } from '@/types/journal'; -import { getSourceLogo } from '@/utils/preprintUtil'; - -interface FeedItemJournalBadgeProps { - journal?: Journal; -} - -/** - * The hashtag-style journal badge (e.g. #biorxiv) on a paper feed item. Only - * the ResearchHub Journal has a page to link to. - */ -export const FeedItemJournalBadge: FC = ({ journal }) => { - if (!journal?.name || !journal.slug) return null; - const logo = getSourceLogo(journal.slug); - if (!logo) return null; - return ( - - ); -}; diff --git a/components/Feed/items/FeedItemPaper.tsx b/components/Feed/items/FeedItemPaper.tsx deleted file mode 100644 index 87ff0f133..000000000 --- a/components/Feed/items/FeedItemPaper.tsx +++ /dev/null @@ -1,212 +0,0 @@ -'use client'; - -import { FC } from 'react'; -import Image from 'next/image'; -import { useSearchParams } from 'next/navigation'; -import { FeedPaperContent, FeedEntry } from '@/types/feed'; -import { - BaseFeedItem, - TitleSection, - ImageSection, - MetadataSection, - FeedItemTopSection, -} from '@/components/Feed/BaseFeedItem'; -import { FeedItemAbstractSection } from '@/components/Feed/FeedItemAbstractSection'; -import { FeedItemJournalBadge } from '@/components/Feed/FeedItemJournalBadge'; -import { AuthorList } from '@/components/ui/AuthorList'; -import { Tooltip } from '@/components/ui/Tooltip'; -import { PopularityScoreTooltip } from '@/components/tooltips/HotScoreTooltip'; -import { PeerReviewTooltip } from '@/components/tooltips/PeerReviewTooltip'; -import { Star } from 'lucide-react'; -import { formatTimestamp } from '@/utils/date'; -import { buildWorkUrl } from '@/utils/url'; - -interface FeedItemPaperProps { - entry: FeedEntry; - href?: string; - showTooltips?: boolean; - showActions?: boolean; - maxLength?: number; - onFeedItemClick?: () => void; - onAbstractExpanded?: () => void; - showBountyInfo?: boolean; - abstractCollapsedByDefault?: boolean; -} - -/** - * Component for rendering a paper feed item using BaseFeedItem - */ -export const FeedItemPaper: FC = ({ - entry, - href, - showTooltips = true, - showActions = true, - maxLength, - onFeedItemClick, - onAbstractExpanded, - showBountyInfo, - abstractCollapsedByDefault, -}) => { - const searchParams = useSearchParams(); - const isDebugMode = searchParams.has('debug'); - - // Extract the paper from the entry's content - const paper = entry.content as FeedPaperContent; - - // Use provided href or create default paper page URL - const paperPageUrl = - href || - buildWorkUrl({ - id: paper.id, - slug: paper.slug, - contentType: 'paper', - }); - - // Construct the dynamic action text - const journalName = paper.journal?.name; - const actionText = journalName ? `published in ${journalName}` : 'published in a journal'; - - // Only show journal badge for specific preprint servers - const ALLOWED_JOURNALS = ['biorxiv', 'arxiv', 'medrxiv', 'chemrxiv']; - const journalSlugLower = paper.journal?.slug?.toLowerCase() || ''; - const shouldShowJournal = ALLOWED_JOURNALS.some((j) => journalSlugLower.includes(j)); - const filteredJournal = shouldShowJournal ? paper.journal : undefined; - - const thumbnailUrl = paper.previewThumbnail || paper.journal?.imageUrl; - const isPdfPreview = thumbnailUrl?.includes('preview'); - - const reviewScore = entry.metrics?.reviewScore; - const hasReviewScore = reviewScore !== undefined && reviewScore > 0; - - return ( - - ) : undefined - } - badges={} - > - {/* Top section with mobile image */} - - ) - } - rightContent={ - isDebugMode && entry.hotScoreV2 !== undefined && entry.hotScoreV2 > 0 ? ( - - } - width="w-72" - position="bottom" - > -
- Popularity Score - {Math.round(entry.hotScoreV2)} -
-
- ) : null - } - leftContent={null} - /> - - - - {/* Authors and Date */} - -
- {paper.authors.length > 0 && ( - ({ - name: author.fullName, - verified: author.user?.isVerified, - authorUrl: author.id === 0 ? undefined : author.profileUrl, - }))} - size="base" - className="text-gray-500 font-normal text-sm" - delimiter="," - delimiterClassName="ml-0" - showAbbreviatedInMobile={true} - hideExpandButton={true} - /> - )} - {(entry.timestamp || paper.createdDate) && ( - <> - {paper.authors.length > 0 && •} - - {formatTimestamp(entry.timestamp || paper.createdDate, false)} - - - )} - {hasReviewScore && ( - <> - • - - } - position="top" - width="w-[320px]" - > - - - {reviewScore.toFixed(1)} - - - - )} -
-
- - -
- ); -}; diff --git a/components/RecentlyVisited/RecentlyVisitedCard.tsx b/components/RecentlyVisited/RecentlyVisitedCard.tsx index c267cbc30..703b8ab13 100644 --- a/components/RecentlyVisited/RecentlyVisitedCard.tsx +++ b/components/RecentlyVisited/RecentlyVisitedCard.tsx @@ -53,7 +53,6 @@ function toRecentPage(visit: VisitRecord): RecentPage | null { const href = buildWorkUrl({ id: visit.id, contentType, - doi: 'doi' in visit ? visit.doi : undefined, slug: visit.slug, }); if (!href || href === '#') return null; diff --git a/components/Search/Search.tsx b/components/Search/Search.tsx index 51f7e4c39..7c16d375c 100644 --- a/components/Search/Search.tsx +++ b/components/Search/Search.tsx @@ -64,20 +64,13 @@ export function Search({ // Default behavior if (suggestion.entityType === 'paper') { - // Check if this is a funding request - if (suggestion.contentType === 'preregistration') { + if (suggestion.contentType === 'preregistration' && suggestion.id) { router.push(`/proposal/${suggestion.id}/${suggestion.slug || ''}`); - } else if (suggestion.isRecent) { - router.push(`/paper/${suggestion.id}/${suggestion.slug}`); + } else if (suggestion.contentType && suggestion.contentType !== 'paper' && suggestion.id) { + router.push(`/post/${suggestion.id}`); } else { - if (suggestion.id) { - router.push(`/paper/${suggestion.id}/${suggestion.slug || ''}`); - } else if ('doi' in suggestion && suggestion.doi) { - router.push(`/paper?doi=${encodeURIComponent(suggestion.doi)}`); - } else { - // Fallback if no identifiers are available - router.push('/'); - } + // Paper pages are retired. + router.push('/'); } } else if (suggestion.entityType === 'user' || suggestion.entityType === 'author') { navigateToAuthorProfile(suggestion.authorProfile.id); diff --git a/components/Search/SearchModal.tsx b/components/Search/SearchModal.tsx index be8b1fa3e..cd1681f99 100644 --- a/components/Search/SearchModal.tsx +++ b/components/Search/SearchModal.tsx @@ -48,16 +48,13 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) { // Navigate based on suggestion type if (suggestion.entityType === 'paper') { - if (suggestion.isRecent && suggestion.contentType && suggestion.contentType !== 'paper') { - if (suggestion.id) { - router.push(`/post/${suggestion.id}`); - } else { - router.push('/'); - } - } else if (suggestion.id) { - router.push(`/paper/${suggestion.id}/${suggestion.slug || ''}`); - } else if ('doi' in suggestion && suggestion.doi) { - router.push(`/paper?doi=${encodeURIComponent(suggestion.doi)}`); + if (suggestion.contentType === 'preregistration' && suggestion.id) { + router.push(`/proposal/${suggestion.id}/${suggestion.slug || ''}`); + } else if (suggestion.contentType && suggestion.contentType !== 'paper' && suggestion.id) { + router.push(`/post/${suggestion.id}`); + } else { + // Paper pages are retired. + router.push('/'); } } else if (suggestion.entityType === 'user' || suggestion.entityType === 'author') { navigateToAuthorProfile(suggestion.authorProfile.id); diff --git a/components/modals/ShareModal.tsx b/components/modals/ShareModal.tsx index fa40c9ab7..fb13fdba6 100644 --- a/components/modals/ShareModal.tsx +++ b/components/modals/ShareModal.tsx @@ -65,7 +65,6 @@ export const SHARE_CONFIGS: Record = { title: (docTitle, url) => { const getShareType = (url: string): string => { if (url.includes('/post/')) return 'Preprint'; - if (url.includes('/paper/')) return 'Paper'; if (url.includes('/proposal/')) return 'Proposal'; if (url.includes('/grant/')) return 'Request for Proposal'; if (url.includes('/report/')) return 'Registered Report'; diff --git a/components/tooltips/HotScoreTooltip.tsx b/components/tooltips/HotScoreTooltip.tsx deleted file mode 100644 index 62765d94f..000000000 --- a/components/tooltips/HotScoreTooltip.tsx +++ /dev/null @@ -1,104 +0,0 @@ -'use client'; - -import React from 'react'; -import { Clock, ArrowUp, Coins, Star, MessageCircle, Eye } from 'lucide-react'; -import { HotScoreBreakdown } from '@/types/feed'; -import Icon from '@/components/ui/icons/Icon'; - -// Custom Github icon component -const GithubIcon = ({ className }: { className?: string }) => ( - - - -); - -// Custom Bluesky icon component -const BlueskyIcon = ({ className }: { className?: string }) => ( - - - -); - -// Wrapper for the custom Icon component to match the expected interface -const EarnIcon = ({ className }: { className?: string }) => ( - -); - -interface PopularityScoreTooltipProps { - score: number; - breakdown?: HotScoreBreakdown; -} - -// Signal configuration with display names and icons -// Keys must match the API response: recency, upvote, tip, peer_review, comment, bounty, github, bluesky, page_views -const SIGNAL_CONFIG: { - key: string; - label: string; - icon: React.ComponentType<{ className?: string }>; -}[] = [ - { key: 'recency', label: 'Recency', icon: Clock }, - { key: 'page_views', label: 'Page Views', icon: Eye }, - { key: 'upvote', label: 'Upvotes', icon: ArrowUp }, - { key: 'tip', label: 'Tips', icon: Coins }, - { key: 'peer_review', label: 'Peer Reviews', icon: Star }, - { key: 'comment', label: 'Comments', icon: MessageCircle }, - { key: 'bounty', label: 'Bounty', icon: EarnIcon }, - { key: 'github', label: 'Github', icon: GithubIcon }, - { key: 'bluesky', label: 'Bluesky', icon: BlueskyIcon }, -]; - -const formatNumber = (num: number | null | undefined) => { - if (num === null || num === undefined) return '0'; - if (num === 0) return '0'; - if (num >= 1000) { - return `${(num / 1000).toFixed(1)}k`; - } - // For small decimals (like recency), show 2 decimal places - if (num > 0 && num < 1) { - return num.toFixed(2); - } - return Math.round(num).toString(); -}; - -export function PopularityScoreTooltip({ score, breakdown }: PopularityScoreTooltipProps) { - const signals = breakdown?.signals || {}; - - const handleClick = (e: React.MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - }; - - return ( -
- {/* Popularity Score - Centered Large Number */} -
-

Popularity Score

- {Math.round(score)} -
- - {/* Explanatory text */} -

- The popularity score is calculated based on multiple engagement signals including recency, - views, votes, tips, and social activity. -

- - {/* Signals List */} -
- {SIGNAL_CONFIG.map(({ key, label, icon: SignalIcon }) => { - const signal = signals[key]; - const rawValue = signal?.raw ?? 0; - - return ( -
-
- - {label} -
- {formatNumber(rawValue)} -
- ); - })} -
-
- ); -} diff --git a/components/ui/MainPageHeader.tsx b/components/ui/MainPageHeader.tsx deleted file mode 100644 index 5b72f2a87..000000000 --- a/components/ui/MainPageHeader.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React, { ReactNode } from 'react'; - -interface MainPageHeaderProps { - title: string; - subtitle: string; - icon: ReactNode; - showTitle?: boolean; -} - -export const MainPageHeader: React.FC = ({ - title, - subtitle, - icon, - showTitle = true, -}) => { - return ( - <> - {!showTitle &&

{title}

} -
- {showTitle &&

{title}

} -

{subtitle}

-
- - ); -}; diff --git a/components/ui/badges/HashtagBadge.tsx b/components/ui/badges/HashtagBadge.tsx deleted file mode 100644 index 968f87f83..000000000 --- a/components/ui/badges/HashtagBadge.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { FC } from 'react'; -import Link from 'next/link'; - -interface HashtagBadgeProps { - label: string; - /** Where the badge goes; without one it is plain text. */ - href?: string; - className?: string; -} - -const STYLE = { fontSize: '13px' }; - -export const HashtagBadge: FC = ({ label, href, className = '' }) => { - const baseClass = `flex-shrink-0 text-gray-500 ${className}`; - if (!href) { - return ( - - #{label} - - ); - } - return ( - e.stopPropagation()} - > - #{label} - - ); -}; diff --git a/components/work/PaperHeaderSkeleton.tsx b/components/work/PaperHeaderSkeleton.tsx deleted file mode 100644 index 44b419158..000000000 --- a/components/work/PaperHeaderSkeleton.tsx +++ /dev/null @@ -1,11 +0,0 @@ -'use client'; - -import { useParams } from 'next/navigation'; -import { WorkHeaderSkeleton } from '@/components/work/WorkSkeleton'; - -export function PaperHeaderSkeleton() { - const params = useParams(); - const slug = typeof params?.slug === 'string' ? params.slug : undefined; - - return ; -} diff --git a/components/work/WorkDocument.tsx b/components/work/WorkDocument.tsx index 2d950c7da..9f5d3e7d8 100644 --- a/components/work/WorkDocument.tsx +++ b/components/work/WorkDocument.tsx @@ -8,7 +8,6 @@ import { WorkMetadata } from '@/services/metadata.service'; import { DocumentViewer } from './DocumentViewer'; import { CommentFeed } from '@/components/Comment/CommentFeed'; import { TabType } from './WorkTabs'; -import { WorkHistoryDisplay } from './WorkHistoryDisplay'; import { Button } from '@/components/ui/Button'; import { ReviewStatusBanner } from '@/components/Bounty/ReviewStatusBanner'; @@ -135,17 +134,6 @@ export const WorkDocument = ({ work, metadata }: WorkDocumentProps) => { /> ); - case 'history': - return ( - <> -

Version History

- - - ); default: return null; } diff --git a/components/work/WorkHeader/WorkHeader.tsx b/components/work/WorkHeader/WorkHeader.tsx index 9995fc5a0..527c1a9d8 100644 --- a/components/work/WorkHeader/WorkHeader.tsx +++ b/components/work/WorkHeader/WorkHeader.tsx @@ -40,7 +40,7 @@ import { useWorkTab } from './WorkTabContext'; interface WorkHeaderProps { work: Work; metadata: WorkMetadata; - contentType?: 'paper' | 'post' | 'fund'; + contentType?: 'post' | 'fund'; updatesCount?: number; className?: string; eyebrow?: ReactNode; @@ -66,7 +66,7 @@ interface WorkHeaderProps { export function WorkHeader({ work, metadata, - contentType = 'paper', + contentType = 'post', updatesCount, className, eyebrow: eyebrowOverride, diff --git a/components/work/WorkHeader/WorkTabContext.tsx b/components/work/WorkHeader/WorkTabContext.tsx index af969c4b2..46e94c092 100644 --- a/components/work/WorkHeader/WorkTabContext.tsx +++ b/components/work/WorkHeader/WorkTabContext.tsx @@ -31,7 +31,6 @@ function getTabFromPath(path: string): TabType { if (path.includes('/applications')) return 'applications'; if (path.includes('/reviews')) return 'reviews'; if (path.includes('/bounties')) return 'bounties'; - if (path.includes('/history')) return 'history'; return 'paper'; } diff --git a/components/work/WorkHistoryDisplay.tsx b/components/work/WorkHistoryDisplay.tsx deleted file mode 100644 index 2f1aa2bde..000000000 --- a/components/work/WorkHistoryDisplay.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import Link from 'next/link'; -import { cn } from '@/utils/styles'; -import { ContentTypeBadge } from '@/components/ui/ContentTypeBadge'; -import { DocumentVersion } from '@/types/work'; -import { VersionOfRecordBadge } from '@/components/ui/VersionOfRecordBadge'; - -interface WorkHistoryDisplayProps { - versions: DocumentVersion[]; - currentPaperId: number; // ID of the work currently being viewed - slug: string; // Slug for navigation -} - -export const WorkHistoryDisplay = ({ versions, currentPaperId, slug }: WorkHistoryDisplayProps) => { - if (!versions || versions.length === 0) { - return

No version history available for this document.

; - } - - // Sort versions descending by published date, then by version number if equal - const sortedVersions = [...versions].sort((a, b) => { - const dateDiff = new Date(b.publishedDate).getTime() - new Date(a.publishedDate).getTime(); - if (dateDiff !== 0) return dateDiff; - // Compare version numbers - return b.version - a.version; - }); - - return ( -
-
- {sortedVersions.map((version) => { - const isViewingThisCard = version.paperId === currentPaperId; - const isLatest = version.isLatest; - const publishedDate = new Date(version.publishedDate); - - // Generate the URL for this version - const versionUrl = `/paper/${version.paperId}/${slug}`; - - return ( -
- -
-
- {/* Left */} -
-
-

- Version {version.version} -

- {version.isVersionOfRecord && } - {version.publicationStatus !== 'PUBLISHED' && ( - - )} -
- {version.message && ( -

- {version.message} -

- )} -
- - Published{' '} - {publishedDate.toLocaleDateString(undefined, { - year: 'numeric', - month: 'long', - day: 'numeric', - })} - -
-
-
-
- -
- ); - })} -
-
- ); -}; diff --git a/components/work/WorkTabs.tsx b/components/work/WorkTabs.tsx index 613354d2e..752ccc274 100644 --- a/components/work/WorkTabs.tsx +++ b/components/work/WorkTabs.tsx @@ -1,6 +1,6 @@ 'use client'; -import { FileText, Star, MessageCircle, History, Bell, MessageCircleQuestion } from 'lucide-react'; +import { FileText, Star, MessageCircle, Bell, MessageCircleQuestion } from 'lucide-react'; import { Work } from '@/types/work'; import { WorkMetadata } from '@/services/metadata.service'; import { useState, useEffect, useMemo } from 'react'; @@ -25,14 +25,13 @@ export type TabType = | 'reviews' | 'bounties' | 'conversation' - | 'history' | 'applications'; interface WorkTabsProps { work: Work; metadata: WorkMetadata; defaultTab?: TabType; - contentType?: 'paper' | 'post' | 'fund'; + contentType?: 'post' | 'fund'; onTabChange: (tab: TabType) => void; updatesCount?: number; } @@ -40,7 +39,7 @@ interface WorkTabsProps { export const WorkTabs = ({ work, metadata, - contentType = 'paper', + contentType = 'post', onTabChange, updatesCount = 0, }: WorkTabsProps) => { @@ -76,7 +75,6 @@ export const WorkTabs = ({ if (path.includes('/applications')) return 'applications'; if (path.includes('/reviews')) return 'reviews'; if (path.includes('/bounties')) return 'bounties'; - if (path.includes('/history') && hasResearchHubJournalVersions) return 'history'; return 'paper'; }; @@ -108,13 +106,11 @@ export const WorkTabs = ({ if (typeof window !== 'undefined') { const baseUrl = - contentType === 'paper' - ? `/paper/${work.id}/${work.slug}` - : contentType === 'fund' - ? `/proposal/${work.id}/${work.slug}` - : work.postType === 'QUESTION' - ? `/question/${work.id}/${work.slug}` - : `/post/${work.id}/${work.slug}`; + contentType === 'fund' + ? `/proposal/${work.id}/${work.slug}` + : work.postType === 'QUESTION' + ? `/question/${work.id}/${work.slug}` + : `/post/${work.id}/${work.slug}`; const tabUrlMap: Partial> = { updates: `${baseUrl}/updates`, @@ -122,7 +118,6 @@ export const WorkTabs = ({ reviews: `${baseUrl}/reviews`, applications: `${baseUrl}/applications`, bounties: `${baseUrl}/bounties`, - history: `${baseUrl}/history`, }; const newUrl = tabUrlMap[tab] || baseUrl; @@ -140,7 +135,6 @@ export const WorkTabs = ({ // Get the appropriate label for the main content tab const getMainTabLabel = () => { - if (contentType === 'paper') return 'Paper'; if (contentType === 'fund') return 'Project'; if (work.postType === 'QUESTION') return 'Question'; return 'Post'; @@ -272,32 +266,7 @@ export const WorkTabs = ({ }, ]; - const tabs = useMemo(() => { - if (hasResearchHubJournalVersions) { - return [ - ...baseTabs, - { - id: 'history', - label: ( -
- - History - - {work.versions?.length || 0} - -
- ), - }, - ]; - } - return baseTabs; - }, [baseTabs, hasResearchHubJournalVersions, activeTab, work.versions?.length]); + const tabs = baseTabs; return ( { const router = useRouter(); diff --git a/next.config.js b/next.config.js index 440895ad4..888123b03 100644 --- a/next.config.js +++ b/next.config.js @@ -166,6 +166,8 @@ const nextConfig = { '/report/32650/identifying-the-point-of-no-return-integrating-single-cell-transcriptomics-and-proteomics-to-map-irreversible-fibroblast-commitment-in-idiopathic-pulmonary-fibrosis-stage-1-registered-report', permanent: true, }, + // Paper pages are retired; anything else under /paper lands on the homepage. + ...['/paper', '/paper/:path*'].map((source) => ({ source, destination: '/', permanent: true })), ], // app/sitemap.ts uses generateSitemaps, which serves only /sitemap/.xml, // and Next still reserves app/sitemap.xml for it, so the index lives at diff --git a/package.json b/package.json index 30a540fa9..5e0b68986 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "start": "next start", "lint": "eslint .", "lint:fix": "eslint . --fix", - "lint:work-pages": "eslint \"app/{paper,post,proposal,question,report}/[[]id[]]/[[]slug[]]/**/page.tsx\"", + "lint:work-pages": "eslint \"app/{post,proposal,question,report}/[[]id[]]/[[]slug[]]/**/page.tsx\"", "format": "prettier --write --ignore-path .gitignore \"./**/*.{js,jsx,ts,tsx,json,css}\"", "type-check": "tsc --noEmit", "type-check:watch": "tsc --noEmit --watch", diff --git a/services/file.service.ts b/services/file.service.ts deleted file mode 100644 index 3366fa579..000000000 --- a/services/file.service.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { ApiClient } from './client'; - -export interface UploadFileResult { - objectKey: string; - absoluteUrl: string; - fileName: string; -} - -export class FileService { - private static readonly BASE_PATH = '/'; - - /** - * Uploads a file to the server - * - * @param file The file to upload - * @returns A promise that resolves to the upload result - */ - static async uploadFile(file: File): Promise { - try { - const formData = new FormData(); - formData.append('filename', file.name); - - const url = process.env.NEXT_PUBLIC_API_URL + this.BASE_PATH + 'paper/upload/'; - - // Get the authorization header using ApiClient helper - const headers = await ApiClient['getHeaders']('POST'); - - // Remove Content-Type header so browser can set correct multipart boundary - delete headers['Content-Type']; - - // Get the presigned URL - const response = await fetch(url, { - method: 'POST', - headers, - body: formData, - }); - - if (!response.ok) { - throw new Error(`Failed to get presigned URL: ${response.statusText}`); - } - - const data = await response.json(); - const { presigned_url: presignedUrl, object_key: objectKey, object_url: objectUrl } = data; - - if (!presignedUrl || !objectKey) { - throw new Error('Invalid response from server'); - } - - // Upload the file to S3 using the presigned URL - const uploadResponse = await fetch(presignedUrl, { - method: 'PUT', - body: file, - headers: { - 'Content-Type': file.type || 'application/octet-stream', - }, - }); - - if (!uploadResponse.ok) { - throw new Error(`Failed to upload file: ${uploadResponse.statusText}`); - } - - return { - objectKey: objectKey, - absoluteUrl: objectUrl, - fileName: file.name, - }; - } catch (error) { - console.error('File upload failed:', error); - throw error; - } - } -} diff --git a/services/paper.service.ts b/services/paper.service.ts index 3156d9b30..744704de2 100644 --- a/services/paper.service.ts +++ b/services/paper.service.ts @@ -2,32 +2,9 @@ import { ApiClient } from './client'; import { isDOI } from '@/utils/doi'; import { Work, transformPaper } from '@/types/work'; -interface CreateByOpenAlexIdResponse { - paper_id: number; -} - -export interface UpdatePaperMetadataPayload { - title?: string; - doi?: string; - publishedDate?: string; // Will be converted to YYYY-MM-DD format - hubs?: number[]; - license?: string; -} - -export interface UpdatePaperAbstractPayload { - abstract: string; -} - export class PaperService { private static readonly BASE_PATH = '/api/paper'; - // TODO: Remove this - static async createByOpenAlexId(openalexId: string) { - return ApiClient.post(`${this.BASE_PATH}/create_by_openalex_id/`, { - openalex_id: openalexId, - }); - } - static async get(identifier: string): Promise { let response; @@ -39,47 +16,4 @@ export class PaperService { return transformPaper(response); } - - /** - * Update paper metadata - */ - static async updateMetadata(paperId: number, payload: UpdatePaperMetadataPayload): Promise { - const apiPayload: any = {}; - - if (payload.title) { - apiPayload.title = payload.title; - apiPayload.paper_title = payload.title; // Backend expects both fields - } - - if (payload.doi) { - apiPayload.doi = payload.doi; - } - - if (payload.publishedDate) { - // Convert from any date format to YYYY-MM-DD - const date = new Date(payload.publishedDate); - if (!isNaN(date.getTime())) { - apiPayload.paper_publish_date = date.toISOString().split('T')[0]; - } - } - - if (payload.hubs) { - apiPayload.hubs = payload.hubs; - } - - if (payload.license) { - apiPayload.pdf_license = payload.license; - } - - const response = await ApiClient.patch(`${this.BASE_PATH}/${paperId}/`, apiPayload); - return transformPaper(response); - } - - /** - * Update paper abstract - */ - static async updateAbstract(paperId: number, payload: UpdatePaperAbstractPayload): Promise { - const response = await ApiClient.patch(`${this.BASE_PATH}/${paperId}/`, payload); - return transformPaper(response); - } } diff --git a/types/search.ts b/types/search.ts index 1ed8cb5c6..8943bff07 100644 --- a/types/search.ts +++ b/types/search.ts @@ -71,7 +71,6 @@ export const transformSearchSuggestion = createTransformer