The Node.js® Website

feat: events/calendar page and a few minor fixes on components related to time, calendars, etc (#6266)

* chore: refactor a few utils

* chore: add calendar redirect

* chore: updated constants and i18n

* chore: add failsafe for blog data parsing

* chore: added gcal types

* feat: updated components for Time and new Calendar/Event components

* feat: updated layouts

* chore: home page changes requested by TSC

* chore: explain about api key

* fix: fixed events page

* chore: updated array type usage

* chore: minor fixes

* chore: updated text

* chore: storybook is pain

* chore: make utc small

* Apply suggestions from code review

Co-authored-by: Brian Muenzenmeyer <brian.muenzenmeyer@gmail.com>
Signed-off-by: Claudio W <cwunder@gnome.org>

* chore: code-review changes

* chore: center content on extra large screens

---------

Signed-off-by: Claudio W <cwunder@gnome.org>
Co-authored-by: Brian Muenzenmeyer <brian.muenzenmeyer@gmail.com>

authored by Claudio W Brian Muenzenmeyer and committed by GitHub 69753c85 c6cb4179

+13 -1
.storybook/main.ts
··· 40 40 // `nodevu` is a Node.js-specific package that requires Node.js modules 41 41 // this is incompatible with Storybook. So we just mock the module 42 42 resolve: { ...config.resolve, alias: { '@nodevu/core': false } }, 43 + // We need to configure `node:` APIs as Externals to WebPack 44 + // since essentially they're not supported on the browser 45 + externals: { 46 + 'node:fs': 'commonjs fs', 47 + 'node:url': 'commonjs url', 48 + 'node:path': 'commonjs path', 49 + 'node:readline': 'commonjs readline', 50 + }, 43 51 // Removes Pesky Critical Dependency Warnings due to `next/font` 44 - ignoreWarnings: [e => e.message.includes('Critical dep')], 52 + ignoreWarnings: [ 53 + e => 54 + e.message.includes('Critical dep') || 55 + e.message.includes('was not found in'), 56 + ], 45 57 }), 46 58 }; 47 59
+5 -1
.storybook/preview.tsx
··· 18 18 }, 19 19 decorators: [ 20 20 Story => ( 21 - <NextIntlClientProvider locale="en" messages={englishLocale}> 21 + <NextIntlClientProvider 22 + locale="en" 23 + timeZone="Etc/UTC" 24 + messages={englishLocale} 25 + > 22 26 <NotificationProvider viewportClassName="absolute top-0 left-0 list-none"> 23 27 <Story /> 24 28 </NotificationProvider>
+2 -1
components/Common/BlogPostCard/index.module.css
··· 1 1 .container { 2 - @apply max-w-full; 2 + @apply max-w-full 3 + flex-1; 3 4 } 4 5 5 6 .subtitle {
+8 -11
components/Common/BlogPostCard/index.tsx
··· 2 2 import type { FC } from 'react'; 3 3 4 4 import AvatarGroup from '@/components/Common/AvatarGroup'; 5 + import FormattedTime from '@/components/Common/FormattedTime'; 5 6 import Preview from '@/components/Common/Preview'; 6 - import { Time } from '@/components/Common/Time'; 7 7 import Link from '@/components/Link'; 8 8 import { mapBlogCategoryToPreviewType } from '@/util/blogUtils'; 9 9 ··· 16 16 title: string; 17 17 category: string; 18 18 description?: string; 19 - authors: Array<Author>; 20 - date: Date; 21 - slug: string; 19 + authors?: Array<Author>; 20 + date?: Date; 21 + slug?: string; 22 22 }; 23 23 24 24 const BlogPostCard: FC<BlogPostCardProps> = ({ ··· 26 26 slug, 27 27 category, 28 28 description, 29 - authors, 29 + authors = [], 30 30 date, 31 31 }) => { 32 32 const t = useTranslations(); ··· 52 52 {description && <p className={styles.description}>{description}</p>} 53 53 54 54 <footer className={styles.footer}> 55 - <AvatarGroup avatars={avatars} /> 55 + <AvatarGroup avatars={avatars ?? []} /> 56 56 57 57 <div className={styles.author}> 58 - <p>{avatars.map(avatar => avatar.alt).join(', ')}</p> 58 + {avatars && <p>{avatars.map(({ alt }) => alt).join(', ')}</p>} 59 59 60 - <Time 61 - date={date} 62 - format={{ day: 'numeric', month: 'short', year: 'numeric' }} 63 - /> 60 + {date && <FormattedTime date={date} />} 64 61 </div> 65 62 </footer> 66 63 </article>
+24
components/Common/FormattedTime.tsx
··· 1 + import type { DateTimeFormatOptions } from 'next-intl'; 2 + import { useFormatter } from 'next-intl'; 3 + import type { FC } from 'react'; 4 + 5 + import { DEFAULT_DATE_FORMAT } from '@/next.calendar.constants.mjs'; 6 + 7 + type FormattedTimeProps = { 8 + date: string | Date; 9 + format?: DateTimeFormatOptions; 10 + }; 11 + 12 + const FormattedTime: FC<FormattedTimeProps> = ({ date, format }) => { 13 + const formatter = useFormatter(); 14 + 15 + const dateObject = new Date(date); 16 + 17 + return ( 18 + <time dateTime={dateObject.toISOString()}> 19 + {formatter.dateTime(dateObject, format ?? DEFAULT_DATE_FORMAT)} 20 + </time> 21 + ); 22 + }; 23 + 24 + export default FormattedTime;
components/Common/PrevNextArrow/index.tsx components/Common/PrevNextArrow.tsx
-18
components/Common/Time/index.tsx
··· 1 - import type { DateTimeFormatOptions } from 'next-intl'; 2 - import { useFormatter } from 'next-intl'; 3 - import type { FC } from 'react'; 4 - import { useMemo } from 'react'; 5 - 6 - type TimeProps = { date: string | Date; format: DateTimeFormatOptions }; 7 - 8 - export const Time: FC<TimeProps> = ({ date, format }) => { 9 - const formatter = useFormatter(); 10 - 11 - const dateObject = useMemo(() => new Date(date), [date]); 12 - 13 - return ( 14 - <time dateTime={dateObject.toISOString()}> 15 - {formatter.dateTime(dateObject, format)} 16 - </time> 17 - ); 18 - };
+21
components/MDX/Calendar/Event/index.module.css
··· 1 + .event { 2 + @apply flex 3 + w-fit 4 + flex-col 5 + gap-1; 6 + 7 + .title { 8 + @apply flex 9 + flex-row 10 + gap-2; 11 + 12 + span { 13 + @apply text-sm 14 + font-bold; 15 + } 16 + } 17 + 18 + a { 19 + @apply text-sm; 20 + } 21 + }
+44
components/MDX/Calendar/Event/index.tsx
··· 1 + import type { FC } from 'react'; 2 + 3 + import FormattedTime from '@/components/Common/FormattedTime'; 4 + import Link from '@/components/Link'; 5 + import { getZoomLink, isZoned } from '@/components/MDX/Calendar/utils'; 6 + import type { CalendarEvent } from '@/types'; 7 + 8 + import styles from './index.module.css'; 9 + 10 + type EventProps = Pick< 11 + CalendarEvent, 12 + 'start' | 'end' | 'summary' | 'location' | 'description' 13 + >; 14 + 15 + const Event: FC<EventProps> = ({ 16 + start, 17 + end, 18 + description, 19 + summary, 20 + location, 21 + }) => ( 22 + <div className={styles.event}> 23 + <div className={styles.title}> 24 + <span> 25 + <FormattedTime 26 + date={isZoned(start) ? start.dateTime : start.date} 27 + format={{ hour: 'numeric', minute: 'numeric' }} 28 + /> 29 + </span> 30 + <span>-</span> 31 + <span> 32 + <FormattedTime 33 + date={isZoned(end) ? end.dateTime : end.date} 34 + format={{ hour: 'numeric', minute: 'numeric' }} 35 + /> 36 + </span> 37 + <small>(UTC)</small> 38 + </div> 39 + 40 + <Link href={getZoomLink({ description, location })}>{summary}</Link> 41 + </div> 42 + ); 43 + 44 + export default Event;
+54
components/MDX/Calendar/UpcomingEvents.tsx
··· 1 + import type { FC } from 'react'; 2 + 3 + import FormattedTime from '@/components/Common/FormattedTime'; 4 + import Event from '@/components/MDX/Calendar/Event'; 5 + import { getZoomLink, isZoned } from '@/components/MDX/Calendar/utils'; 6 + import { CALENDAR_NODEJS_ID } from '@/next.calendar.constants.mjs'; 7 + import { getCalendarEvents } from '@/next.calendar.mjs'; 8 + import type { CalendarEvent } from '@/types'; 9 + 10 + import styles from './calendar.module.css'; 11 + 12 + type GrouppedEntries = Record<string, Array<CalendarEvent>>; 13 + 14 + const UpcomingEvents: FC = async () => { 15 + const events = await getCalendarEvents(CALENDAR_NODEJS_ID); 16 + 17 + const groupedEntries = events.filter(getZoomLink).reduce((acc, event) => { 18 + const startDate = new Date( 19 + isZoned(event.start) ? event.start.dateTime : event.start.date 20 + ); 21 + 22 + const datePerDay = startDate.toDateString(); 23 + 24 + acc[datePerDay] = acc[datePerDay] || []; 25 + acc[datePerDay].push(event); 26 + 27 + return acc; 28 + }, {} as GrouppedEntries); 29 + 30 + const sortedGroupedEntries = Object.entries(groupedEntries).sort( 31 + ([dateA], [dateB]) => new Date(dateA).getTime() - new Date(dateB).getTime() 32 + ); 33 + 34 + return sortedGroupedEntries.map(([date, entries]) => ( 35 + <div key={date} className={styles.events}> 36 + <h4> 37 + <FormattedTime date={date} format={{ day: 'numeric', month: 'long' }} /> 38 + </h4> 39 + 40 + {entries.map(({ id, start, end, summary, location, description }) => ( 41 + <Event 42 + key={id} 43 + start={start} 44 + end={end} 45 + summary={summary} 46 + location={location} 47 + description={description} 48 + /> 49 + ))} 50 + </div> 51 + )); 52 + }; 53 + 54 + export default UpcomingEvents;
+38
components/MDX/Calendar/UpcomingSummits.tsx
··· 1 + import { getTranslations } from 'next-intl/server'; 2 + import type { FC } from 'react'; 3 + 4 + import BlogPostCard from '@/components/Common/BlogPostCard'; 5 + import getBlogData from '@/next-data/blogData'; 6 + 7 + import styles from './calendar.module.css'; 8 + 9 + const UpcomingSummits: FC = async () => { 10 + const t = await getTranslations(); 11 + const { posts } = await getBlogData('events', 0); 12 + 13 + const currentDate = new Date(); 14 + const filteredPosts = posts.filter(post => post.date >= currentDate); 15 + 16 + const fallbackPosts = Array(2).fill({ 17 + title: t('components.mdx.upcomingEvents.defaultTitle'), 18 + categories: ['events'], 19 + }); 20 + 21 + const mappedPosts = fallbackPosts.map((post, key) => { 22 + const actualPost = filteredPosts[key] || post; 23 + 24 + return ( 25 + <BlogPostCard 26 + key={actualPost.slug || key} 27 + title={actualPost.title} 28 + category={actualPost.categories[0]} 29 + date={actualPost.date} 30 + slug={actualPost.slug} 31 + /> 32 + ); 33 + }); 34 + 35 + return <div className={styles.summits}>{mappedPosts}</div>; 36 + }; 37 + 38 + export default UpcomingSummits;
+17
components/MDX/Calendar/calendar.module.css
··· 1 + .events { 2 + @apply flex 3 + flex-col 4 + gap-2; 5 + 6 + h4 { 7 + @apply text-xl 8 + font-bold; 9 + } 10 + } 11 + 12 + .summits { 13 + @apply flex 14 + flex-col 15 + gap-3 16 + md:flex-row; 17 + }
+10
components/MDX/Calendar/utils.ts
··· 1 + import type { CalendarEvent, ZonedCalendarTime } from '@/types'; 2 + 3 + export const isZoned = (d: object): d is ZonedCalendarTime => 4 + 'dateTime' in d && 'timeZone' in d; 5 + 6 + export const getZoomLink = ( 7 + event: Pick<CalendarEvent, 'description' | 'location'> 8 + ) => 9 + event.description?.match(/https:\/\/zoom.us\/j\/\d+/)?.[0] || 10 + event.location?.match(/https:\/\/zoom.us\/j\/\d+/)?.[0];
+1 -1
components/withBadge.tsx
··· 2 2 3 3 import Badge from '@/components/Common/Badge'; 4 4 import { siteConfig } from '@/next.json.mjs'; 5 - import { dateIsBetween } from '@/util/dateIsBetween'; 5 + import { dateIsBetween } from '@/util/dateUtils'; 6 6 7 7 const WithBadge: FC<{ section: string }> = ({ section }) => { 8 8 const badge = siteConfig.websiteBadges[section];
+1 -1
components/withBanner.tsx
··· 2 2 3 3 import Banner from '@/components/Common/Banner'; 4 4 import { siteConfig } from '@/next.json.mjs'; 5 - import { dateIsBetween } from '@/util/dateIsBetween'; 5 + import { dateIsBetween } from '@/util/dateUtils'; 6 6 7 7 const WithBanner: FC<{ section: string }> = ({ section }) => { 8 8 const banner = siteConfig.websiteBanners[section];
+2 -7
components/withMetaBar.tsx
··· 5 5 import GitHub from '@/components/Icons/Social/GitHub'; 6 6 import Link from '@/components/Link'; 7 7 import { useClientContext } from '@/hooks/server'; 8 + import { DEFAULT_DATE_FORMAT } from '@/next.calendar.constants.mjs'; 8 9 import { getGitHubBlobUrl } from '@/util/gitHubUtils'; 9 10 10 - const DATE_FORMAT = { 11 - month: 'short', 12 - day: '2-digit', 13 - year: 'numeric', 14 - } as const; 15 - 16 11 const WithMetaBar: FC = () => { 17 12 const { headings, readingTime, frontmatter, filename } = useClientContext(); 18 13 const formatter = useFormatter(); 19 14 20 15 const lastUpdated = frontmatter.date 21 - ? formatter.dateTime(new Date(frontmatter.date), DATE_FORMAT) 16 + ? formatter.dateTime(new Date(frontmatter.date), DEFAULT_DATE_FORMAT) 22 17 : undefined; 23 18 24 19 return (
+9 -2
i18n/locales/en.json
··· 85 85 "about": { 86 86 "links": { 87 87 "about": "About Node.js", 88 + "aboutSide": "About Node.js®", 88 89 "governance": "Project Governance", 89 90 "releases": "Previous Releases", 90 91 "security": "Security Reporting" ··· 93 94 "getInvolved": { 94 95 "links": { 95 96 "getInvolved": "Get Involved", 96 - "collabSummit": "Collab Summit", 97 - "contribute": "Contribute", 97 + "collabSummit": "Collaborator Summit", 98 + "upcomingEvents": "Upcoming Events", 99 + "contribute": "Contribute to Node.js", 98 100 "codeOfConduct": "Code of Conduct" 99 101 } 100 102 } ··· 149 151 }, 150 152 "languageDropdown": { 151 153 "label": "Choose Language" 154 + } 155 + }, 156 + "mdx": { 157 + "upcomingEvents": { 158 + "defaultTitle": "No Upcoming Event" 152 159 } 153 160 }, 154 161 "metabar": {
+3 -5
layouts/BlogCategoryLayout.tsx
··· 2 2 import type { FC } from 'react'; 3 3 4 4 import { getClientContext } from '@/client-context'; 5 - import { Time } from '@/components/Common/Time'; 5 + import FormattedTime from '@/components/Common/FormattedTime'; 6 6 import Link from '@/components/Link'; 7 7 import Pagination from '@/components/Pagination'; 8 8 import getBlogData from '@/next-data/blogData'; ··· 41 41 <ul className="blog-index"> 42 42 {posts.map(({ slug, date, title }) => ( 43 43 <li key={slug}> 44 - <Time 45 - date={date} 46 - format={{ year: 'numeric', month: 'short', day: '2-digit' }} 47 - /> 44 + <FormattedTime date={date} /> 45 + 48 46 <Link href={slug}>{title}</Link> 49 47 </li> 50 48 ))}
+2 -5
layouts/BlogPostLayout.tsx
··· 1 1 import { useTranslations } from 'next-intl'; 2 2 import type { FC, PropsWithChildren } from 'react'; 3 3 4 - import { Time } from '@/components/Common/Time'; 4 + import FormattedTime from '@/components/Common/FormattedTime'; 5 5 import { useClientContext } from '@/hooks/server'; 6 6 7 7 const BlogPostLayout: FC<PropsWithChildren> = ({ children }) => { ··· 19 19 <span className="blogpost-meta"> 20 20 {t('layouts.blogPost.author.byLine', { author: author || null })} 21 21 22 - <Time 23 - date={date} 24 - format={{ month: 'short', day: '2-digit', year: 'numeric' }} 25 - /> 22 + <FormattedTime date={date} /> 26 23 </span> 27 24 </div> 28 25
+1
layouts/New/Blog.tsx
··· 52 52 'announcements', 53 53 'release', 54 54 'vulnerability', 55 + 'events', 55 56 ])} 56 57 /> 57 58 </main>
+10 -1
layouts/New/layouts.module.css
··· 106 106 text-neutral-800 107 107 dark:text-neutral-400 108 108 xs:text-xs; 109 + 110 + sup { 111 + @apply cursor-help; 112 + } 109 113 } 110 114 } 111 115 } ··· 161 165 } 162 166 163 167 .contentLayout { 164 - @apply grid 168 + @apply mx-auto 169 + grid 165 170 w-full 166 171 max-w-8xl 167 172 grid-rows-[1fr] ··· 174 179 @apply flex 175 180 w-full 176 181 justify-center 182 + border-l 183 + border-l-neutral-200 177 184 bg-gradient-subtle 178 185 px-4 179 186 py-14 187 + dark:border-l-neutral-900 180 188 dark:bg-gradient-subtle-dark 181 189 md:px-14 182 190 lg:px-28 191 + xs:border-l-0 183 192 xs:bg-none 184 193 xs:pb-4 185 194 xs:dark:bg-none;
+5 -1
navigation.json
··· 35 35 "items": { 36 36 "about": { 37 37 "link": "/about", 38 - "label": "components.navigation.about.links.about" 38 + "label": "components.navigation.about.links.aboutSide" 39 39 }, 40 40 "governance": { 41 41 "link": "/about/governance", ··· 61 61 "collabSummit": { 62 62 "link": "/about/get-involved/collab-summit", 63 63 "label": "components.navigation.getInvolved.links.collabSummit" 64 + }, 65 + "upcomingEvents": { 66 + "link": "/about/get-involved/events", 67 + "label": "components.navigation.getInvolved.links.upcomingEvents" 64 68 }, 65 69 "contribute": { 66 70 "link": "/about/get-involved/contribute",
+6 -1
next-data/blogData.ts
··· 6 6 } from '@/next.constants.mjs'; 7 7 import type { BlogPostsRSC } from '@/types'; 8 8 9 + // Prevents React from throwing an Error when not able to fulfil a request 10 + // due to missing category or internal processing errors 11 + const parseBlogDataResponse = (data: string): BlogPostsRSC => 12 + data.startsWith('{') ? JSON.parse(data) : { posts: [], pagination: {} }; 13 + 9 14 const getBlogData = (cat: string, page?: number): Promise<BlogPostsRSC> => { 10 15 // When we're using Static Exports the Next.js Server is not running (during build-time) 11 16 // hence the self-ingestion APIs will not be available. In this case we want to load ··· 26 31 // When we're on RSC with Server capabilities we prefer using Next.js Data Fetching 27 32 // as this will load cached data from the server instead of generating data on the fly 28 33 // this is extremely useful for ISR and SSG as it will not generate this data on every request 29 - return fetch(fetchURL).then(r => r.json()); 34 + return fetch(fetchURL).then(r => r.text().then(parseBlogDataResponse)); 30 35 }; 31 36 32 37 export default getBlogData;
+1 -1
next-data/generators/releaseData.mjs
··· 29 29 * This method is used to generate the Node.js Release Data 30 30 * for self-consumption during RSC and Static Builds 31 31 * 32 - * @returns {Promise<import('../../types').NodeRelease[]>} 32 + * @returns {Promise<Array<import('../../types').NodeRelease>>} 33 33 */ 34 34 const generateReleaseData = () => { 35 35 return nodevu({ fetch: fetch }).then(nodevuOutput => {
+1 -1
next-data/generators/websiteFeeds.mjs
··· 19 19 /** 20 20 * This generates all the Website RSS Feeds that are used for the website 21 21 * 22 - * @type {[string, Feed][]} 22 + * @type {Array<[string, Feed]>} 23 23 */ 24 24 const websiteFeeds = siteConfig.rssFeeds.map( 25 25 ({ category, title, description, file }) => {
+38
next.calendar.constants.mjs
··· 1 + 'use strict'; 2 + 3 + /** 4 + * This is used for Node.js Calendar and any other Google Calendar that we might want to load within the Website 5 + * 6 + * Note that this is a custom Environment Variable that can be defined by us when necessary 7 + */ 8 + export const BASE_CALENDAR_URL = 9 + process.env.NEXT_PUBLIC_CALENDAR_URL || 10 + `https://clients6.google.com/calendar/v3/calendars/`; 11 + 12 + /** 13 + * This is a shared (public) Google Calendar Key (accessible on the Web) for accessing Google's Public Calendar API 14 + * 15 + * This is a PUBLIC available API Key and not a Secret; It's exposed by Google on their Calendar API Docs 16 + * 17 + * Note that this is a custom Environment Variable that can be defined by us when necessary 18 + */ 19 + export const SHARED_CALENDAR_KEY = 20 + process.env.NEXT_PUBLIC_SHARED_CALENDAR_KEY || 21 + 'AIzaSyBNlYH01_9Hc5S1J9vuFmu2nUqBZJNAXxs'; 22 + 23 + /** 24 + * This is Node.js's Public Google Calendar ID used for all public entries from Node.js Calendar 25 + */ 26 + export const CALENDAR_NODEJS_ID = 27 + 'nodejs.org_nr77ama8p7d7f9ajrpnu506c98@group.calendar.google.com'; 28 + 29 + /** 30 + * Default Date format for Calendars and Time Components 31 + * 32 + * @type {import('next-intl').DateTimeFormatOptions} 33 + */ 34 + export const DEFAULT_DATE_FORMAT = { 35 + year: 'numeric', 36 + month: 'short', 37 + day: '2-digit', 38 + };
+39
next.calendar.mjs
··· 1 + 'use strict'; 2 + 3 + import { 4 + BASE_CALENDAR_URL, 5 + SHARED_CALENDAR_KEY, 6 + } from './next.calendar.constants.mjs'; 7 + 8 + /** 9 + * 10 + * @param {string} calendarId 11 + * @param {number} maxResults 12 + * @returns {Promise<Array<import('./types').CalendarEvent>>} 13 + */ 14 + export const getCalendarEvents = async (calendarId = '', maxResults = 20) => { 15 + const currentDate = new Date(); 16 + const nextWeekDate = new Date(); 17 + 18 + nextWeekDate.setDate(currentDate.getDate() + 7); 19 + 20 + const calendarQueryParams = new URLSearchParams({ 21 + calendarId, 22 + maxResults, 23 + singleEvents: true, 24 + timeZone: 'Etc/Utc', 25 + key: SHARED_CALENDAR_KEY, 26 + timeMax: nextWeekDate.toISOString(), 27 + timeMin: currentDate.toISOString(), 28 + }); 29 + 30 + const calendarQueryUrl = new URL(`${BASE_CALENDAR_URL}${calendarId}/events`); 31 + 32 + calendarQueryParams.forEach((value, key) => 33 + calendarQueryUrl.searchParams.append(key, value) 34 + ); 35 + 36 + return fetch(calendarQueryUrl.toString()) 37 + .then(response => response.json()) 38 + .then(calendar => calendar.items); 39 + };
+1 -1
next.dynamic.constants.mjs
··· 12 12 * This is a list of all static routes or pages from the Website that we do not 13 13 * want to allow to be statically built on our Static Export Build. 14 14 * 15 - * @type {((route: import('./types').RouteSegment) => boolean)[]} A list of Ignored Routes by Regular Expressions 15 + * @type {Array<((route: import('./types').RouteSegment) => boolean)>} A list of Ignored Routes by Regular Expressions 16 16 */ 17 17 export const IGNORED_ROUTES = [ 18 18 // This is used to ignore all blog routes except for the English language
+1 -1
next.dynamic.mjs
··· 79 79 * This method returns a list of all routes that exist for a given locale 80 80 * 81 81 * @param {string} locale 82 - * @returns {string[]} 82 + * @returns {Array<string>} 83 83 */ 84 84 const getRoutesByLanguage = async (locale = defaultLocale.code) => { 85 85 const shouldIgnoreStaticRoute = pathname =>
+3 -3
next.helpers.mjs
··· 21 21 * 22 22 * @param {string} root the root directory to search from 23 23 * @param {string} cwd the current working directory 24 - * @returns {Promise<string[]>} a promise containing an array of directories 24 + * @returns {Promise<Array<string>>} a promise containing an array of directories 25 25 */ 26 26 export const getDirectories = async (root, cwd) => { 27 27 return glob('*', { root, cwd, withFileTypes: true }) ··· 46 46 * 47 47 * @param {string} root the root directory to search from 48 48 * @param {string} cwd the given locale code 49 - * @param {string[]} ignore an array of glob patterns to ignore 50 - * @returns {Promise<string[]>} a promise containing an array of paths 49 + * @param {Array<string>} ignore an array of glob patterns to ignore 50 + * @returns {Promise<Array<string>>} a promise containing an array of paths 51 51 */ 52 52 export const getMarkdownFiles = async (root, cwd, ignore = []) => { 53 53 const cacheKey = `${root}${cwd}${ignore.join('')}`;
+1 -1
next.json.mjs
··· 7 7 /** @type {import('./types').SiteNavigation} */ 8 8 export const siteNavigation = _siteNavigation; 9 9 10 - /** @type {Record<string, import('./types').Redirect[]>} */ 10 + /** @type {Record<string, Array<import('./types').Redirect>>} */ 11 11 export const siteRedirects = _siteRedirects; 12 12 13 13 /** @type {import('./types').SiteConfig} */
+1 -2
next.mdx.compiler.mjs
··· 18 18 * @param {'md' | 'mdx'} fileExtension 19 19 * @returns {Promise<{ 20 20 * MDXContent: import('mdx/types').MDXContent; 21 - * headings: import('@vcarl/remark-headings').Heading[]; 21 + * headings: Array<import('@vcarl/remark-headings').Heading>; 22 22 * frontmatter: Record<string, any>; 23 23 * readingTime: import('reading-time').ReadTimeResults; 24 24 * }>} ··· 36 36 remarkPlugins: NEXT_REMARK_PLUGINS, 37 37 format: fileExtension, 38 38 baseUrl: import.meta.url, 39 - jsxRuntime: 'automatic', 40 39 ...reactRuntime, 41 40 }); 42 41
+2 -2
next.mdx.mjs
··· 11 11 /** 12 12 * Provides all our Rehype Plugins that are used within MDX 13 13 * 14 - * @type {import('unified').Plugin[]} 14 + * @type {Array<import('unified').Plugin>} 15 15 */ 16 16 export const NEXT_REHYPE_PLUGINS = [ 17 17 // Generates `id` attributes for headings (H1, ...) ··· 29 29 /** 30 30 * Provides all our Remark Plugins that are used within MDX 31 31 * 32 - * @type {import('unified').Plugin[]} 32 + * @type {Array<import('unified').Plugin>} 33 33 */ 34 34 export const NEXT_REMARK_PLUGINS = [remarkGfm, remarkHeadings, readingTime];
+2 -2
next.mdx.shiki.mjs
··· 37 37 /** 38 38 * @typedef {import('unist').Node} Node 39 39 * @property {string} tagName 40 - * @property {Node[]} children 40 + * @property {Array<import('unist').Node>} children 41 41 */ 42 42 43 43 /** 44 44 * Checks if the given node is a valid code element. 45 45 * 46 - * @param {Node} node - The node to be verified. 46 + * @param {import('unist').Node} node - The node to be verified. 47 47 * 48 48 * @return {boolean} - True when it is a valid code element, false otherwise. 49 49 */
+6
next.mdx.use.mjs
··· 7 7 import DownloadReleasesTable from './components/Downloads/DownloadReleasesTable'; 8 8 import HomeDownloadButton from './components/Home/HomeDownloadButton'; 9 9 import Link from './components/Link'; 10 + import UpcomingEvents from './components/MDX/Calendar/UpcomingEvents'; 11 + import UpcomingSummits from './components/MDX/Calendar/UpcomingSummits'; 10 12 import MDXCodeBox from './components/MDX/CodeBox'; 11 13 import MDXCodeTabs from './components/MDX/CodeTabs'; 12 14 import WithBadge from './components/withBadge'; ··· 38 40 DownloadLink: DownloadLink, 39 41 // Renders a Button Component for `button` tags 40 42 Button: Button, 43 + // Renders an container for Upcoming Node.js Summits 44 + UpcomingSummits: UpcomingSummits, 45 + // Renders an container for Upcoming Node.js Events 46 + UpcomingEvents: UpcomingEvents, 41 47 }; 42 48 43 49 /**
+2 -2
pages/en/about/get-involved/collab-summit.md
··· 3 3 layout: about.hbs 4 4 --- 5 5 6 - # Collab Summit 6 + # Collaborator Summit 7 7 8 - Collaboration Summit is an un-conference for bringing current and 8 + Node.js's Collaborator Summit is an un-conference for bringing current and 9 9 potential contributors together to discuss Node.js with lively collaboration, 10 10 education, and knowledge sharing. Committees and working groups come together 11 11 twice per year to make important decisions while also being able to work on some
+24
pages/en/about/get-involved/events.mdx
··· 1 + --- 2 + title: Upcoming Events 3 + layout: about.hbs 4 + --- 5 + 6 + ## Upcoming Node.js® Summits 7 + 8 + Interested in joining a [Collaborator Summit](/about/get-involved/collab-summit) hosted by the Node.js project? 9 + Check out the list below to find upcoming events. 10 + 11 + Browse [previous Collaborator Summits & Events](/blog/events/) hosted by Node.js. 12 + 13 + <UpcomingSummits /> 14 + 15 + --- 16 + 17 + ## Upcoming Node.js® Events 18 + 19 + The Node.js project holds numerous meetings throughout the year to discuss and plan aspects of the project. 20 + These meetings are open and available to the public. Anyone is welcome to join and participate. 21 + 22 + The following Events are upcoming in the next 7 days. 23 + 24 + <UpcomingEvents />
+16 -14
pages/en/new-design/index.mdx
··· 9 9 <div> 10 10 <h1 className="special">Run JavaScript Everywhere</h1> 11 11 12 - Node.js is a free, open-source, cross-platform JavaScript runtime 12 + Node.js® is a free, open-source, cross-platform JavaScript runtime 13 13 environment that lets developers write command line tools and server-side 14 14 scripts outside of a browser. 15 15 ··· 21 21 <> 22 22 <DownloadButton release={release}>Download Node.js (LTS)</DownloadButton> 23 23 <small> 24 - Downloads Node.js <b>{release.versionWithPrefix}</b> with long-term support. 24 + Downloads Node.js <b>{release.versionWithPrefix}</b> 25 + <sup title="Downloads Node.js binary for your current platform">1</sup> with long-term support. 25 26 Node.js can also be installed via <a href="/download/package-manager">package managers</a>. 26 27 </small> 27 28 </> ··· 31 32 {({ release }) => ( 32 33 <small> 33 34 Want new features sooner? 34 - Get <b>Node.js <DownloadLink release={release}>{release.versionWithPrefix}</DownloadLink></b> instead. 35 + Get <b>Node.js <DownloadLink release={release}>{release.versionWithPrefix}</DownloadLink></b> 36 + <sup title="Downloads Node.js binary for your current platform">1</sup> instead. 35 37 </small> 36 38 )} 37 39 </WithNodeRelease> ··· 96 98 readableStream.on('data', chunk => writableStream.write(chunk)); 97 99 ``` 98 100 99 - ```js displayName="Work with Workers" 100 - // file containing main thread (main.mjs) 101 - import { Worker } from 'node:worker_threads'; 102 - 103 - const w = new Worker('./worker.mjs', { workerData: 'hello!' }); 104 - w.on('message', data => console.log('processed data:', data)); 105 - 106 - // file containing worker (worker.mjs) 107 - import { parentPort, workerData } from 'node:worker_threads'; 101 + ```js displayName="Work with Threads" 102 + import { Worker, isMainThread } from 'node:worker_threads'; 103 + import { workerData, parentPort } from 'node:worker_threads'; 108 104 109 - // do some complex computational workload 110 - parentPort.postMessage(btoa(workerData)); 105 + if (isMainThread) { 106 + const data = 'some data'; 107 + const worker = new Worker(import.meta.filename, { workerData: data }); 108 + worker.on('message', m => console.log('Reply from Thread:', m)); 109 + } else { 110 + const source = workerData; 111 + parentPort.postMessage(btoa(source.toUpperCase())); 112 + } 111 113 ``` 112 114 113 115 </div>
+4
redirects.json
··· 203 203 { 204 204 "source": "/:locale/about/releases", 205 205 "destination": "/:locale/about/previous-releases" 206 + }, 207 + { 208 + "source": "/:locale/blog/weekly-updates/:path*", 209 + "destination": "/:locale/blog/weekly/:path*" 206 210 } 207 211 ], 208 212 "internal": []
+1 -1
shiki.config.mjs
··· 14 14 import typeScriptLanguage from 'shikiji/langs/typescript.mjs'; 15 15 import shikiNordTheme from 'shikiji/themes/nord.mjs'; 16 16 17 - /** @type {import('shikiji').LanguageRegistration[]} */ 17 + /** @type {Array<import('shikiji').LanguageRegistration>} */ 18 18 export const LANGUAGES = [ 19 19 { 20 20 ...javaScriptLanguage[0],
+19
types/calendar.ts
··· 1 + export interface ZonedCalendarTime { 2 + dateTime: string; 3 + timeZone: string; 4 + } 5 + 6 + export interface SimpleCalendarTime { 7 + date: string; 8 + } 9 + 10 + export interface CalendarEvent { 11 + id: string; 12 + summary: string; 13 + location?: string; 14 + creator: string; 15 + start: ZonedCalendarTime | SimpleCalendarTime; 16 + end: ZonedCalendarTime | SimpleCalendarTime; 17 + htmlLink: string; 18 + description?: string; 19 + }
+1
types/index.ts
··· 9 9 export * from './redirects'; 10 10 export * from './server'; 11 11 export * from './github'; 12 + export * from './calendar';
+1 -1
util/__tests__/dateIsBetween.test.mjs util/__tests__/dateUtils.test.mjs
··· 1 - import { dateIsBetween } from '../dateIsBetween'; 1 + import { dateIsBetween } from '../dateUtils'; 2 2 3 3 describe('dateIsBetween', () => { 4 4 it('returns true when the current date is between start and end dates', () => {
+2
util/blogUtils.ts
··· 6 6 case 'release': 7 7 case 'vulnerability': 8 8 return type; 9 + case 'events': 10 + return 'announcements'; 9 11 default: 10 12 return 'announcements'; 11 13 }
util/dateIsBetween.ts util/dateUtils.ts