Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 包名,用于拉取外部真实下载量
const NPM_PACKAGE = '@alibaba-group/open-code-review';

// 将下载量压缩为紧凑格式,与其它指标风格一致: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();
// 实时拉取 npm 月下载量,代表外部社区真实使用量
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') },
{
// 实时 npm 月下载量;请求未完成或失败时回退到 i18n 中的兜底静态值
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
63 changes: 63 additions & 0 deletions pages/src/hooks/useNpmDownloads.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// 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;
}

/**
* 实时获取某个 npm 包的下载量。
* 数据源为 npm 官方统计 API(支持 CORS,可在纯静态页面直接调用)。
* 请求失败时 error 为 true,调用方可据此优雅降级。
*
* 当前用于 HighlightsSection 的「NPM 社区下载量」指标:请求进行中或失败时,
* 组件回退到 i18n 中的兜底静态值。
*/
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 });

// 弱网或 API 无响应时,超时后 abort 请求并降级,避免界面长期卡在 loading
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).


// pkg 可能是 scoped 包名(含 `/`),编码后再插值以保证 URL 路径合法
fetch(`https://api.npmjs.org/downloads/point/${period}/${encodeURIComponent(pkg)}`, {

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.

[maintainability · medium]
Hardcoded API URL. The npm API base URL https://api.npmjs.org/downloads/point/ is hardcoded directly in the fetch call. Per project coding standards, hardcoded URL paths (especially for API endpoints) should be extracted into a configuration constant (e.g., const NPM_API_BASE = '...') or environment variable. This makes the endpoint easier to swap for testing, staging, or if the URL changes.

Suggestion:

Suggested change
fetch(`https://api.npmjs.org/downloads/point/${period}/${encodeURIComponent(pkg)}`, {
fetch(`${NPM_API_BASE}/${period}/${encodeURIComponent(pkg)}`, {

signal: controller.signal,
})
.then((r) => {
if (!r.ok) throw new Error(`npm downloads API responded ${r.status}`);
return r.json();
})
.then((data: { downloads?: number }) => {
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 });
});

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.

[maintainability · medium]
Prefer async/await over Promise chains. The project coding standards require preferring async/await over raw .then()/.catch() chains. Other async code in this project (e.g., HeroSection.tsx, MarkdownRenderer.tsx) consistently uses async/await. Refactoring to an async IIFE with try/catch would improve readability and align with codebase conventions.

Suggestion:

Suggested change
fetch(`https://api.npmjs.org/downloads/point/${period}/${encodeURIComponent(pkg)}`, {
signal: controller.signal,
})
.then((r) => {
if (!r.ok) throw new Error(`npm downloads API responded ${r.status}`);
return r.json();
})
.then((data: { downloads?: number }) => {
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 });
});
(async () => {
try {
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 {
if (!cancelled) 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': 'Battle-tested inside Alibaba Group',

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.

[maintainability · medium]
The caption for stat3 is now "Battle-tested inside Alibaba Group", which is identical to stat1Caption. Having two stats display the exact same caption text on the page will look like a copy-paste mistake to users. Consider giving stat3 a distinct caption that reflects what "REAL-WORLD TASKS" measures (e.g., something about code review tasks executed, or a different description of scale).

'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': 'Alibabaグループ内で実戦検証済み',

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.

[maintainability · low]
The display order of stats (stat1, stat3, stat2, stat4, stat5) is now different from the key numbering (stat1, stat2, stat3, stat4, stat5). This can be confusing for future maintainers who may expect the keys to correspond to display order. Consider either:

  1. Renumbering the i18n keys to match the display order, or
  2. Adding a comment explaining why stat2 (npm downloads) is intentionally placed after stat3.

Also, note that highlights.stat1Caption and highlights.stat3Caption now have identical values ("Alibabaグループ内で実戦検証済み" / "Battle-tested inside Alibaba Group"). If this is intentional, it's fine, but it could also be an opportunity to deduplicate into a single key or give stat3 a more specific caption.

'highlights.stat4Value': '1/9',
'highlights.stat4Label': 'トークンコスト',
'highlights.stat4Caption': 'Claude Code との比較 · 1,000 PR',
Expand Down
12 changes: 6 additions & 6 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.stat2Value': '150K+',
'highlights.stat2Label': 'ЗАГРУЗКИ СООБЩЕСТВА NPM',
'highlights.stat2Caption': 'реальные загрузки npm · 30 дней',
'highlights.stat3Value': '3M+',
'highlights.stat3Label': 'ЗАДАЧ КОД-РЕВЬЮ',

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.

[other · medium]
Translation inconsistency: 'ЗАДАЧ КОД-РЕВЬЮ' means 'CODE REVIEW TASKS' in Russian, but the English source is 'REAL-WORLD TASKS' (zh: '真实任务', ja: '実タスク'). Consider updating to something like 'РЕАЛЬНЫХ ЗАДАЧ' to be consistent with the other languages.

Suggestion:

Suggested change
'highlights.stat3Label': 'ЗАДАЧ КОД-РЕВЬЮ',
'highlights.stat3Label': 'РЕАЛЬНЫХ ЗАДАЧ',

'highlights.stat3Caption': 'выполнено в реальных проектах',
'highlights.stat3Caption': 'в Alibaba Group',
'highlights.stat4Value': '1/9',
'highlights.stat4Label': 'ОТ РАСХОДА ТОКЕНОВ',
'highlights.stat4Caption': 'Claude Code · 1 000 PR',
Expand Down
12 changes: 6 additions & 6 deletions pages/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ 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.stat3Caption': '经阿里巴巴集团内部实战验证',

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]
Potential copy-paste issue: stat3Caption is now identical to stat1Caption ("经阿里巴巴集团内部实战验证"). This means the same caption text will appear twice on the page — once under "内部活跃用户" (stat1) and again under "真实任务" (stat3).

Previously, stat3Caption had a unique value ("已执行的代码审查任务") that described the stat. It seems likely that stat3Caption should have its own distinct caption rather than duplicating stat1Caption. Please verify this is intentional, or provide a unique caption for stat3.

'highlights.stat4Value': '1/9',
'highlights.stat4Label': 'TOKEN 成本',
'highlights.stat4Caption': '对比 Claude Code · 1,000 个 PR',
Expand Down
Loading