0% found this document useful (0 votes)
35 views92 pages

Prompt 1

The HudumaLink Project is a mobile application initially coded in React Native, designed for user authentication and service management. The project structure includes various components for authentication, tabs for navigation, and assets like images. The goal is to replicate the project using Java in Android Studio, maintaining the same architecture and functionality.

Uploaded by

denismfalla9
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
35 views92 pages

Prompt 1

The HudumaLink Project is a mobile application initially coded in React Native, designed for user authentication and service management. The project structure includes various components for authentication, tabs for navigation, and assets like images. The goal is to replicate the project using Java in Android Studio, maintaining the same architecture and functionality.

Uploaded by

denismfalla9
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 92

Here is my HudumaLink Project, it is coded in react native, so I want you to observe it carefully

and generate the same project but this time we are going to specify in Android using (Java)
specifically Android in Java, we are going to use android studio to finish the project, if you have
any question you may ask

The follows in the Project architecture,

HudumaLink App/
├── app/
(auth)
_layout.tsx
Login.tsx
Onboarding.tsx
otp.tsx
(tabs)
(home)
_layout.tsx
index.tsx
Notifications
_layout.tsx
index.tsx
Profile
_layout.tsx
index.tsx
Search
_layout.tsx
index.tsx
_layout.tsx
Provider
[id].tsx
Service
[id].tsx
_layout.tsx
+not-found.tsx
├── assets
Images
Adaptive-icon.png
Favicon.png
Icon.png
splash-icon.png
├── components/
categoryGrid.tsx
serviceCard.tsx
├── constants/
categories.ts
├── hooks/
Auth-content.tsx
services-context.ts
├── mocks/
services.ts
├── types/
index.ts
├── .gitignore
├── app.json/
├── bun.lock/
├── package.json/
└── tsconfig.json

The follows in the Project FILES

HudumaLink App/
├── app/
(auth)
_layout.tsx
import { Stack } from 'expo-router';
import { useAuth } from '@/hooks/auth-context';
import { useEffect } from 'react';
import { router } from 'expo-router';

export default function AuthLayout() {


const { isAuthenticated, isLoading } = useAuth();

useEffect(() => {
if (!isLoading && isAuthenticated) {
router.replace('/(tabs)');
}
}, [isAuthenticated, isLoading]);

if (isLoading) {
return null;
}

return (
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="onboarding" />
<Stack.Screen name="login" />
<Stack.Screen name="otp" />
</Stack>
);
}
Login.tsx
import React, { useState } from 'react';
import { View, Text, StyleSheet, TextInput, TouchableOpacity,
KeyboardAvoidingView, Platform, Alert } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { Phone, ArrowRight } from 'lucide-react-native';
import { router, useLocalSearchParams } from 'expo-router';
import { SafeAreaView } from 'react-native-safe-area-context';

export default function LoginScreen() {


const { role } = useLocalSearchParams<{ role: 'user' | 'provider' }>();
const [phone, setPhone] = useState('');
const [isLoading, setIsLoading] = useState(false);

const handleSendOTP = async () => {


if (!phone || phone.length < 10) {
Alert.alert('Invalid Phone', 'Please enter a valid phone number');
return;
}
setIsLoading(true);
// Simulate OTP sending
setTimeout(() => {
setIsLoading(false);
router.push({
pathname: '/(auth)/otp' as any,
params: { phone, role }
});
}, 1000);
};

return (
<LinearGradient
colors={['#1E40AF', '#0891B2']}
style={styles.container}
>
<SafeAreaView style={styles.safeArea}>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.keyboardView}
>
<View style={styles.content}>
<View style={styles.header}>
<Text style={styles.title}>Welcome to HudumaLink</Text>
<Text style={styles.subtitle}>
{role === 'provider'
? 'Sign in to manage your services'
: 'Sign in to find trusted services'}
</Text>
</View>

<View style={styles.form}>
<View style={styles.inputContainer}>
<View style={styles.inputIcon}>
<Phone size={20} color="#6B7280" />
</View>
<TextInput
style={styles.input}
placeholder="Enter phone number"
placeholderTextColor="#9CA3AF"
value={phone}
onChangeText={setPhone}
keyboardType="phone-pad"
autoComplete="tel"
testID="phone-input"
/>
</View>

<TouchableOpacity
style={[styles.button, isLoading && styles.buttonDisabled]}
onPress={handleSendOTP}
disabled={isLoading}
activeOpacity={0.8}
>
<Text style={styles.buttonText}>
{isLoading ? 'Sending...' : 'Send OTP'}
</Text>
<ArrowRight size={20} color="#fff" />
</TouchableOpacity>

<Text style={styles.terms}>
By continuing, you agree to our Terms of Service and Privacy Policy
</Text>
</View>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
</LinearGradient>
);
}

const styles = StyleSheet.create({


container: {
flex: 1,
},
safeArea: {
flex: 1,
},
keyboardView: {
flex: 1,
},
content: {
flex: 1,
paddingHorizontal: 24,
justifyContent: 'center',
},
header: {
marginBottom: 40,
},
title: {
fontSize: 28,
fontWeight: '700',
color: '#fff',
marginBottom: 8,
textAlign: 'center',
},
subtitle: {
fontSize: 16,
color: '#E0F2FE',
textAlign: 'center',
},
form: {
backgroundColor: '#fff',
borderRadius: 20,
padding: 24,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 5,
},
inputContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#F9FAFB',
borderRadius: 12,
borderWidth: 1,
borderColor: '#E5E7EB',
marginBottom: 20,
},
inputIcon: {
padding: 16,
},
input: {
flex: 1,
fontSize: 16,
color: '#111827',
paddingRight: 16,
paddingVertical: 16,
},
button: {
flexDirection: 'row',
backgroundColor: '#1E40AF',
borderRadius: 12,
padding: 16,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 16,
},
buttonDisabled: {
opacity: 0.6,
},
buttonText: {
fontSize: 16,
fontWeight: '600',
color: '#fff',
marginRight: 8,
},
terms: {
fontSize: 12,
color: '#6B7280',
textAlign: 'center',
lineHeight: 18,
},
});
Onboarding.tsx
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Image, Dimensions } from
'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { Users, Briefcase } from 'lucide-react-native';
import { router } from 'expo-router';
import { SafeAreaView } from 'react-native-safe-area-context';

const { width } = Dimensions.get('window');

export default function OnboardingScreen() {


const handleRoleSelect = (role: 'user' | 'provider') => {
router.push('./login');
};

return (
<LinearGradient
colors={['#1E40AF', '#0891B2']}
style={styles.container}
>
<SafeAreaView style={styles.safeArea}>
<View style={styles.content}>
<View style={styles.header}>
<Text style={styles.logo}>HudumaLink</Text>
<Text style={styles.tagline}>Connect with trusted service
providers</Text>
</View>

<View style={styles.illustration}>
<Image
source={{ uri: 'https://images.unsplash.com/photo-1556745757-
8d76bdb6984b?w=800' }}
style={styles.image}
/>
</View>

<View style={styles.roleSection}>
<Text style={styles.roleTitle}>How would you like to use
HudumaLink?</Text>

<TouchableOpacity
style={styles.roleCard}
onPress={() => handleRoleSelect('user')}
activeOpacity={0.8}
>
<View style={styles.roleIconContainer}>
<Users size={32} color="#1E40AF" />
</View>
<View style={styles.roleTextContainer}>
<Text style={styles.roleCardTitle}>Find Services</Text>
<Text style={styles.roleCardDescription}>
Browse and book services from verified providers
</Text>
</View>
</TouchableOpacity>

<TouchableOpacity
style={styles.roleCard}
onPress={() => handleRoleSelect('provider')}
activeOpacity={0.8}
>
<View style={styles.roleIconContainer}>
<Briefcase size={32} color="#0891B2" />
</View>
<View style={styles.roleTextContainer}>
<Text style={styles.roleCardTitle}>Offer Services</Text>
<Text style={styles.roleCardDescription}>
List your services and connect with customers
</Text>
</View>
</TouchableOpacity>
</View>

<Text style={styles.footer}>
Fast • Reliable • Transparent
</Text>
</View>
</SafeAreaView>
</LinearGradient>
);
}

const styles = StyleSheet.create({


container: {
flex: 1,
},
safeArea: {
flex: 1,
},
content: {
flex: 1,
paddingHorizontal: 24,
justifyContent: 'space-between',
},
header: {
alignItems: 'center',
marginTop: 40,
},
logo: {
fontSize: 36,
fontWeight: '800' as const,
color: '#fff',
marginBottom: 8,
},
tagline: {
fontSize: 16,
color: '#E0F2FE',
textAlign: 'center' as const,
},
illustration: {
alignItems: 'center',
marginVertical: 20,
},
image: {
width: width - 48,
height: 200,
borderRadius: 16,
},
roleSection: {
marginBottom: 40,
},
roleTitle: {
fontSize: 18,
fontWeight: '600' as const,
color: '#fff',
marginBottom: 20,
textAlign: 'center' as const,
},
roleCard: {
flexDirection: 'row' as const,
backgroundColor: '#fff',
borderRadius: 16,
padding: 20,
marginBottom: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.1,
shadowRadius: 8,
elevation: 5,
},
roleIconContainer: {
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: '#F0F9FF',
justifyContent: 'center' as const,
alignItems: 'center' as const,
marginRight: 16,
},
roleTextContainer: {
flex: 1,
justifyContent: 'center' as const,
},
roleCardTitle: {
fontSize: 18,
fontWeight: '700' as const,
color: '#111827',
marginBottom: 4,
},
roleCardDescription: {
fontSize: 14,
color: '#6B7280',
lineHeight: 20,
},
footer: {
fontSize: 14,
color: '#E0F2FE',
textAlign: 'center' as const,
marginBottom: 20,
},
});
otp.tsx
import React, { useState, useRef } from 'react';
import { View, Text, StyleSheet, TextInput, TouchableOpacity, Alert } from 'react-
native';
import { LinearGradient } from 'expo-linear-gradient';
import { Shield, ArrowRight } from 'lucide-react-native';
import { router, useLocalSearchParams } from 'expo-router';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useAuth } from '@/hooks/auth-context';

export default function OTPScreen() {


const { phone, role } = useLocalSearchParams<{ phone: string; role: 'user' |
'provider' }>();
const { login } = useAuth();
const [otp, setOtp] = useState(['', '', '', '', '', '']);
const [isLoading, setIsLoading] = useState(false);
const inputRefs = useRef<(TextInput | null)[]>([]);

const handleOtpChange = (value: string, index: number) => {


const newOtp = [...otp];
newOtp[index] = value;
setOtp(newOtp);

// Auto-focus next input


if (value && index < 5) {
inputRefs.current[index + 1]?.focus();
}
};

const handleVerifyOTP = async () => {


const otpCode = otp.join('');
if (otpCode.length !== 6) {
Alert.alert('Invalid OTP', 'Please enter the complete 6-digit OTP');
return;
}

setIsLoading(true);
try {
// In production, verify OTP with backend
// For demo, accept any 6-digit code
await login(phone, role as 'user' | 'provider');
router.replace('/(tabs)');
} catch (error) {
Alert.alert('Error', 'Failed to verify OTP. Please try again.');
} finally {
setIsLoading(false);
}
};

const handleResendOTP = () => {


Alert.alert('OTP Sent', 'A new OTP has been sent to your phone');
};

return (
<LinearGradient
colors={['#1E40AF', '#0891B2']}
style={styles.container}
>
<SafeAreaView style={styles.safeArea}>
<View style={styles.content}>
<View style={styles.header}>
<View style={styles.iconContainer}>
<Shield size={48} color="#1E40AF" />
</View>
<Text style={styles.title}>Verify Your Phone</Text>
<Text style={styles.subtitle}>
Enter the 6-digit code sent to{'\n'}{phone}
</Text>
</View>

<View style={styles.form}>
<View style={styles.otpContainer}>
{otp.map((digit, index) => (
<TextInput
key={index}
ref={(ref) => { inputRefs.current[index] = ref; }}
style={styles.otpInput}
value={digit}
onChangeText={(value) => handleOtpChange(value, index)}
keyboardType="number-pad"
maxLength={1}
testID={`otp-input-${index}`}
/>
))}
</View>

<TouchableOpacity
style={[styles.button, isLoading && styles.buttonDisabled]}
onPress={handleVerifyOTP}
disabled={isLoading}
activeOpacity={0.8}
>
<Text style={styles.buttonText}>
{isLoading ? 'Verifying...' : 'Verify & Continue'}
</Text>
<ArrowRight size={20} color="#fff" />
</TouchableOpacity>

<TouchableOpacity onPress={handleResendOTP}>
<Text style={styles.resendText}>
Didn't receive the code? <Text
style={styles.resendLink}>Resend</Text>
</Text>
</TouchableOpacity>
</View>
</View>
</SafeAreaView>
</LinearGradient>
);
}

const styles = StyleSheet.create({


container: {
flex: 1,
},
safeArea: {
flex: 1,
},
content: {
flex: 1,
paddingHorizontal: 24,
justifyContent: 'center',
},
header: {
alignItems: 'center',
marginBottom: 40,
},
iconContainer: {
width: 80,
height: 80,
borderRadius: 40,
backgroundColor: '#fff',
justifyContent: 'center',
alignItems: 'center',
marginBottom: 20,
},
title: {
fontSize: 28,
fontWeight: '700',
color: '#fff',
marginBottom: 8,
},
subtitle: {
fontSize: 16,
color: '#E0F2FE',
textAlign: 'center',
lineHeight: 22,
},
form: {
backgroundColor: '#fff',
borderRadius: 20,
padding: 24,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 5,
},
otpContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 30,
},
otpInput: {
width: 45,
height: 55,
borderWidth: 2,
borderColor: '#E5E7EB',
borderRadius: 12,
fontSize: 24,
fontWeight: '600',
textAlign: 'center',
color: '#111827',
backgroundColor: '#F9FAFB',
},
button: {
flexDirection: 'row',
backgroundColor: '#1E40AF',
borderRadius: 12,
padding: 16,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 20,
},
buttonDisabled: {
opacity: 0.6,
},
buttonText: {
fontSize: 16,
fontWeight: '600',
color: '#fff',
marginRight: 8,
},
resendText: {
fontSize: 14,
color: '#6B7280',
textAlign: 'center',
},
resendLink: {
color: '#1E40AF',
fontWeight: '600',
},
});
(tabs)
(home)
Layout
import { Stack } from 'expo-router';

