From 69753c853520f02e896bc66d19a7757067c965a5 Mon Sep 17 00:00:00 2001 From: Claudio W Date: Sat, 27 Jan 2024 21:49:43 +0100 Subject: [PATCH] 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 Signed-off-by: Claudio W * chore: code-review changes * chore: center content on extra large screens --------- Signed-off-by: Claudio W Co-authored-by: Brian Muenzenmeyer --- .storybook/main.ts | 14 ++++- .storybook/preview.tsx | 6 ++- .../Common/BlogPostCard/index.module.css | 3 +- components/Common/BlogPostCard/index.tsx | 19 +++---- components/Common/FormattedTime.tsx | 24 +++++++++ .../index.tsx => PrevNextArrow.tsx} | 0 components/Common/Time/index.tsx | 18 ------- .../MDX/Calendar/Event/index.module.css | 21 ++++++++ components/MDX/Calendar/Event/index.tsx | 44 +++++++++++++++ components/MDX/Calendar/UpcomingEvents.tsx | 54 +++++++++++++++++++ components/MDX/Calendar/UpcomingSummits.tsx | 38 +++++++++++++ components/MDX/Calendar/calendar.module.css | 17 ++++++ components/MDX/Calendar/utils.ts | 10 ++++ components/withBadge.tsx | 2 +- components/withBanner.tsx | 2 +- components/withMetaBar.tsx | 9 +--- i18n/locales/en.json | 11 +++- layouts/BlogCategoryLayout.tsx | 8 ++- layouts/BlogPostLayout.tsx | 7 +-- layouts/New/Blog.tsx | 1 + layouts/New/layouts.module.css | 11 +++- navigation.json | 6 ++- next-data/blogData.ts | 7 ++- next-data/generators/releaseData.mjs | 2 +- next-data/generators/websiteFeeds.mjs | 2 +- next.calendar.constants.mjs | 38 +++++++++++++ next.calendar.mjs | 39 ++++++++++++++ next.dynamic.constants.mjs | 2 +- next.dynamic.mjs | 2 +- next.helpers.mjs | 6 +-- next.json.mjs | 2 +- next.mdx.compiler.mjs | 3 +- next.mdx.mjs | 4 +- next.mdx.shiki.mjs | 4 +- next.mdx.use.mjs | 6 +++ pages/en/about/get-involved/collab-summit.md | 4 +- pages/en/about/get-involved/events.mdx | 24 +++++++++ pages/en/new-design/index.mdx | 32 +++++------ redirects.json | 4 ++ shiki.config.mjs | 2 +- types/calendar.ts | 19 +++++++ types/index.ts | 1 + ...eIsBetween.test.mjs => dateUtils.test.mjs} | 2 +- util/blogUtils.ts | 2 + util/{dateIsBetween.ts => dateUtils.ts} | 0 45 files changed, 443 insertions(+), 89 deletions(-) create mode 100644 components/Common/FormattedTime.tsx rename components/Common/{PrevNextArrow/index.tsx => PrevNextArrow.tsx} (100%) delete mode 100644 components/Common/Time/index.tsx create mode 100644 components/MDX/Calendar/Event/index.module.css create mode 100644 components/MDX/Calendar/Event/index.tsx create mode 100644 components/MDX/Calendar/UpcomingEvents.tsx create mode 100644 components/MDX/Calendar/UpcomingSummits.tsx create mode 100644 components/MDX/Calendar/calendar.module.css create mode 100644 components/MDX/Calendar/utils.ts create mode 100644 next.calendar.constants.mjs create mode 100644 next.calendar.mjs create mode 100644 pages/en/about/get-involved/events.mdx create mode 100644 types/calendar.ts rename util/__tests__/{dateIsBetween.test.mjs => dateUtils.test.mjs} (94%) rename util/{dateIsBetween.ts => dateUtils.ts} (100%) diff --git a/.storybook/main.ts b/.storybook/main.ts index 66a60e7afaa37..4247bcec94030 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -40,8 +40,20 @@ const config: StorybookConfig = { // `nodevu` is a Node.js-specific package that requires Node.js modules // this is incompatible with Storybook. So we just mock the module resolve: { ...config.resolve, alias: { '@nodevu/core': false } }, + // We need to configure `node:` APIs as Externals to WebPack + // since essentially they're not supported on the browser + externals: { + 'node:fs': 'commonjs fs', + 'node:url': 'commonjs url', + 'node:path': 'commonjs path', + 'node:readline': 'commonjs readline', + }, // Removes Pesky Critical Dependency Warnings due to `next/font` - ignoreWarnings: [e => e.message.includes('Critical dep')], + ignoreWarnings: [ + e => + e.message.includes('Critical dep') || + e.message.includes('was not found in'), + ], }), }; diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 7988925b9793f..cf48543c7e3f5 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -18,7 +18,11 @@ const preview: Preview = { }, decorators: [ Story => ( - + diff --git a/components/Common/BlogPostCard/index.module.css b/components/Common/BlogPostCard/index.module.css index 2683007a2f1dd..a2c84c21e5835 100644 --- a/components/Common/BlogPostCard/index.module.css +++ b/components/Common/BlogPostCard/index.module.css @@ -1,5 +1,6 @@ .container { - @apply max-w-full; + @apply max-w-full + flex-1; } .subtitle { diff --git a/components/Common/BlogPostCard/index.tsx b/components/Common/BlogPostCard/index.tsx index 948c244d8b797..2065f0b20d5d2 100644 --- a/components/Common/BlogPostCard/index.tsx +++ b/components/Common/BlogPostCard/index.tsx @@ -2,8 +2,8 @@ import { useTranslations } from 'next-intl'; import type { FC } from 'react'; import AvatarGroup from '@/components/Common/AvatarGroup'; +import FormattedTime from '@/components/Common/FormattedTime'; import Preview from '@/components/Common/Preview'; -import { Time } from '@/components/Common/Time'; import Link from '@/components/Link'; import { mapBlogCategoryToPreviewType } from '@/util/blogUtils'; @@ -16,9 +16,9 @@ type BlogPostCardProps = { title: string; category: string; description?: string; - authors: Array; - date: Date; - slug: string; + authors?: Array; + date?: Date; + slug?: string; }; const BlogPostCard: FC = ({ @@ -26,7 +26,7 @@ const BlogPostCard: FC = ({ slug, category, description, - authors, + authors = [], date, }) => { const t = useTranslations(); @@ -52,15 +52,12 @@ const BlogPostCard: FC = ({ {description &&

{description}

}
- +
-

