Skip to content

feat: more hero cards#6294

Merged
capJavert merged 11 commits into
mainfrom
more-hero-cards
Jul 9, 2026
Merged

feat: more hero cards#6294
capJavert merged 11 commits into
mainfrom
more-hero-cards

Conversation

@capJavert

@capJavert capJavert commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Changes

Add hero cards support to more types.

Events

Did you introduce any new tracking events?

Experiment

Did you introduce any new experiments?

Manual Testing

Caution

Please make sure existing components are not breaking/affected by this PR

Preview domain

https://more-hero-cards.preview.app.daily.dev

@capJavert capJavert self-assigned this Jul 8, 2026
@vercel

vercel Bot commented Jul 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
daily-webapp Ready Ready Preview Jul 9, 2026 2:39pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
storybook Ignored Ignored Jul 9, 2026 2:39pm

Request Review

@capJavert

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Claude finished @capJavert's task in 2m 50s —— View job


Review: more hero cards

  • Gather context (read changed files & diff)
  • Analyze new hero card components for bugs/duplication
  • Check adherence to CLAUDE.md guidelines
  • Post review feedback

Nice extraction of the shared bits (featuredWide.ts, HighlightChip, WhyFeaturedButton) and the type-driven PostTypeToWideCard map reads cleanly. A few things worth considering before merge.


🟡 1. Two sources of truth for "widenable" types (latent layout bug)

WIDENABLE_POST_TYPES (feedHighlightColSpan.ts:5) and PostTypeToWideCard (FeedItemComponent.tsx:147) must stay perfectly in sync, but nothing enforces it.

requestedColSpan → the grid cell wrapper is applied whenever colSpan > 1, driven only by WIDENABLE_POST_TYPES:

// Feed.tsx:707-746 — the wide grid cell is created regardless of PostTypeToWideCard
const isWidened = colSpan > 1;
if (isWidened) { renderedItem = <div style={{ gridColumn: `span ${colSpan}` }}>}

But FeedItemComponent only renders a wide card if the type is in PostTypeToWideCard, otherwise it falls through to the normal <PostTag>:

// FeedItemComponent.tsx:471-479
const WideCard = isWidenedFeaturedPost ? PostTypeToWideCard[itemPost.type] : undefined;

{WideCard ? <WideCard  /> : <PostTag  />}

So if someone adds a type to WIDENABLE_POST_TYPES without a matching PostTypeToWideCard entry, that post gets a multi-column grid cell but renders a normal min-h-card card stretched across N columns → broken layout. They're in sync today, but this is fragile. Suggestion: guard in FeedItemComponent so a missing WideCard falls back to colSpan 1 behavior, or derive WIDENABLE_POST_TYPES from the keys of PostTypeToWideCard (single source of truth). Fix this →

🟡 2. Heavy duplication across the four wide cards

Per the repo's strong "Avoiding Code Duplication" guidance, the four *FeaturedWideGridCard files share ~90% of their body: the isHidden panel, the showTagsPanel panel, the FeedItemContainer wrapper, the inner grid + INNER_GRID_COLS, the glass-vs-ActionButtons footer, and the entire image column (blur bg + WhyFeaturedButton + object-contain image) are essentially byte-for-byte identical. Only the header component, the description source, and the video overlay differ.

That's ~600 duplicated lines. Consider a shared shell that slots the per-type differences, e.g.:

<FeaturedWideCardShell
  post={post} wideColSpan={wideColSpan} domProps={domProps} 
  header={<CollectionCardHeader post={post} />}
  description={post.summary}
  media={<FeaturedWideCardMedia post={post} showVideoOverlay />}
/>

At minimum the image column and the footer (glass/action) block are identical everywhere and are safe, low-risk extractions. This is a startup-pragmatism call, but four copies of a 200-line card is a lot of surface area for future drift.

🟠 3. ShareFeaturedWideGridCard drops edge cases that ShareGrid handles

ShareGrid guards several states that the new wide card doesn't:

  • Deleted shared post (sharedPost.id === DeletedPostId → "This post is no longer available")
  • Private squad shared post
  • Tweet embed preview / SharedPostPreview

The wide card just renders title + sharedTitle/sharedSummary + image. A featured Share hero of a removed post would show a stale card instead of the unavailable state. Is dropping these intentional for the hero layout, or should the deleted/unavailable guard at least be carried over? (Note: for the private-squad case, per CLAUDE.md we lean on backend access controls rather than presentation heuristics, so that one may be fine to omit.)