export default function HomeLayout() {


return (
<Stack>
<Stack.Screen name="index" options={{ title: 'HudumaLink' }} />
</Stack>
);
}
Index
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet, ScrollView, TextInput, TouchableOpacity, FlatList,
RefreshControl } from 'react-native';
import { Search, Filter, MapPin } from 'lucide-react-native';
import { useAuth } from '@/hooks/auth-context';
import { useServices } from '@/hooks/services-context';
import ServiceCard from '@/components/ServiceCard';
import CategoryGrid from '@/components/CategoryGrid';
import { ServiceCategory } from '@/types';
import { router } from 'expo-router';

export default function HomeScreen() {


const { user, isAuthenticated, isLoading } = useAuth();

useEffect(() => {
if (!isLoading && !isAuthenticated) {
router.replace('/(auth)/onboarding' as any);
}
}, [isAuthenticated, isLoading]);
const { filteredServices, setFilters, filters } = useServices();
const [searchQuery, setSearchQuery] = useState('');
const [refreshing, setRefreshing] = useState(false);

const handleSearch = () => {


setFilters({ ...filters, searchQuery });
};

const handleCategorySelect = (category: ServiceCategory) => {


setFilters({
...filters,
category: filters.category === category ? undefined : category
});
};

const onRefresh = () => {


setRefreshing(true);
setTimeout(() => setRefreshing(false), 1000);
};

const ListHeader = () => (


<>
<View style={styles.header}>
<View>
<Text style={styles.greeting}>Hello, {user?.name || 'User'}!</Text>
<Text style={styles.subtitle}>What service do you need today?</Text>
</View>
<TouchableOpacity style={styles.locationButton}>
<MapPin size={16} color="#1E40AF" />
<Text style={styles.locationText}>CIVE Campus</Text>
</TouchableOpacity>
</View>

<View style={styles.searchContainer}>
<View style={styles.searchBar}>
<Search size={20} color="#6B7280" />
<TextInput
style={styles.searchInput}
placeholder="Search services..."
placeholderTextColor="#9CA3AF"
value={searchQuery}
onChangeText={setSearchQuery}
onSubmitEditing={handleSearch}
/>
</View>
<TouchableOpacity style={styles.filterButton}>
<Filter size={20} color="#fff" />
</TouchableOpacity>
</View>

<View style={styles.categoriesSection}>
<Text style={styles.sectionTitle}>Categories</Text>
<CategoryGrid
onSelectCategory={handleCategorySelect}
selectedCategory={filters.category}
/>
</View>

<View style={styles.servicesHeader}>
<Text style={styles.sectionTitle}>
{filters.category ? 'Filtered Services' : 'Popular Services'}
</Text>
<TouchableOpacity onPress={() => setFilters({})}>
<Text style={styles.clearButton}>Clear filters</Text>
</TouchableOpacity>
</View>
</>
);

if (isLoading) {
return (
<View style={[styles.container, { justifyContent: 'center', alignItems: 'center' }]}>
<Text>Loading...</Text>
</View>
);
}

if (!isAuthenticated) {
return null;
}

return (
<FlatList
style={styles.container}
data={filteredServices}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <ServiceCard service={item} />}
ListHeaderComponent={ListHeader}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
contentContainerStyle={styles.listContent}
ListEmptyComponent={
<View style={styles.emptyContainer}>
<Text style={styles.emptyText}>No services found</Text>
<Text style={styles.emptySubtext}>Try adjusting your filters</Text>
</View>
}
/>
);
}

const styles = StyleSheet.create({


container: {
flex: 1,
backgroundColor: '#F9FAFB',
},
listContent: {
paddingBottom: 20,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
paddingTop: 16,
paddingBottom: 12,
backgroundColor: '#fff',
},
greeting: {
fontSize: 24,
fontWeight: '700',
color: '#111827',
},
subtitle: {
fontSize: 14,
color: '#6B7280',
marginTop: 4,
},
locationButton: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#EFF6FF',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 20,
},
locationText: {
fontSize: 12,
color: '#1E40AF',
marginLeft: 4,
fontWeight: '600',
},
searchContainer: {
flexDirection: 'row',
paddingHorizontal: 16,
paddingVertical: 12,
backgroundColor: '#fff',
gap: 12,
},
searchBar: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#F3F4F6',
borderRadius: 12,
paddingHorizontal: 12,
},
searchInput: {
flex: 1,
fontSize: 16,
color: '#111827',
paddingVertical: 12,
paddingLeft: 8,
},
filterButton: {
backgroundColor: '#1E40AF',
borderRadius: 12,
padding: 12,
justifyContent: 'center',
alignItems: 'center',
},
categoriesSection: {
backgroundColor: '#fff',
paddingTop: 16,
paddingBottom: 8,
},
sectionTitle: {
fontSize: 18,
fontWeight: '600',
color: '#111827',
paddingHorizontal: 16,
marginBottom: 12,
},
servicesHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
paddingTop: 20,
paddingBottom: 12,
},
clearButton: {
fontSize: 14,
color: '#1E40AF',
fontWeight: '600',
},
emptyContainer: {
alignItems: 'center',
paddingVertical: 40,
},
emptyText: {
fontSize: 16,
fontWeight: '600',
color: '#6B7280',
},
emptySubtext: {
fontSize: 14,
color: '#9CA3AF',
marginTop: 4,
},
});
Notifications
Layout.tsx
import { Stack } from 'expo-router';

export default function NotificationsLayout() {


return (
<Stack>
<Stack.Screen name="index" options={{ title: 'Notifications' }} />
</Stack>
);
}
Index.tsx
mport React from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
import { Bell, Calendar, Star, CheckCircle, Info } from 'lucide-react-native';

interface Notification {
id: string;
type: 'booking' | 'review' | 'info' | 'promo';
title: string;
message: string;
time: string;
read: boolean;
}

const MOCK_NOTIFICATIONS: Notification[] = [


{
id: '1',
type: 'booking',
title: 'New Booking Request',
message: 'John Mwangi has requested your cleaning service for tomorrow at 2 PM',
time: '2 hours ago',
read: false,
},
{
id: '2',
type: 'review',
title: 'New Review',
message: 'Mary Kamau left a 5-star review for your service',
time: '5 hours ago',
read: false,
},
{
id: '3',
type: 'info',
title: 'Profile Verification',
message: 'Your profile has been verified! You now have a blue tick',
time: '1 day ago',
read: true,
},
{
id: '4',
type: 'promo',
title: 'Special Offer',
message: 'Get 20% off on featured listings this week',
time: '2 days ago',
read: true,
},
];

export default function NotificationsScreen() {


const getIcon = (type: string) => {
switch (type) {
case 'booking':
return <Calendar size={20} color="#1E40AF" />;
case 'review':
return <Star size={20} color="#F59E0B" />;
case 'info':
return <Info size={20} color="#0891B2" />;
case 'promo':
return <Bell size={20} color="#8B5CF6" />;
default:
return <Bell size={20} color="#6B7280" />;
}
};

return (
<ScrollView style={styles.container}>
<View style={styles.header}>
<Text style={styles.headerText}>You have 2 unread notifications</Text>
</View>

{MOCK_NOTIFICATIONS.map((notification) => (
<TouchableOpacity
key={notification.id}
style={[
styles.notificationCard,
!notification.read && styles.unreadCard
]}
>
<View style={styles.iconContainer}>
{getIcon(notification.type)}
</View>
<View style={styles.contentContainer}>
<View style={styles.titleRow}>
<Text style={[
styles.title,
!notification.read && styles.unreadTitle
]}>
{notification.title}
</Text>
{!notification.read && (
<View style={styles.unreadDot} />
)}
</View>
<Text style={styles.message}>{notification.message}</Text>
<Text style={styles.time}>{notification.time}</Text>
</View>
</TouchableOpacity>
))}
</ScrollView>
);
}

