Skip to content

Commit aff4509

Browse files
authored
add versions list filter input, small refactors (#2273)
1 parent 66391d9 commit aff4509

File tree

16 files changed

+264
-101
lines changed

16 files changed

+264
-101
lines changed

components/InputKeyHint.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Label } from '~/common/styleguide';
2+
import tw from '~/util/tailwind';
3+
4+
type Props = {
5+
content: {
6+
key?: string;
7+
label?: string;
8+
}[];
9+
};
10+
11+
export const focusHintLabel = tw`font-light text-palette-gray4`;
12+
export const focusHintKey = tw`min-w-6 rounded-[3px] bg-palette-gray5 px-1 py-[3px] text-center tracking-[0.75px] text-tertiary dark:bg-powder`;
13+
14+
export default function InputKeyHint({ content }: Props) {
15+
return content.map(entry => {
16+
if ('key' in entry) {
17+
return (
18+
<Label key={`key-${entry.key}`} style={focusHintKey}>
19+
{entry.key}
20+
</Label>
21+
);
22+
} else if ('label' in entry) {
23+
return (
24+
<Label key={`key-${entry.label}`} style={focusHintLabel}>
25+
{entry.label}
26+
</Label>
27+
);
28+
}
29+
});
30+
}

components/Package/VersionDownloadsChart/index.tsx

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { ParentSize } from '@visx/responsive';
22
import { Axis, BarSeries, Grid, Tooltip, XYChart } from '@visx/xychart';
33
import { keyBy } from 'es-toolkit/array';
4-
import { omit } from 'es-toolkit/object';
54
import { useRouter } from 'next/router';
65
import { useEffect, useMemo, useState } from 'react';
76
import { Text, View } from 'react-native';
87

98
import { Label } from '~/common/styleguide';
109
import { type NpmPerVersionDownloads, type NpmRegistryData } from '~/types';
10+
import { replaceQueryParam } from '~/util/queryParams';
1111
import { formatNumberToString, pluralize } from '~/util/strings';
1212
import tw from '~/util/tailwind';
1313

@@ -91,22 +91,10 @@ export default function VersionDownloadsChart({ npmDownloads, registryData }: Pr
9191
}
9292

