Update announcement (#579)

This commit is contained in:
Eduard Gert
2026-01-27 11:56:23 +01:00
committed by GitHub
parent 2ccf05149a
commit 4701c0f954
3 changed files with 128 additions and 100 deletions

View File

@@ -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",

View File

@@ -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}
/>
)
}

View File

@@ -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)
} }