const styles = StyleSheet.create({


container: {
flex: 1,
backgroundColor: '#F9FAFB',
},
header: {
backgroundColor: '#EFF6FF',
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#DBEAFE',
},
headerText: {
fontSize: 14,
color: '#1E40AF',
fontWeight: '600',
},
notificationCard: {
flexDirection: 'row',
backgroundColor: '#fff',
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#E5E7EB',
},
unreadCard: {
backgroundColor: '#F0F9FF',
},
iconContainer: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#F3F4F6',
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
contentContainer: {
flex: 1,
},
titleRow: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 4,
},
title: {
fontSize: 16,
fontWeight: '600',
color: '#111827',
flex: 1,
},
unreadTitle: {
color: '#1E40AF',
},
unreadDot: {
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: '#1E40AF',
marginLeft: 8,
},
message: {
fontSize: 14,
color: '#6B7280',
lineHeight: 20,
marginBottom: 4,
},
time: {
fontSize: 12,
color: '#9CA3AF',
},
});
Profile
Layout.tsx
import { Stack } from 'expo-router';

export default function ProfileLayout() {


return (
<Stack>
<Stack.Screen name="index" options={{ title: 'Profile' }} />
</Stack>
);
}
Index.tsx
mport React from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Image, Alert } from
'react-native';
import { Edit2, LogOut, Phone, Mail, MapPin, Star, CheckCircle, Settings, HelpCircle }
from 'lucide-react-native';
import { useAuth } from '@/hooks/auth-context';
import { router } from 'expo-router';
import { Provider } from '@/types';

export default function ProfileScreen() {


const { user, logout, isLoading } = useAuth();

const handleLogout = () => {


Alert.alert(
'Logout',
'Are you sure you want to logout?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Logout',
style: 'destructive',
onPress: async () => {
await logout();
router.replace('/(auth)' as any);
}
}
]
);
};

if (isLoading) {
return (
<View style={[styles.container, { justifyContent: 'center', alignItems: 'center' }]}>
<Text>Loading...</Text>
</View>
);
}

if (!user) return null;

const isProvider = user.role === 'provider';


const provider = user as Provider;

return (
<ScrollView style={styles.container}>
<View style={styles.header}>
<View style={styles.profileSection}>
<Image
source={{ uri: user.profileImage || 'https://images.unsplash.com/photo-
1633332755192-727a05c4013d?w=400' }}
style={styles.profileImage}
/>
<TouchableOpacity style={styles.editButton}>
<Edit2 size={16} color="#fff" />
</TouchableOpacity>
</View>

<View style={styles.nameSection}>
<View style={styles.nameRow}>
<Text style={styles.name}>{user.name}</Text>
{isProvider && provider.isVerified && (
<CheckCircle size={20} color="#0891B2" />
)}
</View>
<Text style={styles.role}>
{isProvider ? 'Service Provider' : 'Customer'}
</Text>
</View>

{isProvider && (
<View style={styles.statsRow}>
<View style={styles.stat}>
<Star size={16} color="#F59E0B" />
<Text style={styles.statValue}>{provider.rating.toFixed(1)}</Text>
<Text style={styles.statLabel}>Rating</Text>
</View>
<View style={styles.stat}>
<Text style={styles.statValue}>{provider.totalReviews}</Text>
<Text style={styles.statLabel}>Reviews</Text>
</View>
<View style={styles.stat}>
<Text style={styles.statValue}>{provider.totalServices}</Text>
<Text style={styles.statLabel}>Services</Text>
</View>
</View>
)}
</View>

<View style={styles.section}>
<Text style={styles.sectionTitle}>Contact Information</Text>

<View style={styles.infoRow}>
<Phone size={18} color="#6B7280" />
<Text style={styles.infoText}>{user.phone}</Text>
</View>

<View style={styles.infoRow}>
<Mail size={18} color="#6B7280" />
<Text style={styles.infoText}>{user.email}</Text>
</View>

{isProvider && (
<View style={styles.infoRow}>
<MapPin size={18} color="#6B7280" />
<Text style={styles.infoText}>{provider.location}</Text>
</View>
)}
</View>

<View style={styles.section}>
<Text style={styles.sectionTitle}>Settings</Text>

<TouchableOpacity style={styles.menuItem}>
<Settings size={20} color="#6B7280" />
<Text style={styles.menuText}>Account Settings</Text>
</TouchableOpacity>

<TouchableOpacity style={styles.menuItem}>
<HelpCircle size={20} color="#6B7280" />
<Text style={styles.menuText}>Help & Support</Text>
</TouchableOpacity>

<TouchableOpacity style={styles.menuItem} onPress={handleLogout}>


<LogOut size={20} color="#EF4444" />
<Text style={[styles.menuText, styles.logoutText]}>Logout</Text>
</TouchableOpacity>
</View>
</ScrollView>
);
}

const styles = StyleSheet.create({


container: {
flex: 1,
backgroundColor: '#F9FAFB',
},
header: {
backgroundColor: '#fff',
paddingVertical: 24,
paddingHorizontal: 16,
borderBottomWidth: 1,
borderBottomColor: '#E5E7EB',
},
profileSection: {
alignItems: 'center',
marginBottom: 16,
},
profileImage: {
width: 100,
height: 100,
borderRadius: 50,
backgroundColor: '#F3F4F6',
},
editButton: {
position: 'absolute',
bottom: 0,
right: '35%',
backgroundColor: '#1E40AF',
borderRadius: 20,
padding: 8,
},
nameSection: {
alignItems: 'center',
marginBottom: 16,
},
nameRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
name: {
fontSize: 24,
fontWeight: '700',
color: '#111827',
},
role: {
fontSize: 14,
color: '#6B7280',
marginTop: 4,
},
statsRow: {
flexDirection: 'row',
justifyContent: 'space-around',
paddingTop: 16,
borderTopWidth: 1,
borderTopColor: '#E5E7EB',
},
stat: {
alignItems: 'center',
},
statValue: {
fontSize: 20,
fontWeight: '600',
color: '#111827',
marginTop: 4,
},
statLabel: {
fontSize: 12,
color: '#6B7280',
marginTop: 2,
},
section: {
backgroundColor: '#fff',
marginTop: 12,
paddingVertical: 16,
paddingHorizontal: 16,
},
sectionTitle: {
fontSize: 16,
fontWeight: '600',
color: '#111827',
marginBottom: 16,
},
infoRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 12,
gap: 12,
},
infoText: {
fontSize: 14,
color: '#374151',
},
menuItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 14,
gap: 12,
},
menuText: {
fontSize: 15,
color: '#374151',
},
logoutText: {
color: '#EF4444',
},
});
Search
Layout.tsx
import { Stack } from 'expo-router';

export default function SearchLayout() {


return (
<Stack>
<Stack.Screen name="index" options={{ title: 'Search Services' }} />
</Stack>
);
}

Index.tsx
mport React, { useState } from 'react';
import { View, Text, StyleSheet, TextInput, ScrollView, TouchableOpacity } from 'react-
native';
import { Search as SearchIcon, SlidersHorizontal, X } from 'lucide-react-native';
import { useServices } from '@/hooks/services-context';
import ServiceCard from '@/components/ServiceCard';
import { ServiceCategory } from '@/types';
import { CATEGORIES } from '@/constants/categories';

export default function SearchScreen() {


const { services, setFilters, filters, filteredServices } = useServices();
const [searchQuery, setSearchQuery] = useState('');
const [showFilters, setShowFilters] = useState(false);
const [priceRange, setPriceRange] = useState({ min: '', max: '' });
const [selectedCategory, setSelectedCategory] = useState<ServiceCategory |
undefined>();
const [minRating, setMinRating] = useState<number | undefined>();

const handleSearch = () => {


setFilters({
...filters,
searchQuery,
category: selectedCategory,
minPrice: priceRange.min ? parseInt(priceRange.min) : undefined,
maxPrice: priceRange.max ? parseInt(priceRange.max) : undefined,
minRating,
});
};

const clearFilters = () => {


setSearchQuery('');
setPriceRange({ min: '', max: '' });
setSelectedCategory(undefined);
setMinRating(undefined);
setFilters({});
};

return (
<View style={styles.container}>
<View style={styles.searchHeader}>
<View style={styles.searchBar}>
<SearchIcon size={20} color="#6B7280" />
<TextInput
style={styles.searchInput}
placeholder="Search services, providers..."
placeholderTextColor="#9CA3AF"
value={searchQuery}
onChangeText={setSearchQuery}
onSubmitEditing={handleSearch}
/>
</View>
<TouchableOpacity
style={styles.filterToggle}
onPress={() => setShowFilters(!showFilters)}
>
<SlidersHorizontal size={20} color="#1E40AF" />
</TouchableOpacity>
</View>

{showFilters && (
<View style={styles.filtersContainer}>
<View style={styles.filterHeader}>
<Text style={styles.filterTitle}>Filters</Text>
<TouchableOpacity onPress={clearFilters}>
<Text style={styles.clearText}>Clear all</Text>
</TouchableOpacity>
</View>

<View style={styles.filterSection}>
<Text style={styles.filterLabel}>Category</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<View style={styles.categoryChips}>
{CATEGORIES.map((cat) => (
<TouchableOpacity
key={cat.id}
style={[
styles.chip,
selectedCategory === cat.id && styles.chipSelected
]}
onPress={() => setSelectedCategory(
selectedCategory === cat.id ? undefined : cat.id
)}
>
<Text style={[
styles.chipText,
selectedCategory === cat.id && styles.chipTextSelected
]}>
{cat.name}
</Text>
</TouchableOpacity>
))}
</View>
</ScrollView>
</View>

<View style={styles.filterSection}>
<Text style={styles.filterLabel}>Price Range (KSh)</Text>
<View style={styles.priceInputs}>
<TextInput
style={styles.priceInput}
placeholder="Min"
placeholderTextColor="#9CA3AF"
value={priceRange.min}
onChangeText={(text) => setPriceRange({ ...priceRange, min: text })}
keyboardType="numeric"
/>
<Text style={styles.priceSeparator}>-</Text>
<TextInput
style={styles.priceInput}
placeholder="Max"
placeholderTextColor="#9CA3AF"
value={priceRange.max}
onChangeText={(text) => setPriceRange({ ...priceRange, max: text })}
keyboardType="numeric"
/>
</View>
</View>

<View style={styles.filterSection}>
<Text style={styles.filterLabel}>Minimum Rating</Text>
<View style={styles.ratingOptions}>
{[4, 3, 2].map((rating) => (
<TouchableOpacity
key={rating}
style={[
styles.ratingChip,
minRating === rating && styles.chipSelected
]}
onPress={() => setMinRating(minRating === rating ? undefined : rating)}
>
<Text style={[
styles.chipText,
minRating === rating && styles.chipTextSelected
]}>
{rating}+ ⭐
</Text>
</TouchableOpacity>
))}
</View>
</View>

<TouchableOpacity style={styles.applyButton} onPress={handleSearch}>


<Text style={styles.applyButtonText}>Apply Filters</Text>
</TouchableOpacity>
</View>
)}

<ScrollView style={styles.results} showsVerticalScrollIndicator={false}>


<Text style={styles.resultsCount}>
{filteredServices.length} services found
</Text>
{filteredServices.map((service) => (
<ServiceCard key={service.id} service={service} />
))}
</ScrollView>
</View>
);
}

