From 7388dae2ae04c93168af1db2148bb64f6f9e1824 Mon Sep 17 00:00:00 2001 From: Ido Shamun <1993245+idoshamun@users.noreply.github.com> Date: Sun, 28 Jun 2026 17:48:59 +0300 Subject: [PATCH] feat(giveback): import action card + submission modal redesign The action surfaces were the remaining un-imported piece of the design pass. Extracts the inline PlatformLogo into a shared GivebackPlatformLogo (used by both the action card and the submission modal), redesigns the submission modal (action brief + parsed instruction steps + platform logo), refreshes the love-actions copy, and adds text-wrap balance/pretty to GivebackSection headers. --- .../components/GivebackActionCard.tsx | 52 +-- .../components/GivebackActionCatalog.tsx | 4 +- .../GivebackActionSubmissionModal.tsx | 338 ++++++++++++------ .../components/GivebackPlatformLogo.tsx | 52 +++ .../giveback/components/GivebackSection.tsx | 2 + 5 files changed, 295 insertions(+), 153 deletions(-) create mode 100644 packages/shared/src/features/giveback/components/GivebackPlatformLogo.tsx diff --git a/packages/shared/src/features/giveback/components/GivebackActionCard.tsx b/packages/shared/src/features/giveback/components/GivebackActionCard.tsx index a0317d0f3bf..672ab2f3e59 100644 --- a/packages/shared/src/features/giveback/components/GivebackActionCard.tsx +++ b/packages/shared/src/features/giveback/components/GivebackActionCard.tsx @@ -1,5 +1,5 @@ import type { ComponentType, ReactElement, ReactNode } from 'react'; -import React, { useState } from 'react'; +import React from 'react'; import classNames from 'classnames'; import { Typography, @@ -15,6 +15,7 @@ import type { ContributionAction } from '../types'; import { ContributionSubmissionStatus } from '../types'; import { formatDonationAmount } from '../utils'; import { getActionPlatformVisual } from '../actionPlatform'; +import { GivebackPlatformLogo } from './GivebackPlatformLogo'; interface GivebackActionCardProps { action: ContributionAction; @@ -39,48 +40,7 @@ const formatCooldownRemaining = (endsAt: string): string => { return `${Math.max(1, minutes)}m`; }; -interface PlatformLogoProps { - logoUrl?: string; - Icon: ComponentType; - forceDark?: boolean; - isDimmed: boolean; -} - -// Prefers the real brand logo (an SVG from the logo CDN) and falls back to the -// internal glyph if there is no logo for the surface or the remote one fails to -// load — so a tile is never broken or blank. The parent tile already pins the -// background and applies the dimmed/grayscale treatment. -const PlatformLogo = ({ - logoUrl, - Icon, - forceDark, - isDimmed, -}: PlatformLogoProps): ReactElement => { - const [failed, setFailed] = useState(false); - - if (logoUrl && !failed) { - return ( - setFailed(true)} - className="size-6 object-contain" - /> - ); - } - - return ( - - ); -}; - -// One sharp, explicit title carries the ask — no competing subtitle. The +// One sharp, explicit title carries the ask - no competing subtitle. The // supporting details (payout, status, "just for love") sit in a calm top/bottom // frame around it so the card stays easy to scan at a glance. export const GivebackActionCard = ({ @@ -206,7 +166,7 @@ export const GivebackActionCard = ({ : 'shadow-1 bg-white text-black group-hover:scale-105', )} > - - {/* Glossy sheen that sweeps across on hover — a small reward flourish. */} + {/* Glossy sheen that sweeps across on hover - a small reward flourish. */} - We can't pay for these, but we'd genuinely appreciate - them. + No donation rides on these. They just help us out. We'd love + you for it. diff --git a/packages/shared/src/features/giveback/components/GivebackActionSubmissionModal.tsx b/packages/shared/src/features/giveback/components/GivebackActionSubmissionModal.tsx index eda65288c99..e5dc5c0be38 100644 --- a/packages/shared/src/features/giveback/components/GivebackActionSubmissionModal.tsx +++ b/packages/shared/src/features/giveback/components/GivebackActionSubmissionModal.tsx @@ -7,34 +7,198 @@ import { } from '../../../components/buttons/Button'; import { Typography, + TypographyColor, TypographyTag, TypographyType, } from '../../../components/typography/Typography'; import { FlexCol, FlexRow } from '../../../components/utilities'; import { RootPortal } from '../../../components/tooltips/Portal'; +import { OpenLinkIcon } from '../../../components/icons'; import { uploadContentImage } from '../../../graphql/posts'; import { useToastNotification } from '../../../hooks/useToastNotification'; import { useLogContext } from '../../../contexts/LogContext'; import { LogEvent } from '../../../lib/log'; import { labels } from '../../../lib/labels'; +import { anchorDefaultRel } from '../../../lib/strings'; import type { ContributionAction } from '../types'; import { formatDonationAmount } from '../utils'; +import { getActionPlatformVisual } from '../actionPlatform'; import { useSubmitContributionAction } from '../hooks/useSubmitContributionAction'; import { GivebackScreenshotField } from './GivebackScreenshotField'; +import { GivebackPlatformLogo } from './GivebackPlatformLogo'; interface GivebackActionSubmissionModalProps { action: ContributionAction; onClose: () => void; } -const getDialogTitle = (isLove: boolean, isSubmitted: boolean): string => { - if (isLove) { - return 'Show some love'; - } - if (isSubmitted) { - return 'Proof submitted'; - } - return 'Submit proof'; +// Instructions may arrive as one paragraph or as several lines (one step each). +// Split on line breaks so multi-line how-tos render as a numbered checklist the +// user can follow top to bottom. +const toInstructionSteps = (instructions: string): string[] => + instructions + .split('\n') + .map((line) => line.trim()) + .filter(Boolean); + +// The explicit "what we're asking you to do" block at the top of every action. +// Leads with the platform identity and the reward, states the ask in a big +// title, and hands the user a one-tap way to go start it on the real surface. +const ActionBrief = ({ + action, + titleId, +}: { + action: ContributionAction; + titleId: string; +}): ReactElement => { + const { metadata } = action; + const isLove = metadata.isLoveAction; + const { + Icon, + name: platformName, + forceDark, + logoUrl, + } = getActionPlatformVisual(metadata.platform); + + return ( + + + + + + + + {platformName} + + + {isLove ? ( + + + Just for love + + + ) : ( + + + +{formatDonationAmount(action.points)} + + + to your causes + + + )} + + + + + {action.title} + + {action.description && ( + + {action.description} + + )} + + + {metadata.externalUrl && ( + + )} + + ); +}; + +// The full how-to, surfaced prominently (not as fine print) so the requirement +// is impossible to miss. Multi-line instructions become a numbered checklist. +const InstructionsBlock = ({ + instructions, +}: { + instructions: string; +}): ReactElement => { + const steps = toInstructionSteps(instructions); + + return ( + + + How to complete it + + {steps.length > 1 ? ( +
    + {steps.map((step, index) => ( +
  1. + + {index + 1} + + + {step} + +
  2. + ))} +
+ ) : ( + + {instructions} + + )} +
+ ); }; export const GivebackActionSubmissionModal = ({ @@ -172,63 +336,42 @@ export const GivebackActionSubmissionModal = ({ aria-hidden className="bg-accent-onion-default/20 pointer-events-none absolute -bottom-24 -left-16 size-56 rounded-full blur-3xl" /> - - - - {getDialogTitle(isLove, isSubmitted)} - + + {!isSubmitted && ( + + )} + + {!isSubmitted && metadata.instructions && ( + + )} + + {isLove && !isSubmitted && ( - {!isLove && isSubmitted - ? "Added to your contribution. We'll only subtract it if validation fails." - : action.title} + This one's a voluntary thank-you. No reward or donation is + attached, we just genuinely appreciate it. - - - {isLove && ( - - - - Just for love - - - This one's a voluntary thank-you — no reward or - donation is attached. We just genuinely appreciate it. - - - {metadata.instructions && ( - - {metadata.instructions} - - )} - )} {!isLove && isSubmitted && ( - + You helped unlock {formatDonationAmount(action.points)} It already counts toward your contribution. If it's rejected, we'll subtract it. @@ -238,31 +381,14 @@ export const GivebackActionSubmissionModal = ({ {!isLove && !isSubmitted && ( - - - - Counts toward your contribution the moment you submit. - - - +{formatDonationAmount(action.points)} - - - {metadata.instructions && ( - - {metadata.instructions} - - )} - + + Done it? Add your proof below. It counts toward your + contribution the moment you submit. + {showUrl && ( )} + - - {isLove ? ( + {/* Pinned action bar: stays visible while the body above scrolls, so + the submit control is always reachable on tall forms. */} + + {isLove ? ( + + ) : ( + <> - ) : ( - <> + {!isSubmitted && ( - {!isSubmitted && ( - - )} - - )} - - + )} + + )} + diff --git a/packages/shared/src/features/giveback/components/GivebackPlatformLogo.tsx b/packages/shared/src/features/giveback/components/GivebackPlatformLogo.tsx new file mode 100644 index 00000000000..3c4b55d7604 --- /dev/null +++ b/packages/shared/src/features/giveback/components/GivebackPlatformLogo.tsx @@ -0,0 +1,52 @@ +import type { ComponentType, ReactElement } from 'react'; +import React, { useState } from 'react'; +import classNames from 'classnames'; +import { IconSize } from '../../../components/Icon'; +import type { IconProps } from '../../../components/Icon'; + +interface GivebackPlatformLogoProps { + logoUrl?: string; + Icon: ComponentType; + forceDark?: boolean; + // Non-actionable tiles (done/in-review/cooldown) already grayscale the parent, + // so the glyph shouldn't be force-darkened on top of that. + isDimmed?: boolean; + iconSize?: IconSize; + className?: string; +} + +// Prefers the real brand logo (an SVG from the logo CDN) and falls back to the +// internal glyph if there is no logo for the surface or the remote one fails to +// load - so a tile is never broken or blank. The parent tile pins the +// background and applies any dimmed/grayscale treatment. +export const GivebackPlatformLogo = ({ + logoUrl, + Icon, + forceDark, + isDimmed, + iconSize = IconSize.Small, + className, +}: GivebackPlatformLogoProps): ReactElement => { + const [failed, setFailed] = useState(false); + + if (logoUrl && !failed) { + return ( + setFailed(true)} + className={classNames('object-contain', className ?? 'size-6')} + /> + ); + } + + return ( + + ); +}; diff --git a/packages/shared/src/features/giveback/components/GivebackSection.tsx b/packages/shared/src/features/giveback/components/GivebackSection.tsx index eff092eeda7..7bc24d12b34 100644 --- a/packages/shared/src/features/giveback/components/GivebackSection.tsx +++ b/packages/shared/src/features/giveback/components/GivebackSection.tsx @@ -39,6 +39,7 @@ export const GivebackSection = ({ tag={TypographyTag.H3} type={TypographyType.Title3} bold + className="[text-wrap:balance]" > {title} @@ -48,6 +49,7 @@ export const GivebackSection = ({ tag={TypographyTag.P} type={TypographyType.Callout} color={TypographyColor.Secondary} + className="[text-wrap:pretty]" > {description}