Skip to content

feat(docs): add Stacked notifications cards#7582

Open
ArthurGamby wants to merge 8 commits intomainfrom
dr-7510-add-sidebar-announcement-banner-to-docs
Open

feat(docs): add Stacked notifications cards#7582
ArthurGamby wants to merge 8 commits intomainfrom
dr-7510-add-sidebar-announcement-banner-to-docs

Conversation

@ArthurGamby
Copy link
Contributor

@ArthurGamby ArthurGamby commented Mar 3, 2026

Summary

  • Adds a dismissible carousel banner component to the docs sidebar (SidebarBannerCarousel)
  • Supports multiple slides with auto-rotation (5s interval), gradient or image headers, badge pills, and a dismiss button (persisted via localStorage)
  • Wires two initial slides: "Prisma 7 is here" (ORM gradient) and "We're hiring" (with docs-social.png image)

Note: This is an exploratory PR — we may want to manage banner content through Sanity or a config file in the future rather than hardcoding it in the layout.

Closes DR-7510

Test plan

  • Run pnpm dev and verify banner appears at bottom of sidebar on desktop
  • Verify banner appears in mobile drawer sidebar
  • Confirm carousel auto-rotates between slides
  • Click the X button — banner should disappear and stay dismissed on refresh
  • Toggle light/dark mode — verify both slides look correct
  • Click the banner link — verify navigation works

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Dismissible, hover-revealed sidebar banner carousel with layered peek visuals and action bar (Read more + Dismiss).
    • Dismissals persist across visits and respect hydration timing.
    • External slides show fetched preview images resolved server-side.
    • Sidebar footer now renders the banner while preserving existing sidebar content.

Add a dismissible carousel banner to the docs sidebar footer with
support for multiple announcement slides, auto-rotation, and
configurable gradient/image headers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Mar 3, 2026

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

Project Deployment Actions Updated (UTC)
blog Ready Ready Preview, Comment Mar 4, 2026 3:42pm
docs Ready Ready Preview, Comment Mar 4, 2026 3:42pm
eclipse Ready Ready Preview, Comment Mar 4, 2026 3:42pm

Request Review

@argos-ci
Copy link

argos-ci bot commented Mar 3, 2026

The latest updates on your projects. Learn more about Argos notifications ↗︎

Build Status Details Updated (UTC)
default (Inspect) ⚠️ Changes detected (Review) 1 removed Mar 4, 2026, 3:48 PM

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 3, 2026

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

Adds a new client-side SidebarBannerCarousel component, a server utility fetchOgImage, and integrates a server-resolved SIDEBAR_SLIDES banner into the docs layout sidebar footer; includes localStorage-based dismiss logic and server-side OG image resolution for external slide links.

Changes

Cohort / File(s) Summary
Docs layout
apps/docs/src/app/(docs)/(default)/layout.tsx
Introduces SIDEBAR_SLIDES, imports SidebarBannerCarousel, fetchOgImage, and cn; resolves OG images server-side for external slide hrefs into slides; injects the carousel as sidebar.footer. Minor signature/formatting tweak on Layout.
Sidebar banner component
apps/docs/src/components/sidebar-banner.tsx
New client component SidebarBannerCarousel and exported SidebarBannerCarouselProps. Implements up to three-card stacked carousel, hover action bar, dismiss flow with 300ms delay, localStorage-persisted dismissed IDs, mount guard, and media preview (image or gradient).
OG image fetcher
apps/docs/src/lib/og-image.ts
New `fetchOgImage(url: string): Promise<string

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ⚠️ Warning The PR title mentions 'Stacked notifications cards' but the actual changes implement a sidebar banner carousel component with multiple slides, dismissal, and image fetching—not a stacked notifications pattern. Update the title to reflect the primary change, such as 'feat(docs): add sidebar banner carousel component' or 'feat(docs): add dismissible sidebar announcement carousel'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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


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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/docs/src/app/(docs)/(default)/layout.tsx (1)

31-47: Extract banner slide data into a config/module.

Keeping campaign copy inline in layout makes updates/reuse/testing harder. Move slides to a dedicated constant (or config source) and pass it in.

Proposed refactor
+import { SIDEBAR_BANNER_SLIDES } from "@/lib/sidebar-banners";
...
             <SidebarBannerCarousel
-              slides={[
-                {
-                  title: "Prisma 7 is here",
-                  description: "Check out the latest release with new features and improvements.",
-                  href: "/docs/v7/release-notes",
-                  gradient: "orm",
-                  badge: "New",
-                },
-                {
-                  title: "We're hiring",
-                  description: "Join the Prisma team and help shape the future of databases.",
-                  href: "https://www.prisma.io/careers",
-                  gradient: "ppg",
-                  image: "/img/docs-social.png",
-                },
-              ]}
+              slides={SIDEBAR_BANNER_SLIDES}
             />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/docs/src/app/`(docs)/(default)/layout.tsx around lines 31 - 47, Extract
the inline slides array from the SidebarBannerCarousel call into a dedicated
exported constant (e.g., BANNER_SLIDES) in a new or existing module and import
it into the layout component; specifically, replace the inline slides prop in
the layout.tsx SidebarBannerCarousel usage with slides={BANNER_SLIDES} and move
the array definition (objects with title, description, href, gradient, badge,
image) into a config file or a top-level constant export so it can be reused and
unit-tested independently from the layout.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/docs/src/components/sidebar-banner.tsx`:
- Around line 49-52: When dereferencing slides[current] in sidebar-banner
(variables dismissed, slides, current), clamp the index first to avoid an
undefined slide if slides shrinks: compute a safeIndex = Math.max(0,
Math.min(current, slides.length - 1)) and use slides[safeIndex] instead of
slides[current]; apply the same clamped-index change to the other occurrence
that also reads slides[current] later in the file.
- Line 23: The dismissal key is global (DISMISSED_KEY) so dismissing one
campaign hides all future banners; update the SidebarBanner component to accept
a campaign-specific key (e.g., add a storageKey prop or derive from a campaignId
prop) and replace usages of DISMISSED_KEY with that campaign-specific key when
reading/writing localStorage (or equivalent). Ensure the default behavior
preserves current behavior (use DISMISSED_KEY as fallback) and update any
functions/methods that call DISMISSED_KEY to use the new storageKey value
instead.

---

Nitpick comments:
In `@apps/docs/src/app/`(docs)/(default)/layout.tsx:
- Around line 31-47: Extract the inline slides array from the
SidebarBannerCarousel call into a dedicated exported constant (e.g.,
BANNER_SLIDES) in a new or existing module and import it into the layout
component; specifically, replace the inline slides prop in the layout.tsx
SidebarBannerCarousel usage with slides={BANNER_SLIDES} and move the array
definition (objects with title, description, href, gradient, badge, image) into
a config file or a top-level constant export so it can be reused and unit-tested
independently from the layout.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ac8da00 and 021817c.

📒 Files selected for processing (2)
  • apps/docs/src/app/(docs)/(default)/layout.tsx
  • apps/docs/src/components/sidebar-banner.tsx

- Fetch og:image server-side with 24h cache, gradient fallback
- Update first slide to Prisma ORM v7.4 blog post
- Remove hiring slide (single announcement for now)
- Use aspect-video for proper 16:9 OG image display

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
apps/docs/src/components/sidebar-banner.tsx (2)

49-52: ⚠️ Potential issue | 🟡 Minor

Clamp the active index before dereferencing slides[current].

If slides shrinks while mounted, current can point out of bounds and crash render.

Proposed fix
-  const slide = slides[current];
+  const safeCurrent = Math.max(0, Math.min(current, slides.length - 1));
+  const slide = slides[safeCurrent];
@@
-                      i === current ? "bg-foreground-neutral-weak" : "bg-stroke-neutral",
+                      i === safeCurrent ? "bg-foreground-neutral-weak" : "bg-stroke-neutral",

Also applies to: 146-151

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/docs/src/components/sidebar-banner.tsx` around lines 49 - 52, The
component dereferences slides[current] without ensuring current is in-range,
which can crash if slides shrinks; fix by clamping the index before use (e.g.,
compute const safeIndex = Math.max(0, Math.min(current, slides.length - 1))) and
use slides[safeIndex] instead of slides[current] wherever the active slide is
read (notably in the assignment of slide and the other occurrence around the
146-151 block); also keep the early return for empty slides to avoid negative
indices.

17-21: ⚠️ Potential issue | 🟠 Major

Version the dismissal storage key per campaign.

Using one global key means dismissing once suppresses future announcements too. Make the key configurable (or campaign-derived).