const styles = StyleSheet.create({


container: {
flex: 1,
backgroundColor: '#F9FAFB',
},
searchHeader: {
flexDirection: 'row',
paddingHorizontal: 16,
paddingVertical: 12,
backgroundColor: '#fff',
gap: 12,
borderBottomWidth: 1,
borderBottomColor: '#E5E7EB',
},
searchBar: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#F3F4F6',
borderRadius: 12,
paddingHorizontal: 12,
},
searchInput: {
flex: 1,
fontSize: 16,
color: '#111827',
paddingVertical: 12,
paddingLeft: 8,
},
filterToggle: {
backgroundColor: '#EFF6FF',
borderRadius: 12,
padding: 12,
justifyContent: 'center',
alignItems: 'center',
},
filtersContainer: {
backgroundColor: '#fff',
paddingHorizontal: 16,
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: '#E5E7EB',
},
filterHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
},
filterTitle: {
fontSize: 18,
fontWeight: '600',
color: '#111827',
},
clearText: {
fontSize: 14,
color: '#1E40AF',
fontWeight: '600',
},
filterSection: {
marginBottom: 20,
},
filterLabel: {
fontSize: 14,
fontWeight: '600',
color: '#374151',
marginBottom: 8,
},
categoryChips: {
flexDirection: 'row',
gap: 8,
},
chip: {
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 20,
backgroundColor: '#F3F4F6',
borderWidth: 1,
borderColor: '#E5E7EB',
},
chipSelected: {
backgroundColor: '#EFF6FF',
borderColor: '#1E40AF',
},
chipText: {
fontSize: 14,
color: '#6B7280',
},
chipTextSelected: {
color: '#1E40AF',
fontWeight: '600',
},
priceInputs: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
priceInput: {
flex: 1,
backgroundColor: '#F3F4F6',
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
fontSize: 16,
color: '#111827',
},
priceSeparator: {
fontSize: 16,
color: '#6B7280',
},
ratingOptions: {
flexDirection: 'row',
gap: 8,
},
ratingChip: {
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 20,
backgroundColor: '#F3F4F6',
borderWidth: 1,
borderColor: '#E5E7EB',
},
applyButton: {
backgroundColor: '#1E40AF',
borderRadius: 12,
paddingVertical: 14,
alignItems: 'center',
},
applyButtonText: {
fontSize: 16,
fontWeight: '600',
color: '#fff',
},
results: {
flex: 1,
},
resultsCount: {
fontSize: 14,
color: '#6B7280',
paddingHorizontal: 16,
paddingVertical: 12,
},
});

Services
Layout.tsx
import { Stack } from 'expo-router';

export default function ServicesLayout() {


return (
<Stack>
<Stack.Screen name="index" options={{ title: 'My Services' }} />
</Stack>
);
}
Index.tsx
mport React, { useState } from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Switch, Alert } from
'react-native';
import { Plus, Edit2, Trash2, Eye, EyeOff } from 'lucide-react-native';
import { useAuth } from '@/hooks/auth-context';
import { useServices } from '@/hooks/services-context';
import { Provider } from '@/types';

export default function MyServicesScreen() {


const { user, toggleAvailability } = useAuth();
const { getProviderServices } = useServices();

if (!user || user.role !== 'provider') {


return (
<View style={styles.container}>
<Text style={styles.errorText}>This page is only for service providers</Text>
</View>
);
}

const provider = user as Provider;


const myServices = getProviderServices(provider.id);

return (
<ScrollView style={styles.container}>
<View style={styles.statusCard}>
<View style={styles.statusHeader}>
<Text style={styles.statusTitle}>Availability Status</Text>
<Switch
value={provider.isAvailable}
onValueChange={toggleAvailability}
trackColor={{ false: '#E5E7EB', true: '#86EFAC' }}
thumbColor={provider.isAvailable ? '#22C55E' : '#9CA3AF'}
/>
</View>
<View style={styles.statusInfo}>
{provider.isAvailable ? (
<>
<View style={styles.statusBadge}>
<Eye size={16} color="#22C55E" />
<Text style={styles.statusText}>You are visible to customers</Text>
</View>
</>
):(
<>
<View style={styles.statusBadge}>
<EyeOff size={16} color="#EF4444" />
<Text style={styles.statusText}>You are hidden from search</Text>
</View>
</>
)}
</View>
</View>

<View style={styles.statsContainer}>
<View style={styles.statCard}>
<Text style={styles.statValue}>{myServices.length}</Text>
<Text style={styles.statLabel}>Services</Text>
</View>
<View style={styles.statCard}>
<Text style={styles.statValue}>{provider.totalReviews}</Text>
<Text style={styles.statLabel}>Reviews</Text>
</View>
<View style={styles.statCard}>
<Text style={styles.statValue}>{provider.rating.toFixed(1)}</Text>
<Text style={styles.statLabel}>Rating</Text>
</View>
</View>

<View style={styles.servicesSection}>
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>My Services</Text>
<TouchableOpacity style={styles.addButton}>
<Plus size={20} color="#fff" />
<Text style={styles.addButtonText}>Add Service</Text>
</TouchableOpacity>
</View>

{myServices.length === 0 ? (
<View style={styles.emptyState}>
<Text style={styles.emptyText}>No services added yet</Text>
<Text style={styles.emptySubtext}>Add your first service to start receiving
bookings</Text>
</View>
):(
myServices.map((service) => (
<View key={service.id} style={styles.serviceCard}>
<View style={styles.serviceHeader}>
<Text style={styles.serviceTitle}>{service.title}</Text>
<View style={styles.serviceActions}>
<TouchableOpacity style={styles.actionButton}>
<Edit2 size={18} color="#6B7280" />
</TouchableOpacity>
<TouchableOpacity
style={styles.actionButton}
onPress={() => Alert.alert('Delete Service', 'Are you sure?')}
>
<Trash2 size={18} color="#EF4444" />
</TouchableOpacity>
</View>
</View>
<Text style={styles.serviceCategory}>{service.category}</Text>
<Text style={styles.serviceDescription} numberOfLines={2}>
{service.description}
</Text>
<View style={styles.serviceFooter}>
<Text style={styles.servicePrice}>
KSh {service.price}/{service.priceUnit}
</Text>
<View style={styles.serviceStats}>
<Text style={styles.serviceStat}>⭐
{service.rating.toFixed(1)}</Text>
<Text style={styles.serviceStat}>👥 {service.totalBookings} bookings</Text>
</View>
</View>
</View>
))
)}
</View>
</ScrollView>
);
}

const styles = StyleSheet.create({


container: {
flex: 1,
backgroundColor: '#F9FAFB',
},
errorText: {
fontSize: 16,
color: '#6B7280',
textAlign: 'center',
marginTop: 40,
},
statusCard: {
backgroundColor: '#fff',
margin: 16,
padding: 16,
borderRadius: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.05,
shadowRadius: 4,
elevation: 2,
},
statusHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
statusTitle: {
fontSize: 18,
fontWeight: '600',
color: '#111827',
},
statusInfo: {
flexDirection: 'row',
alignItems: 'center',
},
statusBadge: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
statusText: {
fontSize: 14,
color: '#6B7280',
},
statsContainer: {
flexDirection: 'row',
paddingHorizontal: 16,
gap: 12,
marginBottom: 20,
},
statCard: {
flex: 1,
backgroundColor: '#fff',
padding: 16,
borderRadius: 12,
alignItems: 'center',
},
statValue: {
fontSize: 24,
fontWeight: '700',
color: '#1E40AF',
},
statLabel: {
fontSize: 12,
color: '#6B7280',
marginTop: 4,
},
servicesSection: {
paddingHorizontal: 16,
paddingBottom: 20,
},
sectionHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
},
sectionTitle: {
fontSize: 20,
fontWeight: '600',
color: '#111827',
},
addButton: {
flexDirection: 'row',
backgroundColor: '#1E40AF',
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 8,
alignItems: 'center',
gap: 6,
},
addButtonText: {
color: '#fff',
fontSize: 14,
fontWeight: '600',
},
emptyState: {
backgroundColor: '#fff',
padding: 32,
borderRadius: 12,
alignItems: 'center',
},
emptyText: {
fontSize: 16,
fontWeight: '600',
color: '#6B7280',
marginBottom: 8,
},
emptySubtext: {
fontSize: 14,
color: '#9CA3AF',
textAlign: 'center',
},
serviceCard: {
backgroundColor: '#fff',
padding: 16,
borderRadius: 12,
marginBottom: 12,
},
serviceHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 8,
},
serviceTitle: {
fontSize: 16,
fontWeight: '600',
color: '#111827',
flex: 1,
},
serviceActions: {
flexDirection: 'row',
gap: 8,
},
actionButton: {
padding: 4,
},
serviceCategory: {
fontSize: 12,
color: '#1E40AF',
textTransform: 'uppercase',
marginBottom: 8,
},
serviceDescription: {
fontSize: 14,
color: '#6B7280',
lineHeight: 20,
marginBottom: 12,
},
serviceFooter: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
servicePrice: {
fontSize: 16,
fontWeight: '600',
color: '#1E40AF',
},
serviceStats: {
flexDirection: 'row',
gap: 12,
},
serviceStat: {
fontSize: 12,
color: '#6B7280',
},
});
_layout.tsx
import { Tabs } from "expo-router";
import { Home, Search, User, Briefcase, Bell } from "lucide-react-native";
import React from "react";
import { useAuth } from "@/hooks/auth-context";

export default function TabLayout() {


const { user } = useAuth();
const isProvider = user?.role === 'provider';

return (
<Tabs
screenOptions={{
tabBarActiveTintColor: '#1E40AF',
tabBarInactiveTintColor: '#6B7280',
headerShown: false,
tabBarStyle: {
backgroundColor: '#fff',
borderTopWidth: 1,
borderTopColor: '#E5E7EB',
paddingBottom: 5,
paddingTop: 5,
height: 60,
},
}}
>
<Tabs.Screen
name="(home)"
options={{
title: "Home",
tabBarIcon: ({ color }) => <Home size={24} color={color} />,
}}
/>
<Tabs.Screen
name="search"
options={{
title: "Search",
tabBarIcon: ({ color }) => <Search size={24} color={color} />,
}}
/>
{isProvider && (
<Tabs.Screen
name="services"
options={{
title: "My Services",
tabBarIcon: ({ color }) => <Briefcase size={24} color={color} />,
}}
/>
)}
<Tabs.Screen
name="notifications"
options={{
title: "Notifications",
tabBarIcon: ({ color }) => <Bell size={24} color={color} />,
}}
/>
<Tabs.Screen
name="profile"
options={{
title: "Profile",
tabBarIcon: ({ color }) => <User size={24} color={color} />,
}}
/>
</Tabs>
);
}

Provider
Id.tsx
import React from 'react';
import { View, Text, StyleSheet, ScrollView, Image, TouchableOpacity, Linking, Alert }
from 'react-native';
import { useLocalSearchParams, router } from 'expo-router';
import { Star, MapPin, Phone, MessageCircle, CheckCircle, Calendar, Briefcase } from
'lucide-react-native';
import { useServices } from '@/hooks/services-context';
import { MOCK_PROVIDERS } from '@/mocks/services';
import ServiceCard from '@/components/ServiceCard';

