first commit
This commit is contained in:
470
src/screens/BarberDetailScreen.tsx
Normal file
470
src/screens/BarberDetailScreen.tsx
Normal file
@@ -0,0 +1,470 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Pressable,
|
||||
Image,
|
||||
Alert,
|
||||
FlatList,
|
||||
} from 'react-native';
|
||||
import { useRoute, useNavigation } from '@react-navigation/native';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import { RootStackParamList } from '../navigation/types';
|
||||
import { supabase, isSupabaseConfigured } from '../services/supabase';
|
||||
import { COLORS, SIZES, FONTS, SHADOWS } from '../constants/theme';
|
||||
import { Barber, Review, Booking } from '../types';
|
||||
import ReviewCard from '../components/ReviewCard';
|
||||
import BookingCard from '../components/BookingCard';
|
||||
|
||||
const BarberDetailScreen: React.FC = () => {
|
||||
const route = useRoute();
|
||||
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>();
|
||||
const { barberId } = route.params as { barberId: string };
|
||||
|
||||
const [barber, setBarber] = useState<Barber | null>(null);
|
||||
const [reviews, setReviews] = useState<Review[]>([]);
|
||||
const [bookings, setBookings] = useState<Booking[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState<'reviews' | 'availability'>('reviews');
|
||||
|
||||
useEffect(() => {
|
||||
loadBarberDetails();
|
||||
}, [barberId]);
|
||||
|
||||
const loadBarberDetails = async () => {
|
||||
try {
|
||||
if (!isSupabaseConfigured) {
|
||||
// Demo mode - use mock data
|
||||
const { mockBarbers, mockReviews } = await import('../data/mockData');
|
||||
const barber = mockBarbers.find(b => b.id === barberId);
|
||||
if (barber) {
|
||||
setBarber(barber);
|
||||
// Load barber-specific reviews
|
||||
const barberReviews = mockReviews.filter(r => r.barber_id === barberId);
|
||||
setReviews(barberReviews);
|
||||
// Mock bookings for demo
|
||||
setBookings([]);
|
||||
}
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const [barberRes, reviewsRes, bookingsRes] = await Promise.all([
|
||||
supabase
|
||||
.from('barbers')
|
||||
.select(`
|
||||
*,
|
||||
user:users(name, photo, email, phone)
|
||||
`)
|
||||
.eq('id', barberId)
|
||||
.single(),
|
||||
supabase
|
||||
.from('reviews')
|
||||
.select(`
|
||||
*,
|
||||
user:users(name, photo)
|
||||
`)
|
||||
.eq('barber_id', barberId)
|
||||
.order('created_at', { ascending: false }),
|
||||
supabase
|
||||
.from('bookings')
|
||||
.select(`
|
||||
*,
|
||||
service:services(name, price),
|
||||
customer:users(name)
|
||||
`)
|
||||
.eq('barber_id', barberId)
|
||||
.eq('status', 'completed')
|
||||
.order('booking_date', { ascending: false })
|
||||
.limit(10)
|
||||
]);
|
||||
|
||||
if (barberRes.data) setBarber(barberRes.data);
|
||||
if (reviewsRes.data) setReviews(reviewsRes.data);
|
||||
if (bookingsRes.data) setBookings(bookingsRes.data);
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar detalhes do barbeiro:', error);
|
||||
Alert.alert('Erro', 'Falha ao carregar detalhes do barbeiro');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBookNow = () => {
|
||||
Alert.alert(
|
||||
'👨💼 Marcar com Barbeiro',
|
||||
`Vou marcar com: ${barber?.user?.name}`,
|
||||
[
|
||||
{
|
||||
text: 'Cancelar',
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Confirmar',
|
||||
onPress: () => {
|
||||
Alert.alert(
|
||||
'✅ Barbeiro Selecionado',
|
||||
`${barber?.user?.name} foi selecionado para a sua marcação.\n\nContinue para escolher serviço, data e horário.`,
|
||||
[
|
||||
{
|
||||
text: 'OK',
|
||||
onPress: () => navigation.navigate('Booking'),
|
||||
},
|
||||
]
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
const renderStars = (rating: number) => {
|
||||
const stars = [];
|
||||
const fullStars = Math.floor(rating);
|
||||
|
||||
for (let i = 0; i < fullStars; i++) {
|
||||
stars.push(
|
||||
<Text key={i} style={styles.star}>★</Text>
|
||||
);
|
||||
}
|
||||
|
||||
const emptyStars = 5 - fullStars;
|
||||
for (let i = 0; i < emptyStars; i++) {
|
||||
stars.push(
|
||||
<Text key={`empty-${i}`} style={[styles.star, styles.emptyStar]}>☆</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return stars;
|
||||
};
|
||||
|
||||
const renderAvailability = () => {
|
||||
if (!barber?.availability) return null;
|
||||
|
||||
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
||||
|
||||
return (
|
||||
<View style={styles.availabilityContainer}>
|
||||
{days.map((day) => {
|
||||
const timeSlots = barber.availability[day.toLowerCase()] || [];
|
||||
return (
|
||||
<View key={day} style={styles.dayRow}>
|
||||
<Text style={styles.dayText}>{day}</Text>
|
||||
<View style={styles.timeSlots}>
|
||||
{timeSlots.length > 0 ? (
|
||||
timeSlots.map((time, index) => (
|
||||
<Text key={index} style={styles.timeSlot}>
|
||||
{time}
|
||||
</Text>
|
||||
))
|
||||
) : (
|
||||
<Text style={styles.closedText}>Fechado</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View style={styles.loadingContainer}>
|
||||
<Text style={styles.loadingText}>A carregar...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (!barber) {
|
||||
return (
|
||||
<View style={styles.errorContainer}>
|
||||
<Text style={styles.errorText}>Barbeiro não encontrado</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container}>
|
||||
{/* Barber Header */}
|
||||
<View style={styles.header}>
|
||||
<Image
|
||||
source={{ uri: barber.user?.photo || 'https://via.placeholder.com/150' }}
|
||||
style={styles.avatar}
|
||||
/>
|
||||
<View style={styles.barberInfo}>
|
||||
<Text style={styles.name}>{barber.user?.name}</Text>
|
||||
<Text style={styles.specialty}>{barber.specialty}</Text>
|
||||
<View style={styles.ratingContainer}>
|
||||
<View style={styles.stars}>
|
||||
{renderStars(barber.rating)}
|
||||
</View>
|
||||
<Text style={styles.rating}>{barber.rating.toFixed(1)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Bio */}
|
||||
<View style={styles.bioSection}>
|
||||
<Text style={styles.sectionTitle}>Sobre</Text>
|
||||
<Text style={styles.bio}>{barber.bio}</Text>
|
||||
</View>
|
||||
|
||||
{/* Contact Info */}
|
||||
<View style={styles.contactSection}>
|
||||
<Text style={styles.sectionTitle}>Contacto</Text>
|
||||
<Text style={styles.contactText}>📧 {barber.user?.email}</Text>
|
||||
<Text style={styles.contactText}>📱 {barber.user?.phone}</Text>
|
||||
</View>
|
||||
|
||||
{/* Book Button */}
|
||||
<Pressable
|
||||
style={({ pressed }) => [styles.bookButton, pressed && styles.bookButtonPressed]}
|
||||
onPress={handleBookNow}
|
||||
>
|
||||
<Text style={styles.bookButtonText}>Marcar Horário</Text>
|
||||
</Pressable>
|
||||
|
||||
{/* Tabs */}
|
||||
<View style={styles.tabsContainer}>
|
||||
<Pressable
|
||||
style={({ pressed }) => [
|
||||
styles.tab,
|
||||
activeTab === 'reviews' && styles.activeTab,
|
||||
pressed && styles.tabPressed
|
||||
]}
|
||||
onPress={() => setActiveTab('reviews')}
|
||||
>
|
||||
<Text style={[styles.tabText, activeTab === 'reviews' && styles.activeTabText]}>
|
||||
Avaliações ({reviews.length})
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={({ pressed }) => [
|
||||
styles.tab,
|
||||
activeTab === 'availability' && styles.activeTab,
|
||||
pressed && styles.tabPressed
|
||||
]}
|
||||
onPress={() => setActiveTab('availability')}
|
||||
>
|
||||
<Text style={[styles.tabText, activeTab === 'availability' && styles.activeTabText]}>
|
||||
Disponibilidade
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{/* Tab Content */}
|
||||
<View style={styles.tabContent}>
|
||||
{activeTab === 'reviews' ? (
|
||||
<>
|
||||
{reviews.length === 0 ? (
|
||||
<Text style={styles.noReviewsText}>Sem avaliações</Text>
|
||||
) : (
|
||||
reviews.map((review) => (
|
||||
<ReviewCard key={review.id} review={review} />
|
||||
))
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
renderAvailability()
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: COLORS.background,
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
loadingText: {
|
||||
...FONTS.body,
|
||||
color: COLORS.textSecondary,
|
||||
},
|
||||
errorContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
errorText: {
|
||||
...FONTS.body,
|
||||
color: COLORS.error,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
padding: SIZES.padding,
|
||||
backgroundColor: COLORS.surface,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: COLORS.border,
|
||||
},
|
||||
avatar: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: 50,
|
||||
marginRight: SIZES.margin,
|
||||
},
|
||||
barberInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
name: {
|
||||
...FONTS.h1,
|
||||
color: COLORS.text,
|
||||
marginBottom: SIZES.base / 2,
|
||||
},
|
||||
specialty: {
|
||||
...FONTS.h3,
|
||||
color: COLORS.primary,
|
||||
marginBottom: SIZES.base,
|
||||
},
|
||||
ratingContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
stars: {
|
||||
flexDirection: 'row',
|
||||
marginRight: SIZES.base,
|
||||
},
|
||||
star: {
|
||||
color: COLORS.primary,
|
||||
fontSize: 18,
|
||||
},
|
||||
emptyStar: {
|
||||
color: COLORS.border,
|
||||
},
|
||||
rating: {
|
||||
...FONTS.h3,
|
||||
color: COLORS.text,
|
||||
},
|
||||
bioSection: {
|
||||
padding: SIZES.padding,
|
||||
},
|
||||
sectionTitle: {
|
||||
...FONTS.h2,
|
||||
color: COLORS.text,
|
||||
marginBottom: SIZES.margin,
|
||||
},
|
||||
bio: {
|
||||
...FONTS.body,
|
||||
color: COLORS.textSecondary,
|
||||
lineHeight: 24,
|
||||
},
|
||||
contactSection: {
|
||||
padding: SIZES.padding,
|
||||
backgroundColor: COLORS.surface,
|
||||
margin: SIZES.margin,
|
||||
borderRadius: SIZES.radius,
|
||||
...SHADOWS.light,
|
||||
},
|
||||
contactText: {
|
||||
...FONTS.body,
|
||||
color: COLORS.text,
|
||||
marginBottom: SIZES.base,
|
||||
},
|
||||
bookButton: {
|
||||
backgroundColor: COLORS.primary,
|
||||
borderRadius: SIZES.radius,
|
||||
padding: SIZES.padding,
|
||||
alignItems: 'center',
|
||||
margin: SIZES.margin,
|
||||
...SHADOWS.medium,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
bookButtonPressed: {
|
||||
opacity: 0.9,
|
||||
},
|
||||
bookButtonText: {
|
||||
...FONTS.h3,
|
||||
color: COLORS.background,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
tabsContainer: {
|
||||
flexDirection: 'row',
|
||||
marginHorizontal: SIZES.margin,
|
||||
marginBottom: SIZES.margin,
|
||||
backgroundColor: COLORS.surface,
|
||||
borderRadius: SIZES.radius,
|
||||
padding: SIZES.base,
|
||||
...SHADOWS.light,
|
||||
},
|
||||
tab: {
|
||||
flex: 1,
|
||||
paddingVertical: SIZES.base,
|
||||
alignItems: 'center',
|
||||
borderRadius: SIZES.base,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
tabPressed: {
|
||||
opacity: 0.7,
|
||||
},
|
||||
activeTab: {
|
||||
backgroundColor: COLORS.primary,
|
||||
},
|
||||
tabText: {
|
||||
...FONTS.body,
|
||||
color: COLORS.textSecondary,
|
||||
},
|
||||
activeTabText: {
|
||||
color: COLORS.background,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
tabContent: {
|
||||
marginHorizontal: SIZES.margin,
|
||||
marginBottom: SIZES.margin * 2,
|
||||
},
|
||||
noReviewsText: {
|
||||
...FONTS.body,
|
||||
color: COLORS.textSecondary,
|
||||
textAlign: 'center',
|
||||
paddingVertical: SIZES.padding,
|
||||
},
|
||||
availabilityContainer: {
|
||||
backgroundColor: COLORS.surface,
|
||||
borderRadius: SIZES.radius,
|
||||
padding: SIZES.padding,
|
||||
...SHADOWS.light,
|
||||
},
|
||||
dayRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingVertical: SIZES.base,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: COLORS.border,
|
||||
},
|
||||
dayText: {
|
||||
...FONTS.body,
|
||||
color: COLORS.text,
|
||||
flex: 1,
|
||||
},
|
||||
timeSlots: {
|
||||
flex: 2,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
timeSlot: {
|
||||
...FONTS.caption,
|
||||
color: COLORS.primary,
|
||||
backgroundColor: COLORS.background,
|
||||
paddingHorizontal: SIZES.base,
|
||||
paddingVertical: SIZES.base / 2,
|
||||
borderRadius: SIZES.base / 2,
|
||||
marginLeft: SIZES.base / 2,
|
||||
marginBottom: SIZES.base / 2,
|
||||
},
|
||||
closedText: {
|
||||
...FONTS.caption,
|
||||
color: COLORS.textSecondary,
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
});
|
||||
|
||||
export default BarberDetailScreen;
|
||||
Reference in New Issue
Block a user