diff --git a/packages/shared/src/features/giveback/components/GivebackFaq.tsx b/packages/shared/src/features/giveback/components/GivebackFaq.tsx index 8f42a0b3d1..b7282501d1 100644 --- a/packages/shared/src/features/giveback/components/GivebackFaq.tsx +++ b/packages/shared/src/features/giveback/components/GivebackFaq.tsx @@ -12,6 +12,7 @@ import { IconSize } from '../../../components/Icon'; import { useLogContext } from '../../../contexts/LogContext'; import { LogEvent } from '../../../lib/log'; import { GivebackSection } from './GivebackSection'; +import { GivebackTabHeading } from './GivebackTabHeading'; interface FaqItem { id: string; @@ -24,13 +25,13 @@ const faqs: FaqItem[] = [ id: 'cost', question: 'Does this cost me anything?', answer: - 'No. daily.dev funds every donation. You never pay a cent. You just take small actions and we turn them into money for good causes.', + 'Nothing. Not a cent. daily.dev funds every donation. You bring the actions, we bring the money.', }, { id: 'how', question: 'How do my actions turn into donations?', answer: - 'Each approved action unlocks a fixed amount that daily.dev donates to the causes you picked. The community meter is the sum of everyone’s actions.', + 'Each action you complete unlocks a fixed amount that daily.dev donates to the causes you picked. The community meter is every dev’s actions added up.', }, { id: 'causes', @@ -54,7 +55,7 @@ const faqs: FaqItem[] = [ id: 'why', question: 'Why is daily.dev doing this?', answer: - 'We’d rather put our growth budget into causes the community cares about than burn it in ad auctions.', + 'Most companies buy growth with ads. We’d rather earn it with you, by helping more developers discover daily.dev, and send that budget to causes the community actually cares about. You bring the growth, the world gets the money.', }, { id: 'geo', @@ -76,7 +77,8 @@ export const GivebackFaq = (): ReactElement => { }; return ( - + +
{faqs.map((faq) => { const isOpen = openId === faq.id; @@ -121,7 +123,7 @@ export const GivebackFaq = (): ReactElement => { tag={TypographyTag.P} type={TypographyType.Callout} color={TypographyColor.Secondary} - className="max-w-2xl pb-4" + className="max-w-2xl pb-4 [text-wrap:pretty]" > {faq.answer} diff --git a/packages/shared/src/features/giveback/components/GivebackPage.tsx b/packages/shared/src/features/giveback/components/GivebackPage.tsx index 6e45a1075e..178e88b0fd 100644 --- a/packages/shared/src/features/giveback/components/GivebackPage.tsx +++ b/packages/shared/src/features/giveback/components/GivebackPage.tsx @@ -9,7 +9,6 @@ import { } from '../../../components/typography/Typography'; import { GivebackBackground } from './GivebackBackground'; import { GivebackHero } from './GivebackHero'; -import { GivebackSponsorTiers } from './GivebackSponsorTiers'; import { GivebackCauseSelection } from './GivebackCauseSelection'; import { GivebackOnboardingBar } from './GivebackOnboardingBar'; import { GivebackLegalFooter } from './GivebackLegalFooter'; @@ -18,6 +17,7 @@ import { GivebackActionCatalog } from './GivebackActionCatalog'; import { GivebackContributionSummary } from './GivebackContributionSummary'; import { GivebackImpactPanel } from './GivebackImpactPanel'; import { GivebackCausesPanel } from './GivebackCausesPanel'; +import { GivebackFaq } from './GivebackFaq'; import { GivebackFundingBar } from './GivebackFundingBar'; import type { GivebackTabId } from './GivebackTabNav'; import { useLogContext } from '../../../contexts/LogContext'; @@ -125,10 +125,6 @@ export const GivebackPage = (): ReactElement => { />
-
- -
- {showPicker && (
@@ -181,6 +177,7 @@ export const GivebackPage = (): ReactElement => { {activeTab === 'causes' && ( )} + {activeTab === 'faq' && }
)} diff --git a/packages/shared/src/features/giveback/components/GivebackSponsorTiers.spec.tsx b/packages/shared/src/features/giveback/components/GivebackSponsorTiers.spec.tsx index bb488474b5..6d9eda34d1 100644 --- a/packages/shared/src/features/giveback/components/GivebackSponsorTiers.spec.tsx +++ b/packages/shared/src/features/giveback/components/GivebackSponsorTiers.spec.tsx @@ -38,7 +38,7 @@ const sponsor = ( ...overrides, }); -it('groups sponsors under their tier labels and renders brand logo cards', () => { +it('renders a brand logo card with an explicit tier pill per sponsor', () => { mockUseContributionSponsors.mockReturnValue({ sponsors: [ sponsor({ id: '1', name: 'Vercel', tier: ContributionSponsorTier.Gold }), diff --git a/packages/shared/src/features/giveback/components/GivebackSponsorTiers.tsx b/packages/shared/src/features/giveback/components/GivebackSponsorTiers.tsx index a36dc4ce76..bd163f0dbd 100644 --- a/packages/shared/src/features/giveback/components/GivebackSponsorTiers.tsx +++ b/packages/shared/src/features/giveback/components/GivebackSponsorTiers.tsx @@ -1,5 +1,5 @@ import type { ReactElement } from 'react'; -import React from 'react'; +import React, { useState } from 'react'; import classNames from 'classnames'; import { FlexCol, FlexRow } from '../../../components/utilities'; import { @@ -8,6 +8,7 @@ import { TypographyTag, TypographyType, } from '../../../components/typography/Typography'; +import { MedalBadgeIcon } from '../../../components/icons'; import { useLogContext } from '../../../contexts/LogContext'; import { LogEvent } from '../../../lib/log'; import { useContributionSponsors } from '../hooks/useContributionSponsors'; @@ -16,47 +17,51 @@ import type { ContributionSponsor } from '../types'; import { ContributionSponsorTier } from '../types'; interface TierStyle { - // Tier marker dot + label color (soft, on-brand accent tints). - dotClass: string; + // Tint for the inline tier label. labelClass: string; - // White-card padding + logo height, stepping down platinum → backer. - chipClass: string; + // A FIXED logo height (not max-height): many brand SVGs ship with only a + // viewBox and no width/height, so `w-auto max-h-*` collapses their width to 0 + // and they render blank. A fixed height lets the browser derive width from the + // viewBox aspect ratio. Each tier also caps width proportional to its height - + // otherwise wide wordmark logos hit a shared cap and shrink, flattening the + // size hierarchy so gold and silver end up looking the same. Steps down by + // prestige: gold biggest, bronze smallest. logoClass: string; + // Fallback name size when a sponsor has no usable logo. + nameType: TypographyType; } const tierStyles: Record = { [ContributionSponsorTier.Gold]: { - dotClass: 'bg-accent-cheese-default', labelClass: 'text-accent-cheese-default', - chipClass: 'px-4 py-2.5', - logoClass: 'h-8 tablet:h-9', + logoClass: 'h-14 max-w-[260px]', + nameType: TypographyType.Body, }, [ContributionSponsorTier.Silver]: { - dotClass: 'bg-text-quaternary', labelClass: 'text-text-secondary', - chipClass: 'px-3.5 py-2', - logoClass: 'h-6 tablet:h-7', + logoClass: 'h-8 max-w-[150px]', + nameType: TypographyType.Footnote, }, [ContributionSponsorTier.Bronze]: { - dotClass: 'bg-accent-bacon-default', - labelClass: 'text-accent-bacon-default', - chipClass: 'px-3 py-1.5', - logoClass: 'h-5 tablet:h-6', + labelClass: 'text-accent-burger-default', + logoClass: 'h-4 max-w-[80px]', + nameType: TypographyType.Caption1, }, }; -// The headline tier sits on its own row inside a warm glow; the lower two -// tiers share the row beneath it. -const HEADLINE_TIERS = [ContributionSponsorTier.Gold]; -const LOWER_TIERS = [ +// Gold first so the strip reads left-to-right by prestige. +const TIER_ORDER: ContributionSponsorTier[] = [ + ContributionSponsorTier.Gold, ContributionSponsorTier.Silver, ContributionSponsorTier.Bronze, ]; -// Brand logos rest on white "medal cards" so they stay legible on the dark page -// regardless of the logo's own colors; logo-less sponsors (e.g. individuals) -// fall back to a quiet name pill so the card is never empty. -const SponsorChip = ({ +// A sponsor logo, monochrome (light-tinted) at rest so the mixed brand logos +// read as one calm row; on hover the tile fills white and reveals the logo's +// true colors. The logo only counts once it actually decodes - until then +// (loading, hung, blocked, 404, zero-size, or no URL) the name shows so nothing +// is ever blank. +const SponsorLogo = ({ sponsor, style, }: { @@ -64,6 +69,10 @@ const SponsorChip = ({ style: TierStyle; }): ReactElement => { const { logEvent } = useLogContext(); + const [logoLoaded, setLogoLoaded] = useState(false); + const [logoFailed, setLogoFailed] = useState(false); + const hasLogo = Boolean(sponsor.logoUrl) && !logoFailed; + const showName = !hasLogo || !logoLoaded; const onClick = () => logEvent({ @@ -72,59 +81,42 @@ const SponsorChip = ({ extra: JSON.stringify({ name: sponsor.name, tier: sponsor.tier }), }); - if (!sponsor.logoUrl) { - const pillClass = - 'flex shrink-0 items-center rounded-12 border border-border-subtlest-tertiary bg-surface-float px-3 py-1.5 transition-transform duration-200 hover:-translate-y-0.5 motion-reduce:transform-none'; - const name = ( - - {sponsor.name} - - ); - - if (!sponsor.url) { - return {name}; - } - - return ( - - {name} - - ); - } - - const cardClass = classNames( - 'flex shrink-0 items-center rounded-12 bg-white shadow-[0_10px_28px_-12px_rgba(0,0,0,0.6)] transition-transform duration-200 hover:-translate-y-1 motion-reduce:transform-none', - style.chipClass, - ); - const logo = ( - {`${sponsor.name} + {hasLogo && ( + {`${sponsor.name} setLogoLoaded(true)} + onError={() => setLogoFailed(true)} + className={classNames( + 'w-auto object-contain transition duration-200 [filter:brightness(0)_invert(1)] group-hover:[filter:none]', + style.logoClass, + !logoLoaded && 'hidden', + )} + /> )} - /> + {showName && ( + + {sponsor.name} + + )} + ); if (!sponsor.url) { return ( - - {logo} + + {body} ); } @@ -135,42 +127,20 @@ const SponsorChip = ({ target="_blank" rel="noopener noreferrer" aria-label={sponsor.name} - className={cardClass} + className={tileClass} onClick={onClick} > - {logo} + {body} ); }; -const SponsorTierGroup = ({ - tier, - sponsors, -}: { - tier: ContributionSponsorTier; - sponsors: ContributionSponsor[]; -}): ReactElement => { - const style = tierStyles[tier]; - - return ( - - - - - {sponsorTierLabel[tier]} - - - {sponsors.map((sponsor) => ( - - ))} - - ); -}; +const Divider = (): ReactElement => ( + +); export const GivebackSponsorTiers = (): ReactElement | null => { const { sponsors } = useContributionSponsors(); @@ -179,30 +149,15 @@ export const GivebackSponsorTiers = (): ReactElement | null => { return null; } - const byTier = (tier: ContributionSponsorTier) => - sponsors.filter((sponsor) => sponsor.tier === tier); - - const headlineTiers = HEADLINE_TIERS.filter( - (tier) => byTier(tier).length > 0, - ); - const lowerTiers = LOWER_TIERS.filter((tier) => byTier(tier).length > 0); + const tierGroups = TIER_ORDER.map((tier) => ({ + tier, + sponsors: sponsors.filter((sponsor) => sponsor.tier === tier), + })).filter((group) => group.sponsors.length > 0); return ( -
- {/* Soft brand glows, echoing the hero, so the wall feels native to the - page rather than a hard panel dropped on top of it. */} -
-
-
-
- - +
+ + {/* Label above the strip, left aligned. */} { Sponsored by - - {headlineTiers.length > 0 && ( -
- {/* Warm halo so the headline tiers glow softly above the rest. */} -
-
-
- - {headlineTiers.map((tier) => ( - - ))} - -
- )} - - {lowerTiers.length > 0 && ( - <> - {headlineTiers.length > 0 && ( -
- )} - - {lowerTiers.map((tier) => ( - - ))} - - - )} - + {/* One compact bordered strip spanning the page column; a divider + separates each tier section. */} +
+ {tierGroups.map((group, index) => { + const style = tierStyles[group.tier]; + return ( + + {index > 0 && } + + + + + {sponsorTierLabel[group.tier]} + + + + {group.sponsors.map((sponsor) => ( + + ))} + + + + ); + })} +
); diff --git a/packages/shared/src/features/giveback/components/GivebackTabNav.spec.tsx b/packages/shared/src/features/giveback/components/GivebackTabNav.spec.tsx index 28c274fdd2..3d622905dc 100644 --- a/packages/shared/src/features/giveback/components/GivebackTabNav.spec.tsx +++ b/packages/shared/src/features/giveback/components/GivebackTabNav.spec.tsx @@ -8,6 +8,7 @@ it('renders the tabs from the shared tab list', () => { expect(screen.getByText('Take action')).toBeInTheDocument(); expect(screen.getByText('Impact')).toBeInTheDocument(); expect(screen.getByText('Causes')).toBeInTheDocument(); + expect(screen.getByText('FAQ')).toBeInTheDocument(); }); it('maps a tab click back to its id', () => { diff --git a/packages/shared/src/features/giveback/components/GivebackTabNav.tsx b/packages/shared/src/features/giveback/components/GivebackTabNav.tsx index 1038657bb4..0189099676 100644 --- a/packages/shared/src/features/giveback/components/GivebackTabNav.tsx +++ b/packages/shared/src/features/giveback/components/GivebackTabNav.tsx @@ -2,7 +2,7 @@ import type { ReactElement } from 'react'; import React from 'react'; import TabList, { TabListVariant } from '../../../components/tabs/TabList'; -export type GivebackTabId = 'actions' | 'impact' | 'causes'; +export type GivebackTabId = 'actions' | 'impact' | 'causes' | 'faq'; interface GivebackTab { id: GivebackTabId; @@ -13,6 +13,7 @@ export const givebackTabs: GivebackTab[] = [ { id: 'actions', label: 'Take action' }, { id: 'impact', label: 'Impact' }, { id: 'causes', label: 'Causes' }, + { id: 'faq', label: 'FAQ' }, ]; interface GivebackTabNavProps {