export default function ProviderProfileScreen() {


const { id } = useLocalSearchParams<{ id: string }>();
const { getProviderServices } = useServices();

const provider = MOCK_PROVIDERS.find(p => p.id === id);


const services = provider ? getProviderServices(provider.id) : [];

if (!provider) {
return (
<View style={styles.container}>
<Text>Provider not found</Text>
</View>
);
}

const handleWhatsApp = () => {


const message = `Hi ${provider.name}, I found your profile on HudumaLink`;
const url = `whatsapp://send?phone=${provider.whatsappNumber}&text=$
{encodeURIComponent(message)}`;
Linking.openURL(url).catch(() => {
Alert.alert('Error', 'WhatsApp is not installed on your device');
});
};

const handleCall = () => {


Linking.openURL(`tel:${provider.phone}`);
};

return (
<ScrollView style={styles.container}>
<View style={styles.header}>
<Image source={{ uri: provider.profileImage }} style={styles.profileImage} />

<View style={styles.nameSection}>
<View style={styles.nameRow}>
<Text style={styles.name}>{provider.name}</Text>
{provider.isVerified && <CheckCircle size={24} color="#0891B2" />}
</View>
{provider.isAvailable ? (
<View style={styles.availableBadge}>
<Text style={styles.availableText}>Available Now</Text>
</View>
):(
<View style={styles.busyBadge}>
<Text style={styles.busyText}>Currently Busy</Text>
</View>
)}
</View>

<Text style={styles.bio}>{provider.bio}</Text>

<View style={styles.locationRow}>
<MapPin size={16} color="#6B7280" />
<Text style={styles.location}>{provider.location}</Text>
</View>

<View style={styles.statsContainer}>
<View style={styles.statCard}>
<Star size={20} color="#F59E0B" />
<Text style={styles.statValue}>{provider.rating.toFixed(1)}</Text>
<Text style={styles.statLabel}>Rating</Text>
</View>
<View style={styles.statCard}>
<MessageCircle size={20} color="#1E40AF" />
<Text style={styles.statValue}>{provider.totalReviews}</Text>
<Text style={styles.statLabel}>Reviews</Text>
</View>
<View style={styles.statCard}>
<Briefcase size={20} color="#0891B2" />
<Text style={styles.statValue}>{provider.totalServices}</Text>
<Text style={styles.statLabel}>Services</Text>
</View>
<View style={styles.statCard}>
<Calendar size={20} color="#8B5CF6" />
<Text style={styles.statValue}>{new
Date(provider.joinedDate).getFullYear()}</Text>
<Text style={styles.statLabel}>Joined</Text>
</View>
</View>

<View style={styles.contactButtons}>
<TouchableOpacity style={styles.contactButton} onPress={handleCall}>
<Phone size={20} color="#fff" />
<Text style={styles.contactButtonText}>Call</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.contactButton, styles.whatsappButton]}
onPress={handleWhatsApp}>
<MessageCircle size={20} color="#fff" />
<Text style={styles.contactButtonText}>WhatsApp</Text>
</TouchableOpacity>
</View>
</View>

<View style={styles.servicesSection}>
<Text style={styles.sectionTitle}>Services ({services.length})</Text>
{services.length === 0 ? (
<Text style={styles.noServices}>No services listed yet</Text>
):(
services.map((service) => (
<ServiceCard key={service.id} service={service} />
))
)}
</View>
</ScrollView>
);
}

const styles = StyleSheet.create({


container: {
flex: 1,
backgroundColor: '#F9FAFB',
},
header: {
backgroundColor: '#fff',
padding: 20,
alignItems: 'center',
borderBottomWidth: 1,
borderBottomColor: '#E5E7EB',
},
profileImage: {
width: 120,
height: 120,
borderRadius: 60,
marginBottom: 16,
backgroundColor: '#F3F4F6',
},
nameSection: {
alignItems: 'center',
marginBottom: 12,
},
nameRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
marginBottom: 8,
},
name: {
fontSize: 24,
fontWeight: '700',
color: '#111827',
},
availableBadge: {
backgroundColor: '#D1FAE5',
paddingHorizontal: 16,
paddingVertical: 6,
borderRadius: 20,
},
availableText: {
color: '#065F46',
fontSize: 14,
fontWeight: '600',
},
busyBadge: {
backgroundColor: '#FEE2E2',
paddingHorizontal: 16,
paddingVertical: 6,
borderRadius: 20,
},
busyText: {
color: '#991B1B',
fontSize: 14,
fontWeight: '600',
},
bio: {
fontSize: 14,
color: '#6B7280',
textAlign: 'center',
lineHeight: 20,
marginBottom: 12,
paddingHorizontal: 20,
},
locationRow: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 20,
},
location: {
fontSize: 14,
color: '#6B7280',
marginLeft: 6,
},
statsContainer: {
flexDirection: 'row',
justifyContent: 'space-around',
width: '100%',
marginBottom: 20,
},
statCard: {
alignItems: 'center',
},
statValue: {
fontSize: 18,
fontWeight: '600',
color: '#111827',
marginTop: 6,
},
statLabel: {
fontSize: 12,
color: '#6B7280',
marginTop: 2,
},
contactButtons: {
flexDirection: 'row',
gap: 12,
width: '100%',
},
contactButton: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#1E40AF',
paddingVertical: 12,
borderRadius: 12,
gap: 8,
},
whatsappButton: {
backgroundColor: '#25D366',
},
contactButtonText: {
fontSize: 14,
fontWeight: '600',
color: '#fff',
},
servicesSection: {
paddingVertical: 20,
},
sectionTitle: {
fontSize: 20,
fontWeight: '600',
color: '#111827',
paddingHorizontal: 16,
marginBottom: 16,
},
noServices: {
fontSize: 14,
color: '#9CA3AF',
textAlign: 'center',
paddingVertical: 20,
},
});
Service
[id].tsx
mport React, { useState } from 'react';
import { View, Text, StyleSheet, ScrollView, Image, TouchableOpacity, Linking, Alert }
from 'react-native';
import { useLocalSearchParams, router } from 'expo-router';
import { Star, MapPin, Phone, MessageCircle, CheckCircle, Clock, Users } from 'lucide-
react-native';
import { useServices } from '@/hooks/services-context';
import { useAuth } from '@/hooks/auth-context';
import { MOCK_PROVIDERS } from '@/mocks/services';

export default function ServiceDetailScreen() {


const { id } = useLocalSearchParams<{ id: string }>();
const { services, getServiceReviews, createBooking } = useServices();
const { user, isLoading: authLoading } = useAuth();
const [selectedImage, setSelectedImage] = useState(0);

const service = services.find(s => s.id === id);


const provider = MOCK_PROVIDERS.find(p => p.id === service?.providerId);
const reviews = service ? getServiceReviews(service.id) : [];

if (!service || !provider) {
return (
<View style={styles.container}>
<Text>Service not found</Text>
</View>
);
}

const handleWhatsApp = () => {


const message = `Hi, I'm interested in your service: ${service.title}`;
const url = `whatsapp://send?phone=${provider.whatsappNumber}&text=$
{encodeURIComponent(message)}`;
Linking.openURL(url).catch(() => {
Alert.alert('Error', 'WhatsApp is not installed on your device');
});
};

const handleCall = () => {


Linking.openURL(`tel:${provider.phone}`);
};
const handleBook = async () => {
if (authLoading) return;

if (!user) {
router.push('/(auth)' as any);
return;
}

await createBooking({
serviceId: service.id,
serviceName: service.title,
providerId: provider.id,
providerName: provider.name,
userId: user.id,
userName: user.name,
date: new Date().toISOString().split('T')[0],
time: '14:00',
});

Alert.alert('Success', 'Booking request sent to provider');


};

return (
<ScrollView style={styles.container}>
<ScrollView horizontal pagingEnabled showsHorizontalScrollIndicator={false}>
{service.images.map((image, index) => (
<Image key={index} source={{ uri: image }} style={styles.image} />
))}
</ScrollView>

<View style={styles.content}>
<View style={styles.header}>
<View style={styles.titleSection}>
<Text style={styles.title}>{service.title}</Text>
<View style={styles.categoryBadge}>
<Text style={styles.categoryText}>{service.category}</Text>
</View>
</View>

<View style={styles.priceSection}>
<Text style={styles.price}>KSh {service.price}</Text>
<Text style={styles.priceUnit}>/{service.priceUnit}</Text>
</View>
</View>
<TouchableOpacity
style={styles.providerCard}
onPress={() => router.push(`/provider/${provider.id}` as any)}
>
<Image source={{ uri: provider.profileImage }} style={styles.providerImage} />
<View style={styles.providerInfo}>
<View style={styles.providerName}>
<Text style={styles.providerNameText}>{provider.name}</Text>
{provider.isVerified && <CheckCircle size={16} color="#0891B2" />}
</View>
<View style={styles.providerMeta}>
<MapPin size={14} color="#6B7280" />
<Text style={styles.providerLocation}>{provider.location}</Text>
</View>
<View style={styles.providerStats}>
<Star size={14} color="#F59E0B" fill="#F59E0B" />
<Text style={styles.providerRating}>{provider.rating.toFixed(1)}</Text>
<Text style={styles.dot}>•</Text>
<Text style={styles.providerReviews}>{provider.totalReviews} reviews</Text>
</View>
</View>
{provider.isAvailable ? (
<View style={styles.availableBadge}>
<Text style={styles.availableText}>Available</Text>
</View>
):(
<View style={styles.busyBadge}>
<Text style={styles.busyText}>Busy</Text>
</View>
)}
</TouchableOpacity>

<View style={styles.section}>
<Text style={styles.sectionTitle}>Description</Text>
<Text style={styles.description}>{service.description}</Text>
</View>

<View style={styles.section}>
<Text style={styles.sectionTitle}>Service Stats</Text>
<View style={styles.statsGrid}>
<View style={styles.statCard}>
<Star size={20} color="#F59E0B" />
<Text style={styles.statValue}>{service.rating.toFixed(1)}</Text>
<Text style={styles.statLabel}>Rating</Text>
</View>
<View style={styles.statCard}>
<Users size={20} color="#1E40AF" />
<Text style={styles.statValue}>{service.totalBookings}</Text>
<Text style={styles.statLabel}>Bookings</Text>
</View>
<View style={styles.statCard}>
<Clock size={20} color="#0891B2" />
<Text style={styles.statValue}>Fast</Text>
<Text style={styles.statLabel}>Response</Text>
</View>
</View>
</View>

<View style={styles.section}>
<Text style={styles.sectionTitle}>Tags</Text>
<View style={styles.tags}>
{service.tags.map((tag, index) => (
<View key={index} style={styles.tag}>
<Text style={styles.tagText}>{tag}</Text>
</View>
))}
</View>
</View>

<View style={styles.section}>
<Text style={styles.sectionTitle}>Reviews ({reviews.length})</Text>
{reviews.length === 0 ? (
<Text style={styles.noReviews}>No reviews yet</Text>
):(
reviews.map((review) => (
<View key={review.id} style={styles.reviewCard}>
<View style={styles.reviewHeader}>
<Image source={{ uri: review.userImage }} style={styles.reviewerImage} />
<View style={styles.reviewerInfo}>
<Text style={styles.reviewerName}>{review.userName}</Text>
<View style={styles.reviewRating}>
{[...Array(5)].map((_, i) => (
<Star
key={i}
size={12}
color="#F59E0B"
fill={i < review.rating ? "#F59E0B" : "transparent"}
/>
))}
<Text style={styles.reviewDate}>{review.createdAt}</Text>
</View>
</View>
</View>
<Text style={styles.reviewComment}>{review.comment}</Text>
</View>
))
)}
</View>
</View>

<View style={styles.footer}>
<View style={styles.contactButtons}>
<TouchableOpacity style={styles.contactButton} onPress={handleCall}>
<Phone size={20} color="#1E40AF" />
<Text style={styles.contactButtonText}>Call</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.contactButton} onPress={handleWhatsApp}>
<MessageCircle size={20} color="#25D366" />
<Text style={styles.contactButtonText}>WhatsApp</Text>
</TouchableOpacity>
</View>
<TouchableOpacity
style={[styles.bookButton, !provider.isAvailable && styles.bookButtonDisabled]}
onPress={handleBook}
disabled={!provider.isAvailable}
>
<Text style={styles.bookButtonText}>
{provider.isAvailable ? 'Book Service' : 'Provider Busy'}
</Text>
</TouchableOpacity>
</View>
</ScrollView>
);
}

