Skip to content

Commit

Permalink
feat: Download layout (#6353)
Browse files Browse the repository at this point in the history
* feat: Download layout

* refactor: utils methods moved into the file

* refactor/docs: deprecated url path

* refactor/docs: download utils

* refactor: markdown formatting

* fix: download URL paths

* feat: exclude option for os dropdown

* refactor: separated type import

* refactor: styling updates

* refactor: type definitions moved into the own file

* test: unit tests for download utils

* fix: icons added into the download/source buttons

* fix: LinkWithArrow import path

* docs/refactor: source button

* refactor: review updates

* chore: code-review and code improvements

* feat: finished download page

* chore: minor fixes

* chore: code-review and bug fixes

* chore: prevent page reload on tab change

* chore: improve activelink matching

* feat: proper download versions and prebuilt binaries

* feat: added more support text for each respective version

* chore: minor bitness fixes for macOS

* refactor: cleanup of certain logic and added docker package manager

* chore: fix shiki interop client-server

* chore: fix unit test

* chore: minor text correction

* chore: nvm uses v prefix

* chore: rename label to ARM64 to keep it easier to understand

* refactor: reduce layout shift and improve select accessibility

* chore: reduce layout shift, simplify codebox and cleanup text

* Apply suggestions from code review

Signed-off-by: Brian Muenzenmeyer <[email protected]>

* added Docker platform logo, grouped by usage and alphabetized

* chore: minor changes and fixes

* chore: reduce build times by making build of these routes on-demand

* fix: keep same bitness if compatible on OS change, verify OS supports bitness

---------

Signed-off-by: Brian Muenzenmeyer <[email protected]>
Co-authored-by: Claudio Wunder <[email protected]>
Co-authored-by: Brian Muenzenmeyer <[email protected]>
  • Loading branch information
3 people authored Feb 25, 2024
1 parent aef8eab commit b624417
Show file tree
Hide file tree
Showing 66 changed files with 2,008 additions and 107 deletions.
3 changes: 3 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@

# lint and format staged files
npx lint-staged

# verify typescript staged files
npx tsc --build .
2 changes: 1 addition & 1 deletion app/[locale]/next-data/api-data/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export const dynamicParams = false;

// Enforces that this route is used as static rendering
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic
export const dynamic = 'force-static';
export const dynamic = 'error';

// Ensures that this endpoint is invalidated and re-executed every X minutes
// so that when new deployments happen, the data is refreshed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const dynamicParams = false;

// Enforces that this route is cached and static as much as possible
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic
export const dynamic = 'force-static';
export const dynamic = 'error';

// Ensures that this endpoint is invalidated and re-executed every X minutes
// so that when new deployments happen, the data is refreshed
Expand Down
2 changes: 1 addition & 1 deletion app/[locale]/next-data/page-data/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const dynamicParams = false;

// Enforces that this route is used as static rendering
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic
export const dynamic = 'force-static';
export const dynamic = 'error';

// Ensures that this endpoint is invalidated and re-executed every X minutes
// so that when new deployments happen, the data is refreshed
Expand Down
2 changes: 1 addition & 1 deletion app/[locale]/next-data/release-data/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const dynamicParams = false;

// Enforces that this route is used as static rendering
// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic
export const dynamic = 'force-static';
export const dynamic = 'error';

// Ensures that this endpoint is invalidated and re-executed every X minutes
// so that when new deployments happen, the data is refreshed
Expand Down
4 changes: 2 additions & 2 deletions components/Common/ActiveLink/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import classNames from 'classnames';
import type { ComponentProps, FC } from 'react';

import Link from '@/components/Link';
import { useClientContext } from '@/hooks';
import { usePathname } from '@/navigation.mjs';

type ActiveLocalizedLinkProps = ComponentProps<typeof Link> & {
activeClassName?: string;
Expand All @@ -19,7 +19,7 @@ const ActiveLink: FC<ActiveLocalizedLinkProps> = ({
href = '',
...props
}) => {
const { pathname } = useClientContext();
const pathname = usePathname();

const finalClassName = classNames(className, {
[activeClassName]: allowSubPath
Expand Down
3 changes: 2 additions & 1 deletion components/Common/Breadcrumbs/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useMemo, type FC } from 'react';
import type { FC } from 'react';
import { useMemo } from 'react';

import BreadcrumbHomeLink from '@/components/Common/Breadcrumbs/BreadcrumbHomeLink';
import BreadcrumbItem from '@/components/Common/Breadcrumbs/BreadcrumbItem';
Expand Down
8 changes: 5 additions & 3 deletions components/Common/CodeBox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import styles from './index.module.css';

// Transforms a code element with plain text content into a more structured
// format for rendering with line numbers
const transformCode = (code: ReactNode): ReactNode => {
const transformCode = (code: ReactNode, language: string): ReactNode => {
if (!isValidElement(code)) {
// Early return when the `CodeBox` child is not a valid element since the
// type is a ReactNode, and can assume any value
Expand All @@ -35,8 +35,10 @@ const transformCode = (code: ReactNode): ReactNode => {
// being an empty string, so we need to remove it
const lines = content.split('\n');

const extraStyle = language.length === 0 ? { fontFamily: 'monospace' } : {};

return (
<code style={{ fontFamily: 'monospace' }}>
<code style={extraStyle}>
{lines
.flatMap((line, lineIndex) => {
const columns = line.split(' ');
Expand Down Expand Up @@ -97,7 +99,7 @@ const CodeBox: FC<PropsWithChildren<CodeBoxProps>> = ({
return (
<div className={styles.root}>
<pre ref={ref} className={styles.content} tabIndex={0}>
{transformCode(children)}
{transformCode(children, language)}
</pre>

{language && (
Expand Down
15 changes: 10 additions & 5 deletions components/Common/LinkTabs/index.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,14 @@
}
}

.tabsSelect > div {
@apply my-6
hidden
w-full
xs:flex;
.tabsSelect {
@apply sm:visible
md:hidden;

> span {
@apply my-6
hidden
w-full
xs:flex;
}
}
2 changes: 1 addition & 1 deletion components/Common/ProgressionSidebar/index.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
xs:hidden;
}

> div {
> span {
@apply hidden
w-full
xs:flex;
Expand Down
12 changes: 6 additions & 6 deletions components/Common/ProgressionSidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,19 @@ const ProgressionSidebar: FC<ProgressionSidebarProps> = ({ groups }) => {

return (
<nav className={styles.wrapper}>
<WithRouterSelect
label={t('components.common.sidebar.title')}
values={selectItems}
defaultValue={currentItem?.value}
/>

{groups.map(({ groupName, items }) => (
<ProgressionSidebarGroup
key={groupName.toString()}
groupName={groupName}
items={items}
/>
))}

<WithRouterSelect
label={t('components.common.sidebar.title')}
values={selectItems}
defaultValue={currentItem?.value}
/>
</nav>
);
};
Expand Down
8 changes: 5 additions & 3 deletions components/Common/Select/index.module.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
.select {
@apply flex
w-fit
@apply inline-flex
flex-col
gap-1.5;

Expand Down Expand Up @@ -47,6 +46,7 @@

.trigger span {
@apply flex
h-5
items-center
gap-2;
}
Expand Down Expand Up @@ -115,9 +115,11 @@
.text {
@apply text-neutral-900
data-[highlighted]:bg-neutral-100
data-[disabled]:text-neutral-600
data-[highlighted]:text-neutral-900
dark:text-white
dark:data-[highlighted]:bg-neutral-900;
dark:data-[highlighted]:bg-neutral-900
dark:data-[disabled]:text-neutral-700;
}

&.dropdown {
Expand Down
19 changes: 15 additions & 4 deletions components/Common/Select/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type SelectValue = {
label: FormattedMessage;
value: string;
iconImage?: React.ReactNode;
disabled?: boolean;
};

type SelectGroup = {
Expand All @@ -34,6 +35,7 @@ type SelectProps = {
label?: string;
inline?: boolean;
onChange?: (value: string) => void;
className?: string;
};

const Select: FC<SelectProps> = ({
Expand All @@ -43,6 +45,7 @@ const Select: FC<SelectProps> = ({
label,
inline,
onChange,
className,
}) => {
const id = useId();

Expand All @@ -61,12 +64,19 @@ const Select: FC<SelectProps> = ({
}, [values]);

return (
<div className={classNames(styles.select, { [styles.inline]: inline })}>
{label && (
<span
className={classNames(
styles.select,
{ [styles.inline]: inline },
className
)}
>
{label && !inline && (
<label className={styles.label} htmlFor={id}>
{label}
</label>
)}

<Primitive.Root value={defaultValue} onValueChange={onChange}>
<Primitive.Trigger
className={styles.trigger}
Expand All @@ -92,10 +102,11 @@ const Select: FC<SelectProps> = ({
</Primitive.Label>
)}

{items.map(({ value, label, iconImage }) => (
{items.map(({ value, label, iconImage, disabled }) => (
<Primitive.Item
key={value}
value={value}
disabled={disabled}
className={classNames(styles.item, styles.text)}
>
<Primitive.ItemText>
Expand All @@ -110,7 +121,7 @@ const Select: FC<SelectProps> = ({
</Primitive.Content>
</Primitive.Portal>
</Primitive.Root>
</div>
</span>
);
};

Expand Down
1 change: 1 addition & 0 deletions components/Containers/NavBar/NavItem/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const NavItem: FC<PropsWithChildren<NavItemProps>> = ({
allowSubPath={href.startsWith('/')}
>
<span className={styles.label}>{children}</span>

{type === 'nav' && href.startsWith('http') && (
<ArrowUpRightIcon className={styles.icon} />
)}
Expand Down
2 changes: 1 addition & 1 deletion components/Containers/Sidebar/index.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
xs:hidden;
}

> div {
> span {
@apply hidden
w-full
xs:flex;
Expand Down
4 changes: 2 additions & 2 deletions components/Downloads/DownloadButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { FC, PropsWithChildren } from 'react';
import Button from '@/components/Common/Button';
import { useDetectOS } from '@/hooks';
import type { NodeRelease } from '@/types';
import { downloadUrlByOS } from '@/util/downloadUrlByOS';
import { getNodeDownloadUrl } from '@/util/getNodeDownloadUrl';

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

Expand All @@ -18,7 +18,7 @@ const DownloadButton: FC<PropsWithChildren<DownloadButtonProps>> = ({
children,
}) => {
const { os, bitness } = useDetectOS();
const downloadLink = downloadUrlByOS(versionWithPrefix, os, bitness);
const downloadLink = getNodeDownloadUrl(versionWithPrefix, os, bitness);

return (
<>
Expand Down
4 changes: 2 additions & 2 deletions components/Downloads/DownloadLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { FC, PropsWithChildren } from 'react';

import { useDetectOS } from '@/hooks';
import type { NodeRelease } from '@/types';
import { downloadUrlByOS } from '@/util/downloadUrlByOS';
import { getNodeDownloadUrl } from '@/util/getNodeDownloadUrl';

type DownloadLinkProps = { release: NodeRelease };

Expand All @@ -13,7 +13,7 @@ const DownloadLink: FC<PropsWithChildren<DownloadLinkProps>> = ({
children,
}) => {
const { os, bitness } = useDetectOS();
const downloadLink = downloadUrlByOS(versionWithPrefix, os, bitness);
const downloadLink = getNodeDownloadUrl(versionWithPrefix, os, bitness);

return <a href={downloadLink}>{children}</a>;
};
Expand Down
91 changes: 91 additions & 0 deletions components/Downloads/Release/BitnessDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
'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 { useDetectOS } from '@/hooks/react-client';
import { ReleaseContext } from '@/providers/releaseProvider';
import { bitnessItems, formatDropdownItems } from '@/util/downloadUtils';

const parseNumericBitness = (bitness: string) =>
/^\d+$/.test(bitness) ? Number(bitness) : bitness;

const BitnessDropdown: FC = () => {
const { bitness: userBitness } = useDetectOS();
const { bitness, os, release, setBitness } = useContext(ReleaseContext);
const t = useTranslations();

// we also reset the bitness when the OS changes, because different OSs have
// different bitnesses available
useEffect(() => setBitness(userBitness), [setBitness, 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 === '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');
}

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 mappedBittnessesValues = bitnessItems[os].map(({ value }) => value);

const currentBittnessExcluded =
// 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)) ||
!mappedBittnessesValues.includes(String(bitness));

const nonExcludedBitness = mappedBittnessesValues.find(
bittness => !disabledItems.includes(bittness)
);

if (currentBittnessExcluded && nonExcludedBitness) {
setBitness(nonExcludedBitness);
}
// we shouldn't react when "actions" change
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [os, disabledItems]);

return (
<Select
label={t('layouts.download.dropdown.bitness')}
values={formatDropdownItems({
items: bitnessItems[os],
disabledItems,
})}
defaultValue={String(bitness)}
onChange={bitness => setBitness(parseNumericBitness(bitness))}
className="w-28"
inline={true}
/>
);
};

export default BitnessDropdown;
Loading

0 comments on commit b624417

Please sign in to comment.