import clsx from 'clsx' ; import Head from 'next/head' ; import { useRouter } from 'next/router' ; import Script from 'next/script' ; import { useEffect , useRef , useState } from 'react' ; import { createPortal } from 'react-dom' ; import slug from 'slug' ; import { AppBar } from '@/components/AppBar' ; import { Calendar } from '@/components/Calendar' ; import { DownloadButton } from '@/components/DownloadButton' ; import { Footer } from '@/components/Footer' ; import { GlobalTableOfContents } from '@/components/GlobalTableOfContents' ; import { Media } from '@/components/media' ; import { MetaPreviews } from '@/components/MetaPreviews' ; import { QuickLinks } from '@/components/QuickLinks' ; import { SubPageTableOfContents , TableOfContents , } from '@/components/TableOfContents' ; import { API_ROOT_HEADER_KEY } from '@/constants/toc' ; import { TOCHeader , useJupyterBookData } from '@/context/jupyterBook' ; import { useCurrentPathname } from '@/hooks/useCurrentPathname' ; import { isExternalUrl } from '@/utils/url' ; import styles from './App.module.scss' ; /**
 * Hook for determining if the Sub Page TOC is enabled for the current page.
 * This will only be true when the user clicks on a level 1 link.
 */ function useSubPageTocEnabled ( ) { const { rootGlobalHeaders } = useJupyterBookData ( ) ; const currentPathname = useCurrentPathname ( ) ; return rootGlobalHeaders . includes ( currentPathname ) ; } function InPageTableOfContents ( ) { const { pageHeaders , globalHeaders } = useJupyterBookData ( ) ; const currentPathname = useCurrentPathname ( ) ; const subPageTocEnabled = useSubPageTocEnabled ( ) ; const sectionHeaders = [ ] ; if ( subPageTocEnabled && currentPathname !== API_ROOT_HEADER_KEY ) { const header = globalHeaders [ currentPathname ] ; for ( const childId of header . children ?? [ ] ) { const childHeader = globalHeaders [ childId ] ; if ( ( childHeader . children ?. length ?? 0 ) > 0 ) { const { text } = childHeader ; const href = `#${ slug ( text ) }` ; sectionHeaders . push ( { href , text } ) ; } } } return ( < > < Media className = "sticky top-12" greaterThanOrEqual = "screen-1425" > < TableOfContents headers = { [ ... sectionHeaders , ... pageHeaders ] } /> < /Media> < Media lessThan = "screen-1425" > < TableOfContents headers = { [ ... sectionHeaders , ... pageHeaders ] } variant = "collapsed" / > < /Media> < />); } function Content ( ) { const pageContentRef = useRef < HTMLDivElement | null > ( null ) ; const router = useRouter ( ) ; const isSearch = router . asPath . includes ( '/search' ) ; const isTutorial = router . asPath . includes ( '/tutorials' ) ; const { globalHeaders , rootGlobalHeaders , pageTitle , pageBodyHtml , pageFrontMatter : { intro : pageIntro } , } = useJupyterBookData ( ) ; const currentPathname = useCurrentPathname ( ) ; const subPageTocEnabled = useSubPageTocEnabled ( ) ; useEffect ( ( ) => { const pageContentNode = pageContentRef . current ; if ( ! pageContentNode ) { return ; } const linkNodes = Array . from ( pageContentNode . querySelectorAll ( 'a' ) ) ; for ( const link of linkNodes ) { const href = link . href . replace ( window . location . origin , '' ) ; if ( ! isExternalUrl ( href ) ) { link . addEventListener ( 'click' , ( event ) => { event . preventDefault ( ) ; router . push ( href ) ; } ) ; } } } , [ router ] ) ; const [ calendarNode , setCalendarNode ] = useState < HTMLDivElement | null > ( null ) ; useEffect ( ( ) => { const node = document . getElementById ( 'napari-calendar' ) ; if ( node ) { setCalendarNode ( node as HTMLDivElement ) ; } } , [ ] ) ; return ( < > { calendarNode && createPortal ( < div className = "space-y-2" > < h3 className = "font-semibold" > Napari Meetings < /h3> < Calendar / > < /div>, calendarNode)} < div className = { clsx ( 'grid grid-cols-3' , 'screen-1150:grid-cols-napari-4 screen-1425:grid-cols-napari-5' , 'justify-center gap-6 screen-495:gap-12' , 'px-6 pt-4 screen-495:px-12 screen-495:pt-9' ) } > { } < Media greaterThanOrEqual = "screen-1150" > < GlobalTableOfContents headers = { globalHeaders } rootHeaders = { rootGlobalHeaders } / > < /Media> { } < div className = { clsx ( 'col-span-3 mb-6 screen-495:mb-12' , 'screen-1425:col-start-2 screen-1425:col-span-3' , 'overflow-x-auto' ) } > { } < section className = "mb-3 screen-875:mb-10" > < h1 className = "text-5xl leading-tight font-bold inline" > { pageTitle } < /h1> { isTutorial && < DownloadButton className = "-mt-4" / > } < /section> { pageIntro && ( < h2 className = "font-semibold text-xs screen-875:text-base mb-6" > { pageIntro } < /h2>)} { } { ! isSearch && ( < Media lessThan = "screen-1425" > < InPageTableOfContents /> < /Media>)} { } { subPageTocEnabled && ( < SubPageTableOfContents className = "mt-6" headerId = { currentPathname } / > ) } { } < div ref = { pageContentRef } className = { clsx ( 'prose max-w-full' , styles . content , isSearch && styles . search ) } role = "main" dangerouslySetInnerHTML = { { __html : pageBodyHtml } } /> { } < QuickLinks className = "mt-20" / > < /div> { } < Media greaterThanOrEqual = "screen-1425" > < InPageTableOfContents /> < /Media> < /div> < />); } /**
 * Scripts that should load during the `beforeInteractive` stage:
 * https://nextjs.org/docs/basic-features/script
 */ const BEFORE_INTERACTIVE_SCRIPTS = [ 'jquery.js' , 'underscore.js' , 'documentation_options.js' , 'doctools.js' , 'language_data.js' , ] ; /**
 * Scripts that should only be loaded on the search page.
 */ const SEARCH_SCRIPTS = [ 'doctools.js' , 'language_data.js' ] ; /**
 * Root application component responsible for rendering the entire napari.org
 * website. This is used by both the client entry and pre-renderer for rendering
 * the application on the client and server.
 */ export function App ( ) { const initialLoadRef = useRef ( true ) ; const router = useRouter ( ) ; const { appScripts , appStyleSheets , pageFrontMatter : { metaDescription , intro } , } = useJupyterBookData ( ) ; const isSearch = router . asPath . includes ( '/search' ) ; useEffect ( ( ) => { initialLoadRef . current = false ; } , [ ] ) ; return ( < > < Head > { } { ( metaDescription || intro ) && ( < meta name = "description" content = { metaDescription || intro } />)} { appStyleSheets . map ( ( props ) => ( < link key = { props . href } { ... props } / > ) ) } < /Head> { } < MetaPreviews /> { } { isSearch && < div id = "documentation_options" data - url_root = "./" / > } < div className = "min-h-screen flex flex-col" > < AppBar /> < main className = "mt-6 flex-grow" > < Content /> < /main> < Footer / > < /div> { appScripts . filter ( ( { src } ) => { if ( ! src ) { return true ; } return ( isSearch || SEARCH_SCRIPTS . every ( ( script ) => ! src . includes ( script ) ) ) ; } ) . map ( ( props ) => { const id = props . src || ( props . children && String ( props . children ) ) || '' ; return ( < Script id = { id } key = { id } { ... props } strategy = { BEFORE_INTERACTIVE_SCRIPTS . some ( ( script ) => id . includes ( script ) ) ? 'beforeInteractive' : undefined } > { props . children } < /Script>); } ) } < />); }