diff --git a/apps/site/app/[locale]/next-data/blog-data/[category]/[page]/route.ts b/apps/site/app/[locale]/next-data/blog-data/[category]/[page]/route.ts index c0061c6a366ad..ed7977b3d6252 100644 --- a/apps/site/app/[locale]/next-data/blog-data/[category]/[page]/route.ts +++ b/apps/site/app/[locale]/next-data/blog-data/[category]/[page]/route.ts @@ -3,8 +3,13 @@ import { providePaginatedBlogPosts, } from '@/next-data/providers/blogData'; import { defaultLocale } from '@/next.locales.mjs'; +import type { BlogCategory } from '@/types'; -type DynamicStaticPaths = { locale: string; category: string; page: string }; +type DynamicStaticPaths = { + locale: string; + category: BlogCategory; + page: string; +}; type StaticParams = { params: Promise }; // This is the Route Handler for the `GET` method which handles the request diff --git a/apps/site/components/Common/AlertBox/index.module.css b/apps/site/components/Common/AlertBox/index.module.css index 3714979473e5d..c3e31ae888704 100644 --- a/apps/site/components/Common/AlertBox/index.module.css +++ b/apps/site/components/Common/AlertBox/index.module.css @@ -10,9 +10,10 @@ text-white; a { - @apply font-ibm-plex-mono + @apply font-bold text-white - underline; + underline + hover:text-white; &:hover { @apply no-underline; diff --git a/apps/site/components/Common/BlogPostCard/index.tsx b/apps/site/components/Common/BlogPostCard/index.tsx index 4f8696dd20565..30815ad9ed62c 100644 --- a/apps/site/components/Common/BlogPostCard/index.tsx +++ b/apps/site/components/Common/BlogPostCard/index.tsx @@ -5,13 +5,14 @@ import FormattedTime from '@/components/Common/FormattedTime'; import Preview from '@/components/Common/Preview'; import Link from '@/components/Link'; import WithAvatarGroup from '@/components/withAvatarGroup'; +import type { BlogCategory } from '@/types'; import { mapBlogCategoryToPreviewType } from '@/util/blogUtils'; import styles from './index.module.css'; type BlogPostCardProps = { title: string; - category: string; + category: BlogCategory; description?: string; authors?: Array; date?: Date; diff --git a/apps/site/components/Common/Button/index.module.css b/apps/site/components/Common/Button/index.module.css index 4d8786bc63b14..6f2d69b8e9bef 100644 --- a/apps/site/components/Common/Button/index.module.css +++ b/apps/site/components/Common/Button/index.module.css @@ -3,6 +3,7 @@ relative inline-flex items-center + justify-center gap-2 py-2.5 text-center diff --git a/apps/site/components/Common/Select/index.module.css b/apps/site/components/Common/Select/index.module.css index 346ee8ee1ef1d..1e13248039a83 100644 --- a/apps/site/components/Common/Select/index.module.css +++ b/apps/site/components/Common/Select/index.module.css @@ -108,8 +108,14 @@ } } -.dropdown:has(.label) .text > span > span { - @apply pl-3; +.dropdown:has(.label) .text > span { + &:has(svg) > svg { + @apply ml-3; + } + + &:not(&:has(svg)) > span { + @apply ml-3; + } } .inline { diff --git a/apps/site/components/Common/Select/index.stories.tsx b/apps/site/components/Common/Select/index.stories.tsx index 59b5da23b7f48..4c24dd474aca0 100644 --- a/apps/site/components/Common/Select/index.stories.tsx +++ b/apps/site/components/Common/Select/index.stories.tsx @@ -1,10 +1,7 @@ import type { Meta as MetaObj, StoryObj } from '@storybook/react'; import Select from '@/components/Common/Select'; -import AIX from '@/components/Icons/Platform/AIX'; -import Apple from '@/components/Icons/Platform/Apple'; -import Linux from '@/components/Icons/Platform/Linux'; -import Microsoft from '@/components/Icons/Platform/Microsoft'; +import OSIcons from '@/components/Icons/OperatingSystem'; type Story = StoryObj; type Meta = MetaObj; @@ -79,22 +76,22 @@ export const InlineSelect: Story = { { value: 'linux', label: 'Linux', - iconImage: , + iconImage: , }, { value: 'macos', label: 'macOS', - iconImage: , + iconImage: , }, { value: 'windows', label: 'Windows', - iconImage: , + iconImage: , }, { value: 'aix', label: 'AIX', - iconImage: , + iconImage: , }, ], }, diff --git a/apps/site/components/Common/Select/index.tsx b/apps/site/components/Common/Select/index.tsx index 66dfed76ff0ab..b2abbbd1b848e 100644 --- a/apps/site/components/Common/Select/index.tsx +++ b/apps/site/components/Common/Select/index.tsx @@ -5,44 +5,47 @@ import * as ScrollPrimitive from '@radix-ui/react-scroll-area'; import * as SelectPrimitive from '@radix-ui/react-select'; import classNames from 'classnames'; import { useEffect, useId, useMemo, useState } from 'react'; -import type { FC } from 'react'; +import type { ReactElement, ReactNode } from 'react'; import Skeleton from '@/components/Common/Skeleton'; import type { FormattedMessage } from '@/types'; import styles from './index.module.css'; -type SelectValue = { - label: FormattedMessage; - value: string; - iconImage?: React.ReactNode; +export type SelectValue = { + label: FormattedMessage | string; + value: T; + iconImage?: ReactElement; disabled?: boolean; }; -type SelectGroup = { - label?: FormattedMessage; - items: Array; +export type SelectGroup = { + label?: FormattedMessage | string; + items: Array>; }; const isStringArray = (values: Array): values is Array => Boolean(values[0] && typeof values[0] === 'string'); -const isValuesArray = (values: Array): values is Array => +const isValuesArray = ( + values: Array +): values is Array> => Boolean(values[0] && typeof values[0] === 'object' && 'value' in values[0]); -type SelectProps = { - values: Array; - defaultValue?: string; +type SelectProps = { + values: Array> | Array | Array>; + defaultValue?: T; placeholder?: string; label?: string; inline?: boolean; - onChange?: (value: string) => void; + onChange?: (value: T) => void; className?: string; ariaLabel?: string; loading?: boolean; + disabled?: boolean; }; -const Select: FC = ({ +const Select = ({ values = [], defaultValue, placeholder, @@ -52,7 +55,8 @@ const Select: FC = ({ className, ariaLabel, loading = false, -}) => { + disabled = false, +}: SelectProps): ReactNode => { const id = useId(); const [value, setValue] = useState(defaultValue); @@ -69,7 +73,7 @@ const Select: FC = ({ return [{ items: mappedValues }]; } - return mappedValues as Array; + return mappedValues as Array>; }, [values]); // We render the actual item slotted to fix/prevent the issue @@ -82,8 +86,39 @@ const Select: FC = ({ [mappedValues, value] ); + const memoizedMappedValues = useMemo(() => { + return mappedValues.map(({ label, items }, key) => ( + + {label && ( + + {label} + + )} + + {items.map(({ value, label, iconImage, disabled }) => ( + + + {iconImage} + {label} + + + ))} + + )); + // We explicitly want to recalculate these values only when the values themselves changed + // This is to prevent re-rendering and re-calcukating the values on every render + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(values)]); + // Both change the internal state and emit the change event - const handleChange = (value: string) => { + const handleChange = (value: T) => { setValue(value); if (typeof onChange === 'function') { @@ -106,15 +141,23 @@ const Select: FC = ({ )} - + - {currentItem?.iconImage} - {currentItem?.label} + {currentItem !== undefined && ( + <> + {currentItem.iconImage} + {currentItem.label} + + )} @@ -129,31 +172,7 @@ const Select: FC = ({ - {mappedValues.map(({ label, items }, key) => ( - - {label && ( - - {label} - - )} - - {items.map(({ value, label, iconImage, disabled }) => ( - - - {iconImage} - {label} - - - ))} - - ))} + {memoizedMappedValues} diff --git a/apps/site/components/Common/Skeleton/index.tsx b/apps/site/components/Common/Skeleton/index.tsx index cb5439345c09b..640c1f2e3852d 100644 --- a/apps/site/components/Common/Skeleton/index.tsx +++ b/apps/site/components/Common/Skeleton/index.tsx @@ -3,12 +3,21 @@ import { isValidElement } from 'react'; import styles from './index.module.css'; -type SkeletonProps = { loading?: boolean }; +type SkeletonProps = { hide?: boolean; loading?: boolean }; const Skeleton: FC> = ({ children, + hide = false, loading = true, }) => { + // This can be used to completely hide the children after the Skeleton has loaded + // If certain criterias do not match. This is useful for conditional rendering without + // changing the actual tree that the Skeleton is wrapping + if (!loading && hide) { + return null; + } + + // If we finished loading, we can hide the Skeleton and render the children tree if (!loading) { return children; } diff --git a/apps/site/components/Containers/MetaBar/index.tsx b/apps/site/components/Containers/MetaBar/index.tsx index b4bc0fccf70f8..febcc88bf3e60 100644 --- a/apps/site/components/Containers/MetaBar/index.tsx +++ b/apps/site/components/Containers/MetaBar/index.tsx @@ -8,7 +8,7 @@ import Link from '@/components/Link'; import styles from './index.module.css'; type MetaBarProps = { - items: Record; + items: Partial>; headings?: { items: Array; minDepth?: number; @@ -33,7 +33,7 @@ const MetaBar: FC = ({ items, headings }) => { .filter(([, value]) => !!value) .map(([key, value]) => ( -
{t(key)}
+
{t(key as IntlMessageKeys)}
{value}
))} diff --git a/apps/site/components/Downloads/DownloadButton/index.tsx b/apps/site/components/Downloads/DownloadButton/index.tsx index c850ed362c8e3..622e655acaf02 100644 --- a/apps/site/components/Downloads/DownloadButton/index.tsx +++ b/apps/site/components/Downloads/DownloadButton/index.tsx @@ -8,7 +8,7 @@ import Button from '@/components/Common/Button'; import { useClientContext } from '@/hooks'; import type { NodeRelease } from '@/types'; import { getNodeDownloadUrl } from '@/util/getNodeDownloadUrl'; -import { getUserBitnessByArchitecture } from '@/util/getUserBitnessByArchitecture'; +import { getUserPlatform } from '@/util/getUserPlatform'; import styles from './index.module.css'; @@ -18,14 +18,10 @@ const DownloadButton: FC> = ({ release: { versionWithPrefix }, children, }) => { - const { - os, - bitness: userBitness, - architecture: userArchitecture, - } = useClientContext(); - - const bitness = getUserBitnessByArchitecture(userArchitecture, userBitness); - const downloadLink = getNodeDownloadUrl(versionWithPrefix, os, bitness); + const { os, bitness, architecture } = useClientContext(); + + const platform = getUserPlatform(architecture, bitness); + const downloadLink = getNodeDownloadUrl(versionWithPrefix, os, platform); return ( <> diff --git a/apps/site/components/Downloads/DownloadLink.tsx b/apps/site/components/Downloads/DownloadLink.tsx index cc23892253585..c4cc62402b7e1 100644 --- a/apps/site/components/Downloads/DownloadLink.tsx +++ b/apps/site/components/Downloads/DownloadLink.tsx @@ -5,6 +5,7 @@ import type { FC, PropsWithChildren } from 'react'; import { useClientContext } from '@/hooks'; import type { NodeRelease } from '@/types'; import { getNodeDownloadUrl } from '@/util/getNodeDownloadUrl'; +import { getUserPlatform } from '@/util/getUserPlatform'; type DownloadLinkProps = { release: NodeRelease }; @@ -12,8 +13,10 @@ const DownloadLink: FC> = ({ release: { versionWithPrefix }, children, }) => { - const { os, bitness } = useClientContext(); - const downloadLink = getNodeDownloadUrl(versionWithPrefix, os, bitness); + const { os, bitness, architecture } = useClientContext(); + + const platform = getUserPlatform(architecture, bitness); + const downloadLink = getNodeDownloadUrl(versionWithPrefix, os, platform); return {children}; }; diff --git a/apps/site/components/Downloads/Release/BitnessDropdown.tsx b/apps/site/components/Downloads/Release/BitnessDropdown.tsx deleted file mode 100644 index e48b44cba48b0..0000000000000 --- a/apps/site/components/Downloads/Release/BitnessDropdown.tsx +++ /dev/null @@ -1,112 +0,0 @@ -'use client'; - -import { useTranslations } from 'next-intl'; -import type { FC } from 'react'; -import { useEffect, useContext, useMemo } from 'react'; -import semVer from 'semver'; - -import Select from '@/components/Common/Select'; -import { useClientContext } from '@/hooks'; -import { ReleaseContext } from '@/providers/releaseProvider'; -import { bitnessItems, formatDropdownItems } from '@/util/downloadUtils'; -import { getUserBitnessByArchitecture } from '@/util/getUserBitnessByArchitecture'; - -const parseNumericBitness = (bitness: string) => - /^\d+$/.test(bitness) ? Number(bitness) : bitness; - -const BitnessDropdown: FC = () => { - const { bitness: userBitness, architecture: userArchitecture } = - useClientContext(); - const { bitness, os, release, setBitness } = useContext(ReleaseContext); - const t = useTranslations(); - - useEffect(() => { - setBitness(getUserBitnessByArchitecture(userArchitecture, userBitness)); - // we shouldn't update the effect on setter state change - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [userArchitecture, userBitness]); - - // @TODO: We should have a proper utility that gives - // disabled OSs, Platforms, based on specific criteria - // this can be an optimisation for the future - // to remove this logic from this component - const disabledItems = useMemo(() => { - const disabledItems = []; - - if (os === 'WIN' && semVer.satisfies(release.version, '< 19.9.0')) { - disabledItems.push('arm64'); - } - - if (os === 'WIN' && semVer.satisfies(release.version, '>= 23.0.0')) { - disabledItems.push('86'); - } - - if (os === 'LINUX' && semVer.satisfies(release.version, '< 4.0.0')) { - disabledItems.push('arm64', 'armv7l'); - } - - if (os === 'LINUX' && semVer.satisfies(release.version, '< 4.4.0')) { - disabledItems.push('ppc64le'); - } - - if (os === 'LINUX' && semVer.satisfies(release.version, '< 6.6.0')) { - disabledItems.push('s390x'); - } - - if (os === 'AIX' && semVer.satisfies(release.version, '< 6.7.0')) { - disabledItems.push('ppc64'); - } - - return disabledItems; - }, [os, release.version]); - - // @TODO: We should have a proper utility that gives - // disabled OSs, Platforms, based on specific criteria - // this can be an optimisation for the future - // to remove this logic from this component - useEffect(() => { - const mappedBitnessValues = bitnessItems[os].map(({ value }) => value); - - const currentBitnessExcluded = - // Different OSs support different Bitnessess, hence we should also check - // if besides the current bitness not being supported for a given release version - // we also should check if it is not supported by the OS - disabledItems.includes(String(bitness)) || - !mappedBitnessValues.includes(String(bitness)); - - const nonExcludedBitness = mappedBitnessValues.find( - bitness => !disabledItems.includes(bitness) - ); - - if (currentBitnessExcluded && nonExcludedBitness) { - // We set it as a Number for cases where it is 64 or 86 otherwise we are - // setting it as a string (ARMv7, ARMv6, etc.) - const numericBitness = Number(nonExcludedBitness); - - setBitness( - numericBitness.toString() === nonExcludedBitness - ? numericBitness - : nonExcludedBitness - ); - } - // we shouldn't react when "actions" change - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [os, disabledItems]); - - return ( - , - MAC: , - LINUX: , - AIX: , - }, - })} - defaultValue={os} - loading={os === 'LOADING'} + + values={parsedOperatingSystems} + defaultValue={release.os !== 'LOADING' ? release.os : undefined} + loading={release.os === 'LOADING'} + placeholder={t('layouts.download.dropdown.unknown')} ariaLabel={t('layouts.download.dropdown.os')} - onChange={value => setOS(value as UserOS)} + onChange={value => release.setOS(value)} className="min-w-[8.5rem]" inline={true} /> diff --git a/apps/site/components/Downloads/Release/PackageManagerDropdown.tsx b/apps/site/components/Downloads/Release/PackageManagerDropdown.tsx new file mode 100644 index 0000000000000..0a0fdafe5173c --- /dev/null +++ b/apps/site/components/Downloads/Release/PackageManagerDropdown.tsx @@ -0,0 +1,49 @@ +'use client'; + +import { useTranslations } from 'next-intl'; +import { useContext, useEffect, useMemo } from 'react'; +import type { FC } from 'react'; + +import Select from '@/components/Common/Select'; +import { ReleaseContext } from '@/providers/releaseProvider'; +import type { PackageManager } from '@/types/release'; +import { nextItem, PACKAGE_MANAGERS, parseCompat } from '@/util/downloadUtils'; + +const PackageManagerDropdown: FC = () => { + const release = useContext(ReleaseContext); + const t = useTranslations(); + + // We parse the compatibility of the dropdown items + const parsedPackageManagers = useMemo( + () => parseCompat(PACKAGE_MANAGERS, release), + // We only want to react on the change of the Version + // eslint-disable-next-line react-hooks/exhaustive-deps + [release.version] + ); + + // We set the Package Manager to the next available Package Manager when the current + // one is not valid anymore due to Version changes + useEffect( + () => + release.setPackageManager( + nextItem(release.packageManager, parsedPackageManagers) + ), + // We only want to react on the change of the Version + // eslint-disable-next-line react-hooks/exhaustive-deps + [release.version] + ); + + return ( + + values={parsedPackageManagers} + defaultValue={release.packageManager} + loading={release.os === 'LOADING' || release.installMethod === ''} + ariaLabel={t('layouts.download.dropdown.packageManager')} + onChange={manager => release.setPackageManager(manager)} + className="min-w-28" + inline={true} + /> + ); +}; + +export default PackageManagerDropdown; diff --git a/apps/site/components/Downloads/Release/PlatformDropdown.tsx b/apps/site/components/Downloads/Release/PlatformDropdown.tsx index 2839fab44b357..6acb57beb0872 100644 --- a/apps/site/components/Downloads/Release/PlatformDropdown.tsx +++ b/apps/site/components/Downloads/Release/PlatformDropdown.tsx @@ -1,87 +1,70 @@ 'use client'; import { useTranslations } from 'next-intl'; -import { useContext, useEffect, useMemo } from 'react'; import type { FC } from 'react'; +import { useEffect, useContext, useMemo } from 'react'; import Select from '@/components/Common/Select'; -import Choco from '@/components/Icons/Platform/Choco'; -import Docker from '@/components/Icons/Platform/Docker'; -import FNM from '@/components/Icons/Platform/FNM'; -import Homebrew from '@/components/Icons/Platform/Homebrew'; -import NVM from '@/components/Icons/Platform/NVM'; +import { useClientContext } from '@/hooks'; import { ReleaseContext } from '@/providers/releaseProvider'; -import type { PackageManager } from '@/types/release'; -import { formatDropdownItems, platformItems } from '@/util/downloadUtils'; - -const supportedHomebrewVersions = ['LTS', 'Current']; +import type { UserPlatform } from '@/types/userOS'; +import { PLATFORMS, nextItem, parseCompat } from '@/util/downloadUtils'; +import { getUserPlatform } from '@/util/getUserPlatform'; const PlatformDropdown: FC = () => { - const { release, os, platform, setPlatform } = useContext(ReleaseContext); - const t = useTranslations(); - - // @TODO: We should have a proper utility that gives - // disabled OSs, Platforms, based on specific criteria - // this can be an optimisation for the future - // to remove this logic from this component - const disabledItems = useMemo(() => { - const disabledItems = []; - - if (os === 'WIN') { - disabledItems.push('BREW', 'NVM'); - } - - if (os === 'LINUX' || os === 'MAC') { - disabledItems.push('CHOCO'); - } - - const releaseSupportsHomebrew = supportedHomebrewVersions.includes( - release.status - ); + const { architecture, bitness } = useClientContext(); - if (!releaseSupportsHomebrew) { - disabledItems.push('BREW'); - } + const release = useContext(ReleaseContext); + const t = useTranslations(); - return disabledItems; - }, [os, release.status]); + useEffect( + () => { + if (architecture && bitness) { + const autoDetectedPlatform = getUserPlatform(architecture, bitness); - // @TODO: We should have a proper utility that gives - // disabled OSs, Platforms, based on specific criteria - // this can be an optimisation for the future - // to remove this logic from this component - useEffect(() => { - const currentPlatformExcluded = disabledItems.includes(platform); + release.setPlatform(autoDetectedPlatform); + } + }, + // Only react on the change of the Client Context Architecture and Bitness + // eslint-disable-next-line react-hooks/exhaustive-deps + [architecture, bitness] + ); - const nonExcludedPlatform = platformItems - .map(({ value }) => value) - .find(platform => !disabledItems.includes(platform)); + // We parse the compatibility of the dropdown items + const parsedPlatforms = useMemo( + () => + // We only want to parse the compatibility when the OS has finished loading + // Otherwise, we would be parsing the compatibility of an empty array + release.os !== 'LOADING' + ? parseCompat(PLATFORMS[release.os], release) + : [], + // We only want to react on the change of the OS, Platform, and Version + // eslint-disable-next-line react-hooks/exhaustive-deps + [release.os, release.platform, release.version] + ); - if (currentPlatformExcluded && nonExcludedPlatform) { - setPlatform(nonExcludedPlatform); - } - // we shouldn't react when "actions" change + // We set the Platform to the next available Architecture when the current + // one is not valid anymore due to OS or Version changes + useEffect( + () => { + if (release.os !== 'LOADING' && release.platform !== '') { + release.setPlatform(nextItem(release.platform, parsedPlatforms)); + } + }, + // We only want to react on the change of the OS and Version // eslint-disable-next-line react-hooks/exhaustive-deps - }, [release.status, disabledItems, platform]); + [release.os, release.version, release.platform] + ); return ( -