const styles = StyleSheet.create({


container: {
flex: 1,
backgroundColor: '#fff',
},
image: {
width: 375,
height: 250,
backgroundColor: '#F3F4F6',
},
content: {
padding: 16,
},
header: {
marginBottom: 20,
},
titleSection: {
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'space-between',
marginBottom: 8,
},
title: {
fontSize: 24,
fontWeight: '700',
color: '#111827',
flex: 1,
},
categoryBadge: {
backgroundColor: '#EFF6FF',
paddingHorizontal: 12,
paddingVertical: 4,
borderRadius: 12,
},
categoryText: {
fontSize: 12,
color: '#1E40AF',
fontWeight: '600',
textTransform: 'capitalize',
},
priceSection: {
flexDirection: 'row',
alignItems: 'baseline',
},
price: {
fontSize: 28,
fontWeight: '700',
color: '#1E40AF',
},
priceUnit: {
fontSize: 16,
color: '#6B7280',
marginLeft: 4,
},
providerCard: {
flexDirection: 'row',
backgroundColor: '#F9FAFB',
padding: 12,
borderRadius: 12,
marginBottom: 20,
},
providerImage: {
width: 50,
height: 50,
borderRadius: 25,
marginRight: 12,
},
providerInfo: {
flex: 1,
},
providerName: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
},
providerNameText: {
fontSize: 16,
fontWeight: '600',
color: '#111827',
},
providerMeta: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 4,
},
providerLocation: {
fontSize: 12,
color: '#6B7280',
marginLeft: 4,
},
providerStats: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 4,
},
providerRating: {
fontSize: 12,
fontWeight: '600',
color: '#111827',
marginLeft: 4,
},
dot: {
fontSize: 12,
color: '#6B7280',
marginHorizontal: 6,
},
providerReviews: {
fontSize: 12,
color: '#6B7280',
},
availableBadge: {
backgroundColor: '#D1FAE5',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 6,
alignSelf: 'center',
},
availableText: {
color: '#065F46',
fontSize: 12,
fontWeight: '600',
},
busyBadge: {
backgroundColor: '#FEE2E2',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 6,
alignSelf: 'center',
},
busyText: {
color: '#991B1B',
fontSize: 12,
fontWeight: '600',
},
section: {
marginBottom: 24,
},
sectionTitle: {
fontSize: 18,
fontWeight: '600',
color: '#111827',
marginBottom: 12,
},
description: {
fontSize: 14,
color: '#6B7280',
lineHeight: 22,
},
statsGrid: {
flexDirection: 'row',
gap: 12,
},
statCard: {
flex: 1,
backgroundColor: '#F9FAFB',
padding: 16,
borderRadius: 12,
alignItems: 'center',
},
statValue: {
fontSize: 18,
fontWeight: '600',
color: '#111827',
marginTop: 8,
},
statLabel: {
fontSize: 12,
color: '#6B7280',
marginTop: 4,
},
tags: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
},
tag: {
backgroundColor: '#F3F4F6',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 16,
},
tagText: {
fontSize: 12,
color: '#6B7280',
},
noReviews: {
fontSize: 14,
color: '#9CA3AF',
fontStyle: 'italic',
},
reviewCard: {
backgroundColor: '#F9FAFB',
padding: 12,
borderRadius: 8,
marginBottom: 12,
},
reviewHeader: {
flexDirection: 'row',
marginBottom: 8,
},
reviewerImage: {
width: 40,
height: 40,
borderRadius: 20,
marginRight: 12,
},
reviewerInfo: {
flex: 1,
},
reviewerName: {
fontSize: 14,
fontWeight: '600',
color: '#111827',
},
reviewRating: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 4,
gap: 4,
},
reviewDate: {
fontSize: 12,
color: '#9CA3AF',
marginLeft: 8,
},
reviewComment: {
fontSize: 14,
color: '#6B7280',
lineHeight: 20,
},
footer: {
padding: 16,
backgroundColor: '#fff',
borderTopWidth: 1,
borderTopColor: '#E5E7EB',
},
contactButtons: {
flexDirection: 'row',
gap: 12,
marginBottom: 12,
},
contactButton: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#F3F4F6',
paddingVertical: 12,
borderRadius: 12,
gap: 8,
},
contactButtonText: {
fontSize: 14,
fontWeight: '600',
color: '#374151',
},
bookButton: {
backgroundColor: '#1E40AF',
paddingVertical: 16,
borderRadius: 12,
alignItems: 'center',
},
bookButtonDisabled: {
backgroundColor: '#9CA3AF',
},
bookButtonText: {
fontSize: 16,
fontWeight: '600',
color: '#fff',
},
});
_layout.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-
query";
import { Stack } from "expo-router";
import * as SplashScreen from "expo-splash-screen";
import React, { useEffect } from "react";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { AuthProvider } from "@/hooks/auth-context";
import { ServicesProvider } from "@/hooks/services-context";

SplashScreen.preventAutoHideAsync();

const queryClient = new QueryClient();

function RootLayoutNav() {
return (
<Stack screenOptions={{ headerBackTitle: "Back" }}>
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="service/[id]" options={{ title: "Service Details", presentation:
"card" }} />
<Stack.Screen name="provider/[id]" options={{ title: "Provider Profile", presentation:
"card" }} />
</Stack>
);
}

export default function RootLayout() {


useEffect(() => {
SplashScreen.hideAsync();
}, []);

return (
<QueryClientProvider client={queryClient}>
<AuthProvider>
<ServicesProvider>
<GestureHandlerRootView style={{ flex: 1 }}>
<RootLayoutNav />
</GestureHandlerRootView>
</ServicesProvider>
</AuthProvider>
</QueryClientProvider>
);
}
+not-found.tsx
mport { Link, Stack } from "expo-router";
import { StyleSheet, Text, View } from "react-native";

export default function NotFoundScreen() {


return (
<>
<Stack.Screen options={{ title: "Oops!" }} />
<View style={styles.container}>
<Text style={styles.title}>This screen doesn&apos;t exist.</Text>

<Link href="/" style={styles.link}>


<Text style={styles.linkText}>Go to home screen!</Text>
</Link>
</View>
</>
);
}

const styles = StyleSheet.create({


container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
padding: 20,
},
title: {
fontSize: 20,
fontWeight: "bold",
},
link: {
marginTop: 15,
paddingVertical: 15,
},
linkText: {
fontSize: 14,
color: "#2e78b7",
},
});

├── assets
Images
Adaptive-icon.png
Favicon.png
Icon.png
splash-icon.png
├── components/
categoryGrid.tsx
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ScrollView } from 'react-native';
import * as Icons from 'lucide-react-native';
import { CATEGORIES } from '@/constants/categories';
import { ServiceCategory } from '@/types';

interface CategoryGridProps {
onSelectCategory: (category: ServiceCategory) => void;
selectedCategory?: ServiceCategory;
}

export default function CategoryGrid({ onSelectCategory, selectedCategory }:


CategoryGridProps) {
return (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.container}
>
{CATEGORIES.map((category) => {
const IconComponent = Icons[category.icon as keyof typeof Icons] as any;
const isSelected = selectedCategory === category.id;

return (
<TouchableOpacity
key={category.id}
style={[
styles.categoryCard,
isSelected && styles.selectedCard,
{ borderColor: isSelected ? category.color : '#E5E7EB' }
]}
onPress={() => onSelectCategory(category.id)}
activeOpacity={0.7}
>
<View style={[styles.iconContainer, { backgroundColor: category.color + '20' }]}>
<IconComponent size={24} color={category.color} />
</View>
<Text style={[styles.categoryName, isSelected && styles.selectedText]}>
{category.name}
</Text>
</TouchableOpacity>
);
})}
</ScrollView>
);
}

const styles = StyleSheet.create({


container: {
paddingHorizontal: 16,
paddingVertical: 12,
gap: 12,
},
categoryCard: {
alignItems: 'center',
padding: 12,
borderRadius: 12,
borderWidth: 2,
borderColor: '#E5E7EB',
backgroundColor: '#fff',
minWidth: 90,
},
selectedCard: {
backgroundColor: '#F0F9FF',
},
iconContainer: {
width: 48,
height: 48,
borderRadius: 24,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 8,
},
categoryName: {
fontSize: 12,
fontWeight: '600',
color: '#6B7280',
textAlign: 'center',
},
selectedText: {
color: '#1E40AF',
},
});
serviceCard.tsx
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Image } from 'react-native';
import { Star, MapPin, CheckCircle } from 'lucide-react-native';
import { Service } from '@/types';
import { getCategoryInfo } from '@/constants/categories';
import { router } from 'expo-router';

interface ServiceCardProps {
service: Service;
}

export default function ServiceCard({ service }: ServiceCardProps) {


const categoryInfo = getCategoryInfo(service.category);

const handlePress = () => {


router.push(`/service/${service.id}` as any);
};

return (
<TouchableOpacity style={styles.container} onPress={handlePress} activeOpacity={0.7}>
<Image source={{ uri: service.images[0] }} style={styles.image} />

<View style={styles.content}>
<View style={styles.header}>
<Text style={styles.title} numberOfLines={1}>{service.title}</Text>
{service.providerAvailable ? (
<View style={styles.availableBadge}>
<Text style={styles.availableText}>Available</Text>
</View>
):(
<View style={styles.busyBadge}>
<Text style={styles.busyText}>Busy</Text>
</View>
)}
</View>

<View style={styles.providerRow}>
<Image source={{ uri: service.providerImage }} style={styles.providerImage} />
<Text style={styles.providerName}>{service.providerName}</Text>
{service.providerVerified && (
<CheckCircle size={16} color="#0891B2" />
)}
</View>

<Text style={styles.description} numberOfLines={2}>


{service.description}
</Text>

<View style={styles.footer}>
<View style={styles.priceContainer}>
<Text style={styles.price}>KSh {service.price}</Text>
<Text style={styles.priceUnit}>/{service.priceUnit}</Text>
</View>

<View style={styles.ratingContainer}>
<Star size={16} color="#F59E0B" fill="#F59E0B" />
<Text style={styles.rating}>{service.rating.toFixed(1)}</Text>
<Text style={styles.reviews}>({service.totalReviews})</Text>
</View>
</View>

<View style={styles.tags}>
{service.tags.slice(0, 3).map((tag, index) => (
<View key={index} style={styles.tag}>
<Text style={styles.tagText}>{tag}</Text>
</View>
))}
</View>
</View>
</TouchableOpacity>
);
}

