Skip to content

Commit 586c2ae

Browse files
committed
Offer arm64 downloads (and improve download UI generally)
1 parent 8c27b88 commit 586c2ae

File tree

7 files changed

+122
-34
lines changed

7 files changed

+122
-34
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,35 @@
11
'use client';
22

33
import sortBy from 'lodash/sortBy';
4+
import partition from 'lodash/partition';
45
import { useEffect, useMemo, useState } from 'react';
56

7+
import { styled } from '@/styles';
8+
69
import { SendEmail } from './components/send-email';
710
import type { DownloadDropdownProps } from './download-button.types';
811
import { Dropdown } from '../dropdown';
9-
import type { DropdownOptionProps } from '../dropdown/dropdown.types';
12+
import type { DropdownOption } from '../dropdown/dropdown.types';
1013

1114
import { StyledHideElementOn } from '@/components/elements/hide-on/hide-on';
1215
import { useIsMobile } from '@/lib/hooks/use-is-mobile';
1316
import { parseUserAgent } from '@/lib/utils/parse-user-agent';
17+
import type { DownloadDictionary } from '@/content/data/download-dictionary';
1418

1519
const LATEST_RELEASE_URL = 'https://github.com/httptoolkit/httptoolkit-desktop/releases/latest';
1620

21+
const DownloadSubText = styled.div`
22+
font-size: ${({ theme }) => theme.fontSizes.text.xs};
23+
margin-top: 4px;
24+
`;
25+
26+
const downloadItemToOption = (item: DownloadDictionary) => ({
27+
as: 'link',
28+
href: item.href || LATEST_RELEASE_URL,
29+
text: item.text,
30+
subtext: item.subtext
31+
} as const);
32+
1733
export const DownloadDropdown = ({
1834
$small,
1935
$variant,
@@ -22,25 +38,27 @@ export const DownloadDropdown = ({
2238
fixedOS,
2339
downloadItems,
2440
}: DownloadDropdownProps) => {
25-
const [operativeSystem, setOperativeSystem] = useState('');
41+
const [operatingSystem, setOperatingSystem] = useState<string>(fixedOS ?? 'windows');
2642
const isMobile = useIsMobile();
27-
const defaultOperativeSystem =
28-
downloadItems.find(os => os.os === operativeSystem && os.defaultText) || downloadItems[0];
2943

30-
const items: DropdownOptionProps[] = useMemo(
31-
() =>
32-
sortBy(downloadItems, [item => (item.os === defaultOperativeSystem.os ? 0 : 1)], 'os', 'desc').map(item => ({
33-
as: 'link',
34-
href: item.href || LATEST_RELEASE_URL,
35-
content: item.text,
36-
})),
37-
[operativeSystem],
44+
const defaultDownload =
45+
downloadItems.find(os => os.os === operatingSystem && os.defaultText) || downloadItems[0];
46+
47+
const items: DropdownOption[] = useMemo(
48+
() => {
49+
const [currentOsDownloads, otherOsDownloads] = partition(downloadItems, ({ os }) => os === operatingSystem);
50+
51+
return [
52+
...sortBy(currentOsDownloads.map(downloadItemToOption), ['desc']),
53+
...(currentOsDownloads.length ? [{ type: 'hr' } as const] : []),
54+
...sortBy(otherOsDownloads.map(downloadItemToOption), ['os', 'desc']),
55+
];
56+
}, [downloadItems, operatingSystem]
3857
);
3958

4059
useEffect(() => {
41-
if (!isMobile && fixedOS) return setOperativeSystem(fixedOS);
42-
43-
setOperativeSystem(parseUserAgent(navigator.userAgent));
60+
if (!isMobile && fixedOS) return setOperatingSystem(fixedOS);
61+
setOperatingSystem(parseUserAgent(navigator.userAgent));
4462
}, []);
4563

4664
// Makes the hide/show with styles to avoid CLS issues
@@ -50,15 +68,20 @@ export const DownloadDropdown = ({
5068
<StyledHideElementOn $hideBelow="md">
5169
<Dropdown
5270
$small={$small}
53-
href={defaultOperativeSystem.href}
71+
href={defaultDownload.href}
5472
$variant={$variant}
5573
$withBorder={$withBorder}
5674
aria-label="Download Items"
5775
items={items}
5876
>
59-
Download for {defaultOperativeSystem.defaultText}
77+
<div>
78+
Download for {defaultDownload.defaultText}
79+
{ defaultDownload.subtext && <DownloadSubText>
80+
{ defaultDownload.subtext }
81+
</DownloadSubText> }
82+
</div>
6083
</Dropdown>
6184
</StyledHideElementOn>
6285
</>
6386
);
64-
};
87+
};

src/components/modules/dropdown/dropdown.styles.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ export const DropdownWrapper = styled.div<Pick<DropdownProps, '$variant'>>`
7070
}
7171
`;
7272

73+
export const DropdownHr = styled.hr`
74+
width: 80%;
75+
opacity: 0.5;
76+
`;
77+
7378
const baseOption = css<DropdownOptionProps>`
7479
background-color: transparent;
7580
border: none;
@@ -97,10 +102,15 @@ const baseOption = css<DropdownOptionProps>`
97102
}
98103
`;
99104

100-
export const LinkDropdownOption = styled(Link)<DropdownOptionProps>`
105+
export const DropdownOptionLink = styled(Link)<DropdownOptionProps>`
101106
${baseOption}
102107
`;
103108

104-
export const DropdownOption = styled.button<DropdownOptionProps>`
109+
export const DropdownOptionButton = styled.button<DropdownOptionProps>`
105110
${baseOption}
106111
`;
112+
113+
export const DropdownOptionSubtext = styled.div`
114+
font-size: ${({ theme }) => theme.fontSizes.text.xs};
115+
margin-top: 5px;
116+
`;

src/components/modules/dropdown/dropdown.types.ts

+13-3
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,29 @@ import type {
88
StyledButtonProps,
99
} from '@/components/elements/button/button.types';
1010

11-
export type OptionComponentType = (props: Component<Omit<DropdownOptionProps, 'content'>>) => JSX.Element;
11+
export type OptionComponentType = (props: Component<Omit<DropdownOptionProps, 'text'>>) => JSX.Element;
12+
13+
export type DropdownOption =
14+
| DropdownDownloadOption
15+
| { type: 'hr' };
16+
17+
export type DropdownDownloadOption = {
18+
as?: ButtonType;
19+
href?: string;
20+
text: string;
21+
subtext?: string;
22+
};
1223

1324
export type DropdownOptionProps = {
1425
as?: ButtonType;
1526
href?: string;
1627
$variant?: StyledButtonProps['$variant'];
17-
content: string;
1828
} & (ButtonWithoutHrefProps | LinkWithHrefProps);
1929

2030
export interface DropdownProps extends StyledButtonProps, AriaAttributes {
2131
icon?: Icon;
2232
iconWeight?: IconWeight;
2333
href?: string;
24-
items: DropdownOptionProps[];
34+
items: DropdownOption[];
2535
$direction?: 'top' | 'bottom';
2636
}

src/components/modules/dropdown/index.tsx

+29-7
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,41 @@
22

33
import { CaretDown } from '@phosphor-icons/react/dist/ssr';
44

5-
import { DropdownOption, DropdownOptionsWrapper, DropdownWrapper, LinkDropdownOption } from './dropdown.styles';
6-
import type { DropdownOptionProps, DropdownProps, OptionComponentType } from './dropdown.types';
5+
import {
6+
DropdownOptionButton,
7+
DropdownOptionsWrapper,
8+
DropdownWrapper,
9+
DropdownOptionLink,
10+
DropdownHr,
11+
DropdownOptionSubtext
12+
} from './dropdown.styles';
13+
import type {
14+
DropdownDownloadOption,
15+
DropdownOption,
16+
DropdownProps,
17+
OptionComponentType
18+
} from './dropdown.types';
719

820
import { Button } from '@/components/elements/button';
921
import type { StyledButtonProps } from '@/components/elements/button/button.types';
1022

11-
const renderOptions = (items: DropdownOptionProps[], $variant: StyledButtonProps['$variant']) => {
12-
return items.map(({ content, as, href, onClick, ...aria }) => {
13-
const OptionComponent: OptionComponentType = as === 'link' || href ? LinkDropdownOption : DropdownOption;
23+
const renderOptions = (items: DropdownOption[], $variant: StyledButtonProps['$variant']) => {
24+
return items.map((item, index) => {
25+
if ('type' in item && item.type === 'hr') {
26+
return <DropdownHr key={`hr-${index}`} />;
27+
}
28+
29+
const { text, subtext, as, href, ...aria } = item as DropdownDownloadOption;
30+
const OptionComponent: OptionComponentType = as === 'link' || href
31+
? DropdownOptionLink
32+
: DropdownOptionButton;
1433

1534
return (
16-
<OptionComponent role="menuitem" key={content} href={href} $variant={$variant} onClick={onClick} {...aria}>
17-
{content}
35+
<OptionComponent role="menuitem" key={`${text}-${subtext}`} href={href} $variant={$variant} {...aria}>
36+
{ text }
37+
{ subtext &&
38+
<DropdownOptionSubtext>{subtext}</DropdownOptionSubtext>
39+
}
1840
</OptionComponent>
1941
);
2042
});

src/content/data/download-dictionary.ts

+19
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export type DownloadDictionary = {
77
slug: string;
88
href: string;
99
text: string;
10+
subtext?: string;
1011
defaultText?: string;
1112
releasePath?: string;
1213
downloadCommand?: string;
@@ -22,6 +23,15 @@ export const getDownloadOptionsDictionary = async (): Promise<DownloadDictionary
2223
href: '/download/osx-dmg',
2324
text: 'MacOS DMG',
2425
defaultText: 'macOS',
26+
subtext: 'Apple Silicon',
27+
releasePath: `v${latestReleaseVersion}/HttpToolkit-${latestReleaseVersion}-arm64.dmg`,
28+
},
29+
{
30+
os: 'mac',
31+
slug: 'osx-dmg',
32+
href: '/download/osx-dmg',
33+
text: 'MacOS DMG',
34+
subtext: 'Intel',
2535
releasePath: `v${latestReleaseVersion}/HttpToolkit-${latestReleaseVersion}-x64.dmg`,
2636
},
2737
{
@@ -89,7 +99,16 @@ export const getDownloadOptionsDictionary = async (): Promise<DownloadDictionary
8999
slug: 'linux-standalone',
90100
href: '/download/linux-standalone',
91101
text: 'Linux Zip',
102+
subtext: 'x64',
92103
releasePath: `v${latestReleaseVersion}/HttpToolkit-${latestReleaseVersion}-linux-x64.zip`,
93104
},
105+
{
106+
os: 'linux',
107+
slug: 'linux-standalone',
108+
href: '/download/linux-standalone',
109+
text: 'Linux Zip',
110+
subtext: 'arm64',
111+
releasePath: `v${latestReleaseVersion}/HttpToolkit-${latestReleaseVersion}-linux-arm64.zip`,
112+
},
94113
];
95114
};

src/lib/utils/parse-user-agent.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1-
export function parseUserAgent(userAgent: string) {
1+
type RecognizedOperatingSystem = 'mac' | 'windows' | 'linux' | 'mobile' | 'unknown';
2+
3+
export function parseUserAgent(userAgent: string): RecognizedOperatingSystem {
24
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent)) {
35
return 'mobile';
46
}
57

6-
let operatingSystem = 'mac';
8+
let operatingSystem: RecognizedOperatingSystem = 'mac';
79
if (userAgent.includes('Windows')) {
810
operatingSystem = 'windows';
911
} else if (userAgent.includes('Macintosh') || userAgent.includes('Mac OS')) {
1012
operatingSystem = 'mac';
1113
} else if (userAgent.includes('Linux')) {
1214
operatingSystem = 'linux';
13-
} // Add more conditions as needed
14-
return operatingSystem;
15+
}
16+
17+
return operatingSystem || 'unknown';
1518
}

src/styles/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ export const theme = {
111111
l: '1.125rem', // 18px / 16px = 1.125.5rem
112112
m: '1rem', // 16px / 16px = 1rem
113113
s: '0.875rem', // 14px / 16px = 0.875
114+
xs: '0.75rem', // 12px / 16px = 0.875
114115
},
115116
button: {
116117
large: '1.5rem',

0 commit comments

Comments
 (0)