feat(docs): add Stacked notifications cards#7582
Conversation
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>
|
The latest updates on your projects. Learn more about Argos notifications ↗︎
|
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds 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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ 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. Comment |
There was a problem hiding this comment.
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
slidesto 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
📒 Files selected for processing (2)
apps/docs/src/app/(docs)/(default)/layout.tsxapps/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>
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
apps/docs/src/components/sidebar-banner.tsx (2)
49-52:⚠️ Potential issue | 🟡 MinorClamp the active index before dereferencing
slides[current].If
slidesshrinks while mounted,currentcan 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 | 🟠 MajorVersion 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
📒 Files selected for processing (3)
apps/docs/src/app/(docs)/(default)/layout.tsxapps/docs/src/components/sidebar-banner.tsxapps/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>
There was a problem hiding this comment.
🧹 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.allpattern 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_cachefrom 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
📒 Files selected for processing (2)
apps/docs/src/app/(docs)/(default)/layout.tsxapps/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>
There was a problem hiding this comment.
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
📒 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>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
SidebarBannerCarousel)docs-social.pngimage)Closes DR-7510
Test plan
pnpm devand verify banner appears at bottom of sidebar on desktop🤖 Generated with Claude Code
Summary by CodeRabbit