Skip to content

Commit 1ef006f

Browse files
ovflowdGeoffreyBoothljharb
authored
feat: introduced new downloads page (#7357)
Co-authored-by: Geoffrey Booth <[email protected]> Co-authored-by: Jordan Harband <[email protected]>
1 parent 68c3996 commit 1ef006f

File tree

193 files changed

+1566
-7872
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

193 files changed

+1566
-7872
lines changed

apps/site/app/[locale]/next-data/blog-data/[category]/[page]/route.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@ import {
33
providePaginatedBlogPosts,
44
} from '@/next-data/providers/blogData';
55
import { defaultLocale } from '@/next.locales.mjs';
6+
import type { BlogCategory } from '@/types';
67

7-
type DynamicStaticPaths = { locale: string; category: string; page: string };
8+
type DynamicStaticPaths = {
9+
locale: string;
10+
category: BlogCategory;
11+
page: string;
12+
};
813
type StaticParams = { params: Promise<DynamicStaticPaths> };
914

1015
// This is the Route Handler for the `GET` method which handles the request

apps/site/components/Common/AlertBox/index.module.css

+3-2
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010
text-white;
1111

1212
a {
13-
@apply font-ibm-plex-mono
13+
@apply font-bold
1414
text-white
15-
underline;
15+
underline
16+
hover:text-white;
1617

1718
&:hover {
1819
@apply no-underline;

apps/site/components/Common/BlogPostCard/index.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ import FormattedTime from '@/components/Common/FormattedTime';
55
import Preview from '@/components/Common/Preview';
66
import Link from '@/components/Link';
77
import WithAvatarGroup from '@/components/withAvatarGroup';
8+
import type { BlogCategory } from '@/types';
89
import { mapBlogCategoryToPreviewType } from '@/util/blogUtils';
910

1011
import styles from './index.module.css';
1112

1213
type BlogPostCardProps = {
1314
title: string;
14-
category: string;
15+
category: BlogCategory;
1516
description?: string;
1617
authors?: Array<string>;
1718
date?: Date;

apps/site/components/Common/Button/index.module.css

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
relative
44
inline-flex
55
items-center
6+
justify-center
67
gap-2
78
py-2.5
89
text-center

apps/site/components/Common/Select/index.module.css

+8-2
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,14 @@
108108
}
109109
}
110110

111-
.dropdown:has(.label) .text > span > span {
112-
@apply pl-3;
111+
.dropdown:has(.label) .text > span {
112+
&:has(svg) > svg {
113+
@apply ml-3;
114+
}
115+
116+
&:not(&:has(svg)) > span {
117+
@apply ml-3;
118+
}
113119
}
114120

115121
.inline {

apps/site/components/Common/Select/index.stories.tsx

+5-8
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import type { Meta as MetaObj, StoryObj } from '@storybook/react';
22

33
import Select from '@/components/Common/Select';
4-
import AIX from '@/components/Icons/Platform/AIX';
5-
import Apple from '@/components/Icons/Platform/Apple';
6-
import Linux from '@/components/Icons/Platform/Linux';
7-
import Microsoft from '@/components/Icons/Platform/Microsoft';
4+
import OSIcons from '@/components/Icons/OperatingSystem';
85

96
type Story = StoryObj<typeof Select>;
107
type Meta = MetaObj<typeof Select>;
@@ -79,22 +76,22 @@ export const InlineSelect: Story = {
7976
{
8077
value: 'linux',
8178
label: 'Linux',
82-
iconImage: <Linux width={16} height={16} />,
79+
iconImage: <OSIcons.Linux width={16} height={16} />,
8380
},
8481
{
8582
value: 'macos',
8683
label: 'macOS',
87-
iconImage: <Apple width={16} height={16} />,
84+
iconImage: <OSIcons.Apple width={16} height={16} />,
8885
},
8986
{
9087
value: 'windows',
9188
label: 'Windows',
92-
iconImage: <Microsoft width={16} height={16} />,
89+
iconImage: <OSIcons.Microsoft width={16} height={16} />,
9390
},
9491
{
9592
value: 'aix',
9693
label: 'AIX',
97-
iconImage: <AIX width={16} height={16} />,
94+
iconImage: <OSIcons.AIX width={16} height={16} />,
9895
},
9996
],
10097
},

apps/site/components/Common/Select/index.tsx

+64-45
Original file line numberDiff line numberDiff line change
@@ -5,44 +5,47 @@ import * as ScrollPrimitive from '@radix-ui/react-scroll-area';
55
import * as SelectPrimitive from '@radix-ui/react-select';
66
import classNames from 'classnames';
77
import { useEffect, useId, useMemo, useState } from 'react';
8-
import type { FC } from 'react';
8+
import type { ReactElement, ReactNode } from 'react';
99

1010
import Skeleton from '@/components/Common/Skeleton';
1111
import type { FormattedMessage } from '@/types';
1212

1313
import styles from './index.module.css';
1414

15-
type SelectValue = {
16-
label: FormattedMessage;
17-
value: string;
18-
iconImage?: React.ReactNode;
15+
export type SelectValue<T extends string> = {
16+
label: FormattedMessage | string;
17+
value: T;
18+
iconImage?: ReactElement<SVGSVGElement>;
1919
disabled?: boolean;
2020
};
2121

22-
type SelectGroup = {
23-
label?: FormattedMessage;
24-
items: Array<SelectValue>;
22+
export type SelectGroup<T extends string> = {
23+
label?: FormattedMessage | string;
24+
items: Array<SelectValue<T>>;
2525
};
2626

2727
const isStringArray = (values: Array<unknown>): values is Array<string> =>
2828
Boolean(values[0] && typeof values[0] === 'string');
2929

30-
const isValuesArray = (values: Array<unknown>): values is Array<SelectValue> =>
30+
const isValuesArray = <T extends string>(
31+
values: Array<unknown>
32+
): values is Array<SelectValue<T>> =>
3133
Boolean(values[0] && typeof values[0] === 'object' && 'value' in values[0]);
3234

33-
type SelectProps = {
34-
values: Array<SelectGroup | string | SelectValue>;
35-
defaultValue?: string;
35+
type SelectProps<T extends string> = {
36+
values: Array<SelectGroup<T>> | Array<T> | Array<SelectValue<T>>;
37+
defaultValue?: T;
3638
placeholder?: string;
3739
label?: string;
3840
inline?: boolean;
39-
onChange?: (value: string) => void;
41+
onChange?: (value: T) => void;
4042
className?: string;
4143
ariaLabel?: string;
4244
loading?: boolean;
45+
disabled?: boolean;
4346
};
4447

45-
const Select: FC<SelectProps> = ({
48+
const Select = <T extends string>({
4649
values = [],
4750
defaultValue,
4851
placeholder,
@@ -52,7 +55,8 @@ const Select: FC<SelectProps> = ({
5255
className,
5356
ariaLabel,
5457
loading = false,
55-
}) => {
58+
disabled = false,
59+
}: SelectProps<T>): ReactNode => {
5660
const id = useId();
5761
const [value, setValue] = useState(defaultValue);
5862

@@ -69,7 +73,7 @@ const Select: FC<SelectProps> = ({
6973
return [{ items: mappedValues }];
7074
}
7175

72-
return mappedValues as Array<SelectGroup>;
76+
return mappedValues as Array<SelectGroup<T>>;
7377
}, [values]);
7478

7579
// We render the actual item slotted to fix/prevent the issue
@@ -82,8 +86,39 @@ const Select: FC<SelectProps> = ({
8286
[mappedValues, value]
8387
);
8488

89+
const memoizedMappedValues = useMemo(() => {
90+
return mappedValues.map(({ label, items }, key) => (
91+
<SelectPrimitive.Group key={label?.toString() ?? key}>
92+
{label && (
93+
<SelectPrimitive.Label
94+
className={classNames(styles.item, styles.label)}
95+
>
96+
{label}
97+
</SelectPrimitive.Label>
98+
)}
99+
100+
{items.map(({ value, label, iconImage, disabled }) => (
101+
<SelectPrimitive.Item
102+
key={value}
103+
value={value}
104+
disabled={disabled}
105+
className={classNames(styles.item, styles.text)}
106+
>
107+
<SelectPrimitive.ItemText>
108+
{iconImage}
109+
<span>{label}</span>
110+
</SelectPrimitive.ItemText>
111+
</SelectPrimitive.Item>
112+
))}
113+
</SelectPrimitive.Group>
114+
));
115+
// We explicitly want to recalculate these values only when the values themselves changed
116+
// This is to prevent re-rendering and re-calcukating the values on every render
117+
// eslint-disable-next-line react-hooks/exhaustive-deps
118+
}, [JSON.stringify(values)]);
119+
85120
// Both change the internal state and emit the change event
86-
const handleChange = (value: string) => {
121+
const handleChange = (value: T) => {
87122
setValue(value);
88123

89124
if (typeof onChange === 'function') {
@@ -106,15 +141,23 @@ const Select: FC<SelectProps> = ({
106141
</label>
107142
)}
108143

109-
<SelectPrimitive.Root value={value} onValueChange={handleChange}>
144+
<SelectPrimitive.Root
145+
value={currentItem !== undefined ? value : undefined}
146+
onValueChange={handleChange}
147+
disabled={disabled}
148+
>
110149
<SelectPrimitive.Trigger
111150
className={styles.trigger}
112151
aria-label={ariaLabel}
113152
id={id}
114153
>
115154
<SelectPrimitive.Value placeholder={placeholder}>
116-
{currentItem?.iconImage}
117-
<span>{currentItem?.label}</span>
155+
{currentItem !== undefined && (
156+
<>
157+
{currentItem.iconImage}
158+
<span>{currentItem.label}</span>
159+
</>
160+
)}
118161
</SelectPrimitive.Value>
119162
<ChevronDownIcon className={styles.icon} />
120163
</SelectPrimitive.Trigger>
@@ -129,31 +172,7 @@ const Select: FC<SelectProps> = ({
129172
<ScrollPrimitive.Root type="auto">
130173
<SelectPrimitive.Viewport>
131174
<ScrollPrimitive.Viewport>
132-
{mappedValues.map(({ label, items }, key) => (
133-
<SelectPrimitive.Group key={label?.toString() ?? key}>
134-
{label && (
135-
<SelectPrimitive.Label
136-
className={classNames(styles.item, styles.label)}
137-
>
138-
{label}
139-
</SelectPrimitive.Label>
140-
)}
141-
142-
{items.map(({ value, label, iconImage, disabled }) => (
143-
<SelectPrimitive.Item
144-
key={value}
145-
value={value}
146-
disabled={disabled}
147-
className={classNames(styles.item, styles.text)}
148-
>
149-
<SelectPrimitive.ItemText>
150-
{iconImage}
151-
<span>{label}</span>
152-
</SelectPrimitive.ItemText>
153-
</SelectPrimitive.Item>
154-
))}
155-
</SelectPrimitive.Group>
156-
))}
175+
{memoizedMappedValues}
157176
</ScrollPrimitive.Viewport>
158177
</SelectPrimitive.Viewport>
159178
<ScrollPrimitive.Scrollbar orientation="vertical">

apps/site/components/Common/Skeleton/index.tsx

+10-1
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,21 @@ import { isValidElement } from 'react';
33

44
import styles from './index.module.css';
55

6-
type SkeletonProps = { loading?: boolean };
6+
type SkeletonProps = { hide?: boolean; loading?: boolean };
77

88
const Skeleton: FC<PropsWithChildren<SkeletonProps>> = ({
99
children,
10+
hide = false,
1011
loading = true,
1112
}) => {
13+
// This can be used to completely hide the children after the Skeleton has loaded
14+
// If certain criterias do not match. This is useful for conditional rendering without
15+
// changing the actual tree that the Skeleton is wrapping
16+
if (!loading && hide) {
17+
return null;
18+
}
19+
20+
// If we finished loading, we can hide the Skeleton and render the children tree
1221
if (!loading) {
1322
return children;
1423
}

apps/site/components/Containers/MetaBar/index.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import Link from '@/components/Link';
88
import styles from './index.module.css';
99

1010
type MetaBarProps = {
11-
items: Record<string, React.ReactNode>;
11+
items: Partial<Record<IntlMessageKeys, React.ReactNode>>;
1212
headings?: {
1313
items: Array<Heading>;
1414
minDepth?: number;
@@ -33,7 +33,7 @@ const MetaBar: FC<MetaBarProps> = ({ items, headings }) => {
3333
.filter(([, value]) => !!value)
3434
.map(([key, value]) => (
3535
<Fragment key={key}>
36-
<dt>{t(key)}</dt>
36+
<dt>{t(key as IntlMessageKeys)}</dt>
3737
<dd>{value}</dd>
3838
</Fragment>
3939
))}

apps/site/components/Downloads/DownloadButton/index.tsx

+5-9
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import Button from '@/components/Common/Button';
88
import { useClientContext } from '@/hooks';
99
import type { NodeRelease } from '@/types';
1010
import { getNodeDownloadUrl } from '@/util/getNodeDownloadUrl';
11-
import { getUserBitnessByArchitecture } from '@/util/getUserBitnessByArchitecture';
11+
import { getUserPlatform } from '@/util/getUserPlatform';
1212

1313
import styles from './index.module.css';
1414

@@ -18,14 +18,10 @@ const DownloadButton: FC<PropsWithChildren<DownloadButtonProps>> = ({
1818
release: { versionWithPrefix },
1919
children,
2020
}) => {
21-
const {
22-
os,
23-
bitness: userBitness,
24-
architecture: userArchitecture,
25-
} = useClientContext();
26-
27-
const bitness = getUserBitnessByArchitecture(userArchitecture, userBitness);
28-
const downloadLink = getNodeDownloadUrl(versionWithPrefix, os, bitness);
21+
const { os, bitness, architecture } = useClientContext();
22+
23+
const platform = getUserPlatform(architecture, bitness);
24+
const downloadLink = getNodeDownloadUrl(versionWithPrefix, os, platform);
2925

3026
return (
3127
<>

apps/site/components/Downloads/DownloadLink.tsx

+5-2
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,18 @@ import type { FC, PropsWithChildren } from 'react';
55
import { useClientContext } from '@/hooks';
66
import type { NodeRelease } from '@/types';
77
import { getNodeDownloadUrl } from '@/util/getNodeDownloadUrl';
8+
import { getUserPlatform } from '@/util/getUserPlatform';
89

910
type DownloadLinkProps = { release: NodeRelease };
1011

1112
const DownloadLink: FC<PropsWithChildren<DownloadLinkProps>> = ({
1213
release: { versionWithPrefix },
1314
children,
1415
}) => {
15-
const { os, bitness } = useClientContext();
16-
const downloadLink = getNodeDownloadUrl(versionWithPrefix, os, bitness);
16+
const { os, bitness, architecture } = useClientContext();
17+
18+
const platform = getUserPlatform(architecture, bitness);
19+
const downloadLink = getNodeDownloadUrl(versionWithPrefix, os, platform);
1720

1821
return <a href={downloadLink}>{children}</a>;
1922
};

0 commit comments

Comments
 (0)