Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add trends tab #77

Merged
merged 1 commit into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/components/CustomTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ const presetTabs = [
name: '版本列表',
key: 'versions',
},
{
name: '下载趋势',
key: 'trends',
},
];

export default function CustomTabs({
Expand Down
97 changes: 93 additions & 4 deletions src/components/RecentDownloads.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import { useRecentDownloads } from '@/hooks/useRecentDownloads';
import { useRecentDownloads, useTotalDownloads } from '@/hooks/useRecentDownloads';
import React from 'react';
import { Empty } from 'antd';

import { Line } from 'react-chartjs-2';
import "chart.js/auto";
import { scales } from 'chart.js/auto';


type RecentDownloadsContentProps = {
pkgName: string;
version: string;
};

type TotalDownloadsProps = {
pkgNameList: string[];
};

const COLOR_LIST = [
'rgb(53, 162, 235, 0.7)',
'rgb(255, 99, 132, 0.7)',
'rgb(255, 205, 86, 0.7)',
'rgb(75, 192, 192, 0.7)',
'rgb(153, 102, 255, 0.7)',
];

export function RecentDownloads({ pkgName, version }: RecentDownloadsContentProps) {
const { data: res, isLoading } = useRecentDownloads(pkgName, version);
if (isLoading || !res) {
Expand Down Expand Up @@ -54,3 +63,83 @@ export function RecentDownloads({ pkgName, version }: RecentDownloadsContentProp
/>
);
}

export function TotalDownloads({ pkgNameList }: TotalDownloadsProps) {
// 通过 swr 来进行缓存,由于 hook 限制,不能直接 map 循环
// 另一种方式是在 useTotalDownloads 中自行维护 cache 逻辑
// 由于希望添加 pkgName 时页面不额外刷新,先采用这种方式
const [pkg1, pkg2, pkg3, pkg4, pkg5] = pkgNameList;
const {data: pkg1Data} = useTotalDownloads(pkg1);
const {data: pkg2Data} = useTotalDownloads(pkg2);
const {data: pkg3Data} = useTotalDownloads(pkg3);
const {data: pkg4Data} = useTotalDownloads(pkg4);
const {data: pkg5Data} = useTotalDownloads(pkg5);

const res = [pkg1Data, pkg2Data, pkg3Data, pkg4Data, pkg5Data];

if (!res.find(_ => _?.downloads)) {
return <Empty description="暂无数据" />;
}

return (
<Line
options={{
scales: {
x: {
display: true,
},
},
elements: {
line: {
tension: 0.4,
},
point: {
radius: 0,
},
},
plugins: {
tooltip: {
enabled: true,
mode: 'nearest',
intersect: false,
axis: 'x',
callbacks: {
// 自定义tooltip的标题
title: function(contexts) {
// 假设所有数据点共享相同的x轴标签
let title = contexts[0].label;
return title || '';
},
// 自定义tooltip的内容
label: function(context) {
// 这里可以访问到所有的数据点
let label = context.dataset.label || '';

if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += context.parsed.y;
}
return label;
},
},
},
},
}}
data={{
labels: res.find(_ => _?.downloads)!.downloads?.map((_) => _.day),
datasets: res.filter(_ => _?.downloads).map((_, index) => {
return {
fill: false,
label: pkgNameList[res.indexOf(_)],
data: _!.downloads.map((_) => _.downloads),
borderColor: COLOR_LIST[index],
// 会影响到 label 展示,虽然 fill false 也一并添加
backgroundColor: COLOR_LIST[index],
}
})
}}
/>
);
}
23 changes: 23 additions & 0 deletions src/hooks/useRecentDownloads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import dayjs from 'dayjs';
import useSwr from 'swr';

const DEFAULT_RANGE = 7;
const INIT_YEAR = 2020;

type DownloadRes = {
downloads: { day: string; downloads: number; }[];
Expand All @@ -20,6 +21,19 @@ function getUrl(pkgName: string, range: number) {
return `${REGISTRY}/downloads/range/${lastWeekStr}:${todayStr}/${pkgName}`;
};

function getTotalUrl(pkgName: string) {
const today = dayjs();
const todayStr = today.format('YYYY-MM-DD');
const years = today.year() - INIT_YEAR + 1;
return new Array(years).fill(0).map((_, index) => {
const year = INIT_YEAR + index;
if (year === today.year()) {
return `${REGISTRY}/downloads/range/${year}-01-01:${todayStr}/${pkgName}`;
}
return `${REGISTRY}/downloads/range/${INIT_YEAR + index}-01-01:${INIT_YEAR + index}-12-31/${pkgName}`;
});
};

function normalizeRes(res: DownloadRes, version: string, range: number): DownloadRes {
// 根据 range,获取最近 range 天的数据
const downloads = res.downloads.slice(-range);
Expand Down Expand Up @@ -47,3 +61,12 @@ export const useRecentDownloads = (pkgName: string, version: string, range: numb
.then(res => normalizeRes(res, version, range));
});
};

export const useTotalDownloads = (pkgName: string) => {
return useSwr<DownloadRes>(pkgName ? `total_downloads: ${pkgName}` : null, async () => {
const res = await Promise.all(getTotalUrl(pkgName).map((url) => fetch(url).then((res) => res.json())));
return { downloads: res.reduce((acc, cur) => acc.concat(cur.downloads), []), versions: {} };
}, {
refreshInterval: 0,
});
};
2 changes: 2 additions & 0 deletions src/pages/package/[...slug]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ThemeProvider as _ThemeProvider } from 'antd-style';
import PageHome from '@/slugs/home';
import PageFiles from '@/slugs/files';
import PageVersions from '@/slugs/versions';
import PageTrends from '@/slugs/trends';
import PageDeps from '@/slugs/deps';
import 'antd/dist/reset.css';
import CustomTabs from '@/components/CustomTabs';
Expand Down Expand Up @@ -65,6 +66,7 @@ const PageMap: Record<string, (params: PageProps) => JSX.Element> = {
deps: PageDeps,
files: PageFiles,
versions: PageVersions,
trends: PageTrends,
} as const;

// 由于路由不支持 @scope 可选参数
Expand Down
60 changes: 60 additions & 0 deletions src/slugs/trends/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
'use client';
import SizeContainer from '@/components/SizeContainer';
import { Card, Select, Typography } from 'antd';
import React, { useState } from 'react';
import { PageProps } from '@/pages/package/[...slug]';
import { TotalDownloads } from '@/components/RecentDownloads';
import { useCachedSearch } from '@/hooks/useSearch';
import { DownOutlined } from '@ant-design/icons';

const MAX_COUNT = 5;

export default function Trends({ manifest: pkg, additionalInfo: needSync, version }: PageProps) {
const [search, setSearch] = useState('');
const [pkgs, setPkgs] = useState([pkg.name]);
const { data: searchResult, isLoading } = useCachedSearch({
keyword: search,
page: 1,
});

const suffix = (
<>
<span>
{pkgs.length} / {MAX_COUNT}
</span>
<DownOutlined />
</>
);

return (
<>
<SizeContainer maxWidth={1072}>
<Select
mode="multiple"
maxCount={MAX_COUNT}
value={pkgs}
style={{ width: '100%' }}
onSearch={setSearch}
suffixIcon={suffix}
placeholder="Please select"
defaultValue={pkgs}
onChange={setPkgs}
options={searchResult?.objects.map((object) => ({
label: (
<>
<Typography.Text>
{object.package.name}
</Typography.Text>
<br />
</>
),
value: object.package.name,
}))}
/>
<Card style={{ marginTop: 24 }}>
<TotalDownloads pkgNameList={pkgs}/>
</Card>
</SizeContainer>
</>
);
}
Loading