Skip to content

[PET-43] Tap leaderboard entry -> view user profile#44

Merged
zigcccc merged 9 commits intomainfrom
PET-43-tap-leaderboard-entry-view-user-profile
Mar 21, 2026
Merged

[PET-43] Tap leaderboard entry -> view user profile#44
zigcccc merged 9 commits intomainfrom
PET-43-tap-leaderboard-entry-view-user-profile

Conversation

@zigcccc
Copy link
Owner

@zigcccc zigcccc commented Mar 21, 2026

Summary

Adds a user profile screen accessible by tapping any entry in a leaderboard. Users can now see a player's daily puzzle stats — win rate, streaks, attempts distribution — without leaving the leaderboard flow.

No backend changes were needed: the existing readUserPuzzleStatistics query already accepts any userId, so this is a pure frontend addition.

Changes

Leaderboard component — entries are now tappable. Added a required onEntryPress: (userId: string) => void prop and wrapped each entry in a Pressable.

Leaderboard screens (weekly + all-time) — pass onEntryPress navigating to the new user-profile screen.

New screen: user-profile.tsx — shows the tapped user's profile:

  • Header title set dynamically via Stack.Screen ("Nalagam podatke..." while loading → nickname once resolved)
  • Member since date formatted with dayjs
  • 4 stat cards: played, win rate, current streak, best streak
  • Attempts distribution graph (reuses existing AttemptsDistributionGraph component)
  • Empty state for users who haven't played yet

_layout.tsx — registers user-profile as a modal stack screen.

Testing

  • Unit tests for user-profile.tsx (10 cases): query args, loading state, dynamic title, stats rendering, empty state, win rate edge case
  • Unit tests for ModalViewBackButton (4 cases): null render, button render, press handler
  • jest.setup.tsx — added dayjs.locale('sl') globally so date assertions match the real app behaviour
  • Updated settings.test.tsx date assertion to use Slovenian month abbreviation (jul.)

Summary by CodeRabbit

  • New Features

    • Added user profile screen showing member details, daily puzzle statistics, win rate, streaks, and attempt distribution.
    • Leaderboard entries are now interactive and navigate to user profiles.
    • Added a modal "Back" button component for improved in-modal navigation.
  • Accessibility

    • Improved loading semantics: various loading indicators now expose role "progressbar" for better assistive support.
  • Tests

    • Added tests for user profile and modal back button; updated tests to reflect accessibility and date-format changes.

@zigcccc zigcccc self-assigned this Mar 21, 2026
@zigcccc zigcccc added the enhancement New feature or request label Mar 21, 2026
@coderabbitai
Copy link

coderabbitai bot commented Mar 21, 2026

Warning

Rate limit exceeded

@zigcccc has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 13 minutes and 30 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8e6d2e72-3a81-4445-82ee-d3d21ee8720e

📥 Commits

Reviewing files that changed from the base of the PR and between b9b17bb and dfd3682.

📒 Files selected for processing (1)
  • src/components/navigation/ModalViewBackButton/ModalViewBackButton.tsx

Walkthrough

Adds a User Profile screen reachable from leaderboards, interactive leaderboard entries, a modal back-button component, adjustments to accessibility roles, test additions/updates, Day.js locale initialization for tests, and configuration/documentation changes for formatter/import organization and date/testing conventions.

Changes

