Skip to content

Commit 033639f

Browse files
committed
Upgrades
1 parent becf8df commit 033639f

File tree

1 file changed

+161
-41
lines changed

1 file changed

+161
-41
lines changed

app/routes/stats/npm/index.tsx

Lines changed: 161 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as React from 'react'
22
import { createFileRoute, Link } from '@tanstack/react-router'
33
import { z } from 'zod'
44
import {
5+
MdArrowBack,
56
MdClose,
67
MdLock,
78
MdLockOpen,
@@ -13,6 +14,8 @@ import * as Plot from '@observablehq/plot'
1314
import { ParentSize } from '@visx/responsive'
1415
import { Tooltip } from '~/components/Tooltip'
1516
import * as d3 from 'd3'
17+
import { useCombobox } from 'downshift'
18+
import { FaAngleRight, FaArrowLeft } from 'react-icons/fa'
1619

1720
type NpmStats = {
1821
start: string
@@ -46,6 +49,15 @@ type TimeInterval = '7-days' | '30-days' | '90-days' | '180-days' | '365-days'
4649

4750
type BinningOption = 'monthly' | 'weekly' | 'daily'
4851

52+
type NpmPackage = {
53+
name: string
54+
description: string
55+
version: string
56+
publisher: {
57+
username: string
58+
}
59+
}
60+
4961
function npmQueryOptions({
5062
packageNames,
5163
interval,
@@ -180,6 +192,7 @@ function NpmStatsChart({
180192

181193
// Compare dates at the start of the day
182194
cutoffDate.setHours(0, 0, 0, 0)
195+
183196
return {
184197
...stat,
185198
downloads: stat.downloads.filter((d) => {
@@ -286,8 +299,8 @@ function NpmStatsChart({
286299
{({ width }) => (
287300
<PlotFigure
288301
options={{
289-
marginLeft: 50,
290-
marginRight: 0,
302+
marginLeft: 70,
303+
marginRight: 10,
291304
marginBottom: 70,
292305
width,
293306
height,
@@ -428,6 +441,110 @@ export const Route = createFileRoute('/stats/npm/')({
428441
component: RouteComponent,
429442
})
430443

444+
function PackageSearch() {
445+
const [items, setItems] = React.useState<NpmPackage[]>([])
446+
const [isLoading, setIsLoading] = React.useState(false)
447+
const navigate = Route.useNavigate()
448+
449+
const {
450+
isOpen,
451+
getMenuProps,
452+
getInputProps,
453+
highlightedIndex,
454+
getItemProps,
455+
reset,
456+
inputValue,
457+
} = useCombobox({
458+
items,
459+
onInputValueChange: ({ inputValue }) => {
460+
if (inputValue && inputValue.length > 2) {
461+
setIsLoading(true)
462+
fetch(
463+
`https://api.npms.io/v2/search?q=${encodeURIComponent(
464+
inputValue
465+
)}&size=10`
466+
)
467+
.then((res) => res.json())
468+
.then((data) => {
469+
const hasInputValue = data.results.find(
470+
(r: any) => r.package.name === inputValue
471+
)
472+
473+
setItems([
474+
...(hasInputValue ? [] : [{ name: inputValue }]),
475+
...data.results.map((r: any) => r.package),
476+
])
477+
setIsLoading(false)
478+
})
479+
.catch(() => {
480+
setIsLoading(false)
481+
})
482+
} else {
483+
setItems([])
484+
}
485+
},
486+
onSelectedItemChange: ({ selectedItem }) => {
487+
if (!selectedItem) return
488+
489+
navigate({
490+
to: '.',
491+
search: (prev) => ({
492+
...prev,
493+
packageNames: [...(prev.packageNames || []), selectedItem.name],
494+
}),
495+
resetScroll: false,
496+
})
497+
reset()
498+
setItems([])
499+
},
500+
})
501+
502+
return (
503+
<div className="flex-1">
504+
<div className="relative">
505+
<input
506+
{...getInputProps()}
507+
placeholder="Search for a package..."
508+
className="w-full bg-gray-500/10 rounded-md px-3 py-2"
509+
/>
510+
<ul
511+
{...getMenuProps()}
512+
className={`absolute z-10 w-full mt-1 bg-white dark:bg-gray-800 rounded-md shadow-lg max-h-60 overflow-auto ${
513+
isOpen ? '' : 'hidden'
514+
}`}
515+
>
516+
{isLoading ? (
517+
<li className="px-3 py-2 text-gray-500">Loading...</li>
518+
) : items.length === 0 ? (
519+
<li className="px-3 py-2 text-gray-500">No packages found</li>
520+
) : (
521+
items.map((item, index) => (
522+
<li
523+
key={item.name}
524+
{...getItemProps({ item, index })}
525+
className={`px-3 py-2 cursor-pointer ${
526+
highlightedIndex === index
527+
? 'bg-gray-500/20 '
528+
: 'hover:bg-gray-500/20'
529+
}`}
530+
>
531+
<div className="font-medium">{item.name}</div>
532+
<div className="text-sm text-gray-500 dark:text-gray-400">
533+
{item.description}
534+
</div>
535+
<div className="text-xs text-gray-400 dark:text-gray-500">
536+
{item.version ? `v${item.version}• ` : ''}
537+
{item.publisher?.username}
538+
</div>
539+
</li>
540+
))
541+
)}
542+
</ul>
543+
</div>
544+
</div>
545+
)
546+
}
547+
431548
function RouteComponent() {
432549
const {
433550
packageNames,
@@ -436,7 +553,6 @@ function RouteComponent() {
436553
viewMode = 'absolute',
437554
binningOption: binningOptionParam,
438555
} = Route.useSearch()
439-
const [searchValue, setSearchValue] = React.useState('')
440556
const [hiddenPackages, setHiddenPackages] = React.useState<Set<string>>(
441557
new Set()
442558
)
@@ -478,34 +594,22 @@ function RouteComponent() {
478594
})
479595
)
480596

481-
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
482-
e.preventDefault()
483-
if (!searchValue) return
484-
485-
setSearchValue('')
486-
navigate({
487-
to: '.',
488-
search: (prev) => ({
489-
...prev,
490-
packageNames: [...(prev.packageNames || []), searchValue],
491-
}),
492-
})
493-
}
494-
495597
const removePackageName = (packageName: string) => {
496598
navigate({
497599
to: '.',
498600
search: (prev) => ({
499601
...prev,
500602
packageNames: prev.packageNames?.filter((name) => name !== packageName),
501603
}),
604+
resetScroll: false,
502605
})
503606
}
504607

505608
const setBinningOption = (newBinningOption: BinningOption) => {
506609
navigate({
507610
to: '.',
508611
search: (prev) => ({ ...prev, binningOption: newBinningOption }),
612+
resetScroll: false,
509613
})
510614
}
511615

@@ -535,6 +639,7 @@ function RouteComponent() {
535639
...prev,
536640
interval: newInterval,
537641
}),
642+
resetScroll: false,
538643
})
539644
}
540645

@@ -545,6 +650,7 @@ function RouteComponent() {
545650
...prev,
546651
viewMode: mode,
547652
}),
653+
resetScroll: false,
548654
})
549655
}
550656

@@ -555,6 +661,7 @@ function RouteComponent() {
555661
...prev,
556662
binningOption: value,
557663
}),
664+
resetScroll: false,
558665
})
559666
}
560667

@@ -565,6 +672,7 @@ function RouteComponent() {
565672
...prev,
566673
baseline: prev.baseline === packageName ? undefined : packageName,
567674
}),
675+
resetScroll: false,
568676
})
569677
}
570678

