The Node.js® Website

feat: added renderer for badges (#6213)

* feat: added renderer for badges

* refactor: refactored banner components and some optimizations

* fix: unit test

* fix: types

authored by Claudio W and committed by GitHub c2bb12fd 132032b7

+7 -7
components/Common/Banner/index.stories.tsx
··· 7 7 8 8 export const Default: Story = { 9 9 args: { 10 - text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', 10 + children: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', 11 11 type: 'default', 12 - url: 'https://github.com/openjs-foundation/summit/issues/360', 12 + link: 'https://github.com/openjs-foundation/summit/issues/360', 13 13 }, 14 14 }; 15 15 16 16 export const Error: Story = { 17 17 args: { 18 - text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', 18 + children: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', 19 19 type: 'error', 20 - url: 'https://github.com/nodejs/nodejs.org/issues/4495', 20 + link: 'https://github.com/nodejs/nodejs.org/issues/4495', 21 21 }, 22 22 }; 23 23 24 24 export const Warning: Story = { 25 25 args: { 26 - text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', 26 + children: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', 27 27 type: 'warning', 28 - url: 'https://github.com/nodejs/nodejs.org/issues/4495', 28 + link: 'https://github.com/nodejs/nodejs.org/issues/4495', 29 29 }, 30 30 }; 31 31 32 32 export const NoLink: Story = { 33 33 args: { 34 - text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', 34 + children: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', 35 35 type: 'default', 36 36 }, 37 37 };
+10 -7
components/Common/Banner/index.tsx
··· 1 1 import { ArrowUpRightIcon } from '@heroicons/react/24/outline'; 2 - import type { FC } from 'react'; 2 + import type { FC, PropsWithChildren } from 'react'; 3 3 4 4 import Link from '@/components/Link'; 5 5 6 6 import styles from './index.module.css'; 7 7 8 8 type BannerProps = { 9 - type: 'default' | 'error' | 'warning'; 10 - text: string; 11 - url?: string; 9 + link?: string; 10 + type?: 'default' | 'warning' | 'error'; 12 11 }; 13 12 14 - const Banner: FC<BannerProps> = ({ type, text, url = '' }) => ( 13 + const Banner: FC<PropsWithChildren<BannerProps>> = ({ 14 + type = 'default', 15 + link, 16 + children, 17 + }) => ( 15 18 <div className={`${styles.banner} ${styles[type] || styles.default}`}> 16 - {(url.length > 0 && <Link href={url}>{text}</Link>) || text} 17 - {url.length > 0 && <ArrowUpRightIcon />} 19 + {link ? <Link href={link}>{children}</Link> : children} 20 + {link && <ArrowUpRightIcon />} 18 21 </div> 19 22 ); 20 23
+21
components/withBadge.tsx
··· 1 + import type { FC } from 'react'; 2 + 3 + import Badge from '@/components/Common/Badge'; 4 + import { siteConfig } from '@/next.json.mjs'; 5 + import { dateIsBetween } from '@/util/dateIsBetween'; 6 + 7 + const WithBadge: FC<{ section: string }> = ({ section }) => { 8 + const badge = siteConfig.websiteBadges[section]; 9 + 10 + if (badge && dateIsBetween(badge.startDate, badge.endDate)) { 11 + return ( 12 + <Badge badgeText={badge.title} kind={badge.kind} href={badge.link}> 13 + {badge.text} 14 + </Badge> 15 + ); 16 + } 17 + 18 + return null; 19 + }; 20 + 21 + export default WithBadge;
+21
components/withBanner.tsx
··· 1 + import type { FC } from 'react'; 2 + 3 + import Banner from '@/components/Common/Banner'; 4 + import { siteConfig } from '@/next.json.mjs'; 5 + import { dateIsBetween } from '@/util/dateIsBetween'; 6 + 7 + const WithBanner: FC<{ section: string }> = ({ section }) => { 8 + const banner = siteConfig.websiteBanners[section]; 9 + 10 + if (banner && dateIsBetween(banner.startDate, banner.endDate)) { 11 + return ( 12 + <Banner type={banner.type} link={banner.link}> 13 + {banner.text} 14 + </Banner> 15 + ); 16 + } 17 + 18 + return null; 19 + }; 20 + 21 + export default WithBanner;
+17 -12
components/withNavBar.tsx
··· 5 5 import type { FC } from 'react'; 6 6 7 7 import NavBar from '@/components/Containers/NavBar'; 8 + import WithBanner from '@/components/withBanner'; 8 9 import { useClientContext, useSiteNavigation } from '@/hooks'; 9 10 import { useRouter } from '@/navigation.mjs'; 10 11 import { availableLocales } from '@/next.locales.mjs'; ··· 21 22 setTheme(resolvedTheme === 'dark' ? 'light' : 'dark'); 22 23 23 24 return ( 24 - <NavBar 25 - onThemeTogglerClick={toggleCurrnetTheme} 26 - languages={{ 27 - currentLanguage: locale, 28 - availableLanguages: availableLocales, 29 - onChange: locale => replace(pathname!, { locale: locale.code }), 30 - }} 31 - navItems={navigationItems.map(([, { label, link }]) => ({ 32 - link, 33 - text: label, 34 - }))} 35 - /> 25 + <> 26 + <WithBanner section="index" /> 27 + 28 + <NavBar 29 + onThemeTogglerClick={toggleCurrnetTheme} 30 + languages={{ 31 + currentLanguage: locale, 32 + availableLanguages: availableLocales, 33 + onChange: locale => replace(pathname!, { locale: locale.code }), 34 + }} 35 + navItems={navigationItems.map(([, { label, link }]) => ({ 36 + link, 37 + text: label, 38 + }))} 39 + /> 40 + </> 36 41 ); 37 42 }; 38 43
+2 -8
site.json
··· 25 25 "category": "vulnerability" 26 26 } 27 27 ], 28 - "websiteBanners": { 29 - "index": { 30 - "startDate": "2023-11-26T00:00:00.000Z", 31 - "endDate": "2023-12-05T00:00:00.000Z", 32 - "link": "https://training.linuxfoundation.org/cyber-monday-js-2023/?utm_source=openjsf&utm_medium=homepage-ticker&utm_campaign=cybermonday23", 33 - "html": "<img src='https://i.imgur.com/8FgdVOy.png' alt='Node.js Training Banner' style='margin:0 auto;border-radius:4px;' />" 34 - } 35 - }, 28 + "websiteBanners": {}, 29 + "websiteBadges": {}, 36 30 "footerLinks": [ 37 31 { 38 32 "link": "https://openjsf.org/wp-content/uploads/sites/84/2021/01/OpenJS-Foundation-Trademark-Policy-2021-01-12.docx.pdf",
+2 -1
types/config.ts
··· 1 - import type { RSSFeed, WebsiteBanner } from './features'; 1 + import type { RSSFeed, WebsiteBadge, WebsiteBanner } from './features'; 2 2 3 3 export interface TwitterConfig { 4 4 username: string; ··· 34 34 twitter: TwitterConfig; 35 35 rssFeeds: Array<RSSFeed>; 36 36 websiteBanners: Record<string, WebsiteBanner>; 37 + websiteBadges: Record<string, WebsiteBadge>; 37 38 footerLinks: Array<FooterConfig>; 38 39 socialLinks: Array<SocialConfig>; 39 40 }
+14 -2
types/features.ts
··· 5 5 blogCategory?: string; 6 6 } 7 7 8 - export interface WebsiteBanner { 8 + interface WithRange { 9 9 startDate: string; 10 10 endDate: string; 11 - text?: string; 11 + } 12 + 13 + export interface WebsiteBanner extends WithRange { 14 + text: string; 15 + link?: string; 16 + /** @deprecated the html field is unsupported on the website redesign */ 12 17 html?: string; 18 + type?: 'default' | 'warning' | 'error'; 19 + } 20 + 21 + export interface WebsiteBadge extends WithRange { 22 + text: string; 13 23 link: string; 24 + title?: string; 25 + kind?: 'default' | 'warning' | 'error'; 14 26 }
-11
types/releases.ts
··· 1 - export interface UpcomingReleaseData { 2 - releaseDate: string; 3 - releaseType: 'Current' | 'LTS' | 'Maintenance' | 'End-of-life'; 4 - alreadyReleased: boolean; 5 - } 6 - 7 - export interface UpcomingRelease { 8 - name: string; 9 - releases: UpcomingReleaseData[]; 10 - } 11 - 12 1 export type NodeReleaseStatus = 13 2 | 'Maintenance LTS' 14 3 | 'Active LTS'
+4 -4
util/__tests__/dateIsBetween.test.mjs
··· 19 19 expect(result).toBe(false); 20 20 }); 21 21 22 - it('returns false when either start or end date is invalid', () => { 22 + it('throws when either start or end date is invalid', () => { 23 23 const invalidStartDate = 'Invalid Start Date'; 24 24 const validEndDate = '2024-01-01T00:00:00.000Z'; 25 25 26 - const result = dateIsBetween(invalidStartDate, validEndDate); 27 - 28 - expect(result).toBe(false); 26 + expect(() => dateIsBetween(invalidStartDate, validEndDate)).toThrow( 27 + 'dateIsBetween got called with invalid dates' 28 + ); 29 29 }); 30 30 });
+1 -1
util/dateIsBetween.ts
··· 4 4 const end = new Date(endDate); 5 5 6 6 if ([start.toString(), end.toString()].includes(invalidDateStr)) { 7 - return false; 7 + throw new Error('dateIsBetween got called with invalid dates'); 8 8 } 9 9 10 10 const now = new Date();