{avatars.map(avatar => avatar.alt).join(', ')}

+ {avatars &&

{avatars.map(({ alt }) => alt).join(', ')}

} -
diff --git a/components/Common/FormattedTime.tsx b/components/Common/FormattedTime.tsx new file mode 100644 index 0000000000000..68aff9ee78dd3 --- /dev/null +++ b/components/Common/FormattedTime.tsx @@ -0,0 +1,24 @@ +import type { DateTimeFormatOptions } from 'next-intl'; +import { useFormatter } from 'next-intl'; +import type { FC } from 'react'; + +import { DEFAULT_DATE_FORMAT } from '@/next.calendar.constants.mjs'; + +type FormattedTimeProps = { + date: string | Date; + format?: DateTimeFormatOptions; +}; + +const FormattedTime: FC = ({ date, format }) => { + const formatter = useFormatter(); + + const dateObject = new Date(date); + + return ( + + ); +}; + +export default FormattedTime; diff --git a/components/Common/PrevNextArrow/index.tsx b/components/Common/PrevNextArrow.tsx similarity index 100% rename from components/Common/PrevNextArrow/index.tsx rename to components/Common/PrevNextArrow.tsx diff --git a/components/Common/Time/index.tsx b/components/Common/Time/index.tsx deleted file mode 100644 index c1802cce712bc..0000000000000 --- a/components/Common/Time/index.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type { DateTimeFormatOptions } from 'next-intl'; -import { useFormatter } from 'next-intl'; -import type { FC } from 'react'; -import { useMemo } from 'react'; - -type TimeProps = { date: string | Date; format: DateTimeFormatOptions }; - -export const Time: FC = ({ date, format }) => { - const formatter = useFormatter(); - - const dateObject = useMemo(() => new Date(date), [date]); - - return ( - - ); -}; diff --git a/components/MDX/Calendar/Event/index.module.css b/components/MDX/Calendar/Event/index.module.css new file mode 100644 index 0000000000000..f36d7d8f92aba --- /dev/null +++ b/components/MDX/Calendar/Event/index.module.css @@ -0,0 +1,21 @@ +.event { + @apply flex + w-fit + flex-col + gap-1; + + .title { + @apply flex + flex-row + gap-2; + + span { + @apply text-sm + font-bold; + } + } + + a { + @apply text-sm; + } +} diff --git a/components/MDX/Calendar/Event/index.tsx b/components/MDX/Calendar/Event/index.tsx new file mode 100644 index 0000000000000..a83a62df801d8 --- /dev/null +++ b/components/MDX/Calendar/Event/index.tsx @@ -0,0 +1,44 @@ +import type { FC } from 'react'; + +import FormattedTime from '@/components/Common/FormattedTime'; +import Link from '@/components/Link'; +import { getZoomLink, isZoned } from '@/components/MDX/Calendar/utils'; +import type { CalendarEvent } from '@/types'; + +import styles from './index.module.css'; + +type EventProps = Pick< + CalendarEvent, + 'start' | 'end' | 'summary' | 'location' | 'description' +>; + +const Event: FC = ({ + start, + end, + description, + summary, + location, +}) => ( +
+
+ + + + - + + + + (UTC) +
+ + {summary} +
+); + +export default Event; diff --git a/components/MDX/Calendar/UpcomingEvents.tsx b/components/MDX/Calendar/UpcomingEvents.tsx new file mode 100644 index 0000000000000..e6070814693ad --- /dev/null +++ b/components/MDX/Calendar/UpcomingEvents.tsx @@ -0,0 +1,54 @@ +import type { FC } from 'react'; + +import FormattedTime from '@/components/Common/FormattedTime'; +import Event from '@/components/MDX/Calendar/Event'; +import { getZoomLink, isZoned } from '@/components/MDX/Calendar/utils'; +import { CALENDAR_NODEJS_ID } from '@/next.calendar.constants.mjs'; +import { getCalendarEvents } from '@/next.calendar.mjs'; +import type { CalendarEvent } from '@/types'; + +import styles from './calendar.module.css'; + +type GrouppedEntries = Record>; + +const UpcomingEvents: FC = async () => { + const events = await getCalendarEvents(CALENDAR_NODEJS_ID); + + const groupedEntries = events.filter(getZoomLink).reduce((acc, event) => { + const startDate = new Date( + isZoned(event.start) ? event.start.dateTime : event.start.date + ); + + const datePerDay = startDate.toDateString(); + + acc[datePerDay] = acc[datePerDay] || []; + acc[datePerDay].push(event); + + return acc; + }, {} as GrouppedEntries); + + const sortedGroupedEntries = Object.entries(groupedEntries).sort( + ([dateA], [dateB]) => new Date(dateA).getTime() - new Date(dateB).getTime() + ); + + return sortedGroupedEntries.map(([date, entries]) => ( +
+

+ +

+ + {entries.map(({ id, start, end, summary, location, description }) => ( + + ))} +
+ )); +}; + +export default UpcomingEvents; diff --git a/components/MDX/Calendar/UpcomingSummits.tsx b/components/MDX/Calendar/UpcomingSummits.tsx new file mode 100644 index 0000000000000..e42d411419cda --- /dev/null +++ b/components/MDX/Calendar/UpcomingSummits.tsx @@ -0,0 +1,38 @@ +import { getTranslations } from 'next-intl/server'; +import type { FC } from 'react'; + +import BlogPostCard from '@/components/Common/BlogPostCard'; +import getBlogData from '@/next-data/blogData'; + +import styles from './calendar.module.css'; + +const UpcomingSummits: FC = async () => { + const t = await getTranslations(); + const { posts } = await getBlogData('events', 0); + + const currentDate = new Date(); + const filteredPosts = posts.filter(post => post.date >= currentDate); + + const fallbackPosts = Array(2).fill({ + title: t('components.mdx.upcomingEvents.defaultTitle'), + categories: ['events'], + }); + + const mappedPosts = fallbackPosts.map((post, key) => { + const actualPost = filteredPosts[key] || post; + + return ( + + ); + }); + + return
{mappedPosts}
; +}; + +export default UpcomingSummits; diff --git a/components/MDX/Calendar/calendar.module.css b/components/MDX/Calendar/calendar.module.css new file mode 100644 index 0000000000000..52af750828ddf --- /dev/null +++ b/components/MDX/Calendar/calendar.module.css @@ -0,0 +1,17 @@ +.events { + @apply flex + flex-col + gap-2; + + h4 { + @apply text-xl + font-bold; + } +} + +.summits { + @apply flex + flex-col + gap-3 + md:flex-row; +} diff --git a/components/MDX/Calendar/utils.ts b/components/MDX/Calendar/utils.ts new file mode 100644 index 0000000000000..7b0f632577172 --- /dev/null +++ b/components/MDX/Calendar/utils.ts @@ -0,0 +1,10 @@ +import type { CalendarEvent, ZonedCalendarTime } from '@/types'; + +export const isZoned = (d: object): d is ZonedCalendarTime => + 'dateTime' in d && 'timeZone' in d; + +export const getZoomLink = ( + event: Pick +) => + event.description?.match(/https:\/\/zoom.us\/j\/\d+/)?.[0] || + event.location?.match(/https:\/\/zoom.us\/j\/\d+/)?.[0]; diff --git a/components/withBadge.tsx b/components/withBadge.tsx index 04e68b70ec206..b927605692ab1 100644 --- a/components/withBadge.tsx +++ b/components/withBadge.tsx @@ -2,7 +2,7 @@ import type { FC } from 'react'; import Badge from '@/components/Common/Badge'; import { siteConfig } from '@/next.json.mjs'; -import { dateIsBetween } from '@/util/dateIsBetween'; +import { dateIsBetween } from '@/util/dateUtils'; const WithBadge: FC<{ section: string }> = ({ section }) => { const badge = siteConfig.websiteBadges[section]; diff --git a/components/withBanner.tsx b/components/withBanner.tsx index 6ec0a358da0c7..af571479ad8f4 100644 --- a/components/withBanner.tsx +++ b/components/withBanner.tsx @@ -2,7 +2,7 @@ import type { FC } from 'react'; import Banner from '@/components/Common/Banner'; import { siteConfig } from '@/next.json.mjs'; -import { dateIsBetween } from '@/util/dateIsBetween'; +import { dateIsBetween } from '@/util/dateUtils'; const WithBanner: FC<{ section: string }> = ({ section }) => { const banner = siteConfig.websiteBanners[section]; diff --git a/components/withMetaBar.tsx b/components/withMetaBar.tsx index 4ae130a3dfab1..4202eb1c233bf 100644 --- a/components/withMetaBar.tsx +++ b/components/withMetaBar.tsx @@ -5,20 +5,15 @@ import MetaBar from '@/components/Containers/MetaBar'; import GitHub from '@/components/Icons/Social/GitHub'; import Link from '@/components/Link'; import { useClientContext } from '@/hooks/server'; +import { DEFAULT_DATE_FORMAT } from '@/next.calendar.constants.mjs'; import { getGitHubBlobUrl } from '@/util/gitHubUtils'; -const DATE_FORMAT = { - month: 'short', - day: '2-digit', - year: 'numeric', -} as const; - const WithMetaBar: FC = () => { const { headings, readingTime, frontmatter, filename } = useClientContext(); const formatter = useFormatter(); const lastUpdated = frontmatter.date - ? formatter.dateTime(new Date(frontmatter.date), DATE_FORMAT) + ? formatter.dateTime(new Date(frontmatter.date), DEFAULT_DATE_FORMAT) : undefined; return ( diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 627f9883aa5a6..7d806fc492486 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -85,6 +85,7 @@ "about": { "links": { "about": "About Node.js", + "aboutSide": "About Node.js®", "governance": "Project Governance", "releases": "Previous Releases", "security": "Security Reporting" @@ -93,8 +94,9 @@ "getInvolved": { "links": { "getInvolved": "Get Involved", - "collabSummit": "Collab Summit", - "contribute": "Contribute", + "collabSummit": "Collaborator Summit", + "upcomingEvents": "Upcoming Events", + "contribute": "Contribute to Node.js", "codeOfConduct": "Code of Conduct" } } @@ -151,6 +153,11 @@ "label": "Choose Language" } }, + "mdx": { + "upcomingEvents": { + "defaultTitle": "No Upcoming Event" + } + }, "metabar": { "lastUpdated": "Last Updated", "readingTime": "Reading Time", diff --git a/layouts/BlogCategoryLayout.tsx b/layouts/BlogCategoryLayout.tsx index 32e5303a852e6..ed55c85f4ba90 100644 --- a/layouts/BlogCategoryLayout.tsx +++ b/layouts/BlogCategoryLayout.tsx @@ -2,7 +2,7 @@ import { getTranslations } from 'next-intl/server'; import type { FC } from 'react'; import { getClientContext } from '@/client-context'; -import { Time } from '@/components/Common/Time'; +import FormattedTime from '@/components/Common/FormattedTime'; import Link from '@/components/Link'; import Pagination from '@/components/Pagination'; import getBlogData from '@/next-data/blogData'; @@ -41,10 +41,8 @@ const BlogCategoryLayout: FC = async () => {
    {posts.map(({ slug, date, title }) => (
  • -
  • ))} diff --git a/layouts/BlogPostLayout.tsx b/layouts/BlogPostLayout.tsx index 8d5965f0b2008..3d3b540c959a2 100644 --- a/layouts/BlogPostLayout.tsx +++ b/layouts/BlogPostLayout.tsx @@ -1,7 +1,7 @@ import { useTranslations } from 'next-intl'; import type { FC, PropsWithChildren } from 'react'; -import { Time } from '@/components/Common/Time'; +import FormattedTime from '@/components/Common/FormattedTime'; import { useClientContext } from '@/hooks/server'; const BlogPostLayout: FC = ({ children }) => { @@ -19,10 +19,7 @@ const BlogPostLayout: FC = ({ children }) => { {t('layouts.blogPost.author.byLine', { author: author || null })} - diff --git a/layouts/New/Blog.tsx b/layouts/New/Blog.tsx index a972204cc0764..27f1d3c92d750 100644 --- a/layouts/New/Blog.tsx +++ b/layouts/New/Blog.tsx @@ -52,6 +52,7 @@ const BlogLayout: FC = async () => { 'announcements', 'release', 'vulnerability', + 'events', ])} /> diff --git a/layouts/New/layouts.module.css b/layouts/New/layouts.module.css index 4915e4d6b890e..a54231bba802c 100644 --- a/layouts/New/layouts.module.css +++ b/layouts/New/layouts.module.css @@ -106,6 +106,10 @@ text-neutral-800 dark:text-neutral-400 xs:text-xs; + + sup { + @apply cursor-help; + } } } } @@ -161,7 +165,8 @@ } .contentLayout { - @apply grid + @apply mx-auto + grid w-full max-w-8xl grid-rows-[1fr] @@ -174,12 +179,16 @@ @apply flex w-full justify-center + border-l + border-l-neutral-200 bg-gradient-subtle px-4 py-14 + dark:border-l-neutral-900 dark:bg-gradient-subtle-dark md:px-14 lg:px-28 + xs:border-l-0 xs:bg-none xs:pb-4 xs:dark:bg-none; diff --git a/navigation.json b/navigation.json index 54017f49e8119..7c71d043b2adf 100644 --- a/navigation.json +++ b/navigation.json @@ -35,7 +35,7 @@ "items": { "about": { "link": "/about", - "label": "components.navigation.about.links.about" + "label": "components.navigation.about.links.aboutSide" }, "governance": { "link": "/about/governance", @@ -62,6 +62,10 @@ "link": "/about/get-involved/collab-summit", "label": "components.navigation.getInvolved.links.collabSummit" }, + "upcomingEvents": { + "link": "/about/get-involved/events", + "label": "components.navigation.getInvolved.links.upcomingEvents" + }, "contribute": { "link": "/about/get-involved/contribute", "label": "components.navigation.getInvolved.links.contribute" diff --git a/next-data/blogData.ts b/next-data/blogData.ts index d8f0f6bc67a87..61c7df13cad94 100644 --- a/next-data/blogData.ts +++ b/next-data/blogData.ts @@ -6,6 +6,11 @@ import { } from '@/next.constants.mjs'; import type { BlogPostsRSC } from '@/types'; +// Prevents React from throwing an Error when not able to fulfil a request +// due to missing category or internal processing errors +const parseBlogDataResponse = (data: string): BlogPostsRSC => + data.startsWith('{') ? JSON.parse(data) : { posts: [], pagination: {} }; + const getBlogData = (cat: string, page?: number): Promise => { // When we're using Static Exports the Next.js Server is not running (during build-time) // hence the self-ingestion APIs will not be available. In this case we want to load @@ -26,7 +31,7 @@ const getBlogData = (cat: string, page?: number): Promise => { // When we're on RSC with Server capabilities we prefer using Next.js Data Fetching // as this will load cached data from the server instead of generating data on the fly // this is extremely useful for ISR and SSG as it will not generate this data on every request - return fetch(fetchURL).then(r => r.json()); + return fetch(fetchURL).then(r => r.text().then(parseBlogDataResponse)); }; export default getBlogData; diff --git a/next-data/generators/releaseData.mjs b/next-data/generators/releaseData.mjs index 901dcfb97dc16..8162fbdcdf983 100644 --- a/next-data/generators/releaseData.mjs +++ b/next-data/generators/releaseData.mjs @@ -29,7 +29,7 @@ const getNodeReleaseStatus = (now, support) => { * This method is used to generate the Node.js Release Data * for self-consumption during RSC and Static Builds * - * @returns {Promise} + * @returns {Promise>} */ const generateReleaseData = () => { return nodevu({ fetch: fetch }).then(nodevuOutput => { diff --git a/next-data/generators/websiteFeeds.mjs b/next-data/generators/websiteFeeds.mjs index 0d5d4fb403712..af4ab72bc3afa 100644 --- a/next-data/generators/websiteFeeds.mjs +++ b/next-data/generators/websiteFeeds.mjs @@ -19,7 +19,7 @@ const generateWebsiteFeeds = ({ posts }) => { /** * This generates all the Website RSS Feeds that are used for the website * - * @type {[string, Feed][]} + * @type {Array<[string, Feed]>} */ const websiteFeeds = siteConfig.rssFeeds.map( ({ category, title, description, file }) => { diff --git a/next.calendar.constants.mjs b/next.calendar.constants.mjs new file mode 100644 index 0000000000000..a78dc8f422997 --- /dev/null +++ b/next.calendar.constants.mjs @@ -0,0 +1,38 @@ +'use strict'; + +/** + * This is used for Node.js Calendar and any other Google Calendar that we might want to load within the Website + * + * Note that this is a custom Environment Variable that can be defined by us when necessary + */ +export const BASE_CALENDAR_URL = + process.env.NEXT_PUBLIC_CALENDAR_URL || + `https://clients6.google.com/calendar/v3/calendars/`; + +/** + * This is a shared (public) Google Calendar Key (accessible on the Web) for accessing Google's Public Calendar API + * + * This is a PUBLIC available API Key and not a Secret; It's exposed by Google on their Calendar API Docs + * + * Note that this is a custom Environment Variable that can be defined by us when necessary + */ +export const SHARED_CALENDAR_KEY = + process.env.NEXT_PUBLIC_SHARED_CALENDAR_KEY || + 'AIzaSyBNlYH01_9Hc5S1J9vuFmu2nUqBZJNAXxs'; + +/** + * This is Node.js's Public Google Calendar ID used for all public entries from Node.js Calendar + */ +export const CALENDAR_NODEJS_ID = + 'nodejs.org_nr77ama8p7d7f9ajrpnu506c98@group.calendar.google.com'; + +/** + * Default Date format for Calendars and Time Components + * + * @type {import('next-intl').DateTimeFormatOptions} + */ +export const DEFAULT_DATE_FORMAT = { + year: 'numeric', + month: 'short', + day: '2-digit', +}; diff --git a/next.calendar.mjs b/next.calendar.mjs new file mode 100644 index 0000000000000..1d898c9aa0c37 --- /dev/null +++ b/next.calendar.mjs @@ -0,0 +1,39 @@ +'use strict'; + +import { + BASE_CALENDAR_URL, + SHARED_CALENDAR_KEY, +} from './next.calendar.constants.mjs'; + +/** + * + * @param {string} calendarId + * @param {number} maxResults + * @returns {Promise>} + */ +export const getCalendarEvents = async (calendarId = '', maxResults = 20) => { + const currentDate = new Date(); + const nextWeekDate = new Date(); + + nextWeekDate.setDate(currentDate.getDate() + 7); + + const calendarQueryParams = new URLSearchParams({ + calendarId, + maxResults, + singleEvents: true, + timeZone: 'Etc/Utc', + key: SHARED_CALENDAR_KEY, + timeMax: nextWeekDate.toISOString(), + timeMin: currentDate.toISOString(), + }); + + const calendarQueryUrl = new URL(`${BASE_CALENDAR_URL}${calendarId}/events`); + + calendarQueryParams.forEach((value, key) => + calendarQueryUrl.searchParams.append(key, value) + ); + + return fetch(calendarQueryUrl.toString()) + .then(response => response.json()) + .then(calendar => calendar.items); +}; diff --git a/next.dynamic.constants.mjs b/next.dynamic.constants.mjs index f9dbedd6a5126..8aa2208faddd4 100644 --- a/next.dynamic.constants.mjs +++ b/next.dynamic.constants.mjs @@ -12,7 +12,7 @@ import { defaultLocale } from './next.locales.mjs'; * This is a list of all static routes or pages from the Website that we do not * want to allow to be statically built on our Static Export Build. * - * @type {((route: import('./types').RouteSegment) => boolean)[]} A list of Ignored Routes by Regular Expressions + * @type {Array<((route: import('./types').RouteSegment) => boolean)>} A list of Ignored Routes by Regular Expressions */ export const IGNORED_ROUTES = [ // This is used to ignore all blog routes except for the English language diff --git a/next.dynamic.mjs b/next.dynamic.mjs index f925e6da8dab9..521e7e472c3d4 100644 --- a/next.dynamic.mjs +++ b/next.dynamic.mjs @@ -79,7 +79,7 @@ const getDynamicRouter = async () => { * This method returns a list of all routes that exist for a given locale * * @param {string} locale - * @returns {string[]} + * @returns {Array} */ const getRoutesByLanguage = async (locale = defaultLocale.code) => { const shouldIgnoreStaticRoute = pathname => diff --git a/next.helpers.mjs b/next.helpers.mjs index 1b6faf07f681d..e4cb68533d1e9 100644 --- a/next.helpers.mjs +++ b/next.helpers.mjs @@ -21,7 +21,7 @@ export const getMatchingRoutes = (route = '', matches = []) => * * @param {string} root the root directory to search from * @param {string} cwd the current working directory - * @returns {Promise} a promise containing an array of directories + * @returns {Promise>} a promise containing an array of directories */ export const getDirectories = async (root, cwd) => { return glob('*', { root, cwd, withFileTypes: true }) @@ -46,8 +46,8 @@ export const getRelativePath = path => fileURLToPath(new URL('.', path)); * * @param {string} root the root directory to search from * @param {string} cwd the given locale code - * @param {string[]} ignore an array of glob patterns to ignore - * @returns {Promise} a promise containing an array of paths + * @param {Array} ignore an array of glob patterns to ignore + * @returns {Promise>} a promise containing an array of paths */ export const getMarkdownFiles = async (root, cwd, ignore = []) => { const cacheKey = `${root}${cwd}${ignore.join('')}`; diff --git a/next.json.mjs b/next.json.mjs index 0ec8f4b89ce69..28df7849e101b 100644 --- a/next.json.mjs +++ b/next.json.mjs @@ -7,7 +7,7 @@ import _siteConfig from './site.json' assert { type: 'json' }; /** @type {import('./types').SiteNavigation} */ export const siteNavigation = _siteNavigation; -/** @type {Record} */ +/** @type {Record>} */ export const siteRedirects = _siteRedirects; /** @type {import('./types').SiteConfig} */ diff --git a/next.mdx.compiler.mjs b/next.mdx.compiler.mjs index 29c672147f9a9..1bc2ebc96f83f 100644 --- a/next.mdx.compiler.mjs +++ b/next.mdx.compiler.mjs @@ -18,7 +18,7 @@ const reactRuntime = { Fragment, jsx, jsxs }; * @param {'md' | 'mdx'} fileExtension * @returns {Promise<{ * MDXContent: import('mdx/types').MDXContent; - * headings: import('@vcarl/remark-headings').Heading[]; + * headings: Array; * frontmatter: Record; * readingTime: import('reading-time').ReadTimeResults; * }>} @@ -36,7 +36,6 @@ export async function compileMDX(source, fileExtension) { remarkPlugins: NEXT_REMARK_PLUGINS, format: fileExtension, baseUrl: import.meta.url, - jsxRuntime: 'automatic', ...reactRuntime, }); diff --git a/next.mdx.mjs b/next.mdx.mjs index 9b6d5fb2894e4..3e06f6ab54605 100644 --- a/next.mdx.mjs +++ b/next.mdx.mjs @@ -11,7 +11,7 @@ import rehypeShikiji from './next.mdx.shiki.mjs'; /** * Provides all our Rehype Plugins that are used within MDX * - * @type {import('unified').Plugin[]} + * @type {Array} */ export const NEXT_REHYPE_PLUGINS = [ // Generates `id` attributes for headings (H1, ...) @@ -29,6 +29,6 @@ export const NEXT_REHYPE_PLUGINS = [ /** * Provides all our Remark Plugins that are used within MDX * - * @type {import('unified').Plugin[]} + * @type {Array} */ export const NEXT_REMARK_PLUGINS = [remarkGfm, remarkHeadings, readingTime]; diff --git a/next.mdx.shiki.mjs b/next.mdx.shiki.mjs index 7709adc57094e..c9ee635b84ffc 100644 --- a/next.mdx.shiki.mjs +++ b/next.mdx.shiki.mjs @@ -37,13 +37,13 @@ function getMetaParameter(meta, key) { /** * @typedef {import('unist').Node} Node * @property {string} tagName - * @property {Node[]} children + * @property {Array} children */ /** * Checks if the given node is a valid code element. * - * @param {Node} node - The node to be verified. + * @param {import('unist').Node} node - The node to be verified. * * @return {boolean} - True when it is a valid code element, false otherwise. */ diff --git a/next.mdx.use.mjs b/next.mdx.use.mjs index 2b7f3d51978e8..0aad7237587d4 100644 --- a/next.mdx.use.mjs +++ b/next.mdx.use.mjs @@ -7,6 +7,8 @@ import DownloadLink from './components/Downloads/DownloadLink'; import DownloadReleasesTable from './components/Downloads/DownloadReleasesTable'; import HomeDownloadButton from './components/Home/HomeDownloadButton'; import Link from './components/Link'; +import UpcomingEvents from './components/MDX/Calendar/UpcomingEvents'; +import UpcomingSummits from './components/MDX/Calendar/UpcomingSummits'; import MDXCodeBox from './components/MDX/CodeBox'; import MDXCodeTabs from './components/MDX/CodeTabs'; import WithBadge from './components/withBadge'; @@ -38,6 +40,10 @@ export const mdxComponents = { DownloadLink: DownloadLink, // Renders a Button Component for `button` tags Button: Button, + // Renders an container for Upcoming Node.js Summits + UpcomingSummits: UpcomingSummits, + // Renders an container for Upcoming Node.js Events + UpcomingEvents: UpcomingEvents, }; /** diff --git a/pages/en/about/get-involved/collab-summit.md b/pages/en/about/get-involved/collab-summit.md index b9161ab99cae9..3a8562ba1e575 100644 --- a/pages/en/about/get-involved/collab-summit.md +++ b/pages/en/about/get-involved/collab-summit.md @@ -3,9 +3,9 @@ title: Collab Summit layout: about.hbs --- -# Collab Summit +# Collaborator Summit -Collaboration Summit is an un-conference for bringing current and +Node.js's Collaborator Summit is an un-conference for bringing current and potential contributors together to discuss Node.js with lively collaboration, education, and knowledge sharing. Committees and working groups come together twice per year to make important decisions while also being able to work on some diff --git a/pages/en/about/get-involved/events.mdx b/pages/en/about/get-involved/events.mdx new file mode 100644 index 0000000000000..2b78e7f1928f6 --- /dev/null +++ b/pages/en/about/get-involved/events.mdx @@ -0,0 +1,24 @@ +--- +title: Upcoming Events +layout: about.hbs +--- + +## Upcoming Node.js® Summits + +Interested in joining a [Collaborator Summit](/about/get-involved/collab-summit) hosted by the Node.js project? +Check out the list below to find upcoming events. + +Browse [previous Collaborator Summits & Events](/blog/events/) hosted by Node.js. + + + +--- + +## Upcoming Node.js® Events + +The Node.js project holds numerous meetings throughout the year to discuss and plan aspects of the project. +These meetings are open and available to the public. Anyone is welcome to join and participate. + +The following Events are upcoming in the next 7 days. + + diff --git a/pages/en/new-design/index.mdx b/pages/en/new-design/index.mdx index b0a60ca340812..3821e766e4ad7 100644 --- a/pages/en/new-design/index.mdx +++ b/pages/en/new-design/index.mdx @@ -9,7 +9,7 @@ layout: home.hbs

    Run JavaScript Everywhere

    - Node.js is a free, open-source, cross-platform JavaScript runtime + Node.js® is a free, open-source, cross-platform JavaScript runtime environment that lets developers write command line tools and server-side scripts outside of a browser. @@ -21,7 +21,8 @@ layout: home.hbs <> Download Node.js (LTS) - Downloads Node.js {release.versionWithPrefix} with long-term support. + Downloads Node.js {release.versionWithPrefix} + 1 with long-term support. Node.js can also be installed via package managers. @@ -31,7 +32,8 @@ layout: home.hbs {({ release }) => ( Want new features sooner? - Get Node.js {release.versionWithPrefix} instead. + Get Node.js {release.versionWithPrefix} + 1 instead. )} @@ -96,18 +98,18 @@ layout: home.hbs readableStream.on('data', chunk => writableStream.write(chunk)); ``` - ```js displayName="Work with Workers" - // file containing main thread (main.mjs) - import { Worker } from 'node:worker_threads'; - - const w = new Worker('./worker.mjs', { workerData: 'hello!' }); - w.on('message', data => console.log('processed data:', data)); - - // file containing worker (worker.mjs) - import { parentPort, workerData } from 'node:worker_threads'; - - // do some complex computational workload - parentPort.postMessage(btoa(workerData)); + ```js displayName="Work with Threads" + import { Worker, isMainThread } from 'node:worker_threads'; + import { workerData, parentPort } from 'node:worker_threads'; + + if (isMainThread) { + const data = 'some data'; + const worker = new Worker(import.meta.filename, { workerData: data }); + worker.on('message', m => console.log('Reply from Thread:', m)); + } else { + const source = workerData; + parentPort.postMessage(btoa(source.toUpperCase())); + } ```
    diff --git a/redirects.json b/redirects.json index d3f1f020b6f2e..cdabda19f8c36 100644 --- a/redirects.json +++ b/redirects.json @@ -203,6 +203,10 @@ { "source": "/:locale/about/releases", "destination": "/:locale/about/previous-releases" + }, + { + "source": "/:locale/blog/weekly-updates/:path*", + "destination": "/:locale/blog/weekly/:path*" } ], "internal": [] diff --git a/shiki.config.mjs b/shiki.config.mjs index 5c073faf25ce7..3afaa8957b31e 100644 --- a/shiki.config.mjs +++ b/shiki.config.mjs @@ -14,7 +14,7 @@ import shellSessionLanguage from 'shikiji/langs/shellsession.mjs'; import typeScriptLanguage from 'shikiji/langs/typescript.mjs'; import shikiNordTheme from 'shikiji/themes/nord.mjs'; -/** @type {import('shikiji').LanguageRegistration[]} */ +/** @type {Array} */ export const LANGUAGES = [ { ...javaScriptLanguage[0], diff --git a/types/calendar.ts b/types/calendar.ts new file mode 100644 index 0000000000000..159b076b0b577 --- /dev/null +++ b/types/calendar.ts @@ -0,0 +1,19 @@ +export interface ZonedCalendarTime { + dateTime: string; + timeZone: string; +} + +export interface SimpleCalendarTime { + date: string; +} + +export interface CalendarEvent { + id: string; + summary: string; + location?: string; + creator: string; + start: ZonedCalendarTime | SimpleCalendarTime; + end: ZonedCalendarTime | SimpleCalendarTime; + htmlLink: string; + description?: string; +} diff --git a/types/index.ts b/types/index.ts index 99dc26f939a17..c26a8e205a2b9 100644 --- a/types/index.ts +++ b/types/index.ts @@ -9,3 +9,4 @@ export * from './releases'; export * from './redirects'; export * from './server'; export * from './github'; +export * from './calendar'; diff --git a/util/__tests__/dateIsBetween.test.mjs b/util/__tests__/dateUtils.test.mjs similarity index 94% rename from util/__tests__/dateIsBetween.test.mjs rename to util/__tests__/dateUtils.test.mjs index f66215b64d276..5d89da8576384 100644 --- a/util/__tests__/dateIsBetween.test.mjs +++ b/util/__tests__/dateUtils.test.mjs @@ -1,4 +1,4 @@ -import { dateIsBetween } from '../dateIsBetween'; +import { dateIsBetween } from '../dateUtils'; describe('dateIsBetween', () => { it('returns true when the current date is between start and end dates', () => { diff --git a/util/blogUtils.ts b/util/blogUtils.ts index 49e77b9d34538..7321e78672693 100644 --- a/util/blogUtils.ts +++ b/util/blogUtils.ts @@ -6,6 +6,8 @@ export const mapBlogCategoryToPreviewType = (type: string): BlogPreviewType => { case 'release': case 'vulnerability': return type; + case 'events': + return 'announcements'; default: return 'announcements'; } diff --git a/util/dateIsBetween.ts b/util/dateUtils.ts similarity index 100% rename from util/dateIsBetween.ts rename to util/dateUtils.ts