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
59 changes: 51 additions & 8 deletions app/earn/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Icon from '@/components/ui/icons/Icon';
import { BountyHubSelector as HubsSelector } from '@/components/Earn/BountyHubSelector';
import SortDropdown, { SortOption } from '@/components/ui/SortDropdown';
import { Badge } from '@/components/ui/Badge';
import { cn } from '@/utils/styles';
import { X } from 'lucide-react';
import { useBounties } from '@/hooks/useBounties';

Expand All @@ -24,6 +25,8 @@ export default function EarnPage() {
restoredScrollPosition,
page,
lastClickedEntryId,
bountyFilter,
handleBountyFilterChange,
} = useBounties();

// Available sort options
Expand All @@ -38,15 +41,55 @@ export default function EarnPage() {
<div className="mt-5 space-y-3">
{/* Top filter bar */}
<div className="flex items-center gap-0 sm:gap-2 flex-wrap justify-between">
<div className="w-1/2 sm:!w-[220px] flex-1 sm:!flex-none pr-1 sm:!pr-0">
<HubsSelector
selectedHubs={selectedHubs}
onChange={handleHubsChange}
displayCountOnly
hideSelectedItems={true}
/>
<div className="flex items-center gap-2 flex-wrap flex-1 min-w-0">
<div className="w-full sm:!w-[220px] pr-0 sm:!pr-0">
<HubsSelector
selectedHubs={selectedHubs}
onChange={handleHubsChange}
displayCountOnly
hideSelectedItems={true}
/>
</div>

{/* Bounty Type Filter */}
<div className="flex items-center bg-gray-100 p-1 rounded-lg">
<button
onClick={() => handleBountyFilterChange('all')}
className={cn(
'px-3 py-1.5 text-xs font-medium rounded-md transition-all',
bountyFilter === 'all'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-500 hover:text-gray-700'
)}
>
All
</button>
<button
onClick={() => handleBountyFilterChange('foundation')}
className={cn(
'px-3 py-1.5 text-xs font-medium rounded-md transition-all',
bountyFilter === 'foundation'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-500 hover:text-gray-700'
)}
>
Foundation
</button>
<button
onClick={() => handleBountyFilterChange('user')}
className={cn(
'px-3 py-1.5 text-xs font-medium rounded-md transition-all',
bountyFilter === 'user'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-500 hover:text-gray-700'
)}
>
Community
</button>
</div>
</div>
<div className="w-1/2 sm:!w-[120px] flex-1 sm:!flex-none pl-1 sm:!pl-0">

<div className="w-1/2 sm:!w-[120px] flex-1 sm:!flex-none pl-1 sm:!pl-0 mt-2 sm:mt-0">
<SortDropdown
value={sort}
onChange={(opt: SortOption) => handleSortChange(opt.value)}
Expand Down
19 changes: 14 additions & 5 deletions components/Bounty/BountyForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand All @@ -413,21 +415,28 @@ 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 = () => {
if (inputAmount === 0) return '';
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) => {
Expand Down
6 changes: 3 additions & 3 deletions components/Bounty/BountyInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ export const BountyInfo: FC<BountyInfoProps> = ({
};

// 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]
);
Expand Down Expand Up @@ -231,15 +231,15 @@ export const BountyInfo: FC<BountyInfoProps> = ({
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"
showExchangeRate={false}
iconColor={isActive ? '#ea580c' : colors.gray[500]}
iconSize={18}
shorten
skipConversion={showUSD}
skipConversion={isFoundation || showUSD}
/>
</div>

Expand Down
8 changes: 5 additions & 3 deletions components/Bounty/BountyInfoSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ export const BountyInfoSummary: FC<BountyInfoSummaryProps> = ({
);

// 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;
Expand All @@ -60,14 +62,14 @@ export const BountyInfoSummary: FC<BountyInfoSummaryProps> = ({
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}
/>
</div>
</div>
Expand Down
7 changes: 6 additions & 1 deletion components/Bounty/BountyMetadataLine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ({
Expand All @@ -30,6 +34,7 @@ export const BountyMetadataLine = ({
showDeadline = true,
bountyStatus,
skipConversion = false,
currency,
}: BountyMetadataLineProps) => {
const { showUSD } = useCurrencyPreference();

Expand All @@ -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}
/>
</div>
Expand Down
29 changes: 17 additions & 12 deletions components/Bounty/lib/bountyUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

/**
Expand Down Expand Up @@ -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 };
}
Expand Down
14 changes: 8 additions & 6 deletions components/Feed/FeedItemActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -398,12 +398,14 @@ export const FeedItemActions: FC<FeedItemActionsProps> = ({
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)');
Expand Down Expand Up @@ -620,7 +622,7 @@ export const FeedItemActions: FC<FeedItemActionsProps> = ({
totalAmount={totalBountyAmount}
href={href}
showUSD={showUSD}
skipConversion={showUSD}
skipConversion={isAllFoundation || showUSD}
/>
}
position="top"
Expand All @@ -640,12 +642,12 @@ export const FeedItemActions: FC<FeedItemActionsProps> = ({
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}
Expand All @@ -667,12 +669,12 @@ export const FeedItemActions: FC<FeedItemActionsProps> = ({
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}
Expand Down
9 changes: 7 additions & 2 deletions components/Feed/items/FeedItemBountyComment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,11 @@ export const FeedItemBountyComment: FC<FeedItemBountyCommentProps> = ({
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';
Expand Down Expand Up @@ -313,7 +317,8 @@ export const FeedItemBountyComment: FC<FeedItemBountyCommentProps> = ({
solutionsCount={solutionsCount}
bountyStatus={bounty.status}
showDeadline={showDeadline}
skipConversion={showUSD}
currency={isFoundation ? 'USD' : showUSD ? 'USD' : 'RSC'}
skipConversion={isFoundation || showUSD}
/>
</div>

Expand Down
12 changes: 7 additions & 5 deletions components/banners/EarningOpportunityBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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}
/>
</>
)}
Expand Down Expand Up @@ -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}
/>
</>
)}
Expand Down
Loading