🟢 Minor

  • HighlightChip and WhyFeaturedButton each call useConditionalFeature(featureHeroCards) independently, so every hero card evaluates the flag twice. Minor (GrowthBook caches), but you could lift one evaluation into the card and pass heroCardsConfig/label down.
  • ✅ Confirmed the dropped showFeedback/usePostFeedback path in the new cards is consistent — only the Article variants render FeedbackGrid, so Freeform/Collection/Share correctly omit it.
  • isVideoPost(post) in ShareFeaturedWideGridCard correctly detects shared YouTube posts (it checks sharedPost.type), so the play icon works.
  • Tests in feedHighlightColSpan.spec.ts line up with the new widenable types and the significance→size mapping. 👍

Overall this is a solid, well-structured extension — #1 (sync guard) is the one I'd address before merge; #2 is the higher-value cleanup; #3 is worth a quick answer.
· branch more-hero-cards

@capJavert

Copy link
Copy Markdown
Contributor Author

@claude review again, especially if new components for post types cover all cases which regulard card components do

@claude

claude Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Claude finished @capJavert's task in 2m 49s —— View job


Review: hero card coverage vs regular cards

  • Read new wide cards (Freeform, Collection, Share, Article)
  • Read regular counterparts (FreeformGrid, CollectionGrid, ShareGrid, ArticleGrid)
  • Compare feature/state coverage (guards, overlays, feedback, flags)
  • Post review

Nice work on the refactor since last round — the FeaturedWideCardShell / FeaturedWideImageColumn / FeaturedWideActions extraction kills the ~600 lines of duplication I flagged, and typing wideCards as Record<WidenablePostType, …> now makes the "widenable without a wide card" mismatch a compile error. Both of my earlier blockers are resolved. ✅

Focusing on your question — do the wide cards cover every case the regular cards handle? — mostly yes, but I found a few gaps:


🟠 1. ArticleFeaturedWideGridCard drops the trending flag

ArticleGrid passes flagProps={{ pinnedAt, trending }} (ArticleGrid.tsx:104), but the wide variant only destructures pinnedAt and passes flagProps={{ pinnedAt }}:

// ArticleFeaturedWideGridCard.tsx:45,162
const { pinnedAt } = post;

flagProps={{ pinnedAt }}

So a trending Article/Video hero loses its "Trending" flag, while the Freeform/Collection/Share wide cards all correctly pass { pinnedAt, trending }. Looks like an oversight. Fix this →

🟠 2. Imageless hero posts render an empty grid column

Every wide card passes imageColumn={image ? <FeaturedWideImageColumn …/> : null}, but FeaturedWideCardShell always applies INNER_GRID_COLS[wideColSpan] (e.g. grid-cols-2) regardless:

// FeaturedWideCardShell.tsx:102-112
<div className={classNames('… grid …', INNER_GRID_COLS[wideColSpan])}>
  <div className="relative flex min-h-0 min-w-0 flex-col …">{content}</div>
  {imageColumn}   // null → second track stays blank
</div>

When imageColumn is null, the content only fills the left track and the remaining N-1 columns are blank — the card is widened across the grid but half/two-thirds empty. The regular cards degrade gracefully (WelcomePostCardFooter / PostCardFooter just omit the cover). Freeform and Collection posts in particular often have no image, and hero eligibility is hero.size > 1 (backend-driven), which doesn't guarantee an image. Worth either collapsing the inner grid to a single column when there's no imageColumn, or gating widening on image presence. Fix this →

🟡 3. ShareFeaturedWideGridCard — shared-tweet & preview paths

Deleted shared post is now handled (👍 addresses last round's #3). Remaining divergences from ShareGrid:

  • Shared tweet body is lost. ShareGrid renders the actual tweet via EmbeddedTweetPreview (ShareGrid.tsx:107-118). The wide card instead hides the title for a bare tweet ({(!isSharedTweet || post.title) && …}) and falls back to sharedPost.summary/sharedTitle. Tweets usually carry their text in contentHtml, not summary, so a title-less shared-tweet hero can render as header + tags + blurred image with no body text at all. Is a tweet-embedded hero in scope, or should these not widen?
  • SharedPostPreview path dropped (sharedPostPreviewFeature) — the wide card shows sharedTitle/sharedSummary + blurred image instead. Probably an intentional hero design, just calling it out.
  • Private-squad state droppedShareGrid shows "This post is in a private squad" (ShareGrid.tsx:94-105); the wide card omits it. Per CLAUDE.md we lean on backend access controls over presentation heuristics, so this one is likely fine to leave.

🟢 Minor

  • CollectionFeaturedWideGridCard doesn't accept/forward onShare, but CollectionGrid passes it to WelcomePostCardFooter (CollectionGrid.tsx:120) for the cover share affordance. ArticleFeaturedWideGridCard correctly wires onShare through useCardCover. Low impact (share is still reachable via the action bar), but inconsistent.
  • The shell renders the showTagsPanel block panel for all wide types, whereas the regular FreeformGrid/CollectionGrid/ShareGrid don't have that branch (only ArticleGrid does). Harmless/arguably an improvement — just noting the wide variants aren't 1:1 with their regular counterparts here.
  • HighlightChip and WhyFeaturedButton still each evaluate featureHeroCards independently (two evals per card). Minor (GrowthBook caches); could lift once into the shell.

Verdict: coverage is close. #1 (trending) is a clear quick fix; #2 (empty column) is the one most likely to produce a visibly broken hero in the wild — I'd confirm behavior for an imageless hero post before merge. #3 is worth a product decision on shared tweets.
· branch more-hero-cards

@capJavert

Copy link
Copy Markdown
Contributor Author

@claude review again, do note that trending flag is purposly not included in wide cards, thats ok, i also enhanced so that wide cards show more content instead of just always image on right

@claude

claude Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Claude finished @capJavert's task in 2m 47s —— View job


Review: hero card coverage (round 3)

  • Re-read wide card shell + image column changes
  • Verify imageless hero posts no longer render an empty column (round 2 feat: profile modal #2)
  • Re-check Share wide card tweet/preview coverage
  • Post review

Thanks for the context. Confirming the two things you called out:

  • Trending flag intentionally omitted — noted, all four cards pass only { pinnedAt }. 👍
  • Imageless hero posts show more content — ✅ resolved. Each card now switches the inner grid to grid-cols-1 when there's no image (image ? INNER_GRID_COLS[wideColSpan] : 'grid-cols-1'), so the content column fills the full width instead of leaving a blank track. Round 2 feat: profile modal #2 is fixed.

Coverage is in good shape. One thing the "config based types" commit reopened, and a couple of smaller notes:


🟠 1. allowedPostTypes (config) and PostTypeToWideCard (code) can drift → stretched normal card

Widening is now driven by GrowthBook config, but the card component is still keyed in code — two independent sources of truth again (this was the compile-time guarantee round 2 closed):

// useFeed.ts:412 — which types get a colSpan>1 grid cell
const allowed = heroCardsConfig.allowedPostTypes ?? {};
return new Set<PostType>(
  (Object.keys(allowed) as PostType[]).filter((type) => allowed[type] === true),
);
// FeedItemComponent.tsx:459 — which types actually get a wide component
const WideCard = isWidenedFeaturedPost ? PostTypeToWideCard[itemPost.type] : undefined;

{WideCard ? <WideCard {...postCardProps} wideColSpan={wideColSpan} /> : <PostTag {...postCardProps}>}

If someone flips e.g. [PostType.Poll]: true in the GrowthBook config (no code change, no compile error), requestedColSpan widens that post to a multi-column grid cell in Feed.tsx, but PostTypeToWideCard[Poll] is undefined → it falls through to a plain <PostTag> stretched across N columns = broken layout. Note the fallback PostTag doesn't even receive wideColSpan, so it can't self-correct.

They're in sync today (config lists exactly Article/VideoYouTube true), but this is now a runtime-config footgun rather than a code invariant. Cheapest guard: intersect widenableTypes with the components that actually exist, so PostTypeToWideCard stays the hard constraint and config is only a toggle within it:

// useFeed.ts
return new Set<PostType>(
  (Object.keys(allowed) as PostType[]).filter(
    (type) => allowed[type] === true && type in PostTypeToWideCard,
  ),
);

Fix this →

🟡 2. ShareFeaturedWideGridCard — shared-tweet body still not rendered

For a shared tweet with no post.title, the title h3 is hidden ({(!isSharedTweet || post.title) && …}) and the body only renders sharedPost.summary / sharedPost.title:

// ShareFeaturedWideGridCard.tsx:51,89,133
const isSharedTweet = isSocialTwitterPost(sharedPost);

{sharedSummary ? <p >{sharedSummary}</p> : null}

Tweets carry their text in contentHtml, not summary — so a title-less shared-tweet hero can render as header + tags + blurred image with no body text. Note ArticleFeaturedWideGridCard already solved exactly this with its isTweetPost/stripHtmlTags(contentHtml) description memo (lines 57-87); the Share card could reuse the same approach. Still a product call — if shared tweets aren't meant to widen this is moot, but as-is they can produce an empty-looking hero. (Deleted shared post is now handled 👍, and per CLAUDE.md I'm leaving the private-squad state to backend access control.)

🟢 Minor

  • WhyFeaturedButton / "?" affordance disappears on imageless heroes. It lives inside FeaturedWideImageColumn, which is only rendered when image exists. So an imageless breaking/major hero shows the HighlightChip but no "why is this featured?" tooltip. Low impact, just inconsistent with image heroes.
  • CollectionFeaturedWideGridCard doesn't forward onShareCollectionGrid passes it to WelcomePostCardFooter (CollectionGrid.tsx:120); ArticleFeaturedWideGridCard wires it via useCardCover. Share is still reachable from the action bar, so low impact.
  • Double flag eval unchangedHighlightChip and WhyFeaturedButton each call useConditionalFeature(featureHeroCards) (two evals per card). GrowthBook caches, so negligible; could lift once into the shell and pass heroCardsConfig/label down.
  • ✅ Shell correctly collapses to a single content column when imageless; FeaturedWideActions glass/non-glass footer and the isHidden/showTagsPanel panels are shared cleanly. Standalone SocialTwitter/Welcome/Poll/Digest/LiveRoom have no wide variant — fine as long as feat: turn links in comments to anchor elements #1's guard keeps config from widening them.

Verdict: the enhancement lands well and #2 (empty column) is fixed. #1 is the one worth a guard before merge — it's a silent, config-triggered layout break with no compile safety net anymore. #2 (shared tweet) is a product decision.
· branch more-hero-cards

@capJavert

Copy link
Copy Markdown
Contributor Author

@claude review

  • Double flag eval unchanged its not an issue, its cheap and intended

@claude

claude Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Claude finished @capJavert's task in 2m 3s —— View job


Review: hero cards (round 4)

Noted — double flag eval is intended/cheap, dropping it.

Both open items from round 3 are now resolved: ✅

  • feat: turn links in comments to anchor elements #1 config/component drift is compile-safe again. widenableTypes is now derived by intersecting config with SUPPORTED_WIDE_POST_TYPES (useFeed.ts:413-418), and wideCards is typed Record<(typeof SUPPORTED_WIDE_POST_TYPES_TUPLE)[number], …> (wideCards.ts:10-19) — a total map, so every widenable type is guaranteed a component at compile time. Flipping a config type that has no card (e.g. Poll) can no longer widen it, since it's not in the tuple. This is the guard I was asking for. 👍
  • feat: profile modal #2 shared-tweet body now renders. ShareFeaturedWideGridCard builds tweetBody from stripHtmlTags(sharedPost.contentHtml ?? post.contentHtml) and shows it when there's no sharedSummary (ShareFeaturedWideGridCard.tsx:57-59, 143-147). A title-less shared tweet no longer renders body-less. 👍
  • WhyFeaturedButton on imageless heroes — also fixed by lifting it into FeaturedWideCardShell (FeaturedWideCardShell.tsx:99) so it no longer depends on the image column existing.

Config defaults are correct per CLAUDE.md: enabled: false is the master switch, so the true entries in allowedPostTypes stay inert until GrowthBook enables the experiment. ✅


🟢 Minor (non-blocking)

  • WhyFeaturedButton may overlap the header menu on imageless heroes. It's absolutely positioned right-3 top-3 on the whole container (WhyFeaturedButton.tsx:33). With an image it sits over the image column (fine), but on an imageless card the content column now spans full width (grid-cols-1), and the PostCardHeader options/⋯ button lives in that same top-right corner. Worth a quick visual check on an imageless breaking/major hero to confirm the ? and the menu don't collide.
  • CollectionFeaturedWideGridCard still doesn't forward onShareCollectionGrid passes it to WelcomePostCardFooter (CollectionGrid.tsx:120), and ArticleFeaturedWideGridCard wires it via useCardCover. Share remains reachable from the action bar, so low impact — just inconsistent across the wide cards.

Verdict: LGTM. Both prior blockers are closed, the config→component path is compile-safe, and coverage now matches the regular cards on the cases that matter. The two minors above are cosmetic/consistency nits, safe to merge without.
· branch more-hero-cards

@capJavert capJavert enabled auto-merge (squash) July 9, 2026 14:35
@capJavert capJavert merged commit 0b1b9f0 into main Jul 9, 2026
12 checks passed
@capJavert capJavert deleted the more-hero-cards branch July 9, 2026 14:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant