feat(core): Establish Application Foundation, Dashboard UI, and API Caching Layer#53
feat(core): Establish Application Foundation, Dashboard UI, and API Caching Layer#53seif-a096 wants to merge 3 commits intoAOSSIE-Org:mainfrom
Conversation
WalkthroughThis PR establishes the complete dashboard foundation for Org Explorer, implementing routing, organization data fetching with caching, GitHub API integration, and multi-page dashboard views. It introduces service layers for secure token storage and IndexedDB-backed caching, styled components with Tailwind v4, and interactive visualization features including graphs and charts. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant UI as UI (TokenModal)
participant Cache as cacheService
participant Storage as localStorage
participant Browser as window.location
User->>UI: Opens app without token
UI->>Cache: Calls cacheService.getToken()
Cache->>Storage: Checks localStorage for token
Storage-->>Cache: Returns null
Cache-->>UI: No token found
UI->>UI: Shows TokenModal
User->>UI: Enters PAT and clicks "Save & Continue"
UI->>UI: Validates token (trim, not empty)
UI->>Cache: Calls cacheService.setToken(token)
Cache->>Storage: Stores token in localStorage
Storage-->>Cache: Stored
Cache-->>UI: Callback complete
UI->>Browser: Calls window.location.reload()
Browser->>Browser: Page reloads
Note over Browser: App reinitializes with stored token
sequenceDiagram
participant App as React Component
participant GithubAPI as githubApi
participant IndexedDB as cacheService (IndexedDB)
participant GitHub as GitHub API
App->>GithubAPI: Calls getOrgRepos(orgName)
GithubAPI->>IndexedDB: Calls get("repos", cacheKey)
alt Cache Hit (< 1 hour)
IndexedDB-->>GithubAPI: Returns cached repos
else Cache Miss or Stale
GithubAPI->>GitHub: Fetches /repos endpoint
GitHub-->>GithubAPI: Returns repo data
GithubAPI->>IndexedDB: Calls set("repos", cacheKey, data)
IndexedDB->>IndexedDB: Stores in repos store + metadata timestamp
IndexedDB-->>GithubAPI: Write complete
end
GithubAPI-->>App: Returns repos array
App->>App: Renders dashboard with data
Note over App: Re-renders use cached data when available<br/>Rate limit preserved via caching layer
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Pull request overview
Establishes the initial OrgExplorer frontend foundation: Vite + React Router app shell, Tailwind v4 styling/theme tokens, core dashboard tabs (overview/repos/contributors) with visualizations, and a client-side GitHub API wrapper backed by IndexedDB caching.
Changes:
- Adds Tailwind v4 + shared UI utilities and baseline styling/theme setup.
- Introduces dashboard layout, routing, and core tabs with charts/visualizations.
- Implements a GitHub API + IndexedDB cache layer and PAT token modal flow.
Reviewed changes
Copilot reviewed 20 out of 23 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| vite.config.ts | Adds Tailwind Vite plugin alongside React plugin. |
| vercel.json | Adds SPA rewrite rules for static hosting. |
| tailwind.config.js | Tailwind content + theme color extensions. |
| src/services/githubApi.ts | GitHub API wrapper with cache-first fetch and rate-limit signaling. |
| src/services/cache.ts | IndexedDB cache + localStorage persistence helpers (token/org). |
| src/main.tsx | Updates React bootstrap entrypoint formatting/imports. |
| src/lib/utils.ts | Adds cn() helper using clsx + tailwind-merge. |
| src/index.css | Tailwind v4 CSS-first theme tokens + base styling + utilities. |
| src/hooks/useRateLimit.ts | Hook to poll/display GitHub rate limit information. |
| src/components/layout/Sidebar.tsx | Adds sidebar nav + API status widget + token CTA. |
| src/components/dashboard/StatCard.tsx | Adds animated stat card component. |
| src/components/dashboard/ReposTab.tsx | Repos table with search/sort/pagination + detail panel trigger. |
| src/components/dashboard/RepoDetailPanel.tsx | Slide-over repo details, activity chart, top contributors. |
| src/components/dashboard/OverviewTab.tsx | Overview dashboard: org header, stat cards, language chart, F1 contributors, top repos. |
| src/components/dashboard/LanguageChart.tsx | Recharts pie chart for top languages. |
| src/components/dashboard/F1Race.tsx | framer-motion “race” visualization for contributors. |
| src/components/dashboard/ContributorsTab.tsx | Contributors grid aggregated from active repos. |
| src/components/common/TokenModal.tsx | Modal for entering/storing GitHub PAT. |
| src/App.tsx | App shell with router + lazy routes + token modal integration. |
| public/aossie-icon.svg | Adds AOSSIE-branded SVG icon. |
| package.json | Adds UI/chart/router/idb deps and Tailwind tooling. |
| package-lock.json | Lockfile updates for added dependencies. |
| index.html | Adds favicon link and updates document title. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const data = await response.json(); | ||
| return { | ||
| limit: data.rate.limit, | ||
| remaining: data.rate.remaining, | ||
| reset: data.rate.reset, | ||
| }; | ||
| } catch { |
There was a problem hiding this comment.
getRateLimit() parses JSON and returns data.rate.* without checking response.ok. On 401/403/5xx, GitHub returns a different payload shape, which will throw and get swallowed as null, and it also won’t dispatch the github-api-limit event like fetchWithCache does. Add an ok/status check (and optionally dispatch the same event for 401/403) before reading data.rate so consumers can distinguish unauthenticated/rate-limited vs network errors.
| const data = await response.json(); | |
| return { | |
| limit: data.rate.limit, | |
| remaining: data.rate.remaining, | |
| reset: data.rate.reset, | |
| }; | |
| } catch { | |
| if (!response.ok) { | |
| if (response.status === 403 || response.status === 401) { | |
| // Trigger generic rate limit / auth event, consistent with fetchWithCache | |
| window.dispatchEvent(new CustomEvent('github-api-limit', { detail: response.headers })); | |
| } | |
| throw new GitHubApiError(response.status, `GitHub API Error: ${response.statusText}`); | |
| } | |
| const data = await response.json(); | |
| return { | |
| limit: data.rate.limit, | |
| remaining: data.rate.remaining, | |
| reset: data.rate.reset, | |
| }; | |
| } catch (error) { | |
| // Propagate API errors so callers can distinguish them from network/parse failures | |
| if (error instanceof GitHubApiError) { | |
| throw error; | |
| } |
| const response = await fetch(`${API_BASE}${url}`, { | ||
| headers: getHeaders(), | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| if (response.status === 403 || response.status === 401) { | ||
| // Trigger generic rate limit event | ||
| window.dispatchEvent(new CustomEvent('github-api-limit', { detail: response.headers })); | ||
| } | ||
| throw new GitHubApiError(response.status, `GitHub API Error: ${response.statusText}`); | ||
| } | ||
|
|
||
| const data = await response.json(); | ||
| await cacheService.set(storeName, cacheKey, data); | ||
| return data; |
There was a problem hiding this comment.
fetchWithCache() will cache any response.ok payload, including GitHub’s 202 Accepted responses from /stats/* endpoints (which typically return a “processing” message). Caching that transient payload can permanently turn the activity view into an empty state until TTL expires. Handle response.status === 202 (and possibly 204) as a cache miss: don’t write to IndexedDB and either return an empty array/typed placeholder or retry with backoff.
| const fetchRateLimit = async () => { | ||
| setIsLoading(true); | ||
| const limitInfo = await githubApi.getRateLimit(); | ||
| setRateLimit(limitInfo); | ||
| setIsLoading(false); | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| fetchRateLimit(); | ||
|
|
||
| const handleLimitHit = () => { | ||
| fetchRateLimit(); | ||
| }; | ||
|
|
||
| window.addEventListener('github-api-limit', handleLimitHit); | ||
|
|
||
| // Poll every 5 minutes or on tab focus | ||
| const interval = setInterval(fetchRateLimit, 5 * 60 * 1000); | ||
| window.addEventListener('focus', fetchRateLimit); | ||
|
|
||
| return () => { | ||
| window.removeEventListener('github-api-limit', handleLimitHit); | ||
| window.removeEventListener('focus', fetchRateLimit); | ||
| clearInterval(interval); | ||
| }; | ||
| }, []); |
There was a problem hiding this comment.
fetchRateLimit is referenced inside a useEffect with an empty dependency array. With eslint-plugin-react-hooks enabled, this will trigger react-hooks/exhaustive-deps warnings and can also lead to stale closures if the function later captures values. Wrap fetchRateLimit in useCallback and include it (and any other referenced values) in the effect dependency list.
| if (token.trim()) { | ||
| cacheService.setToken(token.trim()); | ||
| window.location.reload(); // Reload to re-fetch with token |
There was a problem hiding this comment.
handleSave() forces a full page reload after saving the token. This breaks SPA flow and can reset other client state unnecessarily. Prefer closing the modal and triggering refetches via state/context (or dispatching a dedicated event) so the app updates without a hard reload.
| if (token.trim()) { | |
| cacheService.setToken(token.trim()); | |
| window.location.reload(); // Reload to re-fetch with token | |
| const trimmedToken = token.trim(); | |
| if (trimmedToken) { | |
| cacheService.setToken(trimmedToken); | |
| // Notify the application that the token has been updated without forcing a full reload | |
| window.dispatchEvent(new CustomEvent('tokenUpdated')); | |
| onClose(); |
| <button | ||
| onClick={() => setCurrentPage(p => Math.max(1, p - 1))} | ||
| disabled={currentPage === 1} | ||
| className="p-1.5 sharp-interactive bg-github-dark disabled:opacity-50 disabled:cursor-not-allowed" | ||
| > | ||
| <ChevronLeft className="w-5 h-5" /> | ||
| </button> | ||
| <button | ||
| onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))} | ||
| disabled={currentPage === totalPages} | ||
| className="p-1.5 sharp-interactive bg-github-dark disabled:opacity-50 disabled:cursor-not-allowed" | ||
| > | ||
| <ChevronRight className="w-5 h-5" /> | ||
| </button> |
There was a problem hiding this comment.
Pagination buttons are icon-only (chevrons) and don’t have an accessible label, so screen-reader users won’t know what they do. Add aria-label (e.g., “Previous page” / “Next page”) and consider reflecting the disabled state via aria-disabled as well.
| <div className="w-full bg-github-border h-1 flex"> | ||
| <div | ||
| className={cn( | ||
| "h-full transition-all duration-300", | ||
| rateLimit && (rateLimit.remaining / rateLimit.limit) > 0.2 ? "bg-[var(--color-aossie-green)]" : "bg-red-500" | ||
| )} | ||
| style={{ | ||
| width: rateLimit ? `${(rateLimit.remaining / rateLimit.limit) * 100}%` : '0%' | ||
| }} |
There was a problem hiding this comment.
The rate-limit progress calculations divide by rateLimit.limit without guarding against 0/missing values. If getRateLimit() returns an unexpected payload (or limit is 0), this yields NaN%/Infinity% widths and incorrect coloring. Clamp the ratio with a safe denominator (e.g., Math.max(limit, 1)) and consider an explicit isLoading || !rateLimit branch.
| </div> | ||
| <button | ||
| onClick={onClose} | ||
| className="text-github-muted hover:text-white transition-colors p-1" |
There was a problem hiding this comment.
The modal close button is icon-only (X) and lacks an accessible name. Add an aria-label (e.g., “Close token dialog”) so screen readers can announce it correctly.
| className="text-github-muted hover:text-white transition-colors p-1" | |
| className="text-github-muted hover:text-white transition-colors p-1" | |
| aria-label="Close token dialog" |
| <button | ||
| onClick={onClose} | ||
| className="p-2 sharp-interactive text-github-muted hover:text-white transition-colors ml-4 bg-github-dark" | ||
| > | ||
| <X className="w-5 h-5" /> | ||
| </button> |
There was a problem hiding this comment.
The repo detail panel close button is icon-only and doesn’t provide an accessible name. Add an aria-label (and optionally type="button") so it’s properly announced and doesn’t accidentally submit if nested in a form in the future.
| "dependencies": { | ||
| "clsx": "^2.1.1", | ||
| "date-fns": "^4.1.0", | ||
| "framer-motion": "^12.38.0", | ||
| "idb": "^8.0.3", | ||
| "lucide-react": "^1.7.0", | ||
| "react": "^19.2.0", | ||
| "react-dom": "^19.2.0" | ||
| "react-dom": "^19.2.0", | ||
| "react-router-dom": "^7.13.2", | ||
| "recharts": "^3.8.1", | ||
| "tailwind-merge": "^3.5.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@eslint/js": "^9.39.1", | ||
| "@tailwindcss/vite": "^4.2.2", | ||
| "@types/node": "^24.10.1", | ||
| "@types/react": "^19.2.5", | ||
| "@types/react-dom": "^19.2.3", | ||
| "@vitejs/plugin-react": "^5.1.1", | ||
| "autoprefixer": "^10.4.23", | ||
| "eslint": "^9.39.1", | ||
| "eslint-plugin-react-hooks": "^7.0.1", | ||
| "eslint-plugin-react-refresh": "^0.4.24", | ||
| "globals": "^16.5.0", | ||
| "postcss": "^8.5.8", | ||
| "tailwindcss": "^4.1.18", | ||
| "typescript": "~5.9.3", | ||
| "typescript-eslint": "^8.46.4", | ||
| "vite": "npm:rolldown-vite@7.2.5" |
There was a problem hiding this comment.
No Node.js engine requirement is declared, but dependencies introduced/updated here require Node >= 20 (notably react-router-dom@7 and rolldown-vite). Without an engines.node (and/or an .nvmrc), installs/builds may fail on CI or contributor machines running Node 18. Add an explicit Node version range that matches the strictest dependency requirement.
| <link rel="icon" type="image/svg+xml" href="/aossie-icon.svg" /> | ||
| <title>AOSSIE | WebOrg</title> |
There was a problem hiding this comment.
The document title was changed to “AOSSIE | WebOrg”, but the PR/issue context and app naming elsewhere refer to “OrgExplorer”. If this isn’t an intentional rename, align the <title> with the project name (and any expected branding) to avoid confusing users and affecting browser history/search results.
There was a problem hiding this comment.
Actionable comments posted: 28
🤖 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/App.tsx`:
- Line 27: The Suspense fallback in App.tsx currently contains the hardcoded
user-visible string "LOADING_MODULE..."; update it to use your i18n resource
instead: add/import the translation hook or helper (e.g., useTranslation/t from
your i18n setup) in the App component, add a key like "loadingModule" to the
i18n resource files, and replace the inline text with the translated value
(e.g., t('loadingModule') or <Trans i18nKey="loadingModule" />) while keeping
the existing fallback element and its classes intact so styling is unchanged.
- Around line 28-33: The Routes block in App.tsx lacks a fallback for unmatched
URLs, causing blank screens; add a catch-all Route (path="*") at the end of the
<Routes> list that renders a sensible fallback (e.g., Navigate to "/overview" or
a NotFound component) so unknown paths are handled; update the existing <Routes>
where Route components (Route path="/" element={<Navigate .../>}, Route
path="/overview" element={<OverviewTab/>}, etc.) are declared and append the new
Route with path="*" to prevent empty renders.
In `@src/components/common/TokenModal.tsx`:
- Around line 28-80: The TokenModal component currently contains hard-coded
user-visible strings; extract all text (titles, descriptions, labels,
placeholders, button text, and link text) into i18n resource keys and replace
literals in TokenModal (e.g., the "Connect GitHub Token" header, the paragraph
about token usage, "Personal Access Token" label, placeholder "ghp_...", the
scope description and link text, "Cancel", and "Save & Continue") with localized
lookups (e.g., t('tokenModal.title')) and ensure TokenModal imports/uses the
i18n hook/function (e.g., useTranslation or t) so token, handleSave, and onClose
logic remain unchanged while strings come from resource files.
- Around line 15-19: The current handleSave handler stores the GitHub PAT via
cacheService.setToken(token.trim()) and forces a reload, which persists the
token in client-accessible storage; change this to send the trimmed token to the
backend over HTTPS and let the server set a secure, HTTP-only cookie instead of
calling cacheService.setToken or writing to localStorage. Update handleSave to
POST the token to an auth endpoint (e.g., /api/auth/token) including CSRF
protection and handle the server response (success -> window.location.reload()
if needed, failure -> show error), remove direct local persistence calls to
cacheService.setToken, and ensure token state (the token variable) is cleared
after submission; coordinate with backend to validate and set the secure cookie.
- Around line 23-53: Add proper dialog semantics to the TokenModal component:
give the modal container the attributes role="dialog" and aria-modal="true" and
set aria-labelledby to the heading's id (add an id to the <h2> used in the
header, e.g., modal-title). Ensure the label/input are associated by converting
the visible label to a <label htmlFor="token-input"> and give the input an
id="token-input" (keep value={token} and onChange calling setToken). Add
aria-describedby on the dialog pointing to the explanatory paragraph's id for
additional context, and add an accessible name to the close button (e.g.,
aria-label="Close token modal" on the button that calls onClose).
In `@src/components/dashboard/ContributorsTab.tsx`:
- Line 7: The contributors state currently uses the any[] type; define an
explicit interface (e.g., Contributor with fields login, avatar_url, html_url,
contributions) and replace useState<any[]>() with useState<Contributor[]>() and
update any related variables/params/usages (e.g., setContributors, any local
variables or map callbacks that expect contributor objects) to use the
Contributor type so all occurrences (including uses in render/mapping) have
explicit typing instead of any.
- Line 10: cacheService.getLastOrg() can return null/undefined so guard the
value before using orgName in the downstream API call: after const orgName =
cacheService.getLastOrg(), check if (!orgName) and handle it (early return, show
a placeholder/empty state, or log and skip the API request) instead of passing
undefined into the API; update the code paths that call the API with orgName
(the API call that currently uses orgName) to only run when orgName is truthy or
to use a safe default, and ensure UI state is updated to reflect the missing
org.
In `@src/components/dashboard/F1Race.tsx`:
- Around line 77-80: The avatar <img> in F1Race.tsx currently uses
src={racer.avatar_url} and has no error handling, so add an onError handler to
replace the broken image with a fallback (for example a local /static
placeholder or data-URI) and ensure the handler uses the image element
(e.currentTarget.src = fallback) and idempotently avoids infinite loops (check
current src before replacing or use a boolean state like hasAvatarError). Update
the <img> element in the Avatar (Driver) block to call the new onError and
optionally use a small piece of component state (e.g., hasAvatarError) or guard
to pick between racer.avatar_url and the fallback.
In `@src/components/dashboard/LanguageChart.tsx`:
- Line 44: In LanguageChart update the tooltip/axis formatter signature to
replace the any type with an explicit numeric type: change the formatter param
on the formatter={(value: any) => ...} callback to use (value: number) (or
number | null if nulls are possible) so it matches dataKey="value"; ensure any
string interpolation still works by converting the number to string where needed
and keep the returned array structure unchanged.
In `@src/components/dashboard/OverviewTab.tsx`:
- Around line 24-29: State uses untyped any/any[] for languages, racers,
topRepos, and orgData which weakens TypeScript safety; define explicit
interfaces (e.g., Language, Racer, Repo, OrgData) and replace
useState<any[]>/useState<any> with useState<Language[]>, useState<Racer[]>,
useState<Repo[]>, and useState<OrgData | null> respectively in OverviewTab.tsx,
update setLanguages, setRacers, setTopRepos, and setOrgData usages to conform to
those shapes, and import or declare the interfaces near the top of the file so
the types are enforced across functions that read/write these states.
- Around line 97-99: The component OverviewTab currently only logs fetch
failures; add an error state (e.g., const [error, setError] = useState<string |
null>(null)) and in the catch block of the data-loading function setError with a
user-facing message (or the error.message), clear it on successful loads, and
expose a retry handler that re-invokes the fetch; then update OverviewTab's
render to show conditional fallback UI when error is non-null (toast, inline
message, and/or a Retry button that calls the same fetch function) so users see
and can recover from load failures.
- Around line 81-89: Replace the implicit any with a proper Contributor type and
use Record<string, Contributor> for combinedContributors; specifically define an
interface (e.g., Contributor with login: string, contributions: number, plus
optional fields) and change combinedContributors from Record<string, any> to
Record<string, Contributor>, then update the aggregation loop (the
contributorsArrays.flat().forEach block that references c.login and
c.contributions) to operate against that typed shape and keep the null/undefined
guard (if (!c || !c.login) return) so property accesses are type-safe.
In `@src/components/dashboard/RepoDetailPanel.tsx`:
- Line 64: Replace hardcoded user-visible strings in RepoDetailPanel with i18n
resource keys: remove literal 'NO DESCRIPTION' used next to repo.description,
the "No activity data available." message, and "NO CONTRIBUTORS DATA." and
instead call the localization helper (e.g., t('repo.noDescription'),
t('repo.noActivity'), t('repo.noContributors')) or import strings from the app's
resource bundle; update the JSX in RepoDetailPanel to use these keys
(references: the repo.description render, the activity fallback at lines
~124-126, and the contributors fallback at ~171-173), add the corresponding
entries to the locale resource file(s), and import/initialize the translator in
RepoDetailPanel so all three UI strings come from the i18n resources.
- Around line 7-15: Replace the broad any types by defining explicit interfaces
(e.g. Repo, Contributor, Activity) and use them in RepoDetailPanelProps and the
component state: change repo: any | null to repo: Repo | null in
RepoDetailPanelProps, change contributors state to useState<Contributor[]>([])
and activity state to useState<Activity[]>([]) (and update any handlers that
call setContributors/setActivity to accept these types); ensure the interfaces
include the fields the component reads (e.g. name, owner, stars for Repo; login,
avatarUrl for Contributor; date, type, description for Activity) so all property
accesses inside RepoDetailPanel are properly typed.
- Around line 17-49: The useEffect closure reads orgName from
cacheService.getLastOrg() but the effect dependency array only lists repo, so
the effect won't re-run when orgName changes; update the dependency array to
include orgName (or derive orgName outside/useMemo) so that fetchData (which
calls githubApi.getRepoContributors and githubApi.getRepoActivity) re-executes
with the current orgName; ensure useEffect([...]) includes orgName alongside
repo to fix stale org fetches.
In `@src/components/dashboard/ReposTab.tsx`:
- Line 20: cacheService.getLastOrg() can return null/undefined so add a guard in
the ReposTab component before using orgName: check the result of
cacheService.getLastOrg() (the orgName variable) and bail out or return early
(e.g., no-op render or skip API call) when it's falsy, and only call the repos
API or invoke the function that uses orgName (the code around the orgName
assignment and the subsequent call at the second occurrence) when orgName is a
non-empty string; update both places that reference orgName (the initial const
orgName = cacheService.getLastOrg() and the later use) to perform this guard to
prevent passing undefined/null into the API call.
- Line 170: The date rendering in ReposTab currently uses new
Date(repo.updated_at).toLocaleDateString(), which varies by browser locale;
update the JSX to use date-fns for a consistent format: import format and
parseISO from 'date-fns' at the top of the ReposTab component file and replace
new Date(repo.updated_at).toLocaleDateString() with
format(parseISO(repo.updated_at), 'yyyy-MM-dd') (or your preferred format
string) to ensure deterministic output and handle ISO strings safely.
- Around line 11-14: Replace the broad any types by defining an explicit
Repository interface (e.g., fields like id, name, description?, language?,
stargazers_count, forks_count, open_issues_count, updated_at, etc.) at the top
of ReposTab.tsx, then change the state generics from useState<any[]>([]) to
useState<Repository[]>([]) for repos and from useState<any | null>(null) to
useState<Repository | null>(null) for selectedRepo; also update any handlers or
render code that access repository properties (e.g., where repos, setRepos,
selectedRepo are used) to rely on the new typed fields or make optional checks
where fields may be null/undefined.
In `@src/components/layout/Sidebar.tsx`:
- Around line 93-96: The current expressions in Sidebar.tsx use
(rateLimit.remaining / rateLimit.limit) without guarding against rateLimit.limit
=== 0; update both the className ternary and the style width calculation to
first verify rateLimit && rateLimit.limit > 0 (or compute a safeRatio =
rateLimit && rateLimit.limit > 0 ? rateLimit.remaining / rateLimit.limit : 0)
and then use safeRatio for the > 0.2 check and for the width `${safeRatio *
100}%` so you never divide by zero and invalid CSS is avoided.
In `@src/hooks/useRateLimit.ts`:
- Around line 23-27: The focus handler and interval in useRateLimit are causing
overlapping fetchRateLimit calls; wrap fetchRateLimit in a debounced or
cancellable wrapper and register that instead of calling fetchRateLimit
directly. Specifically, create a debouncedFetchRateLimit using useCallback (or
implement an AbortController inside a useCallback that cancels prior fetches)
and replace window.addEventListener('focus', fetchRateLimit) and the interval
callback with this wrapper; ensure you clear the interval, remove both event
listeners, and abort any in-flight request in the hook cleanup to avoid
redundant API calls and race conditions.
- Around line 9-14: The fetchRateLimit function can throw from
githubApi.getRateLimit causing setIsLoading(true) to never be cleared; wrap the
await call in a try/catch/finally (or try/finally) inside fetchRateLimit, call
setRateLimit only on success, handle/log the error in the catch, and ensure
setIsLoading(false) is executed in the finally block so the loading state is
always cleared.
In `@src/index.css`:
- Around line 28-41: The current CSS only defines WebKit pseudo-elements (::
-webkit-scrollbar, ::-webkit-scrollbar-track, ::-webkit-scrollbar-thumb,
::-webkit-scrollbar-thumb:hover) which only affect Chromium browsers; add
cross‑browser rules using the standard properties scrollbar-color and
scrollbar-width on the scrollable elements (or :root) and set sensible values
that map to your existing variables (e.g., thumb and track colors and width) so
Firefox and other non‑WebKit browsers show a matching styled scrollbar; keep the
WebKit rules as-is and add the standard properties alongside them to ensure
broad support.
In `@src/services/cache.ts`:
- Around line 52-78: The get and set IndexedDB operations (functions get and set
using getDB, TTL_MS, db.transaction, tx.objectStore) lack error handling and can
cause unhandled rejections; wrap the body of both get and set in try/catch
blocks, catch any errors from await getDB(), db.get(), and the transaction
Promise, log the error with contextual info (storeName and key) and for get
return null on failure, while for set log the error and ensure the transaction
is aborted/cleaned up (call tx.abort if available) without letting the exception
bubble up to callers.
- Around line 4-23: OrgExplorerDB currently types every store value as any which
loses TypeScript safety; replace those any types with explicit interfaces or
generics (e.g., define Repo, Contributor, Activity, Metadata interfaces/types)
and use them in the OrgExplorerDB stores (repos, contributors, activity,
metadata.value) or add generic type parameters to your cache accessors (get<T>,
set<T>) so callers can specify the expected type; update references to
OrgExplorerDB, repos, contributors, activity, metadata and the cache get/set
methods to use the new types.
In `@src/services/githubApi.ts`:
- Around line 20-34: In getRateLimit, check response.ok before calling
response.json() and accessing data.rate: if !response.ok return null (or handle
the error) to avoid accessing undefined data.rate; update the getRateLimit
function to first test response.ok on the fetch response, only parse JSON and
read data.rate.limit / data.rate.remaining / data.rate.reset when response.ok is
true, otherwise return null (or propagate an error) so you don't attempt to read
properties from an undefined rate object.
- Around line 72-97: Replace the generic any[] return types by defining precise
response interfaces (e.g., OrgRepo, RepoContributor, CommitActivity) that match
the GitHub API shapes and use those interfaces as the generic type parameters on
githubApi.fetchWithCache and the functions getOrgRepos, getRepoContributors, and
getRepoActivity so each function returns Promise<OrgRepo[]>,
Promise<RepoContributor[]>, and Promise<CommitActivity[]> respectively; add or
import those interfaces near this module and update the function signatures and
fetchWithCache calls to use them for proper type safety.
- Around line 46-56: Add a request timeout to the GitHub API fetch in
src/services/githubApi.ts by using an AbortController: create an AbortController
before calling fetch, pass controller.signal to fetch (alongside headers from
getHeaders()), and set a timer (e.g., via setTimeout) that calls
controller.abort() after the desired timeout; clear the timer after fetch
completes. Catch the abort case (check for DOMException name 'AbortError' or
similar) and rethrow a GitHubApiError with a clear timeout message so callers
can handle it, while preserving the existing status-based logic (including the
github-api-limit event dispatch) for non-timeout failures. Ensure the timer is
cleaned up in all code paths.
- Around line 63-70: getOrgDetails currently calls the GitHub API without
validating the org parameter; add input validation at the start of getOrgDetails
to ensure org is a non-empty string matching GitHub org name rules (e.g. allowed
chars and length, such as /^[A-Za-z0-9-]{1,39}$/), and throw a clear error (e.g.
"Invalid organization name") before making the fetch; apply the same validation
pattern to other service methods that accept org or repo parameters to prevent
malformed API calls.
🪄 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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: fef4ce7f-2c19-4c2c-a5b5-1a2de5321151
⛔ Files ignored due to path filters (2)
package-lock.jsonis excluded by!**/package-lock.jsonpublic/aossie-icon.svgis excluded by!**/*.svg
📒 Files selected for processing (21)
index.htmlpackage.jsonsrc/App.tsxsrc/components/common/TokenModal.tsxsrc/components/dashboard/ContributorsTab.tsxsrc/components/dashboard/F1Race.tsxsrc/components/dashboard/LanguageChart.tsxsrc/components/dashboard/OverviewTab.tsxsrc/components/dashboard/RepoDetailPanel.tsxsrc/components/dashboard/ReposTab.tsxsrc/components/dashboard/StatCard.tsxsrc/components/layout/Sidebar.tsxsrc/hooks/useRateLimit.tssrc/index.csssrc/lib/utils.tssrc/main.tsxsrc/services/cache.tssrc/services/githubApi.tstailwind.config.jsvercel.jsonvite.config.ts
| <Sidebar className="w-64 border-r border-github-border flex-shrink-0" onConnectToken={() => setShowTokenModal(true)} /> | ||
|
|
||
| <main className="flex-1 overflow-y-auto p-8 relative"> | ||
| <Suspense fallback={<div className="flex w-full h-full items-center justify-center text-aossie-green font-mono">LOADING_MODULE...</div>}> |
There was a problem hiding this comment.
Externalize the loading fallback string.
LOADING_MODULE... is user-visible and should come from i18n resources, not inline text.
As per coding guidelines, "User-visible strings should be externalized to resource files (i18n)".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/App.tsx` at line 27, The Suspense fallback in App.tsx currently contains
the hardcoded user-visible string "LOADING_MODULE..."; update it to use your
i18n resource instead: add/import the translation hook or helper (e.g.,
useTranslation/t from your i18n setup) in the App component, add a key like
"loadingModule" to the i18n resource files, and replace the inline text with the
translated value (e.g., t('loadingModule') or <Trans i18nKey="loadingModule" />)
while keeping the existing fallback element and its classes intact so styling is
unchanged.
| <Routes> | ||
| <Route path="/" element={<Navigate to="/overview" replace />} /> | ||
| <Route path="/overview" element={<OverviewTab />} /> | ||
| <Route path="/repos" element={<ReposTab />} /> | ||
| <Route path="/contributors" element={<ContributorsTab />} /> | ||
| </Routes> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether a wildcard route exists in the app routing table.
rg -nP --type=tsx '<Route\s+path="\*"' src/App.tsxRepository: AOSSIE-Org/OrgExplorer
Length of output: 93
🏁 Script executed:
#!/bin/bash
# Check if src/App.tsx exists and read the relevant lines
if [ -f "src/App.tsx" ]; then
echo "=== File exists ==="
wc -l src/App.tsx
echo ""
echo "=== Lines 20-40 (context around lines 28-33) ==="
sed -n '20,40p' src/App.tsx
echo ""
echo "=== Search for wildcard routes in src/App.tsx ==="
rg 'path="\*"' src/App.tsx -n
else
echo "src/App.tsx not found"
find . -name "App.tsx" -o -name "app.tsx" 2>/dev/null | head -5
fiRepository: AOSSIE-Org/OrgExplorer
Length of output: 1156
Add a catch-all route to prevent blank screens on unknown URLs.
Unmatched paths currently render no content. Add a fallback path="*" route.
Proposed fix
<Routes>
<Route path="/" element={<Navigate to="/overview" replace />} />
<Route path="/overview" element={<OverviewTab />} />
<Route path="/repos" element={<ReposTab />} />
<Route path="/contributors" element={<ContributorsTab />} />
+ <Route path="*" element={<Navigate to="/overview" replace />} />
</Routes>📝 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.
| <Routes> | |
| <Route path="/" element={<Navigate to="/overview" replace />} /> | |
| <Route path="/overview" element={<OverviewTab />} /> | |
| <Route path="/repos" element={<ReposTab />} /> | |
| <Route path="/contributors" element={<ContributorsTab />} /> | |
| </Routes> | |
| <Routes> | |
| <Route path="/" element={<Navigate to="/overview" replace />} /> | |
| <Route path="/overview" element={<OverviewTab />} /> | |
| <Route path="/repos" element={<ReposTab />} /> | |
| <Route path="/contributors" element={<ContributorsTab />} /> | |
| <Route path="*" element={<Navigate to="/overview" replace />} /> | |
| </Routes> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/App.tsx` around lines 28 - 33, The Routes block in App.tsx lacks a
fallback for unmatched URLs, causing blank screens; add a catch-all Route
(path="*") at the end of the <Routes> list that renders a sensible fallback
(e.g., Navigate to "/overview" or a NotFound component) so unknown paths are
handled; update the existing <Routes> where Route components (Route path="/"
element={<Navigate .../>}, Route path="/overview" element={<OverviewTab/>},
etc.) are declared and append the new Route with path="*" to prevent empty
renders.
| const handleSave = () => { | ||
| if (token.trim()) { | ||
| cacheService.setToken(token.trim()); | ||
| window.location.reload(); // Reload to re-fetch with token | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify token storage backend used by cacheService.setToken.
fd 'cache.*' src
rg -nP -C3 --glob 'src/**/*.{ts,tsx,js,jsx}' '\bsetToken\s*\(|localStorage|sessionStorage|indexedDB'Repository: AOSSIE-Org/OrgExplorer
Length of output: 2007
Remove localStorage usage for token persistence; use secure HTTP-only cookies instead.
GitHub PAT tokens are stored in localStorage (via cacheService.setToken() at line 17), which persists across sessions and is vulnerable to XSS attacks. Store sensitive authentication credentials in secure HTTP-only cookies that the server can validate and rotate, preventing client-side exposure.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/common/TokenModal.tsx` around lines 15 - 19, The current
handleSave handler stores the GitHub PAT via cacheService.setToken(token.trim())
and forces a reload, which persists the token in client-accessible storage;
change this to send the trimmed token to the backend over HTTPS and let the
server set a secure, HTTP-only cookie instead of calling cacheService.setToken
or writing to localStorage. Update handleSave to POST the token to an auth
endpoint (e.g., /api/auth/token) including CSRF protection and handle the server
response (success -> window.location.reload() if needed, failure -> show error),
remove direct local persistence calls to cacheService.setToken, and ensure token
state (the token variable) is cleared after submission; coordinate with backend
to validate and set the secure cookie.
| <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm"> | ||
| <div className="bg-github-dark sharp-border shadow-none w-full max-w-md overflow-hidden animate-in fade-in duration-200"> | ||
| <div className="flex items-center justify-between p-4 border-b border-github-border"> | ||
| <div className="flex items-center gap-2 text-white"> | ||
| <Key className="w-5 h-5" /> | ||
| <h2 className="font-semibold text-lg">Connect GitHub Token</h2> | ||
| </div> | ||
| <button | ||
| onClick={onClose} | ||
| className="text-github-muted hover:text-white transition-colors p-1" | ||
| > | ||
| <X className="w-5 h-5" /> | ||
| </button> | ||
| </div> | ||
|
|
||
| <div className="p-6 space-y-4"> | ||
| <p className="text-sm text-github-text"> | ||
| Required to continue browsing or to access private repositories. Your token never leaves your browser. | ||
| </p> | ||
|
|
||
| <div className="space-y-2"> | ||
| <label className="text-xs font-semibold text-white uppercase tracking-wider"> | ||
| Personal Access Token | ||
| </label> | ||
| <input | ||
| type="password" | ||
| value={token} | ||
| onChange={(e) => setToken(e.target.value)} | ||
| placeholder="ghp_..." | ||
| className="w-full bg-github-canvas sharp-border px-3 py-2 text-white placeholder:text-github-muted focus:outline-none focus:border-[var(--color-aossie-green)] transition-all font-mono" | ||
| /> |
There was a problem hiding this comment.
Modal is missing key accessibility semantics.
Add dialog attributes and accessible control labeling (role="dialog", aria-modal, aria-labelledby, close button aria-label, and label/input association).
Proposed fix
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
- <div className="bg-github-dark sharp-border shadow-none w-full max-w-md overflow-hidden animate-in fade-in duration-200">
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
+ <div
+ role="dialog"
+ aria-modal="true"
+ aria-labelledby="token-modal-title"
+ className="bg-github-dark sharp-border shadow-none w-full max-w-md overflow-hidden animate-in fade-in duration-200"
+ >
@@
- <h2 className="font-semibold text-lg">Connect GitHub Token</h2>
+ <h2 id="token-modal-title" className="font-semibold text-lg">Connect GitHub Token</h2>
@@
- <button
+ <button
onClick={onClose}
+ aria-label="Close token modal"
className="text-github-muted hover:text-white transition-colors p-1"
>
@@
- <label className="text-xs font-semibold text-white uppercase tracking-wider">
+ <label htmlFor="github-token" className="text-xs font-semibold text-white uppercase tracking-wider">
Personal Access Token
</label>
<input
+ id="github-token"
type="password"As per coding guidelines, the code should follow Lighthouse-aligned best practices for task completion and usability.
📝 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.
| <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm"> | |
| <div className="bg-github-dark sharp-border shadow-none w-full max-w-md overflow-hidden animate-in fade-in duration-200"> | |
| <div className="flex items-center justify-between p-4 border-b border-github-border"> | |
| <div className="flex items-center gap-2 text-white"> | |
| <Key className="w-5 h-5" /> | |
| <h2 className="font-semibold text-lg">Connect GitHub Token</h2> | |
| </div> | |
| <button | |
| onClick={onClose} | |
| className="text-github-muted hover:text-white transition-colors p-1" | |
| > | |
| <X className="w-5 h-5" /> | |
| </button> | |
| </div> | |
| <div className="p-6 space-y-4"> | |
| <p className="text-sm text-github-text"> | |
| Required to continue browsing or to access private repositories. Your token never leaves your browser. | |
| </p> | |
| <div className="space-y-2"> | |
| <label className="text-xs font-semibold text-white uppercase tracking-wider"> | |
| Personal Access Token | |
| </label> | |
| <input | |
| type="password" | |
| value={token} | |
| onChange={(e) => setToken(e.target.value)} | |
| placeholder="ghp_..." | |
| className="w-full bg-github-canvas sharp-border px-3 py-2 text-white placeholder:text-github-muted focus:outline-none focus:border-[var(--color-aossie-green)] transition-all font-mono" | |
| /> | |
| <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm"> | |
| <div | |
| role="dialog" | |
| aria-modal="true" | |
| aria-labelledby="token-modal-title" | |
| className="bg-github-dark sharp-border shadow-none w-full max-w-md overflow-hidden animate-in fade-in duration-200" | |
| > | |
| <div className="flex items-center justify-between p-4 border-b border-github-border"> | |
| <div className="flex items-center gap-2 text-white"> | |
| <Key className="w-5 h-5" /> | |
| <h2 id="token-modal-title" className="font-semibold text-lg">Connect GitHub Token</h2> | |
| </div> | |
| <button | |
| onClick={onClose} | |
| aria-label="Close token modal" | |
| className="text-github-muted hover:text-white transition-colors p-1" | |
| > | |
| <X className="w-5 h-5" /> | |
| </button> | |
| </div> | |
| <div className="p-6 space-y-4"> | |
| <p className="text-sm text-github-text"> | |
| Required to continue browsing or to access private repositories. Your token never leaves your browser. | |
| </p> | |
| <div className="space-y-2"> | |
| <label htmlFor="github-token" className="text-xs font-semibold text-white uppercase tracking-wider"> | |
| Personal Access Token | |
| </label> | |
| <input | |
| id="github-token" | |
| type="password" | |
| value={token} | |
| onChange={(e) => setToken(e.target.value)} | |
| placeholder="ghp_..." | |
| className="w-full bg-github-canvas sharp-border px-3 py-2 text-white placeholder:text-github-muted focus:outline-none focus:border-[var(--color-aossie-green)] transition-all font-mono" | |
| /> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/common/TokenModal.tsx` around lines 23 - 53, Add proper dialog
semantics to the TokenModal component: give the modal container the attributes
role="dialog" and aria-modal="true" and set aria-labelledby to the heading's id
(add an id to the <h2> used in the header, e.g., modal-title). Ensure the
label/input are associated by converting the visible label to a <label
htmlFor="token-input"> and give the input an id="token-input" (keep
value={token} and onChange calling setToken). Add aria-describedby on the dialog
pointing to the explanatory paragraph's id for additional context, and add an
accessible name to the close button (e.g., aria-label="Close token modal" on the
button that calls onClose).
| <h2 className="font-semibold text-lg">Connect GitHub Token</h2> | ||
| </div> | ||
| <button | ||
| onClick={onClose} | ||
| className="text-github-muted hover:text-white transition-colors p-1" | ||
| > | ||
| <X className="w-5 h-5" /> | ||
| </button> | ||
| </div> | ||
|
|
||
| <div className="p-6 space-y-4"> | ||
| <p className="text-sm text-github-text"> | ||
| Required to continue browsing or to access private repositories. Your token never leaves your browser. | ||
| </p> | ||
|
|
||
| <div className="space-y-2"> | ||
| <label className="text-xs font-semibold text-white uppercase tracking-wider"> | ||
| Personal Access Token | ||
| </label> | ||
| <input | ||
| type="password" | ||
| value={token} | ||
| onChange={(e) => setToken(e.target.value)} | ||
| placeholder="ghp_..." | ||
| className="w-full bg-github-canvas sharp-border px-3 py-2 text-white placeholder:text-github-muted focus:outline-none focus:border-[var(--color-aossie-green)] transition-all font-mono" | ||
| /> | ||
| </div> | ||
|
|
||
| <div className="flex items-start gap-2 text-xs text-github-muted bg-github-canvas p-3 sharp-border border-dashed"> | ||
| <ShieldCheck className="w-4 h-4 shrink-0 mt-0.5 text-[var(--color-aossie-yellow)]" /> | ||
| <p className="font-mono"> | ||
| We only request read:org and public_repo scopes. Store token in | ||
| <a href="https://github.com/settings/tokens" target="_blank" rel="noreferrer" className="text-[var(--color-aossie-green)] hover:underline mx-1 font-bold"> | ||
| GitHub Settings | ||
| </a>. | ||
| </p> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div className="p-4 border-t border-github-border bg-github-dark flex justify-end gap-3 font-mono"> | ||
| <button | ||
| onClick={onClose} | ||
| className="px-4 py-2 text-sm text-github-muted uppercase font-bold hover:text-white transition-colors sharp-interactive border-transparent" | ||
| > | ||
| Cancel | ||
| </button> | ||
| <button | ||
| onClick={handleSave} | ||
| disabled={!token.trim()} | ||
| className="px-4 py-2 text-sm bg-[var(--color-aossie-green)] hover:bg-[var(--color-aossie-yellow)] text-black sharp-border font-black uppercase transition-colors disabled:opacity-50 disabled:cursor-not-allowed" | ||
| > | ||
| Save & Continue | ||
| </button> |
There was a problem hiding this comment.
Externalize modal copy for localization.
All user-facing strings in this modal should be moved to i18n resource files.
As per coding guidelines, "User-visible strings should be externalized to resource files (i18n)".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/common/TokenModal.tsx` around lines 28 - 80, The TokenModal
component currently contains hard-coded user-visible strings; extract all text
(titles, descriptions, labels, placeholders, button text, and link text) into
i18n resource keys and replace literals in TokenModal (e.g., the "Connect GitHub
Token" header, the paragraph about token usage, "Personal Access Token" label,
placeholder "ghp_...", the scope description and link text, "Cancel", and "Save
& Continue") with localized lookups (e.g., t('tokenModal.title')) and ensure
TokenModal imports/uses the i18n hook/function (e.g., useTranslation or t) so
token, handleSave, and onClose logic remain unchanged while strings come from
resource files.
| // IndexedDB (large datastores) | ||
| async get<StoreName extends 'repos' | 'contributors' | 'activity'>( | ||
| storeName: StoreName, | ||
| key: string | ||
| ): Promise<any | null> { | ||
| const db = await getDB(); | ||
| const metadata = await db.get('metadata', `${storeName}_${key}`); | ||
|
|
||
| if (!metadata || Date.now() - metadata.timestamp > TTL_MS) { | ||
| return null; // Cache default/miss | ||
| } | ||
| return db.get(storeName, key); | ||
| }, | ||
|
|
||
| async set<StoreName extends 'repos' | 'contributors' | 'activity'>( | ||
| storeName: StoreName, | ||
| key: string, | ||
| value: any | ||
| ) { | ||
| const db = await getDB(); | ||
| const tx = db.transaction([storeName, 'metadata'], 'readwrite'); | ||
| await Promise.all([ | ||
| tx.objectStore(storeName).put(value, key), | ||
| tx.objectStore('metadata').put({ timestamp: Date.now() }, `${storeName}_${key}`), | ||
| tx.done, | ||
| ]); | ||
| }, |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Add error handling for IndexedDB operations.
IndexedDB operations can fail (e.g., storage quota exceeded, private browsing restrictions). The current implementation doesn't handle these failures, which could cause unhandled promise rejections.
♻️ Proposed error handling
async get<StoreName extends 'repos' | 'contributors' | 'activity'>(
storeName: StoreName,
key: string
): Promise<any | null> {
+ try {
const db = await getDB();
const metadata = await db.get('metadata', `${storeName}_${key}`);
if (!metadata || Date.now() - metadata.timestamp > TTL_MS) {
return null;
}
return db.get(storeName, key);
+ } catch (error) {
+ console.warn(`Cache get failed for ${storeName}/${key}:`, error);
+ return null; // Graceful degradation
+ }
},
async set<StoreName extends 'repos' | 'contributors' | 'activity'>(
storeName: StoreName,
key: string,
value: any
) {
+ try {
const db = await getDB();
const tx = db.transaction([storeName, 'metadata'], 'readwrite');
await Promise.all([
tx.objectStore(storeName).put(value, key),
tx.objectStore('metadata').put({ timestamp: Date.now() }, `${storeName}_${key}`),
tx.done,
]);
+ } catch (error) {
+ console.warn(`Cache set failed for ${storeName}/${key}:`, error);
+ // Continue without caching - app should still work
+ }
},📝 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.
| // IndexedDB (large datastores) | |
| async get<StoreName extends 'repos' | 'contributors' | 'activity'>( | |
| storeName: StoreName, | |
| key: string | |
| ): Promise<any | null> { | |
| const db = await getDB(); | |
| const metadata = await db.get('metadata', `${storeName}_${key}`); | |
| if (!metadata || Date.now() - metadata.timestamp > TTL_MS) { | |
| return null; // Cache default/miss | |
| } | |
| return db.get(storeName, key); | |
| }, | |
| async set<StoreName extends 'repos' | 'contributors' | 'activity'>( | |
| storeName: StoreName, | |
| key: string, | |
| value: any | |
| ) { | |
| const db = await getDB(); | |
| const tx = db.transaction([storeName, 'metadata'], 'readwrite'); | |
| await Promise.all([ | |
| tx.objectStore(storeName).put(value, key), | |
| tx.objectStore('metadata').put({ timestamp: Date.now() }, `${storeName}_${key}`), | |
| tx.done, | |
| ]); | |
| }, | |
| // IndexedDB (large datastores) | |
| async get<StoreName extends 'repos' | 'contributors' | 'activity'>( | |
| storeName: StoreName, | |
| key: string | |
| ): Promise<any | null> { | |
| try { | |
| const db = await getDB(); | |
| const metadata = await db.get('metadata', `${storeName}_${key}`); | |
| if (!metadata || Date.now() - metadata.timestamp > TTL_MS) { | |
| return null; // Cache default/miss | |
| } | |
| return db.get(storeName, key); | |
| } catch (error) { | |
| console.warn(`Cache get failed for ${storeName}/${key}:`, error); | |
| return null; // Graceful degradation | |
| } | |
| }, | |
| async set<StoreName extends 'repos' | 'contributors' | 'activity'>( | |
| storeName: StoreName, | |
| key: string, | |
| value: any | |
| ) { | |
| try { | |
| const db = await getDB(); | |
| const tx = db.transaction([storeName, 'metadata'], 'readwrite'); | |
| await Promise.all([ | |
| tx.objectStore(storeName).put(value, key), | |
| tx.objectStore('metadata').put({ timestamp: Date.now() }, `${storeName}_${key}`), | |
| tx.done, | |
| ]); | |
| } catch (error) { | |
| console.warn(`Cache set failed for ${storeName}/${key}:`, error); | |
| // Continue without caching - app should still work | |
| } | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/services/cache.ts` around lines 52 - 78, The get and set IndexedDB
operations (functions get and set using getDB, TTL_MS, db.transaction,
tx.objectStore) lack error handling and can cause unhandled rejections; wrap the
body of both get and set in try/catch blocks, catch any errors from await
getDB(), db.get(), and the transaction Promise, log the error with contextual
info (storeName and key) and for get return null on failure, while for set log
the error and ensure the transaction is aborted/cleaned up (call tx.abort if
available) without letting the exception bubble up to callers.
| const response = await fetch(`${API_BASE}${url}`, { | ||
| headers: getHeaders(), | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| if (response.status === 403 || response.status === 401) { | ||
| // Trigger generic rate limit event | ||
| window.dispatchEvent(new CustomEvent('github-api-limit', { detail: response.headers })); | ||
| } | ||
| throw new GitHubApiError(response.status, `GitHub API Error: ${response.statusText}`); | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider adding request timeout to prevent hanging requests.
Network requests to the GitHub API have no timeout configured. A slow or unresponsive API could leave the UI in a loading state indefinitely.
♻️ Proposed timeout with AbortController
+const FETCH_TIMEOUT_MS = 30000;
+
+async function fetchWithTimeout(url: string, options: RequestInit): Promise<Response> {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
+ try {
+ return await fetch(url, { ...options, signal: controller.signal });
+ } finally {
+ clearTimeout(timeoutId);
+ }
+}
fetchWithCache: async <T>(
url: string,
storeName: 'repos' | 'contributors' | 'activity',
cacheKey: string
): Promise<T> => {
// ...
- const response = await fetch(`${API_BASE}${url}`, {
+ const response = await fetchWithTimeout(`${API_BASE}${url}`, {
headers: getHeaders(),
});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/services/githubApi.ts` around lines 46 - 56, Add a request timeout to the
GitHub API fetch in src/services/githubApi.ts by using an AbortController:
create an AbortController before calling fetch, pass controller.signal to fetch
(alongside headers from getHeaders()), and set a timer (e.g., via setTimeout)
that calls controller.abort() after the desired timeout; clear the timer after
fetch completes. Catch the abort case (check for DOMException name 'AbortError'
or similar) and rethrow a GitHubApiError with a clear timeout message so callers
can handle it, while preserving the existing status-based logic (including the
github-api-limit event dispatch) for non-timeout failures. Ensure the timer is
cleaned up in all code paths.
| getOrgDetails: async (org: string) => { | ||
| // Standard fetch, small enough to bypass idb cache or cache separately | ||
| const response = await fetch(`${API_BASE}/orgs/${org}`, { | ||
| headers: getHeaders(), | ||
| }); | ||
| if (!response.ok) throw new Error('Org not found'); | ||
| return response.json(); | ||
| }, |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Add input validation for organization name.
getOrgDetails doesn't validate the org parameter. An empty or malformed org name could result in unexpected API calls. Consider adding basic validation.
🛡️ Proposed validation
getOrgDetails: async (org: string) => {
+ if (!org || typeof org !== 'string' || org.trim() === '') {
+ throw new Error('Invalid organization name');
+ }
const response = await fetch(`${API_BASE}/orgs/${org}`, {
headers: getHeaders(),
});
if (!response.ok) throw new Error('Org not found');
return response.json();
},Consider applying similar validation to other methods that accept org and repo parameters.
📝 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.
| getOrgDetails: async (org: string) => { | |
| // Standard fetch, small enough to bypass idb cache or cache separately | |
| const response = await fetch(`${API_BASE}/orgs/${org}`, { | |
| headers: getHeaders(), | |
| }); | |
| if (!response.ok) throw new Error('Org not found'); | |
| return response.json(); | |
| }, | |
| getOrgDetails: async (org: string) => { | |
| // Standard fetch, small enough to bypass idb cache or cache separately | |
| if (!org || typeof org !== 'string' || org.trim() === '') { | |
| throw new Error('Invalid organization name'); | |
| } | |
| const response = await fetch(`${API_BASE}/orgs/${org}`, { | |
| headers: getHeaders(), | |
| }); | |
| if (!response.ok) throw new Error('Org not found'); | |
| return response.json(); | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/services/githubApi.ts` around lines 63 - 70, getOrgDetails currently
calls the GitHub API without validating the org parameter; add input validation
at the start of getOrgDetails to ensure org is a non-empty string matching
GitHub org name rules (e.g. allowed chars and length, such as
/^[A-Za-z0-9-]{1,39}$/), and throw a clear error (e.g. "Invalid organization
name") before making the fetch; apply the same validation pattern to other
service methods that accept org or repo parameters to prevent malformed API
calls.
src/services/githubApi.ts
Outdated
| getOrgRepos: async (org: string) => { | ||
| // Using fetchWithCache allows us to cache the entire repo list | ||
| // Note: A real implementation might need to handle pagination if an org has >100 repos. | ||
| // We'll fetch the first 100 for simplicity in this demo. | ||
| return githubApi.fetchWithCache<any[]>( | ||
| `/orgs/${org}/repos?per_page=100&sort=pushed&direction=desc`, | ||
| 'repos', | ||
| org | ||
| ); | ||
| }, | ||
|
|
||
| getRepoContributors: async (org: string, repo: string) => { | ||
| return githubApi.fetchWithCache<any[]>( | ||
| `/repos/${org}/${repo}/contributors?per_page=100`, | ||
| 'contributors', | ||
| `${org}_${repo}` | ||
| ); | ||
| }, | ||
|
|
||
| getRepoActivity: async (org: string, repo: string) => { | ||
| return githubApi.fetchWithCache<any[]>( | ||
| `/repos/${org}/${repo}/stats/commit_activity`, | ||
| 'activity', | ||
| `${org}_${repo}` | ||
| ); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Define explicit return types instead of any[].
The cached endpoint methods return Promise<any[]>, losing type information. Define interfaces matching GitHub API responses for better type safety downstream.
♻️ Proposed type definitions
+interface GitHubRepo {
+ id: number;
+ name: string;
+ full_name: string;
+ html_url: string;
+ description?: string;
+ language?: string;
+ stargazers_count: number;
+ forks_count: number;
+ open_issues_count: number;
+ pushed_at: string;
+}
+
+interface GitHubContributor {
+ login: string;
+ avatar_url: string;
+ html_url: string;
+ contributions: number;
+}
+
+interface GitHubCommitActivity {
+ week: number;
+ total: number;
+ days: number[];
+}
getOrgRepos: async (org: string) => {
- return githubApi.fetchWithCache<any[]>(
+ return githubApi.fetchWithCache<GitHubRepo[]>(
`/orgs/${org}/repos?per_page=100&sort=pushed&direction=desc`,
'repos',
org
);
},
getRepoContributors: async (org: string, repo: string) => {
- return githubApi.fetchWithCache<any[]>(
+ return githubApi.fetchWithCache<GitHubContributor[]>(
`/repos/${org}/${repo}/contributors?per_page=100`,
'contributors',
`${org}_${repo}`
);
},
getRepoActivity: async (org: string, repo: string) => {
- return githubApi.fetchWithCache<any[]>(
+ return githubApi.fetchWithCache<GitHubCommitActivity[]>(
`/repos/${org}/${repo}/stats/commit_activity`,
'activity',
`${org}_${repo}`
);
}📝 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.
| getOrgRepos: async (org: string) => { | |
| // Using fetchWithCache allows us to cache the entire repo list | |
| // Note: A real implementation might need to handle pagination if an org has >100 repos. | |
| // We'll fetch the first 100 for simplicity in this demo. | |
| return githubApi.fetchWithCache<any[]>( | |
| `/orgs/${org}/repos?per_page=100&sort=pushed&direction=desc`, | |
| 'repos', | |
| org | |
| ); | |
| }, | |
| getRepoContributors: async (org: string, repo: string) => { | |
| return githubApi.fetchWithCache<any[]>( | |
| `/repos/${org}/${repo}/contributors?per_page=100`, | |
| 'contributors', | |
| `${org}_${repo}` | |
| ); | |
| }, | |
| getRepoActivity: async (org: string, repo: string) => { | |
| return githubApi.fetchWithCache<any[]>( | |
| `/repos/${org}/${repo}/stats/commit_activity`, | |
| 'activity', | |
| `${org}_${repo}` | |
| ); | |
| } | |
| interface GitHubRepo { | |
| id: number; | |
| name: string; | |
| full_name: string; | |
| html_url: string; | |
| description?: string; | |
| language?: string; | |
| stargazers_count: number; | |
| forks_count: number; | |
| open_issues_count: number; | |
| pushed_at: string; | |
| } | |
| interface GitHubContributor { | |
| login: string; | |
| avatar_url: string; | |
| html_url: string; | |
| contributions: number; | |
| } | |
| interface GitHubCommitActivity { | |
| week: number; | |
| total: number; | |
| days: number[]; | |
| } | |
| getOrgRepos: async (org: string) => { | |
| // Using fetchWithCache allows us to cache the entire repo list | |
| // Note: A real implementation might need to handle pagination if an org has >100 repos. | |
| // We'll fetch the first 100 for simplicity in this demo. | |
| return githubApi.fetchWithCache<GitHubRepo[]>( | |
| `/orgs/${org}/repos?per_page=100&sort=pushed&direction=desc`, | |
| 'repos', | |
| org | |
| ); | |
| }, | |
| getRepoContributors: async (org: string, repo: string) => { | |
| return githubApi.fetchWithCache<GitHubContributor[]>( | |
| `/repos/${org}/${repo}/contributors?per_page=100`, | |
| 'contributors', | |
| `${org}_${repo}` | |
| ); | |
| }, | |
| getRepoActivity: async (org: string, repo: string) => { | |
| return githubApi.fetchWithCache<GitHubCommitActivity[]>( | |
| `/repos/${org}/${repo}/stats/commit_activity`, | |
| 'activity', | |
| `${org}_${repo}` | |
| ); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/services/githubApi.ts` around lines 72 - 97, Replace the generic any[]
return types by defining precise response interfaces (e.g., OrgRepo,
RepoContributor, CommitActivity) that match the GitHub API shapes and use those
interfaces as the generic type parameters on githubApi.fetchWithCache and the
functions getOrgRepos, getRepoContributors, and getRepoActivity so each function
returns Promise<OrgRepo[]>, Promise<RepoContributor[]>, and
Promise<CommitActivity[]> respectively; add or import those interfaces near this
module and update the function signatures and fetchWithCache calls to use them
for proper type safety.
… to OverviewTab, sync graph edge weights, and remove unused components
There was a problem hiding this comment.
Actionable comments posted: 11
🤖 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/dashboard/ContributorRepoGraph.tsx`:
- Around line 401-424: The graph is only operable by pointer; add
keyboard/assistive access by exposing a focusable, screen-reader-friendly
mechanism that maps canvas nodes to DOM controls and keyboard handlers: render
an offscreen or visually-hidden list of nodes (repos/contributors) with buttons
or links tied to the same handlers used by ForceGraph2D (reuse setSelectedNode,
setHoverNodeId and fgRef) and add keyboard handlers (onKeyDown) to allow arrow
navigation and Enter/Space activation to select/toggle nodes; also ensure the
ForceGraph2D wrapper has appropriate aria-label/role and updates aria-live or
aria-selected states so assistive tech sees selection changes.
- Around line 89-90: hoverNodeId and selectedNode persist across dataset
replacements causing stale hover/selection UI; add a useEffect that watches the
nodes and links props (or whatever variables hold the current dataset) and calls
setHoverNodeId(null) and setSelectedNode(null) when they change, and refactor
the click/close handlers (and any consumers) to read/write a selectedNodeId
(e.g., replace selectedNode usage with selectedNodeId state and
setSelectedNodeId) so selection is driven by the current dataset and is cleared
on data refresh; update all references to hoverNodeId, setHoverNodeId,
selectedNode, setSelectedNode, and the click/close handlers accordingly.
- Around line 257-275: The component currently hardcodes user-visible strings
(e.g., the badgeText construction using "commits", plus "Contributor",
"Repository", "Open GitHub Profile" and legend labels rendered in
ContributorRepoGraph) — replace those literals by fetching localized strings
from the app's i18n/resource module (e.g., import the shared resource or i18n.t
and use keys like repoGraph.commits, repoGraph.contributor,
repoGraph.repository, repoGraph.openProfile, repoGraph.legend.*). Update the
code locations that build badgeText (variable badgeText), the side-panel labels
and legend-rendering code (the block around lines ~458–545 mentioned) to use the
resource lookup instead of hardcoded text, and ensure the text measurement
(ctx.measureText) and canvas drawing still use the returned localized string.
- Around line 138-143: The y-force currently treats missing
GraphNode.daysSinceActive as 0 (most recent); change fg.d3Force("y") so it first
checks for null/undefined and uses a sentinel (e.g., 180 days) or another
agreed-upon value for "N/A" before clamping—e.g., days = n.daysSinceActive ==
null ? 180 : Math.max(0, Math.min(180, n.daysSinceActive)); then compute the y
as before so nodes with unknown activity are positioned consistently with the UI
that renders "N/A".
- Around line 110-115: The ResizeObserver in the ContributorRepoGraph component
forces a minimum canvas width via Math.max(520, ...) which causes clipping on
narrow viewports; remove the 520 floor so setGraphWidth(Math.floor(w)) (or
guard-null) in the useEffect/ResizeObserver callback (references: useEffect,
containerRef, ResizeObserver, setGraphWidth). Also stop hiding overflowing graph
content — change the wrapper's overflow style from "hidden" to "auto" or
"visible" (references: the container/wrapper CSS or inline style around the
graph that currently sets overflow: hidden) so the graph can scroll/be reached
on small screens.
- Line 86: The fgRef is currently typed as any and several event/draw callbacks
drop to any; replace these with explicit types from react-force-graph-2d by
creating concrete Node and Link interfaces for this component and typing the ref
as useRef<ForceGraphMethods<MyNode, MyLink> | null>, then update the
ReactForceGraph2D generic parameters and all handlers to use those types (e.g.,
nodeCanvasObject, linkCanvasObject, onNodeClick, onNodeHover, onLinkClick
signatures should use MyNode/MyLink instead of any); ensure canvas draw handlers
accept the typed 2D canvas context and the library-provided node/link parameter
shapes so TypeScript enforces correct fields and removes all remaining any
usages around fgRef and the force-graph callbacks.
In `@src/components/dashboard/ContributorsTab.tsx`:
- Line 187: The useEffect in ContributorsTab is depending on
JSON.stringify(orgNames) which is fragile; stabilize the dependency by memoizing
orgNames (e.g., create a memoized value via useMemo or derive a stable key in
state) or replace the stringified dependency with a stable reference and use a
deep-equality effect helper. Update the dependency array to reference the
memoized variable (the stableOrgNames or similar) used inside the effect, and
ensure any setters or derived values (orgNames) are updated to preserve the
stable identity.
In `@src/components/dashboard/OverviewTab.tsx`:
- Line 300: The useEffect in OverviewTab is depending on
JSON.stringify(orgNames), which is fragile; instead stabilize the dependency by
memoizing a deterministic representation of orgNames (e.g., create a memoized
string or tuple) using useMemo and depend on that memoized value in the
useEffect. Update the component (OverviewTab) to compute memoizedOrgNames =
useMemo(() => /* deterministic representation of orgNames */, [orgNames]) and
replace JSON.stringify(orgNames) with memoizedOrgNames in the useEffect
dependency array so the effect only re-runs when orgNames truly changes.
- Around line 271-273: The variable and comment are misleading:
totalCommitsLast3Months and its comment reference "commits" but
weekBuckets.reduce(...) sums fork + PRs opened + issues opened; rename the
variable and comment to reflect aggregated activity (e.g.,
totalActivityLast3Months or totalContributionsLast3Months) and update derived
avgCommitsPerWeek to avgActivityPerWeek (or similar) along with any state/prop
names and UI labels that display these values; locate usage sites by searching
for weekBuckets, totalCommitsLast3Months, and avgCommitsPerWeek and change their
names/labels to match the new, accurate terminology.
- Around line 79-86: combinedOrgData currently references validOrgs[0] and is
mutated directly; instead create a shallow copy before modifying to avoid
mutating the fetched object. Change the initialization to create a new object
(e.g., use the spread operator on validOrgs[0]) so combinedOrgData =
validOrgs.length > 0 ? { ...validOrgs[0] } : null, then set combinedOrgData.name
and combinedOrgData.description when validOrgs.length > 1; also ensure you guard
against combinedOrgData being null before assigning.
In `@src/components/dashboard/ReposTab.tsx`:
- Line 75: The useEffect inside the ReposTab component currently depends on
JSON.stringify(orgNames), which creates a new string each render and leads to
unnecessary re-runs; replace that by stabilizing the array reference (e.g.,
create memoizedOrgNames via useMemo in ReposTab that returns orgNames but only
changes when its contents change) and use memoizedOrgNames in the useEffect
dependency array (alternatively, swap to a deep-compare effect hook). Update
references to orgNames in the useEffect to use memoizedOrgNames so the effect
only runs when the actual array contents change.
🪄 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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 6d3f88dd-4441-4478-b713-0be779113def
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (10)
.gitignorepackage.jsonsrc/components/dashboard/ContributorRepoGraph.tsxsrc/components/dashboard/ContributorsTab.tsxsrc/components/dashboard/OverviewTab.tsxsrc/components/dashboard/RepoDetailPanel.tsxsrc/components/dashboard/ReposTab.tsxsrc/components/layout/Sidebar.tsxsrc/services/cache.tssrc/services/githubApi.ts
|
|
||
| /* ─── component ──────────────────────────────────────────────────── */ | ||
| export function ContributorRepoGraph({ nodes, links }: Props) { | ||
| const fgRef = useRef<any>(null); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
head -200 src/components/dashboard/ContributorRepoGraph.tsx | cat -nRepository: AOSSIE-Org/OrgExplorer
Length of output: 7038
🏁 Script executed:
cat -n src/components/dashboard/ContributorRepoGraph.tsx | sed -n '120,150p; 160,180p; 340,360p; 380,400p; 410,430p'Repository: AOSSIE-Org/OrgExplorer
Length of output: 4505
🏁 Script executed:
cat package.json | grep -A 5 -B 5 "force-graph"Repository: AOSSIE-Org/OrgExplorer
Length of output: 360
🏁 Script executed:
npm list react-force-graph-2d 2>/dev/null | head -5Repository: AOSSIE-Org/OrgExplorer
Length of output: 115
🌐 Web query:
react-force-graph-2d TypeScript types ForceGraphMethods
💡 Result:
react-force-graph-2d provides full TypeScript support with type definitions in dist/react-force-graph-2d.d.ts (latest version 1.29.1 as of Feb 2026). The main types are ForceGraphProps for component props and ForceGraphMethods for the ref instance methods. Key usage: import React, { useRef, useEffect } from 'react'; import ForceGraph2D from 'react-force-graph-2d'; function MyGraph { const fgRef = useRef(null); useEffect( => { if (fgRef.current) { // Access methods like: fgRef.current.d3Force('charge', d3.forceManyBody.strength(-300)); fgRef.current.zoomToFit(0, 100); } }, []); return ; } ForceGraphMethods includes: - emitParticle(link: LinkObject): ForceGraphInstance - d3Force(forceName: string, forceFn?: ForceFn): ForceFn | ForceGraphInstance - d3ReheatSimulation: ForceGraphInstance - pauseAnimation: ForceGraphInstance - resumeAnimation: ForceGraphInstance - centerAt(x?: number, y?: number, durationMs?: number): ForceGraphInstance - zoom(scale?: number, durationMs?: number): ForceGraphInstance - zoomToFit(durationMs?: number, padding?: number): ForceGraphInstance - getGraphBbox: {x: [number,number], y: [number,number]} - screen2GraphCoords(screenX: number, screenY: number): {x: number, y: number} - graph2ScreenCoords(graphX: number, graphY: number): {x: number, y: number} ForceGraphProps includes graphData, nodeId, linkSource/target, width/height, nodeRelSize/val/label/color/etc., link styling props, force config like dagMode, d3AlphaDecay, etc., event handlers. Types are generic over node/link data: ForceGraph2D<NodeObject = NodeObject, LinkObject = LinkObject>. Official repo: https://github.com/vasturiano/react-force-graph. NPM: https://www.npmjs.com/package/react-force-graph-2d. Types viewable at unpkg.com/react-force-graph-2d@1.29.1/dist/react-force-graph-2d.d.ts (or earlier versions like 1.16.1 shown).
Citations:
- 1: https://app.unpkg.com/react-force-graph-2d@1.16.1/files/dist/react-force-graph-2d.d.ts
- 2: Typescript support vasturiano/react-force-graph#87
- 3: https://npmjs.com/package/react-force-graph-2d
- 4: https://github.com/vasturiano/react-force-graph
- 5: https://www.npmjs.com/package/react-force-graph-2d
- 6: d3Force property is not available in ForceGraph2D vasturiano/react-force-graph#442
- 7: d3Force method vasturiano/react-force-graph#25
- 8: https://app.unpkg.com/react-force-graph-2d@1.14.0/files/dist/react-force-graph-2d.d.ts
- 9: Make graph data generic vasturiano/react-force-graph#435
🌐 Web query:
react-force-graph-2d 1.29.1 NodeObject LinkObject ForceGraphMethods type definitions
💡 Result:
The type definitions for react-force-graph-2d version 1.29.1 are available in the package's dist/react-force-graph-2d.d.ts file (7.2KB). They follow the generic pattern from the underlying force-graph library and recent versions, using generic types for extensibility. NodeObject: type NodeObject<NodeType = {}> = NodeType & { id?: string | number; x?: number; y?: number; vx?: number; vy?: number; fx?: number; fy?: number; [others: string]: any; }; LinkObject: type LinkObject<NodeType = {}, LinkType = {}> = LinkType & { source?: string | number | NodeObject; target?: string | number | NodeObject; [others: string]: any; }; ForceGraphMethods (ref type, extends ForceGraphInstance from force-graph): interface ForceGraphMethods<NodeType = {}, LinkType = {}> { // Link styling emitParticle(link: LinkObject<NodeType, LinkType>): ForceGraphInstance; // Force engine configuration d3Force(forceName: 'link' | 'charge' | 'center' | string): ForceFn<NodeObject> | undefined; d3Force(forceName: 'link' | 'charge' | 'center' | string, forceFn: ForceFn<NodeObject> | null): ForceGraphInstance; d3ReheatSimulation: ForceGraphInstance; // Render control pauseAnimation: ForceGraphInstance; resumeAnimation: ForceGraphInstance; centerAt(x?: number, y?: number, durationMs?: number): ForceGraphInstance; zoom(scale?: number, durationMs?: number): ForceGraphInstance; // ... additional methods like screen2GraphCoords, etc. } The component is typed as React.FunctionComponent<ForceGraphProps<NodeType, LinkType>> with ref returning ForceGraphMethods<NodeType, LinkType>. Accessors are defined as type Accessor<In, Out> = Out | string | ((obj: In) => Out);. View the full .d.ts at https://unpkg.com/react-force-graph-2d@1.29.1/dist/react-force-graph-2d.d.ts (direct raw access). Similar to 3D version and force-graph src/index.d.ts patterns, updated for recent releases (Sep 2025-Feb 2026).
Citations:
- 1: https://registry.npmjs.org/react-force-graph-2d
- 2: https://github.com/vasturiano/react-force-graph
- 3: https://www.npmjs.com/package/react-force-graph-2d
- 4: https://app.unpkg.com/react-force-graph-2d@1.23.15/files/dist/react-force-graph-2d.d.ts
- 5: https://github.com/vasturiano/react-force-graph/blob/master/src/packages/react-force-graph-3d/index.d.ts
- 6: https://app.unpkg.com/react-force-graph-2d@1.29.1
- 7: https://www.npmjs.com/package/force-graph
- 8: https://app.unpkg.com/force-graph@1.47.0/files/src/index.d.ts
- 9: https://app.unpkg.com/react-force-graph-2d@1.14.0/files/dist/react-force-graph-2d.d.ts
- 10: https://app.unpkg.com/react-force-graph-3d%401.29.1/files/dist/react-force-graph-3d.d.ts
- 11: https://app.unpkg.com/react-force-graph-2d@1.16.1/files/dist/react-force-graph-2d.d.ts
🏁 Script executed:
cat -n src/components/dashboard/ContributorRepoGraph.tsx | sed -n '1,10p; 86p; 128p; 129p; 165p; 166p; 347p; 348p; 388p; 389p; 415p; 416p; 417p'Repository: AOSSIE-Org/OrgExplorer
Length of output: 1147
Replace the any types at the force-graph boundary.
The ref, force callbacks, canvas draw handlers, and node event handlers all drop to any, which removes type safety from the most mutation-heavy part of the component. Please wire these up with the library's typed ref/node/link interfaces instead. The react-force-graph-2d library (v1.29.1) provides full TypeScript support with ForceGraphMethods<NodeType, LinkType> for the ref and proper types for node/link parameters. As per coding guidelines, "TypeScript: Avoid 'any', use explicit types".
Lines affected: 86, 128–129, 165–166, 347–348, 388–389, 415–417
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/dashboard/ContributorRepoGraph.tsx` at line 86, The fgRef is
currently typed as any and several event/draw callbacks drop to any; replace
these with explicit types from react-force-graph-2d by creating concrete Node
and Link interfaces for this component and typing the ref as
useRef<ForceGraphMethods<MyNode, MyLink> | null>, then update the
ReactForceGraph2D generic parameters and all handlers to use those types (e.g.,
nodeCanvasObject, linkCanvasObject, onNodeClick, onNodeHover, onLinkClick
signatures should use MyNode/MyLink instead of any); ensure canvas draw handlers
accept the typed 2D canvas context and the library-provided node/link parameter
shapes so TypeScript enforces correct fields and removes all remaining any
usages around fgRef and the force-graph callbacks.
| const [hoverNodeId, setHoverNodeId] = useState<string | null>(null); | ||
| const [selectedNode, setSelectedNode] = useState<GraphNode | null>(null); |
There was a problem hiding this comment.
Reset hover and selection state when the dataset changes.
hoverNodeId and selectedNode survive nodes/links replacement. After an org/search refresh, that can leave the whole graph dimmed because the hovered id no longer exists, and the side panel can keep showing details from the previous dataset.
🧭 Suggested direction
- const [selectedNode, setSelectedNode] = useState<GraphNode | null>(null);
+ const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
+ const selectedNode = useMemo(
+ () => nodes.find((node) => node.id === selectedNodeId) ?? null,
+ [nodes, selectedNodeId],
+ );
+
+ useEffect(() => {
+ if (hoverNodeId && !nodes.some((node) => node.id === hoverNodeId)) {
+ setHoverNodeId(null);
+ }
+ }, [hoverNodeId, nodes]);Then update the click/close handlers to read and write selectedNodeId.
Also applies to: 94-105, 416-420, 427-527
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/dashboard/ContributorRepoGraph.tsx` around lines 89 - 90,
hoverNodeId and selectedNode persist across dataset replacements causing stale
hover/selection UI; add a useEffect that watches the nodes and links props (or
whatever variables hold the current dataset) and calls setHoverNodeId(null) and
setSelectedNode(null) when they change, and refactor the click/close handlers
(and any consumers) to read/write a selectedNodeId (e.g., replace selectedNode
usage with selectedNodeId state and setSelectedNodeId) so selection is driven by
the current dataset and is cleared on data refresh; update all references to
hoverNodeId, setHoverNodeId, selectedNode, setSelectedNode, and the click/close
handlers accordingly.
| useEffect(() => { | ||
| if (!containerRef.current) return; | ||
| const obs = new ResizeObserver((entries) => { | ||
| const w = entries[0]?.contentRect?.width; | ||
| if (w) setGraphWidth(Math.max(520, Math.floor(w))); | ||
| }); |
There was a problem hiding this comment.
Drop the 520px width floor on narrow viewports.
Line 114 forces the canvas to stay at least 520px wide, while Line 400 hides overflow. On smaller screens, that clips part of the graph and makes nodes unreachable.
📱 Suggested fix
- if (w) setGraphWidth(Math.max(520, Math.floor(w)));
+ if (w != null) setGraphWidth(Math.floor(w));Also applies to: 400-405
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/dashboard/ContributorRepoGraph.tsx` around lines 110 - 115,
The ResizeObserver in the ContributorRepoGraph component forces a minimum canvas
width via Math.max(520, ...) which causes clipping on narrow viewports; remove
the 520 floor so setGraphWidth(Math.floor(w)) (or guard-null) in the
useEffect/ResizeObserver callback (references: useEffect, containerRef,
ResizeObserver, setGraphWidth). Also stop hiding overflowing graph content —
change the wrapper's overflow style from "hidden" to "auto" or "visible"
(references: the container/wrapper CSS or inline style around the graph that
currently sets overflow: hidden) so the graph can scroll/be reached on small
screens.
| fg.d3Force("y") | ||
| ?.y((n: GraphNode) => { | ||
| const days = Math.max(0, Math.min(180, n.daysSinceActive || 0)); | ||
| return -300 + (days / 180) * 600; | ||
| }) | ||
| .strength(0.4); |
There was a problem hiding this comment.
Don’t treat unknown activity as “0 days ago.”
Line 140 falls back to 0 when daysSinceActive is missing, but Lines 473-475 and 519-521 render that same case as N/A. That makes the graph place unknown-activity nodes with the most recent contributors/repos.
📊 Suggested fix
- const days = Math.max(0, Math.min(180, n.daysSinceActive || 0));
- return -300 + (days / 180) * 600;
+ if (n.daysSinceActive == null) return 0;
+ const days = Math.max(0, Math.min(180, n.daysSinceActive));
+ return -300 + (days / 180) * 600;Also applies to: 470-476, 516-523
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/dashboard/ContributorRepoGraph.tsx` around lines 138 - 143,
The y-force currently treats missing GraphNode.daysSinceActive as 0 (most
recent); change fg.d3Force("y") so it first checks for null/undefined and uses a
sentinel (e.g., 180 days) or another agreed-upon value for "N/A" before
clamping—e.g., days = n.daysSinceActive == null ? 180 : Math.max(0,
Math.min(180, n.daysSinceActive)); then compute the y as before so nodes with
unknown activity are positioned consistently with the UI that renders "N/A".
| /* commits badge */ | ||
| const badgeY = ay + avatarSize + 26; | ||
| const badgeText = `${formatNum(n.value)} commits`; | ||
| const badgeW = ctx.measureText(badgeText).width + 16; | ||
| const badgeH = 18; | ||
| const bx = cx - badgeW / 2; | ||
|
|
||
| roundRect(ctx, bx, badgeY, badgeW, badgeH, 4); | ||
| ctx.fillStyle = "rgba(140, 198, 63, 0.12)"; | ||
| ctx.fill(); | ||
| ctx.strokeStyle = ACCENT; | ||
| ctx.lineWidth = 0.8; | ||
| ctx.stroke(); | ||
|
|
||
| ctx.fillStyle = ACCENT; | ||
| ctx.font = "600 9px 'Space Mono', monospace"; | ||
| ctx.textAlign = "center"; | ||
| ctx.textBaseline = "middle"; | ||
| ctx.fillText(badgeText, cx, badgeY + badgeH / 2); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Externalize the visible copy from this component.
Strings like commits, Contributor, Repository, Open GitHub Profile, and the legend labels are hardcoded in the canvas renderer and side panel. Move them into the app’s resource files so localization stays consistent. As per coding guidelines, "Internationalization: User-visible strings should be externalized to resource files (i18n)".
Also applies to: 458-545
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/dashboard/ContributorRepoGraph.tsx` around lines 257 - 275,
The component currently hardcodes user-visible strings (e.g., the badgeText
construction using "commits", plus "Contributor", "Repository", "Open GitHub
Profile" and legend labels rendered in ContributorRepoGraph) — replace those
literals by fetching localized strings from the app's i18n/resource module
(e.g., import the shared resource or i18n.t and use keys like repoGraph.commits,
repoGraph.contributor, repoGraph.repository, repoGraph.openProfile,
repoGraph.legend.*). Update the code locations that build badgeText (variable
badgeText), the side-panel labels and legend-rendering code (the block around
lines ~458–545 mentioned) to use the resource lookup instead of hardcoded text,
and ensure the text measurement (ctx.measureText) and canvas drawing still use
the returned localized string.
| return () => { | ||
| cancelled = true; | ||
| }; | ||
| }, [JSON.stringify(orgNames)]); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Avoid JSON.stringify in useEffect dependency array.
Same concern as in ReposTab.tsx — using JSON.stringify(orgNames) is fragile. Consider stabilizing the reference with useMemo or storing in state.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/dashboard/ContributorsTab.tsx` at line 187, The useEffect in
ContributorsTab is depending on JSON.stringify(orgNames) which is fragile;
stabilize the dependency by memoizing orgNames (e.g., create a memoized value
via useMemo or derive a stable key in state) or replace the stringified
dependency with a stable reference and use a deep-equality effect helper. Update
the dependency array to reference the memoized variable (the stableOrgNames or
similar) used inside the effect, and ensure any setters or derived values
(orgNames) are updated to preserve the stable identity.
| const combinedOrgData = validOrgs.length > 0 ? validOrgs[0] : null; // simplified display of org details | ||
| if (validOrgs.length > 1) { | ||
| combinedOrgData.name = validOrgs | ||
| .map((o) => o.name || o.login) | ||
| .join(" + "); | ||
| combinedOrgData.description = | ||
| "Combined statistics for multiple organizations"; | ||
| } |
There was a problem hiding this comment.
Avoid mutating the fetched API response object directly.
Directly mutating combinedOrgData (which references validOrgs[0]) modifies the original object. This can cause unexpected behavior if the object is cached or used elsewhere.
🛡️ Proposed fix using spread operator
-const combinedOrgData = validOrgs.length > 0 ? validOrgs[0] : null;
-if (validOrgs.length > 1) {
- combinedOrgData.name = validOrgs
- .map((o) => o.name || o.login)
- .join(" + ");
- combinedOrgData.description =
- "Combined statistics for multiple organizations";
-}
+const combinedOrgData = validOrgs.length > 0
+ ? validOrgs.length > 1
+ ? {
+ ...validOrgs[0],
+ name: validOrgs.map((o) => o.name || o.login).join(" + "),
+ description: "Combined statistics for multiple organizations",
+ }
+ : { ...validOrgs[0] }
+ : null;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/dashboard/OverviewTab.tsx` around lines 79 - 86,
combinedOrgData currently references validOrgs[0] and is mutated directly;
instead create a shallow copy before modifying to avoid mutating the fetched
object. Change the initialization to create a new object (e.g., use the spread
operator on validOrgs[0]) so combinedOrgData = validOrgs.length > 0 ? {
...validOrgs[0] } : null, then set combinedOrgData.name and
combinedOrgData.description when validOrgs.length > 1; also ensure you guard
against combinedOrgData being null before assigning.
| // --- NEW: Project Velocity & Bus Factor --- | ||
| const totalCommitsLast3Months = weekBuckets.reduce((sum, b) => sum + b.activeTotal, 0); // Active score as proxy for velocity | ||
| const avgCommitsPerWeek = Math.round(totalCommitsLast3Months / 12); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Misleading variable/comment naming.
The comment says "commits" but totalCommitsLast3Months actually counts forks + PRs opened + issues opened. This could confuse future maintainers.
✏️ Suggested naming clarification
-// --- NEW: Project Velocity & Bus Factor ---
-const totalCommitsLast3Months = weekBuckets.reduce((sum, b) => sum + b.activeTotal, 0); // Active score as proxy for velocity
-const avgCommitsPerWeek = Math.round(totalCommitsLast3Months / 12);
+// --- NEW: Project Velocity & Bus Factor ---
+const totalActivityLast3Months = weekBuckets.reduce((sum, b) => sum + b.activeTotal, 0);
+const avgActivityPerWeek = Math.round(totalActivityLast3Months / 12);Also update the state and UI references accordingly.
📝 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.
| // --- NEW: Project Velocity & Bus Factor --- | |
| const totalCommitsLast3Months = weekBuckets.reduce((sum, b) => sum + b.activeTotal, 0); // Active score as proxy for velocity | |
| const avgCommitsPerWeek = Math.round(totalCommitsLast3Months / 12); | |
| // --- NEW: Project Velocity & Bus Factor --- | |
| const totalActivityLast3Months = weekBuckets.reduce((sum, b) => sum + b.activeTotal, 0); | |
| const avgActivityPerWeek = Math.round(totalActivityLast3Months / 12); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/dashboard/OverviewTab.tsx` around lines 271 - 273, The
variable and comment are misleading: totalCommitsLast3Months and its comment
reference "commits" but weekBuckets.reduce(...) sums fork + PRs opened + issues
opened; rename the variable and comment to reflect aggregated activity (e.g.,
totalActivityLast3Months or totalContributionsLast3Months) and update derived
avgCommitsPerWeek to avgActivityPerWeek (or similar) along with any state/prop
names and UI labels that display these values; locate usage sites by searching
for weekBuckets, totalCommitsLast3Months, and avgCommitsPerWeek and change their
names/labels to match the new, accurate terminology.
| }; | ||
|
|
||
| fetchData(); | ||
| }, [JSON.stringify(orgNames)]); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Avoid JSON.stringify in useEffect dependency array.
Same pattern as other tabs — JSON.stringify(orgNames) is fragile. Consider stabilizing with useMemo.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/dashboard/OverviewTab.tsx` at line 300, The useEffect in
OverviewTab is depending on JSON.stringify(orgNames), which is fragile; instead
stabilize the dependency by memoizing a deterministic representation of orgNames
(e.g., create a memoized string or tuple) using useMemo and depend on that
memoized value in the useEffect. Update the component (OverviewTab) to compute
memoizedOrgNames = useMemo(() => /* deterministic representation of orgNames */,
[orgNames]) and replace JSON.stringify(orgNames) with memoizedOrgNames in the
useEffect dependency array so the effect only re-runs when orgNames truly
changes.
| } | ||
| }; | ||
| fetchRepos(); | ||
| }, [JSON.stringify(orgNames)]); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Avoid JSON.stringify in useEffect dependency array.
Using JSON.stringify(orgNames) as a dependency creates a new string on every render, which can cause unnecessary effect re-runs if the reference changes but content is the same. Consider using useMemo to stabilize the array reference or a custom comparison hook.
♻️ Proposed fix using useMemo
+import { useState, useEffect, useMemo } from "react";
-const orgNames = cacheService.getLastOrgs();
+const orgNames = useMemo(() => cacheService.getLastOrgs(), []);
useEffect(() => {
// ...
-}, [JSON.stringify(orgNames)]);
+}, [orgNames]);Or if orgNames needs to react to external changes, consider storing it in state and updating via an event listener.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/dashboard/ReposTab.tsx` at line 75, The useEffect inside the
ReposTab component currently depends on JSON.stringify(orgNames), which creates
a new string each render and leads to unnecessary re-runs; replace that by
stabilizing the array reference (e.g., create memoizedOrgNames via useMemo in
ReposTab that returns orgNames but only changes when its contents change) and
use memoizedOrgNames in the useEffect dependency array (alternatively, swap to a
deep-compare effect hook). Update references to orgNames in the useEffect to use
memoizedOrgNames so the effect only runs when the actual array contents change.
Addressed Issues:
This foundational PR sets up the core infrastructure and addresses multiple open tracking issues:
Fixes #10
Fixes #15
Fixes #17
Fixes #27
Fixes #51
Screenshots/Recordings:
(Add 2-3 screenshots here of the Repositories Tab, the Overview Dashboard, and the F1 Race visualization!)
Additional Notes:
Since the repository was initially empty, this PR introduces the complete, ground-up architecture for OrgExplorer:
@tailwindcss/vite) to fulfill [GOOD FIRST ISSUE]:Migration to Tailwind CSS v4 and Build Tool Optimization #10.Space Mono&Montserratfonts, strict#8CC63F&#F7D100color themes, sharp geometric UI edges, and generated the official AOSSIE SVG favicon (FEATURE : Enhance dashboard with dynamic organization branding and analytics charts #51)./overview,/repos, and/contributorsarchitecture withReact.Suspenselazy loading ([FEATURE]: Implement basic dashboard with Home page and navigation #15 & [FEATURE]: Establish UI & Visualization Foundation for Org Explorer #17).localStorageand aggressively caches heavy repository/contributor payloads viaIndexedDBwith TTL invalidation ([FEATURE]: Implementation of Secure Local PAT Storage and IndexedDB Caching Layer #27).framer-motionto uniquely visualize top active contributors, alongside Recharts for language distributions ([FEATURE]: Establish UI & Visualization Foundation for Org Explorer #17).Checklist
npm run build)Summary by CodeRabbit
Release Notes
New Features
UI/Styling