Skip to content

feat: implement native IndexedDB cache-first data layer#31

Open
swathi2006 wants to merge 7 commits intoAOSSIE-Org:mainfrom
swathi2006:feature/idb-cache
Open

feat: implement native IndexedDB cache-first data layer#31
swathi2006 wants to merge 7 commits intoAOSSIE-Org:mainfrom
swathi2006:feature/idb-cache

Conversation

@swathi2006
Copy link

@swathi2006 swathi2006 commented Mar 2, 2026

Addressed Issues:

Fixes #27

Screenshots/Recordings:

Demonstration of cache-first behavior:
-Tested with Flipkart organisation

  1. First run:
    • No cache found
    • Data fetched from GitHub API
    • Repositories saved to IndexedDB
image image
  1. Subsequent run:
    • Cache detected
    • Data served from IndexedDB
    • No additional API call made

image

Additional Notes:

This introduces a lightweight caching prototype using native IndexedDB.

image

Key points:
• GitHub PAT retained in-memory only to reduce persistent XSS exposure
• Cache-first strategy prevents redundant GitHub API calls
• Repository data cached per organization
• Offline persistence supported
• No external dependency added (keeps bundle size minimal)

This aligns with the project’s cloud-less requirement while improving performance under GitHub rate limits.

Checklist

  • My code follows the project's code style and conventions
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings or errors
  • I have joined the Discord server and I will share a link to this PR with the project maintainers there
  • I have read the Contributing Guidelines

Summary by CodeRabbit

  • New Features

    • New Dashboard UI: org analytics, interactive charts, sortable repo table, contributor views, activity visualizations and modal details.
    • Test Services UI: enter PAT/org and validate connectivity with user-facing alerts and feedback.
    • Offline support: client-side caching and IndexedDB-backed storage for repository data; in-memory token handling.
  • Chores

    • Tailwind CSS integration and UI dependency updates for improved styling and visuals.

@github-actions github-actions bot added the enhancement New feature or request label Mar 2, 2026
@coderabbitai
Copy link

coderabbitai bot commented Mar 2, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Replaces App render with Dashboard; adds cache-first GitHub data stack (tokenService, idbService, cacheService, githubService), TestServices UI, a full Dashboard with multiple visualization components and types, and integrates Tailwind + Recharts via build/CSS changes.

Changes

Cohort / File(s) Summary
App & Test UI
src/App.tsx, src/components/TestServices.tsx
App now renders Dashboard. Adds TestServices component (PAT + org inputs) to exercise githubService.fetchOrgReposWithCache.
Dashboard - core & pages
src/components/Dashboard/Dashboard.tsx, src/components/Dashboard/Home.tsx, src/components/Dashboard/types.ts
New stateful Dashboard with data-fetching, processing, tabs (Overview/Contributors), home CTA, and shared types.
Dashboard - visual components
src/components/Dashboard/StatsCards.tsx, src/components/Dashboard/RepoTable.tsx, src/components/Dashboard/RepoPopularityChart.tsx, src/components/Dashboard/PRChart.tsx, src/components/Dashboard/LanguagePieChart.tsx, src/components/Dashboard/IssueChart.tsx, src/components/Dashboard/ContributorChart.tsx, src/components/Dashboard/ContributorActivity.tsx, src/components/Dashboard/ContributorModal.tsx
Adds multiple chart/table/modal components (Recharts-based) and contributor UI for dashboard visualizations and interactions.
Services - auth, idb, cache, github, barrel
src/services/tokenService.ts, src/services/idbService.ts, src/services/cacheService.ts, src/services/githubService.ts, src/services/index.ts
Introduces tokenService (in-memory PAT), IndexedDB helpers (saveToIDB/getFromIDB), cacheService with TTL + migration, githubService with fetch and cache-aware fetch, and a services barrel export.
Styling & Build config
package.json, vite.config.ts, src/index.css, src/App.css
Integrates Tailwind (deps + Vite plugin), replaces App.css contents with Tailwind placeholder, and updates index.css to include Tailwind base and dark-mode cue.
Auxiliary
src/components/TestServices.tsx, src/components/Dashboard/...*
New small/test components and types to support dashboard; many new exported React components. (*see Dashboard cohorts for full list)

Sequence Diagram(s)

sequenceDiagram
    participant UI as TestServices / Dashboard
    participant Token as tokenService
    participant Service as githubService
    participant Cache as cacheService
    participant IDB as idbService
    participant API as GitHub API

    UI->>Token: setToken(token) [optional]
    UI->>Service: fetchOrgReposWithCache(org, token)
    Service->>Cache: getRepos(org)
    Cache->>IDB: getFromIDB(org)
    IDB-->>Cache: RepoCacheEntry or null
    alt Cache Hit (fresh)
        Cache-->>Service: GitHubRepo[]
    else Cache Miss / Expired / Legacy
        Service->>API: GET /orgs/{org}/repos (Authorization: Bearer token)
        API-->>Service: GitHubRepo[] or error
        Service->>Cache: saveRepos(org, {data, savedAt})
        Cache->>IDB: saveToIDB(org, RepoCacheEntry)
        IDB-->>Cache: saved ✓
        Cache-->>Service: saved ✓
    end
    Service-->>UI: GitHubRepo[] or error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

Typescript Lang

Suggested reviewers

  • Zahnentferner

Poem

🐰 I hopped through code, a carrot-led sprint,
Hid tokens in memory, cached data in mint,
IDB burrowed deep where repos now sleep,
Charts bloom like clover, metrics piled neat,
Dashboards shine bright — a tiny hop, what a treat!

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Beyond core caching requirements, the PR adds Dashboard UI components, Tailwind CSS integration, and visualization libraries. While these support the feature demonstration, they extend beyond the minimal IndexedDB data layer scope. Consider separating UI enhancements (Dashboard, styling, charts) into a follow-up PR to keep the IndexedDB caching layer implementation focused and independently reviewable.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main implementation focus: a native IndexedDB cache-first data layer, which aligns with the primary objective from issue #27.
Linked Issues check ✅ Passed The PR implementation fulfills key requirements from issue #27: secure in-memory PAT storage (tokenService), IndexedDB schema (idbService, cacheService), cache-first API client (githubService.fetchOrgReposWithCache), multi-org caching support, and native IndexedDB without external dependencies.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

❤️ Share

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

@github-actions github-actions bot added size/L and removed size/L labels Mar 3, 2026
@swathi2006 swathi2006 marked this pull request as ready for review March 3, 2026 15:06
@github-actions github-actions bot added size/L and removed size/L labels Mar 3, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🤖 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/TestServices.tsx`:
- Around line 10-56: Replace hardcoded user-visible strings in TestServices.tsx
(alerts inside testFlow, input placeholders, button label) with references to
the i18n resource keys and use the project’s i18n accessors (e.g., i18n.t or
useTranslation hook) so strings are loaded from resource files; update usages
around testFlow (alert("Please enter GitHub token"), alert("Please enter
organization name"), alert("Failed to fetch repositories")), the password/text
input placeholders ("Enter GitHub PAT", "Enter organization name"), and the
button label ("Test Services") to use localized keys (e.g., i18n keys like
github.enterToken, github.enterOrg, github.fetchFailed, github.testButton) and
ensure tokenService.setToken and githubService.fetchOrgReposWithCache logic
remains unchanged.
- Line 55: The button in the TestServices component that calls testFlow is
missing an explicit type and can behave as a submit button in forms; update the
JSX for the button (the element with onClick={testFlow} inside TestServices.tsx)
to include type="button" so its intent is explicit and it won't trigger form
submission or unexpected behavior.

In `@src/services/cacheService.ts`:
- Line 2: Remove the stale commented-out export line for cacheService; delete
the "// export default cacheService;" comment so the file contains only active
code and no dead commented exports, ensuring the module's actual export is
represented by the existing live export(s) for cacheService.
- Around line 41-43: The getRepos function currently lets IndexedDB read errors
propagate and block the network fallback in fetchOrgReposWithCache; modify
getRepos to treat any failures from getFromIDB(org) as cache misses by wrapping
the await getFromIDB(org) call in a try/catch (or catch the Promise) and
returning null on error (optionally logging the error), so
fetchOrgReposWithCache can continue to the network path.

In `@src/services/githubService.ts`:
- Around line 67-70: The cache write after fetching repos should be best-effort:
wrap the call to cacheService.saveRepos(org, { data: repos, savedAt: Date.now()
}) in a try/catch inside the function that performs the fetch (in
src/services/githubService.ts) so any IndexedDB/quota/private-mode errors are
caught and do not propagate to the caller; on failure log the error (e.g., using
the existing logger) but do not rethrow or change the returned repos so the user
still receives the fetched data.
- Around line 41-47: The catch block that logs "Error fetching repositories for
organization ${org}" must not perform UI presentation (alert); remove the alert
calls and instead rethrow a proper Error so callers handle UI. Specifically, in
the catch that currently does console.error(...) and then calls alert(...),
delete the alert branches, and after logging either throw the caught Error (if
error instanceof Error) or throw a new Error(String(error)) to preserve the
original message/stack; leave only logging in the service function and let the
callers show alerts.
- Around line 10-39: The current implementation only returns the first page of
org repos; modify the fetch logic to paginate until all pages are collected by
repeatedly requesting the next page (use the same Authorization/Accept headers)
and concatenating results before returning; detect next page from the
response.headers.get('link') (look for rel="next") or by incrementing a page
query param until an empty result, reuse the existing error handling for each
response, and return the aggregated array instead of a single response.json()
call (references: GITHUB_API_URL, fetch call, response,
response.headers.get('link')).

In `@src/services/idbService.ts`:
- Around line 70-75: The Promise returned around store.get(key) only sets
req.onsuccess, leaving it pending on errors or transaction aborts; update the
async getter to always settle by adding handlers for req.onerror, (and if a
transaction object exists) tx.onerror and tx.onabort to either resolve(null) or
reject with the error so callers don't hang — locate the block that calls
store.get and sets req.onsuccess and add req.onerror = () => resolve(null) or
reject(req.error) (and similarly attach tx.onerror and tx.onabort handlers) to
ensure the promise always resolves or rejects.
- Around line 43-61: The public IDB helpers use `any` and an implicit return
type; update them to be generic: change saveToIDB to saveToIDB<T>(key: string,
value: T): Promise<void> and ensure it returns a Promise<void> (await
transaction completion or return after put completes), and change getFromIDB to
getFromIDB<T>(key: string): Promise<T | null> so callers receive typed payloads;
keep references to openDB(), STORE_NAME, the tx.transaction and
store.objectStore calls but replace untyped parameters with the generic T and
ensure the put/get operations are typed accordingly.
- Around line 43-54: The saveToIDB function currently fires store.put(...) and
returns without awaiting the IndexedDB transaction; update saveToIDB to wait for
the operation to complete by wiring the transaction and request events (e.g.,
listen for tx.oncomplete to resolve and tx.onerror/tx.onabort and
request.onerror to reject) so callers can detect failures (use openDB(),
STORE_NAME, tx, and store.put references); likewise, modify getFromIDB to attach
an onerror handler (and onblocked/tx.onerror as appropriate) to its get request
and reject the returned promise on failure so reads do not hang silently.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1cfc3e7 and af4c8f5.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (7)
  • src/App.tsx
  • src/components/TestServices.tsx
  • src/services/cacheService.ts
  • src/services/githubService.ts
  • src/services/idbService.ts
  • src/services/index.ts
  • src/services/tokenService.ts

Comment on lines +41 to +43
async getRepos(org: string): Promise<GitHubRepo[] | null> {
const entry = await getFromIDB(org)

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Treat cache read failures as cache misses to preserve API fallback.

If IndexedDB read throws, this currently propagates and prevents fetchOrgReposWithCache from hitting the network path.

Proposed fix
   async getRepos(org: string): Promise<GitHubRepo[] | null> {
-    const entry = await getFromIDB(org)
+    let entry: unknown
+    try {
+      entry = await getFromIDB(org)
+    } catch (error) {
+      console.warn(`Cache read failed for ${org}:`, error)
+      return null
+    }
 
     if (!entry) {
       console.log("No cache found")
       return null
📝 Committable suggestion

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

Suggested change
async getRepos(org: string): Promise<GitHubRepo[] | null> {
const entry = await getFromIDB(org)
async getRepos(org: string): Promise<GitHubRepo[] | null> {
let entry: unknown
try {
entry = await getFromIDB(org)
} catch (error) {
console.warn(`Cache read failed for ${org}:`, error)
return null
}
if (!entry) {
console.log("No cache found")
return null
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/cacheService.ts` around lines 41 - 43, The getRepos function
currently lets IndexedDB read errors propagate and block the network fallback in
fetchOrgReposWithCache; modify getRepos to treat any failures from
getFromIDB(org) as cache misses by wrapping the await getFromIDB(org) call in a
try/catch (or catch the Promise) and returning null on error (optionally logging
the error), so fetchOrgReposWithCache can continue to the network path.

Comment on lines +10 to +39
const url = `${GITHUB_API_URL}/orgs/${org}/repos`;
console.log("Fetching from:", url);

const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/vnd.github+json',
}
});

if (!response.ok) {
if (response.status === 401) {
throw new Error("Invalid or expired GitHub token. Please check your PAT.");
}

if (response.status === 403) {
throw new Error("Rate limit exceeded. Please try again later.");
}

if (response.status === 404) {
throw new Error("Organization not found.");
}

throw new Error(
`GitHub API error: ${response.status} ${response.statusText}`
);
}

return await response.json();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fetch all repository pages instead of only the first page.

GitHub org repos API is paginated; the current implementation can silently cache only the first 30 repos for larger organizations.

Proposed fix
-      const url = `${GITHUB_API_URL}/orgs/${org}/repos`;
-      console.log("Fetching from:", url);
-
-      const response = await fetch(url, {
-        method: 'GET',
-        headers: {
-          Authorization: `Bearer ${token}`,
-          Accept: 'application/vnd.github+json',
-        }
-      });
+      const repos: GitHubRepo[] = [];
+      let page = 1;
+
+      while (true) {
+        const url = `${GITHUB_API_URL}/orgs/${encodeURIComponent(org)}/repos?per_page=100&page=${page}`;
+        console.log("Fetching from:", url);
+
+        const response = await fetch(url, {
+          method: "GET",
+          headers: {
+            Authorization: `Bearer ${token}`,
+            Accept: "application/vnd.github+json",
+          },
+        });
 
-     if (!response.ok) {
-  if (response.status === 401) {
-    throw new Error("Invalid or expired GitHub token. Please check your PAT.");
-  }
+        if (!response.ok) {
+          if (response.status === 401) {
+            throw new Error("Invalid or expired GitHub token. Please check your PAT.");
+          }
+          if (response.status === 403) {
+            throw new Error("Rate limit exceeded. Please try again later.");
+          }
+          if (response.status === 404) {
+            throw new Error("Organization not found.");
+          }
+          throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
+        }
 
-  if (response.status === 403) {
-    throw new Error("Rate limit exceeded. Please try again later.");
-  }
+        const batch = (await response.json()) as GitHubRepo[];
+        repos.push(...batch);
 
-  if (response.status === 404) {
-    throw new Error("Organization not found.");
-  }
+        if (batch.length < 100) break;
+        page += 1;
+      }
 
-  throw new Error(
-    `GitHub API error: ${response.status} ${response.statusText}`
-  );
-}
-
-      return await response.json();
+      return repos;
📝 Committable suggestion

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

Suggested change
const url = `${GITHUB_API_URL}/orgs/${org}/repos`;
console.log("Fetching from:", url);
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/vnd.github+json',
}
});
if (!response.ok) {
if (response.status === 401) {
throw new Error("Invalid or expired GitHub token. Please check your PAT.");
}
if (response.status === 403) {
throw new Error("Rate limit exceeded. Please try again later.");
}
if (response.status === 404) {
throw new Error("Organization not found.");
}
throw new Error(
`GitHub API error: ${response.status} ${response.statusText}`
);
}
return await response.json();
const repos: GitHubRepo[] = [];
let page = 1;
while (true) {
const url = `${GITHUB_API_URL}/orgs/${encodeURIComponent(org)}/repos?per_page=100&page=${page}`;
console.log("Fetching from:", url);
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/vnd.github+json",
},
});
if (!response.ok) {
if (response.status === 401) {
throw new Error("Invalid or expired GitHub token. Please check your PAT.");
}
if (response.status === 403) {
throw new Error("Rate limit exceeded. Please try again later.");
}
if (response.status === 404) {
throw new Error("Organization not found.");
}
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
}
const batch = (await response.json()) as GitHubRepo[];
repos.push(...batch);
if (batch.length < 100) break;
page += 1;
}
return repos;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/githubService.ts` around lines 10 - 39, The current
implementation only returns the first page of org repos; modify the fetch logic
to paginate until all pages are collected by repeatedly requesting the next page
(use the same Authorization/Accept headers) and concatenating results before
returning; detect next page from the response.headers.get('link') (look for
rel="next") or by incrementing a page query param until an empty result, reuse
the existing error handling for each response, and return the aggregated array
instead of a single response.json() call (references: GITHUB_API_URL, fetch
call, response, response.headers.get('link')).

@github-actions github-actions bot added size/L and removed size/L labels Mar 3, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/services/githubService.ts (1)

10-39: ⚠️ Potential issue | 🟠 Major

Fetch all repo pages before caching.

Current implementation only stores the first page of organization repositories, which can silently truncate cached data.

Proposed fix
-      const url = `${GITHUB_API_URL}/orgs/${org}/repos`;
-      console.log("Fetching from:", url);
-
-      const response = await fetch(url, {
-        method: 'GET',
-        headers: {
-          Authorization: `Bearer ${token}`,
-          Accept: 'application/vnd.github+json',
-        }
-      });
-
-     if (!response.ok) {
-  if (response.status === 401) {
-    throw new Error("Invalid or expired GitHub token. Please check your PAT.");
-  }
-
-  if (response.status === 403) {
-    throw new Error("Rate limit exceeded. Please try again later.");
-  }
-
-  if (response.status === 404) {
-    throw new Error("Organization not found.");
-  }
-
-  throw new Error(
-    `GitHub API error: ${response.status} ${response.statusText}`
-  );
-}
-
-      return await response.json();
+      const repos: GitHubRepo[] = []
+      let page = 1
+
+      while (true) {
+        const url = `${GITHUB_API_URL}/orgs/${encodeURIComponent(org)}/repos?per_page=100&page=${page}`
+        console.log("Fetching from:", url)
+
+        const response = await fetch(url, {
+          method: "GET",
+          headers: {
+            Authorization: `Bearer ${token}`,
+            Accept: "application/vnd.github+json",
+          }
+        })
+
+        if (!response.ok) {
+          if (response.status === 401) {
+            throw new Error("Invalid or expired GitHub token. Please check your PAT.")
+          }
+          if (response.status === 403) {
+            throw new Error("Rate limit exceeded. Please try again later.")
+          }
+          if (response.status === 404) {
+            throw new Error("Organization not found.")
+          }
+          throw new Error(`GitHub API error: ${response.status} ${response.statusText}`)
+        }
+
+        const pageRepos = (await response.json()) as GitHubRepo[]
+        repos.push(...pageRepos)
+        if (pageRepos.length < 100) break
+        page += 1
+      }
+
+      return repos
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/githubService.ts` around lines 10 - 39, The current fetch in
src/services/githubService.ts only retrieves the first page of an organization's
repos (the fetch call and response handling in the function that builds
`${GITHUB_API_URL}/orgs/${org}/repos`), so update the logic to follow GitHub
pagination and aggregate all pages before returning/caching: loop performing
fetch() calls, parse the Link header for rel="next" (or increment page param)
until there is no next page, concatenate each page's JSON results into a single
array, then apply the existing error handling (response.ok checks) per request
and return/cache the full combined array instead of only the first page.
🤖 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/TestServices.tsx`:
- Around line 9-16: Trim and normalize the input values before validating and
using them: replace direct checks of tokenInput and orgInput with trimmed
versions (e.g., const token = tokenInput?.trim(); const org = orgInput?.trim())
then validate using token === "" or org === "" and use those trimmed vars in
subsequent requests; apply the same change to the other input checks referenced
(lines 22-25) so all uses of tokenInput/orgInput use their trimmed equivalents.

In `@src/services/cacheService.ts`:
- Around line 68-83: The current runtime check only ensures keys exist on the
cache object but not their types; update the validation in the block that casts
to RepoCacheEntry so it also asserts Number.isFinite(typedEntry.savedAt) and
typeof typedEntry.savedAt === "number" (and e.g. Array.isArray(typedEntry.data)
or whatever concrete shape RepoCacheEntry.data must be) and that savedAt is
non-negative before performing the TTL check against MAX_CACHE_AGE; if any of
those type/shape checks fail, log or silently return null instead of proceeding
to use malformed values. Ensure you touch the same block that references
RepoCacheEntry, savedAt, data, and MAX_CACHE_AGE.

---

Duplicate comments:
In `@src/services/githubService.ts`:
- Around line 10-39: The current fetch in src/services/githubService.ts only
retrieves the first page of an organization's repos (the fetch call and response
handling in the function that builds `${GITHUB_API_URL}/orgs/${org}/repos`), so
update the logic to follow GitHub pagination and aggregate all pages before
returning/caching: loop performing fetch() calls, parse the Link header for
rel="next" (or increment page param) until there is no next page, concatenate
each page's JSON results into a single array, then apply the existing error
handling (response.ok checks) per request and return/cache the full combined
array instead of only the first page.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between af4c8f5 and 42136eb.

📒 Files selected for processing (4)
  • src/components/TestServices.tsx
  • src/services/cacheService.ts
  • src/services/githubService.ts
  • src/services/idbService.ts

Comment on lines +68 to +83
if (
typeof entry === "object" &&
entry !== null &&
"data" in entry &&
"savedAt" in entry
) {
const typedEntry = entry as RepoCacheEntry

// TTL CHECK
if (Date.now() - typedEntry.savedAt > MAX_CACHE_AGE) {
console.log("Cache expired")
return null
}

return typedEntry.data
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Harden runtime validation for structured cache entries.

Checking only key presence allows malformed values through (e.g., savedAt not numeric, data not array), which can cause downstream runtime errors.

Proposed fix
-    if (
-      typeof entry === "object" &&
-      entry !== null &&
-      "data" in entry &&
-      "savedAt" in entry
-    ) {
+    if (
+      typeof entry === "object" &&
+      entry !== null &&
+      "data" in entry &&
+      Array.isArray((entry as { data: unknown }).data) &&
+      "savedAt" in entry &&
+      typeof (entry as { savedAt: unknown }).savedAt === "number"
+    ) {
       const typedEntry = entry as RepoCacheEntry
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/cacheService.ts` around lines 68 - 83, The current runtime check
only ensures keys exist on the cache object but not their types; update the
validation in the block that casts to RepoCacheEntry so it also asserts
Number.isFinite(typedEntry.savedAt) and typeof typedEntry.savedAt === "number"
(and e.g. Array.isArray(typedEntry.data) or whatever concrete shape
RepoCacheEntry.data must be) and that savedAt is non-negative before performing
the TTL check against MAX_CACHE_AGE; if any of those type/shape checks fail, log
or silently return null instead of proceeding to use malformed values. Ensure
you touch the same block that references RepoCacheEntry, savedAt, data, and
MAX_CACHE_AGE.

@github-actions github-actions bot added size/L and removed size/L labels Mar 3, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
src/services/cacheService.ts (2)

41-43: ⚠️ Potential issue | 🟠 Major

Treat IndexedDB read failures as cache misses.

If getFromIDB throws, this exits early and prevents the intended network fallback path.

Proposed fix
   async getRepos(org: string): Promise<GitHubRepo[] | null> {
-    const entry = await getFromIDB(org)
+    let entry: unknown
+    try {
+      entry = await getFromIDB(org)
+    } catch (error) {
+      console.warn(`Cache read failed for ${org}:`, error)
+      return null
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/cacheService.ts` around lines 41 - 43, getRepos currently lets
exceptions from getFromIDB bubble up and abort the network fallback; wrap the
call to getFromIDB inside a try/catch in getRepos so any thrown error is treated
as a cache miss (log the error with context using the existing logger if
available) and execution continues to the network fetch path (e.g., the existing
repo-fetching logic after the IDB lookup). Ensure you only swallow IDB read
errors (return null/undefined from the cache branch) rather than masking other
failures from the subsequent network call.

68-73: ⚠️ Potential issue | 🟡 Minor

Remove any and harden cache entry validation.

The current guard still relies on any and allows invalid timestamps (e.g., NaN, negative values) through.

Proposed fix
-   if (
-  typeof entry === "object" &&
-  entry !== null &&
-  Array.isArray((entry as any).data) &&
-  typeof (entry as any).savedAt === "number"
-) {
+   if (
+    typeof entry === "object" &&
+    entry !== null &&
+    "data" in entry &&
+    Array.isArray((entry as { data: unknown }).data) &&
+    "savedAt" in entry &&
+    typeof (entry as { savedAt: unknown }).savedAt === "number"
+   ) {
       const typedEntry = entry as RepoCacheEntry
+      if (!Number.isFinite(typedEntry.savedAt) || typedEntry.savedAt < 0) {
+        return null
+      }

As per coding guidelines "Avoid 'any', use explicit types".

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

In `@src/services/cacheService.ts` around lines 68 - 73, The runtime guard around
cache entries uses `any` and permits invalid timestamps; replace it with a
strict type guard that checks `entry` matches a CacheEntry shape (e.g., an
interface with data: unknown[] and savedAt: number) without `any`, verify
`Array.isArray(entry.data)` and that `Number.isFinite(entry.savedAt)` and
`entry.savedAt >= 0` (and optionally `Number.isInteger(entry.savedAt)` if
timestamps are integer ms), and narrow the type so downstream code treats the
value as the proper CacheEntry type rather than `any` (update the guard around
the `entry` variable and the checks referencing `data` and `savedAt`).
src/components/TestServices.tsx (1)

14-20: ⚠️ Potential issue | 🟠 Major

Externalize user-facing copy to i18n resources.

These strings are still hardcoded (alert messages, placeholders, button label). Please switch them to the project’s translation keys/accessor.

As per coding guidelines, "Internationalization: User-visible strings should be externalized to resource files (i18n)."

Also applies to: 41-41, 50-50, 57-57, 64-64

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

In `@src/components/TestServices.tsx` around lines 14 - 20, Replace hardcoded
user-facing strings in TestServices.tsx with i18n keys: locate the validation
branch that shows alerts for the GitHub token and organization (references:
trimmedOrg and the token-checking block) and change alert("Please enter GitHub
token") and alert("Please enter organization name") to use the project
translation accessor (e.g., t('...') or i18n.t('...')) with appropriate keys;
also update the other hardcoded strings mentioned (placeholders and button label
near the same component at the referenced locations) to use the same translation
keys so all user-visible copy is externalized to i18n resources.
🤖 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/TestServices.tsx`:
- Line 33: The forEach callback currently uses an expression body
(repos.forEach(repo => console.log(repo.name))) which can be mistaken for
returning a value; change the callback to a block body so it clearly returns
nothing — update the repos.forEach callback to use curly braces and a statement
(e.g., in the same callback that references repo.name) and ensure the
console.log call ends with a semicolon, so the callback is an explicit
void-style block.

---

Duplicate comments:
In `@src/components/TestServices.tsx`:
- Around line 14-20: Replace hardcoded user-facing strings in TestServices.tsx
with i18n keys: locate the validation branch that shows alerts for the GitHub
token and organization (references: trimmedOrg and the token-checking block) and
change alert("Please enter GitHub token") and alert("Please enter organization
name") to use the project translation accessor (e.g., t('...') or i18n.t('...'))
with appropriate keys; also update the other hardcoded strings mentioned
(placeholders and button label near the same component at the referenced
locations) to use the same translation keys so all user-visible copy is
externalized to i18n resources.

In `@src/services/cacheService.ts`:
- Around line 41-43: getRepos currently lets exceptions from getFromIDB bubble
up and abort the network fallback; wrap the call to getFromIDB inside a
try/catch in getRepos so any thrown error is treated as a cache miss (log the
error with context using the existing logger if available) and execution
continues to the network fetch path (e.g., the existing repo-fetching logic
after the IDB lookup). Ensure you only swallow IDB read errors (return
null/undefined from the cache branch) rather than masking other failures from
the subsequent network call.
- Around line 68-73: The runtime guard around cache entries uses `any` and
permits invalid timestamps; replace it with a strict type guard that checks
`entry` matches a CacheEntry shape (e.g., an interface with data: unknown[] and
savedAt: number) without `any`, verify `Array.isArray(entry.data)` and that
`Number.isFinite(entry.savedAt)` and `entry.savedAt >= 0` (and optionally
`Number.isInteger(entry.savedAt)` if timestamps are integer ms), and narrow the
type so downstream code treats the value as the proper CacheEntry type rather
than `any` (update the guard around the `entry` variable and the checks
referencing `data` and `savedAt`).

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 42136eb and ef9138f.

📒 Files selected for processing (2)
  • src/components/TestServices.tsx
  • src/services/cacheService.ts


console.log("Repos count:", repos.length)
console.log("Repo names:")
repos.forEach(repo => console.log(repo.name))
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, verify the pattern exists in the file
rg -nP 'forEach\s*\(\s*\w+\s*=>\s*console\.log\(' src/components/TestServices.tsx

Repository: AOSSIE-Org/OrgExplorer

Length of output: 120


🏁 Script executed:

# Also check the full context around line 33
sed -n '25,40p' src/components/TestServices.tsx

Repository: AOSSIE-Org/OrgExplorer

Length of output: 447


🏁 Script executed:

# Verify the file exists and check its basic properties
ls -la src/components/TestServices.tsx && head -35 src/components/TestServices.tsx | tail -10

Repository: AOSSIE-Org/OrgExplorer

Length of output: 416


Fix forEach callback to use block body instead of expression body.

Line 33 uses an expression-bodied callback in forEach, which returns an implicit value and triggers Biome's lint/suspicious/useIterableCallbackReturn rule. Use a block body with curly braces to clarify that no return value is intended.

Proposed fix
-      repos.forEach(repo => console.log(repo.name))
+      repos.forEach((repo) => {
+        console.log(repo.name)
+      })
📝 Committable suggestion

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

Suggested change
repos.forEach(repo => console.log(repo.name))
repos.forEach((repo) => {
console.log(repo.name)
})
🧰 Tools
🪛 Biome (2.4.4)

[error] 33-33: This callback passed to forEach() iterable method should not return a value.

(lint/suspicious/useIterableCallbackReturn)

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

In `@src/components/TestServices.tsx` at line 33, The forEach callback currently
uses an expression body (repos.forEach(repo => console.log(repo.name))) which
can be mistaken for returning a value; change the callback to a block body so it
clearly returns nothing — update the repos.forEach callback to use curly braces
and a statement (e.g., in the same callback that references repo.name) and
ensure the console.log call ends with a semicolon, so the callback is an
explicit void-style block.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 27

♻️ Duplicate comments (2)
src/components/TestServices.tsx (2)

14-20: ⚠️ Potential issue | 🟠 Major

Move user-facing strings to i18n resources.

Alerts, placeholders, and button text are still hardcoded and should be localized via resource keys.

As per coding guidelines, "Internationalization: User-visible strings should be externalized to resource files (i18n)."

Also applies to: 39-42, 50-50, 57-57, 64-64

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

In `@src/components/TestServices.tsx` around lines 14 - 20, Replace hardcoded
user-facing strings in TestServices.tsx with i18n resource keys: do not call
alert("Please enter GitHub token") or alert("Please enter organization name")
directly; instead use the project's i18n utility (e.g., useTranslation()/t or
i18n.t) to load localized messages for the token and org validation messages and
for the other occurrences noted (lines around 39-42, 50, 57, 64). Update the
validation branch where trimmedToken and trimmedOrg are checked to reference the
appropriate resource keys (e.g., t('testServices.errors.missingToken') and
t('testServices.errors.missingOrg')) and ensure any placeholders/buttons in the
same component also use i18n keys.

33-33: ⚠️ Potential issue | 🟡 Minor

Use a block-bodied forEach callback to satisfy linting.

This expression-bodied callback still triggers lint/suspicious/useIterableCallbackReturn.

🔧 Suggested fix
-      repos.forEach(repo => console.log(repo.name))
+      repos.forEach((repo) => {
+        console.log(repo.name)
+      })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/TestServices.tsx` at line 33, The forEach callback on repos
(repos.forEach(repo => console.log(repo.name))) uses an expression-bodied arrow
which triggers lint/suspicious/useIterableCallbackReturn; change it to a
block-bodied callback for the repos.forEach call (make the arrow body use braces
and a statement containing the console.log) so the callback is block-bodied and
satisfies the linter for the TestServices component.
🤖 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/ContributorActivity.tsx`:
- Around line 25-27: The heading text in the ContributorActivity component is
user-visible and must be externalized; update the component
(ContributorActivity) to use the i18n translation API (e.g., useTranslation or
t) and replace the inline template "Commit Activity for {name}" with a call to
the translation key (e.g., t('contributorActivity', { name })); add the new key
with a variable placeholder to the locale resource files (e.g.,
"contributorActivity": "Commit Activity for {{name}}") and ensure the component
imports and uses the translation hook instead of hardcoding the string.

In `@src/components/Dashboard/ContributorChart.tsx`:
- Line 23: The hardcoded header string in ContributorChart.tsx ("Top
Contributors by Commits") must be externalized to i18n: import and use the
project's translation hook (e.g., useTranslation or t) in the ContributorChart
component and replace the literal in the <h3> (or its surrounding JSX) with a
call to the translation key (suggested key: dashboard.topContributorsByCommits);
then add that key and translated values to the locale resource files for
supported languages so the UI reads the title from i18n instead of a hardcoded
string.

In `@src/components/Dashboard/ContributorModal.tsx`:
- Around line 20-21: The modal currently uses inline user-visible strings (e.g.,
the paragraph showing "{contributors.length} contributors found", the header
title prop in ContributorModal, the "commits" label and the "Close" button text)
— replace these literals with i18n resource lookups: import and use your
project's translation helper (e.g., useTranslation/t) in the ContributorModal
component and swap the inline strings for translation keys (e.g.,
contributorsFound, commitsLabel, closeButton) while passing contributors.length
and title into the translated/messages where needed; update the i18n resource
files with the new keys and use the keys in the JSX for the h2, the contributors
count <p>, the commits label, and the Close button.
- Around line 23-28: Add accessible names to the icon-only buttons by adding
aria-label attributes: update the close button (the <button onClick={onClose}>
wrapping the <X /> icon in ContributorModal.tsx) to include aria-label="Close"
(or localized equivalent), and also add a descriptive aria-label to the other
icon-only control referenced around lines 45-46 (the second icon-only <button>
in this component) that reflects its action (e.g., "Open profile" or "Copy
email"); ensure the labels are concise, descriptive, and use aria-label on the
button elements so assistive tech can announce their purpose.
- Around line 23-57: Four button elements in ContributorModal (the close button
with onClick={onClose}, the two icon buttons inside the contributors.map block,
and the bottom action button that also uses onClick={onClose}) are missing
explicit types and may act as implicit submit buttons; update each of those
<button> elements to include type="button" to prevent unintended form submission
(locate them by the onClick={onClose} button, the two icon buttons rendering
<Mail> and <ExternalLink>, and the bottom action button in the component).
- Around line 16-29: Add proper dialog accessibility to the modal wrapper in
ContributorModal.tsx: give the outer modal container div the attributes
role="dialog" and aria-modal="true", add an id to the title element (the <h2>
rendering {title}, e.g., id="contributor-modal-title"), and set aria-labelledby
on the dialog container to that id so screen-readers announce the modal title;
also ensure the close button (onClose handler) has an accessible name (e.g.,
aria-label="Close") so it is keyboard/screen-reader operable.

In `@src/components/Dashboard/Dashboard.tsx`:
- Around line 62-71: Replace hardcoded user-visible strings in the validation
logic by reading them from the i18n/resource layer: replace the literals passed
to setError(...) inside the Dashboard component's validation block (the checks
using trimmedOrg and trimmedToken) with calls to the translation/resource
accessor (e.g., t('dashboard.error.invalidOrg') or Resource.get('invalidOrg'))
and add corresponding keys for "Please enter a valid GitHub organization name."
and "The provided token seems too short. Please use a valid GitHub PAT or leave
it empty." to the resource files; ensure imports/usage of the project's i18n
helper (e.g., the t function or Resource) are present at the top of
Dashboard.tsx and update any tests/fixtures that expect the original strings.
- Around line 157-158: The dashboard is creating fabricated metrics by applying
arbitrary multipliers to totalOpenIssues and totalPRs in setIssueData and
setPrData; either stop showing those phantom values or replace them with real
counts from the API. Fix by removing the derived entries (e.g., the "Closed
Issues", "Merged PRs", "Closed PRs" objects) from the setIssueData/setPrData
calls until you implement real queries, or fetch actual values (closed issues
count, PRs where merged_at != null for merged PRs, and PRs with state ===
'closed' for closed PRs) and populate setIssueData/setPrData with those real
counts using the existing totalOpenIssues/totalPRs variables as needed.
- Line 135: The variable totalPRs in Dashboard.tsx is a fabricated 40% of
totalOpenIssues which double-counts PRs (open_issues_count includes PRs);
replace this with a real PR count or make it explicit that it's an estimate:
either (A) fetch the actual open PR count using the GitHub Search API (query
"repo:OWNER/REPO is:pr is:open") and set totalPRs from that response, or (B)
remove the computed formula and change the UI label where totalPRs is used to
indicate "estimated" or remove the value entirely; update the variable totalPRs
and any UI text in the Dashboard component accordingly.
- Line 213: The navigation/action buttons (e.g., the back button that calls
setIsHome and the other nav/action buttons referenced in the diff) are missing
explicit type attributes and thus default to type="submit"; update those
<button> elements to include type="button" so they don't trigger form
submissions (add type="button" to the button that calls setIsHome and to the
other navigation/action buttons in this component).
- Line 172: Define a Contributor interface describing the API response (e.g.,
fields like login, id, avatar_url, contributions, html_url, etc.), replace any
usages of any with Contributor (for example type repoContributors as
Contributor[] and change the forEach callback signature in
repoContributors.forEach((c: any) => ...) to (c: Contributor) => ...), and
update any state, props, or helper function signatures that hold
repoContributors so the component and related logic consistently use the new
Contributor type.
- Line 356: The JSX line in Dashboard using lastUpdated! can throw if
lastUpdated is null; remove the non-null assertion and guard it instead. Update
the render for the Sync span in the Dashboard component to check lastUpdated
(e.g., conditional rendering or use a safe fallback like new Date(lastUpdated ??
Date.now()).toLocaleTimeString() or display "N/A") and ensure the Database/Clock
block still shows correctly; reference the lastUpdated variable and the Sync
span JSX when making the change.
- Around line 83-84: The PAT is being stored via
tokenService.setToken(finalToken) but never cleared; call
tokenService.removeToken() after the search request completes (both on success
and on error) and also remove the token in the Dashboard component's unmount
cleanup (useEffect cleanup) to avoid persisting `_token`. Locate the usage
around finalToken/tokenService.setToken and add post-request cleanup and an
unmount cleanup that invokes tokenService.removeToken().
- Line 123: Replace the catch (err: any) with a safer unknown-based handler in
the Dashboard component: change to catch (err: unknown) and then narrow the
error using a type guard (e.g., if (err instanceof Error) { /* use err.message
or err.stack */ } else { /* fallback: String(err) */ }) or create an isError
helper; update any subsequent usage that assumed err was a string to use the
narrowed value. This removes the any type and ensures proper, safe error
handling in Dashboard's catch block.
- Around line 324-340: The two clickable card divs that call
openContributorModal('Total Contributors', allContributors) and
openContributorModal('New Contributors', newContributors) are inaccessible
because they rely only on onClick; replace each <div> with a semantic
interactive element (e.g., <button>) or add keyboard handlers and ARIA roles so
keyboard users can activate them (implement onKeyDown to trigger
openContributorModal on Enter/Space and set role="button" and tabIndex=0 if you
keep divs); ensure focus styles and that the click handler logic remains tied to
the same functions (openContributorModal) and that className and visual styling
are preserved.
- Around line 193-196: The current generation of activity data in Dashboard (the
activity variable and setActivityData call) uses Math.random() and must be
replaced: either implement an async fetchActivityData function that calls the
GitHub Events/Commits API for the org/repo(s), aggregate events into 12 weekly
buckets, map results into objects like { week: 'W1', commits: n } and call
setActivityData(activity) from within useEffect (handle auth/ratelimit and
errors), or remove the fake chart and instead supply a static placeholder array
and update the chart title/legend in the Dashboard component to clearly indicate
"Placeholder data" until real data is integrated. Ensure you reference and
update the activity variable, setActivityData invocation, and the component
rendering the activity chart accordingly.
- Line 17: handleSearch currently always calls the GitHub API and ignores the
cache; update handleSearch to implement cache-first behavior by first calling
cacheService.getRepos(orgName) and returning that if present and not expired,
otherwise call the existing githubService method that fetches repos (e.g.,
githubService.fetchRepos or githubService.getRepos with the token from
tokenService.getToken()), then save the API result into
cacheService.saveRepos(orgName, repos) before returning/setting state; ensure
you reference the existing handleSearch function and the
cacheService.getRepos/saveRepos symbols when making the change.

In `@src/components/Dashboard/Home.tsx`:
- Line 19: The Home component contains hard-coded user-visible strings (hero
title, feature-card text, CTA, footer, etc.) that must be externalized to i18n
keys; update the Home component (src/components/Dashboard/Home.tsx) to import
the translation hook (e.g., useTranslation or t function used across the app),
replace each literal span/text node (examples: "Open Source Analytics Platform"
and the other literals at the commented locations) with calls to t('home.<key>')
using descriptive keys (e.g., home.hero.title, home.features.card1.title,
home.cta.text, home.footer.copy), and ensure the corresponding keys are added to
the locale resource files; keep component structure and JSX intact and only swap
literals for t(...) calls and add the import for the i18n hook.
- Around line 32-35: The CTA button in the Home component (the <button> with
onClick={onStart} and the long className) lacks an explicit type, which can
cause it to act as a form submit button; update that button element to include
type="button" to ensure it never submits a form unintentionally (i.e., add
type="button" to the button with onClick={onStart}).

In `@src/components/Dashboard/IssueChart.tsx`:
- Line 13: The hardcoded heading "Issue Analytics" in the IssueChart component
should be pulled from i18n resources; replace the string literal in the <h3>
element inside the IssueChart component with a translation lookup (e.g., using
useTranslation().t or your project's i18n helper) and add an appropriate key
like "issueChart.title" to the locale files; ensure the key is added to all
supported language resource files and update the component to import/use the
translator before rendering the <h3> so the className and structure remain
unchanged.

In `@src/components/Dashboard/LanguagePieChart.tsx`:
- Line 14: In LanguagePieChart component replace the hardcoded heading text in
the h3 ("Language Distribution") with an i18n lookup: import and use your
translation hook (e.g., useTranslation/t) in the LanguagePieChart component and
call t('dashboard.languageDistribution') (or your project's agreed key) instead
of the literal string; also add the corresponding key/value to the i18n resource
files for supported locales.

In `@src/components/Dashboard/PRChart.tsx`:
- Line 13: The hardcoded header text in the PRChart component ("Pull Request
Analytics") must be replaced with a localized string; import and use your app's
i18n utility (e.g., useTranslation/t from react-i18next or the project's i18n
helper) inside the PRChart functional component and replace the literal with a
call like t('dashboard.pullRequestAnalytics'), then add the corresponding
key/value to the i18n resource files for supported locales so the title is
sourced from resources rather than hardcoded.

In `@src/components/Dashboard/RepoPopularityChart.tsx`:
- Line 22: Replace the hardcoded title in the RepoPopularityChart component with
a localized string from your i18n resources: import and use the useTranslation
hook (or the project's translation helper) inside RepoPopularityChart and
replace "Top Repositories by Stars" with a call like
t('dashboard.topRepositoriesByStars') (or the appropriate key); add the new
key/value to the locale resource files so the title is externalized for
translations.

In `@src/components/Dashboard/RepoTable.tsx`:
- Around line 46-79: RepoTable currently has hardcoded user-visible strings
(e.g., "Repositories", "Repository", "Stars", "Forks", "Language", "Open
Issues", "Last Updated", and the "-" placeholder) — replace these with i18n
resource lookups: import your i18n hook (e.g., useTranslation/t) into the
RepoTable component, swap each literal with t('repoTable.xxx') keys (create
sensible keys like repoTable.title, repoTable.repo, repoTable.stars,
repoTable.forks, repoTable.language, repoTable.openIssues,
repoTable.lastUpdated, repoTable.emptyPlaceholder), and add those keys to the
localization resource files for supported locales; leave handler names and
components (handleSort, SortIcon) unchanged. Ensure you also replace any other
occurrences of the same hardcoded strings in this component.
- Around line 53-80: The header cells using onClick directly on <th> (the
Stars/Forks/Last Updated headers that call handleSort and render <SortIcon
column="..."/>) are not keyboard-accessible; update each sortable <th> to
contain a real <button type="button"> that receives the onClick and focus, move
the handleSort(...) call onto that button, and remove the click handler from the
<th> itself; also expose sort state by adding an appropriate aria-sort value on
the <th> (e.g., "ascending", "descending" or "none") based on your current sort
state and ensure SortIcon continues to accept the same column prop so it stays
in sync.

In `@src/components/Dashboard/StatsCards.tsx`:
- Around line 19-55: The component StatsCards.tsx currently hardcodes
user-visible card titles ("Total Repositories", "Total Stars", "Total Forks",
"Open Issues", "Pull Requests", "Languages Used"); update the component to use
the i18n translation function (e.g. call useTranslation() and replace those
title literals with t('stats.totalRepositories'), t('stats.totalStars'),
t('stats.totalForks'), t('stats.openIssues'), t('stats.pullRequests'), and
t('stats.languagesUsed') respectively), and add corresponding keys to your i18n
resource files; ensure the cards array in StatsCards uses the translated strings
and keep the unique symbols (StatsCards component, the cards array, and
useTranslation/t) to locate the changes.

In `@src/components/TestServices.tsx`:
- Line 2: The code currently writes the token with
tokenService.setToken(trimmedToken) but then passes trimmedToken directly into
githubService.fetchOrgReposWithCache(trimmedOrg, trimmedToken), creating two
parallel auth flows; either remove the redundant tokenService.setToken call or
make githubService consume the tokenService instead of accepting a token param.
Fix by choosing one flow: (A) delete the tokenService.setToken(trimmedToken)
calls in TestServices.tsx and keep passing the token into
githubService.fetchOrgReposWithCache(trimmedOrg, trimmedToken); or (B) stop
passing trimmedToken to githubService.fetchOrgReposWithCache and instead call
tokenService.setToken(trimmedToken) then call
githubService.fetchOrgReposWithCache(trimmedOrg) (and update
fetchOrgReposWithCache to read token from tokenService if needed). Ensure
references to tokenService.setToken and
githubService.fetchOrgReposWithCache(trimmedOrg, trimmedToken) are updated
consistently across the file (lines around the existing calls).

---

Duplicate comments:
In `@src/components/TestServices.tsx`:
- Around line 14-20: Replace hardcoded user-facing strings in TestServices.tsx
with i18n resource keys: do not call alert("Please enter GitHub token") or
alert("Please enter organization name") directly; instead use the project's i18n
utility (e.g., useTranslation()/t or i18n.t) to load localized messages for the
token and org validation messages and for the other occurrences noted (lines
around 39-42, 50, 57, 64). Update the validation branch where trimmedToken and
trimmedOrg are checked to reference the appropriate resource keys (e.g.,
t('testServices.errors.missingToken') and t('testServices.errors.missingOrg'))
and ensure any placeholders/buttons in the same component also use i18n keys.
- Line 33: The forEach callback on repos (repos.forEach(repo =>
console.log(repo.name))) uses an expression-bodied arrow which triggers
lint/suspicious/useIterableCallbackReturn; change it to a block-bodied callback
for the repos.forEach call (make the arrow body use braces and a statement
containing the console.log) so the callback is block-bodied and satisfies the
linter for the TestServices component.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3064ee09-74f2-431c-9d32-b8897fcfc1a0

📥 Commits

Reviewing files that changed from the base of the PR and between ef9138f and 53497b8.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (18)
  • package.json
  • src/App.css
  • src/App.tsx
  • src/components/Dashboard/ContributorActivity.tsx
  • src/components/Dashboard/ContributorChart.tsx
  • src/components/Dashboard/ContributorModal.tsx
  • src/components/Dashboard/Dashboard.tsx
  • src/components/Dashboard/Home.tsx
  • src/components/Dashboard/IssueChart.tsx
  • src/components/Dashboard/LanguagePieChart.tsx
  • src/components/Dashboard/PRChart.tsx
  • src/components/Dashboard/RepoPopularityChart.tsx
  • src/components/Dashboard/RepoTable.tsx
  • src/components/Dashboard/StatsCards.tsx
  • src/components/Dashboard/types.ts
  • src/components/TestServices.tsx
  • src/index.css
  • vite.config.ts

Comment on lines +25 to +27
<h3 className="text-sm font-semibold mb-4 text-gray-800 dark:text-gray-100">
Commit Activity for <span className="text-blue-500">{name}</span>
</h3>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Localize contributor activity heading template.

The Commit Activity for ... text is user-visible and currently inline. Externalize the template string to i18n resources (with a variable placeholder for contributor name).

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/Dashboard/ContributorActivity.tsx` around lines 25 - 27, The
heading text in the ContributorActivity component is user-visible and must be
externalized; update the component (ContributorActivity) to use the i18n
translation API (e.g., useTranslation or t) and replace the inline template
"Commit Activity for {name}" with a call to the translation key (e.g.,
t('contributorActivity', { name })); add the new key with a variable placeholder
to the locale resource files (e.g., "contributorActivity": "Commit Activity for
{{name}}") and ensure the component imports and uses the translation hook
instead of hardcoding the string.

export const ContributorChart: React.FC<ContributorChartProps> = ({ data }) => {
return (
<div className="bg-white dark:bg-gray-900 p-6 rounded-xl shadow-sm border border-gray-100 dark:border-gray-800 h-[400px]">
<h3 className="text-lg font-semibold mb-4 text-gray-800 dark:text-gray-100">Top Contributors by Commits</h3>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Externalize contributor chart title.

Line 23 hardcodes the visible title (Top Contributors by Commits). Move it 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/Dashboard/ContributorChart.tsx` at line 23, The hardcoded
header string in ContributorChart.tsx ("Top Contributors by Commits") must be
externalized to i18n: import and use the project's translation hook (e.g.,
useTranslation or t) in the ContributorChart component and replace the literal
in the <h3> (or its surrounding JSX) with a call to the translation key
(suggested key: dashboard.topContributorsByCommits); then add that key and
translated values to the locale resource files for supported languages so the UI
reads the title from i18n instead of a hardcoded string.

Comment on lines +16 to +29
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm animate-in fade-in duration-200">
<div className="bg-white dark:bg-gray-900 w-full max-w-2xl max-h-[80vh] rounded-2xl shadow-2xl flex flex-col overflow-hidden animate-in zoom-in-95 duration-200">
<div className="p-6 border-b border-gray-100 dark:border-gray-800 flex items-center justify-between">
<div>
<h2 className="text-xl font-bold dark:text-white">{title}</h2>
<p className="text-sm text-gray-500">{contributors.length} contributors found</p>
</div>
<button
onClick={onClose}
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full transition-colors"
>
<X className="w-5 h-5 text-gray-500" />
</button>
</div>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Modal lacks core dialog accessibility semantics.

The modal container is missing role="dialog", aria-modal, and an accessible title binding; this is a major accessibility gap for screen-reader and keyboard users.

♿ Proposed fix
-    <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm animate-in fade-in duration-200">
-      <div className="bg-white dark:bg-gray-900 w-full max-w-2xl max-h-[80vh] rounded-2xl shadow-2xl flex flex-col overflow-hidden animate-in zoom-in-95 duration-200">
+    <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm animate-in fade-in duration-200">
+      <div
+        role="dialog"
+        aria-modal="true"
+        aria-labelledby="contributors-modal-title"
+        className="bg-white dark:bg-gray-900 w-full max-w-2xl max-h-[80vh] rounded-2xl shadow-2xl flex flex-col overflow-hidden animate-in zoom-in-95 duration-200"
+      >
@@
-            <h2 className="text-xl font-bold dark:text-white">{title}</h2>
+            <h2 id="contributors-modal-title" className="text-xl font-bold dark:text-white">{title}</h2>
📝 Committable suggestion

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

Suggested change
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm animate-in fade-in duration-200">
<div className="bg-white dark:bg-gray-900 w-full max-w-2xl max-h-[80vh] rounded-2xl shadow-2xl flex flex-col overflow-hidden animate-in zoom-in-95 duration-200">
<div className="p-6 border-b border-gray-100 dark:border-gray-800 flex items-center justify-between">
<div>
<h2 className="text-xl font-bold dark:text-white">{title}</h2>
<p className="text-sm text-gray-500">{contributors.length} contributors found</p>
</div>
<button
onClick={onClose}
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full transition-colors"
>
<X className="w-5 h-5 text-gray-500" />
</button>
</div>
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm animate-in fade-in duration-200">
<div
role="dialog"
aria-modal="true"
aria-labelledby="contributors-modal-title"
className="bg-white dark:bg-gray-900 w-full max-w-2xl max-h-[80vh] rounded-2xl shadow-2xl flex flex-col overflow-hidden animate-in zoom-in-95 duration-200"
>
<div className="p-6 border-b border-gray-100 dark:border-gray-800 flex items-center justify-between">
<div>
<h2 id="contributors-modal-title" className="text-xl font-bold dark:text-white">{title}</h2>
<p className="text-sm text-gray-500">{contributors.length} contributors found</p>
</div>
<button
onClick={onClose}
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full transition-colors"
>
<X className="w-5 h-5 text-gray-500" />
</button>
</div>
🧰 Tools
🪛 Biome (2.4.4)

[error] 23-26: Provide an explicit type prop for the button element.

(lint/a11y/useButtonType)

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

In `@src/components/Dashboard/ContributorModal.tsx` around lines 16 - 29, Add
proper dialog accessibility to the modal wrapper in ContributorModal.tsx: give
the outer modal container div the attributes role="dialog" and
aria-modal="true", add an id to the title element (the <h2> rendering {title},
e.g., id="contributor-modal-title"), and set aria-labelledby on the dialog
container to that id so screen-readers announce the modal title; also ensure the
close button (onClose handler) has an accessible name (e.g., aria-label="Close")
so it is keyboard/screen-reader operable.

Comment on lines +20 to +21
<h2 className="text-xl font-bold dark:text-white">{title}</h2>
<p className="text-sm text-gray-500">{contributors.length} contributors found</p>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Externalize modal copy to i18n resources.

Strings like contributors found, commits, and Close should be sourced from localization resources, not inline literals.

As per coding guidelines, "User-visible strings should be externalized to resource files (i18n)".

Also applies to: 41-41, 58-58

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

In `@src/components/Dashboard/ContributorModal.tsx` around lines 20 - 21, The
modal currently uses inline user-visible strings (e.g., the paragraph showing
"{contributors.length} contributors found", the header title prop in
ContributorModal, the "commits" label and the "Close" button text) — replace
these literals with i18n resource lookups: import and use your project's
translation helper (e.g., useTranslation/t) in the ContributorModal component
and swap the inline strings for translation keys (e.g., contributorsFound,
commitsLabel, closeButton) while passing contributors.length and title into the
translated/messages where needed; update the i18n resource files with the new
keys and use the keys in the JSX for the h2, the contributors count <p>, the
commits label, and the Close button.

Comment on lines +23 to +28
<button
onClick={onClose}
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full transition-colors"
>
<X className="w-5 h-5 text-gray-500" />
</button>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Icon-only buttons need accessible names.

Icon-only controls on Lines 23-28 and Lines 45-46 should include aria-label so assistive tech can announce their purpose.

Also applies to: 45-46

🧰 Tools
🪛 Biome (2.4.4)

[error] 23-26: Provide an explicit type prop for the button element.

(lint/a11y/useButtonType)

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

In `@src/components/Dashboard/ContributorModal.tsx` around lines 23 - 28, Add
accessible names to the icon-only buttons by adding aria-label attributes:
update the close button (the <button onClick={onClose}> wrapping the <X /> icon
in ContributorModal.tsx) to include aria-label="Close" (or localized
equivalent), and also add a descriptive aria-label to the other icon-only
control referenced around lines 45-46 (the second icon-only <button> in this
component) that reflects its action (e.g., "Open profile" or "Copy email");
ensure the labels are concise, descriptive, and use aria-label on the button
elements so assistive tech can announce their purpose.

export const RepoPopularityChart: React.FC<RepoPopularityProps> = ({ data }) => {
return (
<div className="bg-white dark:bg-gray-900 p-6 rounded-xl shadow-sm border border-gray-100 dark:border-gray-800 h-[400px]">
<h3 className="text-lg font-semibold mb-4 text-gray-800 dark:text-gray-100">Top Repositories by Stars</h3>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Localize the chart title string.

Line 22 hardcodes Top Repositories by Stars; this should come from i18n resources.

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/Dashboard/RepoPopularityChart.tsx` at line 22, Replace the
hardcoded title in the RepoPopularityChart component with a localized string
from your i18n resources: import and use the useTranslation hook (or the
project's translation helper) inside RepoPopularityChart and replace "Top
Repositories by Stars" with a call like t('dashboard.topRepositoriesByStars')
(or the appropriate key); add the new key/value to the locale resource files so
the title is externalized for translations.

Comment on lines +46 to +79
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-100">Repositories</h3>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left">
<thead className="bg-gray-50 dark:bg-gray-800/50 text-gray-600 dark:text-gray-400 text-xs uppercase">
<tr>
<th className="px-6 py-4 font-semibold">Repository</th>
<th
className="px-6 py-4 font-semibold cursor-pointer"
onClick={() => handleSort('stargazers_count')}
>
<div className="flex items-center space-x-1">
<span>Stars</span>
<SortIcon column="stargazers_count" />
</div>
</th>
<th
className="px-6 py-4 font-semibold cursor-pointer"
onClick={() => handleSort('forks_count')}
>
<div className="flex items-center space-x-1">
<span>Forks</span>
<SortIcon column="forks_count" />
</div>
</th>
<th className="px-6 py-4 font-semibold">Language</th>
<th className="px-6 py-4 font-semibold">Open Issues</th>
<th
className="px-6 py-4 font-semibold cursor-pointer"
onClick={() => handleSort('updated_at')}
>
<div className="flex items-center space-x-1">
<span>Last Updated</span>
<SortIcon column="updated_at" />
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Move table labels to localization resources.

UI labels such as Repositories, Repository, Stars, Forks, Language, Open Issues, Last Updated, and - are hardcoded; externalize them for i18n support.

As per coding guidelines, "User-visible strings should be externalized to resource files (i18n)".

Also applies to: 106-106

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

In `@src/components/Dashboard/RepoTable.tsx` around lines 46 - 79, RepoTable
currently has hardcoded user-visible strings (e.g., "Repositories",
"Repository", "Stars", "Forks", "Language", "Open Issues", "Last Updated", and
the "-" placeholder) — replace these with i18n resource lookups: import your
i18n hook (e.g., useTranslation/t) into the RepoTable component, swap each
literal with t('repoTable.xxx') keys (create sensible keys like repoTable.title,
repoTable.repo, repoTable.stars, repoTable.forks, repoTable.language,
repoTable.openIssues, repoTable.lastUpdated, repoTable.emptyPlaceholder), and
add those keys to the localization resource files for supported locales; leave
handler names and components (handleSort, SortIcon) unchanged. Ensure you also
replace any other occurrences of the same hardcoded strings in this component.

Comment on lines +53 to +80
<th
className="px-6 py-4 font-semibold cursor-pointer"
onClick={() => handleSort('stargazers_count')}
>
<div className="flex items-center space-x-1">
<span>Stars</span>
<SortIcon column="stargazers_count" />
</div>
</th>
<th
className="px-6 py-4 font-semibold cursor-pointer"
onClick={() => handleSort('forks_count')}
>
<div className="flex items-center space-x-1">
<span>Forks</span>
<SortIcon column="forks_count" />
</div>
</th>
<th className="px-6 py-4 font-semibold">Language</th>
<th className="px-6 py-4 font-semibold">Open Issues</th>
<th
className="px-6 py-4 font-semibold cursor-pointer"
onClick={() => handleSort('updated_at')}
>
<div className="flex items-center space-x-1">
<span>Last Updated</span>
<SortIcon column="updated_at" />
</div>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Sortable headers are not keyboard-accessible.

Using onClick on <th> (Lines 53-80) creates mouse-only sorting controls. Use real <button type="button"> controls inside headers, and expose sort state with aria-sort.

♿ Proposed fix
-              <th 
-                className="px-6 py-4 font-semibold cursor-pointer"
-                onClick={() => handleSort('stargazers_count')}
-              >
-                <div className="flex items-center space-x-1">
+              <th className="px-6 py-4 font-semibold" aria-sort={sortKey === 'stargazers_count' ? (sortOrder === 'asc' ? 'ascending' : 'descending') : 'none'}>
+                <button
+                  type="button"
+                  className="flex items-center space-x-1 cursor-pointer"
+                  onClick={() => handleSort('stargazers_count')}
+                >
                   <span>Stars</span>
                   <SortIcon column="stargazers_count" />
-                </div>
+                </button>
               </th>
@@
-              <th 
-                className="px-6 py-4 font-semibold cursor-pointer"
-                onClick={() => handleSort('forks_count')}
-              >
-                <div className="flex items-center space-x-1">
+              <th className="px-6 py-4 font-semibold" aria-sort={sortKey === 'forks_count' ? (sortOrder === 'asc' ? 'ascending' : 'descending') : 'none'}>
+                <button
+                  type="button"
+                  className="flex items-center space-x-1 cursor-pointer"
+                  onClick={() => handleSort('forks_count')}
+                >
                   <span>Forks</span>
                   <SortIcon column="forks_count" />
-                </div>
+                </button>
               </th>
@@
-              <th 
-                className="px-6 py-4 font-semibold cursor-pointer"
-                onClick={() => handleSort('updated_at')}
-              >
-                <div className="flex items-center space-x-1">
+              <th className="px-6 py-4 font-semibold" aria-sort={sortKey === 'updated_at' ? (sortOrder === 'asc' ? 'ascending' : 'descending') : 'none'}>
+                <button
+                  type="button"
+                  className="flex items-center space-x-1 cursor-pointer"
+                  onClick={() => handleSort('updated_at')}
+                >
                   <span>Last Updated</span>
                   <SortIcon column="updated_at" />
-                </div>
+                </button>
               </th>
📝 Committable suggestion

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

Suggested change
<th
className="px-6 py-4 font-semibold cursor-pointer"
onClick={() => handleSort('stargazers_count')}
>
<div className="flex items-center space-x-1">
<span>Stars</span>
<SortIcon column="stargazers_count" />
</div>
</th>
<th
className="px-6 py-4 font-semibold cursor-pointer"
onClick={() => handleSort('forks_count')}
>
<div className="flex items-center space-x-1">
<span>Forks</span>
<SortIcon column="forks_count" />
</div>
</th>
<th className="px-6 py-4 font-semibold">Language</th>
<th className="px-6 py-4 font-semibold">Open Issues</th>
<th
className="px-6 py-4 font-semibold cursor-pointer"
onClick={() => handleSort('updated_at')}
>
<div className="flex items-center space-x-1">
<span>Last Updated</span>
<SortIcon column="updated_at" />
</div>
<th className="px-6 py-4 font-semibold" aria-sort={sortKey === 'stargazers_count' ? (sortOrder === 'asc' ? 'ascending' : 'descending') : 'none'}>
<button
type="button"
className="flex items-center space-x-1 cursor-pointer"
onClick={() => handleSort('stargazers_count')}
>
<span>Stars</span>
<SortIcon column="stargazers_count" />
</button>
</th>
<th className="px-6 py-4 font-semibold" aria-sort={sortKey === 'forks_count' ? (sortOrder === 'asc' ? 'ascending' : 'descending') : 'none'}>
<button
type="button"
className="flex items-center space-x-1 cursor-pointer"
onClick={() => handleSort('forks_count')}
>
<span>Forks</span>
<SortIcon column="forks_count" />
</button>
</th>
<th className="px-6 py-4 font-semibold">Language</th>
<th className="px-6 py-4 font-semibold">Open Issues</th>
<th className="px-6 py-4 font-semibold" aria-sort={sortKey === 'updated_at' ? (sortOrder === 'asc' ? 'ascending' : 'descending') : 'none'}>
<button
type="button"
className="flex items-center space-x-1 cursor-pointer"
onClick={() => handleSort('updated_at')}
>
<span>Last Updated</span>
<SortIcon column="updated_at" />
</button>
</th>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Dashboard/RepoTable.tsx` around lines 53 - 80, The header
cells using onClick directly on <th> (the Stars/Forks/Last Updated headers that
call handleSort and render <SortIcon column="..."/>) are not
keyboard-accessible; update each sortable <th> to contain a real <button
type="button"> that receives the onClick and focus, move the handleSort(...)
call onto that button, and remove the click handler from the <th> itself; also
expose sort state by adding an appropriate aria-sort value on the <th> (e.g.,
"ascending", "descending" or "none") based on your current sort state and ensure
SortIcon continues to accept the same column prop so it stays in sync.

Comment on lines +19 to +55
title: 'Total Repositories',
value: stats.totalRepos,
icon: BookOpen,
color: 'text-blue-500',
bg: 'bg-blue-50'
},
{
title: 'Total Stars',
value: stats.totalStars.toLocaleString(),
icon: Star,
color: 'text-yellow-500',
bg: 'bg-yellow-50'
},
{
title: 'Total Forks',
value: stats.totalForks.toLocaleString(),
icon: GitFork,
color: 'text-purple-500',
bg: 'bg-purple-50'
},
{
title: 'Open Issues',
value: stats.totalOpenIssues.toLocaleString(),
icon: Bug,
color: 'text-red-500',
bg: 'bg-red-50'
},
{
title: 'Pull Requests',
value: stats.totalPRs.toLocaleString(),
icon: GitPullRequest,
color: 'text-green-500',
bg: 'bg-green-50'
},
{
title: 'Languages Used',
value: stats.languageCount,
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Externalize stats card labels for localization.

Lines 19, 26, 33, 40, 47, and 54 hardcode user-facing strings in the component. Move these labels into i18n resource files instead of inline literals.

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/Dashboard/StatsCards.tsx` around lines 19 - 55, The component
StatsCards.tsx currently hardcodes user-visible card titles ("Total
Repositories", "Total Stars", "Total Forks", "Open Issues", "Pull Requests",
"Languages Used"); update the component to use the i18n translation function
(e.g. call useTranslation() and replace those title literals with
t('stats.totalRepositories'), t('stats.totalStars'), t('stats.totalForks'),
t('stats.openIssues'), t('stats.pullRequests'), and t('stats.languagesUsed')
respectively), and add corresponding keys to your i18n resource files; ensure
the cards array in StatsCards uses the translated strings and keep the unique
symbols (StatsCards component, the cards array, and useTranslation/t) to locate
the changes.

@@ -0,0 +1,68 @@
import { useState } from "react"
import { tokenService, githubService } from "../services"
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Unify token flow: remove redundant tokenService write or consume it in githubService.

tokenService.setToken(trimmedToken) is currently unused by githubService.fetchOrgReposWithCache(trimmedOrg, trimmedToken) (token is passed directly), which creates two parallel auth paths and weakens API clarity.

♻️ Minimal cleanup option
-import { tokenService, githubService } from "../services"
+import { githubService } from "../services"
@@
-tokenService.setToken(trimmedToken)
 // funtion which fetches org repos is called

Also applies to: 23-29

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

In `@src/components/TestServices.tsx` at line 2, The code currently writes the
token with tokenService.setToken(trimmedToken) but then passes trimmedToken
directly into githubService.fetchOrgReposWithCache(trimmedOrg, trimmedToken),
creating two parallel auth flows; either remove the redundant
tokenService.setToken call or make githubService consume the tokenService
instead of accepting a token param. Fix by choosing one flow: (A) delete the
tokenService.setToken(trimmedToken) calls in TestServices.tsx and keep passing
the token into githubService.fetchOrgReposWithCache(trimmedOrg, trimmedToken);
or (B) stop passing trimmedToken to githubService.fetchOrgReposWithCache and
instead call tokenService.setToken(trimmedToken) then call
githubService.fetchOrgReposWithCache(trimmedOrg) (and update
fetchOrgReposWithCache to read token from tokenService if needed). Ensure
references to tokenService.setToken and
githubService.fetchOrgReposWithCache(trimmedOrg, trimmedToken) are updated
consistently across the file (lines around the existing calls).

@github-actions github-actions bot added size/XL and removed size/XL labels Mar 5, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (12)
src/components/Dashboard/Dashboard.tsx (7)

208-208: ⚠️ Potential issue | 🟡 Minor

Add explicit type="button" for non-submit controls.

These buttons are not submit actions and should explicitly declare type="button".

Suggested fix
-<button onClick={() => setIsHome(true)} className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg text-gray-500">
+<button type="button" onClick={() => setIsHome(true)} className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg text-gray-500">
@@
-<button onClick={() => setActiveTab('overview')} className={`py-4 px-1 flex items-center space-x-2 border-b-2 transition-colors ${activeTab === 'overview' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-gray-500 hover:text-gray-700'}`}>
+<button type="button" onClick={() => setActiveTab('overview')} className={`py-4 px-1 flex items-center space-x-2 border-b-2 transition-colors ${activeTab === 'overview' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-gray-500 hover:text-gray-700'}`}>
@@
-<button onClick={() => setActiveTab('contributors')} className={`py-4 px-1 flex items-center space-x-2 border-b-2 transition-colors ${activeTab === 'contributors' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-gray-500 hover:text-gray-700'}`}>
+<button type="button" onClick={() => setActiveTab('contributors')} className={`py-4 px-1 flex items-center space-x-2 border-b-2 transition-colors ${activeTab === 'contributors' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-gray-500 hover:text-gray-700'}`}>

Also applies to: 266-267

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

In `@src/components/Dashboard/Dashboard.tsx` at line 208, The buttons in the
Dashboard component that call setIsHome (and the similar non-submit controls
later in the component) are missing explicit types and will default to "submit"
inside forms; update those <button> elements in Dashboard (the one using
setIsHome and the other buttons around the ChevronLeft/Chevron icons) to include
type="button" so they don’t trigger form submission—locate the buttons in the
Dashboard.tsx component (the handlers referencing setIsHome and other onClick
handlers) and add the type attribute to each.

61-67: ⚠️ Potential issue | 🟡 Minor

Externalize validation messages to i18n resources.

These validation error strings are user-visible and should be resolved through the localization layer.

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/Dashboard/Dashboard.tsx` around lines 61 - 67, Replace the
hard-coded, user-visible validation strings in the Dashboard component (the
setError calls inside the org name validation and the trimmedToken length check)
with i18n resource lookups; use the component's localization mechanism (e.g.,
useTranslation().t or i18n.t) and reference new keys like
"validation.invalidOrgName" and "validation.tokenTooShort" in the resources,
update the corresponding translation files with these keys, and ensure setError
receives the localized string instead of the literal English message.

118-120: 🛠️ Refactor suggestion | 🟠 Major

Remove any from error and contributor parsing paths.

Use unknown for catch and define a contributor DTO instead of any in forEach.

Suggested fix
-    } catch (err: any) {
-      setError(err.message || 'Failed to fetch repositories.');
+    } catch (err: unknown) {
+      const message = err instanceof Error ? err.message : 'Failed to fetch repositories.';
+      setError(message);
       setStats(null);
     }
@@
-            repoContributors.forEach((c: any) => {
+            repoContributors.forEach((c: { login: string; contributions: number }) => {
               contributorMap[c.login] = (contributorMap[c.login] || 0) + c.contributions;
             });
#!/bin/bash
# Verify remaining `any` usages in this component.
rg -nP '\bany\b' src/components/Dashboard/Dashboard.tsx

Also applies to: 167-169

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

In `@src/components/Dashboard/Dashboard.tsx` around lines 118 - 120, The catch
block and contributor parsing use unsafe `any`; change the catch parameter to
`err: unknown` in the Dashboard component and set the error with a safe
narrowing (e.g., setError(err instanceof Error ? err.message : String(err))).
Define a Contributor DTO (interface Contributor { id: string; name: string;
contributions: number; /* add fields used */ }) and replace any usages in the
contributors parsing loop (the forEach that currently types items as any) to use
Contributor (or validate/parse into Contributor objects before pushing). Update
related parsing code around the repository/stats population where setStats is
called so all `any` types are removed and replaced with the Contributor type and
proper runtime checks.

319-334: ⚠️ Potential issue | 🟠 Major

Replace clickable card <div>s with semantic interactive elements.

These cards are mouse-only interactive right now; keyboard users can’t activate them. Use <button> (preferred) or add keyboard handlers + role/tabIndex.

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

In `@src/components/Dashboard/Dashboard.tsx` around lines 319 - 334, Replace the
clickable <div> cards with semantic interactive elements by converting the two
divs that call openContributorModal('Total Contributors', allContributors) and
openContributorModal('New Contributors', newContributors) into <button> elements
(set type="button") so keyboard users can activate them; preserve the existing
className styling, onClick handlers, and inner content, and add accessible names
(aria-label or ensure visible text serves as label) so Total Contributors and
New Contributors remain discoverable to assistive tech.

84-117: ⚠️ Potential issue | 🟠 Major

Cache-first behavior is not implemented in handleSearch.

This path always fetches from GitHub and always marks source as API, so IndexedDB cache usage is bypassed in the main dashboard flow.

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

In `@src/components/Dashboard/Dashboard.tsx` around lines 84 - 117, The code in
handleSearch always fetches from GitHub and sets setDataSource('API'), bypassing
IndexedDB; change to a cache-first flow: first attempt to read cached repos for
the org (implement or call a helper like getCachedRepos(org)) and validate
freshness (e.g., compare cached.length to orgData.public_repos or check
timestamp); if cache is valid, call setRepos(cached), setDataSource('Cache'),
setLastUpdated(cached.timestamp) and skip the network fetch; if no valid cache,
perform the existing fetch logic using finalToken and org, then after successful
fetch call setRepos(allRepos), setDataSource('API'), setLastUpdated(Date.now())
and persist results to IndexedDB via a save function (e.g.,
saveReposToCache(org, allRepos, timestamp)); keep using processData(allRepos,
finalToken, org, actualRepoCount) after either path as needed.

188-191: ⚠️ Potential issue | 🟠 Major

Activity chart data is randomized per run.

Using Math.random() for commits creates non-deterministic, non-real analytics.

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

In `@src/components/Dashboard/Dashboard.tsx` around lines 188 - 191, The activity
chart is populated with non-deterministic values using Math.random() in the
block that builds activity and calls setActivityData; replace this with
deterministic data or a real data source: either (a) compute commits from a
stable fixture or from props/state passed into the Dashboard component, (b)
fetch real analytics via the existing API and map the response into the activity
shape, or (c) use a seeded RNG initialized from a stable seed if synthetic data
is required—ensure the change removes Math.random() usage in the code that
creates the activity array and still calls setActivityData(activity).

130-154: ⚠️ Potential issue | 🟠 Major

Issue/PR chart values are synthetic, not source-backed metrics.

The current multipliers (0.4, 1.5, 3, 0.5) produce fabricated analytics and can mislead users.

Does GitHub's repository field `open_issues_count` include pull requests, and what REST endpoints should be used to compute real open/closed issue counts and open/closed/merged PR counts?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Dashboard/Dashboard.tsx` around lines 130 - 154, The dashboard
currently fabricates issue/PR counts using multipliers (totalPRs, Issue/PR chart
values); replace that with real counts by querying GitHub for each repo in data:
compute issue counts by querying the Issues API or Search API and filtering out
items with a pull_request field (e.g., GET
/repos/{owner}/{repo}/issues?state=open and exclude PRs or use
/search/issues?q=repo:OWNER/REPO+type:issue+state:open for totals), and compute
PR counts via the Pulls API or Search API (GET
/repos/{owner}/{repo}/pulls?state=open for open PRs; for merged use the Search
API or list closed pulls and check merged_at). Aggregate these per-repo totals
to produce real totalOpenIssues and totalPRs and then update setIssueData and
setPrData to use those aggregated values instead of synthetic multipliers;
modify any code using totalOpenIssues or totalPRs (references: totalOpenIssues,
totalPRs, setIssueData, setPrData, data) to use the API-derived counts.
src/components/Dashboard/IssueChart.tsx (1)

13-13: ⚠️ Potential issue | 🟡 Minor

Externalize the chart title text.

Issue Analytics should come from i18n resources instead of being hardcoded in JSX.

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/Dashboard/IssueChart.tsx` at line 13, Replace the hardcoded
"Issue Analytics" header in the IssueChart component with an i18n lookup: import
and use the project's translation hook/function (e.g., useTranslation / t)
inside the IssueChart component and change the h3 content to
t('dashboard.issueAnalytics') (or the agreed resource key), then add that
key/value to the locale resource files; update any tests/fixtures that assert
the header if present.
src/components/Dashboard/ContributorModal.tsx (3)

23-28: ⚠️ Potential issue | 🟠 Major

Add accessible names and explicit type="button" on modal buttons.

Icon-only controls need aria-label, and these non-submit buttons should explicitly set type="button".

Suggested fix
-          <button 
+          <button
+            type="button"
+            aria-label="Close"
             onClick={onClose}
             className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full transition-colors"
           >
@@
-                   <button className="p-1.5 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg text-gray-400 hover:text-blue-500"><Mail className="w-4 h-4" /></button>
-                   <button className="p-1.5 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg text-gray-400 hover:text-blue-500"><ExternalLink className="w-4 h-4" /></button>
+                   <button type="button" aria-label="Email contributor" className="p-1.5 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg text-gray-400 hover:text-blue-500"><Mail className="w-4 h-4" /></button>
+                   <button type="button" aria-label="Open contributor profile" className="p-1.5 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg text-gray-400 hover:text-blue-500"><ExternalLink className="w-4 h-4" /></button>
@@
-          <button 
+          <button
+            type="button"
             onClick={onClose}
             className="px-6 py-2 bg-gray-900 dark:bg-white dark:text-gray-900 text-white rounded-xl font-bold text-sm hover:opacity-90 transition-opacity"
           >

Also applies to: 45-46, 54-57

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

In `@src/components/Dashboard/ContributorModal.tsx` around lines 23 - 28, The
icon-only close button in ContributorModal (the button rendering <X /> with
onClick={onClose}) and the other icon-only buttons in the same component need
accessible names and explicit button types: add type="button" to each non-submit
<button> and add an aria-label describing the action (e.g., aria-label="Close"
for the X button) so assistive tech can identify them; update the other buttons
referenced in this file (the ones rendering icons around the modal controls)
similarly to include descriptive aria-label values and type="button".

17-21: ⚠️ Potential issue | 🟠 Major

Add proper dialog semantics to the modal container.

The modal wrapper should include role="dialog", aria-modal="true", and aria-labelledby tied to the title so assistive tech can identify and announce it correctly.

Suggested fix
-      <div className="bg-white dark:bg-gray-900 w-full max-w-2xl max-h-[80vh] rounded-2xl shadow-2xl flex flex-col overflow-hidden animate-in zoom-in-95 duration-200">
+      <div
+        role="dialog"
+        aria-modal="true"
+        aria-labelledby="contributors-modal-title"
+        className="bg-white dark:bg-gray-900 w-full max-w-2xl max-h-[80vh] rounded-2xl shadow-2xl flex flex-col overflow-hidden animate-in zoom-in-95 duration-200"
+      >
@@
-            <h2 className="text-xl font-bold dark:text-white">{title}</h2>
+            <h2 id="contributors-modal-title" className="text-xl font-bold dark:text-white">{title}</h2>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Dashboard/ContributorModal.tsx` around lines 17 - 21, The
modal container div in ContributorModal.tsx needs proper dialog semantics: add
role="dialog" and aria-modal="true" to the outermost modal wrapper (the div with
className "bg-white ... animate-in") and set aria-labelledby to the id of the
title; also give the <h2> that renders {title} a stable id (e.g.,
"contributor-modal-title") so aria-labelledby can reference it, ensuring
assistive tech announces the dialog correctly.

21-21: ⚠️ Potential issue | 🟡 Minor

Externalize modal copy to i18n resources.

contributors found, commits, and Close should be localized instead of hardcoded literals.

As per coding guidelines, "User-visible strings should be externalized to resource files (i18n)".

Also applies to: 41-41, 58-58

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

In `@src/components/Dashboard/ContributorModal.tsx` at line 21, In
ContributorModal, replace hardcoded user-visible strings ("contributors found",
"commits", "Close") with i18n resource lookups: import and use your localization
hook (e.g., useTranslation/t) in the ContributorModal component and swap the
literal JSX text at the contributors count paragraph, the commits label, and the
Close button to keys such as contributorModal.contributorsFound,
contributorModal.commits, and contributorModal.close (add those keys to the
locale resource files and provide appropriate translations).
src/components/Dashboard/PRChart.tsx (1)

13-13: ⚠️ Potential issue | 🟡 Minor

Externalize the chart title to i18n resources.

Pull Request Analytics is user-visible text and should be sourced from localization resources, not an inline literal.

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/Dashboard/PRChart.tsx` at line 13, The hard-coded title string
in the PRChart component ("Pull Request Analytics") should be replaced with an
i18n lookup: import and use the project's translation hook/function (e.g.,
useTranslation/t) inside PRChart and replace the h3 literal with
t('dashboard.prChart.title') (or your chosen key); also add the corresponding
key/value to the localization resource files for each supported locale. Ensure
the translation key is used in the h3 element in PRChart so all user-visible
text is externalized.
🤖 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/ContributorModal.tsx`:
- Around line 45-46: The Mail and ExternalLink buttons in ContributorModal are
rendered without actions; update the JSX in ContributorModal.tsx to wire them to
real handlers: for the Mail (Mail icon) add an onClick or convert to an anchor
using a mailto: link built from the contributor's email (e.g.,
contributor.email) to open the user's mail client, and for the ExternalLink add
an onClick or anchor with target="_blank" rel="noopener noreferrer" pointing to
the contributor's external URL (e.g., contributor.url or contributor.website);
ensure you use the existing contributor prop/state used in the component and
keep accessibility (aria-label) on the buttons/links.

In `@src/components/Dashboard/Dashboard.tsx`:
- Line 247: The GitHub link in the Dashboard component is using the live input
variable orgName.trim(), which can diverge from the fetched organization shown
in stats; update the href generation (the expression used in
href={`https://github.com/${...}`}) to use the fetched org state (e.g.,
org.login or the fetchedOrg/fetchedOrgName state used to store the API result)
instead of orgName.trim(), falling back to orgName only if the fetched value is
absent, so the link always reflects the displayed organization.

---

Duplicate comments:
In `@src/components/Dashboard/ContributorModal.tsx`:
- Around line 23-28: The icon-only close button in ContributorModal (the button
rendering <X /> with onClick={onClose}) and the other icon-only buttons in the
same component need accessible names and explicit button types: add
type="button" to each non-submit <button> and add an aria-label describing the
action (e.g., aria-label="Close" for the X button) so assistive tech can
identify them; update the other buttons referenced in this file (the ones
rendering icons around the modal controls) similarly to include descriptive
aria-label values and type="button".
- Around line 17-21: The modal container div in ContributorModal.tsx needs
proper dialog semantics: add role="dialog" and aria-modal="true" to the
outermost modal wrapper (the div with className "bg-white ... animate-in") and
set aria-labelledby to the id of the title; also give the <h2> that renders
{title} a stable id (e.g., "contributor-modal-title") so aria-labelledby can
reference it, ensuring assistive tech announces the dialog correctly.
- Line 21: In ContributorModal, replace hardcoded user-visible strings
("contributors found", "commits", "Close") with i18n resource lookups: import
and use your localization hook (e.g., useTranslation/t) in the ContributorModal
component and swap the literal JSX text at the contributors count paragraph, the
commits label, and the Close button to keys such as
contributorModal.contributorsFound, contributorModal.commits, and
contributorModal.close (add those keys to the locale resource files and provide
appropriate translations).

In `@src/components/Dashboard/Dashboard.tsx`:
- Line 208: The buttons in the Dashboard component that call setIsHome (and the
similar non-submit controls later in the component) are missing explicit types
and will default to "submit" inside forms; update those <button> elements in
Dashboard (the one using setIsHome and the other buttons around the
ChevronLeft/Chevron icons) to include type="button" so they don’t trigger form
submission—locate the buttons in the Dashboard.tsx component (the handlers
referencing setIsHome and other onClick handlers) and add the type attribute to
each.
- Around line 61-67: Replace the hard-coded, user-visible validation strings in
the Dashboard component (the setError calls inside the org name validation and
the trimmedToken length check) with i18n resource lookups; use the component's
localization mechanism (e.g., useTranslation().t or i18n.t) and reference new
keys like "validation.invalidOrgName" and "validation.tokenTooShort" in the
resources, update the corresponding translation files with these keys, and
ensure setError receives the localized string instead of the literal English
message.
- Around line 118-120: The catch block and contributor parsing use unsafe `any`;
change the catch parameter to `err: unknown` in the Dashboard component and set
the error with a safe narrowing (e.g., setError(err instanceof Error ?
err.message : String(err))). Define a Contributor DTO (interface Contributor {
id: string; name: string; contributions: number; /* add fields used */ }) and
replace any usages in the contributors parsing loop (the forEach that currently
types items as any) to use Contributor (or validate/parse into Contributor
objects before pushing). Update related parsing code around the repository/stats
population where setStats is called so all `any` types are removed and replaced
with the Contributor type and proper runtime checks.
- Around line 319-334: Replace the clickable <div> cards with semantic
interactive elements by converting the two divs that call
openContributorModal('Total Contributors', allContributors) and
openContributorModal('New Contributors', newContributors) into <button> elements
(set type="button") so keyboard users can activate them; preserve the existing
className styling, onClick handlers, and inner content, and add accessible names
(aria-label or ensure visible text serves as label) so Total Contributors and
New Contributors remain discoverable to assistive tech.
- Around line 84-117: The code in handleSearch always fetches from GitHub and
sets setDataSource('API'), bypassing IndexedDB; change to a cache-first flow:
first attempt to read cached repos for the org (implement or call a helper like
getCachedRepos(org)) and validate freshness (e.g., compare cached.length to
orgData.public_repos or check timestamp); if cache is valid, call
setRepos(cached), setDataSource('Cache'), setLastUpdated(cached.timestamp) and
skip the network fetch; if no valid cache, perform the existing fetch logic
using finalToken and org, then after successful fetch call setRepos(allRepos),
setDataSource('API'), setLastUpdated(Date.now()) and persist results to
IndexedDB via a save function (e.g., saveReposToCache(org, allRepos,
timestamp)); keep using processData(allRepos, finalToken, org, actualRepoCount)
after either path as needed.
- Around line 188-191: The activity chart is populated with non-deterministic
values using Math.random() in the block that builds activity and calls
setActivityData; replace this with deterministic data or a real data source:
either (a) compute commits from a stable fixture or from props/state passed into
the Dashboard component, (b) fetch real analytics via the existing API and map
the response into the activity shape, or (c) use a seeded RNG initialized from a
stable seed if synthetic data is required—ensure the change removes
Math.random() usage in the code that creates the activity array and still calls
setActivityData(activity).
- Around line 130-154: The dashboard currently fabricates issue/PR counts using
multipliers (totalPRs, Issue/PR chart values); replace that with real counts by
querying GitHub for each repo in data: compute issue counts by querying the
Issues API or Search API and filtering out items with a pull_request field
(e.g., GET /repos/{owner}/{repo}/issues?state=open and exclude PRs or use
/search/issues?q=repo:OWNER/REPO+type:issue+state:open for totals), and compute
PR counts via the Pulls API or Search API (GET
/repos/{owner}/{repo}/pulls?state=open for open PRs; for merged use the Search
API or list closed pulls and check merged_at). Aggregate these per-repo totals
to produce real totalOpenIssues and totalPRs and then update setIssueData and
setPrData to use those aggregated values instead of synthetic multipliers;
modify any code using totalOpenIssues or totalPRs (references: totalOpenIssues,
totalPRs, setIssueData, setPrData, data) to use the API-derived counts.

In `@src/components/Dashboard/IssueChart.tsx`:
- Line 13: Replace the hardcoded "Issue Analytics" header in the IssueChart
component with an i18n lookup: import and use the project's translation
hook/function (e.g., useTranslation / t) inside the IssueChart component and
change the h3 content to t('dashboard.issueAnalytics') (or the agreed resource
key), then add that key/value to the locale resource files; update any
tests/fixtures that assert the header if present.

In `@src/components/Dashboard/PRChart.tsx`:
- Line 13: The hard-coded title string in the PRChart component ("Pull Request
Analytics") should be replaced with an i18n lookup: import and use the project's
translation hook/function (e.g., useTranslation/t) inside PRChart and replace
the h3 literal with t('dashboard.prChart.title') (or your chosen key); also add
the corresponding key/value to the localization resource files for each
supported locale. Ensure the translation key is used in the h3 element in
PRChart so all user-visible text is externalized.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2d280065-f6bd-4e7b-bcf4-32067150a741

📥 Commits

Reviewing files that changed from the base of the PR and between 53497b8 and 507504c.

📒 Files selected for processing (4)
  • src/components/Dashboard/ContributorModal.tsx
  • src/components/Dashboard/Dashboard.tsx
  • src/components/Dashboard/IssueChart.tsx
  • src/components/Dashboard/PRChart.tsx

Comment on lines +45 to +46
<button className="p-1.5 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg text-gray-400 hover:text-blue-500"><Mail className="w-4 h-4" /></button>
<button className="p-1.5 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg text-gray-400 hover:text-blue-500"><ExternalLink className="w-4 h-4" /></button>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

These action buttons are non-functional in the current implementation.

The Mail and ExternalLink controls render as clickable buttons but have no onClick/href, so they do nothing when activated.

🧰 Tools
🪛 Biome (2.4.4)

[error] 45-45: Provide an explicit type prop for the button element.

(lint/a11y/useButtonType)


[error] 46-46: Provide an explicit type prop for the button element.

(lint/a11y/useButtonType)

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

In `@src/components/Dashboard/ContributorModal.tsx` around lines 45 - 46, The Mail
and ExternalLink buttons in ContributorModal are rendered without actions;
update the JSX in ContributorModal.tsx to wire them to real handlers: for the
Mail (Mail icon) add an onClick or convert to an anchor using a mailto: link
built from the contributor's email (e.g., contributor.email) to open the user's
mail client, and for the ExternalLink add an onClick or anchor with
target="_blank" rel="noopener noreferrer" pointing to the contributor's external
URL (e.g., contributor.url or contributor.website); ensure you use the existing
contributor prop/state used in the component and keep accessibility (aria-label)
on the buttons/links.

<div className="flex items-center space-x-4 shrink-0">
{stats && (
<a
href={`https://github.com/${orgName.trim()}`}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Bind the GitHub link to fetched org state, not live input state.

Using orgName.trim() here can open a different org than the one currently shown in stats if the input is edited post-fetch.

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

In `@src/components/Dashboard/Dashboard.tsx` at line 247, The GitHub link in the
Dashboard component is using the live input variable orgName.trim(), which can
diverge from the fetched organization shown in stats; update the href generation
(the expression used in href={`https://github.com/${...}`}) to use the fetched
org state (e.g., org.login or the fetchedOrg/fetchedOrgName state used to store
the API result) instead of orgName.trim(), falling back to orgName only if the
fetched value is absent, so the link always reflects the displayed organization.

@github-actions github-actions bot added size/XL and removed size/XL labels Mar 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE]: Implementation of Secure Local PAT Storage and IndexedDB Caching Layer

1 participant