mirror of
https://github.com/aljazceru/nostr-how.git
synced 2025-12-17 06:14:25 +01:00
Initial commit
This commit is contained in:
297
src/components/Layout.jsx
Normal file
297
src/components/Layout.jsx
Normal file
@@ -0,0 +1,297 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import { Hero } from '@/components/Hero'
|
||||
import { Logo, Logomark } from '@/components/Logo'
|
||||
import { MobileNavigation } from '@/components/MobileNavigation'
|
||||
import { Navigation } from '@/components/Navigation'
|
||||
import { Prose } from '@/components/Prose'
|
||||
import { Search } from '@/components/Search'
|
||||
import { ThemeSelector } from '@/components/ThemeSelector'
|
||||
|
||||
const navigation = [
|
||||
{
|
||||
title: 'The Basics',
|
||||
links: [
|
||||
{ title: 'What is Nostr?', href: '/' },
|
||||
{ title: 'Get started', href: '/get-started' },
|
||||
{ title: 'Verify your identity', href: '/verify-your-identity' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'The Protocol',
|
||||
links: [
|
||||
{ title: 'The Nostr Protocol', href: '/the-protocol' },
|
||||
{ title: 'Events', href: '/the-protocol/events' },
|
||||
{ title: 'NIPs', href: '/the-protocol/nips' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'The Clients',
|
||||
links: [
|
||||
{ title: 'How do clients work?', href: '/clients' },
|
||||
{ title: 'Client comparison list', href: '/clients/comparison' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Resources',
|
||||
links: [
|
||||
{ title: 'Other resources', href: '/resources' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Contributing',
|
||||
links: [
|
||||
{ title: 'How to contribute', href: '/contribute' },
|
||||
{ title: 'Help us translate', href: '/contribute/translate' },
|
||||
{ title: 'Write guides', href: '/contribute/write' },
|
||||
{ title: 'Improve the site', href: '/contribute/code' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
function GitHubIcon(props) {
|
||||
return (
|
||||
<svg aria-hidden="true" viewBox="0 0 16 16" {...props}>
|
||||
<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" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function Header({ navigation }) {
|
||||
let [isScrolled, setIsScrolled] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
function onScroll() {
|
||||
setIsScrolled(window.scrollY > 0)
|
||||
}
|
||||
onScroll()
|
||||
window.addEventListener('scroll', onScroll, { passive: true })
|
||||
return () => {
|
||||
window.removeEventListener('scroll', onScroll, { passive: true })
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<header
|
||||
className={clsx(
|
||||
'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',
|
||||
isScrolled
|
||||
? 'dark:bg-slate-900/95 dark:backdrop-blur dark:[@supports(backdrop-filter:blur(0))]:bg-slate-900/75'
|
||||
: 'dark:bg-transparent'
|
||||
)}
|
||||
>
|
||||
<div className="mr-6 flex lg:hidden">
|
||||
<MobileNavigation navigation={navigation} />
|
||||
</div>
|
||||
<div className="relative flex flex-grow basis-0 items-center">
|
||||
<Link href="/" aria-label="Home page">
|
||||
<Logo className="h-9 w-auto fill-slate-700 dark:fill-sky-100 lg:block" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="-my-5 mr-6 sm:mr-8 md:mr-0">
|
||||
<Search />
|
||||
</div>
|
||||
<div className="relative flex basis-0 justify-end gap-6 sm:gap-8 md:flex-grow">
|
||||
<ThemeSelector className="relative z-10" />
|
||||
<Link href="https://github.com/nostr-protocol/nostr" className="group" aria-label="GitHub">
|
||||
<GitHubIcon className="h-6 w-6 fill-slate-400 group-hover:fill-slate-500 dark:group-hover:fill-slate-300" />
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
function useTableOfContents(tableOfContents) {
|
||||
let [currentSection, setCurrentSection] = useState(tableOfContents[0]?.id)
|
||||
|
||||
let getHeadings = useCallback((tableOfContents) => {
|
||||
return tableOfContents
|
||||
.flatMap((node) => [node.id, ...node.children.map((child) => child.id)])
|
||||
.map((id) => {
|
||||
let el = document.getElementById(id)
|
||||
if (!el) return
|
||||
|
||||
let style = window.getComputedStyle(el)
|
||||
let scrollMt = parseFloat(style.scrollMarginTop)
|
||||
|
||||
let top = window.scrollY + el.getBoundingClientRect().top - scrollMt
|
||||
return { id, top }
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (tableOfContents.length === 0) return
|
||||
let headings = getHeadings(tableOfContents)
|
||||
function onScroll() {
|
||||
let top = window.scrollY
|
||||
let current = headings[0].id
|
||||
for (let heading of headings) {
|
||||
if (top >= heading.top) {
|
||||
current = heading.id
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
setCurrentSection(current)
|
||||
}
|
||||
window.addEventListener('scroll', onScroll, { passive: true })
|
||||
onScroll()
|
||||
return () => {
|
||||
window.removeEventListener('scroll', onScroll, { passive: true })
|
||||
}
|
||||
}, [getHeadings, tableOfContents])
|
||||
|
||||
return currentSection
|
||||
}
|
||||
|
||||
export function Layout({ children, title, tableOfContents }) {
|
||||
let router = useRouter()
|
||||
let isHomePage = router.pathname === '/'
|
||||
let allLinks = navigation.flatMap((section) => section.links)
|
||||
let linkIndex = allLinks.findIndex((link) => link.href === router.pathname)
|
||||
let previousPage = allLinks[linkIndex - 1]
|
||||
let nextPage = allLinks[linkIndex + 1]
|
||||
let section = navigation.find((section) =>
|
||||
section.links.find((link) => link.href === router.pathname)
|
||||
)
|
||||
let currentSection = useTableOfContents(tableOfContents)
|
||||
|
||||
function isActive(section) {
|
||||
if (section.id === currentSection) {
|
||||
return true
|
||||
}
|
||||
if (!section.children) {
|
||||
return false
|
||||
}
|
||||
return section.children.findIndex(isActive) > -1
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header navigation={navigation} />
|
||||
|
||||
{isHomePage && <Hero />}
|
||||
|
||||
<div className="relative mx-auto flex max-w-8xl justify-center sm:px-2 lg:px-8 xl:px-12">
|
||||
<div className="hidden lg:relative lg:block lg:flex-none">
|
||||
<div className="absolute inset-y-0 right-0 w-[50vw] bg-slate-50 dark:hidden" />
|
||||
<div className="absolute top-16 bottom-0 right-0 hidden h-12 w-px bg-gradient-to-t from-slate-800 dark:block" />
|
||||
<div className="absolute top-28 bottom-0 right-0 hidden w-px bg-slate-800 dark:block" />
|
||||
<div className="sticky top-[4.5rem] -ml-0.5 h-[calc(100vh-4.5rem)] overflow-y-auto overflow-x-hidden py-16 pl-0.5">
|
||||
<Navigation
|
||||
navigation={navigation}
|
||||
className="w-64 pr-8 xl:w-72 xl:pr-16"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<article>
|
||||
{(title || section) && (
|
||||
<header className="mb-9 space-y-1">
|
||||
{section && (
|
||||
<p className="font-display text-sm font-medium text-sky-500">
|
||||
{section.title}
|
||||
</p>
|
||||
)}
|
||||
{title && (
|
||||
<h1 className="font-display text-3xl tracking-tight text-slate-900 dark:text-white">
|
||||
{title}
|
||||
</h1>
|
||||
)}
|
||||
</header>
|
||||
)}
|
||||
<Prose>{children}</Prose>
|
||||
</article>
|
||||
<dl className="mt-12 flex border-t border-slate-200 pt-6 dark:border-slate-800">
|
||||
{previousPage && (
|
||||
<div>
|
||||
<dt className="font-display text-sm font-medium text-slate-900 dark:text-white">
|
||||
Previous
|
||||
</dt>
|
||||
<dd className="mt-1">
|
||||
<Link
|
||||
href={previousPage.href}
|
||||
className="text-base font-semibold text-slate-500 hover:text-slate-600 dark:text-slate-400 dark:hover:text-slate-300"
|
||||
>
|
||||
<span aria-hidden="true">←</span> {previousPage.title}
|
||||
</Link>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{nextPage && (
|
||||
<div className="ml-auto text-right">
|
||||
<dt className="font-display text-sm font-medium text-slate-900 dark:text-white">
|
||||
Next
|
||||
</dt>
|
||||
<dd className="mt-1">
|
||||
<Link
|
||||
href={nextPage.href}
|
||||
className="text-base font-semibold text-slate-500 hover:text-slate-600 dark:text-slate-400 dark:hover:text-slate-300"
|
||||
>
|
||||
{nextPage.title} <span aria-hidden="true">→</span>
|
||||
</Link>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
</div>
|
||||
<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">
|
||||
<nav aria-labelledby="on-this-page-title" className="w-56">
|
||||
{tableOfContents.length > 0 && (
|
||||
<>
|
||||
<h2
|
||||
id="on-this-page-title"
|
||||
className="font-display text-sm font-medium text-slate-900 dark:text-white"
|
||||
>
|
||||
On this page
|
||||
</h2>
|
||||
<ol role="list" className="mt-4 space-y-3 text-sm">
|
||||
{tableOfContents.map((section) => (
|
||||
<li key={section.id}>
|
||||
<h3>
|
||||
<Link
|
||||
href={`#${section.id}`}
|
||||
className={clsx(
|
||||
isActive(section)
|
||||
? 'text-sky-500'
|
||||
: 'font-normal text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-300'
|
||||
)}
|
||||
>
|
||||
{section.title}
|
||||
</Link>
|
||||
</h3>
|
||||
{section.children.length > 0 && (
|
||||
<ol
|
||||
role="list"
|
||||
className="mt-2 space-y-3 pl-5 text-slate-500 dark:text-slate-400"
|
||||
>
|
||||
{section.children.map((subSection) => (
|
||||
<li key={subSection.id}>
|
||||
<Link
|
||||
href={`#${subSection.id}`}
|
||||
className={
|
||||
isActive(subSection)
|
||||
? 'text-sky-500'
|
||||
: 'hover:text-slate-600 dark:hover:text-slate-300'
|
||||
}
|
||||
>
|
||||
{subSection.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user