Skip to content

feat: extended profile dialog#149

Merged
Flo0806 merged 4 commits intomainfrom
feat/extended-profile-dialog
Feb 26, 2026
Merged

feat: extended profile dialog#149
Flo0806 merged 4 commits intomainfrom
feat/extended-profile-dialog

Conversation

@Flo0806
Copy link
Contributor

@Flo0806 Flo0806 commented Feb 26, 2026

Summary

  • Extend profil dialog

Related issue(s)

Closes #146

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • CI

Checklist

  • Tests added/updated
  • i18n keys added/updated (if needed)
  • No breaking changes

Summary by CodeRabbit

  • New Features

    • Avatars and user chips are now clickable across the app to open user profiles.
    • Profile dialog gains Recent Activity, Shared Repos, and README sections.
  • UI/UX Improvements

    • Enhanced profile header: pronouns, status indicator, responsive layout and larger modal breakpoint.
    • Activity items and repo lists surfaced in the profile.
  • Localization

    • Added translations and schema entries for new profile and activity labels.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 26, 2026

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

📥 Commits

Reviewing files that changed from the base of the PR and between d8af43a and 2babc30.

📒 Files selected for processing (2)
  • app/components/user/UserProfileCard.vue
  • server/api/user/shared-repos.get.ts
 ________________________
< I came, I saw, I CI'd. >
 ------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ

✏️ Tip: You can disable in-progress messages and the fortune message in your review settings.

📝 Walkthrough

Walkthrough

This PR extends user profile interactivity by making avatars and usernames clickable across components to open profile dialogs, introduces a reusable UserChip component, significantly enhances UserProfileCard with new sections (README, Shared Repos, Recent Activity, Pronouns, Status), and adds corresponding server-side APIs for fetching profile data.

Changes

Cohort / File(s) Summary
Avatar Interactivity
app/components/focus/CreatedIssueCard.vue, app/components/focus/InboxUnifiedCard.vue, app/components/issue/IssueHeader.vue, app/components/issue/IssueRow.vue, app/components/issue/IssueSidebar.vue, app/components/repo/RepoStatistics.vue, app/components/user/UserCard.vue
Replaced static or link-based avatars with clickable buttons that invoke openProfile() from useUserProfileDialog to open profile dialogs instead of navigating away.
Reusable User Component
app/components/user/UserChip.vue
New component that renders a clickable user chip with avatar and login, accepting optional avatarUrl and size props, opening the profile dialog on click.
Profile Card & Dialog
app/components/user/UserProfileCard.vue, app/components/user/UserProfileDialog.vue
Enhanced UserProfileCard with new sections: Profile README (collapsible), Shared Repos (with language/stars), Recent Activity (with icons and labels), plus pronouns and status display in header. Updated dialog modal width/padding for responsive sizing.
Profile Composable & Types
app/composables/useUserProfileDialog.ts, shared/types/profile.ts, server/utils/profile.ts
Extended UserProfileData interface to include status field; added pronouns field to GitHubProfile and GitHubUser types; updated profile transformation to include pronouns.
Server Profile APIs
server/api/user/activity.get.ts, server/api/user/profile-readme.get.ts, server/api/user/shared-repos.get.ts, server/api/user/profile.get.ts
Added three new cached endpoints for fetching user activity (max 10 events), profile README (base64 decoded), and shared repos (top 6 by stars); updated existing profile endpoint to fetch pronouns and status via GraphQL.
Activity Utilities & Types
server/utils/activity.ts, server/utils/github.ts
Introduced GitHubEvent, ActivityType, and UserActivityEvent types; added mapEvent() function to normalize GitHub events; added getLoginQuery() helper for validated login param retrieval.
Internationalization
i18n/locales/en.json, i18n/locales/de.json, i18n/schema.json
Added i18n keys for new profile sections: busy, sharedRepos, recentActivity, and nested activity object with event type labels (push, pr, issue, create, release, starred, forked); updated schema with new properties and constraints.
Tests
test/nuxt/profileStore.test.ts, test/nuxt/userProfileDialog.test.ts, test/unit/activity.test.ts
Added pronouns/status mock data to profile tests; added comprehensive unit test suite for mapEvent() covering all event type mappings and field transformations.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Component as Avatar Component
    participant Composable as useUserProfileDialog
    participant Dialog as UserProfileDialog
    participant Card as UserProfileCard
    participant API as Server APIs
    participant GitHub as GitHub API

    User->>Component: Click avatar
    Component->>Composable: openProfile(login)
    Composable->>Dialog: open(login)
    Dialog->>Dialog: Show modal
    Dialog->>Card: Pass login prop
    Card->>API: GET /api/user/profile?login
    Card->>API: GET /api/user/profile-readme?login
    Card->>API: GET /api/user/shared-repos?login
    Card->>API: GET /api/user/activity?login
    par Parallel API Calls
        API->>GitHub: Fetch profile (REST + GraphQL)
        API->>GitHub: Fetch README
        API->>GitHub: Fetch shared repos
        API->>GitHub: Fetch activity events
    end
    API-->>Card: Return aggregated data
    Card->>Card: Render profile, README, repos, activity
    Card-->>Dialog: Display enriched profile
    Dialog-->>User: Show profile modal
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~45 minutes

