diff --git a/.Jules/changelog.md b/.Jules/changelog.md index 11fc864..28e3adc 100644 --- a/.Jules/changelog.md +++ b/.Jules/changelog.md @@ -7,6 +7,13 @@ ## [Unreleased] ### Added +- **Mobile Skeleton Loading:** Implemented skeleton loading state for the Home screen group list. + - **Features:** + - Created reusable `Skeleton` component with pulsing opacity animation. + - Created `GroupCardSkeleton` mimicking the exact layout of group cards. + - Replaced the full-screen spinner with a list of skeletons to reduce perceived latency. + - Wrapped in accessible container (`accessibilityLabel="Loading groups"`). + - **Technical:** Uses `Animated.loop` for performance and `useTheme` for dark mode support. - **Password Strength Meter:** Added a visual password strength indicator to the signup form. - **Features:** - Real-time strength calculation (Length, Uppercase, Lowercase, Number, Symbol). diff --git a/.Jules/knowledge.md b/.Jules/knowledge.md index 43a9ab0..f169d48 100644 --- a/.Jules/knowledge.md +++ b/.Jules/knowledge.md @@ -299,6 +299,32 @@ Commonly used components: - `` and `` for overlays - `` for loading states +### Mobile Skeleton Loading Pattern + +**Date:** 2026-02-14 +**Context:** Creating loading states for mobile lists + +To create smooth skeleton loaders in React Native without external libraries: + +```javascript +// 1. Create base Skeleton component +const opacity = useRef(new Animated.Value(0.3)).current; +useEffect(() => { + Animated.loop( + Animated.sequence([ + Animated.timing(opacity, { toValue: 0.7, duration: 800, useNativeDriver: true }), + Animated.timing(opacity, { toValue: 0.3, duration: 800, useNativeDriver: true }) + ]) + ).start(); +}, []); +return ; +``` + +**Key Implementation Detail:** +- Use `Animated.loop` with `Animated.sequence` for the pulse effect. +- Use `react-native-paper`'s `useTheme` to ensure the skeleton color works in both light and dark modes (`surfaceVariant` is a good choice). +- Compose these primitives into `CardSkeleton` components that match the structure of the real content card. + ### Safe Area Pattern **Date:** 2026-01-01 diff --git a/.Jules/todo.md b/.Jules/todo.md index ebb0c7a..bd1fc36 100644 --- a/.Jules/todo.md +++ b/.Jules/todo.md @@ -57,11 +57,12 @@ - Impact: Native feel, users can easily refresh data - Size: ~150 lines -- [ ] **[ux]** Complete skeleton loading for HomeScreen groups - - File: `mobile/screens/HomeScreen.js` - - Context: Replace ActivityIndicator with skeleton group cards - - Impact: Better loading experience, less jarring - - Size: ~40 lines +- [x] **[ux]** Complete skeleton loading for HomeScreen groups + - Completed: 2026-02-14 + - Files: `mobile/screens/HomeScreen.js`, `mobile/components/ui/Skeleton.js`, `mobile/components/skeletons/GroupCardSkeleton.js` + - Context: Replaced ActivityIndicator with pulsing skeleton cards + - Impact: Professional loading state, prevents layout shift + - Size: ~70 lines - Added: 2026-01-01 - [x] **[a11y]** Complete accessibility labels for all screens diff --git a/mobile/components/skeletons/GroupCardSkeleton.js b/mobile/components/skeletons/GroupCardSkeleton.js new file mode 100644 index 0000000..9aa9f41 --- /dev/null +++ b/mobile/components/skeletons/GroupCardSkeleton.js @@ -0,0 +1,19 @@ +import React from 'react'; +import { Card } from 'react-native-paper'; +import Skeleton from '../ui/Skeleton'; + +const GroupCardSkeleton = () => { + return ( + + } + left={(props) => } + /> + + + + + ); +}; + +export default GroupCardSkeleton; diff --git a/mobile/components/ui/Skeleton.js b/mobile/components/ui/Skeleton.js new file mode 100644 index 0000000..06f733e --- /dev/null +++ b/mobile/components/ui/Skeleton.js @@ -0,0 +1,45 @@ +import React, { useEffect, useRef } from 'react'; +import { Animated } from 'react-native'; +import { useTheme } from 'react-native-paper'; + +const Skeleton = ({ width, height, borderRadius = 4, style }) => { + const theme = useTheme(); + const opacity = useRef(new Animated.Value(0.3)).current; + + useEffect(() => { + const pulse = Animated.loop( + Animated.sequence([ + Animated.timing(opacity, { + toValue: 0.7, + duration: 800, + useNativeDriver: true, + }), + Animated.timing(opacity, { + toValue: 0.3, + duration: 800, + useNativeDriver: true, + }), + ]) + ); + pulse.start(); + + return () => pulse.stop(); + }, [opacity]); + + return ( + + ); +}; + +export default Skeleton; diff --git a/mobile/screens/HomeScreen.js b/mobile/screens/HomeScreen.js index d2f3c38..77ea4c3 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 GroupCardSkeleton from '../components/skeletons/GroupCardSkeleton'; const HomeScreen = ({ navigation }) => { const { token, logout, user } = useContext(AuthContext); @@ -257,8 +257,10 @@ const HomeScreen = ({ navigation }) => { {isLoading ? ( - - + + {[1, 2, 3, 4, 5, 6].map((i) => ( + + ))} ) : (