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
11 changes: 9 additions & 2 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,16 @@ mark {
padding-top: calc(56px + var(--top-bar-height)) !important;
}

/* Reserve space for the fixed MobileBottomNav (h-16 + safe-area inset) plus
the funding power bar docked above it (3.75rem including its gutter). */
/* Reserve space for the fixed MobileBottomNav (h-16 + safe-area inset), so
the last of the page's content isn't stuck behind it. */
.page-layout-with-mobile-bottom-nav {
padding-bottom: calc(4rem + env(safe-area-inset-bottom, 0px));
}

/* As above, plus the funding power bar docked over the nav on the home tabs
(3.75rem including its gutter). Pages without that bar use the shorter
offset, or the unused space reads as a dead gap under the content. */
.page-layout-with-mobile-bottom-nav-and-funding-bar {
padding-bottom: calc(7.75rem + env(safe-area-inset-bottom, 0px));
}
}
20 changes: 13 additions & 7 deletions app/layouts/MobileBottomNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -230,19 +230,25 @@ export const MobileBottomNav: React.FC = () => {
{/* Docked over the content directly above the nav, sharing its scroll
fade so the two read as one piece of chrome. The nav is z-[100].

Only the home tabs get it: elsewhere it covers page content that has
nothing to do with funding power, and PageLayout stops reserving room
for it. Keep this condition in sync with the padding class there.

The wrapper spans the full width but only the bar inside it is drawn,
so it stays `pointer-events-none` and FundingPowerBar re-enables hits
on the bar itself. A fixed element swallows touch drags that start on
it — they scroll the document, not the feed's scrollport — so every
pixel of it that isn't a control is a band the feed can't be scrolled
from, and `opacity-20` makes those bands invisible rather than absent. */}
<div
className={`pointer-events-none fixed bottom-16 left-0 right-0 z-[99] px-3 pb-2 tablet:!hidden transition-opacity duration-300 ease-in-out ${
isScrollingDown ? 'opacity-20' : 'opacity-100'
}`}
>
<FundingPowerBar />
</div>
{isHomeTabPath(pathname) && (
<div
className={`pointer-events-none fixed bottom-16 left-0 right-0 z-[99] px-3 pb-2 tablet:!hidden transition-opacity duration-300 ease-in-out ${
isScrollingDown ? 'opacity-20' : 'opacity-100'
}`}
>
<FundingPowerBar />
</div>
)}

{/* Bottom Navigation Bar */}
<nav
Expand Down
10 changes: 9 additions & 1 deletion app/layouts/PageLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import { ReactNode } from 'react';
import dynamic from 'next/dynamic';
import { usePathname } from 'next/navigation';
import { useMobileNavScroll } from '@/hooks/useMobileNavScroll';
import { isHomeTabPath } from '@/hooks/useFundTabs';
import { cn } from '@/lib/utils';
import { ScrollContainerProvider } from '@/contexts/ScrollContainerContext';
import { GrantProvider } from '@/contexts/GrantContext';
Expand Down Expand Up @@ -56,6 +58,8 @@ function PageLayoutInner({
wideRow = false,
}: PageLayoutProps) {
const isNarrow = contentWidth === 'narrow';
const pathname = usePathname() || '';
const isHomeTab = isHomeTabPath(pathname);

const {
scrollContainerRef,
Expand Down Expand Up @@ -104,7 +108,11 @@ function PageLayoutInner({
ref={scrollContainerRef}
className={cn(
'flex-1 overflow-y-auto overflow-x-hidden overscroll-y-contain relative transition-all duration-150',
'page-layout-with-mobile-bottom-nav',
// The funding power bar only docks above the nav on the home tabs
// (see MobileBottomNav), so only those pages reserve room for it.
isHomeTab
? 'page-layout-with-mobile-bottom-nav-and-funding-bar'
: 'page-layout-with-mobile-bottom-nav',
'pt-[var(--top-bar-height)] mt-0',
'tablet:!pt-0 tablet:!mt-[var(--top-bar-height)]',
isPromoBannerVisible && 'page-layout-with-promo-banner'
Expand Down
2 changes: 1 addition & 1 deletion components/Activity/cards/ActivityFundingGroupCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const FunderName: FC<{ funder: AuthorProfile }> = ({ funder }) => {
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 RFP' : ' funded this proposal.';
const action = isRfp ? ' contributed to the funding pool' : ' funded this proposal.';

return (
<>
Expand Down
4 changes: 3 additions & 1 deletion components/Activity/lib/activityDisplay.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,9 @@ function getDefaultActivityMessage(entry: FeedEntry): ActivityHeaderMessage {
if (entry.contentType === 'USDFUNDRAISECONTRIBUTION' || entry.contentType === 'PURCHASE') {
return {
actor,
verb: isFundingPoolContribution(entry) ? 'contributed to RFP' : 'funded this proposal.',
verb: isFundingPoolContribution(entry)
? 'contributed to the funding pool'
: 'funded this proposal.',
};
}

Expand Down
15 changes: 5 additions & 10 deletions components/Funding/ProposalWorkCard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { FC, useState, useCallback } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useRouter } from 'next/navigation';
import { Coins } from 'lucide-react';
import {
ActivityTimestamp,
Expand Down Expand Up @@ -29,8 +29,6 @@ import type { FeedEntry } from '@/types/feed';
import type { Fundraise } from '@/types/funding';
import type { FundingPool } from '@/types/grant';

const RFP_FUNDING_POOL_PARAM = 'rfpFundingPool';

interface ProposalWorkCardProps {
entry: FeedEntry;
/** Fired when the user opens the proposal, for feed click analytics. */
Expand Down Expand Up @@ -74,10 +72,6 @@ export const ProposalWorkCard: FC<ProposalWorkCardProps> = ({ entry, onNavigate
const { updateLastClickedEntryId } = useNavigation();
const { user } = useUser();
const router = useRouter();
const searchParams = useSearchParams();
const isRfpFundingPoolEnabled =
searchParams.get(RFP_FUNDING_POOL_PARAM) === 'true' ||
searchParams.get(RFP_FUNDING_POOL_PARAM) === '1';
const { isGrantScoped, refresh: refreshProposals } = useFundraises();
const grantAllocate = useGrantAllocateContext();

Expand All @@ -97,12 +91,13 @@ export const ProposalWorkCard: FC<ProposalWorkCardProps> = ({ entry, onNavigate
Number(user.id) === Number(grantAllocate.grantCreatedByUserId);
const canManagePool = isGrantCreator || !!user?.isModerator;

// The button stays visible once the viewer can manage an open pool, even when
// nothing is left to give — the modal explains the exhausted balance rather
// than the action silently disappearing.
const canAllocate =
isRfpFundingPoolEnabled &&
isGrantScoped &&
canManagePool &&
fundingPool?.status === 'OPEN' &&
(fundingPool.amountHolding.rsc ?? 0) > 0 &&
work?.fundraise?.status === 'OPEN' &&
applicationId != null;

Expand Down Expand Up @@ -163,7 +158,7 @@ export const ProposalWorkCard: FC<ProposalWorkCardProps> = ({ entry, onNavigate
{canAllocate && (
<Button
data-testid="allocate-funding-pool"
variant="outlined"
variant="dark"
size="sm"
className="shrink-0 gap-1.5"
onClick={(e) => {
Expand Down
2 changes: 1 addition & 1 deletion components/Notebook/AgentChatPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type { GenerationRequest } from '@/types/agentModels';
import { ENDOWMENT_PROMO_BANNER_FEATURE } from '@/app/layouts/components/EndowmentPromoBanner';
import { useDismissableFeature } from '@/hooks/useDismissableFeature';
import { useEditorIsEmpty } from '@/hooks/useEditorIsEmpty';
import { belowMobileTopBar } from '@/components/Notebook/mobileChromeOffsets';
import { belowMobileTopBar } from '@/components/Notebook/mobileBarOffsets';
import { NoteReviewControls } from '@/components/Notebook/NoteReview/NoteReviewControls';
import { NoteReviewBanner } from '@/components/Notebook/NoteReview/NoteReviewBanner';
import { useNoteAgentReview } from '@/components/Notebook/NoteReview/useNoteAgentReview';
Expand Down
2 changes: 1 addition & 1 deletion components/Notebook/NoteEditorLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { NotesMenu } from './NotesMenu';
import { PublishedStatusSection } from './PublishingForm/components/PublishedStatusSection';
import { PublishingForm } from '@/components/Notebook/PublishingForm';

import { ABOVE_MOBILE_NAV } from './mobileChromeOffsets';
import { ABOVE_MOBILE_NAV } from './mobileBarOffsets';
import { AgentChatPanel, type NoteReviewHandle } from '@/components/Notebook/AgentChatPanel';
import { noteDiffPersistableDoc } from './NoteReview/noteDiffOverlay';
import { NoteReviewControls } from './NoteReview/NoteReviewControls';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/**
* Offsets that keep the notebook's fixed overlays clear of the app's mobile
* chrome.
* Offsets that keep the notebook's fixed overlays clear of the app's own fixed
* bars on mobile — the bottom nav and the top bar, not anything browser- or
* device-specific.
*
* Below the `tablet` breakpoint the app frames the viewport with two fixed
* bars the rest of the layout has to work around: MobileBottomNav across the
Expand All @@ -12,7 +13,9 @@
*
* globals.css reserves the same space for in-flow content
* (`.page-layout-with-mobile-bottom-nav`, `.page-layout-with-promo-banner`);
* fixed overlays sit outside the flow and have to carry their own.
* fixed overlays sit outside the flow and have to carry their own. The
* notebook never shows the funding power bar that the home tabs dock over the
* nav, so the nav's own height is the whole of the bottom offset here.
*
* Entries name the desktop offset they stand in for, so a call site reads as a
* swap — `bottom-6` becomes `ABOVE_MOBILE_NAV.bottom6` — keeping the same gap
Expand Down
65 changes: 47 additions & 18 deletions components/modals/AllocateFundingPoolModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { extractApiErrorMessage } from '@/services/lib/serviceUtils';
import type { FundingPool } from '@/types/grant';
import { formatCurrency } from '@/utils/currency';
import { validatePositiveDecimal } from '@/utils/number';
import { cn } from '@/utils/styles';
import { ID } from '@/types/root';

interface AllocateFundingPoolModalProps {
Expand Down Expand Up @@ -136,6 +137,7 @@ export function AllocateFundingPoolModal({
const { amount: parsedDisplayAmount, error: parsedError } = amountInput.trim()
? validateAmount(amountInput)
: { amount: NaN, error: undefined };
const hasFundsToAllocate = holdingDisplay > 0;
const canSubmit =
Number.isFinite(parsedDisplayAmount) &&
parsedDisplayAmount > 0 &&
Expand All @@ -146,31 +148,54 @@ export function AllocateFundingPoolModal({
!parsedError;

return (
<Modal isOpen={isOpen} onClose={onClose} title="Allocate to proposal">
<Modal isOpen={isOpen} onClose={onClose} title="Allocate community contributions">
<div className="space-y-4">
<p className="text-sm text-gray-600 line-clamp-2">{proposalTitle}</p>

<div className="rounded-lg bg-gray-50 border border-gray-100 px-3 py-2.5 text-sm">
<div className="flex justify-between items-center gap-3">
<span className="text-gray-500">Pool holding</span>
<span className="font-mono font-medium text-gray-900 tabular-nums">
{formatPoolAmount(fundingPool.amountHolding)}
</span>
</div>
<div className="flex justify-between items-center gap-3 mt-1">
<span className="text-gray-500">Already distributed</span>
<span className="font-mono text-gray-700 tabular-nums">
{formatPoolAmount(fundingPool.amountDistributed)}
</span>
<p className="text-sm text-gray-600">
Allocate community contributions towards this proposal{' '}
<span className="font-medium text-gray-900">{proposalTitle}</span>
</p>

<div className="rounded-lg border border-gray-200 bg-gray-50 px-3.5 py-3">
<div className="space-y-2 text-sm">
<div className="flex items-center justify-between gap-3">
<span className="text-gray-600">Total contribution by community</span>
<span className="font-mono tabular-nums text-gray-900">
{formatPoolAmount(fundingPool.amountRaised)}
</span>
</div>
<div className="flex items-center justify-between gap-3">
<span className="text-gray-600">Already allocated to proposals</span>
<span className="font-mono tabular-nums text-gray-900">
{formatPoolAmount(fundingPool.amountDistributed)}
</span>
</div>
<div className="flex items-center justify-between gap-3 border-t border-gray-200 pt-2">
<span className="font-medium text-gray-900">Available to allocate</span>
<span
className={cn(
'font-mono font-semibold tabular-nums',
hasFundsToAllocate ? 'text-green-600' : 'text-gray-400'
)}
>
{formatPoolAmount(fundingPool.amountHolding)}
</span>
</div>
</div>
</div>

{!hasFundsToAllocate && (
<p className="text-sm text-gray-500">
Every community contribution has already been allocated. Once the pool receives new
contributions, you can allocate them here.
</p>
)}

<div>
<div className="flex items-center justify-between mb-1.5">
<label htmlFor="allocate-amount" className="text-sm font-medium text-gray-700">
Amount
Amount to allocate
</label>
{holdingDisplay > 0 && (
{hasFundsToAllocate && (
<button
type="button"
onClick={handleAllocateMax}
Expand All @@ -188,7 +213,11 @@ export function AllocateFundingPoolModal({
placeholder="0.00"
type="text"
inputMode="decimal"
className={amountError ? 'border-red-500' : undefined}
disabled={!hasFundsToAllocate || isSubmitting}
className={cn(
amountError && 'border-red-500',
!hasFundsToAllocate && 'bg-gray-50 opacity-60'
)}
rightElement={
<div className="flex items-center gap-1 pr-3 text-gray-900">
<span className="font-medium">{currencyLabel}</span>
Expand Down
14 changes: 10 additions & 4 deletions components/ui/AvatarStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ interface AvatarStackProps {
}[];
/** Label for the extra count tooltip */
extraCountLabel?: string;
/** Overrides the +N avatar's default gray fill, e.g. to tint it to a legend color. */
extraCountClassName?: string;
/** Overrides the +N avatar's default label color. */
extraCountLabelClassName?: string;
/** When true, shows the label text after avatars */
showLabel?: boolean;
}
Expand All @@ -55,6 +59,8 @@ export const AvatarStack: FC<AvatarStackProps> = ({
totalItemsCount,
allItems,
extraCountLabel = 'Others',
extraCountClassName,
extraCountLabelClassName,
showLabel = true,
}) => {
// Determine how many items to display
Expand Down Expand Up @@ -190,10 +196,10 @@ export const AvatarStack: FC<AvatarStackProps> = ({
src={null}
alt={`+${extraCount}`}
size={size}
className={`${getRingWidth()} ${ringColorClass} bg-gray-100`}
className={cn(getRingWidth(), ringColorClass, 'bg-gray-100', extraCountClassName)}
disableTooltip
label={`+${extraCount}`}
labelClassName="font-semibold"
labelClassName={cn('font-semibold', extraCountLabelClassName)}
labelStyle={{ fontSize: getExtraCountFontSize() }}
/>
) : (
Expand All @@ -206,10 +212,10 @@ export const AvatarStack: FC<AvatarStackProps> = ({
src={null}
alt={`+${extraCount}`}
size={size}
className={`${getRingWidth()} ${ringColorClass} bg-gray-100`}
className={cn(getRingWidth(), ringColorClass, 'bg-gray-100', extraCountClassName)}
disableTooltip
label={`+${extraCount}`}
labelClassName="font-semibold"
labelClassName={cn('font-semibold', extraCountLabelClassName)}
labelStyle={{ fontSize: getExtraCountFontSize() }}
/>
</Tooltip>
Expand Down
Loading
Loading