Skip to content
Merged
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
17 changes: 14 additions & 3 deletions app/grant/[id]/[slug]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,21 @@ 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 badgeAmountUsd = grant ? getGrantBadgeAmount(grant).usd : undefined;

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={
Expand All @@ -81,6 +87,7 @@ export default async function GrantSlugLayout({ params, children }: Props) {
organization={grant?.organization}
applicationVisibility={grant?.applicationVisibility}
fundingPool={grant?.fundingPool ?? null}
grantCreatedByUserId={grant?.createdBy?.id ?? null}
preTitle={
<RegisteredReportRouteTrackerLoader
currentStage="grant"
Expand All @@ -92,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=AaCPGSj6-z6kAq37AcYM&open=AaCPGSj6-z6kAq37AcYM&pullRequest=1096
}

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=AaCPGSrt-z6kAq37AcYO&open=AaCPGSrt-z6kAq37AcYO&pullRequest=1096
}

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=AaCPGSrQ-z6kAq37AcYN&open=AaCPGSrQ-z6kAq37AcYN&pullRequest=1096
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}
/>
);
}
33 changes: 32 additions & 1 deletion components/Funding/GrantPageContent.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
'use client';

import { createContext, useContext, useState, ReactNode } from 'react';
import { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';
import { useActivityFeed } from '@/hooks/useActivityFeed';
import { FeedEntry } from '@/types/feed';
import type { Application } from '@/types/funding';
import type { FundingPool } from '@/types/grant';
import { ID } from '@/types/root';

export type GrantBannerTab = 'proposals' | 'details' | 'activity';

Expand All @@ -22,6 +25,10 @@
lastClickedEntryId: string | null;
restorationTab: string;
};
fundingPool: FundingPool | null;
setFundingPool: (pool: FundingPool | null) => void;
applications: Application[];
grantCreatedByUserId: ID | null;
}

const GrantTabContext = createContext<GrantTabContextValue | null>(null);
Expand All @@ -32,16 +39,32 @@
return ctx;
}

export function useGrantAllocateContext(): GrantTabContextValue | null {
return useContext(GrantTabContext);
}

export function GrantTabProvider({
children,
defaultTab = 'details',
grantId,
fundingPool: initialFundingPool = null,
applications: initialApplications = [],
grantCreatedByUserId = null,
}: {
children: ReactNode;
defaultTab?: GrantBannerTab;
grantId?: number | string;
fundingPool?: FundingPool | null;
applications?: Application[];
grantCreatedByUserId?: ID | null;

Check warning on line 59 in components/Funding/GrantPageContent.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=AaCHjLODowXDkQyCK2dt&open=AaCHjLODowXDkQyCK2dt&pullRequest=1096
}) {
const [activeTab, setActiveTab] = useState<GrantBannerTab>(defaultTab);
const [fundingPool, setFundingPool] = useState<FundingPool | null>(initialFundingPool);

useEffect(() => {
setFundingPool(initialFundingPool);
}, [initialFundingPool]);

const {
entries,
isLoading,
Expand All @@ -59,6 +82,10 @@
grantId,
});

const handleSetFundingPool = useCallback((pool: FundingPool | null) => {
setFundingPool(pool);
}, []);

return (
<GrantTabContext.Provider
value={{
Expand All @@ -77,6 +104,10 @@
lastClickedEntryId,
restorationTab,
},
fundingPool,
setFundingPool: handleSetFundingPool,
applications: initialApplications,
grantCreatedByUserId,
}}
>
{children}
Expand Down
Loading
Loading