From f78121c91691ea99f9c97e9eeac9d216dd08a21b Mon Sep 17 00:00:00 2001 From: Ziga Krasovec Date: Sat, 21 Mar 2026 13:10:44 +0100 Subject: [PATCH 1/9] chore: Added more robust imports sorting config --- .zed/settings.json | 32 +++++++++++++++++++++++++------- biome.json | 22 +++++++++++++++++++++- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/.zed/settings.json b/.zed/settings.json index 9da6164..b08b74a 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -1,39 +1,57 @@ { - "language_servers": ["!eslint", "..."], + "language_servers": [ + "...", + "!eslint" + ], "code_actions_on_format": { "source.action.useSortedAttributes.biome": true, "source.action.useSortedKeys.biome": true, "source.fixAll.biome": true, }, + "prettier": { + "allowed": false, + }, "languages": { "JavaScript": { "formatter": { - "language_server": { "name": "biome" }, + "language_server": { + "name": "biome" + }, }, }, "TypeScript": { "formatter": { - "language_server": { "name": "biome" }, + "language_server": { + "name": "biome" + }, }, }, "TSX": { "formatter": { - "language_server": { "name": "biome" }, + "language_server": { + "name": "biome" + }, }, }, "JSON": { "formatter": { - "language_server": { "name": "biome" }, + "language_server": { + "name": "biome" + }, }, }, "CSS": { "formatter": { - "language_server": { "name": "biome" }, + "language_server": { + "name": "biome" + }, }, }, "YAML": { "formatter": { - "language_server": { "name": "biome" }, + "language_server": { + "name": "biome" + }, }, }, }, diff --git a/biome.json b/biome.json index 5740a11..c377cb8 100644 --- a/biome.json +++ b/biome.json @@ -26,6 +26,26 @@ }, "assist": { "enabled": true, - "actions": { "source": { "organizeImports": "on", "useSortedAttributes": "on" } } + "actions": { + "source": { + "organizeImports": { + "level": "on", + "options": { + "groups": [ + [":NODE:"], + ":BLANK_LINE:", + [":PACKAGE:"], + ":BLANK_LINE:", + ["@e2e/**"], + ":BLANK_LINE:", + ["@/**"], + ":BLANK_LINE:", + [":PATH:"] + ] + } + }, + "useSortedAttributes": "on" + } + } } } From 5b287702c54bd808cacc1440598609a65562e931 Mon Sep 17 00:00:00 2001 From: Ziga Krasovec Date: Sat, 21 Mar 2026 13:12:15 +0100 Subject: [PATCH 2/9] feat: Created ModalViewBackButton component --- .../ModalViewBackButton.test.tsx | 48 +++++++++++++++++++ .../ModalViewBackButton.tsx | 31 ++++++++++++ .../navigation/ModalViewBackButton/index.ts | 1 + src/components/navigation/index.ts | 1 + 4 files changed, 81 insertions(+) create mode 100644 src/components/navigation/ModalViewBackButton/ModalViewBackButton.test.tsx create mode 100644 src/components/navigation/ModalViewBackButton/ModalViewBackButton.tsx create mode 100644 src/components/navigation/ModalViewBackButton/index.ts diff --git a/src/components/navigation/ModalViewBackButton/ModalViewBackButton.test.tsx b/src/components/navigation/ModalViewBackButton/ModalViewBackButton.test.tsx new file mode 100644 index 0000000..3fc357c --- /dev/null +++ b/src/components/navigation/ModalViewBackButton/ModalViewBackButton.test.tsx @@ -0,0 +1,48 @@ +import { fireEvent, render, screen } from '@testing-library/react-native'; +import { useRouter } from 'expo-router'; + +import { ModalViewBackButton } from './ModalViewBackButton'; + +jest.mock('expo-router', () => ({ + ...jest.requireActual('expo-router'), + useRouter: jest.fn(), +})); + +describe('', () => { + const useRouterSpy = useRouter as jest.Mock; + const mockBack = jest.fn(); + + beforeEach(() => { + useRouterSpy.mockReturnValue({ back: mockBack }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render nothing when canGoBack is false', () => { + render(); + + expect(screen.queryByRole('button', { name: /Nazaj/ })).not.toBeOnTheScreen(); + }); + + it('should render nothing when canGoBack is not provided', () => { + render(); + + expect(screen.queryByRole('button', { name: /Nazaj/ })).not.toBeOnTheScreen(); + }); + + it('should render the back button when canGoBack is true', () => { + render(); + + expect(screen.queryByText('Nazaj')).toBeOnTheScreen(); + }); + + it('should call router.back when pressed', () => { + render(); + + fireEvent.press(screen.getByText('Nazaj')); + + expect(mockBack).toHaveBeenCalled(); + }); +}); diff --git a/src/components/navigation/ModalViewBackButton/ModalViewBackButton.tsx b/src/components/navigation/ModalViewBackButton/ModalViewBackButton.tsx new file mode 100644 index 0000000..c9a253f --- /dev/null +++ b/src/components/navigation/ModalViewBackButton/ModalViewBackButton.tsx @@ -0,0 +1,31 @@ +import { useRouter } from 'expo-router'; +import { ChevronLeftIcon } from 'lucide-react-native'; +import { Pressable } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; + +import { Text } from '@/components/ui'; + +export function ModalViewBackButton({ canGoBack }: { canGoBack?: boolean }) { + const router = useRouter(); + + if (!canGoBack) { + return null; + } + + return ( + + + Nazaj + + ); +} + +const styles = StyleSheet.create((theme) => ({ + container: { + flexDirection: 'row', + gap: 0, + paddingLeft: theme.spacing[2], + paddingRight: theme.spacing[4], + alignItems: 'center', + }, +})); diff --git a/src/components/navigation/ModalViewBackButton/index.ts b/src/components/navigation/ModalViewBackButton/index.ts new file mode 100644 index 0000000..55e7cae --- /dev/null +++ b/src/components/navigation/ModalViewBackButton/index.ts @@ -0,0 +1 @@ +export * from './ModalViewBackButton'; diff --git a/src/components/navigation/index.ts b/src/components/navigation/index.ts index 20c56fd..e2818d3 100644 --- a/src/components/navigation/index.ts +++ b/src/components/navigation/index.ts @@ -1 +1,2 @@ export * from './GenericStackScreen'; +export * from './ModalViewBackButton'; From b92847e762de9f677f79459455186b569597841a Mon Sep 17 00:00:00 2001 From: Ziga Krasovec Date: Sat, 21 Mar 2026 13:12:46 +0100 Subject: [PATCH 3/9] feat: Created UserProfile screen --- src/app/(authenticated)/user-profile.tsx | 132 +++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 src/app/(authenticated)/user-profile.tsx diff --git a/src/app/(authenticated)/user-profile.tsx b/src/app/(authenticated)/user-profile.tsx new file mode 100644 index 0000000..8fa1154 --- /dev/null +++ b/src/app/(authenticated)/user-profile.tsx @@ -0,0 +1,132 @@ +import dayjs from 'dayjs'; +import { Stack, useLocalSearchParams } from 'expo-router'; +import { ActivityIndicator, ScrollView, View } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; + +import { AttemptsDistributionGraph } from '@/components/elements'; +import { Card, Text } from '@/components/ui'; +import { puzzleType } from '@/convex/puzzles/models'; +import { usePuzzlesStatisticsQuery, useUserQuery } from '@/hooks/queries'; + +export default function UserProfileScreen() { + const { userId } = useLocalSearchParams<{ userId: string }>(); + + const { isLoading: isLoadingUser, data: user } = useUserQuery({ id: userId }); + const { + isLoading: isLoadingStats, + isNotFound: hasNoStats, + data: stats, + } = usePuzzlesStatisticsQuery({ + puzzleType: puzzleType.enum.daily, + userId, + }); + + const isLoading = isLoadingUser || isLoadingStats; + const title = isLoadingUser ? 'Nalagam podatke...' : (user?.nickname ?? 'Profil'); + const memberSince = user ? dayjs(user._creationTime).format('MMMM YYYY') : ''; + const winRate = stats && stats.totalPlayed > 0 ? Math.ceil((stats.totalWon / stats.totalPlayed) * 100) : 100; + + return ( + <> + + {isLoading ? ( + + + + ) : ( + + + + Član od: {memberSince} + + + {hasNoStats ? ( + + Ta igralec še ni rešil nobene uganke. + + ) : ( + + + Statistika dnevnih izzivov + + + + + Odigranih + + + {stats?.totalPlayed} + + + + + % rešenih + + + {winRate}% + + + + + Trenutni niz + + + {stats?.currentStreak} + + + + + Najdaljši niz + + + {stats?.maxStreak} + + + + + + + + )} + + )} + + ); +} + +const styles = StyleSheet.create((theme, rt) => ({ + loadingContainer: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, + container: { + flexGrow: 1, + paddingTop: 0, + paddingHorizontal: theme.spacing[6], + paddingBottom: rt.insets.bottom + theme.spacing[6], + gap: theme.spacing[6], + }, + header: { + gap: theme.spacing[1], + }, + statsSection: { + gap: theme.spacing[6], + }, + statsRow: { + flexDirection: 'row', + gap: theme.spacing[4], + }, + statEntry: { + flex: 1, + alignItems: 'center', + padding: theme.spacing[4], + backgroundColor: rt.themeName === 'dark' ? theme.colors.grey[20] : theme.colors.grey[5], + borderRadius: 4, + }, +})); From 9d92f219fcd67488b1c06dd83bd036f529c17fd5 Mon Sep 17 00:00:00 2001 From: Ziga Krasovec Date: Sat, 21 Mar 2026 13:13:15 +0100 Subject: [PATCH 4/9] feat: Updated all time and weekly leaderboards screen to link user entry to user profile screen --- src/app/(authenticated)/_layout.tsx | 11 +++++++++++ .../leaderboards/all-time-leaderboard.tsx | 10 ++++++++-- .../leaderboards/weekly-leaderboard.tsx | 10 ++++++++-- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/app/(authenticated)/_layout.tsx b/src/app/(authenticated)/_layout.tsx index e189b63..a45ce0f 100644 --- a/src/app/(authenticated)/_layout.tsx +++ b/src/app/(authenticated)/_layout.tsx @@ -3,6 +3,7 @@ import { useCallback } from 'react'; import { Platform } from 'react-native'; import { StyleSheet, useUnistyles } from 'react-native-unistyles'; +import { ModalViewBackButton } from '@/components/navigation'; import { useUser } from '@/hooks/useUser'; import { getOsMajorVersion } from '@/utils/platform'; @@ -44,6 +45,16 @@ export default function AuthenticatedLayout() { }} /> + onPresentLeaderboardActions(leaderboard)} title={leaderboard.name ?? leaderboard._id} > - + + router.navigate({ pathname: '/(authenticated)/user-profile', params: { userId } }) + } + scores={leaderboard.scores} + /> ))} diff --git a/src/app/(authenticated)/leaderboards/weekly-leaderboard.tsx b/src/app/(authenticated)/leaderboards/weekly-leaderboard.tsx index a7ff3f3..c91949b 100644 --- a/src/app/(authenticated)/leaderboards/weekly-leaderboard.tsx +++ b/src/app/(authenticated)/leaderboards/weekly-leaderboard.tsx @@ -1,4 +1,4 @@ -import { useFocusEffect, useNavigation } from 'expo-router'; +import { useFocusEffect, useNavigation, useRouter } from 'expo-router'; import { useCallback } from 'react'; import { ScrollView, View } from 'react-native'; import { StyleSheet } from 'react-native-unistyles'; @@ -11,6 +11,7 @@ import { useLeaderboards } from '@/hooks/useLeaderboards'; export default function WeeklyLeaderboardScreen() { const navigation = useNavigation(); + const router = useRouter(); const { leaderboards: privateLeaderboards, isLoading, @@ -37,7 +38,12 @@ export default function WeeklyLeaderboardScreen() { onShowActions={() => onPresentLeaderboardActions(leaderboard)} title={leaderboard.name ?? leaderboard._id} > - + + router.navigate({ pathname: '/(authenticated)/user-profile', params: { userId } }) + } + scores={leaderboard.scores} + /> ))} From 103d7dabc529e803b038f16d8aeb2e49d66b86fd Mon Sep 17 00:00:00 2001 From: Ziga Krasovec Date: Sat, 21 Mar 2026 13:13:34 +0100 Subject: [PATCH 5/9] tests: Updated testing setup to set correct dayjs locale --- jest.setup.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/jest.setup.tsx b/jest.setup.tsx index 77521d2..5e6bb8d 100644 --- a/jest.setup.tsx +++ b/jest.setup.tsx @@ -1,3 +1,8 @@ +import dayjs from 'dayjs'; +import 'dayjs/locale/sl'; + +dayjs.locale('sl'); + jest.mock('@react-native-async-storage/async-storage', () => require('@react-native-async-storage/async-storage/jest/async-storage-mock') ); From a9af0ab87f23190a4aa86454628ba4dcb45cb22f Mon Sep 17 00:00:00 2001 From: Ziga Krasovec Date: Sat, 21 Mar 2026 13:13:52 +0100 Subject: [PATCH 6/9] tests: Added unit tests for the newly created ussr profile screen --- src/__tests__/settings.test.tsx | 2 +- src/__tests__/user-profile.test.tsx | 126 ++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 src/__tests__/user-profile.test.tsx diff --git a/src/__tests__/settings.test.tsx b/src/__tests__/settings.test.tsx index 25ad9ee..e76c96c 100644 --- a/src/__tests__/settings.test.tsx +++ b/src/__tests__/settings.test.tsx @@ -118,7 +118,7 @@ describe('Settings screen', () => { expect(screen.queryByRole('button', { name: 'Spremeni' })).toBeOnTheScreen(); expect(screen.queryByText('Profil ustvarjen')).toBeOnTheScreen(); - expect(screen.queryByText('01. Jul 2025')).toBeOnTheScreen(); + expect(screen.queryByText('01. jul. 2025')).toBeOnTheScreen(); expect(screen.queryByText('ID profila')).toBeOnTheScreen(); expect(screen.queryByText(testUser1._id)).toBeOnTheScreen(); diff --git a/src/__tests__/user-profile.test.tsx b/src/__tests__/user-profile.test.tsx new file mode 100644 index 0000000..baccb50 --- /dev/null +++ b/src/__tests__/user-profile.test.tsx @@ -0,0 +1,126 @@ +import { render, screen } from '@testing-library/react-native'; +import { Stack, useLocalSearchParams } from 'expo-router'; + +import UserProfileScreen from '@/app/(authenticated)/user-profile'; +import { puzzleType } from '@/convex/puzzles/models'; +import { usePuzzlesStatisticsQuery, useUserQuery } from '@/hooks/queries'; +import { testPuzzleStatistics1 } from '@/tests/fixtures/puzzleStatistics'; +import { testUser1 } from '@/tests/fixtures/users'; + +jest.mock('expo-router', () => ({ + ...jest.requireActual('expo-router'), + Stack: { + Screen: jest.fn().mockReturnValue(null), + }, + useLocalSearchParams: jest.fn(), +})); + +jest.mock('@/hooks/queries', () => ({ + ...jest.requireActual('@/hooks/queries'), + usePuzzlesStatisticsQuery: jest.fn(), + useUserQuery: jest.fn(), +})); + +describe('', () => { + const StackScreenSpy = Stack.Screen as unknown as jest.Mock; + const useLocalSearchParamsSpy = useLocalSearchParams as jest.Mock; + const usePuzzlesStatisticsQuerySpy = usePuzzlesStatisticsQuery as jest.Mock; + const useUserQuerySpy = useUserQuery as jest.Mock; + + beforeEach(() => { + useLocalSearchParamsSpy.mockReturnValue({ userId: testUser1._id }); + useUserQuerySpy.mockReturnValue({ isLoading: false, data: testUser1 }); + usePuzzlesStatisticsQuerySpy.mockReturnValue({ isLoading: false, isNotFound: false, data: testPuzzleStatistics1 }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should call useUserQuery with the userId from route params', () => { + render(); + + expect(useUserQuerySpy).toHaveBeenCalledWith({ id: testUser1._id }); + }); + + it('should call usePuzzlesStatisticsQuery with "daily" puzzle type and the userId from route params', () => { + render(); + + expect(usePuzzlesStatisticsQuerySpy).toHaveBeenCalledWith({ + puzzleType: puzzleType.enum.daily, + userId: testUser1._id, + }); + }); + + it('should set the screen title to "Nalagam podatke..." while user data is loading', () => { + useUserQuerySpy.mockReturnValue({ isLoading: true, data: undefined }); + render(); + + expect(StackScreenSpy).toHaveBeenCalledWith( + expect.objectContaining({ options: expect.objectContaining({ title: 'Nalagam podatke...' }) }), + undefined + ); + }); + + it('should set the screen title to the user nickname once loaded', () => { + render(); + + expect(StackScreenSpy).toHaveBeenCalledWith( + expect.objectContaining({ options: expect.objectContaining({ title: testUser1.nickname }) }), + undefined + ); + }); + + it('should render loading indicator while user data is loading', () => { + useUserQuerySpy.mockReturnValue({ isLoading: true, data: undefined }); + render(); + + expect(screen.queryByRole('spinbutton', { name: 'Nalagam podatke o uporabniku...' })).toBeOnTheScreen(); + expect(screen.queryByText('Statistika dnevnih izzivov')).not.toBeOnTheScreen(); + }); + + it('should render loading indicator while stats are loading', () => { + usePuzzlesStatisticsQuerySpy.mockReturnValue({ isLoading: true, data: undefined }); + render(); + + expect(screen.queryByRole('spinbutton', { name: 'Nalagam podatke o uporabniku...' })).toBeOnTheScreen(); + expect(screen.queryByText('Statistika dnevnih izzivov')).not.toBeOnTheScreen(); + }); + + it('should render the member since date', () => { + render(); + + // testUser1._creationTime = 1751328000000 (2025-07-01); dayjs sl locale → "julij 2025" + expect(screen.queryByText(/Član od: julij 2025/)).toBeOnTheScreen(); + }); + + it('should render daily stats when data is loaded', () => { + render(); + + expect(screen.queryByRole('spinbutton', { name: 'Nalagam podatke o uporabniku...' })).not.toBeOnTheScreen(); + expect(screen.queryByText('Statistika dnevnih izzivov')).toBeOnTheScreen(); + expect(screen.getByTestId('stat-total-played')).toHaveTextContent(/Odigranih5/); + expect(screen.getByTestId('stat-win-rate')).toHaveTextContent(/% rešenih80%/); + expect(screen.getByTestId('stat-current-streak')).toHaveTextContent(/Trenutni niz2/); + expect(screen.getByTestId('stat-max-streak')).toHaveTextContent(/Najdaljši niz3/); + }); + + it('should render 100% win rate when totalPlayed is 0', () => { + usePuzzlesStatisticsQuerySpy.mockReturnValue({ + isLoading: false, + isNotFound: false, + data: { ...testPuzzleStatistics1, totalPlayed: 0, totalWon: 0 }, + }); + render(); + + expect(screen.queryByText('100%')).toBeOnTheScreen(); + }); + + it('should render empty state when user has no stats', () => { + usePuzzlesStatisticsQuerySpy.mockReturnValue({ isLoading: false, isNotFound: true, data: null }); + render(); + + expect(screen.queryByText('Ta igralec še ni rešil nobene uganke.')).toBeOnTheScreen(); + expect(screen.queryByText('Statistika dnevnih izzivov')).not.toBeOnTheScreen(); + }); +}); From a337de05bd89cb7b68af27c3db84170802da938f Mon Sep 17 00:00:00 2001 From: Ziga Krasovec Date: Sat, 21 Mar 2026 13:14:07 +0100 Subject: [PATCH 7/9] lint: Applied linting corrections --- CLAUDE.md | 36 +++++++++++++++++++ convex/leaderboards/queries.ts | 1 - convex/notifications/queries.ts | 1 - convex/puzzleGuessAttempts/queries.ts | 1 - convex/puzzles/internal.ts | 1 - convex/puzzles/queries.ts | 1 - convex/users/queries.ts | 1 - .../dictionaryEntries/getDictionaryWords.js | 1 + .../getDictionaryWordsExplanations.js | 1 + .../elements/GuessGrid/GuessGrid.hook.ts | 1 - .../elements/Leaderboard/Leaderboard.tsx | 9 ++--- src/components/ui/Button/Button.tsx | 1 + src/components/ui/RadioInput/RadioInput.tsx | 1 - .../useDailyPuzzle/useDailyPuzzle.test.ts | 1 - .../useLeaderboards/useLeaderboards.test.ts | 1 - .../usePushNotifications.test.ts | 1 - .../usePuzzlesStatistics.test.ts | 1 - .../useTrainingPuzzle.test.ts | 1 - src/hooks/useUser/useUser.test.ts | 1 - 19 files changed, 44 insertions(+), 18 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6af560f..5b29c4e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -84,3 +84,39 @@ Validators use `zod` (v4) via `convex-helpers/server/zod4` (`zodToConvex`, `zid` **Daily puzzle timing:** `generateUseQueryHookWithTimestampArg` passes a stable UTC midnight timestamp so the daily puzzle query doesn't re-fire during a session when the date rolls over. Use anytime a query requires a timestamp. **Icons:** use `lucide-react-native` for all icons. Always import using the `Icon`-suffixed export (e.g. `InfoIcon`, `SettingsIcon`, `Share2Icon`) — both `Info` and `InfoIcon` resolve to the same component, but the suffixed form makes it clear the import is an icon. + +**Date formatting:** always use `dayjs` for formatting dates. The `sl` locale is configured globally in `src/app/_layout.tsx`, so Slovenian month names work out of the box (e.g. `dayjs(timestamp).format('MMMM YYYY')`). Never use `toLocaleDateString`. + +## Testing + +**Screen tests** live in `src/__tests__/` mirroring the app's file structure (e.g. `src/__tests__/leaderboards/weekly-leaderboard.test.tsx`). **Component, hook, and utility tests** are co-located with the file they test (e.g. `src/components/ui/Button/Button.test.tsx`). Use `@testing-library/react-native` (`render`, `screen`, `fireEvent`). + +**Mocking hooks:** mock the module with `jest.requireActual` spread, then override only the hooks used by the screen: +```ts +jest.mock('@/hooks/queries', () => ({ + ...jest.requireActual('@/hooks/queries'), + useUserQuery: jest.fn(), +})); +// Then cast to spy: const useUserQuerySpy = useUserQuery as jest.Mock; +``` + +**Mocking expo-router:** same spread pattern. To capture `Stack.Screen` options: +```ts +jest.mock('expo-router', () => ({ + ...jest.requireActual('expo-router'), + Stack: { Screen: jest.fn().mockReturnValue(null) }, + useLocalSearchParams: jest.fn(), +})); +// Stack.Screen is called with (props, undefined) — not (props, {}) +``` + +**Fixtures:** reuse shared test data from `src/tests/fixtures/` (`testUser1`, `testPuzzleStatistics1`, etc.). Add new fixtures there when a new model is introduced. + +**Disambiguating text:** numbers often appear in multiple places (e.g. stat values vs. distribution graph labels). Add `testID` to the container and assert with `toHaveTextContent`: +```ts +expect(screen.getByTestId('stat-total-played')).toHaveTextContent(/Odigranih5/); +``` + +**Locale:** `jest.setup.tsx` sets `dayjs.locale('sl')` globally — date assertions must use Slovenian month names (e.g. `"julij 2025"`, `"01. jul. 2025"`). + +**beforeEach/afterEach:** set up spy return values in `beforeEach`, call `jest.clearAllMocks()` in `afterEach`. diff --git a/convex/leaderboards/queries.ts b/convex/leaderboards/queries.ts index 15fe6ea..93992ae 100644 --- a/convex/leaderboards/queries.ts +++ b/convex/leaderboards/queries.ts @@ -8,7 +8,6 @@ import type { DataModel, Id } from '../_generated/dataModel'; import { generateRandomString, weekBounds, windowAround } from '../shared/helpers'; import { internalMutation, mutation, query } from '../shared/queries'; import type { User } from '../users/models'; - import { createLeaderboardModel, type LeaderboardWithScores, diff --git a/convex/notifications/queries.ts b/convex/notifications/queries.ts index 3622d9c..f2a6b4a 100644 --- a/convex/notifications/queries.ts +++ b/convex/notifications/queries.ts @@ -3,7 +3,6 @@ import { zid } from 'convex-helpers/server/zod4'; import { z } from 'zod'; import { mutation, query } from '../shared/queries'; - import { pushNotifications } from './services'; export const readUserNotificationsStatus = query({ diff --git a/convex/puzzleGuessAttempts/queries.ts b/convex/puzzleGuessAttempts/queries.ts index 8d8363e..a7ccdf8 100644 --- a/convex/puzzleGuessAttempts/queries.ts +++ b/convex/puzzleGuessAttempts/queries.ts @@ -5,7 +5,6 @@ import { z } from 'zod'; import { checkWordleAttempt } from '@/utils/words'; import { mutation, query } from '../shared/queries'; - import { isAttemptCorrect } from './helpers'; import { createPuzzleGuessAttemptModel } from './models'; diff --git a/convex/puzzles/internal.ts b/convex/puzzles/internal.ts index e497c33..73651a7 100644 --- a/convex/puzzles/internal.ts +++ b/convex/puzzles/internal.ts @@ -4,7 +4,6 @@ import { pickRandomWord } from '@/utils/words'; import { internalMutation } from '../_generated/server'; import { pushNotifications } from '../notifications/services'; - import { puzzleType } from './models'; export const createDailyPuzzle = internalMutation({ diff --git a/convex/puzzles/queries.ts b/convex/puzzles/queries.ts index b5338ee..619f2ce 100644 --- a/convex/puzzles/queries.ts +++ b/convex/puzzles/queries.ts @@ -8,7 +8,6 @@ import { isAttemptCorrect } from '../puzzleGuessAttempts/helpers'; import { checkedLetterStatus } from '../puzzleGuessAttempts/models'; import { paginationOptsValidator } from '../shared/models'; import { mutation, query } from '../shared/queries'; - import { puzzleType } from './models'; export const read = query({ diff --git a/convex/users/queries.ts b/convex/users/queries.ts index 0d443d2..fe9f261 100644 --- a/convex/users/queries.ts +++ b/convex/users/queries.ts @@ -6,7 +6,6 @@ import { internal } from '../_generated/api'; import { leaderboardType } from '../leaderboards/models'; import { puzzleType } from '../puzzles/models'; import { internalMutation, mutation, query } from '../shared/queries'; - import { createUserModel, patchUserModel } from './models'; export const read = query({ diff --git a/db/seeders/dictionaryEntries/getDictionaryWords.js b/db/seeders/dictionaryEntries/getDictionaryWords.js index e1859dd..768c5af 100644 --- a/db/seeders/dictionaryEntries/getDictionaryWords.js +++ b/db/seeders/dictionaryEntries/getDictionaryWords.js @@ -2,6 +2,7 @@ import { createWriteStream } from 'node:fs'; import fs, { readFile } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; + import * as cheerio from 'cheerio'; import { XMLParser } from 'fast-xml-parser'; diff --git a/db/seeders/dictionaryEntries/getDictionaryWordsExplanations.js b/db/seeders/dictionaryEntries/getDictionaryWordsExplanations.js index 7ffed7e..ec99144 100644 --- a/db/seeders/dictionaryEntries/getDictionaryWordsExplanations.js +++ b/db/seeders/dictionaryEntries/getDictionaryWordsExplanations.js @@ -2,6 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import readline from 'node:readline'; import { fileURLToPath } from 'node:url'; + import * as cheerio from 'cheerio'; import { fetchDictionaryTermHtml } from './utils.js'; diff --git a/src/components/elements/GuessGrid/GuessGrid.hook.ts b/src/components/elements/GuessGrid/GuessGrid.hook.ts index 9f03868..2ae03e1 100644 --- a/src/components/elements/GuessGrid/GuessGrid.hook.ts +++ b/src/components/elements/GuessGrid/GuessGrid.hook.ts @@ -8,7 +8,6 @@ import { useToaster } from '@/hooks/useToaster'; import { deepClone } from '@/utils/clone'; import type { KeyboardKey } from '../Keyboard'; - import { findCurrentGridRowIdx, getUpdatedGrid } from './GuessGrid.helpers'; const initialGuesses = new Array(6).fill(new Array(5).fill(null)); diff --git a/src/components/elements/Leaderboard/Leaderboard.tsx b/src/components/elements/Leaderboard/Leaderboard.tsx index 65b45a7..c8d0e90 100644 --- a/src/components/elements/Leaderboard/Leaderboard.tsx +++ b/src/components/elements/Leaderboard/Leaderboard.tsx @@ -1,18 +1,19 @@ -import { View } from 'react-native'; +import { Pressable, View } from 'react-native'; import { StyleSheet } from 'react-native-unistyles'; import { Text } from '@/components/ui'; import type { LeaderboardScoreWithUser } from '@/convex/leaderboards/models'; type Props = { + onEntryPress: (userId: string) => void; scores?: LeaderboardScoreWithUser[]; }; -export function Leaderboard({ scores = [] }: Readonly) { +export function Leaderboard({ onEntryPress, scores = [] }: Readonly) { return ( {scores.map((score) => ( - + onEntryPress(score.user._id)} style={styles.entry}> ) { {score.score} - + ))} ); diff --git a/src/components/ui/Button/Button.tsx b/src/components/ui/Button/Button.tsx index 09c491b..3872461 100644 --- a/src/components/ui/Button/Button.tsx +++ b/src/components/ui/Button/Button.tsx @@ -3,6 +3,7 @@ import { ActivityIndicator, Pressable, type StyleProp, Text, type ViewStyle } fr import { StyleSheet, type UnistylesVariants } from 'react-native-unistyles'; import { defaultTheme } from '@/styles/themes'; + import type { IconProps } from '../Icon'; type Props = PropsWithChildren<{ diff --git a/src/components/ui/RadioInput/RadioInput.tsx b/src/components/ui/RadioInput/RadioInput.tsx index 78e786b..8800735 100644 --- a/src/components/ui/RadioInput/RadioInput.tsx +++ b/src/components/ui/RadioInput/RadioInput.tsx @@ -5,7 +5,6 @@ import { StyleSheet } from 'react-native-unistyles'; import { Icon } from '../Icon'; import { Text } from '../Text'; - import { RadioInputContext, useRadioInputContext } from './RadioInput.context'; import type { RadioInputItemProps, RadioInputProps } from './RadioInput.types'; diff --git a/src/hooks/useDailyPuzzle/useDailyPuzzle.test.ts b/src/hooks/useDailyPuzzle/useDailyPuzzle.test.ts index f32a093..69bfafe 100644 --- a/src/hooks/useDailyPuzzle/useDailyPuzzle.test.ts +++ b/src/hooks/useDailyPuzzle/useDailyPuzzle.test.ts @@ -13,7 +13,6 @@ import { useCreatePuzzleGuessAttemptMutation, useMarkPuzzleAsSolvedMutation } fr import { useActiveDailyPuzzleQuery, usePuzzleAttemptsQuery } from '../queries'; import { useToaster } from '../useToaster'; import { useUser } from '../useUser'; - import { useDailyPuzzle } from './useDailyPuzzle'; jest.mock('posthog-react-native', () => ({ diff --git a/src/hooks/useLeaderboards/useLeaderboards.test.ts b/src/hooks/useLeaderboards/useLeaderboards.test.ts index d085157..648cad1 100644 --- a/src/hooks/useLeaderboards/useLeaderboards.test.ts +++ b/src/hooks/useLeaderboards/useLeaderboards.test.ts @@ -24,7 +24,6 @@ import { import { useLeaderboardsQuery } from '../queries'; import { useToaster } from '../useToaster'; import { useUser } from '../useUser'; - import { useLeaderboards } from './useLeaderboards'; jest.mock('posthog-react-native', () => ({ diff --git a/src/hooks/usePushNotifications/usePushNotifications.test.ts b/src/hooks/usePushNotifications/usePushNotifications.test.ts index 4401338..8ff423b 100644 --- a/src/hooks/usePushNotifications/usePushNotifications.test.ts +++ b/src/hooks/usePushNotifications/usePushNotifications.test.ts @@ -7,7 +7,6 @@ import { registerForPushNotificationsAsync } from '@/utils/notifications'; import { useRegisterUserForPushNotificationsMutation, useToggleUserPushNotificationsMutation } from '../mutations'; import { useUserNotificationsStatusQuery } from '../queries'; import { useToaster } from '../useToaster'; - import { usePushNotifications } from './usePushNotifications'; jest.mock('expo-router', () => ({ diff --git a/src/hooks/usePuzzlesStatistics/usePuzzlesStatistics.test.ts b/src/hooks/usePuzzlesStatistics/usePuzzlesStatistics.test.ts index 8b9651c..141bb73 100644 --- a/src/hooks/usePuzzlesStatistics/usePuzzlesStatistics.test.ts +++ b/src/hooks/usePuzzlesStatistics/usePuzzlesStatistics.test.ts @@ -6,7 +6,6 @@ import { testUser1 } from '@/tests/fixtures/users'; import { usePuzzlesStatisticsQuery } from '../queries'; import { useUser } from '../useUser'; - import { usePuzzleStatistics } from './usePuzzlesStatistics'; jest.mock('../queries', () => ({ diff --git a/src/hooks/useTrainingPuzzle/useTrainingPuzzle.test.ts b/src/hooks/useTrainingPuzzle/useTrainingPuzzle.test.ts index c4d8638..59d710e 100644 --- a/src/hooks/useTrainingPuzzle/useTrainingPuzzle.test.ts +++ b/src/hooks/useTrainingPuzzle/useTrainingPuzzle.test.ts @@ -17,7 +17,6 @@ import { import { useActiveTrainingPuzzleQuery, usePuzzleAttemptsQuery } from '../queries'; import { useToaster } from '../useToaster'; import { useUser } from '../useUser'; - import { useTrainingPuzzle } from './useTrainingPuzzle'; jest.mock('posthog-react-native', () => ({ diff --git a/src/hooks/useUser/useUser.test.ts b/src/hooks/useUser/useUser.test.ts index 31045c7..16194ed 100644 --- a/src/hooks/useUser/useUser.test.ts +++ b/src/hooks/useUser/useUser.test.ts @@ -8,7 +8,6 @@ import { testUser1 } from '@/tests/fixtures/users'; import { useCreateUserMutation, useDeleteUserMutation, usePatchUserMutation } from '../mutations'; import { useUserQuery } from '../queries'; import { useToaster } from '../useToaster'; - import { LOADING_USER_ID, useUser } from './useUser'; jest.mock('posthog-react-native', () => ({ From b9b17bba7d3f238a589ad10d937ac1c418a3b7f9 Mon Sep 17 00:00:00 2001 From: Ziga Krasovec Date: Sat, 21 Mar 2026 13:28:27 +0100 Subject: [PATCH 8/9] feedback: Applied PR feedback --- src/__tests__/play/daily-puzzle-solved.test.tsx | 4 ++-- src/__tests__/play/training-puzzle-solved.test.tsx | 4 ++-- src/__tests__/user-profile.test.tsx | 13 +++++++------ .../(authenticated)/play/daily-puzzle-solved.tsx | 2 +- .../(authenticated)/play/training-puzzle-solved.tsx | 2 +- src/app/(authenticated)/user-profile.tsx | 12 ++++++------ .../ModalViewBackButton/ModalViewBackButton.tsx | 1 - src/components/ui/Button/Button.test.tsx | 2 +- src/components/ui/Button/Button.tsx | 2 +- 9 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/__tests__/play/daily-puzzle-solved.test.tsx b/src/__tests__/play/daily-puzzle-solved.test.tsx index 59dc67f..1a210af 100644 --- a/src/__tests__/play/daily-puzzle-solved.test.tsx +++ b/src/__tests__/play/daily-puzzle-solved.test.tsx @@ -90,7 +90,7 @@ describe('Daily puzzle solved screen', () => { usePuzzleStatisticsSpy.mockReturnValue({ isLoading: true, data: undefined }); render(); - expect(screen.queryByRole('spinbutton', { name: 'Nalagam statistiko dnevnih izzivov...' })).toBeOnTheScreen(); + expect(screen.queryByRole('progressbar', { name: 'Nalagam statistiko dnevnih izzivov...' })).toBeOnTheScreen(); expect(screen.queryByText('Statistika dnevnih izzivov')).not.toBeOnTheScreen(); }); @@ -125,7 +125,7 @@ describe('Daily puzzle solved screen', () => { usePuzzleStatisticsSpy.mockReturnValue({ isLoading: false, data: testPuzzleStatistics1 }); render(); - expect(screen.queryByRole('spinbutton', { name: 'Nalagam statistiko dnevnih izzivov...' })).not.toBeOnTheScreen(); + expect(screen.queryByRole('progressbar', { name: 'Nalagam statistiko dnevnih izzivov...' })).not.toBeOnTheScreen(); expect(screen.queryByText('Statistika dnevnih izzivov')).toBeOnTheScreen(); diff --git a/src/__tests__/play/training-puzzle-solved.test.tsx b/src/__tests__/play/training-puzzle-solved.test.tsx index 5bef299..8a8ea00 100644 --- a/src/__tests__/play/training-puzzle-solved.test.tsx +++ b/src/__tests__/play/training-puzzle-solved.test.tsx @@ -91,7 +91,7 @@ describe('Training puzzle solved screen', () => { usePuzzleStatisticsSpy.mockReturnValue({ isLoading: true, data: undefined }); render(); - expect(screen.queryByRole('spinbutton', { name: 'Nalagam statistiko trening izzivov...' })).toBeOnTheScreen(); + expect(screen.queryByRole('progressbar', { name: 'Nalagam statistiko trening izzivov...' })).toBeOnTheScreen(); expect(screen.queryByText('Trening statistika')).not.toBeOnTheScreen(); }); @@ -126,7 +126,7 @@ describe('Training puzzle solved screen', () => { usePuzzleStatisticsSpy.mockReturnValue({ isLoading: false, data: testPuzzleStatistics1 }); render(); - expect(screen.queryByRole('spinbutton', { name: 'Nalagam statistiko trening izzivov...' })).not.toBeOnTheScreen(); + expect(screen.queryByRole('progressbar', { name: 'Nalagam statistiko trening izzivov...' })).not.toBeOnTheScreen(); expect(screen.queryByText('Trening statistika')).toBeOnTheScreen(); diff --git a/src/__tests__/user-profile.test.tsx b/src/__tests__/user-profile.test.tsx index baccb50..37e46ca 100644 --- a/src/__tests__/user-profile.test.tsx +++ b/src/__tests__/user-profile.test.tsx @@ -75,7 +75,7 @@ describe('', () => { useUserQuerySpy.mockReturnValue({ isLoading: true, data: undefined }); render(); - expect(screen.queryByRole('spinbutton', { name: 'Nalagam podatke o uporabniku...' })).toBeOnTheScreen(); + expect(screen.queryByRole('progressbar', { name: 'Nalagam podatke o uporabniku...' })).toBeOnTheScreen(); expect(screen.queryByText('Statistika dnevnih izzivov')).not.toBeOnTheScreen(); }); @@ -83,7 +83,7 @@ describe('', () => { usePuzzlesStatisticsQuerySpy.mockReturnValue({ isLoading: true, data: undefined }); render(); - expect(screen.queryByRole('spinbutton', { name: 'Nalagam podatke o uporabniku...' })).toBeOnTheScreen(); + expect(screen.queryByRole('progressbar', { name: 'Nalagam podatke o uporabniku...' })).toBeOnTheScreen(); expect(screen.queryByText('Statistika dnevnih izzivov')).not.toBeOnTheScreen(); }); @@ -97,7 +97,7 @@ describe('', () => { it('should render daily stats when data is loaded', () => { render(); - expect(screen.queryByRole('spinbutton', { name: 'Nalagam podatke o uporabniku...' })).not.toBeOnTheScreen(); + expect(screen.queryByRole('progressbar', { name: 'Nalagam podatke o uporabniku...' })).not.toBeOnTheScreen(); expect(screen.queryByText('Statistika dnevnih izzivov')).toBeOnTheScreen(); expect(screen.getByTestId('stat-total-played')).toHaveTextContent(/Odigranih5/); expect(screen.getByTestId('stat-win-rate')).toHaveTextContent(/% rešenih80%/); @@ -105,15 +105,16 @@ describe('', () => { expect(screen.getByTestId('stat-max-streak')).toHaveTextContent(/Najdaljši niz3/); }); - it('should render 100% win rate when totalPlayed is 0', () => { + it('should render win rate rounded up to nearest integer', () => { usePuzzlesStatisticsQuerySpy.mockReturnValue({ isLoading: false, isNotFound: false, - data: { ...testPuzzleStatistics1, totalPlayed: 0, totalWon: 0 }, + data: { ...testPuzzleStatistics1, totalPlayed: 3, totalWon: 1 }, }); render(); - expect(screen.queryByText('100%')).toBeOnTheScreen(); + // Math.ceil(1 / 3 * 100) = Math.ceil(33.33) = 34 + expect(screen.getByTestId('stat-win-rate')).toHaveTextContent(/34%/); }); it('should render empty state when user has no stats', () => { diff --git a/src/app/(authenticated)/play/daily-puzzle-solved.tsx b/src/app/(authenticated)/play/daily-puzzle-solved.tsx index 34f70a3..01fb684 100644 --- a/src/app/(authenticated)/play/daily-puzzle-solved.tsx +++ b/src/app/(authenticated)/play/daily-puzzle-solved.tsx @@ -33,7 +33,7 @@ export default function DailyPuzzleSolvedScreen() { diff --git a/src/app/(authenticated)/play/training-puzzle-solved.tsx b/src/app/(authenticated)/play/training-puzzle-solved.tsx index 770ab29..bdbc6e4 100644 --- a/src/app/(authenticated)/play/training-puzzle-solved.tsx +++ b/src/app/(authenticated)/play/training-puzzle-solved.tsx @@ -39,7 +39,7 @@ export default function TrainingPuzzleSolvedScreen() { diff --git a/src/app/(authenticated)/user-profile.tsx b/src/app/(authenticated)/user-profile.tsx index 8fa1154..c76f06e 100644 --- a/src/app/(authenticated)/user-profile.tsx +++ b/src/app/(authenticated)/user-profile.tsx @@ -24,7 +24,7 @@ export default function UserProfileScreen() { const isLoading = isLoadingUser || isLoadingStats; const title = isLoadingUser ? 'Nalagam podatke...' : (user?.nickname ?? 'Profil'); const memberSince = user ? dayjs(user._creationTime).format('MMMM YYYY') : ''; - const winRate = stats && stats.totalPlayed > 0 ? Math.ceil((stats.totalWon / stats.totalPlayed) * 100) : 100; + const winRate = stats ? Math.ceil((stats.totalWon / stats.totalPlayed) * 100) : 0; return ( <> @@ -33,7 +33,7 @@ export default function UserProfileScreen() { @@ -60,7 +60,7 @@ export default function UserProfileScreen() { Odigranih - {stats?.totalPlayed} + {stats.totalPlayed} @@ -76,7 +76,7 @@ export default function UserProfileScreen() { Trenutni niz - {stats?.currentStreak} + {stats.currentStreak} @@ -84,12 +84,12 @@ export default function UserProfileScreen() { Najdaljši niz - {stats?.maxStreak} + {stats.maxStreak} - + )} diff --git a/src/components/navigation/ModalViewBackButton/ModalViewBackButton.tsx b/src/components/navigation/ModalViewBackButton/ModalViewBackButton.tsx index c9a253f..78c485a 100644 --- a/src/components/navigation/ModalViewBackButton/ModalViewBackButton.tsx +++ b/src/components/navigation/ModalViewBackButton/ModalViewBackButton.tsx @@ -23,7 +23,6 @@ export function ModalViewBackButton({ canGoBack }: { canGoBack?: boolean }) { const styles = StyleSheet.create((theme) => ({ container: { flexDirection: 'row', - gap: 0, paddingLeft: theme.spacing[2], paddingRight: theme.spacing[4], alignItems: 'center', diff --git a/src/components/ui/Button/Button.test.tsx b/src/components/ui/Button/Button.test.tsx index 493b967..c185141 100644 --- a/src/components/ui/Button/Button.test.tsx +++ b/src/components/ui/Button/Button.test.tsx @@ -21,7 +21,7 @@ describe('