Layout.jsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. import { useCallback, useEffect, useState } from 'react'
  2. import Link from 'next/link'
  3. import { useRouter } from 'next/router'
  4. import clsx from 'clsx'
  5. import { Hero } from '@/components/Hero'
  6. import { Logo } from '@/components/Logo'
  7. import { MobileNavigation } from '@/components/MobileNavigation'
  8. import { Navigation } from '@/components/Navigation'
  9. import { Prose } from '@/components/Prose'
  10. import { Search } from '@/components/Search'
  11. import { ThemeSelector } from '@/components/ThemeSelector'
  12. function Header({ navigation }) {
  13. let [isScrolled, setIsScrolled] = useState(false)
  14. useEffect(() => {
  15. function onScroll() {
  16. setIsScrolled(window.scrollY > 0)
  17. }
  18. onScroll()
  19. window.addEventListener('scroll', onScroll)
  20. return () => {
  21. window.removeEventListener('scroll', onScroll)
  22. }
  23. }, [])
  24. return (
  25. <header
  26. className={clsx(
  27. 'sticky top-0 z-50 flex flex-wrap items-center justify-between bg-white px-4 py-5 shadow-md shadow-slate-900/5 transition duration-500 dark:shadow-none sm:px-6 lg:px-8',
  28. {
  29. 'dark:bg-slate-900/95 dark:backdrop-blur dark:[@supports(backdrop-filter:blur(0))]:bg-slate-900/75':
  30. isScrolled,
  31. 'dark:bg-transparent': !isScrolled,
  32. }
  33. )}
  34. >
  35. <div className="mr-6 lg:hidden">
  36. <MobileNavigation navigation={navigation} />
  37. </div>
  38. <div className="relative flex flex-grow basis-0 items-center">
  39. <Link href="/">
  40. <a className="block w-10 overflow-hidden lg:w-auto">
  41. <span className="sr-only">Home page</span>
  42. <Logo />
  43. </a>
  44. </Link>
  45. </div>
  46. <div className="-my-5 mr-6 sm:mr-8 md:mr-0">
  47. <Search />
  48. </div>
  49. <div className="relative flex basis-0 justify-end space-x-6 sm:space-x-8 md:flex-grow">
  50. <ThemeSelector className="relative z-10" />
  51. <Link href="https://github.com/coral-xyz/anchor">
  52. <a className="group">
  53. <span className="sr-only">GitHub</span>
  54. <svg
  55. aria-hidden="true"
  56. viewBox="0 0 16 16"
  57. className="h-6 w-6 fill-slate-400 group-hover:fill-slate-500 dark:group-hover:fill-slate-300"
  58. >
  59. <path d="M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z" />
  60. </svg>
  61. </a>
  62. </Link>
  63. </div>
  64. </header>
  65. )
  66. }
  67. export function Layout({ children, title, navigation, tableOfContents }) {
  68. let router = useRouter()
  69. let isHomePage = router.pathname === '/'
  70. let allLinks = navigation.flatMap((section) => section.links)
  71. let linkIndex = allLinks.findIndex((link) => link.href === router.pathname)
  72. let previousPage = allLinks[linkIndex - 1]
  73. let nextPage = allLinks[linkIndex + 1]
  74. let section = navigation.find((section) =>
  75. section.links.find((link) => link.href === router.pathname)
  76. )
  77. let currentSection = useTableOfContents(tableOfContents)
  78. function isActive(section) {
  79. if (section.id === currentSection) {
  80. return true
  81. }
  82. if (!section.children) {
  83. return false
  84. }
  85. return section.children.findIndex(isActive) > -1
  86. }
  87. return (
  88. <>
  89. <Header navigation={navigation} />
  90. {isHomePage && <Hero />}
  91. <div className="relative mx-auto flex max-w-8xl justify-center sm:px-2 lg:px-8 xl:px-12">
  92. <div className="hidden lg:relative lg:block lg:flex-none">
  93. <div className="absolute inset-y-0 right-0 w-[50vw] bg-slate-50 dark:hidden" />
  94. <div className="sticky top-[4.5rem] -ml-0.5 h-[calc(100vh-4.5rem)] overflow-y-auto py-16 pl-0.5">
  95. <div className="absolute top-16 bottom-0 right-0 hidden h-12 w-px bg-gradient-to-t from-slate-800 dark:block" />
  96. <div className="absolute top-28 bottom-0 right-0 hidden w-px bg-slate-800 dark:block" />
  97. <Navigation
  98. navigation={navigation}
  99. className="w-64 pr-8 xl:w-72 xl:pr-16"
  100. />
  101. </div>
  102. </div>
  103. <div className="min-w-0 max-w-2xl flex-auto px-4 py-16 lg:max-w-none lg:pr-0 lg:pl-8 xl:px-16">
  104. <article>
  105. {(title || section) && (
  106. <header className="mb-9 space-y-1">
  107. {section && (
  108. <p className="font-display text-sm font-medium text-sky-500">
  109. {section.title}
  110. </p>
  111. )}
  112. {title && (
  113. <h1 className="font-display text-3xl tracking-tight text-slate-900 dark:text-white">
  114. {title}
  115. </h1>
  116. )}
  117. </header>
  118. )}
  119. <Prose>{children}</Prose>
  120. </article>
  121. <dl className="mt-12 flex border-t border-slate-200 pt-6 dark:border-slate-800">
  122. {previousPage && (
  123. <div>
  124. <dt className="font-display text-sm font-medium text-slate-900 dark:text-white">
  125. Previous
  126. </dt>
  127. <dd className="mt-1">
  128. <Link href={previousPage.href}>
  129. <a className="text-base font-semibold text-slate-500 hover:text-slate-600 dark:text-slate-400 dark:hover:text-slate-300">
  130. &larr; {previousPage.title}
  131. </a>
  132. </Link>
  133. </dd>
  134. </div>
  135. )}
  136. {nextPage && (
  137. <div className="ml-auto text-right">
  138. <dt className="font-display text-sm font-medium text-slate-900 dark:text-white">
  139. Next
  140. </dt>
  141. <dd className="mt-1">
  142. <Link href={nextPage.href}>
  143. <a className="text-base font-semibold text-slate-500 hover:text-slate-600 dark:text-slate-400 dark:hover:text-slate-300">
  144. {nextPage.title} &rarr;
  145. </a>
  146. </Link>
  147. </dd>
  148. </div>
  149. )}
  150. </dl>
  151. </div>
  152. <div className="hidden xl:sticky xl:top-[4.5rem] xl:-mr-6 xl:block xl:h-[calc(100vh-4.5rem)] xl:flex-none xl:overflow-y-auto xl:py-16 xl:pr-6">
  153. <nav aria-labelledby="on-this-page-title" className="w-56">
  154. {tableOfContents.length > 0 && (
  155. <>
  156. <h2
  157. id="on-this-page-title"
  158. className="font-display text-sm font-medium text-slate-900 dark:text-white"
  159. >
  160. On this page
  161. </h2>
  162. <ul className="mt-4 space-y-3 text-sm">
  163. {tableOfContents.map((section) => (
  164. <li key={section.id}>
  165. <h3>
  166. <Link href={`#${section.id}`}>
  167. <a
  168. className={clsx(
  169. isActive(section)
  170. ? 'text-sky-500'
  171. : 'font-normal text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-300'
  172. )}
  173. >
  174. {section.title}
  175. </a>
  176. </Link>
  177. </h3>
  178. {section.children.length > 0 && (
  179. <ul className="mt-2 space-y-3 pl-5 text-slate-500 dark:text-slate-400">
  180. {section.children.map((subSection) => (
  181. <li key={subSection.id}>
  182. <Link href={`#${subSection.id}`}>
  183. <a
  184. className={
  185. isActive(subSection)
  186. ? 'text-sky-500'
  187. : 'hover:text-slate-600 dark:hover:text-slate-300'
  188. }
  189. >
  190. {subSection.title}
  191. </a>
  192. </Link>
  193. </li>
  194. ))}
  195. </ul>
  196. )}
  197. </li>
  198. ))}
  199. </ul>
  200. </>
  201. )}
  202. </nav>
  203. </div>
  204. </div>
  205. </>
  206. )
  207. }
  208. function useTableOfContents(tableOfContents) {
  209. let [currentSection, setCurrentSection] = useState(tableOfContents[0]?.id)
  210. let getHeadings = useCallback(() => {
  211. function* traverse(node) {
  212. if (Array.isArray(node)) {
  213. for (let child of node) {
  214. yield* traverse(child)
  215. }
  216. } else {
  217. let el = document.getElementById(node.id)
  218. if (!el) return
  219. let style = window.getComputedStyle(el)
  220. let scrollMt = parseFloat(style.scrollMarginTop)
  221. let top = window.scrollY + el.getBoundingClientRect().top - scrollMt
  222. yield { id: node.id, top }
  223. for (let child of node.children ?? []) {
  224. yield* traverse(child)
  225. }
  226. }
  227. }
  228. return Array.from(traverse(tableOfContents))
  229. }, [tableOfContents])
  230. useEffect(() => {
  231. let headings = getHeadings()
  232. if (tableOfContents.length === 0 || headings.length === 0) return
  233. function onScroll() {
  234. let sortedHeadings = headings.concat([]).sort((a, b) => a.top - b.top)
  235. let top = window.pageYOffset
  236. let current = sortedHeadings[0].id
  237. for (let i = 0; i < sortedHeadings.length; i++) {
  238. if (top >= sortedHeadings[i].top) {
  239. current = sortedHeadings[i].id
  240. }
  241. }
  242. setCurrentSection(current)
  243. }
  244. window.addEventListener('scroll', onScroll, {
  245. capture: true,
  246. passive: true,
  247. })
  248. onScroll()
  249. return () => {
  250. window.removeEventListener('scroll', onScroll, {
  251. capture: true,
  252. passive: true,
  253. })
  254. }
  255. }, [getHeadings, tableOfContents])
  256. return currentSection
  257. }