Skip to content

Internal Ad Tiles Frontend#4869

Open
ncarazon wants to merge 10 commits into
mainfrom
feat/internal-ad-tiles
Open

Internal Ad Tiles Frontend#4869
ncarazon wants to merge 10 commits into
mainfrom
feat/internal-ad-tiles

Conversation

@ncarazon

@ncarazon ncarazon commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Resolves #4866

Summary

Adds internal ad tiles – a new content surface that lets the programs team promote tournaments and custom campaigns directly in the question feed and similar questions sidebar

Implemented changes:

  • promo_tiles/: consolidated AdTile, TournamentTile, and a shared TileStatusRow into a single folder; added a PromoTile dispatcher that routes by tile type, replacing duplicated routing logic in the feed and sidebar
  • Feed: switched to the unified /ad-tiles/ endpoint; ad tiles filtered by exposure_rate using a seeded random roll (stable per day, rotates daily); tiles inserted at randomized positions (~1 per 10 posts); dismiss removes the slot without shifting remaining tiles
  • Sidebar: promo tile rendered above the similar questions list on both desktop and mobile; shares React Query cache with the feed; dismiss collapses the slot entirely with no replacement
  • Ad tile: shows close date and forecaster count when linked to a tournament project; CTA button and status row share one row on wide screens, stack on narrow; fires internalAdClicked analytics event on click
  • Tournament tile: updated to match design – rule label as plain text, content centered, prize pool chip retained
  • Dismiss: authenticated users only; clicking the × icon closes the slot permanently

Demo videos

  • Ad tile wide layout
internal-ad-tile-wide.mp4
  • Ad tile narrow layout
internal-ad-tile-narrow.mp4
  • Similar question sidebar
internal-ad-tile-similar-sidebar.mp4

Summary by CodeRabbit

  • New Features
    • Promotional tiles (ads and tournament-style promos) now appear in feeds and the question sidebar, including localized status/metadata, optional imagery, and dismiss controls.
    • Similar questions sidebar now combines similar-question results with optional promo tiles.
  • Bug Fixes
    • Improved promo-tile selection and exposure filtering for ad tiles; dismissed promo tiles are hidden from subsequent feed/sidebar rendering.
  • Chores
    • Updated feed/sidebar tile fetching and wiring to support combined promo tiles, plus added a server dismiss action.

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

🚀 Preview Environment

Your preview environment is ready!

Resource Details
🌐 Preview URL https://metaculus-pr-4869-feat-internal-ad-tiles-preview.mtcl.cc
📦 Docker Image ghcr.io/metaculus/metaculus:feat-internal-ad-tiles-578b16f
🗄️ PostgreSQL NeonDB branch preview/pr-4869-feat-internal-ad-tiles
Redis Fly Redis mtc-redis-pr-4869-feat-internal-ad-tiles

Details

  • Commit: e8beb4831a7972e8b96413cac6ebaff4e76c3966
  • Branch: feat/internal-ad-tiles
  • Fly App: metaculus-pr-4869-feat-internal-ad-tiles

ℹ️ Preview Environment Info

Isolation:

  • PostgreSQL and Redis are fully isolated from production
  • Each PR gets its own database branch and Redis instance
  • Changes pushed to this PR will trigger a new deployment

Limitations:

  • Background workers and cron jobs are not deployed in preview environments
  • If you need to test background jobs, use Heroku staging environments

Cleanup:

  • This preview will be automatically destroyed when the PR is closed

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds combined feed tiles, promo tile UI, and dismissal wiring across the posts feed and question sidebar, plus a seeded random helper for deterministic tile selection and ad exposure filtering.

Changes

Combined feed tiles rollout

Layer / File(s) Summary
Combined tile contracts and seeded random
front_end/src/types/projects.ts, front_end/src/utils/posts_feed.ts
AdTileData and CombinedFeedTile types are added, extract helpers narrow the union by type, and seededRandom() provides deterministic RNG.
Tile API and query migration
front_end/src/services/api/misc/misc.shared.ts, front_end/src/components/posts_feed/hooks/use_posts_feed_query.ts, front_end/src/components/posts_feed/index.tsx, front_end/src/app/(main)/actions.ts
MiscApi gains combined tile fetch and dismiss methods, the feed query hook switches to combined tiles, server feed loading fetches combined tiles, and the server action delegates tile dismissal to the misc API.
Feed item tile placement
front_end/src/components/posts_feed/build_feed_items.ts
FeedItem switches to the "tile" discriminator with combined tiles, ad eligibility uses seeded exposure filtering, and insertion mapping uses the eligible tile list.
Promo tile UI components
front_end/src/components/promo_tiles/*
AdTile, TileStatusRow, TournamentTile, and PromoTile are added to render ad/project tile content, analytics, status text, and optional dismiss controls.
Paginated feed tile wiring
front_end/src/components/posts_feed/paginated_feed.tsx
PaginatedPostsFeed migrates to combined tiles, tracks dismissed tile ids, threads dismiss callbacks through layout and card props, and renders tile items through PromoTile.
Sidebar tile selection and dismissal hook
front_end/src/app/(main)/questions/[id]/components/sidebar/similar_questions/use_sidebar_tile.ts
useSidebarTile fetches combined tiles from shared cache, selects one tile deterministically per post and day, and returns dismissal behavior that updates local state, calls the server action, and clears the shared cache entry.
Sidebar similar questions tile integration
front_end/src/app/(main)/questions/[id]/components/question_page_shell/tabs.tsx, front_end/src/app/(main)/questions/[id]/components/sidebar/similar_questions/index.tsx
SimilarQuestionsTab is imported from the sidebar module, and the sidebar component combines question loading with tile loading while conditionally rendering PromoTile and SimilarQuestionsList.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Metaculus/metaculus#4853: Overlaps with the question-page sidebar similar_questions refactor and import path change.
  • Metaculus/metaculus#4855: Adds the backend ad-tiles/ and dismissal endpoints consumed by this frontend work.
  • Metaculus/metaculus#4930: Also changes front_end/src/components/posts_feed/build_feed_items.ts around feed-item keying and tile identity.

Suggested reviewers

  • lsabor
  • cemreinanc
  • hlbmtc

Poem

🐰 I hop through feeds where tiles now gleam,
With ad and project in one bright stream.
A tap, a dismiss, a cached reply,
Daily seeds spin by and by.
Hop-hop, the sidebar joins the dream.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately reflects the main change: adding internal ad tiles to the frontend.
Linked Issues check ✅ Passed The PR implements the requested internal ad tiles frontend surface across feed and sidebar, matching the linked issue's scope.
Out of Scope Changes check ✅ Passed The changes are tightly focused on ad tiles, promo tile components, feed/sidebar integration, and related API/types.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/internal-ad-tiles

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
front_end/src/app/(main)/questions/[id]/components/sidebar/similar_questions/use_sidebar_tile.ts (1)

45-48: ⚡ Quick win

Consider logging errors from the dismissal API call.

The onDismiss callback performs optimistic dismissal (sets dismissed = true immediately) but silently ignores errors from ClientMiscApi.dismissFeedTile(). While this provides good UX, consider logging failures to aid debugging and monitoring.

🔍 Proposed addition to log errors
  const onDismiss = useCallback((id: string) => {
    setDismissed(true);
-   void ClientMiscApi.dismissFeedTile(id);
+   void ClientMiscApi.dismissFeedTile(id).catch((error) => {
+     console.error("Failed to dismiss feed tile:", error);
+   });
  }, []);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@front_end/src/app/`(main)/questions/[id]/components/sidebar/similar_questions/use_sidebar_tile.ts
around lines 45 - 48, The onDismiss callback currently calls
ClientMiscApi.dismissFeedTile(id) and ignores any errors; update onDismiss (and
the async call) to catch failures and log them (e.g., via console.error or the
app's logger) while preserving optimistic UI behavior setDismissed(true).
Specifically, wrap the promise returned by ClientMiscApi.dismissFeedTile(id)
with a .catch or try/catch and log the error with contextual info (include the
id and a short message) so failures are recorded for debugging.
front_end/src/components/promo_tiles/ad_tile.tsx (1)

38-44: 💤 Low value

Consider optimizing images when possible.

Line 74 sets unoptimized={true} on the Image component, which bypasses Next.js image optimization. While this is necessary for external/dynamic images, it may impact performance if internal images are used.

If ad.image or project.header_image can be internal assets, consider conditionally enabling optimization based on the image source.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front_end/src/components/promo_tiles/ad_tile.tsx` around lines 38 - 44, The
Image component in ad_tile.tsx currently forces unoptimized which disables
Next.js image optimization; update the rendering of Image (the Image component
using the image variable / ad.image or project.header_image) to set unoptimized
conditionally: detect whether the src is an internal asset (e.g., src string
startsWith('/') or matches your allowed domains) and only set unoptimized={true}
for external/dynamic URLs, otherwise allow Next.js optimization (omit or set
unoptimized={false}); implement this check in the ad tile component and use that
boolean when rendering the Image to preserve optimization for internal images.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@front_end/src/app/`(main)/questions/[id]/components/sidebar/similar_questions/use_sidebar_tile.ts:
- Around line 32-43: The memoized selection in useSidebarTile uses getDaySeed()
inside the useMemo callback but does not include the day seed in the dependency
array, so compute the day seed outside the callback (e.g. const daySeed =
getDaySeed() or memoize it) and include that daySeed in the useMemo deps for
tile; update the dependency array for the useMemo that returns tile (which uses
seededRandom(post.id + daySeed)) to [tiles, dismissed, post.id, daySeed] so the
tile rotation recomputes when the day changes.
- Line 39: The optimistic ad-tile dismissal lacks error handling: update the
onDismiss handler in use_sidebar_tile.ts (the function that sets dismissed =
true and calls ClientMiscApi.dismissFeedTile(id)) to catch failures from
ClientMiscApi.dismissFeedTile(id), log the error (use existing logger or
console.error) and roll back the optimistic state (set dismissed back to false)
or schedule a retry; ensure you reference the dismissed state variable and the
ClientMiscApi.dismissFeedTile call so the rollback/retry and logging are applied
when the promise rejects.

In `@front_end/src/components/posts_feed/build_feed_items.ts`:
- Around line 44-48: Add documentation to the front-end type for project/ad
tiles indicating exposure_rate is a percent in the range 1–100 (matching backend
PositiveSmallIntegerField with MinValueValidator(1) and MaxValueValidator(100));
update the appropriate type/interface in front_end/src/types/projects.ts (e.g.,
the AdTile or Tile interface that exposes ad.exposure_rate) with a comment/JSDoc
stating "exposure_rate: percent (1-100)" so maintainers won't assume a 0-1
fraction, and ensure any related type name referenced in build_feed_items.ts
(tiles, tile.type, tile.ad.exposure_rate) points to that documented type.

In `@front_end/src/components/posts_feed/paginated_feed.tsx`:
- Around line 146-150: The current handleDismiss uses setDismissedIds and
fire-and-forget ClientMiscApi.dismissFeedTile, which only hides locally and
leaves postsFeedKeys.tiles() cache stale; replace this with a proper mutation
(e.g., useMutation) that performs an optimistic update by removing the id from
the postsFeedKeys.tiles() query data and/or invalidates that query on success,
and on error rolls back the optimistic change and logs the failure; update the
handler (handleDismiss) to call the mutation instead of directly calling
ClientMiscApi.dismissFeedTile and remove/adjust direct setDismissedIds usage so
the shared cache (postsFeedKeys.tiles()) is the single source of truth.

In `@front_end/src/utils/posts_feed.ts`:
- Around line 7-13: The seed fallback currently uses `seed | 0 || 1` which
converts 0 to 1 and breaks determinism for seed=0; in the seededRandom function
change the initialization of s to preserve zero seeds (e.g. replace `let s =
seed | 0 || 1;` with a nullish/null check such as `let s = seed == null ? 1 :
(seed | 0);`) so that seededRandom(0) remains distinct while still defaulting to
1 only when seed is null/undefined; update the initialization of variable `s` in
function `seededRandom` accordingly.

---

Nitpick comments:
In
`@front_end/src/app/`(main)/questions/[id]/components/sidebar/similar_questions/use_sidebar_tile.ts:
- Around line 45-48: The onDismiss callback currently calls
ClientMiscApi.dismissFeedTile(id) and ignores any errors; update onDismiss (and
the async call) to catch failures and log them (e.g., via console.error or the
app's logger) while preserving optimistic UI behavior setDismissed(true).
Specifically, wrap the promise returned by ClientMiscApi.dismissFeedTile(id)
with a .catch or try/catch and log the error with contextual info (include the
id and a short message) so failures are recorded for debugging.

In `@front_end/src/components/promo_tiles/ad_tile.tsx`:
- Around line 38-44: The Image component in ad_tile.tsx currently forces
unoptimized which disables Next.js image optimization; update the rendering of
Image (the Image component using the image variable / ad.image or
project.header_image) to set unoptimized conditionally: detect whether the src
is an internal asset (e.g., src string startsWith('/') or matches your allowed
domains) and only set unoptimized={true} for external/dynamic URLs, otherwise
allow Next.js optimization (omit or set unoptimized={false}); implement this
check in the ad tile component and use that boolean when rendering the Image to
preserve optimization for internal images.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f2e4ef05-3f07-48c7-8d54-7a0028b07ac1

📥 Commits

Reviewing files that changed from the base of the PR and between eec1dcc and 6b7ddef.

📒 Files selected for processing (16)
  • front_end/src/app/(main)/questions/[id]/components/question_page_shell/tabs.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_page_shell/tabs/similar_questions.tsx
  • front_end/src/app/(main)/questions/[id]/components/sidebar/similar_questions/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/sidebar/similar_questions/use_sidebar_tile.ts
  • front_end/src/components/posts_feed/build_feed_items.ts
  • front_end/src/components/posts_feed/feed_tournament_tile.tsx
  • front_end/src/components/posts_feed/hooks/use_posts_feed_query.ts
  • front_end/src/components/posts_feed/index.tsx
  • front_end/src/components/posts_feed/paginated_feed.tsx
  • front_end/src/components/promo_tiles/ad_tile.tsx
  • front_end/src/components/promo_tiles/index.tsx
  • front_end/src/components/promo_tiles/tile_status_row.tsx
  • front_end/src/components/promo_tiles/tournament_tile.tsx
  • front_end/src/services/api/misc/misc.shared.ts
  • front_end/src/types/projects.ts
  • front_end/src/utils/posts_feed.ts
💤 Files with no reviewable changes (2)
  • front_end/src/components/posts_feed/feed_tournament_tile.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_page_shell/tabs/similar_questions.tsx

Comment thread front_end/src/components/posts_feed/build_feed_items.ts
Comment thread front_end/src/components/posts_feed/paginated_feed.tsx
Comment thread front_end/src/utils/posts_feed.ts
@ncarazon ncarazon force-pushed the feat/internal-ad-tiles branch from 642aafd to 784fdf2 Compare June 11, 2026 12:19
@ncarazon ncarazon marked this pull request as ready for review June 11, 2026 13:07

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@front_end/src/types/projects.ts`:
- Around line 196-204: Update the inline comment for the
AdTileData.exposure_rate field to correctly reflect the valid 0–100 range (so 0
means never show and 100 means always show); locate the AdTileData type
declaration and change the comment "percent 1–100" to something like "percent
0–100: chance this ad is shown in a given feed slot" to match the downstream
rand() * 100 < exposure_rate logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 11e5a8b4-816d-45eb-9021-3314b375e1c1

📥 Commits

Reviewing files that changed from the base of the PR and between 6b7ddef and 784fdf2.

📒 Files selected for processing (17)
  • front_end/src/app/(main)/actions.ts
  • front_end/src/app/(main)/questions/[id]/components/question_page_shell/tabs.tsx
  • front_end/src/app/(main)/questions/[id]/components/question_page_shell/tabs/similar_questions.tsx
  • front_end/src/app/(main)/questions/[id]/components/sidebar/similar_questions/index.tsx
  • front_end/src/app/(main)/questions/[id]/components/sidebar/similar_questions/use_sidebar_tile.ts
  • front_end/src/components/posts_feed/build_feed_items.ts
  • front_end/src/components/posts_feed/feed_tournament_tile.tsx
  • front_end/src/components/posts_feed/hooks/use_posts_feed_query.ts
  • front_end/src/components/posts_feed/index.tsx
  • front_end/src/components/posts_feed/paginated_feed.tsx
  • front_end/src/components/promo_tiles/ad_tile.tsx
  • front_end/src/components/promo_tiles/index.tsx
  • front_end/src/components/promo_tiles/tile_status_row.tsx
  • front_end/src/components/promo_tiles/tournament_tile.tsx
  • front_end/src/services/api/misc/misc.shared.ts
  • front_end/src/types/projects.ts
  • front_end/src/utils/posts_feed.ts
💤 Files with no reviewable changes (2)
  • front_end/src/app/(main)/questions/[id]/components/question_page_shell/tabs/similar_questions.tsx
  • front_end/src/components/posts_feed/feed_tournament_tile.tsx
✅ Files skipped from review due to trivial changes (1)
  • front_end/src/app/(main)/questions/[id]/components/question_page_shell/tabs.tsx
🚧 Files skipped from review as they are similar to previous changes (10)
  • front_end/src/utils/posts_feed.ts
  • front_end/src/components/promo_tiles/ad_tile.tsx
  • front_end/src/services/api/misc/misc.shared.ts
  • front_end/src/components/promo_tiles/tile_status_row.tsx
  • front_end/src/app/(main)/questions/[id]/components/sidebar/similar_questions/use_sidebar_tile.ts
  • front_end/src/components/posts_feed/hooks/use_posts_feed_query.ts
  • front_end/src/app/(main)/questions/[id]/components/sidebar/similar_questions/index.tsx
  • front_end/src/components/promo_tiles/tournament_tile.tsx
  • front_end/src/components/posts_feed/build_feed_items.ts
  • front_end/src/components/posts_feed/paginated_feed.tsx

Comment thread front_end/src/types/projects.ts
Comment thread front_end/src/components/promo_tiles/ad_tile.tsx Outdated
Comment thread front_end/src/components/posts_feed/paginated_feed.tsx Outdated

@cemreinanc cemreinanc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

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.

Internal Ad Tiles Frontend

2 participants