mirror of
https://github.com/alibaba/open-code-review.git
synced 2026-08-17 04:34:02 +00:00
feat(pages): show live npm downloads in highlights stats (#794)
Some checks are pending
CI / cross-compile (amd64, darwin) (push) Waiting to run
CI / cross-compile (amd64, windows) (push) Waiting to run
CI / cross-compile (arm64, darwin) (push) Waiting to run
CI / cross-compile (arm64, linux) (push) Waiting to run
CodeQL Advanced / Analyze (go) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Deploy Pages / build (push) Waiting to run
Deploy Pages / deploy (push) Blocked by required conditions
CI / cross-compile (arm64, windows) (push) Waiting to run
CI / test (push) Waiting to run
Some checks are pending
CI / cross-compile (amd64, darwin) (push) Waiting to run
CI / cross-compile (amd64, windows) (push) Waiting to run
CI / cross-compile (arm64, darwin) (push) Waiting to run
CI / cross-compile (arm64, linux) (push) Waiting to run
CodeQL Advanced / Analyze (go) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Deploy Pages / build (push) Waiting to run
Deploy Pages / deploy (push) Blocked by required conditions
CI / cross-compile (arm64, windows) (push) Waiting to run
CI / test (push) Waiting to run
* feat(pages): show live npm downloads in highlights stats Add a useNpmDownloads hook that fetches real monthly download counts from the npm registry API at runtime, and surface them in the Highlights section, falling back to a static i18n value while loading or on error. Also refresh the stats row copy and ordering, and sync the label/caption/value changes across en, zh, ja and ru. The hook encodes the (scoped) package name into the request URL and aborts the fetch after a timeout so the UI degrades promptly on a slow or unresponsive network. * docs(pages): translate highlights npm-downloads comments to English * fix(pages): give stat3 a distinct caption and refactor npm-downloads hook - Give highlights stat3 its own caption instead of duplicating stat1's 'battle-tested' text across all four locales - Correct the Russian stat3 label to match 'real-world tasks' in the other locales - Refactor the npm-downloads fetch to async/await and clear the timeout in a finally block so it no longer lingers after the request settles
This commit is contained in:
parent
f44821d9aa
commit
3c0f00a765
6 changed files with 109 additions and 25 deletions
|
|
@ -4,6 +4,17 @@
|
|||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import { useTranslation } from '../i18n';
|
||||
import { useResponsive } from '../hooks/useResponsive';
|
||||
import { useNpmDownloads } from '../hooks/useNpmDownloads';
|
||||
|
||||
// npm package name, used to fetch real external download counts
|
||||
const NPM_PACKAGE = '@alibaba-group/open-code-review';
|
||||
|
||||
// Compress the download count into a compact format consistent with the other stats: 149176 -> "149K+"
|
||||
function formatNpmDownloads(n: number): string {
|
||||
if (n >= 1_000_000) return `${Math.floor(n / 1_000_000)}M+`;
|
||||
if (n >= 1_000) return `${Math.floor(n / 1_000)}K+`;
|
||||
return `${n}`;
|
||||
}
|
||||
|
||||
// 从字符串中解析数字和前后缀
|
||||
function parseStatValue(value: string): { prefix: string; number: number; suffix: string } {
|
||||
|
|
@ -57,6 +68,8 @@ const CountUpValue: React.FC<{ value: string; isVisible: boolean }> = ({ value,
|
|||
const HighlightsSection: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { isMobile, isTablet } = useResponsive();
|
||||
// Fetch live monthly npm downloads, reflecting real external community usage
|
||||
const npm = useNpmDownloads(NPM_PACKAGE, 'last-month');
|
||||
const sectionRef = useRef<HTMLDivElement>(null);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
|
|
@ -78,8 +91,13 @@ const HighlightsSection: React.FC = () => {
|
|||
|
||||
const stats = [
|
||||
{ value: t('highlights.stat1Value'), label: t('highlights.stat1Label'), caption: t('highlights.stat1Caption') },
|
||||
{ value: t('highlights.stat2Value'), label: t('highlights.stat2Label'), caption: t('highlights.stat2Caption') },
|
||||
{ value: t('highlights.stat3Value'), label: t('highlights.stat3Label'), caption: t('highlights.stat3Caption') },
|
||||
{
|
||||
// Live monthly npm downloads; fall back to the static i18n value while loading or on failure
|
||||
value: npm.downloads !== null ? formatNpmDownloads(npm.downloads) : t('highlights.stat2Value'),
|
||||
label: t('highlights.stat2Label'),
|
||||
caption: t('highlights.stat2Caption'),
|
||||
},
|
||||
{ value: t('highlights.stat4Value'), label: t('highlights.stat4Label'), caption: t('highlights.stat4Caption') },
|
||||
{ value: t('highlights.stat5Value'), label: t('highlights.stat5Label'), caption: t('highlights.stat5Caption') },
|
||||
];
|
||||
|
|
|
|||
66
pages/src/hooks/useNpmDownloads.ts
Normal file
66
pages/src/hooks/useNpmDownloads.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2026 alibaba/open-code-review Contributors
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
type Period = 'last-day' | 'last-week' | 'last-month' | 'last-year';
|
||||
|
||||
interface NpmDownloadsState {
|
||||
downloads: number | null;
|
||||
loading: boolean;
|
||||
error: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the live download count for an npm package.
|
||||
* The data source is the official npm stats API (CORS-enabled, callable directly from a purely static page).
|
||||
* When the request fails, `error` is true so callers can degrade gracefully.
|
||||
*
|
||||
* Currently used by the "NPM community downloads" stat in HighlightsSection: while the
|
||||
* request is in flight or has failed, the component falls back to the static i18n value.
|
||||
*/
|
||||
export function useNpmDownloads(pkg: string, period: Period = 'last-month'): NpmDownloadsState {
|
||||
const [state, setState] = useState<NpmDownloadsState>({
|
||||
downloads: null,
|
||||
loading: true,
|
||||
error: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setState({ downloads: null, loading: true, error: false });
|
||||
|
||||
// On a slow network or an unresponsive API, abort the request after a timeout and degrade,
|
||||
// so the UI does not stay stuck in the loading state indefinitely
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 8000);
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
// pkg may be a scoped package name (containing `/`), so encode it before interpolation to keep the URL path valid
|
||||
const r = await fetch(`https://api.npmjs.org/downloads/point/${period}/${encodeURIComponent(pkg)}`, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!r.ok) throw new Error(`npm downloads API responded ${r.status}`);
|
||||
const data: { downloads?: number } = await r.json();
|
||||
if (cancelled) return;
|
||||
if (typeof data.downloads !== 'number') throw new Error('unexpected payload');
|
||||
setState({ downloads: data.downloads, loading: false, error: false });
|
||||
} catch {
|
||||
if (cancelled) return;
|
||||
setState({ downloads: null, loading: false, error: true });
|
||||
} finally {
|
||||
// Clear the timeout as soon as the request settles, so it doesn't linger and abort a finished request
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearTimeout(timeout);
|
||||
controller.abort();
|
||||
};
|
||||
}, [pkg, period]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
|
@ -25,14 +25,14 @@ export const en = {
|
|||
|
||||
// Highlights
|
||||
'highlights.stat1Value': '20K+',
|
||||
'highlights.stat1Label': 'ACTIVE USERS',
|
||||
'highlights.stat1Label': 'INTERNAL ACTIVE USERS',
|
||||
'highlights.stat1Caption': 'Battle-tested inside Alibaba Group',
|
||||
'highlights.stat2Value': '> 30%',
|
||||
'highlights.stat2Label': 'ADOPTION RATE',
|
||||
'highlights.stat2Caption': 'Battle-tested inside Alibaba Group',
|
||||
'highlights.stat3Value': '1M+',
|
||||
'highlights.stat2Value': '150K+',
|
||||
'highlights.stat2Label': 'NPM COMMUNITY DOWNLOADS',
|
||||
'highlights.stat2Caption': 'Real npm downloads · last 30 days',
|
||||
'highlights.stat3Value': '3M+',
|
||||
'highlights.stat3Label': 'REAL-WORLD TASKS',
|
||||
'highlights.stat3Caption': 'Code review tasks executed',
|
||||
'highlights.stat3Caption': 'Code review tasks executed to date',
|
||||
'highlights.stat4Value': '1/9',
|
||||
'highlights.stat4Label': 'TOKEN COST',
|
||||
'highlights.stat4Caption': 'vs. Claude Code · 1,000 PRs',
|
||||
|
|
|
|||
|
|
@ -27,14 +27,14 @@ export const ja: TranslationKeys = {
|
|||
|
||||
// Highlights
|
||||
'highlights.stat1Value': '20K+',
|
||||
'highlights.stat1Label': 'アクティブユーザー',
|
||||
'highlights.stat1Label': '社内アクティブユーザー',
|
||||
'highlights.stat1Caption': 'Alibabaグループ内で実戦検証済み',
|
||||
'highlights.stat2Value': '> 30%',
|
||||
'highlights.stat2Label': '採用率',
|
||||
'highlights.stat2Caption': 'Alibabaグループ内で実戦検証済み',
|
||||
'highlights.stat3Value': '1M+',
|
||||
'highlights.stat2Value': '150K+',
|
||||
'highlights.stat2Label': 'NPM コミュニティダウンロード',
|
||||
'highlights.stat2Caption': 'npm 過去30日の実ダウンロード数',
|
||||
'highlights.stat3Value': '3M+',
|
||||
'highlights.stat3Label': '実タスク',
|
||||
'highlights.stat3Caption': '実行されたコードレビュータスク',
|
||||
'highlights.stat3Caption': '実行済みのコードレビュータスク',
|
||||
'highlights.stat4Value': '1/9',
|
||||
'highlights.stat4Label': 'トークンコスト',
|
||||
'highlights.stat4Caption': 'Claude Code との比較 · 1,000 PR',
|
||||
|
|
|
|||
|
|
@ -27,14 +27,14 @@ export const ru: TranslationKeys = {
|
|||
|
||||
// Highlights
|
||||
'highlights.stat1Value': '20K+',
|
||||
'highlights.stat1Label': 'АКТИВНЫХ ПОЛЬЗОВАТЕЛЕЙ',
|
||||
'highlights.stat1Label': 'ВНУТРЕННИЕ АКТИВНЫЕ ПОЛЬЗОВАТЕЛИ',
|
||||
'highlights.stat1Caption': 'в Alibaba Group',
|
||||
'highlights.stat2Value': '> 30%',
|
||||
'highlights.stat2Label': 'ЗАМЕЧАНИЙ ПРИНЯТО',
|
||||
'highlights.stat2Caption': 'в Alibaba Group',
|
||||
'highlights.stat3Value': '1M+',
|
||||
'highlights.stat3Label': 'ЗАДАЧ КОД-РЕВЬЮ',
|
||||
'highlights.stat3Caption': 'выполнено в реальных проектах',
|
||||
'highlights.stat2Value': '150K+',
|
||||
'highlights.stat2Label': 'ЗАГРУЗКИ СООБЩЕСТВА NPM',
|
||||
'highlights.stat2Caption': 'реальные загрузки npm · 30 дней',
|
||||
'highlights.stat3Value': '3M+',
|
||||
'highlights.stat3Label': 'РЕАЛЬНЫХ ЗАДАЧ',
|
||||
'highlights.stat3Caption': 'Выполненных задач код-ревью',
|
||||
'highlights.stat4Value': '1/9',
|
||||
'highlights.stat4Label': 'ОТ РАСХОДА ТОКЕНОВ',
|
||||
'highlights.stat4Caption': 'Claude Code · 1 000 PR',
|
||||
|
|
|
|||
|
|
@ -27,12 +27,12 @@ export const zh: TranslationKeys = {
|
|||
|
||||
// Highlights
|
||||
'highlights.stat1Value': '20K+',
|
||||
'highlights.stat1Label': '活跃用户',
|
||||
'highlights.stat1Label': '内部活跃用户',
|
||||
'highlights.stat1Caption': '经阿里巴巴集团内部实战验证',
|
||||
'highlights.stat2Value': '> 30%',
|
||||
'highlights.stat2Label': '采纳率',
|
||||
'highlights.stat2Caption': '经阿里巴巴集团内部实战验证',
|
||||
'highlights.stat3Value': '1M+',
|
||||
'highlights.stat2Value': '150K+',
|
||||
'highlights.stat2Label': 'NPM 社区下载量',
|
||||
'highlights.stat2Caption': 'npm 近 30 天真实下载',
|
||||
'highlights.stat3Value': '3M+',
|
||||
'highlights.stat3Label': '真实任务',
|
||||
'highlights.stat3Caption': '已执行的代码审查任务',
|
||||
'highlights.stat4Value': '1/9',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue