From 049b2efe18117d42efa35c8f54d0c4f4c84231a0 Mon Sep 17 00:00:00 2001 From: Kobe Attias Date: Sun, 11 Oct 2026 17:39:02 -0400 Subject: [PATCH 1/3] Retire the topic pages and the topic browser Sixth chunk of the dead-code plan and the first page retirement: hub/topic pages are no longer part of the product. - /topic/:path* and /browse redirect permanently to the homepage, and the old /journal/:slug redirect, which pointed at /topic/:slug, now goes straight to / as well. - app/topic and app/browse go, with everything only they reached: TopicFeed and its sort/filter bar, TopicList, ManageTopics, the topic list skeleton, TopicSearch, ButtonGroup, useHub, the topic emoji table, the topic-following UI (FollowTopicButton, FollowContext and its provider, follow.service, types/follow), HubService's follow/unfollow/getHubBySlug/ getPrimaryHubs methods, the topic-page title rule and the browse rule in the top bar, the browse page metadata, buildTopicUrl, and the topic sitemap chunk (the remaining chunks are renumbered). - Topic badges stay visible but stop linking anywhere: HashtagBadge renders a plain span when it has no href, and FeedItemTopicBadges, the work page's Topics section and RelatedWorkCard no longer pass one. The journal badge keeps its link only for the ResearchHub Journal (/journal); other journals and preprint servers show as plain badges too. - Search never requested the hub index outside TopicSearch, so the hub suggestion type, its transformer case and the hub branches in the search handlers were unreachable and go with it. Co-Authored-By: Claude Fable 5.1 --- app/browse/components/BrowsePageClient.tsx | 12 - app/browse/components/BrowsePageHeader.tsx | 74 ----- app/browse/loading.tsx | 21 -- app/browse/page.tsx | 43 --- app/layouts/topbar/pageRoutes.tsx | 51 +--- app/sitemap.ts | 18 +- app/topic/[slug]/latest/page.tsx | 9 - app/topic/[slug]/layout.tsx | 28 -- app/topic/[slug]/page.tsx | 7 - app/topic/[slug]/popular/page.tsx | 9 - .../Feed/DocumentFeedSortAndFilters.tsx | 31 -- components/Feed/FeedItemTopicBadges.tsx | 19 +- components/Feed/TopicFeed.tsx | 74 ----- components/Paper/RelatedWorkCard.tsx | 3 +- components/Search/Search.tsx | 7 - components/Search/SearchModal.tsx | 6 - components/Search/SearchSuggestions.tsx | 47 +-- components/Search/TopicSearch.tsx | 124 -------- components/Topic/ManageTopics.tsx | 216 -------------- components/Topic/TopicEmojis.ts | 277 ------------------ components/Topic/TopicList.tsx | 132 --------- components/providers/ClientProviders.tsx | 3 +- components/skeletons/TopicListSkeleton.tsx | 50 ---- components/ui/ButtonGroup.tsx | 117 -------- components/ui/FollowTopicButton.tsx | 82 ------ components/ui/badges/HashtagBadge.tsx | 28 +- components/work/components/JournalSection.tsx | 75 ++--- components/work/components/TopicsSection.tsx | 6 +- contexts/FollowContext.tsx | 220 -------------- hooks/useFeed.ts | 2 +- hooks/useHub.ts | 31 -- lib/metadata-helpers.ts | 18 -- next.config.js | 13 +- services/follow.service.ts | 50 ---- services/hub.service.ts | 65 +--- types/follow.ts | 44 --- types/search.ts | 47 +-- utils/stringUtils.ts | 9 - utils/url.ts | 7 - 39 files changed, 90 insertions(+), 1985 deletions(-) delete mode 100644 app/browse/components/BrowsePageClient.tsx delete mode 100644 app/browse/components/BrowsePageHeader.tsx delete mode 100644 app/browse/loading.tsx delete mode 100644 app/browse/page.tsx delete mode 100644 app/topic/[slug]/latest/page.tsx delete mode 100644 app/topic/[slug]/layout.tsx delete mode 100644 app/topic/[slug]/page.tsx delete mode 100644 app/topic/[slug]/popular/page.tsx delete mode 100644 components/Feed/DocumentFeedSortAndFilters.tsx delete mode 100644 components/Feed/TopicFeed.tsx delete mode 100644 components/Search/TopicSearch.tsx delete mode 100644 components/Topic/ManageTopics.tsx delete mode 100644 components/Topic/TopicEmojis.ts delete mode 100644 components/Topic/TopicList.tsx delete mode 100644 components/skeletons/TopicListSkeleton.tsx delete mode 100644 components/ui/ButtonGroup.tsx delete mode 100644 components/ui/FollowTopicButton.tsx delete mode 100644 contexts/FollowContext.tsx delete mode 100644 hooks/useHub.ts delete mode 100644 services/follow.service.ts delete mode 100644 types/follow.ts diff --git a/app/browse/components/BrowsePageClient.tsx b/app/browse/components/BrowsePageClient.tsx deleted file mode 100644 index f3d231ee1..000000000 --- a/app/browse/components/BrowsePageClient.tsx +++ /dev/null @@ -1,12 +0,0 @@ -'use client'; - -import { Topic } from '@/types/topic'; -import { ManageTopics } from '@/components/Topic/ManageTopics'; - -interface BrowsePageClientProps { - initialTopics: Topic[]; -} - -export function BrowsePageClient({ initialTopics }: BrowsePageClientProps) { - return ; -} diff --git a/app/browse/components/BrowsePageHeader.tsx b/app/browse/components/BrowsePageHeader.tsx deleted file mode 100644 index a292da021..000000000 --- a/app/browse/components/BrowsePageHeader.tsx +++ /dev/null @@ -1,74 +0,0 @@ -'use client'; - -import { TopicSearch } from '@/components/Search/TopicSearch'; -import { SearchSuggestion } from '@/types/search'; -import { ButtonGroup } from '@/components/ui/ButtonGroup'; -import { MainPageHeader } from '@/components/ui/MainPageHeader'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faGrid3 as faGrid3Light } from '@fortawesome/pro-light-svg-icons'; - -interface BrowsePageHeaderProps { - activeTab: 'all' | 'following'; - onTabChange: (tab: 'all' | 'following') => void; - onSearch?: (query: string) => void; - onSearchResults?: (results: SearchSuggestion[]) => void; - onSearching?: (isSearching: boolean) => void; - followingCount?: number; - showTitle?: boolean; -} - -export function BrowsePageHeader({ - activeTab, - onTabChange, - onSearch, - onSearchResults, - onSearching, - followingCount = 0, - showTitle = true, -}: BrowsePageHeaderProps) { - return ( -
-
- {showTitle && ( -
-

Topics

- } - title="Browse" - subtitle="Discover and follow research topics" - showTitle={false} - /> -
- )} - -
-
- -
- -
- onTabChange(value as 'all' | 'following')} - options={[ - { value: 'all', label: 'All Topics' }, - { value: 'following', label: 'Following', badge: followingCount }, - ]} - /> -
-
-
-
- ); -} diff --git a/app/browse/loading.tsx b/app/browse/loading.tsx deleted file mode 100644 index 2ec5dedd8..000000000 --- a/app/browse/loading.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { PageLayout } from '@/app/layouts/PageLayout'; -import { TopicListSkeleton } from '@/components/skeletons/TopicListSkeleton'; - -export default function BrowseLoading() { - return ( - -
- {/* BrowsePageHeader placeholder */} -
-
-
-
- - {/* Search bar placeholder */} -
- - -
- - ); -} diff --git a/app/browse/page.tsx b/app/browse/page.tsx deleted file mode 100644 index cb2142163..000000000 --- a/app/browse/page.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Metadata } from 'next'; -import { Topic } from '@/types/topic'; -import { HubService } from '@/services/hub.service'; -import { PageLayout } from '@/app/layouts/PageLayout'; -import { BrowsePageClient } from './components/BrowsePageClient'; -import { getBrowsePageMetadata } from '@/lib/metadata-helpers'; - -export async function generateMetadata(): Promise { - return getBrowsePageMetadata(); -} - -// This tells Next.js to statically generate this page at build time -export const revalidate = 3600; // Revalidate every hour (3600 seconds) - -async function getTopics() { - try { - const topics = await HubService.getPrimaryHubs(); - return { topics, error: null }; - } catch (error) { - console.error('Failed to fetch topics:', error); - return { topics: [], error: 'Failed to load topics' }; - } -} - -export default async function BrowsePage() { - const { topics, error } = await getTopics(); - - if (error) { - return ( - -
-
{error}
-
-
- ); - } - - return ( - - - - ); -} diff --git a/app/layouts/topbar/pageRoutes.tsx b/app/layouts/topbar/pageRoutes.tsx index 443d70f79..dee2873cb 100644 --- a/app/layouts/topbar/pageRoutes.tsx +++ b/app/layouts/topbar/pageRoutes.tsx @@ -3,14 +3,9 @@ import { faHouse as faHouseLight, faBookmark as faBookmarkLight, faCommentsQuestion, - faGrid3 as faGrid3Light, } from '@fortawesome/pro-light-svg-icons'; -import { ChartNoAxesColumnIncreasing, Shield, Hash, Users, Activity, Settings } from 'lucide-react'; -import Image from 'next/image'; +import { ChartNoAxesColumnIncreasing, Shield, Users, Activity, Settings } from 'lucide-react'; import { Icon } from '@/components/ui/icons'; -import { getTopicEmoji } from '@/components/Topic/TopicEmojis'; -import { toTitleCase } from '@/utils/stringUtils'; -import { getSourceLogo, getPreprintDisplayName } from '@/utils/preprintUtil'; import { HOME_TAB_PATHS, isHomeTabPath } from '@/hooks/useFundTabs'; export interface PageInfo { @@ -25,7 +20,6 @@ export const ROOT_NAVIGATION_PATHS = new Set([ '/peer-review', '/journal', '/notebook', - '/browse', '/leaderboard', '/lists', '/settings', @@ -49,13 +43,6 @@ const ROUTE_RULES: RouteRule[] = [ icon: , }), }, - { - match: (p) => p === '/browse', - getInfo: () => ({ - title: 'Browse', - icon: , - }), - }, { match: (p) => p === '/my-funding', getInfo: () => ({ @@ -204,42 +191,6 @@ const ROUTE_RULES: RouteRule[] = [ icon: , }), }, - { - match: (p) => p.startsWith('/topic/'), - getInfo: (pathname) => { - const topicSlug = pathname.split('/topic/')[1]?.split('/')[0]; - if (!topicSlug) return { title: '' }; - - const preprintLogo = getSourceLogo(topicSlug); - if (preprintLogo && preprintLogo !== 'rhJournal2') { - return { - title: '', - icon: ( - {getPreprintDisplayName(topicSlug)} - ), - }; - } - - const emoji = getTopicEmoji(topicSlug); - const topicName = toTitleCase(topicSlug.replace(/-/g, ' ')); - - return { - title: topicName, - icon: emoji ? ( - {emoji} - ) : ( - - ), - }; - }, - }, ]; export const getPageInfo = (pathname: string, searchParams?: URLSearchParams): PageInfo | null => { diff --git a/app/sitemap.ts b/app/sitemap.ts index a9616b462..3f6ecf895 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -71,7 +71,6 @@ const STATIC_ROUTES: MetadataRoute.Sitemap = [ { url: `${SITE_URL}/fund/proposals`, changeFrequency: 'daily', priority: 0.8 }, { url: `${SITE_URL}/peer-review`, changeFrequency: 'daily', priority: 0.8 }, { url: `${SITE_URL}/journal`, changeFrequency: 'daily', priority: 0.7 }, - { url: `${SITE_URL}/browse`, changeFrequency: 'daily', priority: 0.6 }, { url: `${SITE_URL}/lists`, changeFrequency: 'daily', priority: 0.5 }, { url: `${SITE_URL}/leaderboard`, changeFrequency: 'daily', priority: 0.6 }, { url: `${SITE_URL}/leaderboard/reviewers`, changeFrequency: 'daily', priority: 0.5 }, @@ -83,7 +82,7 @@ const STATIC_ROUTES: MetadataRoute.Sitemap = [ ]; export async function generateSitemaps() { - return [{ id: 0 }, { id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }]; + return [{ id: 0 }, { id: 1 }, { id: 2 }, { id: 3 }]; } export default async function sitemap({ @@ -100,23 +99,12 @@ export default async function sitemap({ (item) => item.content_object ); - if (sitemapId === 2) { - const hubs = await fetchAllPages<{ slug: string }>('/api/hub/?page_size=1000'); - return dedupe( - hubs.map((hub) => ({ - url: `${SITE_URL}/topic/${hub.slug}`, - changeFrequency: 'daily' as const, - priority: 0.6, - })) - ); - } - - if (sitemapId === 3) + if (sitemapId === 2) return buildSlugSitemap( '/api/researchhubpost/?document_type=PREREGISTRATION&page_size=1000', '/proposal/' ); - if (sitemapId === 4) + if (sitemapId === 3) return buildSlugSitemap('/api/researchhubpost/?document_type=GRANT&page_size=100', '/grant/'); return []; diff --git a/app/topic/[slug]/latest/page.tsx b/app/topic/[slug]/latest/page.tsx deleted file mode 100644 index e513126e8..000000000 --- a/app/topic/[slug]/latest/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -'use client'; - -import { TopicFeed } from '@/components/Feed/TopicFeed'; -import { FeedTab } from '@/hooks/useFeed'; - -export default function TopicLatestPage() { - const defaultTab: FeedTab = 'latest'; - return ; -} diff --git a/app/topic/[slug]/layout.tsx b/app/topic/[slug]/layout.tsx deleted file mode 100644 index d5702edb6..000000000 --- a/app/topic/[slug]/layout.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Metadata } from 'next'; -import { HubService } from '@/services/hub.service'; -import { buildOpenGraphMetadata } from '@/lib/metadata'; - -interface GenerateMetadataProps { - params: Promise<{ slug: string }>; -} - -export async function generateMetadata({ params }: GenerateMetadataProps): Promise { - const { slug } = await params; - try { - const topic = await HubService.getHubBySlug(slug); - return buildOpenGraphMetadata({ - title: topic.name, - description: - topic.description || - `Explore ${topic.name} research papers and discussions on ResearchHub.`, - url: `/topic/${slug}`, - image: topic.imageUrl, - }); - } catch { - return {}; - } -} - -export default function TopicLayout({ children }: Readonly<{ children: React.ReactNode }>) { - return children; -} diff --git a/app/topic/[slug]/page.tsx b/app/topic/[slug]/page.tsx deleted file mode 100644 index 77e763503..000000000 --- a/app/topic/[slug]/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { TopicFeed } from '@/components/Feed/TopicFeed'; -import { FeedTab } from '@/hooks/useFeed'; - -export default function TopicFeedPage() { - const defaultTab: FeedTab = 'popular'; - return ; -} diff --git a/app/topic/[slug]/popular/page.tsx b/app/topic/[slug]/popular/page.tsx deleted file mode 100644 index 968ae0cbb..000000000 --- a/app/topic/[slug]/popular/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -'use client'; - -import { TopicFeed } from '@/components/Feed/TopicFeed'; -import { FeedTab } from '@/hooks/useFeed'; - -export default function TopicPopularPage() { - const defaultTab: FeedTab = 'popular'; - return ; -} diff --git a/components/Feed/DocumentFeedSortAndFilters.tsx b/components/Feed/DocumentFeedSortAndFilters.tsx deleted file mode 100644 index d0542cd26..000000000 --- a/components/Feed/DocumentFeedSortAndFilters.tsx +++ /dev/null @@ -1,31 +0,0 @@ -'use client'; - -import { FC } from 'react'; -import { cn } from '@/utils/styles'; - -interface HomeFeedVariant { - variant: 'home'; -} - -interface TopicFeedVariant { - variant: 'topic'; - topicName: string; -} - -type DocumentFeedSortAndFiltersProps = { - className?: string; -} & (HomeFeedVariant | TopicFeedVariant); - -export const DocumentFeedSortAndFilters: FC = (props) => { - const { className } = props; - - return ( -
- {props.variant === 'home' ? ( - Your personalized research briefing. - ) : ( - Your {props.topicName} Research Briefing. - )} -
- ); -}; diff --git a/components/Feed/FeedItemTopicBadges.tsx b/components/Feed/FeedItemTopicBadges.tsx index 4b98e605c..857bba64d 100644 --- a/components/Feed/FeedItemTopicBadges.tsx +++ b/components/Feed/FeedItemTopicBadges.tsx @@ -63,12 +63,8 @@ export const FeedItemTopicBadges: FC = ({ if (category?.slug || subcategory?.slug) { return ( <> - {category?.slug && ( - - )} - {subcategory?.slug && ( - - )} + {category?.slug && } + {subcategory?.slug && } ); } @@ -77,11 +73,7 @@ export const FeedItemTopicBadges: FC = ({ return ( <> {filteredTopics.map((topic) => ( - + ))} ); @@ -94,10 +86,7 @@ export const FeedItemTopicBadges: FC = ({
{journalLogo && journal?.slug && journal?.name && ( - + )} {renderBadges()}
diff --git a/components/Feed/TopicFeed.tsx b/components/Feed/TopicFeed.tsx deleted file mode 100644 index a86deadf4..000000000 --- a/components/Feed/TopicFeed.tsx +++ /dev/null @@ -1,74 +0,0 @@ -'use client'; - -import { FC, useState, useEffect } from 'react'; -import { useParams, notFound } from 'next/navigation'; -import { PageLayout } from '@/app/layouts/PageLayout'; -import { useFeed, FeedTab } from '@/hooks/useFeed'; -import { useHub } from '@/hooks/useHub'; -import { FeedContent } from '@/components/Feed/FeedContent'; -import { DocumentFeedSortAndFilters } from '@/components/Feed/DocumentFeedSortAndFilters'; - -interface TopicFeedProps { - defaultTab: FeedTab; -} - -const getTopicOrdering = (tab: FeedTab): string | undefined => { - if (tab === 'popular') return 'hot_score_v2'; - if (tab === 'latest') return 'latest'; - return undefined; -}; - -export const TopicFeed: FC = ({ defaultTab }) => { - const params = useParams(); - const slug = params?.slug; - const decodedSlug = typeof slug === 'string' ? decodeURIComponent(slug) : null; - const [isNavigating, setIsNavigating] = useState(false); - - const { hub, isLoading: isHubLoading, error: hubError } = useHub(decodedSlug); - - const { - entries, - isLoading: isFeedLoading, - hasMore, - loadMore, - restoredScrollPosition, - page, - lastClickedEntryId, - } = useFeed(defaultTab, { - hubSlug: decodedSlug || '', - ordering: getTopicOrdering(defaultTab), - }); - - useEffect(() => { - setIsNavigating(false); - }, [defaultTab]); - - if (!isHubLoading && (hubError || !hub)) { - notFound(); - } - - const isLoading = isHubLoading || isFeedLoading || isNavigating; - - return ( - - {hub &&

{hub.name}

} - : undefined - } - ordering={getTopicOrdering(defaultTab)} - restoredScrollPosition={restoredScrollPosition} - page={page} - lastClickedEntryId={lastClickedEntryId ?? undefined} - /> -
- ); -}; diff --git a/components/Paper/RelatedWorkCard.tsx b/components/Paper/RelatedWorkCard.tsx index 1a412297f..4bab0757a 100644 --- a/components/Paper/RelatedWorkCard.tsx +++ b/components/Paper/RelatedWorkCard.tsx @@ -199,10 +199,9 @@ export const RelatedWorkCard = ({ // On peer review page without custom handlers, use ClickContext for filtering triggerEvent({ type: 'topic', payload: topic }); } - // Otherwise, let the HashtagBadge handle navigation }} > - +
); })} diff --git a/components/Search/Search.tsx b/components/Search/Search.tsx index 4c547ad51..51f7e4c39 100644 --- a/components/Search/Search.tsx +++ b/components/Search/Search.tsx @@ -81,13 +81,6 @@ export function Search({ } } else if (suggestion.entityType === 'user' || suggestion.entityType === 'author') { navigateToAuthorProfile(suggestion.authorProfile.id); - } else if (suggestion.entityType === 'hub') { - // Handle topic navigation - if ('slug' in suggestion && suggestion.slug) { - router.push(`/topic/${suggestion.slug}`); - } else { - router.push(`/topic/${suggestion.id}`); - } } else if (suggestion.entityType === 'post') { router.push(`/post/${suggestion.id}`); } else { diff --git a/components/Search/SearchModal.tsx b/components/Search/SearchModal.tsx index 29d24473e..be8b1fa3e 100644 --- a/components/Search/SearchModal.tsx +++ b/components/Search/SearchModal.tsx @@ -61,12 +61,6 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) { } } else if (suggestion.entityType === 'user' || suggestion.entityType === 'author') { navigateToAuthorProfile(suggestion.authorProfile.id); - } else if (suggestion.entityType === 'hub') { - if ('slug' in suggestion && suggestion.slug) { - router.push(`/topic/${suggestion.slug}`); - } else { - router.push(`/topic/${suggestion.id}`); - } } else if (suggestion.entityType === 'post') { router.push(`/post/${suggestion.id}`); } diff --git a/components/Search/SearchSuggestions.tsx b/components/Search/SearchSuggestions.tsx index ad1eda2d5..2b5ab27dc 100644 --- a/components/Search/SearchSuggestions.tsx +++ b/components/Search/SearchSuggestions.tsx @@ -1,9 +1,8 @@ import { useState } from 'react'; -import { FileText, History, Search, X, ArrowRight, User, Hash, HelpCircle } from 'lucide-react'; +import { FileText, History, Search, X, ArrowRight, User, HelpCircle } from 'lucide-react'; import Icon from '@/components/ui/icons/Icon'; import { cn } from '@/utils/styles'; import { SearchSuggestion } from '@/types/search'; -import { FollowTopicButton } from '@/components/ui/FollowTopicButton'; import { Avatar } from '@/components/ui/Avatar'; import { VerifiedBadge } from '@/components/ui/VerifiedBadge'; @@ -54,13 +53,7 @@ export function SearchSuggestions({ return null; } - const handleSelect = (suggestion: SearchSuggestion, e: React.MouseEvent) => { - // Don't select if we clicked on a FollowTopicButton - if ((e.target as HTMLElement).closest('.follow-topic-btn')) { - return; - } - - // Call the onSelect handler + const handleSelect = (suggestion: SearchSuggestion) => { if (onSelect) { onSelect(suggestion); } @@ -86,7 +79,6 @@ export function SearchSuggestions({ const isPaperSuggestion = suggestion.entityType === 'paper'; const isUserSuggestion = suggestion.entityType === 'user' || suggestion.entityType === 'author'; - const isTopicSuggestion = suggestion.entityType === 'hub'; const isPostSuggestion = suggestion.entityType === 'post'; const isGrantPost = isPostSuggestion && suggestion?.documentType === 'GRANT'; const isProposalRequest = isPostSuggestion && suggestion?.documentType === 'PREREGISTRATION'; @@ -131,13 +123,6 @@ export function SearchSuggestions({ return false; }; - const safeGetPaperCount = () => { - if (isTopicSuggestion && 'paperCount' in suggestion) { - return suggestion.paperCount ? `${suggestion.paperCount} papers` : 'Research Topic'; - } - return 'Research Topic'; - }; - // Get the smaller icon variant for dropdown mode const getSmallIcon = () => { if (suggestion.isRecent) @@ -158,8 +143,6 @@ export function SearchSuggestions({ ); } - if (isTopicSuggestion) - return ; if (isGrantPost || isProposalRequest) return (
@@ -176,7 +159,7 @@ export function SearchSuggestions({
  • handleSelect(suggestion, e)} + onClick={() => handleSelect(suggestion)} >
    {getSmallIcon()}
    @@ -195,19 +178,7 @@ export function SearchSuggestions({ {isUserSuggestion && (

    {safeGetHeadline()}

    )} - {isTopicSuggestion && ( -

    - Topic โ€ข {safeGetPaperCount()} -

    - )}
    - {isTopicSuggestion && suggestion.id && ( - - )}
  • ); @@ -218,7 +189,7 @@ export function SearchSuggestions({
  • handleSelect(suggestion, e)} + onClick={() => handleSelect(suggestion)} >
    @@ -239,18 +210,8 @@ export function SearchSuggestions({ {isUserSuggestion && (
    {safeGetHeadline()}
    )} - {isTopicSuggestion && ( -
    Topic โ€ข {safeGetPaperCount()}
    - )}
    - {isTopicSuggestion && suggestion.id && ( - - )}
  • ); diff --git a/components/Search/TopicSearch.tsx b/components/Search/TopicSearch.tsx deleted file mode 100644 index d2fdb766a..000000000 --- a/components/Search/TopicSearch.tsx +++ /dev/null @@ -1,124 +0,0 @@ -'use client'; - -import { useState, useRef, useEffect } from 'react'; -import { Search as SearchIcon, X } from 'lucide-react'; -import { cn } from '@/utils/styles'; -import { SearchService } from '@/services/search.service'; -import { SearchSuggestion } from '@/types/search'; - -interface TopicSearchProps { - onSearch?: (query: string) => void; - onSearchResults?: (results: SearchSuggestion[]) => void; - onSearching?: (isSearching: boolean) => void; - className?: string; - placeholder?: string; -} - -export function TopicSearch({ - onSearch, - onSearchResults, - onSearching, - className, - placeholder = 'Search topics...', -}: TopicSearchProps) { - const [query, setQuery] = useState(''); - const [searchResults, setSearchResults] = useState([]); - const [isSearching, setIsSearching] = useState(false); - const [isFocused, setIsFocused] = useState(false); - const containerRef = useRef(null); - const searchTimeoutRef = useRef(undefined); - - // Handle search with debouncing - useEffect(() => { - const performSearch = async () => { - if (query.trim()) { - try { - setIsSearching(true); - onSearching?.(true); - // Search only in the 'hub' index for topics - const results = await SearchService.getSuggestions(query, 'hub'); - setSearchResults(results); - onSearchResults?.(results); - } catch (error) { - console.error('Error searching topics:', error); - setSearchResults([]); - onSearchResults?.([]); - } finally { - setIsSearching(false); - onSearching?.(false); - } - } else { - setSearchResults([]); - onSearchResults?.([]); - } - }; - - // Clear previous timeout - if (searchTimeoutRef.current) { - clearTimeout(searchTimeoutRef.current); - } - - // Debounce search - searchTimeoutRef.current = setTimeout(performSearch, 300); - - // Notify parent of query change - onSearch?.(query); - - return () => { - if (searchTimeoutRef.current) { - clearTimeout(searchTimeoutRef.current); - } - }; - }, [query]); // Only re-run when query changes - - // Handle clicks outside - useEffect(() => { - function handleClickOutside(event: MouseEvent) { - if (containerRef.current && !containerRef.current.contains(event.target as Node)) { - setIsFocused(false); - } - } - - if (isFocused) { - document.addEventListener('mousedown', handleClickOutside); - } - - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [isFocused]); - - const handleClearSearch = () => { - setQuery(''); - setSearchResults([]); - onSearchResults?.([]); - }; - - // Results are now handled by parent component - - return ( -
    -
    - - setQuery(e.target.value)} - onFocus={() => setIsFocused(true)} - /> - {query && ( - - )} -
    - - {/* Search results are handled by parent component */} -
    - ); -} diff --git a/components/Topic/ManageTopics.tsx b/components/Topic/ManageTopics.tsx deleted file mode 100644 index 3d40ddb73..000000000 --- a/components/Topic/ManageTopics.tsx +++ /dev/null @@ -1,216 +0,0 @@ -'use client'; - -import { useState, useCallback, useEffect, useMemo } from 'react'; -import { Topic } from '@/types/topic'; -import { BrowsePageHeader } from '@/app/browse/components/BrowsePageHeader'; -import { useFollowContext } from '@/contexts/FollowContext'; -import { SearchSuggestion } from '@/types/search'; -import { TopicList } from './TopicList'; -import { TopicListSkeleton } from '@/components/skeletons/TopicListSkeleton'; -import { TopicSearch } from '@/components/Search/TopicSearch'; - -interface ManageTopicsProps { - initialTopics?: Topic[]; - showFollowingTab?: boolean; - defaultTab?: 'all' | 'following'; - className?: string; - topicListVariant?: 'default' | 'compact'; - showTitle?: boolean; -} - -export function ManageTopics({ - initialTopics = [], - showFollowingTab = true, - defaultTab = 'all', - className = '', - topicListVariant = 'default', - showTitle = true, -}: ManageTopicsProps) { - const [activeTab, setActiveTab] = useState<'all' | 'following'>(defaultTab); - const [searchQuery, setSearchQuery] = useState(''); - const [searchResults, setSearchResults] = useState([]); - const [isSearching, setIsSearching] = useState(false); - const [hasSearched, setHasSearched] = useState(false); - const { followedTopics, followedTopicObjects, getFollowedTopicObjects } = useFollowContext(); - - useEffect(() => { - const shouldFetchFollowedTopics = activeTab === 'following' && showFollowingTab; - if (shouldFetchFollowedTopics) { - getFollowedTopicObjects(); - } - }, [activeTab, getFollowedTopicObjects, showFollowingTab]); - - const handleSearch = useCallback((query: string) => { - setSearchQuery(query); - if (!query) { - setSearchResults([]); - setIsSearching(false); - setHasSearched(false); - } else { - // Reset hasSearched when starting a new search - setHasSearched(false); - } - }, []); - - const handleSearchResults = useCallback( - (results: SearchSuggestion[]) => { - const isAllTab = activeTab === 'all'; - if (isAllTab) { - setSearchResults(results); - } - }, - [activeTab] - ); - - const handleSearching = useCallback( - (searching: boolean) => { - const isAllTab = activeTab === 'all'; - if (isAllTab) { - setIsSearching(searching); - if (!searching) { - // Mark that we've completed a search - setHasSearched(true); - } - } - }, - [activeTab] - ); - - const handleTabChange = (tab: 'all' | 'following') => { - setActiveTab(tab); - setSearchQuery(''); - setSearchResults([]); - setIsSearching(false); - setHasSearched(false); - }; - - // Sort followed topics by createdDate (most recent first) - const sortedFollowedTopics = useMemo(() => { - return [...followedTopicObjects] - .sort((a, b) => { - // Sort by createdDate in descending order (most recent first) - return new Date(b.createdDate).getTime() - new Date(a.createdDate).getTime(); - }) - .map((obj) => obj.data as Topic); - }, [followedTopicObjects]); - - const getDisplayTopics = () => { - const isFollowingTab = activeTab === 'following'; - const hasSearchQuery = Boolean(searchQuery); - - if (isFollowingTab) { - if (hasSearchQuery) { - const query = searchQuery.toLowerCase(); - return sortedFollowedTopics.filter( - (topic) => - topic.name.toLowerCase().includes(query) || - (topic.description && topic.description.toLowerCase().includes(query)) - ); - } - return sortedFollowedTopics; - } - - return null; - }; - - const displayTopics = getDisplayTopics(); - - // View state booleans - const isAllTab = activeTab === 'all'; - const hasSearchQuery = Boolean(searchQuery); - const isSearchingTopics = isSearching || (hasSearchQuery && !hasSearched); - const hasNoSearchResults = hasSearched && searchResults.length === 0 && hasSearchQuery; - const hasSearchResults = searchResults.length > 0 && hasSearchQuery; - const showAllTopics = !hasSearchQuery && isAllTab; - const hasFollowedTopics = displayTopics && displayTopics.length > 0; - const showFollowedSearchCount = hasSearchQuery && hasFollowedTopics && !isSearching; - const noFollowedTopicsMessage = hasSearchQuery - ? `No followed topics found matching "${searchQuery}"` - : 'You are not following any topics yet'; - - return ( -
    - {showFollowingTab ? ( - - ) : ( -
    - -
    - )} - -
    - {isAllTab ? ( - <> - {hasSearchQuery && ( - <> - {isSearchingTopics && } - - {hasNoSearchResults && ( -
    - No topics found matching "{searchQuery}" -
    - )} - - {hasSearchResults && ( - <> -
    -

    - {searchResults.length} topic{searchResults.length !== 1 ? 's' : ''} found -

    -
    - - - ({ - id: result.id || 0, - name: result.displayName || '', - slug: 'slug' in result && result.slug ? result.slug : `${result.id}`, - description: 'description' in result ? result.description : undefined, - }) as Topic - )} - variant={topicListVariant} - /> - - )} - - )} - - {showAllTopics && } - - ) : ( - <> - {hasFollowedTopics ? ( - <> - {showFollowedSearchCount && ( -
    -

    - {displayTopics.length} topic{displayTopics.length !== 1 ? 's' : ''} found -

    -
    - )} - - - ) : ( -
    {noFollowedTopicsMessage}
    - )} - - )} -
    -
    - ); -} diff --git a/components/Topic/TopicEmojis.ts b/components/Topic/TopicEmojis.ts deleted file mode 100644 index 9c0ccb616..000000000 --- a/components/Topic/TopicEmojis.ts +++ /dev/null @@ -1,277 +0,0 @@ -// Topic emoji mapping for fallbacks when API doesn't provide icons -export const TOPIC_EMOJIS: Record = { - // Primary hubs (from categories) - biology: '๐Ÿงฌ', - chemistry: '๐Ÿงช', - 'computer-science': '๐Ÿ’ป', - physics: 'โš›๏ธ', - economics: '๐Ÿ“ˆ', - medicine: '๐Ÿฅ', - mathematics: '๐Ÿ”ข', - engineering: 'โš™๏ธ', - astrophysics: '๐ŸŒŒ', - 'social-sciences': '๐Ÿ‘ฅ', - 'nonlinear-sciences': 'โˆž', - biochemistry: '๐Ÿงซ', - statistics: '๐Ÿ“Š', - bioengineering: '๐Ÿ”ฌ', - electrochemistry: '๐Ÿ”‹', - // Biology hubs - paleontology: '๐Ÿฆ–', - pathology: '๐Ÿ”ฌ', - 'evolutionary-biology': '๐Ÿงฌ', - neuroscience: '๐Ÿง ', - bioinformatics: '๐Ÿ’ป', - 'systems-biology': '๐Ÿ”—', - genomics: '๐Ÿงฌ', - zoology: '๐Ÿฆ', - 'synthetic-biology': '๐Ÿงช', - 'developmental-biology': '๐ŸŒฑ', - ecology: '๐ŸŒฟ', - pharmacology: '๐Ÿ’Š', - 'cell-biology': '๐Ÿฆ ', - biophysics: 'โš›๏ธ', - 'molecular-biology': '๐Ÿงฌ', - physiology: 'โค๏ธ', - microbiology: '๐Ÿฆ ', - 'cancer-biology': '๐ŸŽ—๏ธ', - 'plant-biology': '๐ŸŒฑ', - 'genetics-and-genomics': '๐Ÿงฌ', - 'biological-physics': '๐Ÿ”ฌ', - 'quantitative-biology': '๐Ÿ“Š', - biomolecules: '๐Ÿงช', - 'molecular-networks': '๐Ÿ”—', - 'other-quantitative-biology': '๐Ÿ“ˆ', - 'populations-and-evolution': '๐ŸŒ', - 'quantitative-methods': '๐Ÿ“Š', - 'subcellular-processes': '๐Ÿ”ฌ', - 'tissues-and-organs': '๐Ÿซ€', - immunology: '๐Ÿ›ก๏ธ', - // Chemistry hubs - 'organic-chemistry': '๐Ÿงช', - 'computational-chemistry': '๐Ÿ’ป', - 'physical-chemistry': 'โš›๏ธ', - 'materials-science': '๐Ÿ”ง', - 'chemical-physics': 'โš›๏ธ', - 'environmental-chemistry': '๐ŸŒฟ', - 'chemical-engineering': 'โš™๏ธ', - 'nuclear-chemistry': 'โ˜ข๏ธ', - 'chemical-biology': '๐Ÿงฌ', - 'supramolecular-chemistry': '๐Ÿ”—', - catalysis: 'โšก', - 'inorganic-chemistry': '๐Ÿงช', - 'materials-chemistry': '๐Ÿงฑ', - astrochemistry: '๐ŸŒŒ', - 'chemical-education': '๐Ÿ“š', - 'food-chemistry': '๐Ÿฝ๏ธ', - geochemistry: '๐ŸŒ', - 'medicinal-chemistry': '๐Ÿ’Š', - nanochemistry: '๐Ÿ”ฌ', - 'organometallic-chemistry': 'โš›๏ธ', - 'surface-chemistry': '๐Ÿงฒ', - 'analytical-chemistry': '๐Ÿ”', - 'analytical-biochemistry': '๐Ÿ”ฌ', - // Computer Science hubs - 'machine-learning': '๐Ÿค–', - 'software-engineering': '๐Ÿ’ป', - 'computer-vision': '๐Ÿ‘๏ธ', - graphics: '๐ŸŽจ', - 'information-retrieval': '๐Ÿ”', - 'human-computer-interaction': '๐Ÿ‘ค', - 'distributed-computing': '๐ŸŒ', - 'natural-language-processing': '๐Ÿ—ฃ๏ธ', - 'game-theory': '๐ŸŽฎ', - 'discrete-mathematics': '๐Ÿ”ข', - multimedia: '๐Ÿ“น', - 'artificial-intelligence': '๐Ÿค–', - 'computational-complexity': '๐Ÿงฉ', - 'computational-geometry': '๐Ÿ“', - 'cryptography-and-security': '๐Ÿ”', - 'data-structures-and-algorithms': '๐Ÿ”ค', - databases: '๐Ÿ—„๏ธ', - 'formal-languages': '๐Ÿ“', - 'hardware-architecture': '๐Ÿ–ฅ๏ธ', - 'neural-computing': '๐Ÿง ', - networking: '๐ŸŒ', - 'operating-systems': 'โš™๏ธ', - 'programming-languages': '๐Ÿ’ป', - sound: '๐ŸŽต', - 'quantum-computing': 'โš›๏ธ', - 'computational-engineering': 'โš™๏ธ', - 'computers-and-society': '๐Ÿ‘ฅ', - 'digital-libraries': '๐Ÿ“š', - 'emerging-technologies': '๐Ÿš€', - 'general-literature': '๐Ÿ“–', - 'multiagent-systems': '๐Ÿค', - 'mathematical-software': '๐Ÿ”ข', - other: '๐Ÿ“ฆ', - performance: 'โšก', - 'symbolic-computation': '๐Ÿ”ฃ', - 'social-networks': '๐ŸŒ', - 'numerical-analysis': '๐Ÿ”ข', - logic: '๐Ÿงฉ', - robotics: '๐Ÿค–', - 'information-theory': '๐Ÿ“ก', - // Physics hubs - 'atomic-physics': 'โš›๏ธ', - optics: '๐Ÿ”ฆ', - 'computational-physics': '๐Ÿ’ป', - 'statistical-mechanics': '๐Ÿ“Š', - 'fluid-dynamics': '๐Ÿ’ง', - superconductivity: '๐Ÿงฒ', - 'soft-matter': '๐Ÿซง', - 'general-relativity': '๐ŸŒŒ', - geophysics: '๐ŸŒ', - 'mathematical-physics': '๐Ÿ“', - instrumentation: '๐Ÿ”ง', - cosmology: '๐ŸŒŒ', - 'condensed-matter': 'โ„๏ธ', - 'high-energy-experiment': 'โšก', - 'high-energy-phenomenology': '๐ŸŒŸ', - 'high-energy-theory': '๐Ÿ’ซ', - 'lattice-physics': '๐Ÿ”ท', - 'nuclear-experiment': 'โ˜ข๏ธ', - 'nuclear-theory': 'โš›๏ธ', - 'plasma-physics': '๐Ÿ”ฅ', - 'quantum-physics': 'โš›๏ธ', - 'disordered-systems': '๐ŸŒ€', - 'mesoscale-physics': '๐Ÿ”ฌ', - 'other-condensed-matter': 'โ„๏ธ', - 'quantum-gases': '๐Ÿ’จ', - 'strongly-correlated': '๐Ÿ”—', - 'general-physics': '๐Ÿ”ญ', - 'accelerator-physics': '๐Ÿš€', - 'atmospheric-physics': '๐ŸŒค๏ธ', - 'applied-physics': 'โš™๏ธ', - 'atomic-clusters': 'โš›๏ธ', - 'classical-physics': '๐ŸŽฑ', - 'physics-education': '๐Ÿ“š', - 'history-of-physics': '๐Ÿ“œ', - 'popular-physics': '๐ŸŒŸ', - 'space-physics': '๐Ÿš€', - 'data-analysis': '๐Ÿ“Š', - // Economics hubs - econometrics: '๐Ÿ“Š', - 'financial-economics': '๐Ÿ’ฐ', - 'computational-finance': '๐Ÿ’ป', - 'economic-theory': '๐Ÿ“ˆ', - 'general-economics': '๐Ÿ’ผ', - 'general-finance': '๐Ÿ’ต', - 'mathematical-finance': '๐Ÿ“', - 'portfolio-management': '๐Ÿ“Š', - 'quantitative-finance': '๐Ÿ“ˆ', - 'risk-management': 'โš ๏ธ', - 'securities-pricing': '๐Ÿ’น', - 'statistical-finance': '๐Ÿ“Š', - 'trading-and-market-microstructure': '๐Ÿ“ˆ', - // Medicine hubs - 'internal-medicine': '๐Ÿฅ', - endocrinology: '๐Ÿ’‰', - pediatrics: '๐Ÿ‘ถ', - psychiatry: '๐Ÿง ', - radiology: '๐Ÿ“ท', - 'medical-physics': 'โš›๏ธ', - 'public-health': '๐Ÿฅ', - 'physical-therapy': '๐Ÿƒ', - nursing: '๐Ÿ‘ฉโ€โš•๏ธ', - cardiology: 'โค๏ธ', - dentistry: '๐Ÿฆท', - 'medical-education': '๐Ÿ“š', - ophthalmology: '๐Ÿ‘๏ธ', - 'health-informatics': '๐Ÿ’ป', - gastroenterology: '๐Ÿ”ฌ', - dermatology: '๐Ÿงด', - urology: '๐Ÿฅ', - 'obstetrics-and-gynecology': '๐Ÿ‘ถ', - neurology: '๐Ÿง ', - nephrology: '๐Ÿฅ', - hematology: '๐Ÿฉธ', - 'pain-medicine': '๐Ÿ’Š', - oncology: '๐ŸŽ—๏ธ', - 'emergency-medicine': '๐Ÿš‘', - rheumatology: '๐Ÿฆด', - 'clinical-trials': '๐Ÿงช', - 'critical-care-medicine': '๐Ÿšจ', - 'health-economics': '๐Ÿ’ฐ', - 'health-policy': '๐Ÿ“‹', - 'medical-ethics': 'โš–๏ธ', - nutrition: '๐Ÿฅ—', - 'reproductive-health': '๐Ÿ‘ถ', - 'sports-medicine': '๐Ÿƒ', - epidemiology: '๐Ÿ“Š', - anesthesiology: '๐Ÿ’‰', - 'infectious-disease': '๐Ÿฆ ', - orthopedics: '๐Ÿฆด', - otolaryngology: '๐Ÿ‘‚', - 'palliative-medicine': '๐Ÿคฒ', - pulmonology: '๐Ÿซ', - surgery: '๐Ÿฅ', - // Mathematics hubs - combinatorics: '๐Ÿ”ข', - 'algebraic-geometry': '๐Ÿ“', - 'algebraic-topology': '๐Ÿ”ท', - 'analysis-of-pdes': 'โˆ‚', - 'category-theory': '๐Ÿ”—', - 'classical-analysis': '๐Ÿ“Š', - 'commutative-algebra': '๐Ÿ”ข', - 'complex-variables': '๐ŸŒ€', - 'differential-geometry': '๐Ÿ“', - 'dynamical-systems': '๐Ÿ”„', - 'functional-analysis': '๐Ÿ“ˆ', - 'general-mathematics': '๐Ÿ”ข', - 'general-topology': '๐Ÿ”ท', - 'geometric-topology': '๐Ÿ“', - 'group-theory': '๐Ÿ”—', - 'history-and-overview': '๐Ÿ“š', - 'k-theory': '๐ŸŽฏ', - 'metric-geometry': '๐Ÿ“', - 'number-theory': '๐Ÿ”ข', - 'operator-algebras': '๐Ÿ”ค', - 'optimization-and-control': '๐ŸŽฏ', - probability: '๐ŸŽฒ', - 'quantum-algebra': 'โš›๏ธ', - 'representation-theory': '๐Ÿ”ท', - 'rings-and-algebras': 'โญ•', - 'spectral-theory': '๐ŸŒˆ', - 'symplectic-geometry': '๐Ÿ”„', - // Engineering hubs - 'signal-processing': '๐Ÿ“ก', - 'audio-and-speech-processing': '๐ŸŽค', - 'image-and-video-processing': '๐Ÿ“ท', - 'systems-and-control': 'โš™๏ธ', - // Astrophysics hubs - 'planetary-science': '๐Ÿช', - galaxies: '๐ŸŒŒ', - 'high-energy-astrophysics': '๐Ÿ’ซ', - 'solar-and-stellar': 'โ˜€๏ธ', - // Social Sciences hubs - 'physics-and-society': '๐Ÿ‘ฅ', - 'scientific-communication-and-education': '๐Ÿ“š', - // Nonlinear Sciences hubs - 'adaptation-and-self-organization': '๐Ÿ”„', - 'chaotic-dynamics': '๐ŸŒ€', - 'cellular-automata': '๐Ÿ”ฒ', - 'pattern-formation': '๐Ÿ”ท', - 'exactly-solvable-systems': 'โœ“', - // Statistics hubs - applications: '๐Ÿ“Š', - methodology: '๐Ÿ“ˆ', - 'statistics-theory': '๐Ÿ“', - 'general-statistics': '๐Ÿ“Š', - computation: '๐Ÿ’ป', - miscellaneous: '๐Ÿ“ฆ', -}; - -/** - * Get the emoji for a topic based on its name or slug - * @param topicName - The name or slug of the topic - * @returns The emoji string or null if not found - */ -export function getTopicEmoji(topicName: string): string | null { - if (!topicName) return null; - - // Convert to lowercase and replace spaces with hyphens to match our mapping keys - const normalizedName = topicName.toLowerCase().replace(/\s+/g, '-'); - - return TOPIC_EMOJIS[normalizedName] || null; -} diff --git a/components/Topic/TopicList.tsx b/components/Topic/TopicList.tsx deleted file mode 100644 index 517a597c4..000000000 --- a/components/Topic/TopicList.tsx +++ /dev/null @@ -1,132 +0,0 @@ -'use client'; - -import Link from 'next/link'; -import { Hash, Check, FileText } from 'lucide-react'; -import { Topic } from '@/types/topic'; -import { FollowTopicButton } from '@/components/ui/FollowTopicButton'; -import { toTitleCase } from '@/utils/stringUtils'; -import { getTopicEmoji } from './TopicEmojis'; -import { useFollowContext } from '@/contexts/FollowContext'; -import { cn } from '@/utils/styles'; - -interface TopicListProps { - topics: Topic[]; - className?: string; - variant?: 'default' | 'compact'; -} - -export function TopicList({ topics, className = '', variant = 'default' }: TopicListProps) { - const { followedTopicIds, toggleFollow } = useFollowContext(); - - const handleCompactClick = async (e: React.MouseEvent, topicId: number) => { - e.preventDefault(); - await toggleFollow(topicId); - }; - - const formatPaperCount = (count: number): string => { - const formattedCount = count.toLocaleString(); - return `${formattedCount} ${count === 1 ? 'paper' : 'papers'}`; - }; - - return ( -
    - {topics.map((topic) => { - const topicTitle = toTitleCase(topic.name); - const isFollowed = followedTopicIds.includes(topic.id); - - // Get emoji from mapping based on name or slug, fallback to Hash icon - const displayIcon = getTopicEmoji(topic.name) || getTopicEmoji(topic.slug); - - if (variant === 'compact') { - return ( - - ); - } - - return ( - -
    -
    -
    - {displayIcon ? ( - {displayIcon} - ) : ( - - )} -
    - -
    -
    - -
    -
    -
    - -
    -

    - {topicTitle} -

    - {topic.paperCount && topic.paperCount > 0 && ( -
    - - - {formatPaperCount(topic.paperCount)} - -
    - )} -
    -
    - - ); - })} -
    - ); -} diff --git a/components/providers/ClientProviders.tsx b/components/providers/ClientProviders.tsx index a246eca42..904d4fdb1 100644 --- a/components/providers/ClientProviders.tsx +++ b/components/providers/ClientProviders.tsx @@ -12,7 +12,6 @@ import { FundingPowerProvider } from '@/contexts/FundingPowerContext'; import { UserProvider } from '@/contexts/UserContext'; import { OrganizationProvider } from '@/contexts/OrganizationContext'; import { OnchainProvider } from '@/contexts/OnchainContext'; -import { FollowProvider } from '@/contexts/FollowContext'; import { ClickProvider } from '@/contexts/ClickContext'; import { NavigationProvider } from '@/contexts/NavigationContext'; import { AuthSharingWrapper } from '@/components/AuthSharingWrapper'; @@ -63,7 +62,7 @@ export function ClientProviders({ children, session }: ClientProvidersProps) { - {children} + {children} diff --git a/components/skeletons/TopicListSkeleton.tsx b/components/skeletons/TopicListSkeleton.tsx deleted file mode 100644 index 627b3b046..000000000 --- a/components/skeletons/TopicListSkeleton.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { Skeleton } from '@/components/ui/Skeleton'; - -interface TopicListSkeletonProps { - count?: number; - variant?: 'default' | 'compact'; -} - -export function TopicListSkeleton({ count = 50, variant = 'default' }: TopicListSkeletonProps) { - if (variant === 'compact') { - return ( -
    - {Array.from({ length: count }).map((_, index) => ( -
    - {/* Icon placeholder */} - - - {/* Text placeholder */} - -
    - ))} -
    - ); - } - - return ( -
    - {Array.from({ length: count }).map((_, index) => ( -
    -
    -
    - {/* Icon placeholder */} - - - {/* Follow button placeholder */} -
    - - {/* Topic name placeholder */} - -
    -
    - ))} -
    - ); -} diff --git a/components/ui/ButtonGroup.tsx b/components/ui/ButtonGroup.tsx deleted file mode 100644 index 932a67fbb..000000000 --- a/components/ui/ButtonGroup.tsx +++ /dev/null @@ -1,117 +0,0 @@ -'use client'; - -import { ReactNode } from 'react'; -import { cn } from '@/utils/styles'; - -interface ButtonGroupOption { - value: string; - label: ReactNode; - badge?: string | number; - /** Extra classes while this option is the active one, e.g. its own colour. */ - activeClassName?: string; -} - -interface ButtonGroupProps { - options: ButtonGroupOption[]; - value: string; - onChange: (value: string) => void; - className?: string; - size?: 'sm' | 'md' | 'lg'; - variant?: 'default' | 'outlined' | 'pill'; -} - -export function ButtonGroup({ - options, - value, - onChange, - className, - size = 'md', - variant = 'default', -}: ButtonGroupProps) { - const sizeClasses = { - sm: 'px-3 py-1.5 text-xs', - md: 'px-4 py-2 text-sm', - lg: 'px-5 py-2.5 text-base', - }; - - const containerPadding = { - sm: 'p-0.5', - md: 'p-1', - lg: 'p-1.5', - }; - - const containerClasses = () => { - switch (variant) { - case 'outlined': - return 'flex gap-0 border border-gray-300 bg-white rounded-lg'; - case 'pill': - return 'flex gap-2 p-1 border border-gray-300 bg-white rounded-lg'; - default: - return cn('flex gap-1 bg-gray-100 rounded-xl', containerPadding[size]); - } - }; - - const buttonClasses = (isActive: boolean) => { - switch (variant) { - case 'outlined': - return cn( - 'font-medium transition-all flex items-center gap-2 border-r border-gray-300 last:border-r-0 first:rounded-l-lg last:rounded-r-lg whitespace-nowrap', - sizeClasses[size], - isActive ? 'bg-gray-100 text-gray-900' : 'bg-white text-gray-600 hover:bg-gray-50' - ); - - case 'pill': - return cn( - 'font-medium transition-all flex items-center gap-2 rounded-md whitespace-nowrap', - sizeClasses[size], - isActive ? 'bg-gray-200 text-gray-900' : 'text-gray-600 hover:bg-gray-100' - ); - - default: - return cn( - 'font-medium rounded-lg transition-all flex items-center gap-2 whitespace-nowrap', - sizeClasses[size], - isActive ? 'bg-white text-gray-900 shadow-sm' : 'text-gray-600 hover:text-gray-900' - ); - } - }; - - const badgeClasses = (isActive: boolean) => { - switch (variant) { - case 'outlined': - return 'bg-gray-200 text-gray-600 rounded font-medium'; - case 'pill': - return 'bg-gray-300 text-gray-700 rounded font-medium'; - default: - return 'bg-gray-200 text-gray-700 rounded font-medium'; - } - }; - - return ( -
    - {options.map((option) => ( - - ))} -
    - ); -} diff --git a/components/ui/FollowTopicButton.tsx b/components/ui/FollowTopicButton.tsx deleted file mode 100644 index 361164b8d..000000000 --- a/components/ui/FollowTopicButton.tsx +++ /dev/null @@ -1,82 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { Button } from '@/components/ui/Button'; -import { HubService } from '@/services/hub.service'; -import { useFollowContext } from '@/contexts/FollowContext'; - -interface FollowTopicButtonProps { - topicId: number; - size?: 'sm' | 'default'; - className?: string; -} - -export function FollowTopicButton({ - topicId, - size = 'sm', - className = '', -}: FollowTopicButtonProps) { - const { isFollowing, toggleFollow } = useFollowContext(); - const [isLoading, setIsLoading] = useState(false); - const [optimisticFollowing, setOptimisticFollowing] = useState(null); - const [isHovered, setIsHovered] = useState(false); - - // Check if this topic is being followed - const following = optimisticFollowing !== null ? optimisticFollowing : isFollowing(topicId); - - const handleFollowToggle = async (e: React.MouseEvent) => { - // Stop event propagation to prevent parent click handlers from firing - e.stopPropagation(); - e.preventDefault(); - - if (isLoading) return; - - // Optimistically update the UI - const currentFollowingState = isFollowing(topicId); - setOptimisticFollowing(!currentFollowingState); - - setIsLoading(true); - try { - // This will update the global context state - await toggleFollow(topicId); - // Clear optimistic state on success - setOptimisticFollowing(null); - } catch (error) { - console.error('Error toggling follow status:', error); - // Revert optimistic update on error - setOptimisticFollowing(null); - } finally { - setIsLoading(false); - } - }; - - const getButtonText = () => { - if (following && isHovered) return 'Unfollow'; - if (following) return 'Following'; - return 'Follow'; - }; - - const getButtonStyles = () => { - if (following && isHovered) { - return 'text-red-600 border-red-300 hover:border-red-400 hover:bg-red-50'; - } - if (following) { - return 'text-gray-600 border-gray-300'; - } - return 'bg-gray-100 text-gray-900 hover:bg-gray-200'; - }; - - return ( - - ); -} diff --git a/components/ui/badges/HashtagBadge.tsx b/components/ui/badges/HashtagBadge.tsx index 216a11d7b..1cb798ac4 100644 --- a/components/ui/badges/HashtagBadge.tsx +++ b/components/ui/badges/HashtagBadge.tsx @@ -3,17 +3,23 @@ import Link from 'next/link'; interface HashtagBadgeProps { label: string; - href: string; + /** Where the badge goes; without one it is plain text. */ + href?: string; className?: string; } -export const HashtagBadge: FC = ({ label, href, className = '' }) => ( - e.stopPropagation()} - > - #{label} - -); +export const HashtagBadge: FC = ({ label, href, className = '' }) => + href ? ( + e.stopPropagation()} + > + #{label} + + ) : ( + + #{label} + + ); diff --git a/components/work/components/JournalSection.tsx b/components/work/components/JournalSection.tsx index f5c48fe81..51323fbb7 100644 --- a/components/work/components/JournalSection.tsx +++ b/components/work/components/JournalSection.tsx @@ -19,45 +19,52 @@ export const JournalSection = ({ journal }: JournalSectionProps) => { const isRHJournal = logo === 'rhJournal2'; const isPreprint = isPreprintServer(journal.slug); const displayName = logo ? getPreprintDisplayName(journal.slug) : journal.name; - const href = isRHJournal ? '/journal' : `/topic/${journal.slug}`; const sectionTitle = isPreprint ? 'Preprint Server' : 'Journal'; + const badge = ( + + {isRHJournal ? ( + <> + + RH Journal + + ) : logo ? ( + {journal.name} + ) : journal.imageUrl ? ( + {journal.name} + ) : ( + {displayName} + )} + + ); + return (
    - - - {isRHJournal ? ( - <> - - RH Journal - - ) : logo ? ( - {journal.name} - ) : journal.imageUrl ? ( - {journal.name} - ) : ( - {displayName} - )} - - + {isRHJournal ? ( + + {badge} + + ) : ( + {badge} + )}
    ); }; diff --git a/components/work/components/TopicsSection.tsx b/components/work/components/TopicsSection.tsx index 5b75866a0..e4b9e9bc4 100644 --- a/components/work/components/TopicsSection.tsx +++ b/components/work/components/TopicsSection.tsx @@ -60,11 +60,7 @@ export const TopicsSection = ({ topics }: TopicsSectionProps) => {
    {displayedTopics.map((topic) => ( - + ))}
    {hasMoreTopics && ( diff --git a/contexts/FollowContext.tsx b/contexts/FollowContext.tsx deleted file mode 100644 index 98ecc860b..000000000 --- a/contexts/FollowContext.tsx +++ /dev/null @@ -1,220 +0,0 @@ -'use client'; - -import { - createContext, - useState, - useContext, - useEffect, - useMemo, - useCallback, - ReactNode, -} from 'react'; -import { useSession } from 'next-auth/react'; -import { HubService } from '@/services/hub.service'; -import { FollowService } from '@/services/follow.service'; -import { Topic } from '@/types/topic'; -import { FollowedObject } from '@/types/follow'; - -interface FollowContextType { - followedTopicIds: number[]; - followedTopics: Topic[]; - followedTopicObjects: FollowedObject[]; - isFollowing: (topicId: number) => boolean; - toggleFollow: (topicId: number) => Promise; - followMultiple: (topicIds: number[]) => Promise; - refreshFollowed: () => Promise; - loading: boolean; - getFollowedTopics: () => Promise; - getFollowedTopicObjects: () => Promise; - /** Opts the caller into the lazy initial fetch. Called for you by `useFollowContext`. */ - activate: () => void; -} - -const FollowContext = createContext(undefined); - -export function FollowProvider({ children }: { children: ReactNode }) { - const { status } = useSession(); - const [followedTopicIds, setFollowedTopicIds] = useState([]); - const [followedTopics, setFollowedTopics] = useState([]); - const [followedTopicObjects, setFollowedTopicObjects] = useState([]); - const [loading, setLoading] = useState(true); - const [isActivated, setIsActivated] = useState(false); - - const activate = useCallback(() => setIsActivated(true), []); - - // Function to refresh the list of followed topics - const refreshFollowed = useCallback(async () => { - setLoading(true); - try { - // Fetch full topic data with metadata - const followedObjects = await FollowService.getFollowedTopics(); - - // Extract topics and IDs - const topics = followedObjects.map((obj) => obj.data as Topic); - const topicIds = topics.map((topic) => topic.id); - - setFollowedTopicObjects(followedObjects); - setFollowedTopics(topics); - setFollowedTopicIds(topicIds); - } catch (error) { - console.error('Error fetching followed topics:', error); - } finally { - setLoading(false); - } - }, []); - - // This provider wraps the whole app, so fetching on mount cost every page an - // `/api/hub/following/` request even when nothing on screen showed follows. - // Instead the first `useFollowContext` consumer to mount activates it, and - // signed-out visitors never follow anything so they skip the call entirely. - useEffect(() => { - if (!isActivated || status === 'loading') return; - - if (status !== 'authenticated') { - setFollowedTopicIds([]); - setFollowedTopics([]); - setFollowedTopicObjects([]); - setLoading(false); - return; - } - - refreshFollowed(); - }, [isActivated, status, refreshFollowed]); - - // Check if a topic is being followed - const isFollowing = (topicId: number): boolean => { - return followedTopicIds.includes(topicId); - }; - - // Toggle follow status for a topic - const toggleFollow = async (topicId: number) => { - const currentlyFollowing = isFollowing(topicId); - - // Optimistically update the UI - setFollowedTopicIds((prev) => - currentlyFollowing ? prev.filter((id) => id !== topicId) : [...prev, topicId] - ); - - try { - if (currentlyFollowing) { - await HubService.unfollowHub(topicId); - } else { - await HubService.followHub(topicId); - } - // Refresh the full data after successful toggle - await refreshFollowed(); - } catch (error) { - console.error('Error toggling follow status:', error); - // Revert on error - setFollowedTopicIds((prev) => - currentlyFollowing ? [...prev, topicId] : prev.filter((id) => id !== topicId) - ); - // Re-throw the error for components to handle - throw error; - } - }; - - // Follow multiple topics at once - const followMultiple = async (topicIds: number[]) => { - // Filter out already followed topics - const toFollow = topicIds.filter((id) => !followedTopicIds.includes(id)); - if (toFollow.length === 0) return; - - // Optimistically update the UI - setFollowedTopicIds((prev) => [...prev, ...toFollow]); - - try { - const response = await FollowService.followMultipleHubs(toFollow); - - // Log any issues for debugging - if (response.not_found.length > 0) { - console.warn('Topics not found:', response.not_found); - } - if (response.already_following.length > 0) { - console.info('Already following:', response.already_following); - } - - // Refresh the full data after successful follow - await refreshFollowed(); - } catch (error) { - console.error('Error following multiple topics:', error); - // Revert on error - setFollowedTopicIds((prev) => prev.filter((id) => !toFollow.includes(id))); - // Re-throw the error for components to handle - throw error; - } - }; - - // Function to get followed topics (returns cached data or fetches if needed) - const getFollowedTopics = async (): Promise => { - if (followedTopics.length === 0 && followedTopicIds.length > 0) { - // If we have IDs but no topic data, fetch the full data - try { - const followedObjects = await FollowService.getFollowedTopics(); - const topics = followedObjects.map((obj) => obj.data as Topic); - setFollowedTopicObjects(followedObjects); - setFollowedTopics(topics); - return topics; - } catch (error) { - console.error('Error fetching followed topics data:', error); - return []; - } - } - return followedTopics; - }; - - // Function to get followed topic objects with metadata - const getFollowedTopicObjects = async (): Promise => { - if (followedTopicObjects.length === 0 && followedTopicIds.length > 0) { - // If we have IDs but no topic objects, fetch the full data - try { - const followedObjects = await FollowService.getFollowedTopics(); - const topics = followedObjects.map((obj) => obj.data as Topic); - setFollowedTopicObjects(followedObjects); - setFollowedTopics(topics); - return followedObjects; - } catch (error) { - console.error('Error fetching followed topic objects:', error); - return []; - } - } - return followedTopicObjects; - }; - - // Create memoized context value - const contextValue = useMemo( - () => ({ - followedTopicIds, - followedTopics, - followedTopicObjects, - isFollowing, - toggleFollow, - followMultiple, - refreshFollowed, - loading, - getFollowedTopics, - getFollowedTopicObjects, - activate, - }), - [followedTopicIds, followedTopics, followedTopicObjects, loading, activate] - ); - - return {children}; -} - -// Hook to use the context -export function useFollowContext() { - const context = useContext(FollowContext); - const activate = context?.activate; - - // Reading the context is what makes the data worth fetching, so the first - // consumer to mount triggers the provider's one-time fetch. - useEffect(() => { - activate?.(); - }, [activate]); - - if (context === undefined) { - throw new Error('useFollowContext must be used within a FollowProvider'); - } - return context; -} diff --git a/hooks/useFeed.ts b/hooks/useFeed.ts index 271a4b980..8e879244e 100644 --- a/hooks/useFeed.ts +++ b/hooks/useFeed.ts @@ -4,7 +4,7 @@ import { FeedService } from '@/services/feed.service'; import { useSession } from 'next-auth/react'; import { useFeedStateRestoration } from './useFeedStateRestoration'; -export type FeedTab = 'following' | 'latest' | 'popular' | 'for-you' | 'journal'; +type FeedTab = 'following' | 'latest' | 'popular' | 'for-you' | 'journal'; export type FundingTab = 'all' | 'open' | 'closed'; export type FeedSource = 'all' | 'researchhub'; diff --git a/hooks/useHub.ts b/hooks/useHub.ts deleted file mode 100644 index a8efe9b46..000000000 --- a/hooks/useHub.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { useState, useEffect } from 'react'; -import { Hub, HubService } from '@/services/hub.service'; - -export function useHub(slug: string | null) { - const [hub, setHub] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - if (!slug) return; - - const fetchHub = async () => { - setIsLoading(true); - setError(null); - - try { - const hubData = await HubService.getHubBySlug(slug); - setHub(hubData); - } catch (err) { - setError(err instanceof Error ? err : new Error('Failed to fetch hub')); - console.error('Error fetching hub:', err); - } finally { - setIsLoading(false); - } - }; - - fetchHub(); - }, [slug]); - - return { hub, isLoading, error }; -} diff --git a/lib/metadata-helpers.ts b/lib/metadata-helpers.ts index e4c218486..f1e9ce544 100644 --- a/lib/metadata-helpers.ts +++ b/lib/metadata-helpers.ts @@ -104,21 +104,3 @@ export function getReferralMetadata({ }, }; } - -export function getBrowsePageMetadata() { - return buildOpenGraphMetadata({ - title: 'Browse Topics - ResearchHub', - description: - 'Explore research topics and communities on ResearchHub. Join discussions, follow your interests, and contribute to scientific advancement.', - url: '/browse', - type: 'website', - tags: [ - 'Research Topics', - 'Scientific Communities', - 'ResearchHub', - 'Academic Topics', - 'Science Categories', - ], - section: 'Browse', - }); -} diff --git a/next.config.js b/next.config.js index e34a90997..3ff483eb9 100644 --- a/next.config.js +++ b/next.config.js @@ -19,7 +19,18 @@ const nextConfig = { }, { source: '/journal/:slug', - destination: '/topic/:slug', + destination: '/', + permanent: true, + }, + // Topic (hub) pages and the topic browser are retired. + { + source: '/topic/:path*', + destination: '/', + permanent: true, + }, + { + source: '/browse', + destination: '/', permanent: true, }, { diff --git a/services/follow.service.ts b/services/follow.service.ts deleted file mode 100644 index 8fa1308d9..000000000 --- a/services/follow.service.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { ApiClient } from './client'; -import { FollowResponse, FollowedObject, transformFollowedObject } from '@/types/follow'; - -interface FollowMultipleResponse { - followed: Array<{ - id: number; - name: string; - slug?: string; - }>; - already_following: Array<{ - id: number; - name: string; - }>; - not_found: number[]; -} - -export class FollowService { - private static readonly BASE_PATH = '/api/hub'; - - /** - * Get all objects that the user is following - * @returns Array of followed objects with their data - */ - static async getFollowedObjects(): Promise { - const response = await ApiClient.get(`${this.BASE_PATH}/following/`); - return response.map(transformFollowedObject); - } - - /** - * Get only followed topics/hubs with their metadata - * @returns Array of FollowedObject containing Topics - */ - static async getFollowedTopics(): Promise { - const followedObjects = await this.getFollowedObjects(); - - // Filter for hub/topic objects that have valid topic data - return followedObjects.filter((obj) => obj.type === 'HUB' && obj.data !== null); - } - - /** - * Follow multiple hubs/topics at once - * @param hubIds Array of hub IDs to follow - * @returns Response with followed, already following, and not found items - */ - static async followMultipleHubs(hubIds: number[]): Promise { - return await ApiClient.post(`${this.BASE_PATH}/follow_multiple/`, { - ids: hubIds, - }); - } -} diff --git a/services/hub.service.ts b/services/hub.service.ts index b5528c7a0..848c83280 100644 --- a/services/hub.service.ts +++ b/services/hub.service.ts @@ -1,46 +1,8 @@ -import { Topic, transformTopicSuggestions, transformTopic } from '@/types/topic'; +import { Topic, transformTopicSuggestions } from '@/types/topic'; import { ApiClient } from './client'; -interface HubResponse { - id: number; - name: string; - slug: string; - hub_image?: string | null; - description?: string; - namespace?: string; - category?: string; - discussion_count?: number; - paper_count?: number; - subscriber_count?: number; - is_locked?: boolean; - is_removed?: boolean; - editor_permission_groups?: any[]; -} - -interface HubDetailResponse { - count: number; - next: string | null; - previous: string | null; - results: HubResponse[]; -} - -interface PrimaryHubsResponse { - count: number; - results: HubResponse[]; -} - -export interface Hub { - id: number; - name: string; - slug: string; - imageUrl?: string; - description?: string; -} - export class HubService { - private static readonly BASE_PATH = '/api/hub'; private static readonly SUGGEST_PATH = '/api/search/hubs/suggest'; - private static readonly PRIMARY_HUBS_PATH = '/api/hub/primary_only'; static async suggestTopics(query: string, limit?: number): Promise { const params = new URLSearchParams({ @@ -56,29 +18,4 @@ export class HubService { return transformTopicSuggestions(response); } - - static async followHub(hubId: number): Promise { - await ApiClient.post(`${this.BASE_PATH}/${hubId}/follow/`); - } - - static async unfollowHub(hubId: number): Promise { - await ApiClient.post(`${this.BASE_PATH}/${hubId}/unfollow/`); - } - - static async getHubBySlug(slug: string): Promise { - const response = await ApiClient.get( - `${this.BASE_PATH}?slug=${encodeURIComponent(slug)}` - ); - - if (!response.results.length) { - throw new Error(`Hub with slug "${slug}" not found`); - } - - return transformTopic(response.results[0]); - } - - static async getPrimaryHubs(): Promise { - const response = await ApiClient.getPublic(this.PRIMARY_HUBS_PATH); - return response.results.map(transformTopic); - } } diff --git a/types/follow.ts b/types/follow.ts deleted file mode 100644 index ee225e527..000000000 --- a/types/follow.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { createTransformer } from './transformer'; -import { Topic, transformTopic } from './topic'; - -export interface FollowResponse { - id: number; - type: 'HUB' | 'PAPER' | 'POST' | 'BOUNTY' | string; - object_id: number; - followed_object: any; // The actual hub, paper, post data - created_date: string; - updated_date: string; -} - -export type FollowedObject = { - id: number; - type: string; - objectId: number; - createdDate: string; - updatedDate: string; - data: Topic | any; // Can be Topic, Paper, etc. based on type -}; - -export const transformFollowedObject = createTransformer( - (raw: FollowResponse) => { - let transformedData: any = null; - - // Transform based on the type - if (raw.type === 'HUB' && raw.followed_object) { - // For hub objects, use the topic transformer on the followed_object - transformedData = transformTopic(raw.followed_object); - } else if (raw.followed_object) { - // For other types (papers, etc.), we can add transformers later - transformedData = raw.followed_object; - } - - return { - id: raw.id, - type: raw.type, - objectId: raw.object_id, - createdDate: raw.created_date, - updatedDate: raw.updated_date, - data: transformedData, - }; - } -); diff --git a/types/search.ts b/types/search.ts index c87ea8f29..1ed8cb5c6 100644 --- a/types/search.ts +++ b/types/search.ts @@ -1,12 +1,11 @@ import { createTransformer } from './transformer'; -import { buildWorkUrl, buildAuthorUrl, buildTopicUrl } from '@/utils/url'; +import { buildWorkUrl, buildAuthorUrl } from '@/utils/url'; import { mapApiDocumentTypeToClientType } from '@/utils/contentTypeMapping'; import { AuthorProfile, extractHeadline } from './authorProfile'; import { ID } from './root'; -import { transformTopic } from './topic'; import { ContentType } from './work'; -export type EntityType = 'user' | 'paper' | 'author' | 'post' | 'hub'; +export type EntityType = 'user' | 'paper' | 'author' | 'post'; export interface BaseSuggestion { entityType: EntityType; @@ -49,17 +48,7 @@ export interface PostSuggestion extends BaseSuggestion { documentType?: string; } -export interface TopicSuggestion extends BaseSuggestion { - entityType: 'hub'; - displayName: string; - id: number; - slug: string; - imageUrl?: string; - description?: string; - paperCount?: number; -} - -export type SearchSuggestion = WorkSuggestion | UserSuggestion | PostSuggestion | TopicSuggestion; +export type SearchSuggestion = WorkSuggestion | UserSuggestion | PostSuggestion; export const transformSearchSuggestion = createTransformer((raw: any) => { try { @@ -174,36 +163,6 @@ export const transformSearchSuggestion = createTransformer { - if (!str) return ''; - return str - .toLowerCase() - .split(' ') - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); -}; - export const pluralizeSuffix = (count: number): string => (count === 1 ? '' : 's'); /** diff --git a/utils/url.ts b/utils/url.ts index 3b2652d32..0623bc4a4 100644 --- a/utils/url.ts +++ b/utils/url.ts @@ -382,10 +382,3 @@ export const buildWorkUrl = ({ export const buildAuthorUrl = (id: string | number) => { return `/author/${id}`; }; - -/** - * Builds a topic URL from a slug - */ -export const buildTopicUrl = (slug: string) => { - return `/topic/${slug}`; -}; From bf68e2bfe03754270e17941551d0ed492452a288 Mon Sep 17 00:00:00 2001 From: Kobe Attias Date: Sun, 11 Oct 2026 17:42:15 -0400 Subject: [PATCH 2/3] Keep the retirement's new lines out of SonarCloud's duplication count The quality gate measures duplicated lines among the lines a change adds. Two more redirect object literals in next.config.js read as a copy of the existing list, and the journal badge's two blocks, re-indented into the new `badge` constant, read as copies of each other. The redirects are now one generated entry, the badge picks its image source once, and HashtagBadge shares its styling between the link and the plain span. Co-Authored-By: Claude Fable 5.1 --- components/ui/badges/HashtagBadge.tsx | 23 ++++++++++++------- components/work/components/JournalSection.tsx | 14 +++-------- next.config.js | 11 +++------ 3 files changed, 21 insertions(+), 27 deletions(-) diff --git a/components/ui/badges/HashtagBadge.tsx b/components/ui/badges/HashtagBadge.tsx index 1cb798ac4..968f87f83 100644 --- a/components/ui/badges/HashtagBadge.tsx +++ b/components/ui/badges/HashtagBadge.tsx @@ -8,18 +8,25 @@ interface HashtagBadgeProps { className?: string; } -export const HashtagBadge: FC = ({ label, href, className = '' }) => - href ? ( +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} - ) : ( - - #{label} - ); +}; diff --git a/components/work/components/JournalSection.tsx b/components/work/components/JournalSection.tsx index 51323fbb7..5c4a6f1b2 100644 --- a/components/work/components/JournalSection.tsx +++ b/components/work/components/JournalSection.tsx @@ -20,6 +20,7 @@ export const JournalSection = ({ journal }: JournalSectionProps) => { const isPreprint = isPreprintServer(journal.slug); const displayName = logo ? getPreprintDisplayName(journal.slug) : journal.name; const sectionTitle = isPreprint ? 'Preprint Server' : 'Journal'; + const imageSrc = logo || journal.imageUrl; const badge = ( { RH Journal - ) : logo ? ( + ) : imageSrc ? ( {journal.name} - ) : journal.imageUrl ? ( - {journal.name} ({ + source, destination: '/', permanent: true, - }, + })), { source: '/trending', destination: '/', From 51a0ed000cc3e8b14f5a7f37cb53632399ca4d05 Mon Sep 17 00:00:00 2001 From: Kobe Attias Date: Sun, 11 Oct 2026 18:25:47 -0400 Subject: [PATCH 3/3] Drop topic badges entirely instead of showing them as plain text Topics are off the product, so the hashtag rows stop rendering rather than lingering as inert text: - FeedItemTopicBadges is replaced by FeedItemJournalBadge, which only shows the journal hashtag on paper cards (the one badge that was never a topic); post and bounty cards had nothing but topics in that row, so it goes. - The work page's Topics section, the related-work card's topic row, the skeleton row that mirrored the badges, and the excluded-topic-slugs constant are deleted. - With no topic badge to click, the topic click plumbing goes too: the onTopicClick props, the peer-review page's topic filter event in useBounties, and ClickContext itself, whose only consumer that was. - useFeedSource and the analytics FeedSource type no longer know a 'topic' source. Co-Authored-By: Claude Fable 5.1 --- components/Feed/FeedItemJournalBadge.tsx | 23 ++++ components/Feed/FeedItemTopicBadges.tsx | 101 ------------------ .../Feed/items/DeprecatedFeedItemBounty.tsx | 15 +-- .../Feed/items/FeedItemBountyComment.tsx | 10 +- components/Feed/items/FeedItemPaper.tsx | 11 +- components/Feed/items/FeedItemPost.tsx | 9 -- components/Feed/skeletons/BountySkeleton.tsx | 3 - .../Feed/skeletons/PaperPostSkeleton.tsx | 3 - components/Feed/skeletons/shared.tsx | 9 -- components/Paper/RelatedWorkCard.tsx | 39 ------- components/providers/ClientProviders.tsx | 99 +++++++++-------- components/work/WorkRightSidebar.tsx | 2 - components/work/components/TopicsSection.tsx | 80 -------------- constants/topics.ts | 2 - contexts/ClickContext.tsx | 38 ------- hooks/useBounties.ts | 23 ---- hooks/useFeedSource.ts | 11 +- types/analytics.ts | 1 - 18 files changed, 77 insertions(+), 402 deletions(-) create mode 100644 components/Feed/FeedItemJournalBadge.tsx delete mode 100644 components/Feed/FeedItemTopicBadges.tsx delete mode 100644 components/work/components/TopicsSection.tsx delete mode 100644 constants/topics.ts delete mode 100644 contexts/ClickContext.tsx diff --git a/components/Feed/FeedItemJournalBadge.tsx b/components/Feed/FeedItemJournalBadge.tsx new file mode 100644 index 000000000..3df98688e --- /dev/null +++ b/components/Feed/FeedItemJournalBadge.tsx @@ -0,0 +1,23 @@ +'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/FeedItemTopicBadges.tsx b/components/Feed/FeedItemTopicBadges.tsx deleted file mode 100644 index 857bba64d..000000000 --- a/components/Feed/FeedItemTopicBadges.tsx +++ /dev/null @@ -1,101 +0,0 @@ -'use client'; - -import { FC, useRef, useState, useEffect, useCallback } from 'react'; -import { HashtagBadge } from '@/components/ui/badges/HashtagBadge'; -import { Topic } from '@/types/topic'; -import { Journal } from '@/types/journal'; -import { EXCLUDED_TOPIC_SLUGS } from '@/constants/topics'; -import { getSourceLogo } from '@/utils/preprintUtil'; - -interface FeedItemTopicBadgesProps { - journal?: Journal; - category?: Topic; - subcategory?: Topic; - topics?: Topic[]; -} - -/** - * Renders topic and journal hashtag-style badges (e.g. #biorxiv, #chemistry) - * for paper, post, and grant feed items. - */ -export const FeedItemTopicBadges: FC = ({ - journal, - category, - subcategory, - topics = [], -}) => { - const scrollRef = useRef(null); - const [showRightBlur, setShowRightBlur] = useState(false); - - const updateBlurState = useCallback(() => { - const el = scrollRef.current; - if (!el) return; - - const hasOverflow = el.scrollWidth > el.clientWidth; - const isAtEnd = el.scrollLeft + el.clientWidth >= el.scrollWidth - 2; - - setShowRightBlur(hasOverflow && !isAtEnd); - }, []); - - useEffect(() => { - updateBlurState(); - - const el = scrollRef.current; - if (!el) return; - - el.addEventListener('scroll', updateBlurState, { passive: true }); - - const resizeObserver = new ResizeObserver(updateBlurState); - resizeObserver.observe(el); - - return () => { - el.removeEventListener('scroll', updateBlurState); - resizeObserver.disconnect(); - }; - }, [updateBlurState]); - - const filteredTopics = topics.filter((topic) => !EXCLUDED_TOPIC_SLUGS.includes(topic.slug)); - - const journalLogo = journal?.name ? getSourceLogo(journal.slug) : null; - const isRHJournal = journalLogo === 'rhJournal2'; - - const renderBadges = () => { - if (category?.slug || subcategory?.slug) { - return ( - <> - {category?.slug && } - {subcategory?.slug && } - - ); - } - - if (filteredTopics.length > 0) { - return ( - <> - {filteredTopics.map((topic) => ( - - ))} - - ); - } - - return null; - }; - - return ( -
    -
    - {journalLogo && journal?.slug && journal?.name && ( - - )} - {renderBadges()} -
    - -
    -
    - ); -}; diff --git a/components/Feed/items/DeprecatedFeedItemBounty.tsx b/components/Feed/items/DeprecatedFeedItemBounty.tsx index 66dcc2d2f..84b1b026f 100644 --- a/components/Feed/items/DeprecatedFeedItemBounty.tsx +++ b/components/Feed/items/DeprecatedFeedItemBounty.tsx @@ -8,7 +8,6 @@ import { MetadataSection, PrimaryActionSection, } from '@/components/Feed/BaseFeedItem'; -import { FeedItemTopicBadges } from '@/components/Feed/FeedItemTopicBadges'; import { AuthorList } from '@/components/ui/AuthorList'; import { Avatar } from '@/components/ui/Avatar'; import { AuthorTooltip } from '@/components/ui/AuthorTooltip'; @@ -39,7 +38,7 @@ interface DeprecatedFeedItemBountyProps { } /** - * Extracts the bounty, title, authors, topics, and other data from the entry + * Extracts the bounty, title, authors, and other data from the entry * regardless of whether it's a true BOUNTY contentType or a PAPER/POST entry * with bounties attached (as created by BountyService). */ @@ -60,9 +59,6 @@ function extractBountyData(entry: FeedEntry) { verified: a.authorProfile?.user?.isVerified, authorUrl: a.authorProfile?.profileUrl, })) ?? [], - topics: relatedWork?.topics ?? [], - category: relatedWork?.category, - subcategory: relatedWork?.subcategory, postType: relatedWork?.postType, commentContent: bountyContent.comment?.content, commentFormat: bountyContent.comment?.contentFormat, @@ -99,9 +95,6 @@ function extractBountyData(entry: FeedEntry) { }, ] : [], - topics: 'topics' in content ? (content.topics ?? []) : [], - category: 'category' in content ? content.category : undefined, - subcategory: 'subcategory' in content ? content.subcategory : undefined, postType: 'postType' in content ? content.postType : undefined, commentContent: bounty?.comment?.content, commentFormat: bounty?.comment?.contentFormat as ContentFormat | undefined, @@ -132,9 +125,6 @@ export const DeprecatedFeedItemBounty: FC = ({ bounty, title, authors, - topics, - category, - subcategory, postType, commentContent, commentFormat, @@ -265,9 +255,6 @@ export const DeprecatedFeedItemBounty: FC = ({ showBountyInfo={false} onFeedItemClick={onFeedItemClick} hideReportButton={false} - badges={ - - } > void; showSupportAndCTAButtons?: boolean; // Show container for Support and CTA buttons showDeadline?: boolean; // Show deadline in metadata line maxLength?: number; @@ -122,7 +120,6 @@ export const FeedItemBountyComment: FC = ({ actionLabels, showFooter = true, // Default to showing the footer hideActions = false, // Prop to hide actions, similar to FeedItemComment - onTopicClick, showSupportAndCTAButtons = true, // Show container for Support and CTA buttons showDeadline = true, // Show deadline in metadata line maxLength, @@ -334,12 +331,7 @@ export const FeedItemBountyComment: FC = ({ onMouseDown={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()} > - +
    )} diff --git a/components/Feed/items/FeedItemPaper.tsx b/components/Feed/items/FeedItemPaper.tsx index 90c1817f1..87ff0f133 100644 --- a/components/Feed/items/FeedItemPaper.tsx +++ b/components/Feed/items/FeedItemPaper.tsx @@ -12,7 +12,7 @@ import { FeedItemTopSection, } from '@/components/Feed/BaseFeedItem'; import { FeedItemAbstractSection } from '@/components/Feed/FeedItemAbstractSection'; -import { FeedItemTopicBadges } from '@/components/Feed/FeedItemTopicBadges'; +import { FeedItemJournalBadge } from '@/components/Feed/FeedItemJournalBadge'; import { AuthorList } from '@/components/ui/AuthorList'; import { Tooltip } from '@/components/ui/Tooltip'; import { PopularityScoreTooltip } from '@/components/tooltips/HotScoreTooltip'; @@ -101,14 +101,7 @@ export const FeedItemPaper: FC = ({ /> ) : undefined } - badges={ - - } + badges={} > {/* Top section with mobile image */} = ({ leftContent={null} /> -
    - -
    - {/* Main content layout with desktop image */} ( @@ -36,8 +35,6 @@ export const BountySkeleton: FC = () => (
    - -
    diff --git a/components/Feed/skeletons/PaperPostSkeleton.tsx b/components/Feed/skeletons/PaperPostSkeleton.tsx index b5d7b2f4d..ca6b837e2 100644 --- a/components/Feed/skeletons/PaperPostSkeleton.tsx +++ b/components/Feed/skeletons/PaperPostSkeleton.tsx @@ -5,7 +5,6 @@ import { SkeletonDescriptionLines, SkeletonFeedItemActionsFooter, SkeletonFeedItemHeader, - SkeletonTopicBadges, } from './shared'; interface PaperPostSkeletonProps { @@ -31,8 +30,6 @@ export const PaperPostSkeleton: FC = ({
    - -
    {showImage && } diff --git a/components/Feed/skeletons/shared.tsx b/components/Feed/skeletons/shared.tsx index 786e97589..667738801 100644 --- a/components/Feed/skeletons/shared.tsx +++ b/components/Feed/skeletons/shared.tsx @@ -30,15 +30,6 @@ export const SkeletonFeedItemHeader: FC<{ actionWidth?: string; className?: stri ); -/** Hashtag-style topic / hub badges โ€” matches `FeedItemTopicBadges`. */ -export const SkeletonTopicBadges: FC = () => ( -
    -
    -
    -
    -
    -); - /** Desktop preview image on the right โ€” matches paper/post `cardImage` column. */ export const SkeletonCardImageRight: FC = () => (
    diff --git a/components/Paper/RelatedWorkCard.tsx b/components/Paper/RelatedWorkCard.tsx index 4bab0757a..4f4fd9e65 100644 --- a/components/Paper/RelatedWorkCard.tsx +++ b/components/Paper/RelatedWorkCard.tsx @@ -2,20 +2,14 @@ import { Work } from '@/types/work'; import { AuthorList } from '@/components/ui/AuthorList'; import { ContentTypeBadge } from '@/components/ui/ContentTypeBadge'; import { truncateText } from '@/utils/stringUtils'; -import { HashtagBadge } from '@/components/ui/badges/HashtagBadge'; -import { Topic } from '@/types/topic'; -import { useClickContext } from '@/contexts/ClickContext'; -import { EXCLUDED_TOPIC_SLUGS } from '@/constants/topics'; import { Fundraise } from '@/types/feed'; import { useExchangeRate } from '@/contexts/ExchangeRateContext'; import { formatRSC } from '@/utils/number'; -import { usePathname } from 'next/navigation'; import { buildWorkUrl } from '@/utils/url'; interface RelatedWorkCardProps { work: Work; onClick?: () => void; - onTopicClick?: (topic: Topic) => void; size?: 'default' | 'sm' | 'lg' | 'xs'; fundraiseData?: Fundraise; onFeedItemClick?: () => void; @@ -24,14 +18,11 @@ interface RelatedWorkCardProps { export const RelatedWorkCard = ({ work, onClick, - onTopicClick, size = 'default', fundraiseData, onFeedItemClick, }: RelatedWorkCardProps) => { - const { triggerEvent } = useClickContext(); const { exchangeRate, isLoading: isLoadingExchangeRate } = useExchangeRate(); - const pathname = usePathname(); if (!work) return null; @@ -177,36 +168,6 @@ export const RelatedWorkCard = ({
    {displayAbstract}
    )} - {/* Topics */} -
    - {(work.topics || []) - .filter((topic) => !EXCLUDED_TOPIC_SLUGS.includes(topic.slug)) - .slice(0, 2) - .map((topic) => { - // Check if we have custom handlers or if we're on the peer review page - const hasCustomHandler = onTopicClick; - const isPeerReviewPage = pathname === '/peer-review'; - const shouldUseClickContext = isPeerReviewPage && !hasCustomHandler; - - return ( -
    { - e.stopPropagation(); - if (onTopicClick) { - onTopicClick(topic); - } else if (shouldUseClickContext) { - // On peer review page without custom handlers, use ClickContext for filtering - triggerEvent({ type: 'topic', payload: topic }); - } - }} - > - -
    - ); - })} -
    - {/* Funding Goal */} {fundraiseData?.goalAmount?.usd && (
    diff --git a/components/providers/ClientProviders.tsx b/components/providers/ClientProviders.tsx index 904d4fdb1..9f8413aac 100644 --- a/components/providers/ClientProviders.tsx +++ b/components/providers/ClientProviders.tsx @@ -12,7 +12,6 @@ import { FundingPowerProvider } from '@/contexts/FundingPowerContext'; import { UserProvider } from '@/contexts/UserContext'; import { OrganizationProvider } from '@/contexts/OrganizationContext'; import { OnchainProvider } from '@/contexts/OnchainContext'; -import { ClickProvider } from '@/contexts/ClickContext'; import { NavigationProvider } from '@/contexts/NavigationContext'; import { AuthSharingWrapper } from '@/components/AuthSharingWrapper'; import { VerificationProvider } from '@/contexts/VerificationContext'; @@ -36,56 +35,54 @@ export function ClientProviders({ children, session }: ClientProvidersProps) { return ( <> - - - - - - - - - - - - - - {/* Above the layouts because the left sidebar - carries the funding power card and mounts - outside PageLayout on some routes. */} - - - - {/* Above the layouts: every page's left - sidebar lists these documents, and - navigating must not fetch them again. */} - - - - - {children} - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + {/* Above the layouts because the left sidebar + carries the funding power card and mounts + outside PageLayout on some routes. */} + + + + {/* Above the layouts: every page's left + sidebar lists these documents, and + navigating must not fetch them again. */} + + + + + {children} + + + + + + + + + + + + + + + + + + + + + + ); } diff --git a/components/work/WorkRightSidebar.tsx b/components/work/WorkRightSidebar.tsx index b9c1966f4..19ac4024c 100644 --- a/components/work/WorkRightSidebar.tsx +++ b/components/work/WorkRightSidebar.tsx @@ -2,7 +2,6 @@ import { Work } from '@/types/work'; import type { WorkMetadata } from '@/services/metadata.service'; -import { TopicsSection } from './components/TopicsSection'; import { DOISection } from './components/DOISection'; import { LicenseSection } from './components/LicenseSection'; import { FormatsSection } from './components/FormatsSection'; @@ -40,7 +39,6 @@ export const WorkRightSidebar = ({ work, metadata }: WorkRightSidebarProps) => { })} /> )} - {work.contentType === 'paper' && } {work.doi && } {work.postType !== 'QUESTION' && } diff --git a/components/work/components/TopicsSection.tsx b/components/work/components/TopicsSection.tsx deleted file mode 100644 index e4b9e9bc4..000000000 --- a/components/work/components/TopicsSection.tsx +++ /dev/null @@ -1,80 +0,0 @@ -'use client'; - -import { ChevronDown } from 'lucide-react'; -import { useState, useMemo } from 'react'; -import { HashtagBadge } from '@/components/ui/badges/HashtagBadge'; -import { EXCLUDED_TOPIC_SLUGS } from '@/constants/topics'; -import { SidebarHeader } from '@/components/ui/SidebarHeader'; - -interface Topic { - id: string | number; - name: string; - slug: string; - namespace?: 'journal' | 'topic' | 'category' | 'subcategory'; -} - -interface TopicsSectionProps { - topics: Topic[]; -} - -export const TopicsSection = ({ topics }: TopicsSectionProps) => { - const [showAllTopics, setShowAllTopics] = useState(false); - - const sortedTopics = useMemo(() => { - const filtered = topics.filter((topic) => !EXCLUDED_TOPIC_SLUGS.includes(topic.slug)); - - const categories = filtered.filter((t) => t.namespace === 'category'); - const subcategories = filtered.filter((t) => t.namespace === 'subcategory'); - const others = filtered.filter( - (t) => t.namespace !== 'category' && t.namespace !== 'subcategory' - ); - - categories.sort((a, b) => Number(a.id) - Number(b.id)); - subcategories.sort((a, b) => Number(a.id) - Number(b.id)); - - const result: Topic[] = []; - - if (categories.length > 0) { - result.push(categories[0]); - } - - if (subcategories.length > 0) { - result.push(subcategories[0]); - } - - result.push(...categories.slice(1)); - result.push(...subcategories.slice(1)); - result.push(...others); - - return result; - }, [topics]); - - if (sortedTopics.length === 0) return null; - - const displayedTopics = showAllTopics ? sortedTopics : sortedTopics.slice(0, 5); - const hasMoreTopics = sortedTopics.length > 5; - - return ( -
    - -
    -
    - {displayedTopics.map((topic) => ( - - ))} -
    - {hasMoreTopics && ( - - )} -
    -
    - ); -}; diff --git a/constants/topics.ts b/constants/topics.ts deleted file mode 100644 index dc97b609a..000000000 --- a/constants/topics.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Topic slugs to exclude from topic lists -export const EXCLUDED_TOPIC_SLUGS: string[] = ['biorxiv-community-reviews']; diff --git a/contexts/ClickContext.tsx b/contexts/ClickContext.tsx deleted file mode 100644 index b6c946b80..000000000 --- a/contexts/ClickContext.tsx +++ /dev/null @@ -1,38 +0,0 @@ -'use client'; - -import { createContext, useContext, useState } from 'react'; - -export interface ClickEvent { - type: string; - payload: any; -} - -interface ClickContextType { - event: ClickEvent | null; - triggerEvent: (event: ClickEvent) => void; - clearEvent: () => void; -} - -const ClickContext = createContext({ - event: null, - triggerEvent: () => {}, - clearEvent: () => {}, -}); - -export function ClickProvider({ children }: { children: React.ReactNode }) { - const [event, setEvent] = useState(null); - - const triggerEvent = (event: ClickEvent) => { - setEvent(event); - }; - - const clearEvent = () => setEvent(null); - - return ( - - {children} - - ); -} - -export const useClickContext = () => useContext(ClickContext); diff --git a/hooks/useBounties.ts b/hooks/useBounties.ts index 96f66d96e..4281632d7 100644 --- a/hooks/useBounties.ts +++ b/hooks/useBounties.ts @@ -3,7 +3,6 @@ import { usePathname, useSearchParams, useRouter } from 'next/navigation'; import { FeedEntry } from '@/types/feed'; import { BountyService } from '@/services/bounty.service'; import { useNavigation, getFeedKey } from '@/contexts/NavigationContext'; -import { useClickContext } from '@/contexts/ClickContext'; interface Hub { id: string | number; @@ -17,7 +16,6 @@ export const useBounties = () => { const searchParams = useSearchParams(); const router = useRouter(); const { isBackNavigation, getFeedState, clearFeedState } = useNavigation(); - const { event, clearEvent } = useClickContext(); // Read sort from URL params, default to 'personalized' const sortFromUrl = searchParams.get('sort') || 'personalized'; @@ -273,27 +271,6 @@ export const useBounties = () => { }); } }, [sort]); - - useEffect(() => { - if (event && event.type === 'topic') { - const topic = event.payload; - - const newHub: Hub = { - id: topic.id, - name: topic.name, - description: topic.description, - }; - setSelectedHubs([newHub]); - const params = new URLSearchParams(searchParams.toString()); - params.set('topics', String(topic.id)); - previousHubsParamRef.current = String(topic.id); - router.replace(`?${params.toString()}`, { scroll: false }); - setPage(1); - fetchBounties(true, [newHub]); - clearEvent(); - } - }, [event, router, searchParams]); - return { entries, isLoading, diff --git a/hooks/useFeedSource.ts b/hooks/useFeedSource.ts index 1ba81a098..e6d3c49c5 100644 --- a/hooks/useFeedSource.ts +++ b/hooks/useFeedSource.ts @@ -29,8 +29,6 @@ import { FeedSource } from '@/types/analytics'; * - /peer-review โ†’ source: 'peer-review', tab: 'unknown' * - /fund โ†’ source: 'fund', tab: 'unknown' * - /journal?tab=all โ†’ source: 'journal', tab: 'all' - * - /topic/ai/popular โ†’ source: 'topic', tab: 'popular' - * - /topic/ai/latest โ†’ source: 'topic', tab: 'latest' * - /fund/needs-funding โ†’ source: 'fund', tab: 'needs-funding' * - /author/153397 โ†’ source: 'author', tab: 'contributions' * - /list/123 โ†’ source: 'list', tab: '123' @@ -53,7 +51,6 @@ function isValidFeedSource(source: string): source is FeedSource { 'peer-review', 'fund', 'journal', - 'topic', 'author', 'list', ]; @@ -82,30 +79,26 @@ export function useFeedSource(): FeedSourceInfo { // Extract source from first path segment const source = pathSegments[0] || 'home'; - const isTopicTab = source === 'topic'; const isAuthorTab = source === 'author'; const isListTab = source === 'list'; const feedSource = toFeedSource(source); - // Extract tab: 1) query param, 2) third path segment for topic pages, 3) second path segment, 4) unknown + // Extract tab: 1) query param, 2) second path segment, 3) unknown let tab: string; const queryTab = searchParams.get('tab'); const pathTab = pathSegments[1]; - const topicTab = pathSegments[2]; if (queryTab) { tab = queryTab; - } else if (isTopicTab && topicTab) { - tab = topicTab; } else if (isListTab && pathTab) { // For list pages, use the list ID (second path segment) as tab tab = pathTab; } else if (isAuthorTab) { // For author pages, default to 'contributions' when no query param tab = 'contributions'; - } else if (pathTab && !isTopicTab && !isAuthorTab && !isListTab) { + } else if (pathTab && !isAuthorTab && !isListTab) { tab = pathTab; } else { tab = 'unknown'; diff --git a/types/analytics.ts b/types/analytics.ts index 50a0f1812..142b9f4f0 100644 --- a/types/analytics.ts +++ b/types/analytics.ts @@ -32,7 +32,6 @@ export type FeedSource = | 'peer-review' | 'fund' | 'journal' - | 'topic' | 'author' | 'list' | 'unknown';