Skip to content

feat: 趋势页增强 - RSS数据源、描述字段、README预览、时间范围筛选#96

Merged
AmintaCCCP merged 3 commits intomainfrom
feature/trending-rss-enhancement
Apr 23, 2026
Merged

feat: 趋势页增强 - RSS数据源、描述字段、README预览、时间范围筛选#96
AmintaCCCP merged 3 commits intomainfrom
feature/trending-rss-enhancement

Conversation

@AmintaCCCP
Copy link
Copy Markdown
Owner

@AmintaCCCP AmintaCCCP commented Apr 23, 2026

变更内容

1. 趋势频道重命名

  • 中文名从「热门仓库」改为「趋势(Trending)」
  • 描述更新为「GitHub 趋势仓库,支持今日/本周/本月筛选」

2. RSS 数据源替换

  • 趋势页数据源从 GitHub Search API 改为 GitHubTrendingRSS
  • 支持三种时间范围筛选:
    • 📅 今日: daily/all.xml
    • 📅 本周: weekly/all.xml(默认)
    • 📅 本月: monthly/all.xml
  • 趋势页工具栏新增时间范围下拉选择器

3. RSS 描述解析

  • 解析 RSS <description> 中的 XML/HTML 标签
  • 使用 DOMParser + innerHTML 解码 HTML 实体并剥离标签
  • 只显示纯文本描述,不显示语法标签

4. GitHub API 字段补充

  • RSS 数据只包含基础信息(name, link, stars/forks from description)
  • 通过 GitHub API /repos/{owner}/{repo} 补充缺失字段:
    • id, language, description, topics, created_at, updated_at, pushed_at
    • 使用 GitHub API 的 description 替代 RSS 中的原始描述(更准确)