const styles = StyleSheet.create({


container: {
backgroundColor: '#fff',
borderRadius: 12,
marginHorizontal: 16,
marginVertical: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
overflow: 'hidden',
},
image: {
width: '100%',
height: 180,
backgroundColor: '#f3f4f6',
},
content: {
padding: 16,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
title: {
fontSize: 18,
fontWeight: '600',
color: '#111827',
flex: 1,
marginRight: 8,
},
availableBadge: {
backgroundColor: '#D1FAE5',
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 4,
},
availableText: {
color: '#065F46',
fontSize: 12,
fontWeight: '600',
},
busyBadge: {
backgroundColor: '#FEE2E2',
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 4,
},
busyText: {
color: '#991B1B',
fontSize: 12,
fontWeight: '600',
},
providerRow: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
},
providerImage: {
width: 24,
height: 24,
borderRadius: 12,
marginRight: 8,
backgroundColor: '#f3f4f6',
},
providerName: {
fontSize: 14,
color: '#6B7280',
marginRight: 4,
},
description: {
fontSize: 14,
color: '#6B7280',
lineHeight: 20,
marginBottom: 12,
},
footer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
priceContainer: {
flexDirection: 'row',
alignItems: 'baseline',
},
price: {
fontSize: 20,
fontWeight: '700',
color: '#1E40AF',
},
priceUnit: {
fontSize: 14,
color: '#6B7280',
marginLeft: 2,
},
ratingContainer: {
flexDirection: 'row',
alignItems: 'center',
},
rating: {
fontSize: 14,
fontWeight: '600',
color: '#111827',
marginLeft: 4,
},
reviews: {
fontSize: 14,
color: '#6B7280',
marginLeft: 2,
},
tags: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 6,
},
tag: {
backgroundColor: '#F3F4F6',
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 12,
},
tagText: {
fontSize: 12,
color: '#6B7280',
},
});
├── constants/
categories.ts
mport { CategoryInfo, ServiceCategory } from '@/types';

export const CATEGORIES: CategoryInfo[] = [


{ id: 'cleaning', name: 'Cleaning', icon: 'Sparkles', color: '#3B82F6' },
{ id: 'plumbing', name: 'Plumbing', icon: 'Wrench', color: '#10B981' },
{ id: 'electrical', name: 'Electrical', icon: 'Zap', color: '#F59E0B' },
{ id: 'carpentry', name: 'Carpentry', icon: 'Hammer', color: '#8B5CF6' },
{ id: 'painting', name: 'Painting', icon: 'Paintbrush', color: '#EC4899' },
{ id: 'gardening', name: 'Gardening', icon: 'Flower', color: '#84CC16' },
{ id: 'tutoring', name: 'Tutoring', icon: 'GraduationCap', color: '#06B6D4' },
{ id: 'beauty', name: 'Beauty', icon: 'Sparkle', color: '#F472B6' },
{ id: 'fitness', name: 'Fitness', icon: 'Dumbbell', color: '#EF4444' },
{ id: 'delivery', name: 'Delivery', icon: 'Truck', color: '#6366F1' },
{ id: 'tech', name: 'Tech Support', icon: 'Monitor', color: '#0EA5E9' },
{ id: 'other', name: 'Other', icon: 'MoreHorizontal', color: '#64748B' },
];

export const getCategoryInfo = (id: ServiceCategory): CategoryInfo => {


return CATEGORIES.find(cat => cat.id === id) || CATEGORIES[CATEGORIES.length -
1];
};

├── hooks/
Auth-content.tsx
mport createContextHook from '@nkzw/create-context-hook';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useState, useEffect } from 'react';
import { User, UserRole, Provider } from '@/types';
import { MOCK_PROVIDERS } from '@/mocks/services';

interface AuthState {
user: User | Provider | null;
isLoading: boolean;
isAuthenticated: boolean;
login: (phone: string, role: UserRole) => Promise<void>;
logout: () => Promise<void>;
updateProfile: (updates: Partial<User | Provider>) => Promise<void>;
toggleAvailability?: () => Promise<void>;
}

export const [AuthProvider, useAuth] = createContextHook<AuthState>(() => {


const [user, setUser] = useState<User | Provider | null>(null);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
loadUser();
}, []);

const loadUser = async () => {


try {
const storedUser = await AsyncStorage.getItem('user');
if (storedUser) {
setUser(JSON.parse(storedUser));
}
} catch (error) {
console.error('Error loading user:', error);
} finally {
setIsLoading(false);
}
};

const login = async (phone: string, role: UserRole) => {


// Simulate OTP verification delay
await new Promise(resolve => setTimeout(resolve, 1000));

// Check if this is an existing provider


const existingProvider = MOCK_PROVIDERS.find(p => p.phone === phone);

let newUser: User | Provider;

if (existingProvider) {
newUser = existingProvider;
} else if (role === 'provider') {
newUser = {
id: Date.now().toString(),
name: 'New Provider',
email: `provider${Date.now()}@example.com`,
phone,
role: 'provider',
bio: 'New service provider',
location: 'CIVE Campus',
isVerified: false,
isAvailable: true,
rating: 0,
totalReviews: 0,
totalServices: 0,
whatsappNumber: phone,
joinedDate: new Date().toISOString().split('T')[0],
createdAt: new Date().toISOString(),
} as Provider;
} else {
newUser = {
id: Date.now().toString(),
name: 'New User',
email: `user${Date.now()}@example.com`,
phone,
role: 'user',
createdAt: new Date().toISOString(),
};
}
setUser(newUser);
await AsyncStorage.setItem('user', JSON.stringify(newUser));
};

const logout = async () => {


setUser(null);
await AsyncStorage.removeItem('user');
};

const updateProfile = async (updates: Partial<User | Provider>) => {


if (!user) return;

const updatedUser = { ...user, ...updates };


setUser(updatedUser);
await AsyncStorage.setItem('user', JSON.stringify(updatedUser));
};

const toggleAvailability = async () => {


if (!user || user.role !== 'provider') return;

const provider = user as Provider;


const updatedProvider = { ...provider, isAvailable: !provider.isAvailable };
setUser(updatedProvider);
await AsyncStorage.setItem('user', JSON.stringify(updatedProvider));
};

return {
user,
isLoading,
isAuthenticated: !!user,
login,
logout,
updateProfile,
toggleAvailability: user?.role === 'provider' ? toggleAvailability : undefined,
};
});
services-context.ts
mport createContextHook from '@nkzw/create-context-hook';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useState, useEffect, useMemo } from 'react';
import { Service, Review, FilterOptions, Booking } from '@/types';
import { MOCK_SERVICES, MOCK_REVIEWS } from '@/mocks/services';
import { useAuth } from './auth-context';
interface ServicesState {
services: Service[];
reviews: Review[];
bookings: Booking[];
isLoading: boolean;
filters: FilterOptions;
setFilters: (filters: FilterOptions) => void;
filteredServices: Service[];
addService: (service: Omit<Service, 'id' | 'createdAt'>) => Promise<void>;
updateService: (id: string, updates: Partial<Service>) => Promise<void>;
deleteService: (id: string) => Promise<void>;
addReview: (review: Omit<Review, 'id' | 'createdAt'>) => Promise<void>;
getServiceReviews: (serviceId: string) => Review[];
getProviderServices: (providerId: string) => Service[];
createBooking: (booking: Omit<Booking, 'id' | 'createdAt' | 'status'>) => Promise<void>;
getUserBookings: (userId: string) => Booking[];
getProviderBookings: (providerId: string) => Booking[];
}

export const [ServicesProvider, useServices] = createContextHook<ServicesState>(() => {


const { user } = useAuth();
const [services, setServices] = useState<Service[]>(MOCK_SERVICES);
const [reviews, setReviews] = useState<Review[]>(MOCK_REVIEWS);
const [bookings, setBookings] = useState<Booking[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [filters, setFilters] = useState<FilterOptions>({});

useEffect(() => {
loadData();
}, []);

const loadData = async () => {


try {
const [storedServices, storedReviews, storedBookings] = await Promise.all([
AsyncStorage.getItem('services'),
AsyncStorage.getItem('reviews'),
AsyncStorage.getItem('bookings'),
]);

if (storedServices) {
const parsed = JSON.parse(storedServices);
setServices([...MOCK_SERVICES, ...parsed]);
}
if (storedReviews) {
const parsed = JSON.parse(storedReviews);
setReviews([...MOCK_REVIEWS, ...parsed]);
}
if (storedBookings) {
setBookings(JSON.parse(storedBookings));
}
} catch (error) {
console.error('Error loading data:', error);
} finally {
setIsLoading(false);
}
};

const saveServices = async (newServices: Service[]) => {


const customServices = newServices.filter(s => !MOCK_SERVICES.find(ms => ms.id ===
s.id));
await AsyncStorage.setItem('services', JSON.stringify(customServices));
};

const saveReviews = async (newReviews: Review[]) => {


const customReviews = newReviews.filter(r => !MOCK_REVIEWS.find(mr => mr.id === r.id));
await AsyncStorage.setItem('reviews', JSON.stringify(customReviews));
};

const filteredServices = useMemo(() => {


let result = [...services];

if (filters.category) {
result = result.filter(s => s.category === filters.category);
}

if (filters.searchQuery) {
const query = filters.searchQuery.toLowerCase();
result = result.filter(s =>
s.title.toLowerCase().includes(query) ||
s.description.toLowerCase().includes(query) ||
s.providerName.toLowerCase().includes(query)
);
}

if (filters.minPrice !== undefined) {


result = result.filter(s => s.price >= filters.minPrice!);
}
if (filters.maxPrice !== undefined) {
result = result.filter(s => s.price <= filters.maxPrice!);
}

if (filters.minRating !== undefined) {


result = result.filter(s => s.rating >= filters.minRating!);
}

if (filters.sortBy) {
result.sort((a, b) => {
switch (filters.sortBy) {
case 'price':
return a.price - b.price;
case 'rating':
return b.rating - a.rating;
case 'popularity':
return b.totalBookings - a.totalBookings;
default:
return 0;
}
});
}

return result;
}, [services, filters]);

const addService = async (service: Omit<Service, 'id' | 'createdAt'>) => {


const newService: Service = {
...service,
id: Date.now().toString(),
createdAt: new Date().toISOString(),
};
const updated = [...services, newService];
setServices(updated);
await saveServices(updated);
};

const updateService = async (id: string, updates: Partial<Service>) => {


const updated = services.map(s => s.id === id ? { ...s, ...updates } : s);
setServices(updated);
await saveServices(updated);
};
const deleteService = async (id: string) => {
const updated = services.filter(s => s.id !== id);
setServices(updated);
await saveServices(updated);
};

const addReview = async (review: Omit<Review, 'id' | 'createdAt'>) => {


const newReview: Review = {
...review,
id: Date.now().toString(),
createdAt: new Date().toISOString(),
};
const updated = [...reviews, newReview];
setReviews(updated);
await saveReviews(updated);

// Update service rating


const serviceReviews = updated.filter(r => r.serviceId === review.serviceId);
const avgRating = serviceReviews.reduce((acc, r) => acc + r.rating, 0) /
serviceReviews.length;
await updateService(review.serviceId, {
rating: avgRating,
totalReviews: serviceReviews.length
});
};

const getServiceReviews = (serviceId: string) => {


return reviews.filter(r => r.serviceId === serviceId);
};

const getProviderServices = (providerId: string) => {


return services.filter(s => s.providerId === providerId);
};

const createBooking = async (booking: Omit<Booking, 'id' | 'createdAt' | 'status'>) => {


const newBooking: Booking = {
...booking,
id: Date.now().toString(),
status: 'pending',
createdAt: new Date().toISOString(),
};
const updated = [...bookings, newBooking];
setBookings(updated);
await AsyncStorage.setItem('bookings', JSON.stringify(updated));
};

const getUserBookings = (userId: string) => {


return bookings.filter(b => b.userId === userId);
};

const getProviderBookings = (providerId: string) => {


return bookings.filter(b => b.providerId === providerId);
};

return {
services,
reviews,
bookings,
isLoading,
filters,
setFilters,
filteredServices,
addService,
updateService,
deleteService,
addReview,
getServiceReviews,
getProviderServices,
createBooking,
getUserBookings,
getProviderBookings,
};
});
├── mocks/
services.ts
import { Service, Provider, Review } from '@/types';

export const MOCK_PROVIDERS: Provider[] = [


{
id: '1',
name: 'John Mwangi',
email: 'john@example.com',
phone: '+254712345678',
role: 'provider',
bio: 'Professional cleaner with 5+ years experience. Specialized in residential and
office cleaning.',
location: 'CIVE Campus, Block A',
isVerified: true,
isAvailable: true,
rating: 4.8,
totalReviews: 127,
totalServices: 3,
whatsappNumber: '+254712345678',
joinedDate: '2023-01-15',
profileImage: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?
w=400',
createdAt: '2023-01-15',
},
{
id: '2',
name: 'Sarah Wanjiru',
email: 'sarah@example.com',
phone: '+254723456789',
role: 'provider',
bio: 'Certified electrician. Quick response time and affordable rates.',
location: 'CIVE Campus, Block B',
isVerified: true,
isAvailable: false,
rating: 4.9,
totalReviews: 89,
totalServices: 2,
whatsappNumber: '+254723456789',
joinedDate: '2023-03-20',
profileImage: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?
w=400',
createdAt: '2023-03-20',
},
{
id: '3',
name: 'Peter Ochieng',
email: 'peter@example.com',
phone: '+254734567890',
role: 'provider',
bio: 'Expert plumber. No job too big or small. Available 24/7 for emergencies.',
location: 'CIVE Campus, Block C',
isVerified: false,
isAvailable: true,
rating: 4.5,
totalReviews: 64,
totalServices: 4,
whatsappNumber: '+254734567890',
joinedDate: '2023-06-10',
profileImage: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?
w=400',
createdAt: '2023-06-10',
},
];

export const MOCK_SERVICES: Service[] = [


{
id: '1',
providerId: '1',
providerName: 'John Mwangi',
providerImage: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?
w=400',
providerRating: 4.8,
providerVerified: true,
providerAvailable: true,
title: 'Professional House Cleaning',
description: 'Complete house cleaning service including living room, bedrooms,
kitchen, and bathrooms. Eco-friendly products used.',
category: 'cleaning',
price: 2000,
priceUnit: 'fixed',
images: [
'https://images.unsplash.com/photo-1581578731548-c64695cc6952?w=800',
'https://images.unsplash.com/photo-1527515545081-5db817172677?w=800',
],
rating: 4.8,
totalReviews: 45,
totalBookings: 120,
tags: ['eco-friendly', 'fast', 'reliable'],
createdAt: '2023-01-20',
},
{
id: '2',
providerId: '1',
providerName: 'John Mwangi',
providerImage: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?
w=400',
providerRating: 4.8,
providerVerified: true,
providerAvailable: true,
title: 'Office Cleaning Service',
description: 'Professional office cleaning for small to medium businesses. Evening and
weekend availability.',
category: 'cleaning',
price: 500,
priceUnit: 'hour',
images: [
'https://images.unsplash.com/photo-1497366216548-37526070297c?w=800',
],
rating: 4.7,
totalReviews: 32,
totalBookings: 85,
tags: ['office', 'flexible', 'professional'],
createdAt: '2023-02-10',
},
{
id: '3',
providerId: '2',
providerName: 'Sarah Wanjiru',
providerImage: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?
w=400',
providerRating: 4.9,
providerVerified: true,
providerAvailable: false,
title: 'Electrical Repairs & Installation',
description: 'Licensed electrician for all electrical needs. Wiring, repairs, installations,
and safety inspections.',
category: 'electrical',
price: 1500,
priceUnit: 'hour',
images: [
'https://images.unsplash.com/photo-1621905251189-08b45d6a269e?w=800',
],
rating: 4.9,
totalReviews: 56,
totalBookings: 150,
tags: ['licensed', 'emergency', 'safe'],
createdAt: '2023-03-25',
},
{
id: '4',
providerId: '3',
providerName: 'Peter Ochieng',
providerImage: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?
w=400',
providerRating: 4.5,
providerVerified: false,
providerAvailable: true,
title: 'Plumbing Services',
description: 'Expert plumbing services including leak repairs, pipe installation, and
drain cleaning.',
category: 'plumbing',
price: 1200,
priceUnit: 'hour',
images: [
'https://images.unsplash.com/photo-1585704032915-c3400ca199e7?w=800',
],
rating: 4.5,
totalReviews: 28,
totalBookings: 75,
tags: ['24/7', 'emergency', 'experienced'],
createdAt: '2023-06-15',
},
{
id: '5',
providerId: '3',
providerName: 'Peter Ochieng',
providerImage: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?
w=400',
providerRating: 4.5,
providerVerified: false,
providerAvailable: true,
title: 'Math & Physics Tutoring',
description: 'Experienced tutor for high school and college level mathematics and
physics. One-on-one or group sessions.',
category: 'tutoring',
price: 800,
priceUnit: 'hour',
images: [
'https://images.unsplash.com/photo-1509062522246-3755977927d7?w=800',
],
rating: 4.6,
totalReviews: 18,
totalBookings: 45,
tags: ['experienced', 'patient', 'results'],
createdAt: '2023-07-01',
},
];

export const MOCK_REVIEWS: Review[] = [


{
id: '1',
serviceId: '1',
userId: 'u1',
userName: 'Mary Kamau',
userImage: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?
w=400',
rating: 5,
comment: 'Excellent service! John was professional, punctual, and did a thorough job.
My house has never been cleaner.',
createdAt: '2024-01-10',
},
{
id: '2',
serviceId: '1',
userId: 'u2',
userName: 'David Njoroge',
userImage: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?
w=400',
rating: 4,
comment: 'Good service overall. Would have liked a bit more attention to detail in the
bathroom, but satisfied with the work.',
createdAt: '2024-01-05',
},
{
id: '3',
serviceId: '3',
userId: 'u3',
userName: 'Grace Muthoni',
userImage: 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=400',
rating: 5,
comment: 'Sarah fixed our electrical issue quickly and explained everything clearly.
Very professional!',
createdAt: '2023-12-20',
},
];
├── types/
index.ts
xport type UserRole = 'user' | 'provider';

export interface User {


id: string;
name: string;
email: string;
phone: string;
role: UserRole;
profileImage?: string;
createdAt: string;
}

export interface Provider extends User {


role: 'provider';
bio: string;
location: string;
isVerified: boolean;
isAvailable: boolean;
rating: number;
totalReviews: number;
totalServices: number;
whatsappNumber: string;
joinedDate: string;
}

export interface Service {


id: string;
providerId: string;
providerName: string;
providerImage?: string;
providerRating: number;
providerVerified: boolean;
providerAvailable: boolean;
title: string;
description: string;
category: ServiceCategory;
price: number;
priceUnit: 'hour' | 'day' | 'fixed' | 'quote';
images: string[];
rating: number;
totalReviews: number;
totalBookings: number;
tags: string[];
createdAt: string;
}

export interface Review {


id: string;
serviceId: string;
userId: string;
userName: string;
userImage?: string;
rating: number;
comment: string;
createdAt: string;
}

export interface Booking {


id: string;
serviceId: string;
serviceName: string;
providerId: string;
providerName: string;
userId: string;
userName: string;
status: 'pending' | 'confirmed' | 'completed' | 'cancelled';
date: string;
time: string;
notes?: string;
createdAt: string;
}

export type ServiceCategory =


| 'cleaning'
| 'plumbing'
| 'electrical'
| 'carpentry'
| 'painting'
| 'gardening'
| 'tutoring'
| 'beauty'
| 'fitness'
| 'delivery'
| 'tech'
| 'other';

export interface CategoryInfo {


id: ServiceCategory;
name: string;
icon: string;
color: string;
}

export interface FilterOptions {


category?: ServiceCategory;
minPrice?: number;
maxPrice?: number;
minRating?: number;
sortBy?: 'price' | 'rating' | 'popularity';
searchQuery?: string;
}
├── .gitignore
Learn more
https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files

# dependencies
node_modules/

# Expo
.expo/
dist/
web-build/
expo-env.d.ts

# Native
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision

# Metro
.metro-health-check*

# debug
npm-debug.*
yarn-debug.*
yarn-error.*

# macOS
.DS_Store
*.pem

# local env files


.env*.local

# typescript
*.tsbuildinfo
.vercel

├── app.json/
{
"expo": {
"name": "HudumaLink App",
"slug": "hudumalink-app",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "myapp",
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"splash": {
"image": "./assets/images/splash-icon.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "app.rork.hudumalink-app"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/images/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "app.rork.hudumalink-app"
},
"web": {
"favicon": "./assets/images/favicon.png"
},
"plugins": [
[
"expo-router",
{
"origin": "https://rork.com/"
}
]
],
"experiments": {
"typedRoutes": true
}
}
}
├── bun.lock/
├── package.json/
{
"name": "expo-app",
"main": "expo-router/entry",
"version": "1.0.0",
"scripts": {
"start": "bunx rork start -p 5ubi9481sacgznzbjft3y --tunnel",
"start-web": "bunx rork start -p 5ubi9481sacgznzbjft3y --web --tunnel",
"start-web-dev": "DEBUG=expo* bunx rork start -p 5ubi9481sacgznzbjft3y --web --tunnel",
"lint": "expo lint"
},
"dependencies": {
"@expo/vector-icons": "^14.1.0",
"@nkzw/create-context-hook": "^1.1.0",
"@react-native-async-storage/async-storage": "2.1.2",
"@react-navigation/native": "^7.1.6",
"@tanstack/react-query": "^5.83.0",
"expo": "^53.0.4",
"expo-blur": "~14.1.4",
"expo-constants": "~17.1.4",
"expo-font": "~13.3.0",
"expo-haptics": "~14.1.4",
"expo-image": "~2.1.6",
"expo-image-picker": "~16.1.4",
"expo-linear-gradient": "~14.1.4",
"expo-linking": "~7.1.4",
"expo-location": "~18.1.4",
"expo-router": "~5.0.3",
"expo-splash-screen": "~0.30.7",
"expo-status-bar": "~2.2.3",
"expo-symbols": "~0.4.4",
"expo-system-ui": "~5.0.6",
"expo-web-browser": "~14.1.6",
"lucide-react-native": "^0.475.0",
"nativewind": "^4.1.23",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-native": "0.79.1",
"react-native-gesture-handler": "~2.24.0",
"react-native-safe-area-context": "5.3.0",
"react-native-screens": "~4.10.0",
"react-native-svg": "15.11.2",
"react-native-web": "^0.20.0",
"zustand": "^5.0.2"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@expo/ngrok": "^4.1.0",
"@types/react": "~19.0.10",
"eslint": "^9.31.0",
"eslint-config-expo": "^9.2.0",
"typescript": "~5.8.3"
},
"private": true
}

└── tsconfig.json

"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true,
"paths": {
"@/*": [
"./*"
]
}
},
"include": [
"**/*.ts",
"**/*.tsx",
".expo/types/**/*.ts",
"expo-env.d.ts"
]
}

You might also like