feat(repos): show topic badges on repo cards#201
Conversation
…loses PRODHOSH#175) fetchGitHubUser now returns a discriminated union instead of nullable JSON. The page component only calls notFound() on a genuine 404. Rate limits and server errors throw, which the error boundary catches with a retry option. generateMetadata falls back gracefully on any failure. Signed-off-by: Adit Jain <aditjain2005@gmail.com>
… assertion
Addresses CodeRabbit review:
- fetchGitHubUser: wrapped in try-catch so network errors return { status: 'error' } instead of throwing
- fetchGitHubRepos: wrapped in try-catch for consistency with other fetch functions
- Replaced (user.name as string) with typeof runtime check
- Skipped generateMetadata try-catch: redundant since fetchGitHubUser now never throws
Signed-off-by: Adit Jain <aditjain2005@gmail.com>
…ags) Merges the discriminated union error handling from this PR with the OG/Twitter metadata from PR PRODHOSH#174 (now on main). Uses typeof runtime checks to safely access user fields from Record<string, unknown>. Signed-off-by: Adit Jain <aditjain2005@gmail.com>
Signed-off-by: Adit Jain <aditjain2005@gmail.com>
Replaced Record<string, unknown> with a proper interface matching the GitHub REST API response. Removes redundant typeof guards in generateMetadata. Fixes tsc --noEmit failure where ProfileView expected typed GitHubUser. Signed-off-by: Adit Jain <aditjain2005@gmail.com>
Adds repository topics as pill badges on each repo card (up to 3 + overflow count). Changes: - Accept header changed to mercy-preview to include topics in REST response - GitHubRepo and GitHubRepoLike interfaces updated with topics field - mapRepos passes topics through - ProfileView renders topic pills using pill-tag-soft style from DESIGN.md Signed-off-by: Adit Jain <aditjain2005@gmail.com>
📝 WalkthroughWalkthroughThe PR adds repository topics to the repo data flow and renders them as pills on repo cards. It also changes GitHub user fetching to use a different repos request media type and status-based metadata generation. ChangesProfile topics and user fetch handling
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/profile/ProfileView.tsx`:
- Around line 547-548: The overflow badge in ProfileView should use the required
“+N more” label instead of only “+N”. Update the JSX in ProfileView.tsx where
the topics overflow count is rendered so the displayed text includes the word
“more” while still using repo.topics.length and the existing overflow
calculation logic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 48eff74f-d57a-4c81-ba9e-83f8c8ff07c1
📒 Files selected for processing (4)
src/app/[username]/page.tsxsrc/components/profile/ProfileView.tsxsrc/lib/profile-data.tssrc/types/index.ts
…flow label Signed-off-by: Adit Jain <aditjain2005@gmail.com>
|
Re the pre-merge warnings:
|
Took main's updated page.tsx and reapplied the mercy-preview Accept header for topic fetching. All other topic changes (types, profile-data, ProfileView, github.ts) merged cleanly. Signed-off-by: Adit Jain <aditjain2005@gmail.com>
Signed-off-by: Adit Jain <aditjain2005@gmail.com>
|
Merge conflicts resolved. Ready for review. |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/app/[username]/page.tsx (2)
56-72: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
generateMetadatareads a result shapefetchGitHubUserno longer returns.
fetchGitHubUsernow returns eithernullor the raw user object (res.json()), but this code treats it as a discriminated union:result.status !== "ok"(Line 57) andresult.data(Line 63).result.statuswill always beundefined, so the OK branch always runs, and whenresultisnull(non-OK/error)result.statusthrows aTypeError.result.datais also alwaysundefined, so the user fields below dereferenceundefined.Branch on
nulland use the result directly as the user.🔧 Proposed fix
- const result = await fetchGitHubUser(username); - if (result.status !== "ok") { + const user = await fetchGitHubUser(username); + if (!user) { return { title: `${username} - OSSfolio`, description: `Open-source profile for ${username}.`, }; } - const user = result.data; const displayName = user.name || username;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/`[username]/page.tsx around lines 56 - 72, `generateMetadata` in the page component is still treating `fetchGitHubUser` as a status/data union even though it now returns either `null` or the raw user object. Update the logic to branch on a null result from `fetchGitHubUser(username)` and use the returned object directly as the user, removing the `status` and `data` access in `generateMetadata` so the metadata fields are built from the actual user payload.
116-123: 🩺 Stability & Availability | 🔴 CriticalReplace the stale
userResultbranch insrc/app/[username]/page.tsx:116-123.fetchGitHubUsernow returnsnullon non-OK responses and only throwsRateLimit, souserResult.status/code/datais invalid here;const user = userResult.dataalso redeclaresuserand breaks the build.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/`[username]/page.tsx around lines 116 - 123, The `userResult` handling in `Page` is stale and no longer matches `fetchGitHubUser`’s return shape. Update the branch around `userResult` to handle the new contract: treat `null` as the not-found case, let the `RateLimit` throw path propagate, and remove any `status/code/data` access. Also fix the duplicate `user` declaration by assigning the fetched value to the existing `user` binding instead of redeclaring it.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/app/`[username]/page.tsx:
- Around line 97-102: The fetched page state in the [username]/page.tsx
component is using `any`, which removes type safety for the rest of the page.
Update the declarations for user, repos, liveStats, orgs, and
contributionCalendar to use the concrete return types from the corresponding
fetch helpers, and thread those types through the logic that consumes them in
the page component.
- Around line 28-38: The rate-limit path in the data fetch logic is swallowing
the intended exception inside the JSON parsing try/catch, so the `RateLimit`
signal never reaches `ProfilePage`. Update the fetch helper in
`src/app/[username]/page.tsx` to inspect the failed GitHub response first, store
whether it matches a rate-limit condition, then throw the `RateLimit` error
after the parsing block so it escapes the empty catch; this allows the `catch
(e)` handling in `ProfilePage` to set `rateLimited` correctly.
In `@src/components/profile/ProfileView.tsx`:
- Line 697: The language swatch in ProfileView uses an invalid fallback hex
color, so the inline style fallback is ignored. Update the fallback in the span
rendering for the language badge to use the valid 6-digit gray color already
used elsewhere in ProfileView (the repo-language swatch), keeping the
LANG_COLORS lookup and the badge markup otherwise unchanged.
---
Outside diff comments:
In `@src/app/`[username]/page.tsx:
- Around line 56-72: `generateMetadata` in the page component is still treating
`fetchGitHubUser` as a status/data union even though it now returns either
`null` or the raw user object. Update the logic to branch on a null result from
`fetchGitHubUser(username)` and use the returned object directly as the user,
removing the `status` and `data` access in `generateMetadata` so the metadata
fields are built from the actual user payload.
- Around line 116-123: The `userResult` handling in `Page` is stale and no
longer matches `fetchGitHubUser`’s return shape. Update the branch around
`userResult` to handle the new contract: treat `null` as the not-found case, let
the `RateLimit` throw path propagate, and remove any `status/code/data` access.
Also fix the duplicate `user` declaration by assigning the fetched value to the
existing `user` binding instead of redeclaring it.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 289eb550-e8e8-49c9-9f30-f222c85b4dde
📒 Files selected for processing (4)
src/app/[username]/page.tsxsrc/components/profile/ProfileView.tsxsrc/lib/github.tssrc/lib/profile-data.ts
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/app/[username]/page.tsx (2)
56-72: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
generateMetadatareads a result shapefetchGitHubUserno longer returns.
fetchGitHubUsernow returns eithernullor the raw user object (res.json()), but this code treats it as a discriminated union:result.status !== "ok"(Line 57) andresult.data(Line 63).result.statuswill always beundefined, so the OK branch always runs, and whenresultisnull(non-OK/error)result.statusthrows aTypeError.result.datais also alwaysundefined, so the user fields below dereferenceundefined.Branch on
nulland use the result directly as the user.🔧 Proposed fix
- const result = await fetchGitHubUser(username); - if (result.status !== "ok") { + const user = await fetchGitHubUser(username); + if (!user) { return { title: `${username} - OSSfolio`, description: `Open-source profile for ${username}.`, }; } - const user = result.data; const displayName = user.name || username;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/`[username]/page.tsx around lines 56 - 72, `generateMetadata` in the page component is still treating `fetchGitHubUser` as a status/data union even though it now returns either `null` or the raw user object. Update the logic to branch on a null result from `fetchGitHubUser(username)` and use the returned object directly as the user, removing the `status` and `data` access in `generateMetadata` so the metadata fields are built from the actual user payload.
116-123: 🩺 Stability & Availability | 🔴 CriticalReplace the stale
userResultbranch insrc/app/[username]/page.tsx:116-123.fetchGitHubUsernow returnsnullon non-OK responses and only throwsRateLimit, souserResult.status/code/datais invalid here;const user = userResult.dataalso redeclaresuserand breaks the build.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/`[username]/page.tsx around lines 116 - 123, The `userResult` handling in `Page` is stale and no longer matches `fetchGitHubUser`’s return shape. Update the branch around `userResult` to handle the new contract: treat `null` as the not-found case, let the `RateLimit` throw path propagate, and remove any `status/code/data` access. Also fix the duplicate `user` declaration by assigning the fetched value to the existing `user` binding instead of redeclaring it.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/app/`[username]/page.tsx:
- Around line 97-102: The fetched page state in the [username]/page.tsx
component is using `any`, which removes type safety for the rest of the page.
Update the declarations for user, repos, liveStats, orgs, and
contributionCalendar to use the concrete return types from the corresponding
fetch helpers, and thread those types through the logic that consumes them in
the page component.
- Around line 28-38: The rate-limit path in the data fetch logic is swallowing
the intended exception inside the JSON parsing try/catch, so the `RateLimit`
signal never reaches `ProfilePage`. Update the fetch helper in
`src/app/[username]/page.tsx` to inspect the failed GitHub response first, store
whether it matches a rate-limit condition, then throw the `RateLimit` error
after the parsing block so it escapes the empty catch; this allows the `catch
(e)` handling in `ProfilePage` to set `rateLimited` correctly.
In `@src/components/profile/ProfileView.tsx`:
- Line 697: The language swatch in ProfileView uses an invalid fallback hex
color, so the inline style fallback is ignored. Update the fallback in the span
rendering for the language badge to use the valid 6-digit gray color already
used elsewhere in ProfileView (the repo-language swatch), keeping the
LANG_COLORS lookup and the badge markup otherwise unchanged.
---
Outside diff comments:
In `@src/app/`[username]/page.tsx:
- Around line 56-72: `generateMetadata` in the page component is still treating
`fetchGitHubUser` as a status/data union even though it now returns either
`null` or the raw user object. Update the logic to branch on a null result from
`fetchGitHubUser(username)` and use the returned object directly as the user,
removing the `status` and `data` access in `generateMetadata` so the metadata
fields are built from the actual user payload.
- Around line 116-123: The `userResult` handling in `Page` is stale and no
longer matches `fetchGitHubUser`’s return shape. Update the branch around
`userResult` to handle the new contract: treat `null` as the not-found case, let
the `RateLimit` throw path propagate, and remove any `status/code/data` access.
Also fix the duplicate `user` declaration by assigning the fetched value to the
existing `user` binding instead of redeclaring it.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 289eb550-e8e8-49c9-9f30-f222c85b4dde
📒 Files selected for processing (4)
src/app/[username]/page.tsxsrc/components/profile/ProfileView.tsxsrc/lib/github.tssrc/lib/profile-data.ts
🛑 Comments failed to post (3)
src/app/[username]/page.tsx (2)
28-38: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
RateLimiterror is swallowed and never propagates.The
throw new Error("RateLimit")at Line 33 sits inside thetrywhosecatch {}(Line 35) is empty, so the throw is immediately caught and discarded; the function then falls through toreturn null. As a result, thecatch (e)rate-limit detection inProfilePage(Line 107) can never fire andrateLimitedstaysfalse.Detect the rate limit first, then throw outside the parsing
try.🔧 Proposed fix
if (!res.ok) { - // Detect rate limit based on GitHub response - try { - const err = await res.json(); - if (err.message && err.message.toLowerCase().includes("rate limit")) { - throw new Error("RateLimit"); - } - } catch {} - return null; + // Detect rate limit based on GitHub response + let isRateLimit = false; + try { + const err = await res.json(); + isRateLimit = !!err.message && err.message.toLowerCase().includes("rate limit"); + } catch {} + if (isRateLimit) throw new Error("RateLimit"); + 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.if (!res.ok) { // Detect rate limit based on GitHub response let isRateLimit = false; try { const err = await res.json(); isRateLimit = !!err.message && err.message.toLowerCase().includes("rate limit"); } catch {} if (isRateLimit) throw new Error("RateLimit"); return null; } return res.json();🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/`[username]/page.tsx around lines 28 - 38, The rate-limit path in the data fetch logic is swallowing the intended exception inside the JSON parsing try/catch, so the `RateLimit` signal never reaches `ProfilePage`. Update the fetch helper in `src/app/[username]/page.tsx` to inspect the failed GitHub response first, store whether it matches a rate-limit condition, then throw the `RateLimit` error after the parsing block so it escapes the empty catch; this allows the `catch (e)` handling in `ProfilePage` to set `rateLimited` correctly.
97-102: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
Avoid
anyfor the fetched values.
user,repos,liveStats,orgs, andcontributionCalendarare all typedany, which disables type checking across the rest of the page (and contributed to the undetecteduserResult/shape mismatch above). Prefer the concrete return types from the fetch helpers.🧰 Tools
🪛 GitHub Actions: CI - Build & Lint / 0_build.txt
[error] 98-98: TypeScript (tsc --noEmit) error TS2451: Cannot redeclare block-scoped variable 'user'.
🪛 GitHub Actions: CI - Build & Lint / build
[error] 98-98: TypeScript (tsc --noEmit) failed: TS2451 Cannot redeclare block-scoped variable 'user'.
🪛 GitHub Check: build
[failure] 98-98:
Cannot redeclare block-scoped variable 'user'.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/`[username]/page.tsx around lines 97 - 102, The fetched page state in the [username]/page.tsx component is using `any`, which removes type safety for the rest of the page. Update the declarations for user, repos, liveStats, orgs, and contributionCalendar to use the concrete return types from the corresponding fetch helpers, and thread those types through the logic that consumes them in the page component.src/components/profile/ProfileView.tsx (1)
697-697: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Invalid fallback hex color
#9a9aa.
#9a9aahas only 5 digits and is not a valid CSS hex color, so the fallback swatch will be ignored/transparent. The repo-language swatch on Line 550 correctly uses#9a9a9a.🎨 Proposed fix
- <span style={{ width: "10px", height: "10px", backgroundColor: LANG_COLORS[language] ?? "`#9a9aa`", borderRadius: "9999px", flexShrink: 0, display: "inline-block" }}></span>{language} + <span style={{ width: "10px", height: "10px", backgroundColor: LANG_COLORS[language] ?? "`#9a9a9a`", borderRadius: "9999px", flexShrink: 0, display: "inline-block" }}></span>{language}📝 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.<span style={{ width: "10px", height: "10px", backgroundColor: LANG_COLORS[language] ?? "`#9a9a9a`", borderRadius: "9999px", flexShrink: 0, display: "inline-block" }}></span>{language}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/profile/ProfileView.tsx` at line 697, The language swatch in ProfileView uses an invalid fallback hex color, so the inline style fallback is ignored. Update the fallback in the span rendering for the language badge to use the valid 6-digit gray color already used elsewhere in ProfileView (the repo-language swatch), keeping the LANG_COLORS lookup and the badge markup otherwise unchanged.
Signed-off-by: Adit Jain <aditjain2005@gmail.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/app/[username]/page.tsx (1)
56-63: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winRestore the actual
fetchGitHubUserreturn contract here.Lines 57-63 assume
fetchGitHubUser()returns{ status, data }, but this file still uses it asuser | nullat Line 116. That mismatch breaks metadata generation in both directions: successful lookups always take the fallback path becauseresult.statusisundefined, and failed lookups can crash onnull.statusbefore returning fallback metadata. Read the returned user object directly here, or changefetchGitHubUserto a consistent discriminated union everywhere.Suggested fix
export async function generateMetadata({ params }: ProfilePageProps): Promise<Metadata> { const { username } = await params; - const result = await fetchGitHubUser(username); - if (result.status !== "ok") { + const user = await fetchGitHubUser(username); + if (!user) { return { title: `${username} - OSSfolio`, description: `Open-source profile for ${username}.`, }; } - const user = result.data; const displayName = user.name || username; const bio = user.bio || "";🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/`[username]/page.tsx around lines 56 - 63, The metadata loader in page.tsx is using the wrong `fetchGitHubUser` contract, causing it to treat successful responses as failures and risking a null dereference on failed responses. Update the logic around `fetchGitHubUser`, `generateMetadata`, and the `user` handling so they all use one consistent return shape: either read the user object directly here and handle null safely, or convert `fetchGitHubUser` to a discriminated union and update every caller to match.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/app/`[username]/page.tsx:
- Around line 56-63: The metadata loader in page.tsx is using the wrong
`fetchGitHubUser` contract, causing it to treat successful responses as failures
and risking a null dereference on failed responses. Update the logic around
`fetchGitHubUser`, `generateMetadata`, and the `user` handling so they all use
one consistent return shape: either read the user object directly here and
handle null safely, or convert `fetchGitHubUser` to a discriminated union and
update every caller to match.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 2e72a350-0f53-4511-9935-32815482d4f0
📒 Files selected for processing (1)
src/app/[username]/page.tsx
|
🎉 Your PR just got merged, @Adit-Jain-srm — thank you for contributing to OSSfolio! Your work is now part of the project. Here's what to do next:
We really appreciate you taking the time. See you in the next PR! 🚀 |
Summary
Adds repository topic badges to the repo cards on profile pages. Topics give instant context about what a project does (e.g., "react", "machine-learning", "cli-tool").
Related Issue
Closes #150
Type of Change
Changes Made
fetchGitHubRepostoapplication/vnd.github.mercy-preview+jsonwhich includes thetopicsarray in the REST responsetopics?: string[]toGitHubRepointerface in ProfileView andGitHubRepoLikein profile-data.ts, plustopics: string[]to theRepotypemapReposnow passestopicsthrough (defaults to[])Pill style matches DESIGN.md's
pill-tag-softpattern: canvas-soft background, hairline border, rounded-full, micro (11px) font size.AI Usage
The mercy-preview Accept header is GitHub's way of opting into the topics field in REST responses. The topics come as a flat
string[]on each repo object. The pill rendering uses inline styles matching the existing pattern in the codebase.Screenshots (if UI change)
Topic pills appear as small grey rounded badges below each repo description, before the language/stars/forks row. Example:
reacttypescriptnextjs+2Checklist
mainconsole.logleft insrc/schema.sqland a new migration file are includedDESIGN.mdand followed the design system (colors, spacing, typography, components)feat:,fix:,docs:, etc.)Summary by CodeRabbit