Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
fab0350
Add FundingPool types and grant badge helpers
nicktytarenko Sep 9, 2026
189c1f4
Add FundingPool service
nicktytarenko Sep 9, 2026
60c082c
Add RFP Contribute CTA and funding-pool mode on contribute
nicktytarenko Sep 9, 2026
dcac837
Add RFP funding pool contribute flow and badge totals
nicktytarenko Sep 9, 2026
3250232
Owner/mod Allocate modal wired to distribute + application id + refresh
nicktytarenko Sep 9, 2026
ec990f6
Refactor grant amount handling in ContributeToFundraiseModal and Work…
nicktytarenko Sep 9, 2026
7f22068
Remove unused comments from feed, funding, and grant type definitions
nicktytarenko Sep 10, 2026
c6e5e86
Merge branch 'rfp-funding-pool-types' into rfp-funding-pool-services
nicktytarenko Sep 10, 2026
77c78bf
Refine comments in FundingPoolService: removed redundant note on serv…
nicktytarenko Sep 10, 2026
9fd4a74
Merge branch 'rfp-funding-pool-services' of https://github.com/Resear…
nicktytarenko Sep 10, 2026
8e496f6
Remove unused grantAmountUsd prop from GrantSlugLayout and WorkHeader…
nicktytarenko Sep 10, 2026
d5bac83
Merge branch 'rfp-funding-pool-contribute' of https://github.com/Rese…
nicktytarenko Sep 10, 2026
ef7ffa9
Refactor AllocateFundingPoolModal to use validatePositiveDecimal for …
nicktytarenko Sep 10, 2026
5ff099e
Refactor funding pool components to incorporate currency preference h…
nicktytarenko Sep 10, 2026
0d35ac7
Enhance Activity components to support current document context; upda…
nicktytarenko Sep 11, 2026
9df9344
Merge branch 'main' into rfp-funding-pool-types
nicktytarenko Sep 11, 2026
d11fa1b
Merge branch 'rfp-funding-pool-types' into rfp-funding-pool-services
nicktytarenko Sep 11, 2026
e1ed146
Merge pull request #1094 from ResearchHub/rfp-funding-pool-services
nicktytarenko Sep 11, 2026
8e7a86f
Merge branch 'rfp-funding-pool-types' of https://github.com/ResearchH…
nicktytarenko Sep 11, 2026
38c3508
Merge pull request #1095 from ResearchHub/rfp-funding-pool-contribute
nicktytarenko Sep 11, 2026
7dc2297
Merge branch 'rfp-funding-pool-types' of https://github.com/ResearchH…
nicktytarenko Sep 11, 2026
9850279
Refactor FunderSummary component to simplify props and improve text c…
nicktytarenko Sep 11, 2026
0c8a67b
Merge pull request #1096 from ResearchHub/rfp-funding-pool-allocate
nicktytarenko Sep 11, 2026
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
20 changes: 17 additions & 3 deletions app/grant/[id]/[slug]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ 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';
import { getGrantBadgeAmount } from '@/types/grant';

interface Props {
params: Promise<{
Expand Down Expand Up @@ -58,26 +59,35 @@ export default async function GrantSlugLayout({ params, children }: Props) {
const grant = work.note?.post?.grant;
const grantId = grant?.id ?? undefined;
const grantTitle = grant?.shortTitle || work.title;
const badgeAmountUsd = grant ? getGrantBadgeAmount(grant).usd : undefined;
const isPending = grant?.status === 'PENDING';
const isActive =
grant?.status === 'OPEN' && (grant?.endDate ? isDeadlineInFuture(grant.endDate) : true);

const metadata = await MetadataService.get(work.unifiedDocumentId?.toString() || '');

return (
<GrantTabProvider defaultTab="details" grantId={grantId}>
<GrantTabProvider
defaultTab="details"
grantId={grantId}
fundingPool={grant?.fundingPool ?? null}
applications={grant?.applications ?? []}
grantCreatedByUserId={grant?.createdBy?.id ?? null}
>
<PageLayout
fundraiseGrantId={grantId ? Number(grantId) : undefined}
topBanner={
<WorkHeaderGrant
work={work}
metadata={metadata}
amountUsd={grant?.amount?.usd}
amountUsd={badgeAmountUsd}
grantId={grantId?.toString()}
isActive={isActive}
isPending={isPending}
organization={grant?.organization}
applicationVisibility={grant?.applicationVisibility}
fundingPool={grant?.fundingPool ?? null}
grantCreatedByUserId={grant?.createdBy?.id ?? null}
preTitle={
<RegisteredReportRouteTrackerLoader
currentStage="grant"
Expand All @@ -89,7 +99,11 @@ export default async function GrantSlugLayout({ params, children }: Props) {
}
rightSidebar={
<Suspense fallback={<ActivitySidebarSkeleton />}>
<ActivitySidebarServer grantId={grantId} grantTitle={grantTitle} />
<ActivitySidebarServer
grantId={grantId}
grantTitle={grantTitle}
currentDocumentId={work.id}
/>
</Suspense>
}
>
Expand Down
30 changes: 27 additions & 3 deletions components/Activity/cards/ActivityCardCompact.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,43 @@
getReviewEarning,
getReviewScore,
} from '../lib/activityDisplay.utils';
import { getActivityBounty, shouldShowAuthorBadge } from '../lib/activityWork.utils';
import {
getActivityBounty,
getActivityWork,
shouldShowAuthorBadge,
} from '../lib/activityWork.utils';
import { formatTimeAgo } from '@/utils/date';
import { Tooltip } from '@/components/ui/Tooltip';
import type { FeedEntry } from '@/types/feed';
import { ID } from '@/types/root';

interface ActivityCardCompactProps {
entry: FeedEntry;
currentDocumentId?: ID;

Check warning on line 33 in components/Activity/cards/ActivityCardCompact.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Consider removing 'undefined' type or '?' specifier, one of them is redundant.

See more on https://sonarcloud.io/project/issues?id=ResearchHub_web&issues=AaCPWCmNyaqWZM1Rf4M8&open=AaCPWCmNyaqWZM1Rf4M8&pullRequest=1093
}

function isEntryAboutDocument(entry: FeedEntry, documentId: ID): boolean {
const targetId = Number(documentId);
if (!Number.isFinite(targetId)) return false;

const work = getActivityWork(entry);
if (work?.id != null && Number(work.id) === targetId) return true;

if (entry.relatedWork?.id != null && Number(entry.relatedWork.id) === targetId) return true;

const contentId = entry.content?.id;
return contentId != null && Number(contentId) === targetId;
}

/** Compact activity row used in the activity sidebar. */
export const ActivityCardCompact: FC<ActivityCardCompactProps> = ({ entry }) => {
export const ActivityCardCompact: FC<ActivityCardCompactProps> = ({ entry, currentDocumentId }) => {
const { title, href } = getEntryMeta(entry);

if (!title) return null;

/** When the entry's work is this post, hide the title link (already on that page). */
const hideTitle = currentDocumentId != null && isEntryAboutDocument(entry, currentDocumentId);

const message = getActivityHeaderMessage(entry);
const actionIcon = getActionIcon(entry);
const reviewScore = getReviewScore(entry);
Expand Down Expand Up @@ -118,7 +140,9 @@
className="text-sm leading-5"
isAuthor={shouldShowAuthorBadge(entry, message.actor.id)}
/>
<span className="mt-1 block text-sm leading-tight line-clamp-2">{titleEl}</span>
{!hideTitle && (
<span className="mt-1 block text-sm leading-tight line-clamp-2">{titleEl}</span>
)}
<Tooltip content={new Date(entry.timestamp).toLocaleString()}>
<span className="mt-1 block w-fit cursor-default text-xs text-gray-400">
{formatTimeAgo(entry.timestamp)}
Expand Down
8 changes: 5 additions & 3 deletions components/Activity/cards/ActivityFundingGroupCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,10 @@ const FunderName: FC<{ funder: AuthorProfile }> = ({ funder }) => {
);
};

const FunderSummary: FC<{ funders: AuthorProfile[] }> = ({ funders }) => {
const FunderSummary: FC<{ funders: AuthorProfile[]; isRfp: boolean }> = ({ funders, isRfp }) => {
const named = funders.slice(0, MAX_NAMED_FUNDERS);
const remaining = funders.length - named.length;
const action = isRfp ? ' contributed to this RFP.' : ' funded this proposal.';

return (
<>
Expand All @@ -80,7 +81,7 @@ const FunderSummary: FC<{ funders: AuthorProfile[] }> = ({ funders }) => {
{remaining > 0 && (
<span className="text-gray-500">{` and ${remaining} ${remaining === 1 ? 'other' : 'others'}`}</span>
)}
<span className="text-gray-500"> funded this proposal.</span>
<span className="text-gray-500">{action}</span>
</>
);
};
Expand All @@ -98,6 +99,7 @@ export const ActivityFundingGroupCard: FC<ActivityFundingGroupCardProps> = ({ ro
const latestEntryId = String(latestEntry.id);
const presentation = getWorkCardPresentation(latestEntry, work, { showUSD, exchangeRate });
const total = toPreferredTotal(totals, showUSD, exchangeRate);
const isRfp = work.documentType === 'funding_request';

const avatarItems = funders.map((funder) => ({
src: funder.profileImage || '',
Expand Down Expand Up @@ -129,7 +131,7 @@ export const ActivityFundingGroupCard: FC<ActivityFundingGroupCardProps> = ({ ro
</div>

<div className="min-w-0 flex-1 pt-1 text-sm leading-6">
<FunderSummary funders={funders} />{' '}
<FunderSummary funders={funders} isRfp={isRfp} />{' '}
<ContributionAmount contribution={total} className="align-middle" />
</div>
</div>
Expand Down
12 changes: 11 additions & 1 deletion components/Activity/lib/activityDisplay.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ function getFundingActivityMessage(content: FeedFundingActivityContent): Activit
};
}

export function isFundingPoolContribution(entry: FeedEntry): boolean {
if (entry.contentType !== 'PURCHASE' && entry.contentType !== 'USDFUNDRAISECONTRIBUTION') {
return false;
}
return entry.relatedWork?.contentType === 'funding_request';
}

function getDefaultActivityMessage(entry: FeedEntry): ActivityHeaderMessage {
const actor = entry.content.createdBy;

Expand Down Expand Up @@ -129,7 +136,10 @@ function getDefaultActivityMessage(entry: FeedEntry): ActivityHeaderMessage {
}

if (entry.contentType === 'USDFUNDRAISECONTRIBUTION' || entry.contentType === 'PURCHASE') {
return { actor, verb: 'funded this proposal.' };
return {
actor,
verb: isFundingPoolContribution(entry) ? 'contributed to this RFP.' : 'funded this proposal.',
};
}

return {
Expand Down
16 changes: 14 additions & 2 deletions components/Activity/sidebar/ActivitySidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,22 @@
import { ActivityCardCompact } from '../cards/ActivityCardCompact';
import type { FeedEntry } from '@/types/feed';
import { SidebarHeader } from '@/components/ui/SidebarHeader';
import { ID } from '@/types/root';

interface ActivitySidebarProps {
topSection?: ReactNode;
entries?: FeedEntry[];
grantTitle?: string;
/** Post id of the page being viewed — hides same-document title links. */
currentDocumentId?: ID;

Check warning on line 15 in components/Activity/sidebar/ActivitySidebar.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Consider removing 'undefined' type or '?' specifier, one of them is redundant.

See more on https://sonarcloud.io/project/issues?id=ResearchHub_web&issues=AaCPWCnsyaqWZM1Rf4M-&open=AaCPWCnsyaqWZM1Rf4M-&pullRequest=1093
}

export const ActivitySidebar: FC<ActivitySidebarProps> = ({ topSection, entries, grantTitle }) => {
export const ActivitySidebar: FC<ActivitySidebarProps> = ({
topSection,
entries,
grantTitle,
currentDocumentId,
}) => {
const hasEntries = entries && entries.length > 0;

return (
Expand Down Expand Up @@ -45,7 +53,11 @@
) : (
<div className="divide-y divide-gray-200">
{entries.map((entry) => (
<ActivityCardCompact key={entry.id} entry={entry} />
<ActivityCardCompact
key={entry.id}
entry={entry}
currentDocumentId={currentDocumentId}
/>
))}
</div>
)}
Expand Down
13 changes: 12 additions & 1 deletion components/Activity/sidebar/ActivitySidebarServer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@
import { ActivityService, ActivityScope } from '@/services/activity.service';
import { ActivitySidebar } from './ActivitySidebar';
import type { FeedEntry } from '@/types/feed';
import { ID } from '@/types/root';

interface ActivitySidebarServerProps {
topSection?: ReactNode;
grantId?: number | string;
grantTitle?: string;
/** Post id of the page being viewed — hides same-document title links. */
currentDocumentId?: ID;

Check warning on line 12 in components/Activity/sidebar/ActivitySidebarServer.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Consider removing 'undefined' type or '?' specifier, one of them is redundant.

See more on https://sonarcloud.io/project/issues?id=ResearchHub_web&issues=AaCPWCnDyaqWZM1Rf4M9&open=AaCPWCnDyaqWZM1Rf4M9&pullRequest=1093
scope?: ActivityScope;
}

export async function ActivitySidebarServer({
topSection,
grantId,
grantTitle,
currentDocumentId,
scope = 'grants',
}: ActivitySidebarServerProps) {
let entries: FeedEntry[] = [];
Expand All @@ -29,5 +33,12 @@
console.error('Error loading activity sidebar entries:', error);
}

return <ActivitySidebar topSection={topSection} entries={entries} grantTitle={grantTitle} />;
return (
<ActivitySidebar
topSection={topSection}
entries={entries}
grantTitle={grantTitle}
currentDocumentId={currentDocumentId}
/>
);
}
6 changes: 3 additions & 3 deletions components/Feed/items/FeedItemGrantWithApplicants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { useCurrencyPreference } from '@/contexts/CurrencyPreferenceContext';
import { useExchangeRate } from '@/contexts/ExchangeRateContext';
import { formatCurrency } from '@/utils/currency';
import { Application } from '@/types/funding';
import { getGrantBadgeAmount } from '@/types/grant';

interface FeedItemGrantWithApplicantsProps {
entry: FeedEntry;
Expand Down Expand Up @@ -149,9 +150,8 @@ export const FeedItemGrantWithApplicants: FC<FeedItemGrantWithApplicantsProps> =
slug: content.slug,
});

const budgetAmount = showUSD
? Math.round(grant.amount?.usd || 0)
: Math.round(grant.amount?.rsc || 0);
const badgeAmount = getGrantBadgeAmount(grant);
const budgetAmount = showUSD ? Math.round(badgeAmount.usd) : Math.round(badgeAmount.rsc);

const allProposals = grant.applicants?.filter((a) => a.fundraise) ?? [];
const shown = expanded ? allProposals : allProposals.slice(0, VISIBLE_PROPOSALS);
Expand Down
5 changes: 2 additions & 3 deletions components/Fund/FundraiseProgress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { FC, useState } from 'react';
import { Progress } from '@/components/ui/Progress';
import { ContributorsButton } from '@/components/ui/ContributorsButton';
import { Clock } from 'lucide-react';
import { Clock, Coins } from 'lucide-react';
import { formatDeadline, formatExactTime } from '@/utils/date';
import { isFundraiseActive } from '@/components/Fund/lib/fundraiseUtils';
import type { Fundraise } from '@/types/funding';
Expand All @@ -12,7 +12,6 @@ import { Button } from '@/components/ui/Button';
import { cn } from '@/utils/styles';
import { CurrencyBadge } from '@/components/ui/CurrencyBadge';
import { ContributeToFundraiseModal } from '@/components/modals/ContributeToFundraiseModal';
import { Icon } from '../ui/icons';
import { useCurrencyPreference } from '@/contexts/CurrencyPreferenceContext';
import { useShareModalContext } from '@/contexts/ShareContext';
import { useRouter } from 'next/navigation';
Expand Down Expand Up @@ -257,7 +256,7 @@ export const FundraiseProgress: FC<FundraiseProgressProps> = ({
)}
onClick={handleContributeClick}
>
<Icon name="giveRSC" size={18} color="white" />
<Coins className="w-[18px] h-[18px]" />
Fund proposal
</Button>
) : onDetailsClick ? (
Expand Down
5 changes: 2 additions & 3 deletions components/Fund/FundraiseProgressV2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { FC, useState } from 'react';
import { Progress } from '@/components/ui/Progress';
import { AvatarStack } from '@/components/ui/AvatarStack';
import { ContributorsButton } from '@/components/ui/ContributorsButton';
import { Clock } from 'lucide-react';
import { Clock, Coins } from 'lucide-react';
import { formatDeadline, formatExactTime, formatDate } from '@/utils/date';
import { isFundraiseActive } from '@/components/Fund/lib/fundraiseUtils';
import type { Fundraise } from '@/types/funding';
Expand All @@ -13,7 +13,6 @@ import { Button } from '@/components/ui/Button';
import { cn } from '@/utils/styles';
import { CurrencyBadge } from '@/components/ui/CurrencyBadge';
import { ContributeToFundraiseModal } from '@/components/modals/ContributeToFundraiseModal';
import { Icon } from '../ui/icons';
import { useCurrencyPreference } from '@/contexts/CurrencyPreferenceContext';
import { useShareModalContext } from '@/contexts/ShareContext';
import { useRouter } from 'next/navigation';
Expand Down Expand Up @@ -347,7 +346,7 @@ export const FundraiseProgress: FC<FundraiseProgressProps> = ({
className="flex items-center gap-1.5 bg-orange-500 hover:bg-orange-600 text-white font-semibold transition-all duration-200 border-0"
onClick={handleContributeClick}
>
<Icon name="giveRSC" size={20} color="white" />
<Coins className="w-5 h-5" />
Fund this research
</Button>
) : onDetailsClick ? (
Expand Down
3 changes: 2 additions & 1 deletion components/Funding/GrantCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { FC } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { FeedEntry, FeedGrantContent } from '@/types/feed';
import { getGrantBadgeAmount } from '@/types/grant';
import { cn } from '@/utils/styles';
import { buildWorkUrl } from '@/utils/url';
import { Users } from 'lucide-react';
Expand Down Expand Up @@ -31,7 +32,7 @@ export const GrantCard: FC<GrantCardProps> = ({ entry, className }) => {

const isClosed = grant.status === 'CLOSED';
const applicantCount = grant.applicants?.length ?? 0;
const amount = grant.amount?.usd ?? 0;
const amount = getGrantBadgeAmount(grant).usd;

return (
<Link
Expand Down
6 changes: 4 additions & 2 deletions components/Funding/GrantCarousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { FC, useRef, useState, useEffect } from 'react';
import Link from 'next/link';
import { FeedEntry, FeedGrantContent } from '@/types/feed';
import { getGrantBadgeAmount } from '@/types/grant';
import { Carousel } from '@/components/ui/Carousel';
import { FundingProposalCard } from './FundingProposalCard';
import { ProposalCardSkeleton } from '@/components/skeletons/ProposalCardSkeleton';
Expand Down Expand Up @@ -80,6 +81,7 @@ export const GrantCarousel: FC<GrantCarouselProps> = ({

const hasFetchedProposals = hasBeenVisible && (entries.length > 0 || !isLoading);
const showSkeleton = !hasFetchedProposals;
const badgeUsd = getGrantBadgeAmount(grantData).usd;

return (
<section ref={sectionRef} className={cn('py-5', className)}>
Expand All @@ -90,7 +92,7 @@ export const GrantCarousel: FC<GrantCarouselProps> = ({
{getShortTitle(content.grant.shortTitle, content.title)}
</h2>
</Link>
{grantData.amount?.usd && (
{badgeUsd > 0 && (
<span
className={cn(
'inline-flex items-center px-2 py-0.5 rounded-md text-xs font-bold font-mono',
Expand All @@ -99,7 +101,7 @@ export const GrantCarousel: FC<GrantCarouselProps> = ({
: 'bg-green-50 border border-green-200 text-green-700'
)}
>
{formatCompactUSD(grantData.amount.usd)} pool
{formatCompactUSD(badgeUsd)} pool
</span>
)}
</div>
Expand Down
Loading
Loading