feat: implement native IndexedDB cache-first data layer#31
feat: implement native IndexedDB cache-first data layer#31swathi2006 wants to merge 7 commits intoAOSSIE-Org:mainfrom
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughReplaces 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (7)
src/App.tsxsrc/components/TestServices.tsxsrc/services/cacheService.tssrc/services/githubService.tssrc/services/idbService.tssrc/services/index.tssrc/services/tokenService.ts
| async getRepos(org: string): Promise<GitHubRepo[] | null> { | ||
| const entry = await getFromIDB(org) | ||
|
|
There was a problem hiding this comment.
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.
| 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.
| 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(); |
There was a problem hiding this comment.
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.
| 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')).
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
src/services/githubService.ts (1)
10-39:⚠️ Potential issue | 🟠 MajorFetch 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
📒 Files selected for processing (4)
src/components/TestServices.tsxsrc/services/cacheService.tssrc/services/githubService.tssrc/services/idbService.ts
src/services/cacheService.ts
Outdated
| 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 | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (3)
src/services/cacheService.ts (2)
41-43:⚠️ Potential issue | 🟠 MajorTreat IndexedDB read failures as cache misses.
If
getFromIDBthrows, 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 | 🟡 MinorRemove
anyand harden cache entry validation.The current guard still relies on
anyand 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 | 🟠 MajorExternalize user-facing copy to i18n resources.
These strings are still hardcoded (
alertmessages, 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`).
|
|
||
| console.log("Repos count:", repos.length) | ||
| console.log("Repo names:") | ||
| repos.forEach(repo => console.log(repo.name)) |
There was a problem hiding this comment.
🧩 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.tsxRepository: AOSSIE-Org/OrgExplorer
Length of output: 120
🏁 Script executed:
# Also check the full context around line 33
sed -n '25,40p' src/components/TestServices.tsxRepository: 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 -10Repository: 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.
| 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.
There was a problem hiding this comment.
Actionable comments posted: 27
♻️ Duplicate comments (2)
src/components/TestServices.tsx (2)
14-20:⚠️ Potential issue | 🟠 MajorMove 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 | 🟡 MinorUse a block-bodied
forEachcallback 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
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (18)
package.jsonsrc/App.csssrc/App.tsxsrc/components/Dashboard/ContributorActivity.tsxsrc/components/Dashboard/ContributorChart.tsxsrc/components/Dashboard/ContributorModal.tsxsrc/components/Dashboard/Dashboard.tsxsrc/components/Dashboard/Home.tsxsrc/components/Dashboard/IssueChart.tsxsrc/components/Dashboard/LanguagePieChart.tsxsrc/components/Dashboard/PRChart.tsxsrc/components/Dashboard/RepoPopularityChart.tsxsrc/components/Dashboard/RepoTable.tsxsrc/components/Dashboard/StatsCards.tsxsrc/components/Dashboard/types.tssrc/components/TestServices.tsxsrc/index.cssvite.config.ts
| <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> |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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.
| <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> |
There was a problem hiding this comment.
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.
| <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.
| <h2 className="text-xl font-bold dark:text-white">{title}</h2> | ||
| <p className="text-sm text-gray-500">{contributors.length} contributors found</p> |
There was a problem hiding this comment.
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.
| <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> |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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.
| <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" /> |
There was a problem hiding this comment.
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.
| <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> |
There was a problem hiding this comment.
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.
| <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.
| 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, |
There was a problem hiding this comment.
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" | |||
There was a problem hiding this comment.
🧹 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 calledAlso 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).
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (12)
src/components/Dashboard/Dashboard.tsx (7)
208-208:⚠️ Potential issue | 🟡 MinorAdd 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 | 🟡 MinorExternalize 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 | 🟠 MajorRemove
anyfrom error and contributor parsing paths.Use
unknownforcatchand define a contributor DTO instead ofanyinforEach.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.tsxAlso 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 | 🟠 MajorReplace 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 | 🟠 MajorCache-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 | 🟠 MajorActivity 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 | 🟠 MajorIssue/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 | 🟡 MinorExternalize the chart title text.
Issue Analyticsshould 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 | 🟠 MajorAdd accessible names and explicit
type="button"on modal buttons.Icon-only controls need
aria-label, and these non-submit buttons should explicitly settype="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 | 🟠 MajorAdd proper dialog semantics to the modal container.
The modal wrapper should include
role="dialog",aria-modal="true", andaria-labelledbytied 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 | 🟡 MinorExternalize modal copy to i18n resources.
contributors found,commits, andCloseshould 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 | 🟡 MinorExternalize the chart title to i18n resources.
Pull Request Analyticsis 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
📒 Files selected for processing (4)
src/components/Dashboard/ContributorModal.tsxsrc/components/Dashboard/Dashboard.tsxsrc/components/Dashboard/IssueChart.tsxsrc/components/Dashboard/PRChart.tsx
| <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> |
There was a problem hiding this comment.
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()}`} |
There was a problem hiding this comment.
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.
Addressed Issues:
Fixes #27
Screenshots/Recordings:
Demonstration of cache-first behavior:
-Tested with Flipkart organisation
Additional Notes:
This introduces a lightweight caching prototype using native IndexedDB.
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
Summary by CodeRabbit
New Features
Chores