Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
TypographyTag,
TypographyType,
} from '../../../components/typography/Typography';
import { GiftIcon, InfoIcon } from '../../../components/icons';
import { InfoIcon } from '../../../components/icons';
import { IconSize } from '../../../components/Icon';
import {
ProfilePicture,
Expand All @@ -18,135 +18,94 @@ import { useGivebackContribution } from '../hooks/useGivebackContribution';
import { useContributionActions } from '../hooks/useContributionActions';
import { formatDonationAmount } from '../utils';

// Personal recap shown above the action catalog: how much the visitor has
// unlocked for their causes and the next reward they're working toward. No
// rank, level or leaderboard — the campaign starts from scratch, so this stays
// a purely personal progress cue.
// Personal stat card above the action catalog. A rounded-square avatar (matching
// daily.dev's app tiles) carries identity with a small level badge tucked in its
// corner; the money you've unlocked is the hero number beside it. Clean and
// concrete, no floating ring, no game-y framing.
export const GivebackContributionSummary = (): ReactElement => {
const { user } = useAuthContext();
const { earnedPoints, nextReward, pointsToNext, currentLevel, isPending } =
const { earnedPoints, currentLevel, isPending } =
useGivebackContribution(true);
// Shares the catalog's query key, so this adds no extra request. Sums the
// visitor's completions across every action into one "actions taken" count.
// Shares the catalog's query key, so this adds no extra request.
const { actions, isPending: isActionsPending } = useContributionActions(true);
const actionsTaken = actions.reduce(
(sum, action) => sum + action.userCompletions,
0,
);

if (isPending) {
// Matches the flat loaded layout (avatar + stat lines) so it doesn't flash
// from a card into a flat row when data lands.
return (
<FlexCol className="gap-1.5">
<div className="h-3 w-32 animate-pulse rounded-8 bg-surface-float" />
<div className="h-8 w-44 animate-pulse rounded-8 bg-surface-float" />
</FlexCol>
<FlexRow className="items-center gap-4 tablet:gap-5">
<div className="size-[72px] shrink-0 animate-pulse rounded-16 bg-surface-float" />
<FlexCol className="min-w-0 flex-1 gap-2">
<div className="h-7 w-48 max-w-full animate-pulse rounded-8 bg-surface-float" />
<div className="h-4 w-32 max-w-full animate-pulse rounded-8 bg-surface-float" />
</FlexCol>
</FlexRow>
);
}

return (
<FlexRow className="flex-wrap items-center gap-x-5 gap-y-4">
<FlexRow className="items-center gap-4 tablet:gap-5">
{user && (
<div className="relative shrink-0">
<ProfilePicture
user={user}
size={ProfileImageSize.XXLarge}
rounded="full"
className="ring-2 ring-accent-cabbage-default"
rounded={ProfileImageSize.XXLarge}
className="ring-1 ring-border-subtlest-tertiary"
/>
<span className="absolute -bottom-1.5 left-1/2 -translate-x-1/2 whitespace-nowrap rounded-full bg-accent-cabbage-default px-2 py-0.5 font-bold uppercase tracking-wide text-white ring-2 ring-background-default typo-caption2">
<span className="absolute -bottom-2 left-1/2 -translate-x-1/2 whitespace-nowrap rounded-6 border border-border-subtlest-tertiary bg-background-default px-2 py-0.5 font-bold uppercase tracking-wide text-accent-cabbage-default ring-2 ring-background-default typo-caption2">
Lvl {currentLevel}
</span>
</div>
)}

<FlexCol className="min-w-0 flex-1 gap-1">
<FlexRow className="items-center gap-1">
<Typography
tag={TypographyTag.Span}
type={TypographyType.Caption1}
color={TypographyColor.Tertiary}
bold
className="uppercase tracking-wider"
>
Your contribution
</Typography>
<span className="group/info relative flex">
<button
type="button"
aria-label="How your contribution is counted"
className="flex text-text-tertiary transition-colors hover:text-text-primary group-focus-within/info:text-text-primary"
>
<InfoIcon size={IconSize.Size16} />
</button>
<span
role="tooltip"
className="pointer-events-none absolute left-0 top-full z-3 mt-2 w-56 rounded-10 border border-border-subtlest-tertiary bg-background-default p-2.5 text-left opacity-0 shadow-2 transition-opacity duration-150 group-focus-within/info:opacity-100 group-hover/info:opacity-100"
<Typography
bold
type={TypographyType.LargeTitle}
className="tabular-nums text-status-success"
>
{formatDonationAmount(earnedPoints)}
</Typography>

{!isActionsPending && (
<FlexRow className="items-center gap-1.5">
<Typography
tag={TypographyTag.Span}
type={TypographyType.Caption1}
color={TypographyColor.Tertiary}
>
<Typography
tag={TypographyTag.Span}
type={TypographyType.Caption1}
color={TypographyColor.Tertiary}
{actionsTaken} {actionsTaken === 1 ? 'action' : 'actions'} taken
</Typography>
<span className="group/info relative flex">
<button
type="button"
aria-label="How your contribution is counted"
className="flex text-text-tertiary transition-colors hover:text-text-primary group-focus-within/info:text-text-primary"
>
Counts the moment you act, because we trust you. If a submission
is rejected, we&apos;ll subtract it.
</Typography>
<InfoIcon size={IconSize.Size16} />
</button>
<span
role="tooltip"
className="pointer-events-none absolute left-0 top-full z-3 mt-2 w-56 rounded-10 border border-border-subtlest-tertiary bg-background-default p-2.5 text-left opacity-0 shadow-2 transition-opacity duration-150 group-focus-within/info:opacity-100 group-hover/info:opacity-100"
>
<Typography
tag={TypographyTag.Span}
type={TypographyType.Caption1}
color={TypographyColor.Tertiary}
>
It counts the moment you act. We trust you. If something gets
rejected, we just subtract it.
</Typography>
</span>
</span>
</span>
</FlexRow>
<FlexRow className="items-baseline gap-1.5">
<Typography
bold
type={TypographyType.Title2}
className="tabular-nums text-status-success"
>
{formatDonationAmount(earnedPoints)}
</Typography>
<Typography
tag={TypographyTag.Span}
type={TypographyType.Caption1}
color={TypographyColor.Tertiary}
>
unlocked for your causes
</Typography>
</FlexRow>
{!isActionsPending && (
<Typography
tag={TypographyTag.Span}
type={TypographyType.Caption1}
color={TypographyColor.Tertiary}
>
{actionsTaken} {actionsTaken === 1 ? 'action' : 'actions'} taken
</Typography>
</FlexRow>
)}
</FlexCol>

{nextReward && (
<FlexCol className="shrink-0 items-end gap-0.5">
<Typography
tag={TypographyTag.Span}
type={TypographyType.Caption2}
color={TypographyColor.Tertiary}
bold
className="uppercase tracking-wider"
>
Next reward
</Typography>
<FlexRow className="items-center gap-1.5 text-accent-cheese-default [&_svg]:size-4">
<GiftIcon />
<Typography bold type={TypographyType.Footnote}>
{nextReward.title}
</Typography>
</FlexRow>
<Typography
tag={TypographyTag.Span}
bold
type={TypographyType.Caption1}
className="tabular-nums text-accent-cabbage-default"
>
{formatDonationAmount(pointsToNext)} to go
</Typography>
</FlexCol>
)}
</FlexRow>
);
};
Loading