From 5bc3ca18326fb6624770b9173a7567ba84f0d58e Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Tue, 18 Nov 2025 12:36:52 -0800 Subject: [PATCH] feat: integrate React Query and configure build profiles - Add React Query provider and client - Create reusable UI components (Loading, Error, Empty) - Implement custom hooks with fallback data - Integrate hooks into Resources and Community screens - Add pull-to-refresh support - Configure EAS Build profiles and environment variables --- .gitignore | 1 + mobile/.env.example | 5 ++ mobile/.gitignore | 20 ++++++ mobile/app.json | 15 ++++- mobile/app/(tabs)/community.tsx | 91 ++++++++++++++++++++++++++- mobile/app/(tabs)/resources.tsx | 77 ++++++++++++----------- mobile/app/_layout.tsx | 7 ++- mobile/app/lib/query-client.ts | 11 ++++ mobile/components/ui/EmptyState.tsx | 38 +++++++++++ mobile/components/ui/ErrorState.tsx | 52 +++++++++++++++ mobile/components/ui/LoadingState.tsx | 28 +++++++++ mobile/eas.json | 28 +++++++++ mobile/hooks/useCommunity.ts | 56 +++++++++++++++++ mobile/hooks/useResources.ts | 49 +++++++++++++++ mobile/hooks/useUser.ts | 35 +++++++++++ mobile/package-lock.json | 2 +- mobile/package.json | 2 +- 17 files changed, 470 insertions(+), 47 deletions(-) create mode 100644 mobile/.env.example create mode 100644 mobile/.gitignore create mode 100644 mobile/app/lib/query-client.ts create mode 100644 mobile/components/ui/EmptyState.tsx create mode 100644 mobile/components/ui/ErrorState.tsx create mode 100644 mobile/components/ui/LoadingState.tsx create mode 100644 mobile/eas.json create mode 100644 mobile/hooks/useCommunity.ts create mode 100644 mobile/hooks/useResources.ts create mode 100644 mobile/hooks/useUser.ts diff --git a/.gitignore b/.gitignore index 485bc4d..543622e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ dist/ build/ .cache/ .env* +!.env.example # Next.js web/.next/ diff --git a/mobile/.env.example b/mobile/.env.example new file mode 100644 index 0000000..2a4662c --- /dev/null +++ b/mobile/.env.example @@ -0,0 +1,5 @@ +# API Configuration +EXPO_PUBLIC_API_URL=http://localhost:8000/api/v1 + +# Feature Flags +EXPO_PUBLIC_ENABLE_ANALYTICS=false diff --git a/mobile/.gitignore b/mobile/.gitignore new file mode 100644 index 0000000..d6e2d41 --- /dev/null +++ b/mobile/.gitignore @@ -0,0 +1,20 @@ +# Expo +.expo/ +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision +*.orig.* + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +*.pem + +# Native builds +android/ +ios/ diff --git a/mobile/app.json b/mobile/app.json index a918c95..a84a3dc 100644 --- a/mobile/app.json +++ b/mobile/app.json @@ -30,6 +30,17 @@ }, "plugins": [ "expo-router" - ] + ], + "updates": { + "url": "https://u.expo.dev/your-project-id" + }, + "runtimeVersion": { + "policy": "appVersion" + }, + "extra": { + "eas": { + "projectId": "your-project-id" + } + } } -} +} \ No newline at end of file diff --git a/mobile/app/(tabs)/community.tsx b/mobile/app/(tabs)/community.tsx index eb6bf9a..44b4add 100644 --- a/mobile/app/(tabs)/community.tsx +++ b/mobile/app/(tabs)/community.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import { View, ScrollView, @@ -6,8 +6,12 @@ import { StyleSheet, SafeAreaView, TouchableOpacity, + RefreshControl, } from 'react-native'; -import { Ionicons } from '@expo/vector-icons'; +import { useCommunity } from '../../hooks/useCommunity'; +import { LoadingState } from '../../components/ui/LoadingState'; +import { ErrorState } from '../../components/ui/ErrorState'; +import { EmptyState } from '../../components/ui/EmptyState'; const styles = StyleSheet.create({ container: { @@ -84,9 +88,51 @@ const styles = StyleSheet.create({ color: '#000', marginBottom: 12, }, + postCard: { + backgroundColor: '#fff', + borderRadius: 12, + padding: 16, + marginBottom: 12, + }, + postHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: 8, + }, + postAuthor: { + fontWeight: '600', + fontSize: 14, + }, + postTime: { + color: '#666', + fontSize: 12, + }, + postContent: { + fontSize: 14, + lineHeight: 20, + color: '#333', + marginBottom: 12, + }, + postFooter: { + flexDirection: 'row', + borderTopWidth: 1, + borderTopColor: '#f0f0f0', + paddingTop: 12, + }, + postStat: { + marginRight: 16, + color: '#666', + fontSize: 12, + }, }); export default function CommunityScreen() { + const { data: posts, isLoading, isError, refetch } = useCommunity(); + + const onRefresh = useCallback(() => { + refetch(); + }, [refetch]); + const communities = [ { title: 'Support Group', @@ -114,9 +160,22 @@ export default function CommunityScreen() { }, ]; + if (isLoading) { + return ; + } + + if (isError) { + return ; + } + return ( - + + } + > Community @@ -143,6 +202,32 @@ export default function CommunityScreen() { ))} + + Recent Activity + + + {!posts || posts.length === 0 ? ( + + + + ) : ( + posts.map((post) => ( + + + + {post.author} + {post.timestamp} + + {post.content} + + {post.likes} Likes + {post.comments} Comments + + + + )) + )} + Community Guidelines diff --git a/mobile/app/(tabs)/resources.tsx b/mobile/app/(tabs)/resources.tsx index 0fc272f..0168e5d 100644 --- a/mobile/app/(tabs)/resources.tsx +++ b/mobile/app/(tabs)/resources.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import { View, ScrollView, @@ -6,7 +6,12 @@ import { StyleSheet, SafeAreaView, TouchableOpacity, + RefreshControl, } from 'react-native'; +import { useResources } from '../../hooks/useResources'; +import { LoadingState } from '../../components/ui/LoadingState'; +import { ErrorState } from '../../components/ui/ErrorState'; +import { EmptyState } from '../../components/ui/EmptyState'; const styles = StyleSheet.create({ container: { @@ -75,36 +80,28 @@ const styles = StyleSheet.create({ }); export default function ResourcesScreen() { - const resources = [ - { - title: 'Financial Support', - description: 'Resources for managing medical costs and financial burdens', - }, - { - title: 'Mental Health', - description: 'Mental health services and counseling resources', - }, - { - title: 'Medical Information', - description: 'Reliable health information and diagnosis resources', - }, - { - title: 'Support Groups', - description: 'Connect with others on similar health journeys', - }, - { - title: 'Nutrition & Wellness', - description: 'Health and wellness resources during treatment', - }, - { - title: 'Legal Resources', - description: 'Information about health law and patient rights', - }, - ]; + const { data: resources, isLoading, isError, refetch } = useResources(); + + const onRefresh = useCallback(() => { + refetch(); + }, [refetch]); + + if (isLoading) { + return ; + } + + if (isError) { + return ; + } return ( - + + } + > Resources @@ -112,17 +109,21 @@ export default function ResourcesScreen() { - {resources.map((resource, index) => ( - - - {resource.title} - {resource.description} - - Learn More → - + {!resources || resources.length === 0 ? ( + + ) : ( + resources.map((resource) => ( + + + {resource.title} + {resource.description} + + Learn More → + + - - ))} + )) + )} More Coming Soon diff --git a/mobile/app/_layout.tsx b/mobile/app/_layout.tsx index 7b8aa7e..7a51add 100644 --- a/mobile/app/_layout.tsx +++ b/mobile/app/_layout.tsx @@ -31,6 +31,9 @@ function useProtectedRoute() { }, [isAuthenticated, isInitialized, segments]); } +import { QueryClientProvider } from '@tanstack/react-query'; +import { queryClient } from './lib/query-client'; + export default function RootLayout() { const { initialize, isInitialized } = useAuthStore(); @@ -53,13 +56,13 @@ export default function RootLayout() { } return ( - <> + - + ); } diff --git a/mobile/app/lib/query-client.ts b/mobile/app/lib/query-client.ts new file mode 100644 index 0000000..16f2068 --- /dev/null +++ b/mobile/app/lib/query-client.ts @@ -0,0 +1,11 @@ +import { QueryClient } from '@tanstack/react-query'; + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 2, + staleTime: 1000 * 60 * 5, // 5 minutes + gcTime: 1000 * 60 * 30, // 30 minutes + }, + }, +}); diff --git a/mobile/components/ui/EmptyState.tsx b/mobile/components/ui/EmptyState.tsx new file mode 100644 index 0000000..5b792cd --- /dev/null +++ b/mobile/components/ui/EmptyState.tsx @@ -0,0 +1,38 @@ +import { View, Text, StyleSheet } from 'react-native'; + +interface EmptyStateProps { + title?: string; + message?: string; +} + +export function EmptyState({ + title = 'No Data', + message = 'There is nothing to show here yet.' +}: EmptyStateProps) { + return ( + + {title} + {message} + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 24, + }, + title: { + fontSize: 18, + fontWeight: 'bold', + color: '#333333', + marginBottom: 8, + }, + message: { + fontSize: 16, + color: '#666666', + textAlign: 'center', + }, +}); diff --git a/mobile/components/ui/ErrorState.tsx b/mobile/components/ui/ErrorState.tsx new file mode 100644 index 0000000..81adb61 --- /dev/null +++ b/mobile/components/ui/ErrorState.tsx @@ -0,0 +1,52 @@ +import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; + +interface ErrorStateProps { + message?: string; + onRetry?: () => void; +} + +export function ErrorState({ message = 'Something went wrong', onRetry }: ErrorStateProps) { + return ( + + Oops! + {message} + {onRetry && ( + + Try Again + + )} + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 24, + }, + title: { + fontSize: 20, + fontWeight: 'bold', + color: '#333333', + marginBottom: 8, + }, + message: { + fontSize: 16, + color: '#666666', + textAlign: 'center', + marginBottom: 24, + }, + button: { + backgroundColor: '#007AFF', + paddingHorizontal: 24, + paddingVertical: 12, + borderRadius: 8, + }, + buttonText: { + color: '#FFFFFF', + fontSize: 16, + fontWeight: '600', + }, +}); diff --git a/mobile/components/ui/LoadingState.tsx b/mobile/components/ui/LoadingState.tsx new file mode 100644 index 0000000..3da5550 --- /dev/null +++ b/mobile/components/ui/LoadingState.tsx @@ -0,0 +1,28 @@ +import { View, ActivityIndicator, StyleSheet, Text } from 'react-native'; + +interface LoadingStateProps { + message?: string; +} + +export function LoadingState({ message = 'Loading...' }: LoadingStateProps) { + return ( + + + {message && {message}} + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 20, + }, + text: { + marginTop: 12, + fontSize: 16, + color: '#666666', + }, +}); diff --git a/mobile/eas.json b/mobile/eas.json new file mode 100644 index 0000000..9ebc33c --- /dev/null +++ b/mobile/eas.json @@ -0,0 +1,28 @@ +{ + "cli": { + "version": ">= 7.0.0" + }, + "build": { + "development": { + "developmentClient": true, + "distribution": "internal", + "env": { + "APP_VARIANT": "development" + } + }, + "preview": { + "distribution": "internal", + "env": { + "APP_VARIANT": "preview" + } + }, + "production": { + "env": { + "APP_VARIANT": "production" + } + } + }, + "submit": { + "production": {} + } +} diff --git a/mobile/hooks/useCommunity.ts b/mobile/hooks/useCommunity.ts new file mode 100644 index 0000000..186695b --- /dev/null +++ b/mobile/hooks/useCommunity.ts @@ -0,0 +1,56 @@ +import { useQuery } from '@tanstack/react-query'; + +export interface Post { + id: string; + author: string; + content: string; + likes: number; + comments: number; + timestamp: string; +} + +const FALLBACK_POSTS: Post[] = [ + { + id: '1', + author: 'Sarah M.', + content: 'Just finished my first meditation session! Feeling so much calmer.', + likes: 12, + comments: 3, + timestamp: '2h ago', + }, + { + id: '2', + author: 'James K.', + content: 'Anyone have recommendations for good books on cognitive behavioral therapy?', + likes: 8, + comments: 5, + timestamp: '5h ago', + }, + { + id: '3', + author: 'Emily R.', + content: 'Remember to take it one day at a time. You got this!', + likes: 25, + comments: 2, + timestamp: '1d ago', + }, +]; + +async function fetchCommunityPosts(): Promise { + // Simulate API call + // const response = await fetch(`${process.env.EXPO_PUBLIC_API_URL}/community/posts`); + // if (!response.ok) throw new Error('Failed to fetch posts'); + // return response.json(); + + // Return fallback data for now + return new Promise((resolve) => { + setTimeout(() => resolve(FALLBACK_POSTS), 1000); + }); +} + +export function useCommunity() { + return useQuery({ + queryKey: ['community-posts'], + queryFn: fetchCommunityPosts, + }); +} diff --git a/mobile/hooks/useResources.ts b/mobile/hooks/useResources.ts new file mode 100644 index 0000000..12f9587 --- /dev/null +++ b/mobile/hooks/useResources.ts @@ -0,0 +1,49 @@ +import { useQuery } from '@tanstack/react-query'; + +export interface Resource { + id: string; + title: string; + description: string; + category: string; + imageUrl?: string; +} + +const FALLBACK_RESOURCES: Resource[] = [ + { + id: '1', + title: 'Understanding Anxiety', + description: 'Learn about the symptoms and coping mechanisms for anxiety.', + category: 'Mental Health', + }, + { + id: '2', + title: 'Meditation Basics', + description: 'A beginner guide to meditation and mindfulness.', + category: 'Wellness', + }, + { + id: '3', + title: 'Healthy Sleep Habits', + description: 'Tips for improving your sleep quality and hygiene.', + category: 'Lifestyle', + }, +]; + +async function fetchResources(): Promise { + // Simulate API call + // const response = await fetch(`${process.env.EXPO_PUBLIC_API_URL}/resources`); + // if (!response.ok) throw new Error('Failed to fetch resources'); + // return response.json(); + + // Return fallback data for now + return new Promise((resolve) => { + setTimeout(() => resolve(FALLBACK_RESOURCES), 1000); + }); +} + +export function useResources() { + return useQuery({ + queryKey: ['resources'], + queryFn: fetchResources, + }); +} diff --git a/mobile/hooks/useUser.ts b/mobile/hooks/useUser.ts new file mode 100644 index 0000000..e7c4f8a --- /dev/null +++ b/mobile/hooks/useUser.ts @@ -0,0 +1,35 @@ +import { useQuery } from '@tanstack/react-query'; + +export interface UserProfile { + id: string; + name: string; + email: string; + avatarUrl?: string; + bio?: string; +} + +const FALLBACK_USER: UserProfile = { + id: '1', + name: 'Alex Johnson', + email: 'alex.johnson@example.com', + bio: 'On a journey to better mental health.', +}; + +async function fetchUserProfile(): Promise { + // Simulate API call + // const response = await fetch(`${process.env.EXPO_PUBLIC_API_URL}/user/profile`); + // if (!response.ok) throw new Error('Failed to fetch user profile'); + // return response.json(); + + // Return fallback data for now + return new Promise((resolve) => { + setTimeout(() => resolve(FALLBACK_USER), 500); + }); +} + +export function useUser() { + return useQuery({ + queryKey: ['user-profile'], + queryFn: fetchUserProfile, + }); +} diff --git a/mobile/package-lock.json b/mobile/package-lock.json index 66a342a..00fba70 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "dependencies": { "@react-native-async-storage/async-storage": "^2.2.0", - "@tanstack/react-query": "^5.25.0", + "@tanstack/react-query": "^5.90.10", "axios": "^1.6.0", "expo": "^51.0.0", "expo-constants": "~15.4.0", diff --git a/mobile/package.json b/mobile/package.json index 0e4f38a..eaeee2a 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -17,7 +17,7 @@ }, "dependencies": { "@react-native-async-storage/async-storage": "^2.2.0", - "@tanstack/react-query": "^5.25.0", + "@tanstack/react-query": "^5.90.10", "axios": "^1.6.0", "expo": "^51.0.0", "expo-constants": "~15.4.0",