Skip to content
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
20 changes: 19 additions & 1 deletion pages/src/components/HighlightsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 } {
Expand Down Expand Up @@ -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);

Expand All @@ -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') },
];
Expand Down
66 changes: 66 additions & 0 deletions pages/src/hooks/useNpmDownloads.ts
Original file line number Diff line number Diff line change
@@ -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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · medium]
Resource leak: setTimeout not cleared on fetch completion. The 8-second timeout is only cleared in the effect cleanup function. If the fetch resolves or rejects well before the timeout, the timer keeps running and will call controller.abort() on an already-finished request. While functionally harmless, it's a minor resource leak. Clear the timeout in both success and error paths (or in a finally block).


(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;
}
12 changes: 6 additions & 6 deletions pages/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
12 changes: 6 additions & 6 deletions pages/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
14 changes: 7 additions & 7 deletions pages/src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
10 changes: 5 additions & 5 deletions pages/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down