9393
setMode(nextMode);
94-
95-
const queryParams = omit(router.query, [CHART_MODE_QUERY_PARAM]);
96-
97-
void router.replace(
98-
{
99-
pathname: router.pathname,
100-
query:
101-
nextMode === DEFAULT_CHART_MODE
102-
? queryParams
103-
: { ...queryParams, [CHART_MODE_QUERY_PARAM]: nextMode },
104-
},
105-
undefined,
106-
{
107-
shallow: true,
108-
scroll: false,
109-
}
94+
replaceQueryParam(
95+
router,
96+
CHART_MODE_QUERY_PARAM,
97+
nextMode === DEFAULT_CHART_MODE ? undefined : nextMode
11098
);
11199
}
112100

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { useRouter } from 'next/router';
2+
import { useEffect, useMemo, useRef, useState } from 'react';
3+
import { type ColorValue, TextInput, View } from 'react-native';
4+
import { useDebouncedCallback } from 'use-debounce';
5+
6+
import { Caption, H6, Label, useLayout } from '~/common/styleguide';
7+
import { Button } from '~/components/Button';
8+
import { Search } from '~/components/Icons';
9+
import InputKeyHint from '~/components/InputKeyHint';
10+
import { type NpmPerVersionDownloads, type NpmRegistryData } from '~/types';
11+
import { parseQueryParams, replaceQueryParam } from '~/util/queryParams';
12+
import { pluralize } from '~/util/strings';
13+
import tw from '~/util/tailwind';
14+
15+
import VersionBox from './VersionBox';
16+
17+
const VERSIONS_TO_SHOW = 25;
18+
19+
type Props = {
20+
registryData: NpmRegistryData;
21+
npmDownloads?: NpmPerVersionDownloads;
22+
};
23+
24+
export default function VersionsSection({ registryData, npmDownloads }: Props) {
25+
const router = useRouter();
26+
const { isSmallScreen } = useLayout();
27+
28+
const [shouldShowAll, setShowAll] = useState(false);
29+
const [isInputFocused, setInputFocused] = useState(false);
30+
const inputRef = useRef<TextInput>(null);
31+
32+
const routeVersionSearch = useMemo(
33+
() => parseQueryParams(router.query).versionSearch?.toLowerCase() ?? '',
34+
[router.query]
35+
);
36+
const [versionSearch, setVersionSearch] = useState(routeVersionSearch);
37+
38+
useEffect(() => {
39+
setVersionSearch(currentVersionSearch =>
40+
currentVersionSearch === routeVersionSearch ? currentVersionSearch : routeVersionSearch
41+
);
42+
}, [routeVersionSearch]);
43+
44+
useEffect(() => setShowAll(false), [versionSearch]);
45+
46+
const versions = useMemo(
47+
() =>
48+
Object.entries(registryData.versions).sort(
49+
(a, b) => -registryData.time[a[1].version].localeCompare(registryData.time[b[1].version])
50+
),
51+
[registryData]
52+
);
53+
54+
const filteredVersions = useMemo(
55+
() =>
56+
versionSearch
57+
? versions.filter(([version, versionData]) =>
58+
[version, versionData.version].some(value =>
59+
value.toLowerCase().includes(versionSearch)
60+
)
61+
)
62+
: versions,
63+
[versionSearch, versions]
64+
);
65+
const visibleVersions = useMemo(
66+
() => filteredVersions.slice(0, shouldShowAll ? filteredVersions.length : VERSIONS_TO_SHOW),
67+
[filteredVersions, shouldShowAll]
68+
);
69+
70+
const updateVersionSearchQuery = useDebouncedCallback((versionSearch: string) => {
71+
replaceQueryParam(router, 'versionSearch', versionSearch);
72+
}, 200);
73+
74+
return (
75+
<>
76+
<H6 style={tw`mt-3 flex items-end justify-between text-secondary`}>
77+
<span>Versions</span>
78+
<Label style={tw`font-light text-secondary`}>
79+
<span style={tw`font-medium text-primary-darker dark:text-primary-dark`}>
80+
{filteredVersions.length}
81+
</span>{' '}
82+
matching {pluralize('version', filteredVersions.length)}
83+
</Label>
84+
</H6>
85+
<View style={tw`gap-2`}>
86+
<View
87+
style={tw`flex-row items-center rounded-lg border-2 border-default bg-palette-gray1 dark:bg-dark`}>
88+
<View style={tw`pointer-events-none absolute left-4`}>
89+
<Search style={tw`text-icon`} />
90+
</View>
91+
<TextInput
92+
ref={inputRef}
93+
id="version-search"
94+
autoComplete="off"
95+
value={versionSearch}
96+
onChangeText={text => {
97+
setVersionSearch(text);
98+
updateVersionSearchQuery(text.trim());
99+
}}
100+
onKeyPress={event => {
101+
if ('key' in event) {
102+
if (inputRef.current && event.key === 'Escape') {
103+
if (versionSearch) {
104+
event.preventDefault();
105+
inputRef.current.clear();
106+
setVersionSearch('');
107+
replaceQueryParam(router, 'versionSearch', undefined);
108+
} else {
109+
inputRef.current.blur();
110+
}
111+
}
112+
}
113+
}}
114+
onFocus={() => setInputFocused(true)}
115+
onBlur={() => setInputFocused(false)}
116+
placeholder="Filter versions…"
117+
style={tw`h-11 flex-1 rounded-lg bg-palette-gray1 p-3 pl-11 text-base text-black dark:bg-dark dark:text-white`}
118+
placeholderTextColor={tw`text-palette-gray4`.color as ColorValue}
119+
/>
120+
{!isSmallScreen && (
121+
<View style={tw`pointer-events-none absolute right-4 flex-row items-center gap-1`}>
122+
{isInputFocused && (
123+
<InputKeyHint
124+
content={[
125+
{ label: 'press' },
126+
{ key: 'Esc' },
127+
{ label: `to ${(versionSearch?.length ?? 0) > 0 ? 'clear' : 'blur'}` },
128+
]}
129+
/>
130+
)}
131+
</View>
132+
)}
133+
</View>
134+
</View>
135+
<View style={tw`gap-2`}>
136+
{visibleVersions.length ? (
137+
visibleVersions.map(([version, versionData]) => (
138+
<VersionBox
139+
key={version}
140+
time={registryData.time[versionData.version]}
141+
versionData={versionData}
142+
downloads={npmDownloads?.downloads[versionData.version]}
143+
/>
144+
))
145+
) : (
146+
<View style={tw`rounded-xl border border-dashed border-default px-4 py-5`}>
147+
<Label style={tw`text-center text-secondary`}>
148+
No versions match &quot;{versionSearch?.trim()}&quot; query.
149+
</Label>
150+
</View>
151+
)}
152+
</View>
153+
{!shouldShowAll && filteredVersions.length > VERSIONS_TO_SHOW && (
154+
<Button onPress={() => setShowAll(true)} style={tw`mx-auto mt-2 px-4 py-2`}>
155+
<Caption style={tw`text-white`}>Show all versions</Caption>
156+
</Button>
157+
)}
158+
</>
159+
);
160+
}