Proposed fix
 interface SidebarBannerCarouselProps {
   slides: BannerSlide[];
   /** Auto-rotate interval in ms `@default` 5000 */
   interval?: number;
+  /** Change per campaign to re-show new announcements */
+  storageKey?: string;
 }
 
 const DISMISSED_KEY = "sidebar-banner-dismissed";
 
-export function SidebarBannerCarousel({ slides, interval = 5000 }: SidebarBannerCarouselProps) {
+export function SidebarBannerCarousel({
+  slides,
+  interval = 5000,
+  storageKey = DISMISSED_KEY,
+}: SidebarBannerCarouselProps) {
@@
   useEffect(() => {
-    setDismissed(localStorage.getItem(DISMISSED_KEY) === "true");
-  }, []);
+    setDismissed(localStorage.getItem(storageKey) === "true");
+  }, [storageKey]);
@@
-    localStorage.setItem(DISMISSED_KEY, "true");
+    localStorage.setItem(storageKey, "true");

Also applies to: 23-23, 25-32, 56-57

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/docs/src/components/sidebar-banner.tsx` around lines 17 - 21, The
dismissal storage key is global—change it to be campaign-specific by deriving it
from each banner/campaign identifier: add a campaignId (or id) field to the
BannerSlide type (or accept a dismissalKey/campaignId prop on
SidebarBannerCarouselProps) and use that value when constructing the
localStorage/sessionStorage key instead of the single global key; update the
code that reads/writes dismissal state for slides (references to slides and
BannerSlide and the SidebarBannerCarousel component) to include the
campaign-derived key so dismissing one campaign does not suppress others.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/docs/src/lib/og-image.ts`:
- Around line 13-16: The extracted og:image value (from match?.[1]) may be
relative and must be normalized: after extracting content, construct a new URL
using the page's source URL as the base (use the URL constructor with try/catch)
to resolve relative paths, validate that the resulting URL has protocol 'http:'
or 'https:' and return its href; if constructing the URL fails or the protocol
is not http(s), return null. Apply this logic where the current html match and
match?.[1] are used so the function returns a normalized absolute HTTP(S) URL or
null.

---

Duplicate comments:
In `@apps/docs/src/components/sidebar-banner.tsx`:
- Around line 49-52: The component dereferences slides[current] without ensuring
current is in-range, which can crash if slides shrinks; fix by clamping the
index before use (e.g., compute const safeIndex = Math.max(0, Math.min(current,
slides.length - 1))) and use slides[safeIndex] instead of slides[current]
wherever the active slide is read (notably in the assignment of slide and the
other occurrence around the 146-151 block); also keep the early return for empty
slides to avoid negative indices.
- Around line 17-21: The dismissal storage key is global—change it to be
campaign-specific by deriving it from each banner/campaign identifier: add a
campaignId (or id) field to the BannerSlide type (or accept a
dismissalKey/campaignId prop on SidebarBannerCarouselProps) and use that value
when constructing the localStorage/sessionStorage key instead of the single
global key; update the code that reads/writes dismissal state for slides
(references to slides and BannerSlide and the SidebarBannerCarousel component)
to include the campaign-derived key so dismissing one campaign does not suppress
others.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 021817c and 20c376d.

📒 Files selected for processing (3)
  • apps/docs/src/app/(docs)/(default)/layout.tsx
  • apps/docs/src/components/sidebar-banner.tsx
  • apps/docs/src/lib/og-image.ts

Replaces the carousel with a stacked card notification pattern:
- Ghost/peek cards visible behind the front card
- Hover lifts the stack and reveals Read more / Dismiss actions
- Dismiss slides out the front card, next one takes its place
- Per-card dismissal persisted in localStorage
- Three slides: v7.4 blog, Prisma Next announcement, ORM 7 launch

This is an alternative design option — revert this commit to
go back to the carousel version.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replaces the carousel with a stacked card notification pattern:
- Ghost/peek cards stack upward above the front card
- Hover pushes stack down and reveals Read more / Dismiss actions
- Dismiss removes the front card, next one takes its place
- Per-card dismissal persisted in localStorage
- Three slides: v7.4 blog, Prisma Next announcement, ORM 7 launch

This is an alternative design option — revert this commit to
go back to the carousel version.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/docs/src/app/(docs)/(default)/layout.tsx (1)

48-57: Server-side OG fetching is well-structured, but consider caching for performance.

The Promise.all pattern is efficient for parallel resolution, and the graceful fallback (returning original slide when no image is found) is good defensive coding.

