Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
fa373a2
fund sorting dropdown
Oct 16, 2025
575531a
small optimizations
Oct 16, 2025
61c5c74
making newest the new default
Oct 16, 2025
618f8e5
replacing best with hot score
Oct 16, 2025
3459560
putting newest at top of the dropdown
Oct 16, 2025
085de02
Merge branch 'main' of https://github.com/ResearchHub/web into fund_s…
Oct 16, 2025
fbceb84
adding skeleton loader and setting Raised to Amount for grants
Oct 17, 2025
4f885f4
small fixes for skeleton loader on sorting change
Oct 17, 2025
969b49d
clearing entries to show skeleton loader properly
Oct 17, 2025
d78fd36
more tests for skeleton loader
Oct 17, 2025
4337a70
more fixes for skeleton loader not properly loading
Oct 17, 2025
056a46a
Merge branch 'main' of https://github.com/ResearchHub/web into fund_s…
Oct 20, 2025
144f718
Merge branch 'main' of https://github.com/ResearchHub/web into fund_s…
Oct 21, 2025
87040e0
[Fund Sorting] Changing Options to match expected backend parameters
Oct 22, 2025
cd9859a
[Fund Sorting] Reverting Previous changes
Oct 22, 2025
d05f9bc
Merge branch 'main' of https://github.com/ResearchHub/web into fund_s…
Oct 23, 2025
e2cb6f0
Merge branch 'main' of https://github.com/ResearchHub/web into fund_s…
Oct 27, 2025
f5faa90
[Fund Sorting] Updating UI for new parameters we want, hiding drop do…
Oct 28, 2025
baff4ab
[Fund Sorting] Sticking with 'sort_by' instead of 'sort' for the base…
Oct 28, 2025
46656d0
[Fund Sorting] removed includeEnded parameter when switching to Previ…
Oct 28, 2025
75f95ec
[Fund Sorting] Updated sort_by to the more DRF standard ordering para…
Oct 28, 2025
70c640c
[Fund Sorting] Renaming Applicants to Funders for Proposals
Oct 29, 2025
6cb02fb
[Fund Sorting] Small style tweaks for better mobile UX
Oct 29, 2025
29646f4
[Fund Sorting] Optimizations and refinements
Oct 29, 2025
0e073d9
[Fund Sorting] Linter Fixes
Oct 29, 2025
604faf0
[Fund Sorting] More Linter Fixes
Oct 29, 2025
b18ac1e
[Fund Sorting] changing snake/camel cases and using star for Best
Oct 29, 2025
49705a3
[Fund Sorting] Remove dependency on state variable isSortChanging
yattias Oct 29, 2025
f6c8d76
[Fund Sorting] Small Include Ended toggle switch style fix
Oct 29, 2025
c5da452
[Funding sorting] shim to use sort_by in homepage
yattias Oct 29, 2025
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
100 changes: 50 additions & 50 deletions app/fund/components/FundPageContent.tsx
Original file line number Diff line number Diff line change
@@ -1,77 +1,77 @@
'use client';

import { useRouter, useSearchParams } from 'next/navigation';
import { PageLayout } from '@/app/layouts/PageLayout';
import { useFeed } from '@/hooks/useFeed';
import { FeedContent } from '@/components/Feed/FeedContent';
import { FundRightSidebar } from '@/components/Fund/FundRightSidebar';
import { GrantRightSidebar } from '@/components/Fund/GrantRightSidebar';
import { MainPageHeader } from '@/components/ui/MainPageHeader';
import { MarketplaceTabs, MarketplaceTab } from '@/components/Fund/MarketplaceTabs';
import {
MarketplaceTabs,
MarketplaceTab,
FundingSortOption,
} from '@/components/Fund/MarketplaceTabs';
import Icon from '@/components/ui/icons/Icon';
import { createTabConfig } from '@/components/Fund/lib/FundingFeedConfig';

interface FundPageContentProps {
marketplaceTab: MarketplaceTab;
}

