diff --git a/app/my-funding/components/MyFundingPage.tsx b/app/my-funding/components/MyFundingPage.tsx index 9fd425f6f..fd6fa2592 100644 --- a/app/my-funding/components/MyFundingPage.tsx +++ b/app/my-funding/components/MyFundingPage.tsx @@ -464,7 +464,7 @@ function heroCovers( ): HeroCover[] { // Ordered by what was given and described by it, so the deck is the same // before and after the activity loads. - // A funded proposal without a cover is drawn with the proposal icon. + // A funded proposal or RFP pool without a cover is drawn with its kind's icon. const funded = [...fundedRows] .sort((a, b) => b.youGave.usd - a.youGave.usd || b.youGave.rsc - a.youGave.rsc) .map( @@ -472,6 +472,7 @@ function heroCovers( key: row.key, title: row.title, image: row.image, + tone: row.kind === 'pool' ? 'blue' : 'green', status: `You gave ${format(row.youGave, { shorten: true })}`, detail: row.scientist.fullName, }) diff --git a/components/Funding/dashboard/FundedResearchList.tsx b/components/Funding/dashboard/FundedResearchList.tsx index 2a8f9937d..292b6d74a 100644 --- a/components/Funding/dashboard/FundedResearchList.tsx +++ b/components/Funding/dashboard/FundedResearchList.tsx @@ -10,6 +10,8 @@ import { import type { FundedRow } from '@/components/Funding/dashboard/lib/myFundingModel'; import { useMoneyFormat } from '@/components/Funding/dashboard/lib/useMoneyFormat'; import { formatTimeAgo } from '@/utils/date'; +import { pluralizeSuffix } from '@/utils/stringUtils'; +import { cn } from '@/utils/styles'; interface FundedResearchListProps { readonly rows: readonly FundedRow[]; @@ -21,34 +23,63 @@ interface FundedResearchListProps { /** * Research you funded: each proposal with its scientist, the latest word from - * its author or reviewers, and what you gave. + * its author or reviewers, and what you gave; then each RFP pool you put money + * into, with its creator and what you gave. */ export function FundedResearchList({ rows, limit, onSeeAll }: FundedResearchListProps) { if (rows.length === 0) return null; const shown = limit ? rows.slice(0, limit) : rows; + const shownProposals = shown.filter((row) => row.kind === 'proposal'); + const shownPools = shown.filter((row) => row.kind === 'pool'); + const proposalCount = rows.filter((row) => row.kind === 'proposal').length; + const poolCount = rows.length - proposalCount; return (
0 && `${proposalCount} proposal${pluralizeSuffix(proposalCount)}`, + poolCount > 0 && `${poolCount} RFP pool${pluralizeSuffix(poolCount)}`, + ] + .filter(Boolean) + .join(' · ')} action={ limit != null && rows.length > limit && onSeeAll && See all {rows.length} } /> - {/* One card holding every row, like Your peer reviews. */} - + {shownProposals.length > 0 && } + {shownPools.length > 0 && ( + <> +

0 && 'mt-6' + )} + > + RFP pools you funded +

+ + + )}
); } +/** One card holding every row, like Your peer reviews. */ +function FundedResearchRows({ rows }: { readonly rows: readonly FundedRow[] }) { + return ( + + ); +} + function FundedResearchRow({ row }: { readonly row: FundedRow }) { const format = useMoneyFormat(); diff --git a/components/Funding/dashboard/lib/myFundingModel.ts b/components/Funding/dashboard/lib/myFundingModel.ts index b6b996153..e4068ddd4 100644 --- a/components/Funding/dashboard/lib/myFundingModel.ts +++ b/components/Funding/dashboard/lib/myFundingModel.ts @@ -141,9 +141,10 @@ export function latestNewsByPost(entries: readonly FeedEntry[]): Map ): FundedRow[] { if (!overview) return []; - const rows = overview.supportedProposals.map( - (proposal): FundedRow => ({ - key: `funded-${proposal.id}`, - postId: proposal.id, - title: proposal.title.trim() || 'Untitled proposal', - href: buildWorkUrl({ id: proposal.id, slug: proposal.slug, contentType: 'preregistration' }), - image: proposal.image, - scientist: proposal.createdBy.authorProfile, - youGave: proposal.fundedAmount, - news: news.get(proposal.id), - }) - ); + const rows = [ + ...overview.supportedProposals.map( + (proposal): FundedRow => ({ + key: `funded-${proposal.id}`, + kind: 'proposal', + postId: proposal.id, + title: proposal.title.trim() || 'Untitled proposal', + href: buildWorkUrl({ + id: proposal.id, + slug: proposal.slug, + contentType: 'preregistration', + }), + image: proposal.image, + scientist: proposal.createdBy.authorProfile, + youGave: proposal.fundedAmount, + news: news.get(proposal.id), + }) + ), + ...overview.supportedFundingPools.map( + (pool): FundedRow => ({ + key: `funded-${pool.id}`, + kind: 'pool', + postId: pool.id, + title: pool.title.trim() || 'Untitled RFP', + href: buildWorkUrl({ id: pool.id, slug: pool.slug, contentType: 'funding_request' }), + image: pool.image, + scientist: pool.createdBy.authorProfile, + youGave: pool.fundedAmount, + }) + ), + ]; const newsAt = (row: FundedRow) => (row.news ? new Date(row.news.timestamp).getTime() : 0); return rows.sort( @@ -184,11 +204,11 @@ export function buildFundedRows( ); } -/** A scientist the user supports, with what they gave across that scientist's proposals. */ +/** A scientist the user supports, with what they gave across their proposals and RFP pools. */ export interface SupportedPerson { readonly profile: AuthorProfile; readonly youGave: Money; - /** The proposal of theirs the user gave the most to. */ + /** The title of the proposal or RFP of theirs the user gave the most to. */ readonly proposalTitle?: string; } diff --git a/types/funder.ts b/types/funder.ts index ab615063d..d9183b723 100644 --- a/types/funder.ts +++ b/types/funder.ts @@ -11,11 +11,12 @@ export interface SupportedResearcher { fundedAmount: CurrencyAmount; } -export interface SupportedProposal { +/** A proposal the user funded, or an RFP whose funding pool they put money into. */ +export interface SupportedPost { id: number; title: string; slug: string; - /** The proposal's cover, when it has one. */ + /** The post's cover, when it has one. */ image: string | null; createdBy: { id: number; @@ -35,7 +36,8 @@ export interface FunderOverview { matchedFunds: CurrencyAmount; distributedFunds: CurrencyAmount; supportedResearchers: SupportedResearcher[]; - supportedProposals: SupportedProposal[]; + supportedProposals: SupportedPost[]; + supportedFundingPools: SupportedPost[]; // Display headline ($1.24M) — alias of distributedFunds for the hero copy. totalGiven: CurrencyAmount; // Alias of matchedFunds for the hero copy. @@ -67,7 +69,7 @@ function formatMatchRatio(given: CurrencyAmount, match: CurrencyAmount): string } export function transformFunderOverview(raw: any): FunderOverview { - const proposals = extractProposals(raw.supported_proposals); + const proposals = extractPosts(raw.supported_proposals); const researchers = extractResearchers(proposals); const distributedFunds: CurrencyAmount = { @@ -94,6 +96,7 @@ export function transformFunderOverview(raw: any): FunderOverview { distributedFunds, supportedResearchers: researchers, supportedProposals: proposals, + supportedFundingPools: extractPosts(raw.supported_funding_pools), totalGiven, communityMatch, totalDeployed, @@ -104,8 +107,8 @@ export function transformFunderOverview(raw: any): FunderOverview { }; } -function extractProposals(rawProposals: any[]): SupportedProposal[] { - return (rawProposals ?? []) +function extractPosts(rawPosts: any[]): SupportedPost[] { + return (rawPosts ?? []) .filter((p: any) => p.created_by) .map((p: any) => ({ id: p.id, @@ -123,7 +126,7 @@ function extractProposals(rawProposals: any[]): SupportedProposal[] { })); } -function extractResearchers(proposals: SupportedProposal[]): SupportedResearcher[] { +function extractResearchers(proposals: SupportedPost[]): SupportedResearcher[] { const map = new Map(); for (const proposal of proposals) {