Possibly related issues

  • User Profile Quick-View: reusable dialog for viewing any user #146 — Implements the core user profile quick-view dialog functionality with clickable avatars across components, profile card data display, and server-side APIs for profile enrichment (activity, README, shared repos, pronouns, status).
  • User Profile Dialog: remaining features #147 — Directly addresses objectives including pronouns/status display, README rendering, shared repos section, recent activity summary, UserChip component creation, and wiring dialog interactivity into avatar/name occurrences across components.

Possibly related PRs

Suggested reviewers

  • Gonzo17

🐰 Avatars awake! No more dormant and plain,
Click them to glimpse what awaits in the main,
Pronouns and status and repos they've starred,
Activity badges and READMEs unbarred,
A profile dialog that's quick to appear!

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Multiple out-of-scope items from issue #146 were implemented: pronouns, status display, README rendering, shared repos, and recent activity sections. Remove implementations of pronouns, status, README, shared repos, and recent activity features, or update issue #146 to include them in scope if intentional.
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description is minimal and contains a typo ('profil' instead of 'profile'). While it covers required sections, the summary lacks detail about what was actually extended. Expand the summary to specifically describe the enhancements made (e.g., added pronouns, status, README, shared repos, activity sections) and fix the typo.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: extended profile dialog' clearly describes the main feature addition and is consistent with the PR objectives.
Linked Issues check ✅ Passed All core requirements from issue #146 are met: UserProfileCard component, UserProfileDialog wrapper, profile API endpoint, and contribution graph with 3 skins implemented.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/extended-profile-dialog

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (5)
server/api/user/shared-repos.get.ts (1)

46-46: Normalize cache key casing to avoid duplicate entries.

Line 46 should normalize login casing so UserA:UserB and usera:userb don’t fragment cache entries.

Proposed fix
-    getKey: (_token: string, myLogin: string, otherLogin: string) => `${myLogin}:${otherLogin}`,
+    getKey: (_token: string, myLogin: string, otherLogin: string) =>
+      `${myLogin.toLowerCase()}:${otherLogin.toLowerCase()}`,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/user/shared-repos.get.ts` at line 46, The cache key generator
getKey currently uses raw login strings which allows casing variations to create
duplicate cache entries; update getKey to normalize both inputs (e.g., convert
myLogin and otherLogin to a consistent case and trim whitespace) before
composing the key—use safe conversion (String(...) or defensive checks) then
template the normalized values as `${normalizedMy}:${normalizedOther}` so
"UserA:UserB" and "usera:userb" map to the same cache key.
server/utils/github.ts (1)

50-66: Good centralization of login validation.

The getLoginQuery helper consolidates login parameter validation, reducing duplication across API routes. The GITHUB_LOGIN_PATTERN correctly enforces GitHub's username constraints: up to 39 characters, alphanumeric characters and single hyphens only, no leading/trailing/consecutive hyphens.

Consider differentiating the error message between missing (!login) and invalid (fails regex) cases for clearer client feedback.

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

In `@server/utils/github.ts` around lines 50 - 66, Update getLoginQuery (and
similarly getOrgQuery if desired) to throw distinct error messages for missing
vs invalid values: first trim and check for falsy (throw createError with
statusCode 400 and message like "Missing login parameter"), then validate
against GITHUB_LOGIN_PATTERN and if it fails throw createError with statusCode
400 and message "Invalid login parameter"; apply the same two-step check to
getOrgQuery (use "Missing org parameter" vs "Invalid org parameter") so clients
receive clearer feedback.
server/api/user/profile.get.ts (1)

69-71: Minor: Redundant query parameter access.

getQuery(event).login is checked, then getLoginQuery(event) reads and validates it again. This works but could be slightly streamlined.

♻️ Optional simplification
-  if (getQuery(event).login) {
-    return fetchOtherProfile(token, userId, getLoginQuery(event))
+  const loginParam = getQuery(event).login as string | undefined
+  if (loginParam) {
+    const login = getLoginQuery(event) // validates
+    return fetchOtherProfile(token, userId, login)
   }

Alternatively, you could make getLoginQuery return string | undefined when the param is absent, avoiding the double-read entirely.

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

In `@server/api/user/profile.get.ts` around lines 69 - 71, The code redundantly
reads the login query twice; call getLoginQuery(event) once, store its result in
a variable (e.g., login = getLoginQuery(event)), check that variable for
truthiness, and if present pass it to fetchOtherProfile(token, userId, login);
update getLoginQuery to return string | undefined if necessary so the single
read covers both the existence check and the validated value used by
fetchOtherProfile.
server/api/user/profile-readme.get.ts (1)

13-13: Consider handling missing or truncated content.

GitHub may omit the content field for large files (>1MB) and instead provide a download_url. If data.content is undefined, Buffer.from(undefined, 'base64') will throw.

🛡️ Optional defensive check
-      return Buffer.from(data.content, 'base64').toString('utf-8')
+      if (!data.content) return null
+      return Buffer.from(data.content, 'base64').toString('utf-8')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/user/profile-readme.get.ts` at line 13, The current return uses
Buffer.from(data.content, 'base64').toString('utf-8') and will throw if
data.content is undefined; update the handler that returns the README (the code
around Buffer.from(data.content...)) to first check for data.content and if
missing, fetch the raw text from data.download_url (or return a clear error) and
then return the UTF-8 string; ensure you reference data.content and
data.download_url and avoid calling Buffer.from with undefined.
app/components/user/UserProfileCard.vue (1)

345-345: Consider a dedicated i18n key instead of reusing issues.sidebar.showLess.

Reusing a translation key from the issues domain creates implicit coupling. If the issues translation changes context (e.g., "Show fewer issues"), this component's UI could become confusing.

💡 Suggested approach

Add a generic or profile-specific key:

-          {{ activityExpanded ? t('issues.sidebar.showLess') : `+${activity.length - 3}` }}
+          {{ activityExpanded ? t('common.showLess') : `+${activity.length - 3}` }}

Then add the key to your i18n files under a common namespace.

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

In `@app/components/user/UserProfileCard.vue` at line 345, Replace the hardcoded
reuse of the issues domain key in UserProfileCard.vue (the expression using
activityExpanded, activity.length and t('issues.sidebar.showLess')) with a
dedicated i18n key (e.g., t('common.activity.showMore') or
t('profile.activity.showMore')) and update the toggle text logic to use that key
when activityExpanded is false; then add the new key(s) to the i18n resource
files (under a common or profile namespace) with appropriate translations for
both states so the component no longer depends on issues.sidebar.showLess.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/components/user/UserProfileCard.vue`:
- Around line 72-82: The switch in activityLabel(ev: UserActivityEvent) is
missing a default/exhaustiveness branch so it can return undefined for new
activity types; add a final default case that both returns a safe fallback label
(e.g. t('user.profile.activity.unknown') or similar) and enforces compile-time
exhaustiveness by assigning ev to a never-typed variable (e.g. default: const
_exhaustive: never = ev; return t(...)), ensuring TypeScript will error if
UserActivityEvent gains new variants and preventing undefined being rendered.

In `@server/api/user/shared-repos.get.ts`:
- Around line 20-24: The two githubFetchAllWithToken calls that fetch repos with
params { per_page: 100, type: 'owner' } will never yield matching full_name
values for shared repos; update the params for both calls (the ones invoking
githubFetchAllWithToken<GitHubRepo>(token, `/users/${otherLogin}/repos`, ...)
and the counterpart for the current user) to use type: 'all' instead of 'owner'
so collaborator repositories are included and shared full_name matches are
possible.

In `@server/utils/profile.ts`:
- Line 26: The mapper currently forwards user.pronouns directly which can
produce undefined; update the mapper that sets pronouns (the line using
user.pronouns) to normalize undefined to null (e.g. use the nullish-coalescing
check on user.pronouns so the output property is either a string or null),
ensuring the mapped pronouns field never leaks undefined.

---

Nitpick comments:
In `@app/components/user/UserProfileCard.vue`:
- Line 345: Replace the hardcoded reuse of the issues domain key in
UserProfileCard.vue (the expression using activityExpanded, activity.length and
t('issues.sidebar.showLess')) with a dedicated i18n key (e.g.,
t('common.activity.showMore') or t('profile.activity.showMore')) and update the
toggle text logic to use that key when activityExpanded is false; then add the
new key(s) to the i18n resource files (under a common or profile namespace) with
appropriate translations for both states so the component no longer depends on
issues.sidebar.showLess.

In `@server/api/user/profile-readme.get.ts`:
- Line 13: The current return uses Buffer.from(data.content,
'base64').toString('utf-8') and will throw if data.content is undefined; update
the handler that returns the README (the code around
Buffer.from(data.content...)) to first check for data.content and if missing,
fetch the raw text from data.download_url (or return a clear error) and then
return the UTF-8 string; ensure you reference data.content and data.download_url
and avoid calling Buffer.from with undefined.

In `@server/api/user/profile.get.ts`:
- Around line 69-71: The code redundantly reads the login query twice; call
getLoginQuery(event) once, store its result in a variable (e.g., login =
getLoginQuery(event)), check that variable for truthiness, and if present pass
it to fetchOtherProfile(token, userId, login); update getLoginQuery to return
string | undefined if necessary so the single read covers both the existence
check and the validated value used by fetchOtherProfile.

In `@server/api/user/shared-repos.get.ts`:
- Line 46: The cache key generator getKey currently uses raw login strings which
allows casing variations to create duplicate cache entries; update getKey to
normalize both inputs (e.g., convert myLogin and otherLogin to a consistent case
and trim whitespace) before composing the key—use safe conversion (String(...)
or defensive checks) then template the normalized values as
`${normalizedMy}:${normalizedOther}` so "UserA:UserB" and "usera:userb" map to
the same cache key.

In `@server/utils/github.ts`:
- Around line 50-66: Update getLoginQuery (and similarly getOrgQuery if desired)
to throw distinct error messages for missing vs invalid values: first trim and
check for falsy (throw createError with statusCode 400 and message like "Missing
login parameter"), then validate against GITHUB_LOGIN_PATTERN and if it fails
throw createError with statusCode 400 and message "Invalid login parameter";
apply the same two-step check to getOrgQuery (use "Missing org parameter" vs
"Invalid org parameter") so clients receive clearer feedback.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bd899af and d8af43a.

📒 Files selected for processing (25)
  • app/components/focus/CreatedIssueCard.vue
  • app/components/focus/InboxUnifiedCard.vue
  • app/components/issue/IssueHeader.vue
  • app/components/issue/IssueRow.vue
  • app/components/issue/IssueSidebar.vue
  • app/components/repo/RepoStatistics.vue
  • app/components/user/UserCard.vue
  • app/components/user/UserChip.vue
  • app/components/user/UserProfileCard.vue
  • app/components/user/UserProfileDialog.vue
  • app/composables/useUserProfileDialog.ts
  • i18n/locales/de.json
  • i18n/locales/en.json
  • i18n/schema.json
  • server/api/user/activity.get.ts
  • server/api/user/profile-readme.get.ts
  • server/api/user/profile.get.ts
  • server/api/user/shared-repos.get.ts
  • server/utils/activity.ts
  • server/utils/github.ts
  • server/utils/profile.ts
  • shared/types/profile.ts
  • test/nuxt/profileStore.test.ts
  • test/nuxt/userProfileDialog.test.ts
  • test/unit/activity.test.ts

Comment on lines +72 to +82
function activityLabel(ev: UserActivityEvent) {
switch (ev.type) {
case 'push': return t('user.profile.activity.push', { branch: ev.ref ?? 'main' })
case 'pr': return t('user.profile.activity.pr', { action: ev.action ?? 'opened', number: ev.number ?? 0 })
case 'issue': return t('user.profile.activity.issue', { action: ev.action ?? 'opened', number: ev.number ?? 0 })
case 'create': return t('user.profile.activity.create', { type: ev.refType ?? 'branch', ref: ev.ref ?? '' })
case 'release': return t('user.profile.activity.release', { tag: ev.tagName ?? '' })
case 'star': return t('user.profile.activity.starred')
case 'fork': return t('user.profile.activity.forked')
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add a default case or exhaustiveness check to activityLabel.

The switch statement lacks a default case. If ActivityType is extended in the future, this function will silently return undefined, rendering as empty text in the template.

🛡️ Proposed fix with exhaustiveness check
 function activityLabel(ev: UserActivityEvent) {
   switch (ev.type) {
     case 'push': return t('user.profile.activity.push', { branch: ev.ref ?? 'main' })
     case 'pr': return t('user.profile.activity.pr', { action: ev.action ?? 'opened', number: ev.number ?? 0 })
     case 'issue': return t('user.profile.activity.issue', { action: ev.action ?? 'opened', number: ev.number ?? 0 })
     case 'create': return t('user.profile.activity.create', { type: ev.refType ?? 'branch', ref: ev.ref ?? '' })
     case 'release': return t('user.profile.activity.release', { tag: ev.tagName ?? '' })
     case 'star': return t('user.profile.activity.starred')
     case 'fork': return t('user.profile.activity.forked')
+    default: {
+      const _exhaustive: never = ev.type
+      return ''
+    }
   }
 }
📝 Committable suggestion

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

Suggested change
function activityLabel(ev: UserActivityEvent) {
switch (ev.type) {
case 'push': return t('user.profile.activity.push', { branch: ev.ref ?? 'main' })
case 'pr': return t('user.profile.activity.pr', { action: ev.action ?? 'opened', number: ev.number ?? 0 })
case 'issue': return t('user.profile.activity.issue', { action: ev.action ?? 'opened', number: ev.number ?? 0 })
case 'create': return t('user.profile.activity.create', { type: ev.refType ?? 'branch', ref: ev.ref ?? '' })
case 'release': return t('user.profile.activity.release', { tag: ev.tagName ?? '' })
case 'star': return t('user.profile.activity.starred')
case 'fork': return t('user.profile.activity.forked')
}
}
function activityLabel(ev: UserActivityEvent) {
switch (ev.type) {
case 'push': return t('user.profile.activity.push', { branch: ev.ref ?? 'main' })
case 'pr': return t('user.profile.activity.pr', { action: ev.action ?? 'opened', number: ev.number ?? 0 })
case 'issue': return t('user.profile.activity.issue', { action: ev.action ?? 'opened', number: ev.number ?? 0 })
case 'create': return t('user.profile.activity.create', { type: ev.refType ?? 'branch', ref: ev.ref ?? '' })
case 'release': return t('user.profile.activity.release', { tag: ev.tagName ?? '' })
case 'star': return t('user.profile.activity.starred')
case 'fork': return t('user.profile.activity.forked')
default: {
const _exhaustive: never = ev.type
return ''
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/user/UserProfileCard.vue` around lines 72 - 82, The switch in
activityLabel(ev: UserActivityEvent) is missing a default/exhaustiveness branch
so it can return undefined for new activity types; add a final default case that
both returns a safe fallback label (e.g. t('user.profile.activity.unknown') or
similar) and enforces compile-time exhaustiveness by assigning ev to a
never-typed variable (e.g. default: const _exhaustive: never = ev; return
t(...)), ensuring TypeScript will error if UserActivityEvent gains new variants
and preventing undefined being rendered.

login: user.login,
name: user.name,
avatarUrl: user.avatar_url,
pronouns: user.pronouns,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Normalize pronouns to null in the mapper output.

Line 26 currently forwards the raw value directly; normalizing avoids leaking undefined into a string | null contract.

Proposed fix
-    pronouns: user.pronouns,
+    pronouns: user.pronouns ?? null,
📝 Committable suggestion

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

Suggested change
pronouns: user.pronouns,
pronouns: user.pronouns ?? null,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/utils/profile.ts` at line 26, The mapper currently forwards
user.pronouns directly which can produce undefined; update the mapper that sets
pronouns (the line using user.pronouns) to normalize undefined to null (e.g. use
the nullish-coalescing check on user.pronouns so the output property is either a
string or null), ensuring the mapped pronouns field never leaks undefined.

@Flo0806 Flo0806 merged commit 81dcf00 into main Feb 26, 2026
8 of 10 checks passed
@Flo0806 Flo0806 deleted the feat/extended-profile-dialog branch February 26, 2026 22:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

User Profile Quick-View: reusable dialog for viewing any user

1 participant