diff --git a/.Jules/changelog.md b/.Jules/changelog.md index 11fc864..0f04b65 100644 --- a/.Jules/changelog.md +++ b/.Jules/changelog.md @@ -123,3 +123,8 @@ - `.jules/todo.md` - `.jules/knowledge.md` - `.jules/changelog.md` + +### 2026-03-08 +- Added generic `Skeleton` component and `GroupListSkeleton` component to mobile app +- Replaced basic ActivityIndicator loading screen with `GroupListSkeleton` on mobile `HomeScreen` +- Refactored `FriendsScreen` inline skeleton logic to use the new generic `Skeleton` component diff --git a/.Jules/knowledge.md b/.Jules/knowledge.md index 43a9ab0..a5062c3 100644 --- a/.Jules/knowledge.md +++ b/.Jules/knowledge.md @@ -756,3 +756,8 @@ _Document errors and their solutions here as you encounter them._ - react-native-paper: UI components - axios: API calls (via api/client.js) - expo: Platform SDK + +### Development Workflows +- The mobile project is configured with `react-native-web`. You can test it in a browser using `npx expo start --web`. +- Use `AsyncStorage` values (`auth_token`, `refresh_token`, `user_data`) directly into Playwright's `window.localStorage` to bypass login flows for Playwright testing via react-native-web. +- When creating files in new subdirectories inside `/mobile/`, use `mkdir -p` before the write operation to ensure the path exists. diff --git a/.Jules/todo.md b/.Jules/todo.md index ebb0c7a..326aef8 100644 --- a/.Jules/todo.md +++ b/.Jules/todo.md @@ -57,8 +57,9 @@ - Impact: Native feel, users can easily refresh data - Size: ~150 lines -- [ ] **[ux]** Complete skeleton loading for HomeScreen groups - - File: `mobile/screens/HomeScreen.js` +- [x] **[ux]** Complete skeleton loading for HomeScreen groups + - Completed: 2026-03-08 + - File: `mobile/screens/HomeScreen.js`, `mobile/components/ui/Skeleton.js`, `mobile/components/skeletons/GroupListSkeleton.js` - Context: Replace ActivityIndicator with skeleton group cards - Impact: Better loading experience, less jarring - Size: ~40 lines @@ -168,3 +169,7 @@ - Completed: 2026-02-08 - Files modified: `web/components/ui/PasswordStrength.tsx`, `web/pages/Auth.tsx` - Impact: Provides visual feedback on password complexity during signup +- [x] **[ux]** Complete skeleton loading for HomeScreen groups + - Completed: 2026-03-08 + - Files modified: `mobile/screens/HomeScreen.js`, `mobile/screens/FriendsScreen.js`, `mobile/components/ui/Skeleton.js`, `mobile/components/skeletons/GroupListSkeleton.js` + - Impact: Better loading experience across the mobile app via generic skeleton components. diff --git a/mobile/components/skeletons/GroupListSkeleton.js b/mobile/components/skeletons/GroupListSkeleton.js new file mode 100644 index 0000000..02d0d1e --- /dev/null +++ b/mobile/components/skeletons/GroupListSkeleton.js @@ -0,0 +1,44 @@ +import React from 'react'; +import { StyleSheet, View } from 'react-native'; +import { Card } from 'react-native-paper'; +import Skeleton from '../ui/Skeleton'; + +const GroupListSkeleton = ({ count = 5 }) => { + return ( + + {Array.from({ length: count }).map((_, index) => ( + + } + left={(props) => ( + + )} + /> + + + + + ))} + + ); +}; + +const styles = StyleSheet.create({ + container: { + padding: 16, + }, + card: { + marginBottom: 16, + }, +}); + +export default GroupListSkeleton; diff --git a/mobile/components/ui/Skeleton.js b/mobile/components/ui/Skeleton.js new file mode 100644 index 0000000..e66aafd --- /dev/null +++ b/mobile/components/ui/Skeleton.js @@ -0,0 +1,51 @@ +import React, { useEffect, useRef } from 'react'; +import { Animated, StyleSheet, View } from 'react-native'; +import { useTheme } from 'react-native-paper'; + +const Skeleton = ({ width, height, borderRadius = 4, style }) => { + const theme = useTheme(); + const opacityAnim = useRef(new Animated.Value(0.3)).current; + + useEffect(() => { + const loop = Animated.loop( + Animated.sequence([ + Animated.timing(opacityAnim, { + toValue: 1, + duration: 700, + useNativeDriver: true, + }), + Animated.timing(opacityAnim, { + toValue: 0.3, + duration: 700, + useNativeDriver: true, + }), + ]) + ); + loop.start(); + return () => loop.stop(); + }, [opacityAnim]); + + return ( + + ); +}; + +const styles = StyleSheet.create({ + skeleton: { + overflow: 'hidden', + }, +}); + +export default Skeleton; diff --git a/mobile/screens/FriendsScreen.js b/mobile/screens/FriendsScreen.js index c778a95..3c8b87c 100644 --- a/mobile/screens/FriendsScreen.js +++ b/mobile/screens/FriendsScreen.js @@ -1,6 +1,6 @@ import { useIsFocused } from "@react-navigation/native"; -import { useContext, useEffect, useRef, useState } from "react"; -import { Alert, Animated, FlatList, RefreshControl, StyleSheet, View } from "react-native"; +import { useContext, useEffect, useState } from "react"; +import { Alert, FlatList, RefreshControl, StyleSheet, View } from "react-native"; import { Appbar, Avatar, @@ -15,6 +15,7 @@ import { triggerPullRefreshHaptic } from '../components/ui/hapticUtils'; import { getFriendsBalance, getGroups } from "../api/groups"; import { AuthContext } from "../context/AuthContext"; import { formatCurrency } from "../utils/currency"; +import Skeleton from "../components/ui/Skeleton"; const FriendsScreen = () => { const { token, user } = useContext(AuthContext); @@ -167,42 +168,12 @@ const FriendsScreen = () => { ); }; - // Shimmer skeleton components - const opacityAnim = useRef(new Animated.Value(0.3)).current; - useEffect(() => { - const loop = Animated.loop( - Animated.sequence([ - Animated.timing(opacityAnim, { - toValue: 1, - duration: 700, - useNativeDriver: true, - }), - Animated.timing(opacityAnim, { - toValue: 0.3, - duration: 700, - useNativeDriver: true, - }), - ]) - ); - loop.start(); - return () => loop.stop(); - }, [opacityAnim]); - const SkeletonRow = () => ( - + - - + + ); @@ -315,23 +286,6 @@ const styles = StyleSheet.create({ alignItems: "center", marginBottom: 14, }, - skeletonAvatar: { - width: 48, - height: 48, - borderRadius: 24, - backgroundColor: "#e0e0e0", - }, - skeletonLine: { - height: 14, - backgroundColor: "#e0e0e0", - borderRadius: 6, - marginBottom: 6, - }, - skeletonLineSmall: { - height: 12, - backgroundColor: "#e0e0e0", - borderRadius: 6, - }, }); export default FriendsScreen; diff --git a/mobile/screens/HomeScreen.js b/mobile/screens/HomeScreen.js index d2f3c38..7f5cf14 100644 --- a/mobile/screens/HomeScreen.js +++ b/mobile/screens/HomeScreen.js @@ -1,7 +1,6 @@ import { useContext, useEffect, useState } from "react"; import { Alert, FlatList, RefreshControl, StyleSheet, View } from "react-native"; import { - ActivityIndicator, Appbar, Avatar, Modal, @@ -17,6 +16,7 @@ import * as Haptics from "expo-haptics"; import { createGroup, getGroups, getOptimizedSettlements } from "../api/groups"; import { AuthContext } from "../context/AuthContext"; import { formatCurrency, getCurrencySymbol } from "../utils/currency"; +import GroupListSkeleton from "../components/skeletons/GroupListSkeleton"; const HomeScreen = ({ navigation }) => { const { token, logout, user } = useContext(AuthContext); @@ -257,9 +257,7 @@ const HomeScreen = ({ navigation }) => { {isLoading ? ( - - - + ) : (