diff --git a/.Jules/changelog.md b/.Jules/changelog.md index 11fc864..c91381d 100644 --- a/.Jules/changelog.md +++ b/.Jules/changelog.md @@ -123,3 +123,8 @@ - `.jules/todo.md` - `.jules/knowledge.md` - `.jules/changelog.md` + +### Mobile +- Added reusable animated `Skeleton` primitive component in `mobile/components/ui/Skeleton.js`. +- Created `HomeScreenSkeleton` loading view for groups list. +- Replaced basic `ActivityIndicator` in `HomeScreen.js` with new skeleton loading experience. diff --git a/.Jules/knowledge.md b/.Jules/knowledge.md index 43a9ab0..acbdef9 100644 --- a/.Jules/knowledge.md +++ b/.Jules/knowledge.md @@ -756,3 +756,6 @@ _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 +- When using `Animated.loop` within a `useEffect` hook in React Native components, assign the loop to a variable and explicitly call `.stop()` on it in the cleanup function to prevent memory leaks. +- Playwright in the testing environment cannot be run with `headless=False`; attempting to do so will result in an 'Executable doesn't support UI mode' error. +- Always clean up temporary Playwright verification scripts, logs, and generated screenshots before committing changes to maintain repository hygiene. diff --git a/.Jules/todo.md b/.Jules/todo.md index ebb0c7a..40fe0c3 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-02-14 + - Files modified: `mobile/screens/HomeScreen.js`, `mobile/components/skeletons/HomeScreenSkeleton.js`, `mobile/components/ui/Skeleton.js` - Context: Replace ActivityIndicator with skeleton group cards - Impact: Better loading experience, less jarring - Size: ~40 lines diff --git a/mobile/components/skeletons/HomeScreenSkeleton.js b/mobile/components/skeletons/HomeScreenSkeleton.js new file mode 100644 index 0000000..af398ac --- /dev/null +++ b/mobile/components/skeletons/HomeScreenSkeleton.js @@ -0,0 +1,36 @@ +import React from 'react'; +import { View, StyleSheet } from 'react-native'; +import { Card } from 'react-native-paper'; +import Skeleton from '../ui/Skeleton'; + +const HomeScreenSkeleton = () => { + return ( + + {Array.from({ length: 5 }).map((_, i) => ( + + } + left={() => } + /> + + + + + ))} + + ); +}; + +const styles = StyleSheet.create({ + container: { + padding: 16, + }, + card: { + marginBottom: 16, + }, + contentSkeleton: { + marginTop: 4, + }, +}); + +export default HomeScreenSkeleton; diff --git a/mobile/components/ui/Skeleton.js b/mobile/components/ui/Skeleton.js new file mode 100644 index 0000000..f9973af --- /dev/null +++ b/mobile/components/ui/Skeleton.js @@ -0,0 +1,45 @@ +import React, { useEffect, useRef } from 'react'; +import { Animated, View, StyleSheet } 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 ( + + ); +}; + +export default Skeleton; diff --git a/mobile/screens/HomeScreen.js b/mobile/screens/HomeScreen.js index d2f3c38..3bca9d0 100644 --- a/mobile/screens/HomeScreen.js +++ b/mobile/screens/HomeScreen.js @@ -17,6 +17,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 HomeScreenSkeleton from "../components/skeletons/HomeScreenSkeleton"; const HomeScreen = ({ navigation }) => { const { token, logout, user } = useContext(AuthContext); @@ -257,9 +258,7 @@ const HomeScreen = ({ navigation }) => { {isLoading ? ( - - - + ) : (