mirror of
https://github.com/netbirdio/docs.git
synced 2026-04-16 07:26:35 +00:00
Update announcement (#579)
This commit is contained in:
@@ -41,6 +41,7 @@
|
|||||||
"allof-merge": "^0.6.6",
|
"allof-merge": "^0.6.6",
|
||||||
"autoprefixer": "^10.4.7",
|
"autoprefixer": "^10.4.7",
|
||||||
"clsx": "^1.2.0",
|
"clsx": "^1.2.0",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
"ejs": "^3.1.9",
|
"ejs": "^3.1.9",
|
||||||
"focus-visible": "^5.2.0",
|
"focus-visible": "^5.2.0",
|
||||||
"framer-motion": "10.12.9",
|
"framer-motion": "10.12.9",
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import { useEffect, useRef } from 'react'
|
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
import {
|
import { useAnnouncements } from '@/components/announcement-banner/AnnouncementBannerProvider'
|
||||||
announcement,
|
|
||||||
useAnnouncements,
|
|
||||||
} from '@/components/announcement-banner/AnnouncementBannerProvider'
|
|
||||||
import { useCustomQueryURL } from '@/hooks/useCustomQueryURL'
|
import { useCustomQueryURL } from '@/hooks/useCustomQueryURL'
|
||||||
|
|
||||||
function ArrowRightIcon(props) {
|
function ArrowRightIcon(props) {
|
||||||
@@ -38,39 +34,11 @@ function CloseIcon(props) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AnnouncementBanner() {
|
function AnnouncementItem({ announcement, onClose }) {
|
||||||
let { isVisible, close, reportHeight } = useAnnouncements()
|
const announcementLink = useCustomQueryURL(announcement.link || '')
|
||||||
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={bannerRef}
|
|
||||||
id="announcement-banner"
|
id="announcement-banner"
|
||||||
className={clsx(
|
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'
|
'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'
|
||||||
@@ -100,7 +68,7 @@ export function AnnouncementBanner() {
|
|||||||
{announcement.closeable ? (
|
{announcement.closeable ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={close}
|
onClick={() => onClose(announcement.hash)}
|
||||||
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"
|
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"
|
aria-label="Dismiss announcement"
|
||||||
>
|
>
|
||||||
@@ -110,3 +78,27 @@ export function AnnouncementBanner() {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function AnnouncementBanner() {
|
||||||
|
const { announcements, closeAnnouncement } = useAnnouncements()
|
||||||
|
|
||||||
|
if (!announcements) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const openAnnouncements = announcements.filter((a) => a.isOpen)
|
||||||
|
|
||||||
|
if (openAnnouncements.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the first open announcement
|
||||||
|
const announcement = openAnnouncements[0]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnnouncementItem
|
||||||
|
announcement={announcement}
|
||||||
|
onClose={closeAnnouncement}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,87 +1,122 @@
|
|||||||
|
import md5 from 'crypto-js/md5'
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
useCallback,
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
|
|
||||||
import { useLocalStorage } from '@/hooks/useLocalStorage'
|
const ANNOUNCEMENTS_URL =
|
||||||
|
'https://raw.githubusercontent.com/netbirdio/dashboard/main/announcements.json'
|
||||||
const BANNER_ENABLED = true
|
const STORAGE_KEY = 'netbird-announcements'
|
||||||
|
const CACHE_DURATION_MS = 30 * 60 * 1000
|
||||||
export const announcement = {
|
const BANNER_HEIGHT = 33
|
||||||
tag: 'New',
|
|
||||||
text: 'Simplified IdP Integration',
|
|
||||||
link: '/selfhosted/identity-providers',
|
|
||||||
linkText: 'Learn More',
|
|
||||||
linkAlt: 'Learn more about the embedded Identity Provider powered by DEX for self-hosted installations',
|
|
||||||
isExternal: false,
|
|
||||||
closeable: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
const AnnouncementContext = createContext({
|
const AnnouncementContext = createContext({
|
||||||
close: () => {},
|
|
||||||
isVisible: false,
|
|
||||||
bannerHeight: 0,
|
bannerHeight: 0,
|
||||||
reportHeight: () => {},
|
announcements: undefined,
|
||||||
|
closeAnnouncement: () => {},
|
||||||
})
|
})
|
||||||
|
|
||||||
export function AnnouncementBannerProvider({ children }) {
|
const getAnnouncements = async () => {
|
||||||
let [mounted, setMounted] = useState(false)
|
try {
|
||||||
let [closedAnnouncement, setClosedAnnouncement] = useLocalStorage(
|
let stored = null
|
||||||
'netbird-announcement',
|
try {
|
||||||
undefined
|
const data = localStorage.getItem(STORAGE_KEY)
|
||||||
)
|
stored = data ? JSON.parse(data) : null
|
||||||
let announcementId = announcement.text
|
} catch {}
|
||||||
let [bannerHeight, setBannerHeight] = useState(0)
|
|
||||||
|
|
||||||
let close = () => {
|
const now = Date.now()
|
||||||
setClosedAnnouncement(announcementId)
|
|
||||||
}
|
|
||||||
|
|
||||||
let isActive = useMemo(() => {
|
let raw
|
||||||
if (!mounted) return false
|
|
||||||
if (!BANNER_ENABLED) return false
|
|
||||||
return closedAnnouncement !== announcementId
|
|
||||||
}, [announcementId, closedAnnouncement, mounted])
|
|
||||||
|
|
||||||
let isVisible = isActive // Always visible when active, regardless of scroll
|
if (stored && now - stored.timestamp < CACHE_DURATION_MS) {
|
||||||
|
raw = stored.announcements
|
||||||
|
} else {
|
||||||
|
const response = await fetch(ANNOUNCEMENTS_URL)
|
||||||
|
if (!response.ok) return []
|
||||||
|
|
||||||
let reportHeight = useCallback((height) => {
|
raw = await response.json()
|
||||||
setBannerHeight(height)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setMounted(true)
|
|
||||||
return () => setMounted(false)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Removed scroll-based hiding to make banner always sticky
|
|
||||||
// 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])
|
|
||||||
|
// Filter announcements - show all for docs site (not cloud-specific)
|
||||||
|
const filtered = raw.filter((a) => !a.isCloudOnly)
|
||||||
|
const hashes = new Set(filtered.map((a) => md5(a.text).toString()))
|
||||||
|
const closed = (stored?.closedAnnouncements ?? []).filter((h) =>
|
||||||
|
hashes.has(h)
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem(
|
||||||
|
STORAGE_KEY,
|
||||||
|
JSON.stringify({
|
||||||
|
timestamp: now,
|
||||||
|
announcements: raw,
|
||||||
|
closedAnnouncements: closed,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return filtered.map((a) => {
|
||||||
|
const hash = md5(a.text).toString()
|
||||||
|
return { ...a, hash, isOpen: !closed.includes(hash) }
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveAnnouncements = (closedAnnouncements) => {
|
||||||
|
try {
|
||||||
|
const data = localStorage.getItem(STORAGE_KEY)
|
||||||
|
const stored = data ? JSON.parse(data) : null
|
||||||
|
if (stored) {
|
||||||
|
localStorage.setItem(
|
||||||
|
STORAGE_KEY,
|
||||||
|
JSON.stringify({ ...stored, closedAnnouncements })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AnnouncementBannerProvider({ children }) {
|
||||||
|
const [announcements, setAnnouncements] = useState(undefined)
|
||||||
|
const fetchingRef = useRef(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (announcements !== undefined || fetchingRef.current) return
|
||||||
|
fetchingRef.current = true
|
||||||
|
getAnnouncements()
|
||||||
|
.then((a) => setAnnouncements(a))
|
||||||
|
.finally(() => (fetchingRef.current = false))
|
||||||
|
}, [announcements])
|
||||||
|
|
||||||
|
const closeAnnouncement = useCallback(
|
||||||
|
(hash) => {
|
||||||
|
if (!announcements) return
|
||||||
|
const updated = announcements.map((a) =>
|
||||||
|
a.hash === hash ? { ...a, isOpen: false } : a
|
||||||
|
)
|
||||||
|
const closedAnnouncements = updated
|
||||||
|
.filter((a) => !a.isOpen)
|
||||||
|
.map((a) => a.hash)
|
||||||
|
saveAnnouncements(closedAnnouncements)
|
||||||
|
setAnnouncements(updated)
|
||||||
|
},
|
||||||
|
[announcements]
|
||||||
|
)
|
||||||
|
|
||||||
|
const bannerHeight = announcements?.some((a) => a.isOpen) ? BANNER_HEIGHT : 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnnouncementContext.Provider
|
<AnnouncementContext.Provider
|
||||||
value={{ close, isVisible, bannerHeight, reportHeight }}
|
value={{
|
||||||
|
bannerHeight,
|
||||||
|
announcements,
|
||||||
|
closeAnnouncement,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</AnnouncementContext.Provider>
|
</AnnouncementContext.Provider>
|
||||||
@@ -90,4 +125,4 @@ export function AnnouncementBannerProvider({ children }) {
|
|||||||
|
|
||||||
export function useAnnouncements() {
|
export function useAnnouncements() {
|
||||||
return useContext(AnnouncementContext)
|
return useContext(AnnouncementContext)
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user