components/Search.tsx

Lines changed: 18 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { useEffect, useEffectEvent, useMemo, useRef, useState } from 'react';
33
import { type ColorValue, type StyleProp, TextInput, View, type ViewStyle } from 'react-native';
44
import { useDebouncedCallback } from 'use-debounce';
55

6-
import { Label, P, useLayout } from '~/common/styleguide';
6+
import { P, useLayout } from '~/common/styleguide';
7+
import InputKeyHint from '~/components/InputKeyHint';
78
import { type Query } from '~/types';
89
import isAppleDevice from '~/util/isAppleDevice';
910
import tw from '~/util/tailwind';
@@ -61,9 +62,6 @@ export default function Search({ query, total, style, isHomePage = false }: Prop
6162
void replace(urlWithQuery('/packages', { search, offset: undefined }));
6263
}
6364

64-
const focusHintLabel = tw`font-light text-palette-gray4`;
65-
const focusHintKey = tw`min-w-6 rounded-[3px] bg-palette-gray5 px-1 py-[3px] text-center tracking-[0.75px] text-tertiary dark:bg-powder`;
66-
6765
return (
6866
<>
6967
<View style={[tw`items-center bg-palette-gray6 py-3.5 dark:bg-dark`, style]}>
@@ -123,28 +121,23 @@ export default function Search({ query, total, style, isHomePage = false }: Prop
123121
{!isSmallScreen && (
124122
<View style={tw`pointer-events-none absolute right-4 flex-row items-center gap-1`}>
125123
{isInputFocused ? (
126-
<>
127-
<Label style={focusHintLabel}>press</Label>
128-
{isHomePage ? (
129-
<>
130-
<Label style={focusHintKey}>Enter</Label>
131-
<Label style={focusHintLabel}>to search</Label>
132-
</>
133-
) : (
134-
<>
135-
<Label style={focusHintKey}>Esc</Label>
136-
<Label style={focusHintLabel}>
137-
to {(search?.length ?? 0) > 0 ? 'clear' : 'blur'}
138-
</Label>
139-
</>
140-
)}
141-
</>
124+
isHomePage ? (
125+
<InputKeyHint
126+
content={[{ label: 'press' }, { key: 'Enter' }, { label: 'to search' }]}
127+
/>
128+
) : (
129+
<InputKeyHint
130+
content={[
131+
{ label: 'press' },
132+
{ key: 'Esc' },
133+
{ label: `to ${(search?.length ?? 0) > 0 ? 'clear' : 'blur'}` },
134+
]}
135+
/>
136+
)
142137
) : (
143-
<>
144-
<Label style={focusHintKey}>{isApple ? 'Cmd' : 'Ctrl'}</Label>
145-
<Label style={focusHintLabel}>+</Label>
146-
<Label style={focusHintKey}>K</Label>
147-
</>
138+
<InputKeyHint
139+
content={[{ key: isApple ? 'Cmd' : 'Ctrl' }, { label: '+' }, { key: 'K' }]}
140+
/>
148141
)}
149142
</View>
150143
)}

pages/api/libraries/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import data from '~/assets/data.json';
55
import { getBookmarksFromCookie } from '~/context/BookmarksContext';
66
import { type DataAssetType, type QueryOrder, type SortedDataType } from '~/types';
77
import { NUM_PER_PAGE } from '~/util/Constants';
8-
import { parseQueryParams } from '~/util/parseQueryParams';
8+
import { parseQueryParams } from '~/util/queryParams';
99
import { handleFilterLibraries } from '~/util/search';
1010
import * as Sorting from '~/util/sorting';
1111

pages/api/library/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { type NextApiRequest, type NextApiResponse } from 'next';
22

33
import data from '~/assets/data.json';
44
import { type DataAssetType } from '~/types';
5-
import { parseQueryParams } from '~/util/parseQueryParams';
5+
import { parseQueryParams } from '~/util/queryParams';
66

77
const DATASET = data as DataAssetType;
88

pages/api/proxy/npm-stat.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { type NextApiRequest, type NextApiResponse } from 'next';
22

33
import { NEXT_10M_CACHE_HEADER } from '~/util/Constants';
44
import { TimeRange } from '~/util/datetime';
5-
import { parseQueryParams } from '~/util/parseQueryParams';
5+
import { parseQueryParams } from '~/util/queryParams';
66

77
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
88
const { name } = parseQueryParams(req.query);

pages/package/[name]/[scopedName]/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import PackageOverviewScene from '~/scenes/PackageOverviewScene';
55
import { type PackageOverviewPageProps } from '~/types/pages';
66
import { EMPTY_PACKAGE_DATA, NEXT_10M_CACHE_HEADER } from '~/util/Constants';
77
import { getPackagePageErrorProps } from '~/util/getPackagePageErrorProps';
8-
import { parseQueryParams } from '~/util/parseQueryParams';
8+
import { parseQueryParams } from '~/util/queryParams';
99
import { ssrFetch } from '~/util/SSRFetch';
1010

1111
export default function ScopedOverviewPage({

pages/package/[name]/[scopedName]/score.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import PackageScoreScene from '~/scenes/PackageScoreScene';
55
import { type PackageScorePageProps } from '~/types/pages';
66
import { EMPTY_PACKAGE_DATA } from '~/util/Constants';
77
import { getPackagePageErrorProps } from '~/util/getPackagePageErrorProps';
8-
import { parseQueryParams } from '~/util/parseQueryParams';
8+
import { parseQueryParams } from '~/util/queryParams';
99
import { ssrFetch } from '~/util/SSRFetch';
1010

1111
export default function ScorePage({ apiData, packageName, errorMessage }: PackageScorePageProps) {

pages/package/[name]/[scopedName]/versions.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import PackageVersionsScene from '~/scenes/PackageVersionsScene';
55
import { type PackageVersionsPageProps } from '~/types/pages';
66
import { EMPTY_PACKAGE_DATA, NEXT_10M_CACHE_HEADER } from '~/util/Constants';
77
import { getPackagePageErrorProps } from '~/util/getPackagePageErrorProps';
8-
import { parseQueryParams } from '~/util/parseQueryParams';
8+
import { parseQueryParams } from '~/util/queryParams';
99
import { ssrFetch } from '~/util/SSRFetch';
1010

1111
export default function ScopedVersionsPage({

0 commit comments

Comments
 (0)