5. 仓库卡片描述字段

  • 已有功能:卡片显示仓库原始描述(repo.description
  • AI 分析后显示 AI 摘要(repo.ai_summary),带紫色 Bot 图标区分

6. README 预览

  • 已有功能:点击仓库卡片可预览 README.md
  • 复用仓库页的 ReadmeModal 组件,支持 Markdown 渲染、目录导航、字体大小切换

技术细节

  • 新增 TrendingTimeRange 类型(daily | weekly | monthly
  • 新增 store 状态 trendingTimeRange 和 action setTrendingTimeRange
  • 时间范围变更自动触发趋势数据刷新
  • RSS 解析支持分页(基于 RSS items 数量计算 totalCount 和 hasMore)

Summary by CodeRabbit

  • New Features
    • Added a time-range filter (Daily / Weekly / Monthly) for Trending repositories with a calendar icon in the toolbar.
    • Changing the time range reloads trending results from the first page (no appended results) so you see fresh, relevant lists immediately.
    • Repository cards now always open the README modal on click and show descriptions/tags/platform icons across devices.

1. 趋势频道名称从'热门仓库(Trending)'改为'趋势(Trending)'
2. 趋势页数据源改为 GitHubTrendingRSS,支持今日/本周/本月筛选
   - 今日: https://mshibanami.github.io/GitHubTrendingRSS/daily/all.xml
   - 本周: https://mshibanami.github.io/GitHubTrendingRSS/weekly/all.xml
   - 本月: https://mshibanami.github.io/GitHubTrendingRSS/monthly/all.xml
3. RSS描述字段解析:剥离XML/HTML标签,仅显示纯文本描述
4. 通过GitHub API补充RSS缺失字段(id, language, description, topics等)
5. 仓库卡片已支持描述字段显示(原始描述+AI分析摘要)
6. 点击仓库卡片可预览README.md,复用仓库页的Markdown渲染组件
7. 新增TrendingTimeRange类型和store状态管理
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

📝 Walkthrough

Walkthrough

Adds a selectable trending time range (daily/weekly/monthly) to state and UI, threads it into the trending data fetch path, switches trending fetch to GitHubTrendingRSS with RSS parsing and supplemental /repos lookups, and triggers a non-appended reload when the time range changes.

Changes

Cohort / File(s) Summary
Type Definitions
src/types/index.ts
Add `TrendingTimeRange = 'daily'
Store Management
src/store/useAppStore.ts
Introduce trendingTimeRange (default 'weekly'), persist/rehydrate it, add setTrendingTimeRange action, and update trending channel metadata (Chinese name/description).
API Integration
src/services/githubApi.ts
Change getTrendingRepositories(..., timeRange) signature; replace GitHub Search calls with GitHubTrendingRSS feed fetch + XML parsing, derive owner/name and stars/forks from RSS, construct DiscoveryRepo entries, and throttle supplemental /repos/{owner}/{repo} calls to fill missing fields; recalc pagination and handle RSS failures.
UI Components
src/components/DiscoveryView.tsx
Add trending time-range dropdown (daily/weekly/monthly) with Calendar icon; pass trendingTimeRange and fixed perPage=20 into getTrendingRepositories; update refreshChannel dependencies and add effect to reload page 1 (replace results) when time range changes while trending active.
Subscription Card
src/components/SubscriptionRepoCard.tsx
Always open README modal on card click (removed desktopSafeMode gating); simplify ReadmeModal rendering and click handler dependencies.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant UI as DiscoveryView (UI)
    participant Store as useAppStore
    participant API as GitHubApiService
    participant RSS as GitHubTrendingRSS

    User->>UI: Select time range (daily/weekly/monthly)
    UI->>Store: setTrendingTimeRange(range)
    Store->>Store: Update trendingTimeRange
    Store-->>UI: State updated
    UI->>UI: Effect detects trendingTimeRange change
    UI->>API: getTrendingRepositories(platform='trending', page=1, perPage=20, timeRange)
    API->>RSS: Fetch RSS feed for selected timeRange
    RSS-->>API: Return feed items
    API->>API: Parse items -> construct DiscoveryRepo list
    API->>API: Throttled calls to /repos/{owner}/{repo} for missing fields
    API-->>UI: Return paginated trending results
    UI->>User: Render updated trending repositories
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through feeds both near and far,
Picked daily, weekly, monthly from a jar,
Parsed RSS crumbs with eager paws,
Fetched missing bits with gentle pause,
Now trending hops to tune-of-your-choice stars ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes all major changes: RSS data source, description field parsing, README preview, and time range filtering—covering the main objectives of the trending page enhancement.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/trending-rss-enhancement

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/components/DiscoveryView.tsx (1)

2-39: ⚠️ Potential issue | 🔴 Critical

Add missing Calendar and TrendingTimeRange imports.

Both symbols are used in the new time-range dropdown (lines 1068, 1071) but not imported, causing TypeScript compilation to fail.

Import fix
   Globe,
-  X
+  X,
+  Calendar
 } from 'lucide-react';
@@
   SortBy,
   SortOrder,
-  TopicCategory 
+  TopicCategory,
+  TrendingTimeRange
 } from '../types';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/DiscoveryView.tsx` around lines 2 - 39, The build fails
because the DiscoveryView component uses the Calendar icon and the
TrendingTimeRange type in the time-range dropdown but they were not imported;
add Calendar to the lucide-react import list (alongside RefreshCw, TrendingUp,
etc.) and import TrendingTimeRange from the types module (alongside
DiscoveryChannelId, DiscoveryRepo, SortBy, etc.) so both symbols (Calendar and
TrendingTimeRange) are available to the dropdown logic in DiscoveryView.
src/services/githubApi.ts (1)

1-16: ⚠️ Potential issue | 🔴 Critical

Import TrendingTimeRange and DiscoveryRepo from ../types.

These types are used on lines 497-499 and 518 but are not imported, causing TypeScript compilation to fail.

Fix
import { 
  Repository, 
  Release, 
  GitHubUser, 
  DiscoveryPlatform, 
+  DiscoveryRepo,
  ProgrammingLanguage, 
  SortBy, 
  SortOrder, 
  PaginatedDiscoveryRepositories,
  DiscoveryChannelId,
+  TrendingTimeRange,
  TopicCategory,
  SubscriptionRepo,
  SubscriptionDev,
  GitHubSearchUserResponse,
  GitHubUserDetail
} from '../types';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/githubApi.ts` around lines 1 - 16, The file's top import block
in src/services/githubApi.ts is missing two types used later (TrendingTimeRange
and DiscoveryRepo); update the import list that currently includes Repository,
Release, GitHubUser, etc. to also import TrendingTimeRange and DiscoveryRepo
from '../types' so references to TrendingTimeRange (used around the
trending-related functions) and DiscoveryRepo (used where discovery repositories
are typed) resolve and TypeScript compiles.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/DiscoveryView.tsx`:
- Around line 760-765: The useEffect that reacts to trendingTimeRange changes
calls refreshChannel('trending', 1, false) but does not reset the pagination
state, so currentPage remains at the previous value; update the effect to also
reset the pagination state (e.g., call setCurrentPage(1) or the component's
pagination state setter) before/when invoking refreshChannel so the UI uses page
1 results for the new trendingTimeRange; locate the useEffect that references
trendingTimeRange, selectedDiscoveryChannel and refreshChannel and add the
currentPage reset there (or modify refreshChannel to accept a flag that resets
currentPage internally).

In `@src/services/githubApi.ts`:
- Around line 548-609: The supplemental-update block leaves r.id as 0 on API
failure and spawns all requests at once; change
Promise.all(reposNeedUpdate.map(...)) to a sequential or concurrency-limited
loop (e.g., for...of with await or use a p-limit) so the await setTimeout(...)
actually throttles requests, and inside the catch ensure you assign a stable,
unique fallback id instead of 0 (e.g., set r.id to a generated negative or
composite id using r.rank, channel or a monotonic counter) so React keys/store
updates won't break; keep the rest of the makeRequest(...) handling and the
existing field fallbacks but move the 80ms delay into the sequential flow so it
limits bursts.
- Around line 527-535: The current RSS description decoding uses a temporary DOM
node and tempDiv.innerHTML which risks executing untrusted markup; replace that
logic by feeding the raw description string (from descriptionEl?.textContent)
into a standalone DOMParser (parseFromString(..., 'text/html')) and extract the
safe text via the parsedDocument.body.textContent (or textContent fallback) to
decode entities and strip tags without touching document.innerHTML; update the
block around descriptionEl, tempDiv and description to use DOMParser and remove
creation/appending of any DOM nodes.

---

Outside diff comments:
In `@src/components/DiscoveryView.tsx`:
- Around line 2-39: The build fails because the DiscoveryView component uses the
Calendar icon and the TrendingTimeRange type in the time-range dropdown but they
were not imported; add Calendar to the lucide-react import list (alongside
RefreshCw, TrendingUp, etc.) and import TrendingTimeRange from the types module
(alongside DiscoveryChannelId, DiscoveryRepo, SortBy, etc.) so both symbols
(Calendar and TrendingTimeRange) are available to the dropdown logic in
DiscoveryView.

In `@src/services/githubApi.ts`:
- Around line 1-16: The file's top import block in src/services/githubApi.ts is
missing two types used later (TrendingTimeRange and DiscoveryRepo); update the
import list that currently includes Repository, Release, GitHubUser, etc. to
also import TrendingTimeRange and DiscoveryRepo from '../types' so references to
TrendingTimeRange (used around the trending-related functions) and DiscoveryRepo
(used where discovery repositories are typed) resolve and TypeScript compiles.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f8d00e10-8227-426b-9337-676727b37cf1

📥 Commits

Reviewing files that changed from the base of the PR and between 5ef0131 and a2ebfee.

📒 Files selected for processing (4)
  • src/components/DiscoveryView.tsx
  • src/services/githubApi.ts
  • src/store/useAppStore.ts
  • src/types/index.ts

Comment on lines +760 to +765
// 趋势时间范围改变时刷新数据
useEffect(() => {
if (selectedDiscoveryChannel === 'trending' && trendingTimeRange) {
refreshChannel('trending', 1, false);
}
}, [trendingTimeRange, selectedDiscoveryChannel, refreshChannel]);
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.

⚠️ Potential issue | 🟠 Major

Reset pagination when the trending range changes.

This fetches page 1 for the new range but leaves currentPage unchanged. If the user switches ranges while on page 2+, the UI slices the new page-1 results from the old page offset and can render an empty page.

🐛 Proposed pagination fix
   // 趋势时间范围改变时刷新数据
   useEffect(() => {
     if (selectedDiscoveryChannel === 'trending' && trendingTimeRange) {
+      setCurrentPage(1);
       refreshChannel('trending', 1, false);
     }
   }, [trendingTimeRange, selectedDiscoveryChannel, refreshChannel]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/DiscoveryView.tsx` around lines 760 - 765, The useEffect that
reacts to trendingTimeRange changes calls refreshChannel('trending', 1, false)
but does not reset the pagination state, so currentPage remains at the previous
value; update the effect to also reset the pagination state (e.g., call
setCurrentPage(1) or the component's pagination state setter) before/when
invoking refreshChannel so the UI uses page 1 results for the new
trendingTimeRange; locate the useEffect that references trendingTimeRange,
selectedDiscoveryChannel and refreshChannel and add the currentPage reset there
(or modify refreshChannel to accept a flag that resets currentPage internally).

Comment thread src/services/githubApi.ts
Comment on lines +527 to +535
// Parse description - strip XML/HTML tags
const descriptionEl = item.querySelector('description');
let description = descriptionEl?.textContent || '';
// Decode HTML entities and strip HTML tags
const tempDiv = document.createElement('div');
tempDiv.innerHTML = description;
description = tempDiv.textContent || tempDiv.innerText || '';
// Clean up extra whitespace
description = description.replace(/\s+/g, ' ').trim();
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.

⚠️ Potential issue | 🟠 Major

Avoid innerHTML for external RSS content decoding.

The RSS description is untrusted external content. Using innerHTML creates a DOM node that could execute scripts if the feed is compromised. Use DOMParser with a sandboxed context to safely decode HTML entities and extract text content instead.

🛡️ Safer decode approach
-        // Decode HTML entities and strip HTML tags
-        const tempDiv = document.createElement('div');
-        tempDiv.innerHTML = description;
-        description = tempDiv.textContent || tempDiv.innerText || '';
+        // Decode HTML entities and strip HTML tags without assigning untrusted markup to the live DOM
+        const decodedDoc = new DOMParser().parseFromString(description, 'text/html');
+        description = decodedDoc.body.textContent || '';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Parse description - strip XML/HTML tags
const descriptionEl = item.querySelector('description');
let description = descriptionEl?.textContent || '';
// Decode HTML entities and strip HTML tags
const tempDiv = document.createElement('div');
tempDiv.innerHTML = description;
description = tempDiv.textContent || tempDiv.innerText || '';
// Clean up extra whitespace
description = description.replace(/\s+/g, ' ').trim();
// Parse description - strip XML/HTML tags
const descriptionEl = item.querySelector('description');
let description = descriptionEl?.textContent || '';
// Decode HTML entities and strip HTML tags without assigning untrusted markup to the live DOM
const decodedDoc = new DOMParser().parseFromString(description, 'text/html');
description = decodedDoc.body.textContent || '';
// Clean up extra whitespace
description = description.replace(/\s+/g, ' ').trim();
🧰 Tools
🪛 ast-grep (0.42.1)

[warning] 531-531: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: tempDiv.innerHTML = description
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html

(unsafe-html-content-assignment)


[warning] 531-531: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: tempDiv.innerHTML = description
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/githubApi.ts` around lines 527 - 535, The current RSS
description decoding uses a temporary DOM node and tempDiv.innerHTML which risks
executing untrusted markup; replace that logic by feeding the raw description
string (from descriptionEl?.textContent) into a standalone DOMParser
(parseFromString(..., 'text/html')) and extract the safe text via the
parsedDocument.body.textContent (or textContent fallback) to decode entities and
strip tags without touching document.innerHTML; update the block around
descriptionEl, tempDiv and description to use DOMParser and remove
creation/appending of any DOM nodes.

Comment thread src/services/githubApi.ts
Comment on lines +548 to +609
repos.push({
id: 0, // will be filled by GitHub API
name: repoName,
full_name: `${owner}/${repoName}`,
description: description,
html_url: link,
stargazers_count: stars,
forks_count: forks,
forks: forks,
language: null,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
pushed_at: new Date().toISOString(),
owner: {
login: owner,
avatar_url: `https://github.com/${owner}.png`,
},
topics: [],
rank: i + 1,
channel: 'trending' as DiscoveryChannelId,
platform,
});
}

// Supplement missing fields via GitHub API
const reposNeedUpdate = repos.filter(r => r.id === 0 || r.stargazers_count === 0 || r.forks_count === 0 || !r.language);
if (reposNeedUpdate.length > 0) {
await Promise.all(reposNeedUpdate.map(async (r) => {
try {
const [owner, repo] = r.full_name.split('/');
if (!owner || !repo) return;
const data = await this.makeRequest<{
id: number;
stargazers_count: number;
forks_count: number;
forks: number;
language: string | null;
description: string | null;
topics: string[];
created_at: string;
updated_at: string;
pushed_at: string;
}>(`/repos/${owner}/${repo}`);
r.id = data.id;
r.stargazers_count = data.stargazers_count ?? r.stargazers_count;
r.forks_count = data.forks_count ?? r.forks_count;
r.forks = data.forks ?? r.forks;
r.language = data.language ?? r.language;
r.topics = data.topics ?? r.topics;
r.created_at = data.created_at ?? r.created_at;
r.updated_at = data.updated_at ?? r.updated_at;
r.pushed_at = data.pushed_at ?? r.pushed_at;
// Use GitHub API description as fallback (RSS description may contain emoji markers)
if (data.description) {
r.description = data.description;
}
} catch (e) {
console.warn(`Failed to fetch repo details for ${r.full_name}:`, e);
}
// Avoid GitHub API rate limiting
await new Promise(resolve => setTimeout(resolve, 80));
}));
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.

⚠️ Potential issue | 🟠 Major

Keep fallback IDs unique and actually throttle supplemental GitHub calls.

If /repos/{owner}/{repo} fails, every repo keeps id: 0, which breaks React keys and store updates by id. Also, Promise.all starts all requests immediately, so the delay at Line 608 does not throttle request bursts.

🐛 Proposed reliability fix
         repos.push({
-          id: 0, // will be filled by GitHub API
+          id: -(i + 1), // temporary unique id; replaced by GitHub API when enrichment succeeds
           name: repoName,
           full_name: `${owner}/${repoName}`,
@@
-      const reposNeedUpdate = repos.filter(r => r.id === 0 || r.stargazers_count === 0 || r.forks_count === 0 || !r.language);
+      const reposNeedUpdate = repos.filter(r => r.id < 0 || r.stargazers_count === 0 || r.forks_count === 0 || !r.language);
       if (reposNeedUpdate.length > 0) {
-        await Promise.all(reposNeedUpdate.map(async (r) => {
+        for (const r of reposNeedUpdate) {
           try {
             const [owner, repo] = r.full_name.split('/');
-            if (!owner || !repo) return;
+            if (!owner || !repo) continue;
             const data = await this.makeRequest<{
               id: number;
@@
           } catch (e) {
             console.warn(`Failed to fetch repo details for ${r.full_name}:`, e);
           }
           // Avoid GitHub API rate limiting
           await new Promise(resolve => setTimeout(resolve, 80));
-        }));
+        }
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
repos.push({
id: 0, // will be filled by GitHub API
name: repoName,
full_name: `${owner}/${repoName}`,
description: description,
html_url: link,
stargazers_count: stars,
forks_count: forks,
forks: forks,
language: null,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
pushed_at: new Date().toISOString(),
owner: {
login: owner,
avatar_url: `https://github.com/${owner}.png`,
},
topics: [],
rank: i + 1,
channel: 'trending' as DiscoveryChannelId,
platform,
});
}
// Supplement missing fields via GitHub API
const reposNeedUpdate = repos.filter(r => r.id === 0 || r.stargazers_count === 0 || r.forks_count === 0 || !r.language);
if (reposNeedUpdate.length > 0) {
await Promise.all(reposNeedUpdate.map(async (r) => {
try {
const [owner, repo] = r.full_name.split('/');
if (!owner || !repo) return;
const data = await this.makeRequest<{
id: number;
stargazers_count: number;
forks_count: number;
forks: number;
language: string | null;
description: string | null;
topics: string[];
created_at: string;
updated_at: string;
pushed_at: string;
}>(`/repos/${owner}/${repo}`);
r.id = data.id;
r.stargazers_count = data.stargazers_count ?? r.stargazers_count;
r.forks_count = data.forks_count ?? r.forks_count;
r.forks = data.forks ?? r.forks;
r.language = data.language ?? r.language;
r.topics = data.topics ?? r.topics;
r.created_at = data.created_at ?? r.created_at;
r.updated_at = data.updated_at ?? r.updated_at;
r.pushed_at = data.pushed_at ?? r.pushed_at;
// Use GitHub API description as fallback (RSS description may contain emoji markers)
if (data.description) {
r.description = data.description;
}
} catch (e) {
console.warn(`Failed to fetch repo details for ${r.full_name}:`, e);
}
// Avoid GitHub API rate limiting
await new Promise(resolve => setTimeout(resolve, 80));
}));
repos.push({
id: -(i + 1), // temporary unique id; replaced by GitHub API when enrichment succeeds
name: repoName,
full_name: `${owner}/${repoName}`,
description: description,
html_url: link,
stargazers_count: stars,
forks_count: forks,
forks: forks,
language: null,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
pushed_at: new Date().toISOString(),
owner: {
login: owner,
avatar_url: `https://github.com/${owner}.png`,
},
topics: [],
rank: i + 1,
channel: 'trending' as DiscoveryChannelId,
platform,
});
}
// Supplement missing fields via GitHub API
const reposNeedUpdate = repos.filter(r => r.id < 0 || r.stargazers_count === 0 || r.forks_count === 0 || !r.language);
if (reposNeedUpdate.length > 0) {
for (const r of reposNeedUpdate) {
try {
const [owner, repo] = r.full_name.split('/');
if (!owner || !repo) continue;
const data = await this.makeRequest<{
id: number;
stargazers_count: number;
forks_count: number;
forks: number;
language: string | null;
description: string | null;
topics: string[];
created_at: string;
updated_at: string;
pushed_at: string;
}>(`/repos/${owner}/${repo}`);
r.id = data.id;
r.stargazers_count = data.stargazers_count ?? r.stargazers_count;
r.forks_count = data.forks_count ?? r.forks_count;
r.forks = data.forks ?? r.forks;
r.language = data.language ?? r.language;
r.topics = data.topics ?? r.topics;
r.created_at = data.created_at ?? r.created_at;
r.updated_at = data.updated_at ?? r.updated_at;
r.pushed_at = data.pushed_at ?? r.pushed_at;
// Use GitHub API description as fallback (RSS description may contain emoji markers)
if (data.description) {
r.description = data.description;
}
} catch (e) {
console.warn(`Failed to fetch repo details for ${r.full_name}:`, e);
}
// Avoid GitHub API rate limiting
await new Promise(resolve => setTimeout(resolve, 80));
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/githubApi.ts` around lines 548 - 609, The supplemental-update
block leaves r.id as 0 on API failure and spawns all requests at once; change
Promise.all(reposNeedUpdate.map(...)) to a sequential or concurrency-limited
loop (e.g., for...of with await or use a p-limit) so the await setTimeout(...)
actually throttles requests, and inside the catch ensure you assign a stable,
unique fallback id instead of 0 (e.g., set r.id to a generated negative or
composite id using r.rank, channel or a monotonic counter) so React keys/store
updates won't break; keep the rest of the makeRequest(...) handling and the
existing field fallbacks but move the 80ms delay into the sequential flow so it
limits bursts.

Clawd added 2 commits April 23, 2026 20:07
- 补充 DiscoveryView 中遗漏的 Calendar lucide-react 导入(修复运行时 Calendar is not defined 报错)
- 趋势频道中文名从'趋势(Trending)'修正为'趋势',英文名保持'Trending'
1. 修复发现频道迁移逻辑,强制使用默认name/nameEn/description
   - 只保留用户enabled状态,其他字段从defaultChannel获取
   - 解决旧数据'name: 热门仓库'覆盖新name问题

2. 修复SubscriptionRepoCard在desktopSafeMode下的功能
   - 移除描述/AI摘要/标签的desktopSafeMode限制
   - 卡片点击打开README不再受desktopSafeMode限制
   - ReadmeModal在desktop模式下也可正常渲染
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/components/SubscriptionRepoCard.tsx (1)

282-293: Desktop mode: clickable card without visual affordance.

Now that handleCardClick always opens the README modal regardless of desktopSafeMode, the card is clickable in desktop mode but still renders without cursor-pointer or hover styles (lines 285–288). Consider also applying cursor-pointer in the desktopSafeMode branch so users get a visual hint that the card is interactive.

💡 Proposed tweak
       className={`bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 p-5 transition-shadow duration-200 ${
         desktopSafeMode
-          ? 'rounded-lg'
+          ? 'rounded-lg cursor-pointer'
           : 'rounded-xl hover:shadow-lg hover:border-blue-300 dark:hover:border-blue-600 cursor-pointer'
       }`}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/SubscriptionRepoCard.tsx` around lines 282 - 293, The card is
clickable because handleCardClick always opens the README modal, but when
desktopSafeMode is true the class branch omits cursor-pointer/hover affordances;
update the className logic for the container (where desktopSafeMode is checked)
to include visual affordances (at minimum add cursor-pointer and a subtle hover
style) in the desktopSafeMode branch so users know the card is interactive;
ensure you modify the same JSX element that sets className (the div using
desktopSafeMode) so both branches present consistent click affordance when
handleCardClick is enabled.
src/store/useAppStore.ts (1)

203-246: trendingTimeRange resets to 'weekly' on every reload.

trendingTimeRange is absent from both PersistedAppState (Pick at lines 203–246) and partialize (lines 1230–1302), so it is never written to storage. In addition, normalizePersistedState unconditionally sets it to 'weekly' at line 410 (placed after ...safePersisted), so even if a value were ever present in storage it would be discarded on merge. Net effect: a user's daily/monthly selection is lost on every page reload / app restart, while other discovery filters (discoveryPlatform, discoveryLanguage, discoverySortBy, discoverySortOrder, …) are persisted. Confirm whether session-only behavior is intentional; if not, add it to the persisted keys and stop hard-overriding it in normalize.

♻️ Proposed fix (if persistence is desired)
 type PersistedAppState = Partial<
   Pick<
     AppState,
     ...
     | 'discoverySortBy'
     | 'discoverySortOrder'
+    | 'trendingTimeRange'
     | 'subscriptionRepos'
     ...
   >
 > & {
-    trendingTimeRange: 'weekly' as TrendingTimeRange,
+    trendingTimeRange: (['daily', 'weekly', 'monthly'] as const).includes(
+      safePersisted.trendingTimeRange as TrendingTimeRange
+    )
+      ? (safePersisted.trendingTimeRange as TrendingTimeRange)
+      : 'weekly',
       discoverySortOrder: state.discoverySortOrder,
       discoverySelectedTopic: state.discoverySelectedTopic,
+      trendingTimeRange: state.trendingTimeRange,
       }),

Also applies to: 410-410, 664-664, 1230-1302

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/store/useAppStore.ts` around lines 203 - 246, trendingTimeRange is
missing from the persisted keys and is being force-reset in
normalizePersistedState, so user selection is never stored; to fix, add
'trendingTimeRange' to the PersistedAppState Pick and include it in the
partialize persistence whitelist (the partialize function used for storage), and
remove the unconditional assignment that sets trendingTimeRange = 'weekly'
inside normalizePersistedState (or only set that default when no value exists)
so stored values are merged correctly; update references to trendingTimeRange in
normalizePersistedState, partialize, and the PersistedAppState type to ensure it
persists across reloads.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/components/SubscriptionRepoCard.tsx`:
- Around line 282-293: The card is clickable because handleCardClick always
opens the README modal, but when desktopSafeMode is true the class branch omits
cursor-pointer/hover affordances; update the className logic for the container
(where desktopSafeMode is checked) to include visual affordances (at minimum add
cursor-pointer and a subtle hover style) in the desktopSafeMode branch so users
know the card is interactive; ensure you modify the same JSX element that sets
className (the div using desktopSafeMode) so both branches present consistent
click affordance when handleCardClick is enabled.

In `@src/store/useAppStore.ts`:
- Around line 203-246: trendingTimeRange is missing from the persisted keys and
is being force-reset in normalizePersistedState, so user selection is never
stored; to fix, add 'trendingTimeRange' to the PersistedAppState Pick and
include it in the partialize persistence whitelist (the partialize function used
for storage), and remove the unconditional assignment that sets
trendingTimeRange = 'weekly' inside normalizePersistedState (or only set that
default when no value exists) so stored values are merged correctly; update
references to trendingTimeRange in normalizePersistedState, partialize, and the
PersistedAppState type to ensure it persists across reloads.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2421bb49-34bd-4e04-ba68-87f1235b91e0

📥 Commits

Reviewing files that changed from the base of the PR and between c709b76 and 4754a91.

📒 Files selected for processing (2)
  • src/components/SubscriptionRepoCard.tsx
  • src/store/useAppStore.ts

@AmintaCCCP AmintaCCCP merged commit 16d18db into main Apr 23, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant