Prompt 1
Prompt 1
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
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
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';
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';
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>
);
}
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>
);
}
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);
}
};
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>
);
}
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);
<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>
}
/>
);
}
interface Notification {
id: string;
type: 'booking' | 'review' | 'info' | 'promo';
title: string;
message: string;
time: string;
read: boolean;
}
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>
);
}
if (isLoading) {
return (
<View style={[styles.container, { justifyContent: 'center', alignItems: 'center' }]}>
<Text>Loading...</Text>
</View>
);
}
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>
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';
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>
Services
Layout.tsx
import { Stack } from 'expo-router';
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>
);
}
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';
if (!provider) {
return (
<View style={styles.container}>
<Text>Provider not found</Text>
</View>
);
}
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>
);
}
if (!service || !provider) {
return (
<View style={styles.container}>
<Text>Service not found</Text>
</View>
);
}
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',
});
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>
);
}
SplashScreen.preventAutoHideAsync();
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>
);
}
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";
├── 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;
}
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>
);
}
interface ServiceCardProps {
service: Service;
}
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>
<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>
);
}
├── 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>;
}
useEffect(() => {
loadUser();
}, []);
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));
};
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[];
}
useEffect(() => {
loadData();
}, []);
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);
}
};
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.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]);
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';
# 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
# 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"
]
}