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
5 changes: 3 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/png" href="/icon.png" />
<link rel="apple-touch-icon" href="/icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GitHub Stars Manager - AI-Powered Repository Management</title>
<meta name="description" content="Intelligent management of your GitHub starred repositories with AI-powered analysis and release tracking" />
Expand All @@ -15,4 +16,4 @@
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
</html>
6 changes: 4 additions & 2 deletions nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ events {
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
resolver 127.0.0.11 ipv6=off valid=30s;

# Hide nginx version
server_tokens off;
Expand Down Expand Up @@ -32,7 +33,8 @@ http {
server_name localhost;
# Backend API proxy
location /api/ {
proxy_pass http://backend:3000/api/;
set $backend_upstream backend:3000;
proxy_pass http://$backend_upstream;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
Expand Down Expand Up @@ -86,4 +88,4 @@ http {
internal;
}
}
}
}
8 changes: 4 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import { LoginScreen } from './components/LoginScreen';
import { Header } from './components/Header';
import { SearchBar } from './components/SearchBar';
Expand All @@ -16,13 +16,13 @@ function App() {
const {
isAuthenticated,
currentView,
selectedCategory,
theme,
searchResults,
repositories
repositories,
setSelectedCategory,
} = useAppStore();

const [selectedCategory, setSelectedCategory] = useState('all');

// 自动检查更新
useAutoUpdateCheck();

Expand Down
12 changes: 8 additions & 4 deletions src/components/LoginScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { Star, Github, Key, ArrowRight, AlertCircle } from 'lucide-react';
import { Github, Key, ArrowRight, AlertCircle } from 'lucide-react';
import { useAppStore } from '../store/useAppStore';
import { GitHubApiService } from '../services/githubApi';

Expand Down Expand Up @@ -67,8 +67,12 @@ export const LoginScreen: React.FC = () => {
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4">
<div className="max-w-md w-full">
<div className="text-center mb-8">
<div className="flex items-center justify-center w-16 h-16 bg-blue-600 rounded-2xl mx-auto mb-4 shadow-lg">
<Star className="w-8 h-8 text-white" />
<div className="flex items-center justify-center w-16 h-16 bg-white rounded-2xl mx-auto mb-4 shadow-lg ring-1 ring-blue-100 overflow-hidden">
<img
src="./icon.png"
alt="GitHub Stars Manager"
className="w-full h-full object-cover"
Comment on lines +71 to +74
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

Use a base-aware icon path to avoid broken logo URLs.

Line 72 uses a relative path ("./icon.png"). On non-root paths this can resolve incorrectly and 404. Prefer a base-aware absolute asset path.

🔧 Proposed fix
-            <img
-              src="./icon.png"
+            <img
+              src={`${import.meta.env.BASE_URL}icon.png`}
               alt="GitHub Stars Manager"
               className="w-full h-full object-cover"
             />
📝 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
<img
src="./icon.png"
alt="GitHub Stars Manager"
className="w-full h-full object-cover"
<img
src={`${import.meta.env.BASE_URL}icon.png`}
alt="GitHub Stars Manager"
className="w-full h-full object-cover"
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/LoginScreen.tsx` around lines 71 - 74, The img in LoginScreen
uses a relative src="./icon.png" which breaks on non-root routes; update the img
src to be base-aware by either importing the asset into the component (e.g.,
import icon from '.../icon.png' and assign that to the src) or by using the
public base URL (e.g., process.env.PUBLIC_URL + '/icon.png') so the logo
resolves correctly regardless of route; modify the src attribute in the img
element inside LoginScreen accordingly.

/>
</div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">
GitHub Stars Manager
Expand Down Expand Up @@ -177,4 +181,4 @@ export const LoginScreen: React.FC = () => {
</div>
</div>
);
};
};
79 changes: 73 additions & 6 deletions src/components/RepositoryList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useRef, useEffect } from 'react';
import React, { useState, useRef, useEffect, useMemo } from 'react';
import { Bot, ChevronDown, Pause, Play } from 'lucide-react';
import { RepositoryCard } from './RepositoryCard';

Expand Down Expand Up @@ -34,6 +34,9 @@ export const RepositoryList: React.FC<RepositoryListProps> = ({
const [showDropdown, setShowDropdown] = useState(false);
const [isPaused, setIsPaused] = useState(false);
const [disableCardAnimations, setDisableCardAnimations] = useState(false);
const previousCategoryRef = useRef(selectedCategory);
const savedScrollYRef = useRef<number | null>(null);
const restoreScrollFrameRef = useRef<number | null>(null);
Comment on lines +37 to +39
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

Guard scroll restoration when category changes during sync.

Current logic always restores saved scroll on sync end. If the user switches category while syncing (Line 124 scrolls to top), the later restore can jump back to the old position and break the new-category flow.

🔧 Proposed fix
   const previousCategoryRef = useRef(selectedCategory);
   const savedScrollYRef = useRef<number | null>(null);
   const restoreScrollFrameRef = useRef<number | null>(null);
+  const syncStartCategoryRef = useRef<string | null>(null);

@@
       if (isSyncing) {
+        syncStartCategoryRef.current = previousCategoryRef.current;
         savedScrollYRef.current = window.scrollY;
         if (restoreScrollFrameRef.current !== null) {
           cancelAnimationFrame(restoreScrollFrameRef.current);
           restoreScrollFrameRef.current = null;
         }
         return;
       }

       const targetScrollY = savedScrollYRef.current;
       if (targetScrollY === null) return;
+      if (
+        syncStartCategoryRef.current !== null &&
+        syncStartCategoryRef.current !== previousCategoryRef.current
+      ) {
+        savedScrollYRef.current = null;
+        syncStartCategoryRef.current = null;
+        return;
+      }

       restoreScrollFrameRef.current = window.requestAnimationFrame(() => {
         restoreScrollFrameRef.current = window.requestAnimationFrame(() => {
           window.scrollTo({ top: targetScrollY, behavior: 'auto' });
           restoreScrollFrameRef.current = null;
           savedScrollYRef.current = null;
+          syncStartCategoryRef.current = null;
         });
       });

Also applies to: 163-184

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

In `@src/components/RepositoryList.tsx` around lines 37 - 39, Ensure scroll
restoration only occurs if the category hasn't changed: when saving scroll state
(use savedScrollYRef/previousCategoryRef), record previousCategoryRef.current =
selectedCategory; when scheduling the restore via restoreScrollFrameRef, check
that previousCategoryRef.current === selectedCategory before applying
window.scrollTo and only then clear savedScrollYRef/restoreScrollFrameRef;
additionally, on category change update previousCategoryRef and cancel any
pending restore by calling cancelAnimationFrame(restoreScrollFrameRef.current)
and clearing savedScrollYRef/restoreScrollFrameRef so a late restore cannot jump
the user back to the old category.


// 使用 useRef 来管理停止状态,确保在异步操作中能正确访问最新值
const shouldStopRef = useRef(false);
Expand Down Expand Up @@ -85,12 +88,52 @@ export const RepositoryList: React.FC<RepositoryListProps> = ({
const startIndex = filteredRepositories.length === 0 ? 0 : 1;
const endIndex = Math.min(visibleCount, filteredRepositories.length);
const visibleRepositories = filteredRepositories.slice(0, visibleCount);

// Reset visible count when filters or data change
const filterResetKey = useMemo(() => JSON.stringify({
selectedCategory,
query: searchFilters.query,
languages: searchFilters.languages,
tags: searchFilters.tags,
platforms: searchFilters.platforms,
sortBy: searchFilters.sortBy,
sortOrder: searchFilters.sortOrder,
minStars: searchFilters.minStars,
maxStars: searchFilters.maxStars,
isAnalyzed: searchFilters.isAnalyzed,
isSubscribed: searchFilters.isSubscribed,
}), [
selectedCategory,
searchFilters.query,
searchFilters.languages,
searchFilters.tags,
searchFilters.platforms,
searchFilters.sortBy,
searchFilters.sortOrder,
searchFilters.minStars,
searchFilters.maxStars,
searchFilters.isAnalyzed,
searchFilters.isSubscribed,
]);

// Reset visible count only when filter context changes.
useEffect(() => {
setVisibleCount(LOAD_BATCH);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedCategory, repositories, filteredRepositories.length]);
}, [filterResetKey]);

useEffect(() => {
if (previousCategoryRef.current !== selectedCategory) {
window.scrollTo({ top: 0, behavior: 'auto' });
previousCategoryRef.current = selectedCategory;
}
}, [selectedCategory]);

// Clamp visible count when result set becomes smaller, but do not collapse
// back to the initial batch during backend sync refreshes.
useEffect(() => {
setVisibleCount((count) => {
if (filteredRepositories.length === 0) return LOAD_BATCH;
return Math.min(count, filteredRepositories.length);
});
}, [filteredRepositories.length]);

// IntersectionObserver to load more on demand
useEffect(() => {
Expand All @@ -117,11 +160,35 @@ export const RepositoryList: React.FC<RepositoryListProps> = ({
useEffect(() => {
const handleSyncVisualState = (event: Event) => {
const customEvent = event as CustomEvent<{ isSyncing?: boolean }>;
setDisableCardAnimations(!!customEvent.detail?.isSyncing);
const isSyncing = !!customEvent.detail?.isSyncing;
setDisableCardAnimations(isSyncing);

if (isSyncing) {
savedScrollYRef.current = window.scrollY;
if (restoreScrollFrameRef.current !== null) {
cancelAnimationFrame(restoreScrollFrameRef.current);
restoreScrollFrameRef.current = null;
}
return;
}

const targetScrollY = savedScrollYRef.current;
if (targetScrollY === null) return;

restoreScrollFrameRef.current = window.requestAnimationFrame(() => {
restoreScrollFrameRef.current = window.requestAnimationFrame(() => {
window.scrollTo({ top: targetScrollY, behavior: 'auto' });
restoreScrollFrameRef.current = null;
savedScrollYRef.current = null;
});
});
};

window.addEventListener('gsm:repository-sync-visual-state', handleSyncVisualState as EventListener);
return () => {
if (restoreScrollFrameRef.current !== null) {
cancelAnimationFrame(restoreScrollFrameRef.current);
}
window.removeEventListener('gsm:repository-sync-visual-state', handleSyncVisualState as EventListener);
};
}, []);
Expand Down
7 changes: 7 additions & 0 deletions src/store/useAppStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ interface AppActions {
// UI actions
setTheme: (theme: 'light' | 'dark') => void;
setCurrentView: (view: 'repositories' | 'releases' | 'settings') => void;
setSelectedCategory: (category: string) => void;
setLanguage: (language: 'zh' | 'en') => void;

// Update actions
Expand Down Expand Up @@ -112,6 +113,8 @@ type PersistedAppState = Partial<
| 'customCategories'
| 'assetFilters'
| 'theme'
| 'currentView'
| 'selectedCategory'
Comment on lines +116 to +117
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

Validate persisted selectedCategory during rehydration.

Persisting category id is great, but stale/removed custom category ids can be restored and lead to empty repository results (see src/components/RepositoryList.tsx Line 52). Add a fallback to 'all' when persisted id is no longer valid.

🔧 Proposed fix
 const normalizePersistedState = (
   persisted: PersistedAppState | undefined,
   currentState: AppState & AppActions
 ): Partial<AppState & AppActions> => {
   const safePersisted = persisted ?? {};

+  const persistedCustomCategories = Array.isArray(safePersisted.customCategories)
+    ? safePersisted.customCategories
+    : [];
+  const validCategoryIds = new Set([
+    ...defaultCategories.map((cat) => cat.id),
+    ...persistedCustomCategories.map((cat) => cat.id),
+  ]);
+  const normalizedSelectedCategory =
+    typeof safePersisted.selectedCategory === 'string' &&
+    validCategoryIds.has(safePersisted.selectedCategory)
+      ? safePersisted.selectedCategory
+      : 'all';
+
   const repositories = Array.isArray(safePersisted.repositories) ? safePersisted.repositories : [];
   const releases = Array.isArray(safePersisted.releases) ? safePersisted.releases : [];
@@
-    customCategories: Array.isArray(safePersisted.customCategories) ? safePersisted.customCategories : [],
+    customCategories: persistedCustomCategories,
+    selectedCategory: normalizedSelectedCategory,

Also applies to: 484-485

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

In `@src/store/useAppStore.ts` around lines 116 - 117, Persisted selectedCategory
can be stale; update the rehydration logic in useAppStore to validate the
restored selectedCategory id against the current categories list and fall back
to 'all' when the id is missing or invalid. Specifically, in the code path that
handles store rehydration (the code that reads/restores selectedCategory), check
whether the restored id exists in the categories array (or map) used by the app
and if not assign 'all' before setting the state; reference the selectedCategory
state/key and the rehydrate/restore handler in useAppStore so RepositoryList and
other consumers always receive a valid category id.

| 'language'
| 'searchFilters'
>
Expand Down Expand Up @@ -276,6 +279,7 @@ export const useAppStore = create<AppState & AppActions>()(
assetFilters: [],
theme: 'light',
currentView: 'repositories',
selectedCategory: 'all',
language: 'zh',
updateNotification: null,
analysisProgress: { current: 0, total: 0 },
Expand Down Expand Up @@ -429,6 +433,7 @@ export const useAppStore = create<AppState & AppActions>()(
// UI actions
setTheme: (theme) => set({ theme }),
setCurrentView: (currentView) => set({ currentView }),
setSelectedCategory: (selectedCategory) => set({ selectedCategory }),
setLanguage: (language) => set({ language }),

// Update actions
Expand Down Expand Up @@ -476,6 +481,8 @@ export const useAppStore = create<AppState & AppActions>()(

// 持久化UI设置
theme: state.theme,
currentView: state.currentView,
selectedCategory: state.selectedCategory,
language: state.language,

// backendApiSecret: 保留在内存中,不持久化(安全考虑)
Expand Down
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ export interface AppState {
// UI
theme: 'light' | 'dark';
currentView: 'repositories' | 'releases' | 'settings';
selectedCategory: string;
language: 'zh' | 'en';

// Update
Expand Down
Loading