Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions app/(home)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@ import { ReactNode } from 'react';
import { PageLayout } from '@/app/layouts/PageLayout';
import { FundSidebar } from '@/components/Funding/FundSidebar';
import { HomeTabs } from '@/components/Funding/HomeTabs';
import { HomeFeedsProvider } from '@/components/Funding/HomeFeedsProvider';

/**
* Shared shell for homepage hub tabs (Activity / Fund / Proposals).
* Keeps PageLayout, HomeTabs, and FundSidebar mounted while only the feed slot swaps.
* Shared shell for homepage tabs (Activity / Fund / Proposals).
* Keeps PageLayout, HomeTabs, feed providers, 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 (
<PageLayout rightSidebar={<FundSidebar />}>
<HomeTabs />
{children}
<HomeFeedsProvider>
<HomeTabs />
{children}
</HomeFeedsProvider>
</PageLayout>
);
}
38 changes: 36 additions & 2 deletions app/activity/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { useCallback, useEffect, useMemo, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
import { useInView } from 'react-intersection-observer';
import { LayoutList, Star, Coins, Reply } from 'lucide-react';
import { PageLayout } from '@/app/layouts/PageLayout';
Expand All @@ -10,6 +10,8 @@ import { PillTabs } from '@/components/ui/PillTabs';
import { ActivityCardFull } from '@/components/Activity/ActivityCardFull';
import { ActivityCardSkeleton } from '@/components/Activity/ActivityCardSkeleton';
import { useActivityFeed, ActivityTab } from '@/hooks/useActivityFeed';
import { useFeedScrollTracking } from '@/hooks/useFeedScrollTracking';
import { getFeedKey } from '@/contexts/NavigationContext';
import { ActivityScope } from '@/services/activity.service';
import { GrantService } from '@/services/grant.service';

Expand All @@ -31,6 +33,7 @@ function isValidTab(value: string | null): value is ActivityTab {

export default function ActivityPage() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();

const tabParam = searchParams.get('tab');
Expand All @@ -50,11 +53,42 @@ export default function ActivityPage() {
});
}, [grantIdParam]);

const { entries, isLoading, isLoadingMore, hasMore, loadMore } = useActivityFeed({
const {
entries,
isLoading,
isLoadingMore,
hasMore,
page,
loadMore,
restoredScrollPosition,
lastClickedEntryId,
restorationTab,
} = useActivityFeed({
scope,
grantId: grantIdParam || undefined,
});

const feedKey = useMemo(() => {
const queryParams: Record<string, string> = {};
for (const [key, value] of searchParams) {
queryParams[key] = value;
}
return getFeedKey({
pathname,
tab: restorationTab,
queryParams: Object.keys(queryParams).length > 0 ? queryParams : undefined,
});
}, [pathname, restorationTab, searchParams]);

useFeedScrollTracking({
feedKey,
entries,
hasMore,
page,
restoredScrollPosition,
lastClickedEntryId: lastClickedEntryId ?? undefined,
});

const { ref: sentinelRef } = useInView({
threshold: 0,
rootMargin: '200px',
Expand Down
35 changes: 11 additions & 24 deletions app/fund/FundGrantsPageContent.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,24 @@
'use client';

import { useMemo, useState } from 'react';
import { useEffect } from 'react';
import { FeedContent } from '@/components/Feed/FeedContent';
import { useFeed } from '@/hooks/useFeed';
import { GrantSortAndFilters } from '@/components/Funding/GrantSortAndFilters';
import type { GrantSortOption } from '@/components/Funding/lib/grantSortConfig';
import { useGrantFeed } from '@/contexts/GrantFeedContext';

export function FundGrantsPageContent() {
const [grantSort, setGrantSort] = useState<GrantSortOption>('newest');
const { entries, isLoading, hasMore, loadMore, sortBy, setSortBy, activate } = useGrantFeed();

const grantFeedOptions = useMemo(
() => ({
endpoint: 'grant_feed' as const,
contentType: 'GRANT',
ordering: grantSort,
}),
[grantSort]
);

const {
entries: grantEntries,
isLoading: isGrantFeedLoading,
hasMore: hasMoreGrants,
loadMore: loadMoreGrants,
} = useFeed('all', grantFeedOptions);
useEffect(() => {
activate();
}, [activate]);

return (
<FeedContent
entries={grantEntries}
isLoading={isGrantFeedLoading}
hasMore={hasMoreGrants}
loadMore={loadMoreGrants}
filters={<GrantSortAndFilters sortBy={grantSort} onSortChange={setGrantSort} />}
entries={entries}
isLoading={isLoading}
hasMore={hasMore}
loadMore={loadMore}
filters={<GrantSortAndFilters sortBy={sortBy} onSortChange={setSortBy} />}
skeletonVariant="grant"
showGrantHeaders={false}
showPostHeaders={false}
Expand Down
2 changes: 2 additions & 0 deletions app/grant/[id]/[slug]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { isDeadlineInFuture } from '@/utils/date';
import { GrantTabProvider } from '@/components/Funding/GrantPageContent';
import { WorkHeaderGrant } from '@/components/work/WorkHeader/index';
import { RegisteredReportRouteTrackerLoader } from '@/components/work/RegisteredReportRouteTrackerLoader';
import { SearchHistoryTracker } from '@/components/work/SearchHistoryTracker';

interface Props {
params: Promise<{
Expand Down Expand Up @@ -96,6 +97,7 @@ export default async function GrantSlugLayout({ params, children }: Props) {
}
>
{children}
<SearchHistoryTracker work={work} />
</PageLayout>
</GrantTabProvider>
);
Expand Down
20 changes: 8 additions & 12 deletions app/layouts/LeftSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,21 +55,17 @@ export const LeftSidebar: React.FC<LeftSidebarProps> = ({ forceMinimize = false
</Link>
</div>

<div className={`px-4 mt-6 ${forceMinimize ? '!px-2' : 'tablet:max-sidebar-compact:!px-2'}`}>
<div
className={`mt-6 px-3 ${forceMinimize ? '!flex !justify-center !px-2' : 'tablet:max-sidebar-compact:!flex tablet:max-sidebar-compact:!justify-center tablet:max-sidebar-compact:!px-2'}`}
>
<PublishMenu forceMinimize={forceMinimize} />
</div>

<div className="flex-1 mt-2 overflow-y-auto">
<div
className={`px-4 py-4 ${forceMinimize ? '!px-2' : 'tablet:max-sidebar-compact:!px-2'}`}
>
<Navigation
currentPath={pathname || ''}
onUnimplementedFeature={handleUnimplementedFeature}
forceMinimize={forceMinimize}
/>
</div>
</div>
<Navigation
currentPath={pathname || ''}
onUnimplementedFeature={handleUnimplementedFeature}
forceMinimize={forceMinimize}
/>

<div className={forceMinimize ? '!hidden' : 'tablet:max-sidebar-compact:!hidden'}>
<FooterLinks />
Expand Down
122 changes: 67 additions & 55 deletions app/layouts/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import { useAuthenticatedAction } from '@/contexts/AuthModalContext';
import { useRouter } from 'next/navigation';
import { useCallback } from 'react';
import Link from 'next/link';
import Icon from '@/components/ui/icons/Icon';
import { IconName } from '@/components/ui/icons/Icon';
Expand All @@ -13,6 +12,7 @@
import { Badge } from '@/components/ui/Badge';
import { useDismissableFeature } from '@/hooks/useDismissableFeature';
import { isHomeTabPath } from '@/hooks/useFundTabs';
import { cn } from '@/utils/styles';

const ENDOWMENT_NAV_FEATURE = 'endowment_nav_new_badge';
// Stop showing the "New" badge on the Endowment nav item after this date,
Expand Down Expand Up @@ -80,9 +80,6 @@
onUnimplementedFeature,
forceMinimize = false,
}) => {
const { executeAuthenticatedAction } = useAuthenticatedAction();
const router = useRouter();

// Dismissable "New" badge for the Endowment nav item. Lifted to the parent
// so the click handler in NavLink can call dismissFeature() without each
// NavLink unconditionally calling the hook.
Expand All @@ -92,13 +89,6 @@
dismissStatus: endowmentBadgeStatus,
} = useDismissableFeature(ENDOWMENT_NAV_FEATURE);

const handleNavigate = useCallback(
(href: string) => {
router.push(href);
},
[router]
);

const navigationItems: NavigationItem[] = [
{
label: 'Home',
Expand All @@ -114,6 +104,13 @@
requiresAuth: true,
description: 'Track the impact of the research you fund',
},
{
label: 'Notebook',
href: '/notebook',
iconKey: 'notebook',
requiresAuth: true,
description: 'Access your research notebook',
},
{
label: 'Peer Review',
href: '/earn',
Expand All @@ -136,17 +133,18 @@
},
];

const getButtonStyles = (path: string, currentPath: string) => {
const getButtonStyles = (path: string) => {
const isActive = isPathActive(path);

// Use either responsive or force minimized classes
const responsiveClasses = forceMinimize
? '!px-2 !justify-center'
: 'tablet:max-sidebar-compact:!px-2 tablet:max-sidebar-compact:!justify-center';

return isActive
? `flex items-center w-full px-5 py-3.5 text-[15px] font-medium text-primary-600 ${responsiveClasses} bg-primary-50 rounded-lg group`
: `flex items-center w-full px-5 py-3.5 text-[15px] font-medium text-gray-700 ${responsiveClasses} hover:bg-gray-50 rounded-lg group`;
return cn(
'flex w-full items-center rounded-lg px-3 py-2.5 text-[15px] transition-colors',
forceMinimize
? '!justify-center !px-2'
: 'tablet:max-sidebar-compact:!justify-center tablet:max-sidebar-compact:!px-2',
isActive
? 'bg-primary-50 font-semibold text-primary-600'
: 'font-medium text-gray-700 hover:bg-gray-50'
);
};

const isPathActive = (path: string) => {
Expand Down Expand Up @@ -182,10 +180,10 @@
showNewBadge?: boolean;
/** Fired alongside navigation to mark the new-badge feature as seen. */
onDismissNew?: () => void;
}> = ({ item, currentPath, onUnimplementedFeature, showNewBadge, onDismissNew }) => {

Check failure on line 183 in app/layouts/Navigation.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 28 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=ResearchHub_web&issues=AZ_T3IvT7s3WKsGdNXNn&open=AZ_T3IvT7s3WKsGdNXNn&pullRequest=997
const { executeAuthenticatedAction } = useAuthenticatedAction();
const router = useRouter();
const buttonStyles = getButtonStyles(item.href, currentPath);
const buttonStyles = getButtonStyles(item.href);
const isActive = isPathActive(item.href);

// Set icon colors based on active state
Expand Down Expand Up @@ -220,26 +218,32 @@
// Determine if the current item is the Home item using FontAwesome
const isHomeIcon = item.isFontAwesome && item.iconKey === 'home';

// Conditionally apply minimized classes
const iconContainerClass = forceMinimize
? 'h-[26px] w-[26px] mr-0 flex items-center justify-center flex-shrink-0'
: 'h-[26px] w-[26px] mr-4 tablet:max-sidebar-compact:!mr-0 flex items-center justify-center flex-shrink-0';
const iconContainerClass = cn(
'flex h-[26px] w-[26px] flex-shrink-0 items-center justify-center',
forceMinimize ? 'mr-0' : 'mr-3.5 tablet:max-sidebar-compact:!mr-0'
);

const textContainerClass = forceMinimize
? 'flex items-center justify-between w-full min-w-0 !hidden'
: 'w-full min-w-0 tablet:max-sidebar-compact:!hidden';
? 'hidden'
: 'flex w-full min-w-0 items-center tablet:max-sidebar-compact:!hidden';

return (
<Link href={item.href} onClick={handleClick} className={buttonStyles} scroll={false}>
<Link
href={item.href}
onClick={handleClick}
className={buttonStyles}
aria-current={isActive ? 'page' : undefined}
scroll={false}
>
<div className={iconContainerClass}>
{isHomeIcon ? (
<FontAwesomeIcon
icon={isActive ? faHouseSolid : faHouseLight}
fontSize={20}
fontSize={22}
color={iconColor}
/>
) : item.isLucideSprout ? (
<Sprout size={22} color={iconColor} strokeWidth={isActive ? 2.25 : 2} />
<Sprout size={24} color={iconColor} strokeWidth={isActive ? 2.25 : 2} />
) : item.isLucideStar ? (
<Star
size={22}
Expand All @@ -254,7 +258,7 @@
)}
</div>
<div className={textContainerClass}>
<span className="inline-flex items-center gap-2 truncate text-[16px] font-semibold">
<span className="inline-flex min-w-0 items-center gap-2 truncate">
{item.label}
{showNewBadge && (
<Badge
Expand All @@ -272,29 +276,37 @@
};

return (
<div className="space-y-1.5">
{navigationItems.map((item) => {
const isBeforeEndowmentCutoff = Date.now() < ENDOWMENT_NEW_BADGE_CUTOFF.getTime();
const isEndowmentNewBadge =
item.newFeatureName === ENDOWMENT_NAV_FEATURE &&
item.isNew === true &&
isBeforeEndowmentCutoff &&
endowmentBadgeStatus === 'checked' &&
!isEndowmentBadgeDismissed;

return (
<NavLink
key={item.href}
item={item}
currentPath={currentPath}
onUnimplementedFeature={onUnimplementedFeature}
showNewBadge={isEndowmentNewBadge}
onDismissNew={
item.newFeatureName === ENDOWMENT_NAV_FEATURE ? dismissEndowmentBadge : undefined
}
/>
);
})}
</div>
<nav
aria-label="Primary navigation"
className={cn(
'flex-1 overflow-y-auto px-3 pt-6',
forceMinimize ? '!px-2' : 'tablet:max-sidebar-compact:!px-2'
)}
>
<div className="space-y-2">
{navigationItems.map((item) => {
const isBeforeEndowmentCutoff = Date.now() < ENDOWMENT_NEW_BADGE_CUTOFF.getTime();
const isEndowmentNewBadge =
item.newFeatureName === ENDOWMENT_NAV_FEATURE &&
item.isNew === true &&
isBeforeEndowmentCutoff &&
endowmentBadgeStatus === 'checked' &&
!isEndowmentBadgeDismissed;

return (
<NavLink
key={item.href}
item={item}
currentPath={currentPath}
onUnimplementedFeature={onUnimplementedFeature}
showNewBadge={isEndowmentNewBadge}
onDismissNew={
item.newFeatureName === ENDOWMENT_NAV_FEATURE ? dismissEndowmentBadge : undefined
}
/>
);
})}
</div>
</nav>
);
};
Loading