diff --git a/components/Bounty/BountyForm.tsx b/components/Bounty/BountyForm.tsx index dd5fa1578..8710c8d9e 100644 --- a/components/Bounty/BountyForm.tsx +++ b/components/Bounty/BountyForm.tsx @@ -404,7 +404,9 @@ export function BountyForm({ workId, onSubmitSuccess, className }: BountyFormPro const getFormattedInputValue = () => { if (inputAmount === 0) return ''; - return inputAmount.toLocaleString(); + return inputAmount.toLocaleString(undefined, { + maximumFractionDigits: currency === 'USD' ? 2 : 0, + }); }; const toggleCurrency = () => { @@ -413,7 +415,13 @@ export function BountyForm({ workId, onSubmitSuccess, className }: BountyFormPro toast.error('Exchange rate is loading. Please wait before switching to USD.'); return; } - setCurrency(currency === 'RSC' ? 'USD' : 'RSC'); + + const nextCurrency = currency === 'RSC' ? 'USD' : 'RSC'; + const nextAmount = + nextCurrency === 'USD' ? inputAmount * exchangeRate : inputAmount / exchangeRate; + + setCurrency(nextCurrency); + setInputAmount(nextCurrency === 'USD' ? Number(nextAmount.toFixed(2)) : Math.round(nextAmount)); }; const getConvertedAmount = () => { @@ -421,13 +429,14 @@ export function BountyForm({ workId, onSubmitSuccess, className }: BountyFormPro if (isExchangeRateLoading) return ''; return currency === 'RSC' - ? `≈ $${(inputAmount * exchangeRate).toLocaleString()} USD` - : `≈ ${(inputAmount / exchangeRate).toLocaleString()} RSC`; + ? `≈ $${(inputAmount * exchangeRate).toLocaleString(undefined, { maximumFractionDigits: 2 })} USD` + : `≈ ${Math.round(inputAmount / exchangeRate).toLocaleString()} RSC`; }; const getRscAmount = () => { if (isExchangeRateLoading) return currency === 'RSC' ? inputAmount : 0; - return currency === 'RSC' ? inputAmount : inputAmount / exchangeRate; + const amount = currency === 'RSC' ? inputAmount : inputAmount / exchangeRate; + return Math.round(amount); }; const handleEditorContent = (content: any) => { diff --git a/components/Bounty/BountyInfo.tsx b/components/Bounty/BountyInfo.tsx index 3b243d033..8d50212a6 100644 --- a/components/Bounty/BountyInfo.tsx +++ b/components/Bounty/BountyInfo.tsx @@ -190,7 +190,7 @@ export const BountyInfo: FC = ({ }; // Get display amount (handles Foundation bounties with flat $150 USD) - const { amount: displayAmount } = useMemo( + const { amount: displayAmount, isFoundation } = useMemo( () => getBountyDisplayAmount(bounty, exchangeRate, showUSD), [bounty, exchangeRate, showUSD] ); @@ -231,7 +231,7 @@ export const BountyInfo: FC = ({ variant="text" size="md" showText={true} - currency={showUSD ? 'USD' : 'RSC'} + currency={isFoundation ? 'USD' : showUSD ? 'USD' : 'RSC'} className="p-0 gap-0" textColor={isActive ? 'text-orange-700' : 'text-gray-600'} fontWeight="font-bold" @@ -239,7 +239,7 @@ export const BountyInfo: FC = ({ iconColor={isActive ? '#ea580c' : colors.gray[500]} iconSize={18} shorten - skipConversion={showUSD} + skipConversion={isFoundation || showUSD} /> diff --git a/components/Bounty/BountyInfoSummary.tsx b/components/Bounty/BountyInfoSummary.tsx index e7fc6fbc8..42af20fbf 100644 --- a/components/Bounty/BountyInfoSummary.tsx +++ b/components/Bounty/BountyInfoSummary.tsx @@ -31,11 +31,13 @@ export const BountyInfoSummary: FC = ({ ); // Calculate display amount (handles Foundation bounties with flat $150 USD) - const { amount: totalAmount } = useMemo( + const { amount: totalAmount, foundationBountyCount } = useMemo( () => getTotalBountyDisplayAmount(openBounties, exchangeRate, showUSD), [openBounties, exchangeRate, showUSD] ); + const isAllFoundation = foundationBountyCount > 0 && foundationBountyCount === openBounties.length; + // If no open bounties, don't render anything if (openBounties.length === 0) { return null; @@ -60,14 +62,14 @@ export const BountyInfoSummary: FC = ({ variant="text" size="xl" showText={true} - currency={showUSD ? 'USD' : 'RSC'} + currency={isAllFoundation ? 'USD' : showUSD ? 'USD' : 'RSC'} className="p-0 gap-0" textColor="text-gray-700" showExchangeRate={false} iconColor={colors.gray[700]} iconSize={24} shorten - skipConversion={showUSD} + skipConversion={isAllFoundation || showUSD} /> diff --git a/components/Bounty/BountyMetadataLine.tsx b/components/Bounty/BountyMetadataLine.tsx index 5a97ca053..cc0dd5223 100644 --- a/components/Bounty/BountyMetadataLine.tsx +++ b/components/Bounty/BountyMetadataLine.tsx @@ -19,6 +19,10 @@ interface BountyMetadataLineProps { * Useful when the caller has pre-calculated the amount (e.g., Foundation bounty flat fee). */ skipConversion?: boolean; + /** + * Optional currency override. If provided, uses this currency instead of the global preference. + */ + currency?: 'RSC' | 'USD'; } export const BountyMetadataLine = ({ @@ -30,6 +34,7 @@ export const BountyMetadataLine = ({ showDeadline = true, bountyStatus, skipConversion = false, + currency, }: BountyMetadataLineProps) => { const { showUSD } = useCurrencyPreference(); @@ -54,7 +59,7 @@ export const BountyMetadataLine = ({ amount={amount} size="sm" variant={isOpen ? 'badge' : 'disabled'} - currency={showUSD ? 'USD' : 'RSC'} + currency={currency || (showUSD ? 'USD' : 'RSC')} skipConversion={skipConversion} /> diff --git a/components/Bounty/lib/bountyUtil.ts b/components/Bounty/lib/bountyUtil.ts index d5cafce2d..e2ecd24ae 100644 --- a/components/Bounty/lib/bountyUtil.ts +++ b/components/Bounty/lib/bountyUtil.ts @@ -523,10 +523,20 @@ const getCreatorUserId = (bounty: Bounty): number | undefined => { * @returns True if the bounty was created by the Foundation account */ export const isFoundationBounty = (bounty: Bounty): boolean => { - if (!FOUNDATION_USER_ID) return false; - const creatorUserId = getCreatorUserId(bounty); - return creatorUserId === FOUNDATION_USER_ID; + + if (FOUNDATION_USER_ID && creatorUserId === FOUNDATION_USER_ID) { + return true; + } + + // Fallback check for Foundation account by name + const authorProfile = bounty.createdBy?.authorProfile; + const fullName = authorProfile?.fullName || bounty.createdBy?.fullName; + if (fullName?.toLowerCase() === 'researchhub foundation') { + return true; + } + + return false; }; /** @@ -571,19 +581,14 @@ export const getBountyDisplayAmount = ( showUSD: boolean ): { amount: number; isFoundation: boolean } => { const isFoundation = isFoundationBounty(bounty); - const rscAmount = parseFloat(bounty.totalAmount || bounty.amount || '0'); if (isFoundation) { - if (showUSD) { - // Show flat $150 USD for Foundation bounties - return { amount: FOUNDATION_BOUNTY_FLAT_USD, isFoundation: true }; - } - // Show RSC equivalent of $150 USD for Foundation bounties - // exchangeRate is USD per RSC, so RSC = USD / exchangeRate - const rscEquivalent = exchangeRate > 0 ? FOUNDATION_BOUNTY_FLAT_USD / exchangeRate : rscAmount; - return { amount: Math.round(rscEquivalent), isFoundation: true }; + // ALWAYS return flat $150 USD for Foundation bounties to avoid confusion + return { amount: FOUNDATION_BOUNTY_FLAT_USD, isFoundation: true }; } + const rscAmount = parseFloat(bounty.totalAmount || bounty.amount || '0'); + if (showUSD) { return { amount: Math.round(rscAmount * exchangeRate), isFoundation: false }; } diff --git a/components/Feed/FeedItemActions.tsx b/components/Feed/FeedItemActions.tsx index 66a7391f1..28ac96a0e 100644 --- a/components/Feed/FeedItemActions.tsx +++ b/components/Feed/FeedItemActions.tsx @@ -398,12 +398,14 @@ export const FeedItemActions: FC = ({ const hasOpenBounties = openBounties.length > 0; // Calculate total bounty amount for open bounties (handles Foundation bounties with flat $150 USD) - const { amount: totalBountyAmount } = getTotalBountyDisplayAmount( + const { amount: totalBountyAmount, foundationBountyCount } = getTotalBountyDisplayAmount( openBounties, exchangeRate, showUSD ); + const isAllFoundation = foundationBountyCount > 0 && foundationBountyCount === openBounties.length; + // Use media queries to determine screen size const isMobile = useMediaQuery('(max-width: 480px)'); const isTabletOrSmaller = useMediaQuery('(max-width: 768px)'); @@ -620,7 +622,7 @@ export const FeedItemActions: FC = ({ totalAmount={totalBountyAmount} href={href} showUSD={showUSD} - skipConversion={showUSD} + skipConversion={isAllFoundation || showUSD} /> } position="top" @@ -640,12 +642,12 @@ export const FeedItemActions: FC = ({ textColor="inherit" iconColor="inherit" iconSize={18} - currency={showUSD ? 'USD' : 'RSC'} + currency={isAllFoundation ? 'USD' : showUSD ? 'USD' : 'RSC'} shorten={true} showExchangeRate={false} showIcon={true} showText={false} - skipConversion={showUSD} + skipConversion={isAllFoundation || showUSD} /> } showTooltip={false} @@ -667,12 +669,12 @@ export const FeedItemActions: FC = ({ textColor="inherit" iconColor="inherit" iconSize={18} - currency={showUSD ? 'USD' : 'RSC'} + currency={isAllFoundation ? 'USD' : showUSD ? 'USD' : 'RSC'} shorten={true} showExchangeRate={false} showIcon={true} showText={false} - skipConversion={showUSD} + skipConversion={isAllFoundation || showUSD} /> } showTooltip={false} diff --git a/components/Feed/items/FeedItemBountyComment.tsx b/components/Feed/items/FeedItemBountyComment.tsx index 307e37225..c94562475 100644 --- a/components/Feed/items/FeedItemBountyComment.tsx +++ b/components/Feed/items/FeedItemBountyComment.tsx @@ -144,7 +144,11 @@ export const FeedItemBountyComment: FC = ({ const hasSolutions = solutionsCount > 0; // Calculate display amount (handles Foundation bounties with flat $150 USD) - const { amount: displayBountyAmount } = getBountyDisplayAmount(bounty, exchangeRate, showUSD); + const { amount: displayBountyAmount, isFoundation } = getBountyDisplayAmount( + bounty, + exchangeRate, + showUSD + ); // Always use generic action text without amount const bountyActionText = 'created a bounty'; @@ -313,7 +317,8 @@ export const FeedItemBountyComment: FC = ({ solutionsCount={solutionsCount} bountyStatus={bounty.status} showDeadline={showDeadline} - skipConversion={showUSD} + currency={isFoundation ? 'USD' : showUSD ? 'USD' : 'RSC'} + skipConversion={isFoundation || showUSD} /> diff --git a/components/banners/EarningOpportunityBanner.tsx b/components/banners/EarningOpportunityBanner.tsx index 260b6404a..82dcf19b2 100644 --- a/components/banners/EarningOpportunityBanner.tsx +++ b/components/banners/EarningOpportunityBanner.tsx @@ -34,11 +34,13 @@ export const EarningOpportunityBanner = ({ // Calculate display amount (handles Foundation bounties with flat $150 USD) const openBounties = useMemo(() => getOpenBounties(metadata.bounties || []), [metadata.bounties]); - const { amount: displayAmount } = useMemo( + const { amount: displayAmount, foundationBountyCount } = useMemo( () => getTotalBountyDisplayAmount(openBounties, exchangeRate, showUSD), [openBounties, exchangeRate, showUSD] ); + const isAllFoundation = foundationBountyCount > 0 && foundationBountyCount === openBounties.length; + // Check if we can display the bounty amount (exchange rate loaded if USD preferred) const canDisplayAmount = !showUSD || (showUSD && !isExchangeRateLoading && exchangeRate > 0); @@ -81,14 +83,14 @@ export const EarningOpportunityBanner = ({ amount={displayAmount} variant="text" size="sm" - currency={showUSD ? 'USD' : 'RSC'} + currency={isAllFoundation ? 'USD' : showUSD ? 'USD' : 'RSC'} showExchangeRate={false} showText={true} showIcon={false} textColor="text-orange-600" fontWeight="font-semibold" className="p-0 text-sm inline-flex" - skipConversion={showUSD} + skipConversion={isAllFoundation || showUSD} /> )} @@ -124,14 +126,14 @@ export const EarningOpportunityBanner = ({ amount={displayAmount} variant="text" size="sm" - currency={showUSD ? 'USD' : 'RSC'} + currency={isAllFoundation ? 'USD' : showUSD ? 'USD' : 'RSC'} showExchangeRate={false} showText={true} showIcon={false} textColor="text-orange-600" fontWeight="font-semibold" className="p-0 text-base inline-flex" - skipConversion={showUSD} + skipConversion={isAllFoundation || showUSD} /> )} diff --git a/components/tooltips/BountyTooltip.tsx b/components/tooltips/BountyTooltip.tsx index 7146204eb..e412a38d0 100644 --- a/components/tooltips/BountyTooltip.tsx +++ b/components/tooltips/BountyTooltip.tsx @@ -11,6 +11,8 @@ interface BountyTooltipProps { showUSD?: boolean; /** If true, the amount is already in the target currency and should not be converted */ skipConversion?: boolean; + /** If true, the bounty is from the foundation and should be displayed in USD */ + isFoundation?: boolean; } export function BountyTooltip({ @@ -18,6 +20,7 @@ export function BountyTooltip({ href, showUSD = false, skipConversion = false, + isFoundation = false, }: BountyTooltipProps) { const handleClick = (e: React.MouseEvent) => { e.stopPropagation(); @@ -38,12 +41,12 @@ export function BountyTooltip({ iconColor="#f97316" iconSize={20} fontWeight="font-bold" - currency={showUSD ? 'USD' : 'RSC'} + currency={isFoundation ? 'USD' : showUSD ? 'USD' : 'RSC'} shorten={true} showExchangeRate={false} showIcon={true} showText={false} - skipConversion={skipConversion} + skipConversion={isFoundation || skipConversion} /> diff --git a/types/feed.ts b/types/feed.ts index 9f9369b23..ca2a0104c 100644 --- a/types/feed.ts +++ b/types/feed.ts @@ -951,7 +951,8 @@ export const transformFeedEntry = (feedEntry: RawApiFeedEntry): FeedEntry => { : undefined, tips: [], // Default empty tips awardedBountyAmount: (content as any)?.awardedBountyAmount, - isAwardedForFoundationBounty: (content as any)?.bounty_creator_id, + isAwardedForFoundationBounty: + (content as any)?.bounty_creator_id?.toString() === FOUNDATION_USER_ID?.toString(), } as FeedEntry; }; @@ -1113,6 +1114,8 @@ export const transformBountyCommentToFeedItem = ( userVote: comment.userVote, tips: comment.tips, awardedBountyAmount: comment.awardedBountyAmount, + isAwardedForFoundationBounty: + comment.bountyCreatorId?.toString() === FOUNDATION_USER_ID?.toString(), }; };