From 8b910ee775ea6c4b22a739e2be44c22b479eb8a9 Mon Sep 17 00:00:00 2001 From: nicktytarenko Date: Sat, 1 Aug 2026 16:36:28 +0300 Subject: [PATCH 1/2] Make Activity the homepage hub with shared Fund/Proposals shell. Move Activity, Fund, and Proposals under a persistent (home) layout with HomeTabs and FundSidebar so tab switches only swap the feed slot. Co-authored-by: Cursor --- app/(home)/fund/loading.tsx | 16 +++ app/{ => (home)}/fund/page.tsx | 38 +------- app/(home)/fund/proposals/loading.tsx | 16 +++ app/(home)/fund/proposals/page.tsx | 19 ++++ app/(home)/layout.tsx | 19 ++++ app/(home)/loading.tsx | 11 +++ app/(home)/page.tsx | 13 +++ app/about/components/CTASection.tsx | 2 +- .../ExpertFinderClientLayout.tsx | 2 +- app/fund/loading.tsx | 26 ----- app/fund/proposals/loading.tsx | 25 ----- app/fund/proposals/page.tsx | 69 ------------- .../components/RightSidebarContainer.tsx | 2 +- app/layouts/topbar/pageRoutes.tsx | 14 +-- app/moderators/ModerationClientLayout.tsx | 2 +- app/page.tsx | 37 ------- app/robots.ts | 2 - app/sitemap.ts | 4 +- .../Funding/FundActivityPageContent.tsx | 39 ++++++++ components/Funding/FundingBannerSkeleton.tsx | 45 --------- components/Funding/HomeTabs.tsx | 20 ++++ components/Funding/MarketplaceCards.tsx | 32 ------ components/landing/LandingPageHero.tsx | 2 +- hooks/useFeedTabs.ts | 4 +- hooks/useFundTabs.ts | 65 ------------- hooks/useFundTabs.tsx | 97 +++++++++++++++++++ utils/navigation.ts | 28 ++---- 27 files changed, 276 insertions(+), 373 deletions(-) create mode 100644 app/(home)/fund/loading.tsx rename app/{ => (home)}/fund/page.tsx (66%) create mode 100644 app/(home)/fund/proposals/loading.tsx create mode 100644 app/(home)/fund/proposals/page.tsx create mode 100644 app/(home)/layout.tsx create mode 100644 app/(home)/loading.tsx create mode 100644 app/(home)/page.tsx delete mode 100644 app/fund/loading.tsx delete mode 100644 app/fund/proposals/loading.tsx delete mode 100644 app/fund/proposals/page.tsx delete mode 100644 app/page.tsx create mode 100644 components/Funding/FundActivityPageContent.tsx delete mode 100644 components/Funding/FundingBannerSkeleton.tsx create mode 100644 components/Funding/HomeTabs.tsx delete mode 100644 components/Funding/MarketplaceCards.tsx delete mode 100644 hooks/useFundTabs.ts create mode 100644 hooks/useFundTabs.tsx diff --git a/app/(home)/fund/loading.tsx b/app/(home)/fund/loading.tsx new file mode 100644 index 000000000..7e50ad550 --- /dev/null +++ b/app/(home)/fund/loading.tsx @@ -0,0 +1,16 @@ +import { FeedItemSkeleton } from '@/components/Feed/FeedItemSkeleton'; + +export default function FundLoading() { + return ( +
+
+
+
+
+ {Array.from({ length: 3 }, (_, i) => ( + + ))} +
+
+ ); +} diff --git a/app/fund/page.tsx b/app/(home)/fund/page.tsx similarity index 66% rename from app/fund/page.tsx rename to app/(home)/fund/page.tsx index a5892037b..ef2eacae6 100644 --- a/app/fund/page.tsx +++ b/app/(home)/fund/page.tsx @@ -1,44 +1,16 @@ -import { Suspense } from 'react'; import { Metadata } from 'next'; import { buildOpenGraphMetadata } from '@/lib/metadata'; -import { PageLayout } from '@/app/layouts/PageLayout'; -import { FundingSidebarServer } from '@/components/Funding/FundingSidebarServer'; -import { ActivitySidebarSkeleton } from '@/components/Funding/ActivitySidebarSkeleton'; -import { HeroHeader } from '@/components/ui/HeroHeader'; -import { FundGrantsPageContent } from './FundGrantsPageContent'; -import { MarketplaceCards } from '@/components/Funding/MarketplaceCards'; -import { FundingHeroPanel } from '@/components/Funding/FundingHeroPanel'; -import { OpenFundingOpportunityCTA } from './OpenFundingOpportunityCTA'; +import { FundGrantsPageContent } from '@/app/fund/FundGrantsPageContent'; export const metadata: Metadata = buildOpenGraphMetadata({ - title: 'Funding Opportunities', + title: 'Fund', description: 'Apply for funding opportunities via proposals.', url: '/fund', }); -export default async function FundPage() { +export default function FundPage() { return ( - - Apply for funding opportunities via proposals. -

- } - cta={} />} - alignTop - > - - - } - rightSidebar={ - }> - - - } - > + <>