One consideration: these external HTTP fetches run on every page request. For a docs site with significant traffic, this could add latency. A caching layer (e.g., unstable_cache from Next.js or memoizing with a reasonable TTL) would help.

💡 Example with Next.js caching
import { unstable_cache } from "next/cache";

const getCachedOgImage = unstable_cache(
  async (url: string) => fetchOgImage(url),
  ["og-image"],
  { revalidate: 86400 } // Cache for 24 hours
);

// Then in the layout:
const ogImage = await getCachedOgImage(slide.href);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/docs/src/app/`(docs)/(default)/layout.tsx around lines 48 - 57, The
slide OG fetch loop currently calls fetchOgImage for every request via
Promise.all over SIDEBAR_SLIDES; wrap that call in a cache to avoid repeated
external fetches — create a cached wrapper (e.g., getCachedOgImage using Next.js
unstable_cache or a simple in-memory/memo with TTL) and replace direct calls to
fetchOgImage in the map with await getCachedOgImage(slide.href), keeping the
same fallback behavior (return original slide when no image) so the rest of the
logic (Promise.all, SIDEBAR_SLIDES mapping) remains unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/docs/src/app/`(docs)/(default)/layout.tsx:
- Around line 48-57: The slide OG fetch loop currently calls fetchOgImage for
every request via Promise.all over SIDEBAR_SLIDES; wrap that call in a cache to
avoid repeated external fetches — create a cached wrapper (e.g.,
getCachedOgImage using Next.js unstable_cache or a simple in-memory/memo with
TTL) and replace direct calls to fetchOgImage in the map with await
getCachedOgImage(slide.href), keeping the same fallback behavior (return
original slide when no image) so the rest of the logic (Promise.all,
SIDEBAR_SLIDES mapping) remains unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c94793e0-955b-4e3a-81e6-eb496e72def2

📥 Commits

Reviewing files that changed from the base of the PR and between 20c376d and 1c46dbf.

📒 Files selected for processing (2)
  • apps/docs/src/app/(docs)/(default)/layout.tsx
  • apps/docs/src/components/sidebar-banner.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/docs/src/components/sidebar-banner.tsx

Replaces the carousel with a stacked card notification pattern:
- Peek cards visible above the front card with perspective narrowing
- Hover fans the deck out and reveals Read more / Dismiss actions
- Dismiss removes the front card, next one takes its place
- Per-card dismissal persisted in localStorage
- Slides render in array order, empty array hides the component
- Three slides: v7.4 blog, Prisma Next announcement, ORM 7 launch

This is an alternative design option — revert this commit to
go back to the carousel version.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/docs/src/components/sidebar-banner.tsx`:
- Around line 86-90: The hover-only reveal for actions in the SidebarBanner
component (the onMouseEnter/onMouseLeave callbacks and the setHovered/hovered
state) hides the “Read more” and “Dismiss” controls on touch devices; update the
component to ensure actions are reachable on mobile by removing hover-only
gating or adding touch/focus-safe alternatives — for example, add
onTouchStart/onFocus handlers that setHovered(true) (and onTouchEnd/onBlur to
unset), or detect coarse pointers via a CSS media query and always render the
action buttons when a mobile drawer prop is open; ensure the UI still uses the
hovered state (setHovered/hovered) but falls back to always-visible or
touch-accessible controls so Read more/Dismiss are tappable.
- Around line 48-58: The delayed callback in handleDismiss captures a stale
dismissedIds snapshot and can overwrite prior dismissals; inside the setTimeout
replace the snapshot-based update with a functional state updater: call
setDismissedIds(prev => { const next = new Set(prev); next.add(href);
localStorage.setItem(DISMISSED_KEY, JSON.stringify([...next])); return next; });
and then keep setDismissingHref(null) after that; this ensures updates use the
latest dismissedIds and localStorage is written from the computed next set.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c80b3749-7d09-4faf-881d-dd9508153cf9

📥 Commits

Reviewing files that changed from the base of the PR and between bbd1d5e and 63bb2b0.

📒 Files selected for processing (1)
  • apps/docs/src/components/sidebar-banner.tsx

- Set single slide for Prisma Next blog post (pris.ly/pn-anouncement)
- Open link in new tab
- Ready to add more slides later by extending the array

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ArthurGamby ArthurGamby changed the title feat(docs): add sidebar announcement banner carousel feat(docs): add Stacked notifications cards Mar 4, 2026
coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 4, 2026
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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