-
Notifications
You must be signed in to change notification settings - Fork 888
feat(docs): add Stacked notifications cards #7582
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ArthurGamby
wants to merge
9
commits into
main
Choose a base branch
from
dr-7510-add-sidebar-announcement-banner-to-docs
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+260
−9
Open
Changes from 5 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
021817c
feat(docs): add sidebar announcement banner carousel (DR-7510)
ArthurGamby 20c376d
feat(docs): add OG image fetching, update announcement to v7.4 blog post
ArthurGamby 1c46dbf
feat(docs): alternative stacked card design for sidebar banner
ArthurGamby bbd1d5e
feat(docs): alternative stacked card design for sidebar banner
ArthurGamby 63bb2b0
feat(docs): stacked card design for sidebar announcement banner
ArthurGamby 5b17d46
feat(docs): showcase Prisma Next announcement in sidebar banner
ArthurGamby 4e92278
feat(docs): hide sidebar banner on mobile views
ArthurGamby b1e3692
fix(docs): use functional state updater to avoid stale dismissal state
ArthurGamby d04fd71
Merge branch 'main' into dr-7510-add-sidebar-announcement-banner-to-docs
ArthurGamby File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,193 @@ | ||
| "use client"; | ||
|
|
||
| import { useEffect, useState } from "react"; | ||
| import Link from "next/link"; | ||
| import { cn } from "@prisma-docs/ui/lib/cn"; | ||
|
|
||
| interface BannerSlide { | ||
| title: string; | ||
| description: string; | ||
| href: string; | ||
| gradient?: "orm" | "ppg"; | ||
| badge?: string; | ||
| image?: string; | ||
| } | ||
|
|
||
| interface SidebarBannerCarouselProps { | ||
| slides: BannerSlide[]; | ||
| } | ||
|
|
||
| const DISMISSED_KEY = "sidebar-banner-dismissed-ids"; | ||
|
|
||
| export function SidebarBannerCarousel({ slides }: SidebarBannerCarouselProps) { | ||
| const [dismissedIds, setDismissedIds] = useState<Set<string>>(new Set()); | ||
| const [mounted, setMounted] = useState(false); | ||
| const [dismissingHref, setDismissingHref] = useState<string | null>(null); | ||
| const [hovered, setHovered] = useState(false); | ||
|
|
||
| useEffect(() => { | ||
| try { | ||
| const stored = JSON.parse(localStorage.getItem(DISMISSED_KEY) || "[]"); | ||
| setDismissedIds(new Set(stored)); | ||
| } catch { | ||
| /* empty */ | ||
| } | ||
| setMounted(true); | ||
| }, []); | ||
|
|
||
| if (!mounted) return null; | ||
|
|
||
| const visibleSlides = slides.filter( | ||
| (s) => !dismissedIds.has(s.href) && s.href !== dismissingHref, | ||
| ); | ||
|
|
||
| if (visibleSlides.length === 0) return null; | ||
|
|
||
| const peekCount = Math.min(visibleSlides.length - 1, 3); | ||
|
|
||
| function handleDismiss(e: React.MouseEvent, href: string) { | ||
| e.preventDefault(); | ||
| e.stopPropagation(); | ||
| setDismissingHref(href); | ||
| setTimeout(() => { | ||
| const next = new Set(dismissedIds); | ||
| next.add(href); | ||
| setDismissedIds(next); | ||
| localStorage.setItem(DISMISSED_KEY, JSON.stringify([...next])); | ||
| setDismissingHref(null); | ||
| }, 300); | ||
| } | ||
|
|
||
| const front = visibleSlides[0]; | ||
|
|
||
| // Peek cards rendered furthest-back first so DOM order = visual stacking | ||
| const peekCards = visibleSlides.slice(1, 4).map((_, idx, arr) => { | ||
| // i=1 is closest to front, i=peekCount is furthest back | ||
| const i = arr.length - idx; | ||
| const inset = i * 4; | ||
| return ( | ||
| <div | ||
| key={`peek-${i}`} | ||
| className="border border-stroke-neutral bg-background-default shadow-drop-low transition-all duration-300 ease-out" | ||
| aria-hidden | ||
| style={{ | ||
| height: hovered ? 10 : 7, | ||
| marginLeft: inset, | ||
| marginRight: inset, | ||
| borderRadius: "12px 12px 0 0", | ||
| borderBottom: "none", | ||
| opacity: hovered ? 0.4 + (arr.length - i) * 0.15 : 0.25 + (arr.length - i) * 0.1, | ||
| }} | ||
| /> | ||
| ); | ||
| }); | ||
|
|
||
| return ( | ||
| <div | ||
| className="flex flex-col" | ||
| onMouseEnter={() => setHovered(true)} | ||
| onMouseLeave={() => setHovered(false)} | ||
| > | ||
ArthurGamby marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| {/* Peek cards above — each one narrower, creating depth perspective */} | ||
| {peekCount > 0 && ( | ||
| <div className="flex flex-col -mb-px">{peekCards}</div> | ||
| )} | ||
|
|
||
| {/* Front card */} | ||
| <div | ||
| className={cn( | ||
| "relative rounded-high border border-stroke-neutral overflow-hidden shadow-drop-low", | ||
| "bg-background-default transition-shadow hover:shadow-drop", | ||
| )} | ||
| > | ||
| {/* Title + description */} | ||
| <div className="p-3 pb-0"> | ||
| <div className="flex items-center gap-1.5 mb-1"> | ||
| <span className="text-sm font-semibold text-foreground-neutral leading-tight"> | ||
| {front.title} | ||
| </span> | ||
| {front.badge && ( | ||
| <span | ||
| className={cn( | ||
| "text-2xs font-medium px-1.5 py-0.5 rounded-circle shrink-0", | ||
| front.gradient === "ppg" | ||
| ? "bg-background-ppg text-foreground-ppg" | ||
| : "bg-background-orm text-foreground-orm", | ||
| )} | ||
| > | ||
| {front.badge} | ||
| </span> | ||
| )} | ||
| </div> | ||
| <p className="text-xs text-foreground-neutral-weak truncate">{front.description}</p> | ||
| </div> | ||
|
|
||
| {/* Image preview */} | ||
| <div | ||
| className={cn( | ||
| "relative mx-3 mt-2 rounded-square overflow-hidden aspect-video", | ||
| !front.image && (front.gradient === "ppg" ? "bg-gradient-ppg" : "bg-gradient-orm"), | ||
| )} | ||
| > | ||
| {/* eslint-disable-next-line @next/next/no-img-element */} | ||
| {front.image ? ( | ||
| <img | ||
| src={front.image.startsWith("http") ? front.image : `/docs${front.image}`} | ||
| alt="" | ||
| className="absolute inset-0 size-full object-cover" | ||
| /> | ||
| ) : ( | ||
| <div className="flex items-center justify-center size-full"> | ||
| <svg | ||
| viewBox="0 0 28 37" | ||
| fill="none" | ||
| xmlns="http://www.w3.org/2000/svg" | ||
| className="h-8 w-auto opacity-40" | ||
| > | ||
| <path | ||
| d="M27.4 8.42L15.52.32a3.2 3.2 0 00-3.36 0L.32 8.42A3.22 3.22 0 000 11.1v16.2a3.22 3.22 0 001.6 2.78l11.88 7.6a3.2 3.2 0 003.36 0l11.56-7.6a3.2 3.2 0 001.6-2.78V11.1a3.22 3.22 0 00-1.6-2.68zM12.16 33.48L2.24 27.18a1.6 1.6 0 01-.8-1.38v-7.4l10.72 6.5v8.58zm1.28-10.6L2.28 16.22l5.08-3.16 11.16 6.76-5.08 3.06zm13.12-4.56v7.38a1.6 1.6 0 01-.8 1.38l-9.92 6.3v-8.56l10.72-6.5z" | ||
| fill="currentColor" | ||
| className={cn( | ||
| front.gradient === "ppg" | ||
| ? "text-foreground-ppg-strong" | ||
| : "text-foreground-orm-strong", | ||
| )} | ||
| /> | ||
| </svg> | ||
| </div> | ||
| )} | ||
| </div> | ||
|
|
||
| {/* Action bar — appears on hover */} | ||
| <div | ||
| className={cn( | ||
| "flex items-center justify-between px-3 overflow-hidden transition-all duration-300 ease-out", | ||
| hovered ? "max-h-12 opacity-100 py-2.5" : "max-h-0 opacity-0 py-0", | ||
| )} | ||
| > | ||
| <Link | ||
| href={front.href} | ||
| className={cn( | ||
| "text-xs font-medium transition-colors", | ||
| front.gradient === "ppg" | ||
| ? "text-foreground-ppg hover:text-foreground-ppg-strong" | ||
| : "text-foreground-orm hover:text-foreground-orm-strong", | ||
| )} | ||
| > | ||
| Read more | ||
| </Link> | ||
| <button | ||
| type="button" | ||
| onClick={(e) => handleDismiss(e, front.href)} | ||
| className="text-xs text-foreground-neutral-weaker hover:text-foreground-neutral-weak transition-colors" | ||
| > | ||
| Dismiss | ||
| </button> | ||
| </div> | ||
|
|
||
| {/* Bottom padding when action bar is hidden */} | ||
| <div className={cn("transition-all duration-300 ease-out", hovered ? "h-0" : "h-3")} /> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| /** | ||
| * Fetches the og:image URL from a given page URL. | ||
| * Returns null if the fetch fails or no og:image is found. | ||
| */ | ||
| export async function fetchOgImage(url: string): Promise<string | null> { | ||
| try { | ||
| const res = await fetch(url, { | ||
| next: { revalidate: 86400 }, | ||
| signal: AbortSignal.timeout(5000), | ||
| }); | ||
| if (!res.ok) return null; | ||
| const html = await res.text(); | ||
| const match = | ||
| html.match(/<meta[^>]+property=["']og:image["'][^>]+content=["']([^"']+)["']/i) ?? | ||
| html.match(/<meta[^>]+content=["']([^"']+)["'][^>]+property=["']og:image["']/i); | ||
| return match?.[1] ?? null; | ||
ArthurGamby marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.