ResearchHub provides direct funding pathways for scientific research. Browse open funding @@ -75,6 +47,6 @@ export default async function FundPage() {

-
+ ); } diff --git a/app/(home)/fund/proposals/loading.tsx b/app/(home)/fund/proposals/loading.tsx new file mode 100644 index 000000000..8c6fe7880 --- /dev/null +++ b/app/(home)/fund/proposals/loading.tsx @@ -0,0 +1,16 @@ +import { FeedItemSkeleton } from '@/components/Feed/FeedItemSkeleton'; + +export default function FundProposalsLoading() { + return ( +
+
+
+
+
+ {Array.from({ length: 3 }, (_, i) => ( + + ))} +
+
+ ); +} diff --git a/app/(home)/fund/proposals/page.tsx b/app/(home)/fund/proposals/page.tsx new file mode 100644 index 000000000..45dc8e745 --- /dev/null +++ b/app/(home)/fund/proposals/page.tsx @@ -0,0 +1,19 @@ +import { Metadata } from 'next'; +import { buildOpenGraphMetadata } from '@/lib/metadata'; +import { ProposalFeed } from '@/components/Funding/ProposalFeed'; +import { ProposalSortAndFilters } from '@/components/Funding/ProposalSortAndFilters'; + +export const metadata: Metadata = buildOpenGraphMetadata({ + title: 'Proposals', + description: 'Propose research, get reviewed, receive funding.', + url: '/fund/proposals', +}); + +export default function FundProposalsPage() { + return ( +
+ + +
+ ); +} diff --git a/app/(home)/layout.tsx b/app/(home)/layout.tsx new file mode 100644 index 000000000..7c9573e32 --- /dev/null +++ b/app/(home)/layout.tsx @@ -0,0 +1,19 @@ +import { ReactNode } from 'react'; +import { PageLayout } from '@/app/layouts/PageLayout'; +import { FundSidebar } from '@/components/Funding/FundSidebar'; +import { HomeTabs } from '@/components/Funding/HomeTabs'; + +/** + * Shared shell for homepage hub tabs (Activity / Fund / Proposals). + * Keeps PageLayout, HomeTabs, and FundSidebar mounted while only the feed slot swaps. + * + * `/fund/dashboard` lives outside this group and keeps its own layout. + */ +export default function HomeLayout({ children }: { children: ReactNode }) { + return ( + }> + + {children} + + ); +} diff --git a/app/(home)/loading.tsx b/app/(home)/loading.tsx new file mode 100644 index 000000000..e8b789e45 --- /dev/null +++ b/app/(home)/loading.tsx @@ -0,0 +1,11 @@ +import { ActivityCardSkeleton } from '@/components/Activity/ActivityCardSkeleton'; + +export default function HomeLoading() { + return ( +
+ {[...Array(6)].map((_, i) => ( + + ))} +
+ ); +} diff --git a/app/(home)/page.tsx b/app/(home)/page.tsx new file mode 100644 index 000000000..15e121f31 --- /dev/null +++ b/app/(home)/page.tsx @@ -0,0 +1,13 @@ +import { Metadata } from 'next'; +import { buildOpenGraphMetadata } from '@/lib/metadata'; +import { FundActivityPageContent } from '@/components/Funding/FundActivityPageContent'; + +export const metadata: Metadata = buildOpenGraphMetadata({ + title: 'Activity', + description: 'Recent activity across funding opportunities and proposals.', + url: '/', +}); + +export default function HomeActivityPage() { + return ; +} diff --git a/app/about/components/CTASection.tsx b/app/about/components/CTASection.tsx index be22ae7fe..59448b13d 100644 --- a/app/about/components/CTASection.tsx +++ b/app/about/components/CTASection.tsx @@ -45,7 +45,7 @@ export const CTASection = () => { { kind: 'action', label: 'Join ResearchHub', - onAction: () => showAuthModal(() => router.push('/popular')), + onAction: () => showAuthModal(() => router.push('/')), }, { kind: 'link', href: '/fund', label: 'Fund science', external: true }, ]; diff --git a/app/expert-finder/ExpertFinderClientLayout.tsx b/app/expert-finder/ExpertFinderClientLayout.tsx index 2d19c9a61..0b960b393 100644 --- a/app/expert-finder/ExpertFinderClientLayout.tsx +++ b/app/expert-finder/ExpertFinderClientLayout.tsx @@ -29,7 +29,7 @@ export default function ExpertFinderClientLayout({ children }: ExpertFinderClien useEffect(() => { if (!isLoading && !canAccessExpertFinder) { - router.push('/popular'); + router.push('/'); } }, [isLoading, canAccessExpertFinder, router]); diff --git a/app/fund/loading.tsx b/app/fund/loading.tsx deleted file mode 100644 index 14d6d1fb6..000000000 --- a/app/fund/loading.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { PageLayout } from '@/app/layouts/PageLayout'; -import { ActivitySidebarSkeleton } from '@/components/Funding/ActivitySidebarSkeleton'; -import { FundingBannerSkeleton } from '@/components/Funding/FundingBannerSkeleton'; -import { FeedItemSkeleton } from '@/components/Feed/FeedItemSkeleton'; - -export default function FundLoading() { - return ( - } - rightSidebar={} - > -
-
-
-
- - {/* Feed skeletons */} -
- {Array.from({ length: 3 }, (_, i) => ( - - ))} -
-
- - ); -} diff --git a/app/fund/proposals/loading.tsx b/app/fund/proposals/loading.tsx deleted file mode 100644 index aa48bffd6..000000000 --- a/app/fund/proposals/loading.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { PageLayout } from '@/app/layouts/PageLayout'; -import { ActivitySidebarSkeleton } from '@/components/Funding/ActivitySidebarSkeleton'; -import { FundingBannerSkeleton } from '@/components/Funding/FundingBannerSkeleton'; -import { FeedItemSkeleton } from '@/components/Feed/FeedItemSkeleton'; - -export default function FundProposalsLoading() { - return ( - } - rightSidebar={} - > -
-
-
-
- -
- {Array.from({ length: 3 }, (_, i) => ( - - ))} -
-
- - ); -} diff --git a/app/fund/proposals/page.tsx b/app/fund/proposals/page.tsx deleted file mode 100644 index 7629cee78..000000000 --- a/app/fund/proposals/page.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { Suspense } from 'react'; -import { Metadata } from 'next'; -import Link from 'next/link'; -import { ArrowUpFromLine } from 'lucide-react'; -import { buildOpenGraphMetadata } from '@/lib/metadata'; -import { PageLayout } from '@/app/layouts/PageLayout'; -import { ProposalFeed } from '@/components/Funding/ProposalFeed'; -import { ProposalSortAndFilters } from '@/components/Funding/ProposalSortAndFilters'; -import { FundingSidebarServer } from '@/components/Funding/FundingSidebarServer'; -import { ActivitySidebarSkeleton } from '@/components/Funding/ActivitySidebarSkeleton'; -import { HeroHeader } from '@/components/ui/HeroHeader'; -import { Button } from '@/components/ui/Button'; -import { SubmitProposalTooltip } from '@/components/tooltips/SubmitProposalTooltip'; -import { MarketplaceCards } from '@/components/Funding/MarketplaceCards'; -import { FundingHeroPanel } from '@/components/Funding/FundingHeroPanel'; - -export const metadata: Metadata = buildOpenGraphMetadata({ - title: 'Proposals', - description: 'Propose research, get reviewed, receive funding.', - url: '/fund/proposals', -}); - -function SubmitProposalCTA() { - return ( - - - - - - ); -} - -export default async function FundProposalsPage() { - return ( - - Propose research, get reviewed, receive funding. -

- } - cta={} />} - alignTop - > - - - } - rightSidebar={ - }> - - - } - > -
- - -
-
- ); -} diff --git a/app/layouts/components/RightSidebarContainer.tsx b/app/layouts/components/RightSidebarContainer.tsx index 79a67d315..481f8af49 100644 --- a/app/layouts/components/RightSidebarContainer.tsx +++ b/app/layouts/components/RightSidebarContainer.tsx @@ -11,7 +11,7 @@ import { RightSidebar } from '../RightSidebar'; function getSidebarInstanceKey(pathname: string, rightSidebar: boolean | ReactNode): string { if (typeof rightSidebar !== 'boolean') { - return `custom:${pathname}`; + return 'custom'; } if (pathname.startsWith('/paper/create')) { diff --git a/app/layouts/topbar/pageRoutes.tsx b/app/layouts/topbar/pageRoutes.tsx index 961c236ac..6579fac18 100644 --- a/app/layouts/topbar/pageRoutes.tsx +++ b/app/layouts/topbar/pageRoutes.tsx @@ -12,6 +12,7 @@ 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 { title: string; @@ -19,15 +20,8 @@ export interface PageInfo { } export const ROOT_NAVIGATION_PATHS = new Set([ - '/', - '/following', - '/latest', - '/popular', - '/for-you', - '/feed', + ...HOME_TAB_PATHS, '/earn', - '/fund', - '/fund/proposals', '/journal', '/notebook', '/browse', @@ -47,7 +41,7 @@ interface RouteRule { const ROUTE_RULES: RouteRule[] = [ { - match: (p) => ['/', '/following', '/latest', '/popular', '/for-you', '/feed'].includes(p), + match: (p) => isHomeTabPath(p), getInfo: () => ({ title: 'Home', icon: , @@ -152,7 +146,7 @@ const ROUTE_RULES: RouteRule[] = [ }), }, { - match: (p) => p === '/fund' || p.startsWith('/fund/') || p.startsWith('/grant/'), + match: (p) => p.startsWith('/fund/') || p.startsWith('/grant/'), getInfo: () => ({ title: 'Fund', icon: , diff --git a/app/moderators/ModerationClientLayout.tsx b/app/moderators/ModerationClientLayout.tsx index ae7ea4830..bd58db083 100644 --- a/app/moderators/ModerationClientLayout.tsx +++ b/app/moderators/ModerationClientLayout.tsx @@ -26,7 +26,7 @@ export default function ModerationClientLayout({ children }: ModerationClientLay useEffect(() => { if (!isLoading && !isModerator) { - router.push('/popular'); + router.push('/'); } }, [isLoading, isModerator, router]); diff --git a/app/page.tsx b/app/page.tsx deleted file mode 100644 index 23b44dca0..000000000 --- a/app/page.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Metadata } from 'next'; -import { getServerSession } from 'next-auth/next'; -import { buildOpenGraphMetadata } from '@/lib/metadata'; -import { handleTrendingRedirect } from '@/utils/navigation'; -import { LandingPage } from '@/components/landing/LandingPage'; - -export const metadata: Metadata = buildOpenGraphMetadata({ - title: 'Open Science Community & Research Platform', - description: - 'ResearchHub is a collaborative platform for sharing, reviewing, and funding scientific research. Join the open science community.', - url: '/', -}); - -export default async function Home({ - searchParams, -}: { - searchParams: Promise<{ [key: string]: string | string[] | undefined }>; -}) { - const session = await getServerSession(); - - const resolvedSearchParams = await searchParams; - - const urlSearchParams = new URLSearchParams(); - Object.entries(resolvedSearchParams).forEach(([key, value]) => { - if (typeof value === 'string') { - urlSearchParams.set(key, value); - } else if (Array.isArray(value)) { - if (value.length > 0) { - urlSearchParams.set(key, value[0]); - } - } - }); - - handleTrendingRedirect(!!session?.user, urlSearchParams); - - return ; -} diff --git a/app/robots.ts b/app/robots.ts index e824a1714..f6c1ff37f 100644 --- a/app/robots.ts +++ b/app/robots.ts @@ -15,8 +15,6 @@ export default function robots(): MetadataRoute.Robots { '/dashboard/', '/notifications/', '/activity/', - '/for-you/', - '/following/', '/notebook/', '/moderators/', '/expert-finder/', diff --git a/app/sitemap.ts b/app/sitemap.ts index 3d6ef82c3..e6c74d4f6 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -66,9 +66,7 @@ async function buildSlugSitemap( } const STATIC_ROUTES: MetadataRoute.Sitemap = [ - { url: SITE_URL, changeFrequency: 'daily', priority: 1 }, - { url: `${SITE_URL}/popular`, changeFrequency: 'hourly', priority: 0.9 }, - { url: `${SITE_URL}/latest`, changeFrequency: 'hourly', priority: 0.8 }, + { url: SITE_URL, changeFrequency: 'hourly', priority: 1 }, { url: `${SITE_URL}/fund`, changeFrequency: 'daily', priority: 0.8 }, { url: `${SITE_URL}/fund/proposals`, changeFrequency: 'daily', priority: 0.7 }, { url: `${SITE_URL}/earn`, changeFrequency: 'daily', priority: 0.8 }, diff --git a/components/Funding/FundActivityPageContent.tsx b/components/Funding/FundActivityPageContent.tsx new file mode 100644 index 000000000..cbc6be4a6 --- /dev/null +++ b/components/Funding/FundActivityPageContent.tsx @@ -0,0 +1,39 @@ +'use client'; + +import { useInView } from 'react-intersection-observer'; +import { ActivityCardFull } from '@/components/Activity/ActivityCardFull'; +import { ActivityCardSkeleton } from '@/components/Activity/ActivityCardSkeleton'; +import { useActivityFeed } from '@/hooks/useActivityFeed'; + +export function FundActivityPageContent() { + const { entries, isLoading, isLoadingMore, hasMore, loadMore } = useActivityFeed({}); + + const { ref: sentinelRef } = useInView({ + threshold: 0, + rootMargin: '200px', + onChange: (inView) => { + if (inView && hasMore && !isLoading && !isLoadingMore) { + loadMore(); + } + }, + }); + + return ( +
+ {entries.map((entry) => ( + + ))} + + {(isLoading || isLoadingMore) && + [...Array(6)].map((_, i) => )} + + {!isLoading && !isLoadingMore && entries.length === 0 && ( +
+

No activity found

+
+ )} + + {!isLoading && !isLoadingMore && hasMore &&
} +
+ ); +} diff --git a/components/Funding/FundingBannerSkeleton.tsx b/components/Funding/FundingBannerSkeleton.tsx deleted file mode 100644 index a411457f8..000000000 --- a/components/Funding/FundingBannerSkeleton.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { ResearchCoinSnapshotSkeleton } from '@/components/ResearchCoin/ResearchCoinSnapshot'; - -interface FundingBannerSkeletonProps { - showTabs?: boolean; -} - -export function FundingBannerSkeleton({ showTabs = false }: FundingBannerSkeletonProps) { - return ( -
-
-
-
-
-
-
- -
- -
-
-
- -
- -
-
- - {showTabs && ( -
-
-
-
-
-
-
-
-
-
-
-
- )} -
-
- ); -} diff --git a/components/Funding/HomeTabs.tsx b/components/Funding/HomeTabs.tsx new file mode 100644 index 000000000..de1b63e62 --- /dev/null +++ b/components/Funding/HomeTabs.tsx @@ -0,0 +1,20 @@ +'use client'; + +import { FeedTabs } from '@/components/Feed/FeedTabs'; +import { useContentTabsVisibilitySentinel } from '@/hooks/useContentTabsVisibilitySentinel'; +import { useFundTabs } from '@/hooks/useFundTabs'; + +/** + * Homepage hub tabs (Activity / Fund / Proposals). Pill style matches the old + * for-you feed; the sentinel lifts a sticky copy into the TopBar on scroll. + */ +export function HomeTabs() { + const { tabs, highlightedTab, handleTabChange } = useFundTabs(); + const tabsSentinelRef = useContentTabsVisibilitySentinel(); + + return ( +
+ +
+ ); +} diff --git a/components/Funding/MarketplaceCards.tsx b/components/Funding/MarketplaceCards.tsx deleted file mode 100644 index fa7357a94..000000000 --- a/components/Funding/MarketplaceCards.tsx +++ /dev/null @@ -1,32 +0,0 @@ -'use client'; - -import { Tabs } from '@/components/ui/Tabs'; -import { useUser } from '@/contexts/UserContext'; -import { useContentTabsVisibilitySentinel } from '@/hooks/useContentTabsVisibilitySentinel'; -import { FUND_TABS, type FundTab } from '@/hooks/useFundTabs'; - -export type MarketplaceTab = FundTab; - -interface MarketplaceCardsProps { - selected?: MarketplaceTab; -} - -export function MarketplaceCards({ selected = 'grants' }: MarketplaceCardsProps) { - const { user, isLoading: isLoadingUser } = useUser(); - // Pull the tabs up only when the panel is or will be taller (snapshot visible). - // Logged-out users see just the CTA (shorter panel), so no offset is needed. - const snapshotVisible = isLoadingUser || !!user; - const tabsSentinelRef = useContentTabsVisibilitySentinel(); - - return ( -
- {}} - variant="primary" - className="mt-4" - /> -
- ); -} diff --git a/components/landing/LandingPageHero.tsx b/components/landing/LandingPageHero.tsx index 3feee2a37..f293dc4b8 100644 --- a/components/landing/LandingPageHero.tsx +++ b/components/landing/LandingPageHero.tsx @@ -16,7 +16,7 @@ export function LandingPageHero() { }; const handleExplore = () => { - router.push('/popular'); + router.push('/'); }; return ( diff --git a/hooks/useFeedTabs.ts b/hooks/useFeedTabs.ts index 384def73d..57caa33ed 100644 --- a/hooks/useFeedTabs.ts +++ b/hooks/useFeedTabs.ts @@ -18,7 +18,9 @@ export const useFeedTabs = (onBeforeNavigate?: () => void) => { const isTopicPage = pathname.startsWith('/topic/'); const isJournalPage = pathname.startsWith('/journal'); - const isHomeFeedPage = ['/', '/following', '/latest', '/popular', '/for-you', '/feed'].includes( + // Legacy research-feed routes (redirected to `/` for homepage hub). + // `/` is now the Activity hub tab — not a research feed. + const isHomeFeedPage = ['/following', '/latest', '/popular', '/for-you', '/feed'].includes( pathname ); diff --git a/hooks/useFundTabs.ts b/hooks/useFundTabs.ts deleted file mode 100644 index 78c44293b..000000000 --- a/hooks/useFundTabs.ts +++ /dev/null @@ -1,65 +0,0 @@ -'use client'; - -import { useMemo } from 'react'; -import { usePathname, useRouter } from 'next/navigation'; -import { ArrowDownCircle, ArrowUpCircle } from 'lucide-react'; - -export type FundTab = 'grants' | 'proposals'; - -export const FUND_TABS = [ - { - id: 'grants' as const, - label: 'Funding Opportunities', - href: '/fund', - icon: ArrowDownCircle, - iconClassName: 'w-5 h-5', - activeClassName: 'text-emerald-600 border-b-emerald-600', - }, - { - id: 'proposals' as const, - label: 'Proposals', - href: '/fund/proposals', - icon: ArrowUpCircle, - iconClassName: 'w-5 h-5', - activeClassName: 'text-primary-600 border-b-primary-600', - }, -]; - -export function useFundTabs() { - const pathname = usePathname(); - const router = useRouter(); - - const isFundPage = pathname === '/fund' || pathname === '/fund/proposals'; - - const activeTab = useMemo((): FundTab => { - if (pathname === '/fund/proposals') return 'proposals'; - return 'grants'; - }, [pathname]); - - const tabs = useMemo(() => FUND_TABS, []); - - const handleTabChange = (tab: string, e?: React.MouseEvent) => { - if (tab === activeTab) { - e?.preventDefault(); - return; - } - - const href = FUND_TABS.find((t) => t.id === tab)?.href; - if (!href) return; - - if (e && !e.metaKey && !e.ctrlKey && !e.shiftKey && e.button === 0) { - e.preventDefault(); - router.push(href); - } else if (!e) { - router.push(href); - } - }; - - return { - tabs, - activeTab, - highlightedTab: activeTab, - handleTabChange, - isFundPage, - }; -} diff --git a/hooks/useFundTabs.tsx b/hooks/useFundTabs.tsx new file mode 100644 index 000000000..8a099685b --- /dev/null +++ b/hooks/useFundTabs.tsx @@ -0,0 +1,97 @@ +'use client'; + +import { useMemo } from 'react'; +import { usePathname, useRouter } from 'next/navigation'; +import { FileText, Waves, type LucideIcon, type LucideProps } from 'lucide-react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faBullhorn } from '@fortawesome/free-solid-svg-icons'; +import { useScrollContainer } from '@/contexts/ScrollContainerContext'; + +/** Sized via PillTabs' className to match Lucide stroke icons in the pills. */ +function BullhornIcon({ className }: LucideProps) { + return ; +} + +export type FundTab = 'activity' | 'fund' | 'proposals'; + +/** Routes that make up the homepage hub — all of them highlight "Home" in the nav. */ +export const HOME_TAB_PATHS = ['/', '/fund', '/fund/proposals']; + +export const isHomeTabPath = (pathname: string) => HOME_TAB_PATHS.includes(pathname); + +export const FUND_TABS = [ + { + id: 'activity' as const, + label: 'Activity', + href: '/', + icon: Waves, + scroll: false, + }, + { + id: 'fund' as const, + label: 'Fund', + href: '/fund', + icon: BullhornIcon as LucideIcon, + scroll: false, + }, + { + id: 'proposals' as const, + label: 'Proposals', + href: '/fund/proposals', + icon: FileText, + scroll: false, + }, +]; + +/** Homepage hub: Activity / Fund / Proposals (shared shell + FundSidebar). */ +export function useFundTabs() { + const pathname = usePathname(); + const router = useRouter(); + const scrollContainerRef = useScrollContainer(); + + const isFundPage = isHomeTabPath(pathname); + + const activeTab = useMemo((): FundTab => { + if (pathname === '/fund/proposals') return 'proposals'; + if (pathname === '/fund') return 'fund'; + return 'activity'; + }, [pathname]); + + const tabs = useMemo(() => FUND_TABS, []); + + const scrollToTop = () => { + const container = scrollContainerRef?.current; + if (container) { + container.scrollTop = 0; + } else { + window.scrollTo(0, 0); + } + }; + + const handleTabChange = (tab: string, e?: React.MouseEvent) => { + if (tab === activeTab) { + e?.preventDefault(); + return; + } + + const href = FUND_TABS.find((t) => t.id === tab)?.href; + if (!href) return; + + if (e && !e.metaKey && !e.ctrlKey && !e.shiftKey && e.button === 0) { + e.preventDefault(); + scrollToTop(); + router.push(href, { scroll: false }); + } else if (!e) { + scrollToTop(); + router.push(href, { scroll: false }); + } + }; + + return { + tabs, + activeTab, + highlightedTab: activeTab, + handleTabChange, + isFundPage, + }; +} diff --git a/utils/navigation.ts b/utils/navigation.ts index 74dac91e5..865bcf316 100644 --- a/utils/navigation.ts +++ b/utils/navigation.ts @@ -124,27 +124,15 @@ export function handleMissingSlugRedirect( } /** - * Handles redirection to trending page if user is authorized - * @param isUserLoggedIn Whether the user is logged in - * @param searchParams Optional search parameters to preserve in the redirect + * Redirects into the homepage Activity hub. + * Preserves search parameters when provided. */ -export function handleTrendingRedirect(isUserLoggedIn: boolean, searchParams?: URLSearchParams) { - if (isUserLoggedIn) { - let redirectUrl = '/for-you'; +export function handleTrendingRedirect(_isUserLoggedIn: boolean, searchParams?: URLSearchParams) { + let redirectUrl = '/'; - // Preserve search parameters if provided - if (searchParams && searchParams.toString()) { - redirectUrl += `?${searchParams.toString()}`; - } - - redirect(redirectUrl); - } else { - let popularUrl = '/popular'; - - if (searchParams && searchParams.toString()) { - popularUrl += `?${searchParams.toString()}`; - } - - redirect(popularUrl); + if (searchParams && searchParams.toString()) { + redirectUrl += `?${searchParams.toString()}`; } + + redirect(redirectUrl); } From 2134f1181757cfeb6189d0730d23dd9ab2ccc637 Mon Sep 17 00:00:00 2001 From: nicktytarenko Date: Sun, 2 Aug 2026 17:35:33 +0300 Subject: [PATCH 2/2] cleanup --- app/(home)/layout.tsx | 6 ------ components/Funding/HomeTabs.tsx | 4 ---- hooks/useFeedTabs.ts | 16 +++++++++------- hooks/useFundTabs.tsx | 4 +--- 4 files changed, 10 insertions(+), 20 deletions(-) diff --git a/app/(home)/layout.tsx b/app/(home)/layout.tsx index 7c9573e32..e77f8c13c 100644 --- a/app/(home)/layout.tsx +++ b/app/(home)/layout.tsx @@ -3,12 +3,6 @@ import { PageLayout } from '@/app/layouts/PageLayout'; import { FundSidebar } from '@/components/Funding/FundSidebar'; import { HomeTabs } from '@/components/Funding/HomeTabs'; -/** - * Shared shell for homepage hub tabs (Activity / Fund / Proposals). - * Keeps PageLayout, HomeTabs, and FundSidebar mounted while only the feed slot swaps. - * - * `/fund/dashboard` lives outside this group and keeps its own layout. - */ export default function HomeLayout({ children }: { children: ReactNode }) { return ( }> diff --git a/components/Funding/HomeTabs.tsx b/components/Funding/HomeTabs.tsx index de1b63e62..d23500658 100644 --- a/components/Funding/HomeTabs.tsx +++ b/components/Funding/HomeTabs.tsx @@ -4,10 +4,6 @@ import { FeedTabs } from '@/components/Feed/FeedTabs'; import { useContentTabsVisibilitySentinel } from '@/hooks/useContentTabsVisibilitySentinel'; import { useFundTabs } from '@/hooks/useFundTabs'; -/** - * Homepage hub tabs (Activity / Fund / Proposals). Pill style matches the old - * for-you feed; the sentinel lifts a sticky copy into the TopBar on scroll. - */ export function HomeTabs() { const { tabs, highlightedTab, handleTabChange } = useFundTabs(); const tabsSentinelRef = useContentTabsVisibilitySentinel(); diff --git a/hooks/useFeedTabs.ts b/hooks/useFeedTabs.ts index 57caa33ed..920ca8aef 100644 --- a/hooks/useFeedTabs.ts +++ b/hooks/useFeedTabs.ts @@ -18,15 +18,17 @@ export const useFeedTabs = (onBeforeNavigate?: () => void) => { const isTopicPage = pathname.startsWith('/topic/'); const isJournalPage = pathname.startsWith('/journal'); - // Legacy research-feed routes (redirected to `/` for homepage hub). - // `/` is now the Activity hub tab — not a research feed. - const isHomeFeedPage = ['/following', '/latest', '/popular', '/for-you', '/feed'].includes( - pathname - ); + const isPersonalizedFeedPage = [ + '/following', + '/latest', + '/popular', + '/for-you', + '/feed', + ].includes(pathname); const isFeedPage = useMemo( - () => isHomeFeedPage || isTopicPage || isJournalPage, - [isHomeFeedPage, isTopicPage, isJournalPage] + () => isPersonalizedFeedPage || isTopicPage || isJournalPage, + [isPersonalizedFeedPage, isTopicPage, isJournalPage] ); const topicSlug = isTopicPage ? pathname.split('/')[2] : null; diff --git a/hooks/useFundTabs.tsx b/hooks/useFundTabs.tsx index 8a099685b..836e88703 100644 --- a/hooks/useFundTabs.tsx +++ b/hooks/useFundTabs.tsx @@ -7,14 +7,12 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faBullhorn } from '@fortawesome/free-solid-svg-icons'; import { useScrollContainer } from '@/contexts/ScrollContainerContext'; -/** Sized via PillTabs' className to match Lucide stroke icons in the pills. */ function BullhornIcon({ className }: LucideProps) { return ; } export type FundTab = 'activity' | 'fund' | 'proposals'; -/** Routes that make up the homepage hub — all of them highlight "Home" in the nav. */ export const HOME_TAB_PATHS = ['/', '/fund', '/fund/proposals']; export const isHomeTabPath = (pathname: string) => HOME_TAB_PATHS.includes(pathname); @@ -43,7 +41,7 @@ export const FUND_TABS = [ }, ]; -/** Homepage hub: Activity / Fund / Proposals (shared shell + FundSidebar). */ +/** Homepage tabs: Activity / Fund / Proposals. */ export function useFundTabs() { const pathname = usePathname(); const router = useRouter();