11'use client' ;
22
3- import { ArrowRightIcon , ChevronDownIcon } from '@radix-ui/react-icons' ;
3+ import { ArrowRightIcon , CaretSortIcon } from '@radix-ui/react-icons' ;
44import * as RadixSelect from '@radix-ui/react-select' ;
55import { useRouter } from 'next/navigation' ;
6- import { createContext , ReactNode , useContext , useEffect , useMemo , useState } from 'react' ;
6+ import {
7+ createContext ,
8+ ReactNode ,
9+ useCallback ,
10+ useContext ,
11+ useEffect ,
12+ useMemo ,
13+ useRef ,
14+ useState ,
15+ } from 'react' ;
716import { getMDXComponent } from 'sentry-docs/getMDXComponent' ;
817
918import { Alert } from '../alert' ;
@@ -28,14 +37,23 @@ import {
2837 platformContentUrl ,
2938 RUNTIMES ,
3039} from './frameworks' ;
40+ import frameworkStyles from './style.module.scss' ;
3141
3242const DEFAULT_VALUE = 'none' ;
3343const GUIDE_VALUE_PREFIX = 'guide:' ;
3444const groupLabelClassName = 'px-2 pt-2 pb-1 text-xs font-medium text-[var(--gray-11)]' ;
3545
46+ /** The state of a switchable block while a framework is selected. */
47+ type ContentStatus =
48+ | { state : 'loading' | 'unchanged' }
49+ /** `step` is the title of the `h2` step that contains the block, `targetId` the block's id. */
50+ | { state : 'changed' ; targetId : string ; step ?: string } ;
51+
3652type FrameworkContextValue = {
3753 framework : Framework | null ;
3854 platform : string ;
55+ /** Records the status of the block with `includePath`, or removes it with `null`. */
56+ reportContent : ( includePath : string , status : ContentStatus | null ) => void ;
3957 /** The runtime guide the page belongs to, e.g. `node`. */
4058 runtime : string ;
4159} ;
@@ -56,6 +74,65 @@ function ItemLabel({icon, title}: {icon: string; title: string}) {
5674 ) ;
5775}
5876
77+ function stepTitle ( el : HTMLElement ) : string | undefined {
78+ const step = Array . from ( document . querySelectorAll < HTMLElement > ( 'main h2[id]' ) )
79+ . filter ( h => h . compareDocumentPosition ( el ) & Node . DOCUMENT_POSITION_FOLLOWING )
80+ . pop ( ) ;
81+ return ( step ?. querySelector ( 'a' ) ?? step ) ?. textContent ?. trim ( ) ;
82+ }
83+
84+ function FrameworkSummary ( {
85+ contents,
86+ framework,
87+ platform,
88+ } : {
89+ contents : Record < string , ContentStatus > ;
90+ framework : Framework ;
91+ platform : string ;
92+ } ) {
93+ const statuses = Object . values ( contents ) ;
94+ if ( statuses . some ( c => c . state === 'loading' ) ) {
95+ return (
96+ < p className = { frameworkStyles . summary } > Loading the steps for { framework . title } …</ p >
97+ ) ;
98+ }
99+
100+ const changed = statuses
101+ . filter ( c => c . state === 'changed' )
102+ . map ( c => ( { ...c , el : document . getElementById ( c . targetId ) } ) )
103+ . filter ( c => c . el )
104+ . sort ( ( a , b ) =>
105+ a . el ! . compareDocumentPosition ( b . el ! ) & Node . DOCUMENT_POSITION_FOLLOWING ? - 1 : 1
106+ ) ;
107+ const steps = changed . filter (
108+ ( c , i ) => changed . findIndex ( other => other . step === c . step ) === i
109+ ) ;
110+
111+ return (
112+ < p className = { frameworkStyles . summary } >
113+ < PlatformIcon
114+ platform = { `${ platform } -${ framework . key } ` }
115+ size = { 16 }
116+ format = "sm"
117+ className = "!border-none !shadow-none"
118+ />
119+ { steps . length > 0 ? (
120+ < span >
121+ Updated for { framework . title } :{ ' ' }
122+ { steps . map ( ( c , i ) => (
123+ < span key = { c . targetId } >
124+ { i > 0 && ', ' }
125+ < a href = { `#${ c . targetId } ` } > { c . step ?? 'Show the change' } </ a >
126+ </ span >
127+ ) ) }
128+ </ span >
129+ ) : (
130+ < span > The steps for { framework . title } are the same as without a framework.</ span >
131+ ) }
132+ </ p >
133+ ) ;
134+ }
135+
59136function readFrameworkFromUrl ( frameworks : Framework [ ] ) : Framework | null {
60137 const key = new URLSearchParams ( window . location . search ) . get ( FRAMEWORK_QUERY_PARAM ) ;
61138 return frameworks . find ( f => f . key === key ) ?? null ;
@@ -90,6 +167,21 @@ export function FrameworkSelector({
90167 const frameworks = useMemo ( ( ) => runtimeConfig ?. frameworks ?? [ ] , [ runtimeConfig ] ) ;
91168 const frameworkGuides = runtimeConfig ?. frameworkGuides ?? [ ] ;
92169 const [ framework , setFramework ] = useState < Framework | null > ( null ) ;
170+ const [ contents , setContents ] = useState < Record < string , ContentStatus > > ( { } ) ;
171+
172+ const reportContent = useCallback (
173+ ( includePath : string , status : ContentStatus | null ) =>
174+ setContents ( prev => {
175+ const next = { ...prev } ;
176+ if ( status ) {
177+ next [ includePath ] = status ;
178+ } else {
179+ delete next [ includePath ] ;
180+ }
181+ return next ;
182+ } ) ,
183+ [ ]
184+ ) ;
93185
94186 // The page is statically rendered, so the query param can only be read after mount.
95187 useEffect ( ( ) => {
@@ -107,8 +199,8 @@ export function FrameworkSelector({
107199 } ;
108200
109201 const contextValue = useMemo (
110- ( ) => ( { framework, platform, runtime} ) ,
111- [ framework , platform , runtime ]
202+ ( ) => ( { framework, platform, reportContent , runtime} ) ,
203+ [ framework , platform , reportContent , runtime ]
112204 ) ;
113205
114206 return (
@@ -129,7 +221,7 @@ export function FrameworkSelector({
129221 >
130222 < RadixSelect . Value />
131223 < RadixSelect . Icon >
132- < ChevronDownIcon />
224+ < CaretSortIcon />
133225 </ RadixSelect . Icon >
134226 </ RadixSelect . Trigger >
135227 < RadixSelect . Content
@@ -139,10 +231,7 @@ export function FrameworkSelector({
139231 >
140232 < RadixSelect . Item value = { DEFAULT_VALUE } className = { styles . item } >
141233 < RadixSelect . ItemText >
142- < ItemLabel
143- icon = { `${ platform } -${ runtime } ` }
144- title = { `None (plain ${ runtimeConfig ?. title ?? runtime } )` }
145- />
234+ < ItemLabel icon = { `${ platform } -${ runtime } ` } title = "No framework" />
146235 </ RadixSelect . ItemText >
147236 </ RadixSelect . Item >
148237 { frameworks . length > 0 && (
@@ -182,6 +271,9 @@ export function FrameworkSelector({
182271 </ RadixSelect . Root >
183272 </ div >
184273 </ div >
274+ { framework && (
275+ < FrameworkSummary contents = { contents } framework = { framework } platform = { platform } />
276+ ) }
185277 { children }
186278 </ FrameworkContext . Provider >
187279 ) ;
@@ -294,18 +386,40 @@ function RemoteFrameworkContent({
294386 framework,
295387 includePath,
296388 platform,
389+ reportContent,
297390 runtime,
298391} : {
299392 fallback : ReactNode ;
300393 framework : Framework ;
301394 includePath : string ;
302395 platform : string ;
396+ reportContent : FrameworkContextValue [ 'reportContent' ] ;
303397 runtime : string ;
304398} ) {
305399 const url = platformContentUrl ( platform , runtime , framework . key , includePath ) ;
306400 const [ payload , setPayload ] = useState < PlatformContentPayload | null > ( null ) ;
307401 const [ error , setError ] = useState ( false ) ;
308402 const codeContext = useContext ( CodeContext ) ;
403+ const changedRef = useRef < HTMLDivElement > ( null ) ;
404+ const targetId = `framework-content-${ includePath . replace ( / \/ / g, '-' ) } ` ;
405+
406+ useEffect ( ( ) => {
407+ if ( error ) {
408+ reportContent ( includePath , { state : 'unchanged' } ) ;
409+ } else if ( ! payload ) {
410+ reportContent ( includePath , { state : 'loading' } ) ;
411+ } else if ( ! payload . changed || ! changedRef . current ) {
412+ reportContent ( includePath , { state : 'unchanged' } ) ;
413+ } else {
414+ reportContent ( includePath , {
415+ state : 'changed' ,
416+ step : stepTitle ( changedRef . current ) ,
417+ targetId,
418+ } ) ;
419+ }
420+ } , [ error , includePath , payload , reportContent , targetId ] ) ;
421+
422+ useEffect ( ( ) => ( ) => reportContent ( includePath , null ) , [ includePath , reportContent ] ) ;
309423
310424 useEffect ( ( ) => {
311425 let cancelled = false ;
@@ -333,7 +447,7 @@ function RemoteFrameworkContent({
333447 }
334448 } , [ payload , codeContext ?. onboardingOptions ] ) ;
335449
336- if ( error ) {
450+ if ( error || payload ?. changed === false ) {
337451 return fallback ;
338452 }
339453
@@ -345,7 +459,20 @@ function RemoteFrameworkContent({
345459 ) ;
346460 }
347461
348- return < mdx . MDX code = { payload . code } /> ;
462+ return (
463+ < div key = { url } ref = { changedRef } id = { targetId } className = { frameworkStyles . changed } >
464+ < p className = { frameworkStyles . label } >
465+ < PlatformIcon
466+ platform = { `${ platform } -${ framework . key } ` }
467+ size = { 16 }
468+ format = "sm"
469+ className = "!border-none !shadow-none"
470+ />
471+ For { framework . title }
472+ </ p >
473+ < mdx . MDX code = { payload . code } />
474+ </ div >
475+ ) ;
349476}
350477
351478/**
@@ -371,6 +498,7 @@ export function FrameworkContent({
371498 framework = { context . framework }
372499 includePath = { includePath }
373500 platform = { context . platform }
501+ reportContent = { context . reportContent }
374502 runtime = { context . runtime }
375503 />
376504 ) ;
0 commit comments