Cohort / File(s) Summary
User Profile Feature
src/app/(authenticated)/user-profile.tsx, src/__tests__/user-profile.test.tsx
New UserProfileScreen default export: reads userId from route params, queries user and daily puzzle stats, computes formatted member-since and win rate, renders loading/empty states, stats, and distribution graph; tests cover loading, title updates, data rendering, and empty-state behavior.
Navigation Layout / Modal Screen
src/app/(authenticated)/_layout.tsx
Added Stack.Screen named user-profile with modal presentation and custom header options (large title, tint, back button component).
Leaderboard Interaction
src/app/(authenticated)/leaderboards/all-time-leaderboard.tsx, src/app/(authenticated)/leaderboards/weekly-leaderboard.tsx, src/components/elements/Leaderboard/Leaderboard.tsx
Leaderboards now use useRouter() and pass onEntryPress to Leaderboard; Leaderboard requires onEntryPress and renders entries as Pressable that call onEntryPress(userId) to navigate to the user profile.
Modal Back Button Component
src/components/navigation/ModalViewBackButton/ModalViewBackButton.tsx, src/components/navigation/ModalViewBackButton/index.ts, src/components/navigation/ModalViewBackButton/ModalViewBackButton.test.tsx, src/components/navigation/index.ts
Added exported ModalViewBackButton component (conditional canGoBack prop, calls router.back), barrel export, index re-export, and tests verifying render/behavior.
Accessibility role updates
src/components/ui/Button/Button.tsx, src/app/(authenticated)/play/daily-puzzle-solved.tsx, src/app/(authenticated)/play/training-puzzle-solved.tsx, src/__tests__/play/*, src/components/ui/Button/Button.test.tsx
Changed ActivityIndicator accessibilityRole from spinbutton to progressbar; updated tests to assert progressbar.
Jest / Day.js locale init & tests
jest.setup.tsx, src/__tests__/settings.test.tsx
Initialized Day.js with Slovenian ('sl') locale in Jest setup; updated settings test expectation to match localized date formatting ('01. jul. 2025').
Formatter / Tooling / Docs
.zed/settings.json, biome.json, CLAUDE.md
Reordered language servers and added prettier.allowed: false in .zed/settings.json; updated Biome import organization groups; added documentation for date formatting (dayjs/sl) and project testing conventions.
Import spacing / small formatting
multiple files: convex/..., db/seeders/..., src/components/..., src/hooks/..., src/__tests__/...
Normalized blank-line spacing between import groups across many files; minor import reformatting and non-functional whitespace changes.

Sequence Diagram(s)

sequenceDiagram
  participant Leaderboard as Leaderboard UI
  participant Router as Expo Router
  participant UserProfile as UserProfileScreen
  participant Queries as useUserQuery / usePuzzlesStatisticsQuery
  participant DB as Convex/Database

  Leaderboard->>Router: onEntryPress(userId)
  Router->>UserProfile: navigate to /user-profile?userId
  UserProfile->>Queries: fetch user(id) & stats(puzzleType.daily, userId)
  Queries->>DB: query user & statistics
  DB-->>Queries: userData & statsData
  Queries-->>UserProfile: resolved data
  UserProfile->>UserProfile: compute formatted date & win-rate
  UserProfile-->>Leaderboard: (renders UI / updates title via Stack.Screen options)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[PET-43] Tap leaderboard entry -> view user profile' directly describes the main feature: making leaderboard entries tappable to navigate to a user profile screen. This aligns with the changeset's primary objective of adding this interaction.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch PET-43-tap-leaderboard-entry-view-user-profile

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

❤️ Share

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

Tip

You can generate walkthrough in a markdown collapsible section to save space.

Enable the reviews.collapse_walkthrough setting to generate walkthrough in a markdown collapsible section.

@github-actions
Copy link

github-actions bot commented Mar 21, 2026

🚀 Expo preview is ready!

  • Project → petka-app
  • Platforms → android, ios
  • Scheme → petkaapp
  • Runtime Version → 1.6
  • More info

Learn more about 𝝠 Expo Github Action

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
db/seeders/dictionaryEntries/getDictionaryWords.js (1)

86-94: ⚠️ Potential issue | 🔴 Critical

Critical: forEach with async callback doesn't wait for promises.

The forEach loop with an async callback will not wait for the isValidDictionaryTerm promises to resolve before continuing. This means writeStream.end() on line 97 will execute immediately, likely before any validation or writes complete, resulting in incomplete or missing data.

🔧 Recommended fix using for...of loop
-    words.forEach(async (item) => {
+    for (const item of words) {
       const isValid = await isValidDictionaryTerm(item.word);
       if (isValid) {
         console.log(`Writing word: ${item.word} with frequency: ${item.frequency}`);
         writeStream.write(`${JSON.stringify(item)}\n`);
       } else {
         console.warn('Found illegal word: ', item.word);
       }
-    });
+    }

Note: This is existing code, not part of the current PR changes. However, it represents a data integrity risk that should be addressed.

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

In `@db/seeders/dictionaryEntries/getDictionaryWords.js` around lines 86 - 94, The
async callback passed to words.forEach does not await isValidDictionaryTerm, so
validations and writes may run after writeStream.end(); replace the forEach with
an explicit asynchronous iteration that awaits each validation and write (e.g.,
a for...of loop over words that awaits isValidDictionaryTerm(item.word) and then
calls writeStream.write or console.warn accordingly) or collect promises via
words.map(...) and await Promise.all before calling writeStream.end(); update
the block referencing words.forEach, isValidDictionaryTerm, writeStream.write,
and ensure writeStream.end() is called only after all validations/writes
complete.
🧹 Nitpick comments (3)
db/seeders/dictionaryEntries/getDictionaryWordsExplanations.js (1)

50-50: Consider awaiting the async function call.

The processEntries() function is async but called without await. In a script context, this could cause the process to exit before completion, potentially leaving incomplete output.

⏳ Suggested fix to await the async operation
-processEntries();
+await processEntries();

Note: This is existing code, not part of the current PR changes.

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

In `@db/seeders/dictionaryEntries/getDictionaryWordsExplanations.js` at line 50,
The call to the async function processEntries() is not awaited, so the script
may exit before it completes; update the invocation to await its completion
(e.g., use top-level await if the file is an ES module or wrap the call in an
async IIFE) and ensure errors are handled (catch/rethrow or log and set non-zero
exit) so processEntries() finishes before the script exits.
src/components/elements/Leaderboard/Leaderboard.tsx (1)

16-16: Consider adding visual press feedback for better UX.

The Pressable doesn't provide visual feedback when pressed. You could use the pressed state to add opacity or highlight styling, similar to the containerPressed style in the Button component.

💡 Optional: Add press feedback
-        <Pressable key={score.position} onPress={() => onEntryPress(score.user._id)} style={styles.entry}>
+        <Pressable
+          key={score.position}
+          onPress={() => onEntryPress(score.user._id)}
+          style={({ pressed }) => [styles.entry, pressed && styles.entryPressed]}
+        >

Then add to styles:

+  entryPressed: {
+    opacity: 0.6,
+  },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/elements/Leaderboard/Leaderboard.tsx` at line 16, The
Pressable in Leaderboard (the element with key={score.position} and onPress={()
=> onEntryPress(score.user._id)}) currently uses a static style (styles.entry)
and lacks visual feedback; change its style prop to the functional form that
receives ({ pressed }) and returns an array/combination like [styles.entry,
pressed && styles.containerPressed] (or apply opacity/highlight when pressed)
and add a corresponding styles.containerPressed entry (matching the Button's
visual feedback pattern) to the stylesheet so entries visibly respond to touch.
src/components/navigation/ModalViewBackButton/ModalViewBackButton.tsx (1)

17-17: Consider setting icon color for theme consistency.

The ChevronLeftIcon doesn't have an explicit color prop. While this works, it may not match headerTintColor set in the layout, potentially causing a visual mismatch in dark mode where the icon could remain black while headerTintColor is the theme foreground.

💡 Optional: Pass theme color to the icon
+import { useUnistyles } from 'react-native-unistyles';
+
 export function ModalViewBackButton({ canGoBack }: { canGoBack?: boolean }) {
+  const { theme } = useUnistyles();
   const router = useRouter();
   // ...
   return (
     <Pressable onPress={router.back} style={styles.container}>
-      <ChevronLeftIcon size={28} strokeWidth={2} />
+      <ChevronLeftIcon color={theme.colors.foreground} size={28} strokeWidth={2} />
       <Text weight="medium">Nazaj</Text>
     </Pressable>
   );
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/navigation/ModalViewBackButton/ModalViewBackButton.tsx` at
line 17, The ChevronLeftIcon in ModalViewBackButton lacks an explicit color,
which can cause it to not follow the current theme; update ModalViewBackButton
to pass the theme foreground color (e.g., headerTintColor or colors.text from
your theme) into the ChevronLeftIcon via its color prop—retrieve the color from
the same source you use for header text (useTheme() or the headerTintColor prop)
and supply it like <ChevronLeftIcon color={themeColor} size={28} strokeWidth={2}
/> so the icon matches dark/light mode.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/`(authenticated)/user-profile.tsx:
- Line 36: The accessibilityRole on the ActivityIndicator is incorrect—replace
or remove accessibilityRole="spinbutton" on the ActivityIndicator component:
either remove the prop so the ActivityIndicator uses its default accessible role
or change it to accessibilityRole="progressbar" to reflect a loading indicator;
update the JSX where ActivityIndicator (or the loading indicator component) is
rendered to use the correct accessible role.

---

Outside diff comments:
In `@db/seeders/dictionaryEntries/getDictionaryWords.js`:
- Around line 86-94: The async callback passed to words.forEach does not await
isValidDictionaryTerm, so validations and writes may run after
writeStream.end(); replace the forEach with an explicit asynchronous iteration
that awaits each validation and write (e.g., a for...of loop over words that
awaits isValidDictionaryTerm(item.word) and then calls writeStream.write or
console.warn accordingly) or collect promises via words.map(...) and await
Promise.all before calling writeStream.end(); update the block referencing
words.forEach, isValidDictionaryTerm, writeStream.write, and ensure
writeStream.end() is called only after all validations/writes complete.

---

Nitpick comments:
In `@db/seeders/dictionaryEntries/getDictionaryWordsExplanations.js`:
- Line 50: The call to the async function processEntries() is not awaited, so
the script may exit before it completes; update the invocation to await its
completion (e.g., use top-level await if the file is an ES module or wrap the
call in an async IIFE) and ensure errors are handled (catch/rethrow or log and
set non-zero exit) so processEntries() finishes before the script exits.

In `@src/components/elements/Leaderboard/Leaderboard.tsx`:
- Line 16: The Pressable in Leaderboard (the element with key={score.position}
and onPress={() => onEntryPress(score.user._id)}) currently uses a static style
(styles.entry) and lacks visual feedback; change its style prop to the
functional form that receives ({ pressed }) and returns an array/combination
like [styles.entry, pressed && styles.containerPressed] (or apply
opacity/highlight when pressed) and add a corresponding styles.containerPressed
entry (matching the Button's visual feedback pattern) to the stylesheet so
entries visibly respond to touch.

In `@src/components/navigation/ModalViewBackButton/ModalViewBackButton.tsx`:
- Line 17: The ChevronLeftIcon in ModalViewBackButton lacks an explicit color,
which can cause it to not follow the current theme; update ModalViewBackButton
to pass the theme foreground color (e.g., headerTintColor or colors.text from
your theme) into the ChevronLeftIcon via its color prop—retrieve the color from
the same source you use for header text (useTheme() or the headerTintColor prop)
and supply it like <ChevronLeftIcon color={themeColor} size={28} strokeWidth={2}
/> so the icon matches dark/light mode.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3ecef82d-6a3a-4cda-acac-c238be253bb6

📥 Commits

Reviewing files that changed from the base of the PR and between 4151294 and a337de0.

📒 Files selected for processing (32)
  • .zed/settings.json
  • CLAUDE.md
  • biome.json
  • convex/leaderboards/queries.ts
  • convex/notifications/queries.ts
  • convex/puzzleGuessAttempts/queries.ts
  • convex/puzzles/internal.ts
  • convex/puzzles/queries.ts
  • convex/users/queries.ts
  • db/seeders/dictionaryEntries/getDictionaryWords.js
  • db/seeders/dictionaryEntries/getDictionaryWordsExplanations.js
  • jest.setup.tsx
  • src/__tests__/settings.test.tsx
  • src/__tests__/user-profile.test.tsx
  • src/app/(authenticated)/_layout.tsx
  • src/app/(authenticated)/leaderboards/all-time-leaderboard.tsx
  • src/app/(authenticated)/leaderboards/weekly-leaderboard.tsx
  • src/app/(authenticated)/user-profile.tsx
  • src/components/elements/GuessGrid/GuessGrid.hook.ts
  • src/components/elements/Leaderboard/Leaderboard.tsx
  • src/components/navigation/ModalViewBackButton/ModalViewBackButton.test.tsx
  • src/components/navigation/ModalViewBackButton/ModalViewBackButton.tsx
  • src/components/navigation/ModalViewBackButton/index.ts
  • src/components/navigation/index.ts
  • src/components/ui/Button/Button.tsx
  • src/components/ui/RadioInput/RadioInput.tsx
  • src/hooks/useDailyPuzzle/useDailyPuzzle.test.ts
  • src/hooks/useLeaderboards/useLeaderboards.test.ts
  • src/hooks/usePushNotifications/usePushNotifications.test.ts
  • src/hooks/usePuzzlesStatistics/usePuzzlesStatistics.test.ts
  • src/hooks/useTrainingPuzzle/useTrainingPuzzle.test.ts
  • src/hooks/useUser/useUser.test.ts
💤 Files with no reviewable changes (14)
  • convex/puzzles/internal.ts
  • convex/notifications/queries.ts
  • convex/leaderboards/queries.ts
  • src/components/ui/RadioInput/RadioInput.tsx
  • src/hooks/usePuzzlesStatistics/usePuzzlesStatistics.test.ts
  • convex/users/queries.ts
  • convex/puzzleGuessAttempts/queries.ts
  • src/components/elements/GuessGrid/GuessGrid.hook.ts
  • src/hooks/usePushNotifications/usePushNotifications.test.ts
  • src/hooks/useUser/useUser.test.ts
  • src/hooks/useDailyPuzzle/useDailyPuzzle.test.ts
  • convex/puzzles/queries.ts
  • src/hooks/useTrainingPuzzle/useTrainingPuzzle.test.ts
  • src/hooks/useLeaderboards/useLeaderboards.test.ts

@zigcccc zigcccc merged commit 127d4a6 into main Mar 21, 2026
4 checks passed
@zigcccc zigcccc deleted the PET-43-tap-leaderboard-entry-view-user-profile branch March 21, 2026 12:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant