mirror of
https://github.com/netbirdio/docs.git
synced 2026-04-15 23:16:36 +00:00
added banner (#484)
This commit is contained in:
committed by
GitHub
parent
fe9d74b566
commit
36b8eb060a
@@ -12,6 +12,7 @@ import {
|
|||||||
import { useMobileNavigationStore } from '@/components/MobileNavigation'
|
import { useMobileNavigationStore } from '@/components/MobileNavigation'
|
||||||
import { ModeToggle } from '@/components/ModeToggle'
|
import { ModeToggle } from '@/components/ModeToggle'
|
||||||
import { MobileSearch, Search } from '@/components/Search'
|
import { MobileSearch, Search } from '@/components/Search'
|
||||||
|
import { useAnnouncements } from '@/components/announcement-banner/AnnouncementBannerProvider'
|
||||||
|
|
||||||
function TopLevelNavItem({ href, children }) {
|
function TopLevelNavItem({ href, children }) {
|
||||||
return (
|
return (
|
||||||
@@ -29,6 +30,7 @@ function TopLevelNavItem({ href, children }) {
|
|||||||
export const Header = forwardRef(function Header({ className }, ref) {
|
export const Header = forwardRef(function Header({ className }, ref) {
|
||||||
let { isOpen: mobileNavIsOpen } = useMobileNavigationStore()
|
let { isOpen: mobileNavIsOpen } = useMobileNavigationStore()
|
||||||
let isInsideMobileNavigation = useIsInsideMobileNavigation()
|
let isInsideMobileNavigation = useIsInsideMobileNavigation()
|
||||||
|
let { bannerHeight } = useAnnouncements()
|
||||||
|
|
||||||
let { scrollY } = useScroll()
|
let { scrollY } = useScroll()
|
||||||
let bgOpacityLight = useTransform(scrollY, [0, 72], [0.5, 0.9])
|
let bgOpacityLight = useTransform(scrollY, [0, 72], [0.5, 0.9])
|
||||||
@@ -39,7 +41,7 @@ export const Header = forwardRef(function Header({ className }, ref) {
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
className,
|
className,
|
||||||
'fixed inset-x-0 top-0 z-50 flex h-14 items-center justify-between gap-12 px-4 transition sm:px-6 lg:left-72 lg:z-30 lg:px-8 xl:left-80',
|
'fixed inset-x-0 top-0 z-40 flex h-14 items-center justify-between gap-12 px-4 transition sm:px-6 lg:left-72 lg:z-30 lg:px-8 xl:left-80',
|
||||||
!isInsideMobileNavigation &&
|
!isInsideMobileNavigation &&
|
||||||
'backdrop-blur-sm dark:backdrop-blur lg:left-72 xl:left-80',
|
'backdrop-blur-sm dark:backdrop-blur lg:left-72 xl:left-80',
|
||||||
isInsideMobileNavigation
|
isInsideMobileNavigation
|
||||||
@@ -49,6 +51,7 @@ export const Header = forwardRef(function Header({ className }, ref) {
|
|||||||
style={{
|
style={{
|
||||||
'--bg-opacity-light': bgOpacityLight,
|
'--bg-opacity-light': bgOpacityLight,
|
||||||
'--bg-opacity-dark': bgOpacityDark,
|
'--bg-opacity-dark': bgOpacityDark,
|
||||||
|
top: bannerHeight,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
|||||||
import { faPaperclip } from '@fortawesome/free-solid-svg-icons';
|
import { faPaperclip } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { faGithub } from '@fortawesome/free-brands-svg-icons';
|
import { faGithub } from '@fortawesome/free-brands-svg-icons';
|
||||||
import {toast} from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
|
import {AnnouncementBanner} from "@/components/announcement-banner/AnnouncementBanner";
|
||||||
|
import {useAnnouncements} from "@/components/announcement-banner/AnnouncementBannerProvider";
|
||||||
|
|
||||||
const navigation = [
|
const navigation = [
|
||||||
{
|
{
|
||||||
@@ -166,13 +168,20 @@ export function Layout({ children, title, tableOfContents }) {
|
|||||||
return section.children.findIndex(isActive) > -1
|
return section.children.findIndex(isActive) > -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let { bannerHeight } = useAnnouncements()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<AnnouncementBanner />
|
||||||
<HeroPattern/>
|
<HeroPattern/>
|
||||||
<div className="relative mx-auto flex max-w-8xl sm:px-2 lg:px-8 xl:px-12 lg:ml-72 xl:ml-80">
|
<div
|
||||||
|
className="relative mx-auto flex max-w-8xl sm:px-2 lg:px-8 xl:px-12 lg:ml-72 xl:ml-80"
|
||||||
|
style={{ paddingTop: bannerHeight }}
|
||||||
|
>
|
||||||
<motion.header
|
<motion.header
|
||||||
layoutScroll
|
layoutScroll
|
||||||
className="contents lg:pointer-events-none lg:fixed lg:inset-0 lg:z-40 lg:flex"
|
className="contents lg:pointer-events-none lg:fixed lg:inset-0 lg:z-40 lg:flex"
|
||||||
|
style={{ top: bannerHeight }}
|
||||||
>
|
>
|
||||||
<div className="contents lg:pointer-events-auto lg:block lg:w-72 lg:overflow-y-auto lg:border-r lg:border-zinc-900/10 lg:px-6 lg:pb-8 lg:pt-4 lg:dark:border-white/10 xl:w-80">
|
<div className="contents lg:pointer-events-auto lg:block lg:w-72 lg:overflow-y-auto lg:border-r lg:border-zinc-900/10 lg:px-6 lg:pb-8 lg:pt-4 lg:dark:border-white/10 xl:w-80">
|
||||||
<div className="hidden lg:flex">
|
<div className="hidden lg:flex">
|
||||||
@@ -190,7 +199,10 @@ export function Layout({ children, title, tableOfContents }) {
|
|||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
{!router.route.startsWith("/ipa/resources") && <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 pl-12">
|
{!router.route.startsWith("/ipa/resources") && <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 pl-12"
|
||||||
|
style={{ top: `calc(${bannerHeight}px + 4.5rem)` }}
|
||||||
|
>
|
||||||
<ol role="list" className="mt-4 space-y-3 text-sm mb-8">
|
<ol role="list" className="mt-4 space-y-3 text-sm mb-8">
|
||||||
<li key="copy-link">
|
<li key="copy-link">
|
||||||
<button
|
<button
|
||||||
|
|||||||
112
src/components/announcement-banner/AnnouncementBanner.jsx
Normal file
112
src/components/announcement-banner/AnnouncementBanner.jsx
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
import {
|
||||||
|
announcement,
|
||||||
|
useAnnouncements,
|
||||||
|
} from '@/components/announcement-banner/AnnouncementBannerProvider'
|
||||||
|
import { useCustomQueryURL } from '@/hooks/useCustomQueryURL'
|
||||||
|
|
||||||
|
function ArrowRightIcon(props) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 16 16" aria-hidden="true" {...props}>
|
||||||
|
<path
|
||||||
|
d="M4.5 2.5l6 5.5-6 5.5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CloseIcon(props) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 16 16" aria-hidden="true" {...props}>
|
||||||
|
<path
|
||||||
|
d="M4 4l8 8M12 4l-8 8"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AnnouncementBanner() {
|
||||||
|
let { isVisible, close, reportHeight } = useAnnouncements()
|
||||||
|
let announcementLink = useCustomQueryURL(announcement.link || '')
|
||||||
|
let bannerRef = useRef(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isVisible) {
|
||||||
|
reportHeight(0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHeight() {
|
||||||
|
if (bannerRef.current) {
|
||||||
|
reportHeight(bannerRef.current.offsetHeight || 0)
|
||||||
|
} else {
|
||||||
|
reportHeight(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateHeight()
|
||||||
|
window.addEventListener('resize', updateHeight)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', updateHeight)
|
||||||
|
}
|
||||||
|
}, [isVisible, reportHeight])
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={bannerRef}
|
||||||
|
id="announcement-banner"
|
||||||
|
className={clsx(
|
||||||
|
'sticky top-0 z-50 flex w-full items-center justify-center border-b border-zinc-800 bg-netbird/95 px-4 py-1.5 text-[11px] font-medium text-black shadow-sm backdrop-blur'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-start gap-1 pr-8 leading-snug md:flex-row md:items-center">
|
||||||
|
{announcement.tag ? (
|
||||||
|
<div className="mr-2 inline rounded-md bg-black/70 px-2 py-1 text-[9px] font-semibold uppercase tracking-[0.18em] text-white">
|
||||||
|
{announcement.tag}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<span className="mr-2 text-[12px] md:text-[13px]">
|
||||||
|
{announcement.text}
|
||||||
|
</span>
|
||||||
|
{announcement.link ? (
|
||||||
|
<Link
|
||||||
|
href={announcementLink}
|
||||||
|
target={announcement.isExternal ? '_blank' : undefined}
|
||||||
|
className="inline-flex items-center gap-1 text-[12px] text-black underline underline-offset-4"
|
||||||
|
title={announcement.linkAlt}
|
||||||
|
>
|
||||||
|
{announcement.linkText}
|
||||||
|
<ArrowRightIcon className="h-3 w-3" />
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{announcement.closeable ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={close}
|
||||||
|
className="absolute right-2 top-1/2 -translate-y-1/2 rounded-md p-1 text-black transition hover:bg-black/10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-black"
|
||||||
|
aria-label="Dismiss announcement"
|
||||||
|
>
|
||||||
|
<CloseIcon className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
|
||||||
|
import { useLocalStorage } from '@/hooks/useLocalStorage'
|
||||||
|
|
||||||
|
const BANNER_ENABLED = true
|
||||||
|
|
||||||
|
export const announcement = {
|
||||||
|
tag: '',
|
||||||
|
text: 'NetBird v0.60 Released - Native SSH Access',
|
||||||
|
link: 'https://netbird.io/knowledge-hub/native-identity-aware-ssh',
|
||||||
|
linkText: 'Read Release Article',
|
||||||
|
linkAlt: 'Learn more about the NetBird v0.60 release',
|
||||||
|
isExternal: false,
|
||||||
|
closeable: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnnouncementContext = createContext({
|
||||||
|
close: () => {},
|
||||||
|
isVisible: false,
|
||||||
|
bannerHeight: 0,
|
||||||
|
reportHeight: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
export function AnnouncementBannerProvider({ children }) {
|
||||||
|
let [mounted, setMounted] = useState(false)
|
||||||
|
let [closedAnnouncement, setClosedAnnouncement] = useLocalStorage(
|
||||||
|
'netbird-announcement',
|
||||||
|
undefined
|
||||||
|
)
|
||||||
|
let announcementId = announcement.text
|
||||||
|
let [isHiddenByScroll, setIsHiddenByScroll] = useState(false)
|
||||||
|
let [bannerHeight, setBannerHeight] = useState(0)
|
||||||
|
|
||||||
|
let close = () => {
|
||||||
|
setClosedAnnouncement(announcementId)
|
||||||
|
}
|
||||||
|
|
||||||
|
let isActive = useMemo(() => {
|
||||||
|
if (!mounted) return false
|
||||||
|
if (!BANNER_ENABLED) return false
|
||||||
|
return closedAnnouncement !== announcementId
|
||||||
|
}, [announcementId, closedAnnouncement, mounted])
|
||||||
|
|
||||||
|
let isVisible = isActive && !isHiddenByScroll
|
||||||
|
|
||||||
|
let reportHeight = useCallback((height) => {
|
||||||
|
setBannerHeight(height)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true)
|
||||||
|
return () => setMounted(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleScroll() {
|
||||||
|
setIsHiddenByScroll(window.scrollY > 30)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleScroll()
|
||||||
|
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||||
|
return () => window.removeEventListener('scroll', handleScroll)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isVisible && bannerHeight !== 0) {
|
||||||
|
setBannerHeight(0)
|
||||||
|
}
|
||||||
|
}, [bannerHeight, isVisible])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnnouncementContext.Provider
|
||||||
|
value={{ close, isVisible, bannerHeight, reportHeight }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AnnouncementContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAnnouncements() {
|
||||||
|
return useContext(AnnouncementContext)
|
||||||
|
}
|
||||||
30
src/hooks/useCustomQueryURL.js
Normal file
30
src/hooks/useCustomQueryURL.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { useMemo } from 'react'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
|
export function useCustomQueryURL(target = '') {
|
||||||
|
let router = useRouter()
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
if (!target) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
if (target.startsWith('http')) {
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
|
||||||
|
let asPath = router?.asPath ?? ''
|
||||||
|
let queryIndex = asPath.indexOf('?')
|
||||||
|
|
||||||
|
if (queryIndex === -1) {
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentQuery = asPath.slice(queryIndex + 1)
|
||||||
|
if (!currentQuery) {
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
|
||||||
|
let separator = target.includes('?') ? '&' : '?'
|
||||||
|
return `${target}${separator}${currentQuery}`
|
||||||
|
}, [router?.asPath, target])
|
||||||
|
}
|
||||||
51
src/hooks/useLocalStorage.js
Normal file
51
src/hooks/useLocalStorage.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
export function useLocalStorage(key, initialValue) {
|
||||||
|
let [storedValue, setStoredValue] = useState(() => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return initialValue
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let item = window.localStorage.getItem(key)
|
||||||
|
return item ? JSON.parse(item) : initialValue
|
||||||
|
} catch {
|
||||||
|
return initialValue
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let item = window.localStorage.getItem(key)
|
||||||
|
setStoredValue(item ? JSON.parse(item) : initialValue)
|
||||||
|
} catch {
|
||||||
|
setStoredValue(initialValue)
|
||||||
|
}
|
||||||
|
}, [initialValue, key])
|
||||||
|
|
||||||
|
let setValue = useCallback(
|
||||||
|
(value) => {
|
||||||
|
setStoredValue((previousValue) => {
|
||||||
|
let valueToStore =
|
||||||
|
typeof value === 'function' ? value(previousValue) : value
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
if (valueToStore === undefined) {
|
||||||
|
window.localStorage.removeItem(key)
|
||||||
|
} else {
|
||||||
|
window.localStorage.setItem(key, JSON.stringify(valueToStore))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return valueToStore
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[key]
|
||||||
|
)
|
||||||
|
|
||||||
|
return [storedValue, setValue]
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import {slugifyWithCounter} from "@sindresorhus/slugify";
|
|||||||
import {ToastContainer} from "react-toastify";
|
import {ToastContainer} from "react-toastify";
|
||||||
import 'react-toastify/dist/ReactToastify.css';
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
import {dom} from "@fortawesome/fontawesome-svg-core";
|
import {dom} from "@fortawesome/fontawesome-svg-core";
|
||||||
|
import {AnnouncementBannerProvider} from "@/components/announcement-banner/AnnouncementBannerProvider";
|
||||||
|
|
||||||
function onRouteChange() {
|
function onRouteChange() {
|
||||||
useMobileNavigationStore.getState().close()
|
useMobileNavigationStore.getState().close()
|
||||||
@@ -33,11 +34,13 @@ export default function App({ Component, pageProps }) {
|
|||||||
}
|
}
|
||||||
<meta name="description" content={pageProps.description} />
|
<meta name="description" content={pageProps.description} />
|
||||||
</Head>
|
</Head>
|
||||||
|
<AnnouncementBannerProvider>
|
||||||
<MDXProvider components={mdxComponents}>
|
<MDXProvider components={mdxComponents}>
|
||||||
<Layout title={pageProps.title?.toString()} tableOfContents={tableOfContents} {...pageProps}>
|
<Layout title={pageProps.title?.toString()} tableOfContents={tableOfContents} {...pageProps}>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</Layout>
|
</Layout>
|
||||||
</MDXProvider>
|
</MDXProvider>
|
||||||
|
</AnnouncementBannerProvider>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
typography: require('./typography'),
|
typography: require('./typography'),
|
||||||
extend: {
|
extend: {
|
||||||
|
colors: {
|
||||||
|
netbird: '#f68330',
|
||||||
|
},
|
||||||
boxShadow: {
|
boxShadow: {
|
||||||
glow: '0 0 4px rgb(0 0 0 / 0.1)',
|
glow: '0 0 4px rgb(0 0 0 / 0.1)',
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user