-
Notifications
You must be signed in to change notification settings - Fork 36
feat: create announcements banner #617
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| import { ArrowUpRightIcon } from '@heroicons/react/24/outline'; | ||
| import Banner from '@node-core/ui-components/Common/Banner'; | ||
| import { useEffect, useState } from 'preact/hooks'; | ||
|
|
||
| import { STATIC_DATA } from '../../constants.mjs'; | ||
| import { isBannerActive } from '../../utils/banner.mjs'; | ||
|
|
||
| /** @import { BannerEntry, RemoteConfig } from './types.d.ts' */ | ||
|
|
||
| /** | ||
| * Asynchronously fetches and displays announcement banners from the remote config. | ||
| * Global banners are rendered above version-specific ones. | ||
| * Non-blocking: silently ignores fetch/parse failures. | ||
| */ | ||
| export default () => { | ||
| const [banners, setBanners] = useState(/** @type {BannerEntry[]} */ ([])); | ||
|
|
||
| useEffect(() => { | ||
| const { remoteConfig, versionMajor } = STATIC_DATA; | ||
|
|
||
| if (!remoteConfig) { | ||
| return; | ||
| } | ||
|
|
||
| fetch(remoteConfig, { | ||
| signal: AbortSignal.timeout(2500), | ||
| }) | ||
| .then(async res => { | ||
| if (!res.ok) { | ||
| return; | ||
| } | ||
|
|
||
| /** @type {RemoteConfig} */ | ||
| const config = await res.json(); | ||
|
|
||
| const active = []; | ||
|
|
||
| const globalBanner = config.global?.banner; | ||
| if (globalBanner && isBannerActive(globalBanner)) { | ||
| active.push(globalBanner); | ||
| } | ||
|
|
||
| const versionBanner = config[`v${versionMajor}`]?.banner; | ||
| if (versionBanner && isBannerActive(versionBanner)) { | ||
| active.push(versionBanner); | ||
araujogui marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| setBanners(active); | ||
| }) | ||
| .catch(error => { | ||
| console.error(error); | ||
avivkeller marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }); | ||
| }, []); | ||
|
|
||
| if (!banners.length) { | ||
| return null; | ||
| } | ||
|
|
||
| return ( | ||
| <div> | ||
| {banners.map(banner => ( | ||
| <Banner key={banner.link} type={banner.type}> | ||
araujogui marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| {banner.link ? <a href={banner.link}>{banner.text}</a> : banner.text} | ||
|
||
| {banner.link && <ArrowUpRightIcon />} | ||
araujogui marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
araujogui marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| </Banner> | ||
| ))} | ||
| </div> | ||
araujogui marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import type { BannerProps } from '@node-core/ui-components/Common/Banner'; | ||
|
|
||
| export type BannerEntry = { | ||
| startDate?: string; | ||
| endDate?: string; | ||
| text: string; | ||
| link?: string; | ||
| type?: BannerProps['type']; | ||
| }; | ||
|
|
||
| export type RemoteConfig = Record<string, { banner?: BannerEntry } | undefined>; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import assert from 'node:assert/strict'; | ||
| import { describe, it } from 'node:test'; | ||
|
|
||
| import { isBannerActive } from '../banner.mjs'; | ||
|
|
||
| const PAST = new Date(Date.now() - 86_400_000).toISOString(); // yesterday | ||
| const FUTURE = new Date(Date.now() + 86_400_000).toISOString(); // tomorrow | ||
|
|
||
| const banner = (overrides = {}) => ({ | ||
| text: 'Test banner', | ||
| ...overrides, | ||
| }); | ||
|
|
||
| describe('isBannerActive', () => { | ||
| describe('no startDate, no endDate', () => { | ||
| it('is always active', () => { | ||
| assert.equal(isBannerActive(banner()), true); | ||
| }); | ||
| }); | ||
|
|
||
| describe('startDate only', () => { | ||
| it('is active when startDate is in the past', () => { | ||
| assert.equal(isBannerActive(banner({ startDate: PAST })), true); | ||
| }); | ||
|
|
||
| it('is not active when startDate is in the future', () => { | ||
| assert.equal(isBannerActive(banner({ startDate: FUTURE })), false); | ||
| }); | ||
| }); | ||
|
|
||
| describe('endDate only', () => { | ||
| it('is active when endDate is in the future', () => { | ||
| assert.equal(isBannerActive(banner({ endDate: FUTURE })), true); | ||
| }); | ||
|
|
||
| it('is not active when endDate is in the past', () => { | ||
| assert.equal(isBannerActive(banner({ endDate: PAST })), false); | ||
| }); | ||
| }); | ||
|
|
||
| describe('startDate and endDate', () => { | ||
| it('is active when now is within the range', () => { | ||
| assert.equal( | ||
| isBannerActive(banner({ startDate: PAST, endDate: FUTURE })), | ||
| true | ||
| ); | ||
| }); | ||
|
|
||
| it('is not active when now is before the range', () => { | ||
| assert.equal( | ||
| isBannerActive(banner({ startDate: FUTURE, endDate: FUTURE })), | ||
| false | ||
| ); | ||
| }); | ||
|
|
||
| it('is not active when now is after the range', () => { | ||
| assert.equal( | ||
| isBannerActive(banner({ startDate: PAST, endDate: PAST })), | ||
| false | ||
| ); | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| /** @import { BannerEntry } from '../components/AnnouncementBanner/types' */ | ||
|
|
||
| /** | ||
| * Checks whether a banner should be displayed based on its date range. | ||
| * Both `startDate` and `endDate` are optional; if omitted the banner is | ||
| * considered open-ended in that direction. | ||
| * | ||
| * @param {BannerEntry} banner | ||
| * @returns {boolean} | ||
| */ | ||
| export const isBannerActive = banner => { | ||
| const now = Date.now(); | ||
| if (banner.startDate && now < new Date(banner.startDate).getTime()) { | ||
| return false; | ||
| } | ||
| if (banner.endDate && now > new Date(banner.endDate).getTime()) { | ||
| return false; | ||
| } | ||
| return true; | ||
| }; |
Uh oh!
There was an error while loading. Please reload this page.