export function FundPageContent({ marketplaceTab }: FundPageContentProps) {
const getFundraiseStatus = (tab: MarketplaceTab): 'OPEN' | 'CLOSED' | undefined => {
if (tab === 'needs-funding') return 'OPEN';
if (tab === 'previously-funded') return 'CLOSED';
return undefined;
};

const getOrdering = (tab: MarketplaceTab): string | undefined => {
if (tab === 'needs-funding') return 'amount_raised';
return undefined;
};

const { entries, isLoading, hasMore, loadMore } = useFeed('all', {
contentType: marketplaceTab === 'grants' ? 'GRANT' : 'PREREGISTRATION',
endpoint: marketplaceTab === 'grants' ? 'grant_feed' : 'funding_feed',
fundraiseStatus: getFundraiseStatus(marketplaceTab),
ordering: getOrdering(marketplaceTab),
});
const router = useRouter();
const searchParams = useSearchParams();
const sortBy = (searchParams.get('ordering') as FundingSortOption) || '';
const includeEnded = searchParams.get('include_ended') !== 'false';
const TAB_CONFIG = createTabConfig(<GrantRightSidebar />, <FundRightSidebar />);
const config = TAB_CONFIG[marketplaceTab];

const getTitle = (tab: MarketplaceTab): string => {
switch (tab) {
case 'grants':
return 'Request for Proposals';
case 'needs-funding':
return 'Proposals';
case 'previously-funded':
return 'Previously Funded';
default:
return '';
const handleSortChange = (newSort: FundingSortOption) => {
const params = new URLSearchParams(searchParams.toString());
if (newSort) {
params.set('ordering', newSort);
} else {
params.delete('ordering');
}
router.push(`?${params.toString()}`, { scroll: false });
};

const getSubtitle = (tab: MarketplaceTab): string => {
switch (tab) {
case 'grants':
return 'Explore available funding opportunities';
case 'needs-funding':
return 'Fund breakthrough research shaping tomorrow';
case 'previously-funded':
return 'Browse research that has been successfully funded';
default:
return '';
const handleIncludeEndedChange = (newIncludeEnded: boolean) => {
const params = new URLSearchParams(searchParams.toString());
if (newIncludeEnded) {
// Default is true, so we don't need to set the parameter
params.delete('include_ended');
} else {
// Only set parameter when explicitly excluding ended proposals
params.set('include_ended', 'false');
}
router.push(`?${params.toString()}`, { scroll: false });
};

const header = (
<MainPageHeader
icon={<Icon name="solidHand" size={26} color="#3971ff" />}
title={getTitle(marketplaceTab)}
subtitle={getSubtitle(marketplaceTab)}
/>
);

const rightSidebar = marketplaceTab === 'grants' ? <GrantRightSidebar /> : <FundRightSidebar />;
const { entries, isLoading, hasMore, loadMore } = useFeed('all', {
contentType: config.contentType,
endpoint: config.endpoint,
fundraiseStatus: config.fundraiseStatus,
ordering: sortBy || undefined,
includeEnded: includeEnded,
});

return (
<PageLayout rightSidebar={rightSidebar}>
{header}
<MarketplaceTabs activeTab={marketplaceTab} onTabChange={() => {}} />
<PageLayout rightSidebar={config.sidebar}>
<MainPageHeader
icon={<Icon name="solidHand" size={26} color="#3971ff" />}
title={config.title}
subtitle={config.subtitle}
/>
<MarketplaceTabs
activeTab={marketplaceTab}
onTabChange={() => {}}
sortBy={sortBy}
onSortChange={handleSortChange}
includeEnded={includeEnded}
onIncludeEndedChange={handleIncludeEndedChange}
/>
<FeedContent
entries={entries}
isLoading={isLoading}
Expand Down
2 changes: 2 additions & 0 deletions app/fund/previously-funded/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client';

import { FundPageContent } from '../components/FundPageContent';

export default function PreviouslyFundedPage() {
Expand Down
126 changes: 94 additions & 32 deletions components/Fund/MarketplaceTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,66 +2,128 @@

import { FC } from 'react';
import { Tabs } from '@/components/ui/Tabs';
import { useRouter } from 'next/navigation';
import { useRouter, useSearchParams } from 'next/navigation';
import { BaseMenu, BaseMenuItem } from '@/components/ui/form/BaseMenu';
import { Button } from '@/components/ui/Button';
import { Switch } from '@/components/ui/Switch';
import { ChevronDown } from 'lucide-react';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { useIsMobile } from '@/hooks/useIsMobile';
import { getSortOptions } from './lib/FundingFeedConfig';

export type MarketplaceTab = 'grants' | 'needs-funding' | 'previously-funded';
export type FundingSortOption = '' | 'upvotes' | 'most_applicants' | 'amount_raised';

const getTabs = (isMobile: boolean) => [
{ id: 'grants' as const, label: isMobile ? 'RFPs' : 'Request for Proposals' },
{ id: 'needs-funding' as const, label: 'Proposals' },
{ id: 'previously-funded' as const, label: 'Previously Funded' },
];

const TAB_ROUTES: Record<MarketplaceTab, string> = {
grants: '/fund/grants',
'needs-funding': '/fund/needs-funding',
'previously-funded': '/fund/previously-funded',
};

interface MarketplaceTabsProps {
activeTab: MarketplaceTab;
onTabChange: (tab: MarketplaceTab) => void;
disableTabs?: boolean;
sortBy: FundingSortOption;
onSortChange: (sort: FundingSortOption) => void;
includeEnded: boolean;
onIncludeEndedChange: (includeEnded: boolean) => void;
}

export const MarketplaceTabs: FC<MarketplaceTabsProps> = ({
activeTab,
onTabChange,
disableTabs,
sortBy,
onSortChange,
includeEnded,
onIncludeEndedChange,
}) => {
const router = useRouter();

const tabs = [
{
id: 'grants',
label: 'Request for Proposals',
},
{
id: 'needs-funding',
label: 'Proposals',
},
{
id: 'previously-funded',
label: 'Previously Funded',
},
];
const searchParams = useSearchParams();
const isMobile = useIsMobile();
const sortOptions = getSortOptions(activeTab);
const tabs = getTabs(isMobile);

const handleTabChange = (tabId: string) => {
if (disableTabs) return;

const tab = tabId as MarketplaceTab;

// Navigate to the appropriate route
if (tab === 'grants') {
router.push('/fund/grants');
} else if (tab === 'needs-funding') {
router.push('/fund/needs-funding');
} else if (tab === 'previously-funded') {
router.push('/fund/previously-funded');
// If switching to previously-funded tab, clear all parameters
if (tab === 'previously-funded') {
// Also call onSortChange to update the parent component's state
onSortChange('');
// Call onIncludeEndedChange to update the parent component's state
onIncludeEndedChange(true);
// Navigate without any query parameters
router.push(TAB_ROUTES[tab]);
onTabChange(tab);
return;
}

// For other tabs, preserve existing parameters
const newParams = new URLSearchParams(searchParams.toString());
const queryString = newParams.toString();
router.push(queryString ? `${TAB_ROUTES[tab]}?${queryString}` : TAB_ROUTES[tab]);
onTabChange(tab);
};

const { label, icon: CurrentIcon } =
sortOptions.find((option) => option.value === sortBy) || sortOptions[0];

return (
<div className="bg-white pb-6">
<div className="full-w border-b border-gray-200">
<div className="flex items-center justify-between">
<Tabs
tabs={tabs}
activeTab={activeTab}
onTabChange={handleTabChange}
variant="primary"
className="border-b-0"
/>
<div className="flex items-center gap-4">
<div className="flex-1">
<Tabs
tabs={tabs}
activeTab={activeTab}
onTabChange={handleTabChange}
variant="primary"
className="border-b-0"
/>
</div>
{activeTab !== 'previously-funded' && (
<BaseMenu
align="end"
trigger={
<Button variant="outlined" size="sm" className="flex items-center gap-1">
<CurrentIcon className="h-4 w-4 mr-1" />
<span>{label}</span>
<ChevronDown className="h-4 w-4" />
</Button>
}
>
{sortOptions.map(({ value, label: optionLabel, icon: OptionIcon }) => (
<BaseMenuItem
key={value || 'newest'}
onClick={() => onSortChange(value)}
className={sortBy === value ? 'bg-gray-100' : ''}
>
<div className="flex items-center gap-2">
<OptionIcon className="h-4 w-4" />
<span>{optionLabel}</span>
</div>
</BaseMenuItem>
))}
<DropdownMenu.Separator className="my-1 h-px bg-gray-200" />
<BaseMenuItem className="flex items-center justify-between">
<span>Include ended</span>
<Switch
checked={includeEnded}
onCheckedChange={onIncludeEndedChange}
className="h-4 w-7 [&>span]:h-3 [&>span]:w-3 [&>span]:translate-x-0 [&[aria-checked=true]>span]:translate-x-3 ml-5"
/>
</BaseMenuItem>
</BaseMenu>
)}
</div>
</div>
</div>
Expand Down
60 changes: 60 additions & 0 deletions components/Fund/lib/FundingFeedConfig.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { ReactNode } from 'react';
import { MarketplaceTab, FundingSortOption } from '../MarketplaceTabs';
import { Star, ArrowUp, DollarSign, Users } from 'lucide-react';

export type SortOption = {
label: string;
value: FundingSortOption;
icon: typeof Star | typeof ArrowUp | typeof DollarSign | typeof Users;
};

export const getSortOptions = (activeTab: MarketplaceTab): SortOption[] => [
{ label: 'Best', value: '', icon: Star },
{ label: 'Top', value: 'upvotes', icon: ArrowUp },
{
label: activeTab === 'grants' ? 'Applicants' : 'Funders',
value: 'most_applicants',
icon: Users,
},
{ label: activeTab === 'grants' ? 'Amount' : 'Raised', value: 'amount_raised', icon: DollarSign },
];

export type TabConfig = {
title: string;
subtitle: string;
contentType: 'GRANT' | 'PREREGISTRATION';
endpoint: 'grant_feed' | 'funding_feed';
sidebar: ReactNode;
fundraiseStatus?: 'OPEN' | 'CLOSED';
};

// This will be populated with actual sidebar components when imported
export const createTabConfig = (
GrantRightSidebar: ReactNode,
FundRightSidebar: ReactNode
): Record<MarketplaceTab, TabConfig> => ({
grants: {
title: 'Request for Proposals',
subtitle: 'Explore available funding opportunities',
contentType: 'GRANT',
endpoint: 'grant_feed',
sidebar: GrantRightSidebar,
fundraiseStatus: undefined,
},
'needs-funding': {
title: 'Proposals',
subtitle: 'Fund breakthrough research shaping tomorrow',
contentType: 'PREREGISTRATION',
endpoint: 'funding_feed',
sidebar: FundRightSidebar,
fundraiseStatus: 'OPEN',
},
'previously-funded': {
title: 'Previously Funded',
subtitle: 'Browse research that has been successfully funded',
contentType: 'PREREGISTRATION',
endpoint: 'funding_feed',
sidebar: FundRightSidebar,
fundraiseStatus: 'CLOSED',
},
});
Loading