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
This commit is contained in:
fullsizemalt 2025-11-18 12:36:52 -08:00
parent 79edb05c20
commit 5bc3ca1832
17 changed files with 470 additions and 47 deletions

1
.gitignore vendored
View file

@ -6,6 +6,7 @@ dist/
build/
.cache/
.env*
!.env.example
# Next.js
web/.next/

5
mobile/.env.example Normal file
View file

@ -0,0 +1,5 @@
# API Configuration
EXPO_PUBLIC_API_URL=http://localhost:8000/api/v1
# Feature Flags
EXPO_PUBLIC_ENABLE_ANALYTICS=false

20
mobile/.gitignore vendored Normal file
View file

@ -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/

View file

@ -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"
}
}
}
}

View file

@ -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 <LoadingState message="Loading community..." />;
}
if (isError) {
return <ErrorState message="Failed to load community content" onRetry={refetch} />;
}
return (
<SafeAreaView style={styles.container}>
<ScrollView showsVerticalScrollIndicator={false}>
<ScrollView
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl refreshing={isLoading} onRefresh={onRefresh} />
}
>
<View style={styles.header}>
<Text style={styles.headerTitle}>Community</Text>
</View>
@ -143,6 +202,32 @@ export default function CommunityScreen() {
</View>
))}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Recent Activity</Text>
</View>
{!posts || posts.length === 0 ? (
<View style={styles.section}>
<EmptyState title="No Recent Activity" message="Be the first to post!" />
</View>
) : (
posts.map((post) => (
<View key={post.id} style={styles.section}>
<View style={styles.postCard}>
<View style={styles.postHeader}>
<Text style={styles.postAuthor}>{post.author}</Text>
<Text style={styles.postTime}>{post.timestamp}</Text>
</View>
<Text style={styles.postContent}>{post.content}</Text>
<View style={styles.postFooter}>
<Text style={styles.postStat}>{post.likes} Likes</Text>
<Text style={styles.postStat}>{post.comments} Comments</Text>
</View>
</View>
</View>
))
)}
<View style={[styles.section, { marginBottom: 40 }]}>
<View style={styles.communityCard}>
<Text style={styles.communityTitle}>Community Guidelines</Text>

View file

@ -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 <LoadingState message="Loading resources..." />;
}
if (isError) {
return <ErrorState message="Failed to load resources" onRetry={refetch} />;
}
return (
<SafeAreaView style={styles.container}>
<ScrollView showsVerticalScrollIndicator={false}>
<ScrollView
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl refreshing={isLoading} onRefresh={onRefresh} />
}
>
<View style={styles.header}>
<Text style={styles.headerTitle}>Resources</Text>
<Text style={styles.headerDescription}>
@ -112,8 +109,11 @@ export default function ResourcesScreen() {
</Text>
</View>
{resources.map((resource, index) => (
<View key={index} style={styles.section}>
{!resources || resources.length === 0 ? (
<EmptyState title="No Resources Found" message="Check back later for updates." />
) : (
resources.map((resource) => (
<View key={resource.id} style={styles.section}>
<View style={styles.resourceCard}>
<Text style={styles.resourceTitle}>{resource.title}</Text>
<Text style={styles.resourceDescription}>{resource.description}</Text>
@ -122,7 +122,8 @@ export default function ResourcesScreen() {
</TouchableOpacity>
</View>
</View>
))}
))
)}
<View style={[styles.section, { marginBottom: 40 }]}>
<Text style={styles.sectionTitle}>More Coming Soon</Text>

View file

@ -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 (
<>
<QueryClientProvider client={queryClient}>
<StatusBar style="dark" />
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
</Stack>
</>
</QueryClientProvider>
);
}

View file

@ -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
},
},
});

View file

@ -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 (
<View style={styles.container}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.message}>{message}</Text>
</View>
);
}
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',
},
});

View file

@ -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 (
<View style={styles.container}>
<Text style={styles.title}>Oops!</Text>
<Text style={styles.message}>{message}</Text>
{onRetry && (
<TouchableOpacity style={styles.button} onPress={onRetry}>
<Text style={styles.buttonText}>Try Again</Text>
</TouchableOpacity>
)}
</View>
);
}
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',
},
});

View file

@ -0,0 +1,28 @@
import { View, ActivityIndicator, StyleSheet, Text } from 'react-native';
interface LoadingStateProps {
message?: string;
}
export function LoadingState({ message = 'Loading...' }: LoadingStateProps) {
return (
<View style={styles.container}>
<ActivityIndicator size="large" color="#007AFF" />
{message && <Text style={styles.text}>{message}</Text>}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
text: {
marginTop: 12,
fontSize: 16,
color: '#666666',
},
});

28
mobile/eas.json Normal file
View file

@ -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": {}
}
}

View file

@ -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<Post[]> {
// 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,
});
}

View file

@ -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<Resource[]> {
// 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,
});
}

35
mobile/hooks/useUser.ts Normal file
View file

@ -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<UserProfile> {
// 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,
});
}

View file

@ -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",

View file

@ -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",