diff --git a/.Jules/changelog.md b/.Jules/changelog.md index 11fc864..8eaf80e 100644 --- a/.Jules/changelog.md +++ b/.Jules/changelog.md @@ -7,6 +7,14 @@ ## [Unreleased] ### Added +- **Mobile Skeleton Loading:** Implemented skeleton loading states for the HomeScreen group list. + - **Features:** + - Created generic `Skeleton` primitive with pulsing animation. + - Created `GroupListSkeleton` component matching `HapticCard` layout. + - Integrated into `HomeScreen` to replace simple spinner. + - Accessible with `accessibilityRole="progressbar"`. + - **Technical:** Created `mobile/components/ui/Skeleton.js` and `mobile/components/skeletons/GroupListSkeleton.js`. + - **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/todo.md b/.Jules/todo.md index ebb0c7a..078fa81 100644 --- a/.Jules/todo.md +++ b/.Jules/todo.md @@ -57,7 +57,8 @@ - Impact: Native feel, users can easily refresh data - Size: ~150 lines -- [ ] **[ux]** Complete skeleton loading for HomeScreen groups +- [x] **[ux]** Complete skeleton loading for HomeScreen groups + - Completed: 2026-02-09 - File: `mobile/screens/HomeScreen.js` - Context: Replace ActivityIndicator with skeleton group cards - Impact: Better loading experience, less jarring diff --git a/mobile/components/skeletons/GroupListSkeleton.js b/mobile/components/skeletons/GroupListSkeleton.js new file mode 100644 index 0000000..9db0fbf --- /dev/null +++ b/mobile/components/skeletons/GroupListSkeleton.js @@ -0,0 +1,67 @@ +import React from 'react'; +import { View, StyleSheet, FlatList } from 'react-native'; +import { Card } from 'react-native-paper'; +import Skeleton from '../ui/Skeleton'; + +const GroupListSkeleton = () => { + const dummyData = [1, 2, 3, 4, 5]; // Render 5 skeleton items + + const renderItem = () => ( + + } + subtitle={} + left={(props) => ( + + )} + /> + + + + + ); + + return ( + + item.toString()} + contentContainerStyle={styles.list} + scrollEnabled={false} // Disable scrolling for skeleton state + /> + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + list: { + padding: 16, + }, + card: { + marginBottom: 16, + }, + skeletonTitle: { + marginTop: 4, + marginBottom: 4, + }, + skeletonSubtitle: { + marginTop: 4, + }, + skeletonAvatar: { + marginRight: 8, + }, + skeletonStatus: { + marginTop: 8, + } +}); + +export default GroupListSkeleton; diff --git a/mobile/components/ui/Skeleton.js b/mobile/components/ui/Skeleton.js new file mode 100644 index 0000000..94bc2ec --- /dev/null +++ b/mobile/components/ui/Skeleton.js @@ -0,0 +1,60 @@ +import React, { useEffect, useRef } from 'react'; +import { Animated, StyleSheet } from 'react-native'; +import { Surface, 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 animation = Animated.loop( + Animated.sequence([ + Animated.timing(opacity, { + toValue: 1, + duration: 1000, + useNativeDriver: true, + }), + Animated.timing(opacity, { + toValue: 0.3, + duration: 1000, + useNativeDriver: true, + }), + ]) + ); + animation.start(); + + return () => animation.stop(); + }, [opacity]); + + return ( + + + + ); +}; + +const styles = StyleSheet.create({ + skeleton: { + overflow: 'hidden', + }, +}); + +export default Skeleton; diff --git a/mobile/screens/HomeScreen.js b/mobile/screens/HomeScreen.js index d2f3c38..44f4487 100644 --- a/mobile/screens/HomeScreen.js +++ b/mobile/screens/HomeScreen.js @@ -10,6 +10,7 @@ import { TextInput, useTheme, } from "react-native-paper"; +import GroupListSkeleton from "../components/skeletons/GroupListSkeleton"; import HapticButton from '../components/ui/HapticButton'; import HapticCard from '../components/ui/HapticCard'; import { HapticAppbarAction } from '../components/ui/HapticAppbar'; @@ -257,9 +258,7 @@ const HomeScreen = ({ navigation }) => { {isLoading ? ( - - - + ) : (