Skip to content

Internal Ad Tiles Frontend#4869

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

Internal Ad Tiles Frontend#4869
ncarazon wants to merge 9 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 tournaments) now appear in feeds and question sidebars, with status/metadata and dismiss controls.
    • Improved promo rendering including ad-specific displays and analytics.
  • Bug Fixes

    • Better tile filtering and placement for more consistent feed behavior; dismissed tiles stay hidden.
  • Chores

    • Under-the-hood feed/tile data and fetching adjusted for combined promo handling.

@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-fbe261b
🗄️ PostgreSQL NeonDB branch preview/pr-4869-feat-internal-ad-tiles
Redis Fly Redis mtc-redis-pr-4869-feat-internal-ad-tiles

Details

  • Commit: fbe261b3ccd04117f97508d285523545eccd1ee5
  • 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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dcb904c1-c3dc-4323-af1a-8a6eea27e701

📥 Commits

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

📒 Files selected for processing (2)
  • front_end/src/components/posts_feed/paginated_feed.tsx
  • front_end/src/components/promo_tiles/ad_tile.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • front_end/src/components/posts_feed/paginated_feed.tsx
  • front_end/src/components/promo_tiles/ad_tile.tsx

📝 Walkthrough

Walkthrough

Adds CombinedFeedTile types and seeded RNG; fetch/dismiss APIs for combined tiles; renders promo tiles (ad/project) in feed and question sidebar; supports deterministic daily tile selection, exposure-rate filtering, and client/server dismissal flow.

Changes

Combined feed tiles rollout

Layer / File(s) Summary
Combined tile contracts and deterministic random helper
front_end/src/types/projects.ts, front_end/src/utils/posts_feed.ts
AdTileData and CombinedFeedTile union types are introduced with narrowing type aliases, and seededRandom() provides deterministic RNG for daily tile rotation and ad exposure filtering.
Tile API endpoints and query hook 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
Misc API gains getCombinedFeedTiles() and dismissFeedTile(dismissId) methods; useCombinedFeedTilesQuery replaces the project-tile query hook; server feed component fetches combined tiles.
Feed item union and tile eligibility placement
front_end/src/components/posts_feed/build_feed_items.ts
FeedItem union switches to CombinedFeedTile discriminator, ad tiles are filtered by exposure rate using seeded random, and insertion mapping uses only eligible tiles.
Promo tile UI components and dispatcher
front_end/src/components/promo_tiles/*
TileStatusRow, TournamentTile, and AdTile components render tile status, project/ad UI, analytics, and optional dismiss buttons; PromoTile dispatches between ad and project variants and re-exports related components.
Paginated feed query, dismissal state, and tile rendering
front_end/src/components/posts_feed/paginated_feed.tsx
Feed loads combined tiles via useCombinedFeedTilesQuery, tracks and filters dismissed tile ids from feed items, threads onDismiss callback through layout/card props, and renders tiles via PromoTile.
Sidebar tile selection and dismissal hook
front_end/src/app/(main)/questions/[id]/components/sidebar/similar_questions/use_sidebar_tile.ts
useSidebarTile hook fetches combined tiles from shared cache, selects a daily deterministic tile per question, exposes onDismiss callback that calls the server action and removes the tile from cache, and returns tile, optional dismiss callback, and loading state.
Sidebar similar questions component with tile integration
front_end/src/app/(main)/questions/[id]/components/sidebar/similar_questions/index.tsx
Updated SimilarQuestions sidebar imports useSidebarTile and PromoTile, combines separate questions and tile loading states, and conditionally renders both PromoTile and SimilarQuestionsList.
Question page tab import rewire
front_end/src/app/(main)/questions/[id]/components/question_page_shell/tabs.tsx
SimilarQuestionsTab import path switches to the sidebar similar_questions module.
Server action for tile dismissal
front_end/src/app/(main)/actions.ts
New dismissFeedTile(id: string) server action delegates to serverMiscApi.dismissFeedTile(id).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • elisescu
  • lsabor
  • cemreinanc

Poem

🐰 I nibble seeds of random chance,
I place small tiles to make them dance,
Ads and projects line the feed,
Dismiss a tile — I'll take heed,
Hop, click, and cache now finds its stance.

🚥 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 'Internal Ad Tiles Frontend' is concise, specific, and accurately summarizes the main purpose of the changeset: introducing ad tiles functionality to the frontend.
Linked Issues check ✅ Passed The PR resolves #4866 and implements the core coding requirements: new ad tile components (AdTile, TournamentTile, TileStatusRow, PromoTile), integration with /ad-tiles endpoint, seeded random exposure filtering, feed item placement logic, sidebar integration with React Query caching, and dismiss functionality with analytics.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing internal ad tiles: new promo tile components, feed tile system refactoring, API integration, sidebar integration, utility functions, and server actions. No unrelated changes detected.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 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 and usage tips.

@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 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
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