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
3 changes: 2 additions & 1 deletion app/my-funding/components/MyFundingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -464,14 +464,15 @@ 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(
(row): HeroCover => ({
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,
})
Expand Down
47 changes: 39 additions & 8 deletions components/Funding/dashboard/FundedResearchList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand All @@ -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 (
<section aria-label="Research you funded">
<DashboardSectionHeader
title="Research you funded"
meta={`${rows.length} ${rows.length === 1 ? 'proposal' : 'proposals'}`}
meta={[
proposalCount > 0 && `${proposalCount} proposal${pluralizeSuffix(proposalCount)}`,
poolCount > 0 && `${poolCount} RFP pool${pluralizeSuffix(poolCount)}`,
]
.filter(Boolean)
.join(' · ')}
action={
limit != null &&
rows.length > limit &&
onSeeAll && <SeeAllButton onClick={onSeeAll}>See all {rows.length}</SeeAllButton>
}
/>
{/* One card holding every row, like Your peer reviews. */}
<ul className="divide-y divide-gray-100 overflow-hidden rounded-xl border border-gray-200 bg-white">
{shown.map((row) => (
<FundedResearchRow key={row.key} row={row} />
))}
</ul>
{shownProposals.length > 0 && <FundedResearchRows rows={shownProposals} />}
{shownPools.length > 0 && (
<>
<h3
className={cn(
'mb-3 text-sm font-semibold text-gray-700',
shownProposals.length > 0 && 'mt-6'
)}
>
RFP pools you funded
</h3>
<FundedResearchRows rows={shownPools} />
</>
)}
</section>
);
}

/** One card holding every row, like Your peer reviews. */
function FundedResearchRows({ rows }: { readonly rows: readonly FundedRow[] }) {
return (
<ul className="divide-y divide-gray-100 overflow-hidden rounded-xl border border-gray-200 bg-white">
{rows.map((row) => (
<FundedResearchRow key={row.key} row={row} />
))}
</ul>
);
}

function FundedResearchRow({ row }: { readonly row: FundedRow }) {
const format = useMoneyFormat();

Expand Down
58 changes: 39 additions & 19 deletions components/Funding/dashboard/lib/myFundingModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,10 @@ export function latestNewsByPost(entries: readonly FeedEntry[]): Map<number, Pro
return news;
}

/** One proposal the user funded, as Research you funded lists it. */
/** One proposal or RFP pool the user funded, as Research you funded lists it. */
export interface FundedRow {
readonly key: string;
readonly kind: 'proposal' | 'pool';
readonly postId: number;
readonly title: string;
readonly href: string;
Expand All @@ -154,28 +155,47 @@ export interface FundedRow {
}

/**
* Research you funded, from the funding overview alone (which proposals the
* user gave to, how much, and who wrote them) with the latest news on each
* from the activity. The ones with the newest news come first, then the ones
* given the most.
* Research you funded, from the funding overview alone (which proposals and
* RFP pools the user gave to, how much, and who wrote them) with the latest
* news on each proposal from the activity. The ones with the newest news come
* first, then the ones given the most.
*/
export function buildFundedRows(
overview: FunderOverview | null,
news: ReadonlyMap<number, ProposalNews>
): 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(
Expand All @@ -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;
}

Expand Down
17 changes: 10 additions & 7 deletions types/funder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -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 = {
Expand All @@ -94,6 +96,7 @@ export function transformFunderOverview(raw: any): FunderOverview {
distributedFunds,
supportedResearchers: researchers,
supportedProposals: proposals,
supportedFundingPools: extractPosts(raw.supported_funding_pools),
totalGiven,
communityMatch,
totalDeployed,
Expand All @@ -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,
Expand All @@ -123,7 +126,7 @@ function extractProposals(rawProposals: any[]): SupportedProposal[] {
}));
}

function extractResearchers(proposals: SupportedProposal[]): SupportedResearcher[] {
function extractResearchers(proposals: SupportedPost[]): SupportedResearcher[] {
const map = new Map<number, SupportedResearcher>();

for (const proposal of proposals) {
Expand Down
Loading