@@ -575,22 +683,18 @@ function RouteComponent() {
575683

576684
return (
577685
<div className="min-h-dvh p-4 space-y-4">
578-
<div className="bg-white dark:bg-black/50 rounded-lg p-4">
579-
<Link to=".">
580-
<h1 className="text-3xl font-bold">NPM Stats</h1>
686+
<div className="bg-white dark:bg-black/50 rounded-lg p-4 flex items-center gap-2 text-xl">
687+
<Link to="/" className="hover:text-blue-500">
688+
Home
689+
</Link>
690+
<FaAngleRight />
691+
<Link to="." className="hover:text-blue-500">
692+
NPM Stats
581693
</Link>
582694
</div>
583695
<div className="bg-white dark:bg-black/50 rounded-lg space-y-4 p-4">
584696
<div className="flex gap-4 flex-wrap">
585-
<form className="flex gap-2 flex-1" onSubmit={handleSubmit}>
586-
<input
587-
type="text"
588-
value={searchValue}
589-
placeholder="package-name"
590-
className="bg-gray-500/10 rounded-md px-3 py-2 flex-1"
591-
onChange={(e) => setSearchValue(e.target.value)}
592-
/>
593-
</form>
697+
<PackageSearch />
594698
<select
595699
value={interval}
596700
onChange={(e) =>
@@ -691,15 +795,6 @@ function RouteComponent() {
691795
)}
692796
</button>
693797
</Tooltip>
694-
<Tooltip content="Use as baseline for comparison">
695-
<button
696-
onClick={() => handleBaselineChange(packageName)}
697-
className="p-1 hover:text-blue-500"
698-
>
699-
{baseline === packageName ? <MdLock /> : <MdLockOpen />}
700-
</button>
701-
</Tooltip>
702-
703798
<button
704799
onClick={() => togglePackageVisibility(packageName)}
705800
className={`px-1 hover:text-blue-500 ${
@@ -708,6 +803,14 @@ function RouteComponent() {
708803
>
709804
{packageName}
710805
</button>
806+
<Tooltip content="Use as baseline for comparison">
807+
<button
808+
onClick={() => handleBaselineChange(packageName)}
809+
className="p-1 hover:text-blue-500"
810+
>
811+
{baseline === packageName ? <MdLock /> : <MdLockOpen />}
812+
</button>
813+
</Tooltip>
711814
<button
712815
onClick={() => removePackageName(packageName)}
713816
className="p-1 text-gray-500 hover:text-red-500"
@@ -718,7 +821,7 @@ function RouteComponent() {
718821
))}
719822
</div>
720823
{packageNames?.length ? (
721-
<div className="p-4 rounded-lg bg-white dark:bg-gray-900">
824+
<div className="">
722825
<div className="space-y-4">
723826
<NpmStatsChart
724827
stats={validStats}
@@ -740,6 +843,9 @@ function RouteComponent() {
740843
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
741844
Growth
742845
</th>
846+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
847+
% Growth
848+
</th>
743849
</tr>
744850
</thead>
745851
<tbody className="bg-white dark:bg-gray-900">
@@ -756,7 +862,8 @@ function RouteComponent() {
756862
const lastValue =
757863
sortedDownloads[sortedDownloads.length - 1]
758864
?.downloads || 1
759-
const growth =
865+
const growth = lastValue - firstValue
866+
const growthPercentage =
760867
((lastValue - firstValue) / firstValue) * 100
761868

762869
return {
@@ -766,6 +873,7 @@ function RouteComponent() {
766873
0
767874
),
768875
growth,
876+
growthPercentage,
769877
}
770878
})
771879
.filter(Boolean)
@@ -788,7 +896,19 @@ function RouteComponent() {
788896
}`}
789897
>
790898
{stat!.growth > 0 ? '+' : ''}
791-
{stat!.growth.toFixed(1)}%
899+
{formatNumber(stat!.growth)}
900+
</td>
901+
<td
902+
className={`px-6 py-4 whitespace-nowrap text-sm ${
903+
stat!.growthPercentage > 0
904+
? 'text-green-500'
905+
: stat!.growthPercentage < 0
906+
? 'text-red-500'
907+
: 'text-gray-500'
908+
}`}
909+
>
910+
{stat!.growthPercentage > 0 ? '+' : ''}
911+
{stat!.growthPercentage.toFixed(1)}%
792912
</td>
793913
</tr>
794914
))}

0 commit comments

Comments
 (0)