-
-
- {Math.round(percentage)}% funded
-
-
-
-
- {backersCount.toLocaleString('en-US')}
- {' '}
- total backers
-
-
-
-
- {hasSponsors && (
-
-
-
- Sponsors topping up the pot
-
-
- {formatDonationAmount(sponsoredAmount)}
-
-
-
-
-
-
- {sponsorGoalShare}% of the {formatDonationAmount(goalAmount)}{' '}
- goal · {budgetSponsors.length} sponsors
-
-
- )}
-
-
-
- Funded by daily.dev, not you. Only approved actions count toward the
- goal.
-
-
-
- );
-};
diff --git a/packages/shared/src/features/giveback/components/GivebackImpactPanel.spec.tsx b/packages/shared/src/features/giveback/components/GivebackImpactPanel.spec.tsx
index 46e342a90b2..19d0bbd11e4 100644
--- a/packages/shared/src/features/giveback/components/GivebackImpactPanel.spec.tsx
+++ b/packages/shared/src/features/giveback/components/GivebackImpactPanel.spec.tsx
@@ -1,23 +1,25 @@
import React from 'react';
import { act, fireEvent, render, screen } from '@testing-library/react';
import { GivebackImpactPanel } from './GivebackImpactPanel';
-import { useContributionStatus } from '../hooks/useContributionStatus';
-import { useContributionSponsors } from '../hooks/useContributionSponsors';
import { useGivebackContribution } from '../hooks/useGivebackContribution';
import { useContributionRewards } from '../hooks/useContributionRewards';
import { useContributionUserRewards } from '../hooks/useContributionUserRewards';
import { useClaimContributionReward } from '../hooks/useClaimContributionReward';
+import { useContributionCausePicker } from '../hooks/useContributionCausePicker';
+import { useContributionActions } from '../hooks/useContributionActions';
import { useLogContext } from '../../../contexts/LogContext';
+import { useAuthContext } from '../../../contexts/AuthContext';
import { LogEvent } from '../../../lib/log';
import { ContributionRewardType, type ContributionRewardTier } from '../types';
-jest.mock('../hooks/useContributionStatus');
-jest.mock('../hooks/useContributionSponsors');
jest.mock('../hooks/useGivebackContribution');
jest.mock('../hooks/useContributionRewards');
jest.mock('../hooks/useContributionUserRewards');
jest.mock('../hooks/useClaimContributionReward');
+jest.mock('../hooks/useContributionCausePicker');
+jest.mock('../hooks/useContributionActions');
jest.mock('../../../contexts/LogContext');
+jest.mock('../../../contexts/AuthContext');
// Resolve the reveal/count-up animations synchronously so assertions read final
// values, not mid-animation frames.
@@ -26,12 +28,6 @@ jest.mock('../useGivebackMotion', () => ({
useCountUp: (target: number) => target,
}));
-const mockStatus = useContributionStatus as jest.MockedFunction<
- typeof useContributionStatus
->;
-const mockSponsors = useContributionSponsors as jest.MockedFunction<
- typeof useContributionSponsors
->;
const mockContribution = useGivebackContribution as jest.MockedFunction<
typeof useGivebackContribution
>;
@@ -44,7 +40,14 @@ const mockUserRewards = useContributionUserRewards as jest.MockedFunction<
const mockClaim = useClaimContributionReward as jest.MockedFunction<
typeof useClaimContributionReward
>;
+const mockCausePicker = useContributionCausePicker as jest.MockedFunction<
+ typeof useContributionCausePicker
+>;
+const mockActions = useContributionActions as jest.MockedFunction<
+ typeof useContributionActions
+>;
const mockLog = useLogContext as jest.MockedFunction;
+const mockAuth = useAuthContext as jest.MockedFunction;
const logEvent = jest.fn();
const tiers: ContributionRewardTier[] = [
@@ -73,20 +76,6 @@ const tiers: ContributionRewardTier[] = [
beforeEach(() => {
jest.clearAllMocks();
- mockStatus.mockReturnValue({
- status: {
- enabled: true,
- eligible: true,
- currentCyclePoints: 4000,
- currentCycleTargetPoints: 10000,
- lifetimePoints: 0,
- lifetimeAmountCents: 0,
- contributorsCount: 128,
- userPoints: 40,
- },
- isPending: false,
- });
- mockSponsors.mockReturnValue({ sponsors: [], isPending: false });
mockContribution.mockReturnValue({
earnedPoints: 40,
nextReward: tiers[1],
@@ -102,28 +91,35 @@ beforeEach(() => {
claim: jest.fn().mockResolvedValue(undefined),
isPending: false,
});
+ mockCausePicker.mockReturnValue({
+ causes: [],
+ selectedCauseIds: [],
+ isPending: false,
+ });
+ mockActions.mockReturnValue({
+ actions: [],
+ categories: [],
+ rewardTiers: [],
+ claimedRewardIds: [],
+ isPending: false,
+ });
mockLog.mockReturnValue({ logEvent } as unknown as ReturnType<
typeof useLogContext
>);
-});
-
-it('renders the funding progress section with live totals', () => {
- render();
-
- expect(screen.getByText('Funding progress')).toBeInTheDocument();
- expect(screen.getByText('$4,000')).toBeInTheDocument();
- expect(screen.getByText('128')).toBeInTheDocument();
+ mockAuth.mockReturnValue({ user: null } as unknown as ReturnType<
+ typeof useAuthContext
+ >);
});
it('renders the reward-ladder journey with the current level', () => {
render();
- expect(screen.getByText('Your journey')).toBeInTheDocument();
+ expect(screen.getByText(/for causes you love/)).toBeInTheDocument();
expect(screen.getByText('Sticker pack')).toBeInTheDocument();
expect(screen.getByText('One month of Plus')).toBeInTheDocument();
expect(screen.getByText('Hoodie')).toBeInTheDocument();
// $40 earned: the next milestone is the $100 tier.
- expect(screen.getByText('Next up: One month of Plus')).toBeInTheDocument();
+ expect(screen.getByText('$60 to your next reward')).toBeInTheDocument();
expect(screen.getByText('$60 to go')).toBeInTheDocument();
});
@@ -131,7 +127,7 @@ it('offers a claim for an unlocked, unclaimed tier and logs it', async () => {
render();
// The $25 tier is reached at $40 and not yet claimed.
- const claimButton = screen.getByRole('button', { name: 'Claim' });
+ const claimButton = screen.getByRole('button', { name: /Claim reward/ });
expect(claimButton).toBeInTheDocument();
expect(screen.getByText(/ready to claim/)).toBeInTheDocument();
diff --git a/packages/shared/src/features/giveback/components/GivebackImpactPanel.tsx b/packages/shared/src/features/giveback/components/GivebackImpactPanel.tsx
index 12d0b0b4e4c..035a4bc2e4b 100644
--- a/packages/shared/src/features/giveback/components/GivebackImpactPanel.tsx
+++ b/packages/shared/src/features/giveback/components/GivebackImpactPanel.tsx
@@ -1,24 +1,17 @@
import type { ReactElement } from 'react';
import React from 'react';
import { FlexCol } from '../../../components/utilities';
-import { GivebackCommunityGoalProgress } from './GivebackCommunityGoalProgress';
import { GivebackPersonalRoadmap } from './GivebackPersonalRoadmap';
interface GivebackImpactPanelProps {
onTakeAction: () => void;
}
-// The Impact tab: the campaign's funding progress (community pot + sponsors)
-// followed by the visitor's own reward-ladder journey. The leaderboard and live
-// community feed from the design are intentionally skipped — the campaign starts
-// from scratch, so the social-proof half stays funding + personal progress.
+// The Impact tab: the visitor's own reward-ladder journey through the campaign.
export const GivebackImpactPanel = ({
onTakeAction,
}: GivebackImpactPanelProps): ReactElement => (
-
-
-
-
+
);
diff --git a/packages/shared/src/features/giveback/components/GivebackPersonalRoadmap.tsx b/packages/shared/src/features/giveback/components/GivebackPersonalRoadmap.tsx
index 2cec2b7eb08..527deb4343f 100644
--- a/packages/shared/src/features/giveback/components/GivebackPersonalRoadmap.tsx
+++ b/packages/shared/src/features/giveback/components/GivebackPersonalRoadmap.tsx
@@ -1,5 +1,5 @@
import type { ReactElement } from 'react';
-import React, { useMemo, useState } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
import classNames from 'classnames';
import { FlexCol, FlexRow } from '../../../components/utilities';
import {
@@ -10,6 +10,7 @@ import {
} from '../../../components/typography/Typography';
import {
Button,
+ ButtonColor,
ButtonSize,
ButtonVariant,
} from '../../../components/buttons/Button';
@@ -21,18 +22,43 @@ import {
LockIcon,
MedalBadgeIcon,
StarIcon,
+ UserIcon,
VIcon,
} from '../../../components/icons';
-import ConfettiSvg from '../../../svg/ConfettiSvg';
+import {
+ ProfilePicture,
+ ProfileImageSize,
+} from '../../../components/ProfilePicture';
+import { useAuthContext } from '../../../contexts/AuthContext';
+import type { LoggedUser } from '../../../lib/user';
import { useLogContext } from '../../../contexts/LogContext';
import { LogEvent } from '../../../lib/log';
import { useGivebackContribution } from '../hooks/useGivebackContribution';
import { useContributionRewards } from '../hooks/useContributionRewards';
import { useContributionUserRewards } from '../hooks/useContributionUserRewards';
import { useClaimContributionReward } from '../hooks/useClaimContributionReward';
+import { useContributionCausePicker } from '../hooks/useContributionCausePicker';
+import { useContributionActions } from '../hooks/useContributionActions';
import { ContributionRewardType } from '../types';
import { formatDonationAmount } from '../utils';
import { GivebackMeterShine } from './GivebackMeterShine';
+import { GivebackTabHeading } from './GivebackTabHeading';
+
+// Joins up to three cause names into a natural list ("a, b, and c"), so the
+// impact headline names exactly who the visitor's actions are funding.
+const formatCauseNames = (names: string[]): string | null => {
+ const shown = names.slice(0, 3);
+ if (shown.length === 0) {
+ return null;
+ }
+ if (shown.length === 1) {
+ return shown[0];
+ }
+ const head = shown.slice(0, -1).join(', ');
+ const tail = shown[shown.length - 1];
+ const suffix = names.length > 3 ? ', and more' : '';
+ return `${head}, and ${tail}${suffix}`;
+};
// How many upcoming levels to reveal after the one you're on. The ladder can be
// long, so we only ever render a window of it.
@@ -60,21 +86,12 @@ interface RoadmapLevel {
};
}
-// One state drives every visual cue on a node, so "done", "you are here", and
-// "locked" never disagree (RPG / battle-pass clarity).
-type NodeState = 'claimed' | 'summit' | 'current' | 'unlocked' | 'locked';
-
-const nodeStyles: Record = {
- claimed: 'bg-accent-avocado-default text-white',
- summit:
- 'bg-gradient-to-br from-accent-cheese-default to-accent-bacon-default text-white shadow-2',
- current:
- 'bg-gradient-to-br from-accent-cabbage-default to-accent-onion-default text-white shadow-2-cabbage',
- unlocked: 'bg-accent-cabbage-default text-white',
- locked:
- 'border border-border-subtlest-tertiary bg-surface-float text-text-quaternary',
-};
-
+// Contrast-first, branded palette so color carries meaning, not decoration:
+// • markers are calm surface tiles by default (high contrast on the dark page)
+// • green is only a "done" check accent, never a saturated fill
+// • cabbage (brand) is the single live accent: you, your next goal, claimable
+// • the summit alone gets a brand gradient fill so it reads as "the big one"
+// • locked stays muted/dimmed
type ConnectorFill =
| { type: 'full' }
| { type: 'partial'; progress: number }
@@ -90,15 +107,18 @@ interface RoadmapNode {
connector?: ConnectorFill;
}
+// A straight 3px track between nodes. Cleared segments are green; the live
+// segment leading up to you fills in brand cabbage. One color per state, no
+// gradients, so the rail reads as a single calm path.
const Connector = ({ fill }: { fill: ConnectorFill }): ReactElement => (
-
+
+
+
+
+
+ Level {level.levelNumber} · {requirementLabel}
+
+ {isCurrent && (
+
+ You're here
+
+ )}
+
+
+ {reward.title}
+
+ {isNext && reward.description && (
+
+ {reward.description}
+
+ )}
+
+
+ {/* Right-hand slot for claim / done / lock. The current goal's
+ action sits below the progress bar instead (see isNext). */}
+ {action && (
+
{action}
+ )}
+
+
+ {isNext && (
+
+
+
+
+
+
+
+
+ {formatDonationAmount(amountToNext)} to go
+
+
+
+
+ )}
+
+
+
+ );
+};
diff --git a/packages/shared/src/features/giveback/components/GivebackRoadmapRail.tsx b/packages/shared/src/features/giveback/components/GivebackRoadmapRail.tsx
new file mode 100644
index 00000000000..3653e08e645
--- /dev/null
+++ b/packages/shared/src/features/giveback/components/GivebackRoadmapRail.tsx
@@ -0,0 +1,75 @@
+import type { ReactElement } from 'react';
+import React from 'react';
+import classNames from 'classnames';
+import {
+ Typography,
+ TypographyColor,
+ TypographyType,
+} from '../../../components/typography/Typography';
+import type { ConnectorFill } from './givebackRoadmapTypes';
+
+// A straight 3px track between nodes. Cleared segments are green; the live
+// segment leading up to you fills in brand cabbage. One color per state, no
+// gradients, so the rail reads as a single calm path.
+export const Connector = ({ fill }: { fill: ConnectorFill }): ReactElement => (
+