.
This commit is contained in:
@@ -25,6 +25,7 @@ export default function Booking() {
|
||||
|
||||
const selectedService = shop?.services.find((s) => s.id === serviceId);
|
||||
const selectedBarber = shop?.barbers.find((b) => b.id === barberId);
|
||||
const stepLabels = ['Serviço', 'Equipa', 'Dia', 'Confirmar'];
|
||||
|
||||
const availableDates = useMemo(() => {
|
||||
const dates = [];
|
||||
@@ -84,6 +85,43 @@ export default function Booking() {
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={styles.scrollContent}>
|
||||
<View style={styles.stepTracker}>
|
||||
{stepLabels.map((label, index) => {
|
||||
const stepNumber = index + 1;
|
||||
const active = stepNumber === step;
|
||||
const done = stepNumber < step;
|
||||
return (
|
||||
<View key={label} style={styles.stepTrackItem}>
|
||||
<View style={[styles.stepDot, active && styles.stepDotActive, done && styles.stepDotDone]}>
|
||||
<Text style={[styles.stepDotText, (active || done) && styles.stepDotTextActive]}>
|
||||
{done ? '✓' : stepNumber}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={[styles.stepTrackLabel, active && styles.stepTrackLabelActive]} numberOfLines={1}>
|
||||
{label}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
<Card style={styles.contextCard}>
|
||||
<View style={styles.contextHeader}>
|
||||
<Text style={styles.contextKicker}>A marcar em</Text>
|
||||
<Text style={styles.contextTitle} numberOfLines={1}>{shop.name}</Text>
|
||||
</View>
|
||||
<View style={styles.contextGrid}>
|
||||
<View style={styles.contextCell}>
|
||||
<Text style={styles.contextLabel}>Serviço</Text>
|
||||
<Text style={styles.contextValue} numberOfLines={1}>{selectedService?.name || 'Por escolher'}</Text>
|
||||
</View>
|
||||
<View style={styles.contextCell}>
|
||||
<Text style={styles.contextLabel}>Profissional</Text>
|
||||
<Text style={styles.contextValue} numberOfLines={1}>{selectedBarber?.name || 'Por escolher'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Card>
|
||||
|
||||
{step === 1 && (
|
||||
<View style={styles.stepBox}>
|
||||
<Text style={styles.stepLabel}>Escolha o serviço</Text>
|
||||
@@ -94,7 +132,9 @@ export default function Booking() {
|
||||
<Text style={styles.choiceName}>{s.name}</Text>
|
||||
<Text style={styles.choiceMeta}>{s.duration} min</Text>
|
||||
</View>
|
||||
<Text style={styles.choicePrice}>{currency(s.price)}</Text>
|
||||
<View style={styles.pricePill}>
|
||||
<Text style={styles.choicePrice}>{currency(s.price)}</Text>
|
||||
</View>
|
||||
</Card>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
@@ -147,7 +187,10 @@ export default function Booking() {
|
||||
</View>
|
||||
|
||||
{slot !== '' && (
|
||||
<Button style={{ marginTop: 24 }} onPress={() => setStep(4)}>Continuar</Button>
|
||||
<View style={styles.footerAction}>
|
||||
<Text style={styles.selectedSlot}>Selecionado: {slot}</Text>
|
||||
<Button onPress={() => setStep(4)}>Continuar</Button>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
@@ -229,10 +272,102 @@ const styles = StyleSheet.create({
|
||||
scrollContent: {
|
||||
padding: 20,
|
||||
paddingBottom: 40,
|
||||
gap: 18,
|
||||
},
|
||||
stepBox: {
|
||||
gap: 12,
|
||||
},
|
||||
stepTracker: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: radius.sm,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
padding: 12,
|
||||
...shadows.soft,
|
||||
},
|
||||
stepTrackItem: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
},
|
||||
stepDot: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: radius.pill,
|
||||
backgroundColor: colors.surfaceMuted,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
stepDotActive: {
|
||||
backgroundColor: colors.primary,
|
||||
borderColor: colors.primary,
|
||||
},
|
||||
stepDotDone: {
|
||||
backgroundColor: colors.surfaceDark,
|
||||
borderColor: colors.surfaceDark,
|
||||
},
|
||||
stepDotText: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 12,
|
||||
fontWeight: '900',
|
||||
},
|
||||
stepDotTextActive: {
|
||||
color: colors.textInverse,
|
||||
},
|
||||
stepTrackLabel: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 11,
|
||||
fontWeight: '700',
|
||||
},
|
||||
stepTrackLabelActive: {
|
||||
color: colors.text,
|
||||
},
|
||||
contextCard: {
|
||||
gap: 14,
|
||||
padding: 16,
|
||||
},
|
||||
contextHeader: {
|
||||
gap: 2,
|
||||
},
|
||||
contextKicker: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 12,
|
||||
fontWeight: '800',
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
contextTitle: {
|
||||
color: colors.text,
|
||||
fontSize: 20,
|
||||
fontWeight: '900',
|
||||
},
|
||||
contextGrid: {
|
||||
flexDirection: 'row',
|
||||
gap: 10,
|
||||
},
|
||||
contextCell: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.surfaceMuted,
|
||||
borderRadius: radius.sm,
|
||||
padding: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
contextLabel: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 11,
|
||||
fontWeight: '800',
|
||||
textTransform: 'uppercase',
|
||||
marginBottom: 4,
|
||||
},
|
||||
contextValue: {
|
||||
color: colors.text,
|
||||
fontSize: 13,
|
||||
fontWeight: '800',
|
||||
},
|
||||
stepLabel: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 12,
|
||||
@@ -264,9 +399,15 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
choicePrice: {
|
||||
color: colors.primaryDark,
|
||||
fontSize: 16,
|
||||
fontSize: 14,
|
||||
fontWeight: '800',
|
||||
},
|
||||
pricePill: {
|
||||
backgroundColor: colors.primarySoft,
|
||||
borderRadius: radius.pill,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 6,
|
||||
},
|
||||
avatarMini: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
@@ -397,4 +538,19 @@ const styles = StyleSheet.create({
|
||||
notifBtnTxtActive: {
|
||||
color: colors.primaryDark,
|
||||
},
|
||||
footerAction: {
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: radius.sm,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
padding: 14,
|
||||
gap: 12,
|
||||
...shadows.soft,
|
||||
},
|
||||
selectedSlot: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 13,
|
||||
fontWeight: '700',
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -164,11 +164,10 @@ export default function Dashboard() {
|
||||
<SafeAreaView style={styles.container}>
|
||||
{/* Header Dashboard */}
|
||||
<View style={styles.header}>
|
||||
<Image
|
||||
source={require('../../assets/logo.png')}
|
||||
style={styles.headerLogo}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
<View style={styles.headerCopy}>
|
||||
<Text style={styles.headerKicker}>Painel</Text>
|
||||
<Text style={styles.headerTitle} numberOfLines={1}>{shop.name}</Text>
|
||||
</View>
|
||||
<TouchableOpacity style={styles.brandPill} onPress={() => navigation.navigate('BarberProfileTab' as never)}>
|
||||
<Text style={styles.brandText}>{user?.name.charAt(0)}</Text>
|
||||
</TouchableOpacity>
|
||||
@@ -196,6 +195,24 @@ export default function Dashboard() {
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={styles.content}>
|
||||
<View style={styles.overviewGrid}>
|
||||
<Card style={styles.overviewCard}>
|
||||
<Text style={styles.overviewValue}>{shopAppointments.length}</Text>
|
||||
<Text style={styles.overviewLabel}>Hoje</Text>
|
||||
</Card>
|
||||
<Card style={styles.overviewCard}>
|
||||
<Text style={styles.overviewValue}>{shop.services.length}</Text>
|
||||
<Text style={styles.overviewLabel}>Serviços</Text>
|
||||
</Card>
|
||||
<Card style={styles.overviewCardWide}>
|
||||
<View>
|
||||
<Text style={styles.overviewValue}>{shop.products.reduce((sum, p) => sum + p.stock, 0)}</Text>
|
||||
<Text style={styles.overviewLabel}>Produtos em stock</Text>
|
||||
</View>
|
||||
<Text style={styles.overviewRating}>★ {shop.rating.toFixed(1)}</Text>
|
||||
</Card>
|
||||
</View>
|
||||
|
||||
{activeTab === 'agenda' && (
|
||||
<View style={styles.tabBox}>
|
||||
<View style={styles.agendaHeader}>
|
||||
@@ -473,9 +490,20 @@ const styles = StyleSheet.create({
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border,
|
||||
},
|
||||
headerLogo: {
|
||||
width: 140,
|
||||
height: 40,
|
||||
headerCopy: {
|
||||
flex: 1,
|
||||
},
|
||||
headerKicker: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 12,
|
||||
fontWeight: '800',
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
headerTitle: {
|
||||
color: colors.text,
|
||||
fontSize: 22,
|
||||
fontWeight: '900',
|
||||
marginTop: 2,
|
||||
},
|
||||
brandPill: {
|
||||
width: 44,
|
||||
@@ -525,6 +553,41 @@ const styles = StyleSheet.create({
|
||||
content: {
|
||||
padding: 20,
|
||||
paddingBottom: 40,
|
||||
gap: 20,
|
||||
},
|
||||
overviewGrid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: 10,
|
||||
},
|
||||
overviewCard: {
|
||||
flex: 1,
|
||||
minWidth: '47%',
|
||||
padding: 16,
|
||||
gap: 4,
|
||||
},
|
||||
overviewCardWide: {
|
||||
width: '100%',
|
||||
padding: 16,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
overviewValue: {
|
||||
color: colors.text,
|
||||
fontSize: 26,
|
||||
fontWeight: '900',
|
||||
},
|
||||
overviewLabel: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 12,
|
||||
fontWeight: '800',
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
overviewRating: {
|
||||
color: colors.star,
|
||||
fontSize: 18,
|
||||
fontWeight: '900',
|
||||
},
|
||||
tabBox: {
|
||||
gap: 20,
|
||||
|
||||
@@ -28,25 +28,24 @@ export default function Explore() {
|
||||
return (b.rating || 0) - (a.rating || 0);
|
||||
});
|
||||
}, [shops, query, filter]);
|
||||
const featuredShops = filtered.slice(0, 3);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<ScrollView contentContainerStyle={styles.content} keyboardShouldPersistTaps="handled">
|
||||
<View style={styles.heroPanel}>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<View>
|
||||
<Text style={styles.greeting}>
|
||||
{user ? `Olá, ${user.name.split(' ')[0]}` : 'Descobre'}
|
||||
</Text>
|
||||
<Text style={styles.headline}>Barbearias</Text>
|
||||
</View>
|
||||
<View style={styles.brandPill}>
|
||||
<Text style={styles.brandText}>SA</Text>
|
||||
</View>
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerCopy}>
|
||||
<Text style={styles.greeting}>
|
||||
{user ? `Olá, ${user.name.split(' ')[0]}` : 'Descobre'}
|
||||
</Text>
|
||||
<Text style={styles.headline}>Encontra um espaço</Text>
|
||||
</View>
|
||||
<View style={styles.brandPill}>
|
||||
<Text style={styles.brandText}>SA</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Search */}
|
||||
<View style={styles.searchPanel}>
|
||||
<View style={styles.searchBox}>
|
||||
<Text style={styles.searchIcon}>⌕</Text>
|
||||
<TextInput
|
||||
@@ -57,26 +56,23 @@ export default function Explore() {
|
||||
style={styles.searchInput}
|
||||
/>
|
||||
</View>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.filters}>
|
||||
{[
|
||||
['todas', 'Melhor avaliação'],
|
||||
['top', 'Top avaliadas'],
|
||||
['servicos', 'Mais serviços'],
|
||||
].map(([id, label]) => (
|
||||
<TouchableOpacity
|
||||
key={id}
|
||||
onPress={() => setFilter(id as typeof filter)}
|
||||
style={[styles.chip, filter === id && styles.chipActive]}
|
||||
>
|
||||
<Text style={[styles.chipText, filter === id && styles.chipTextActive]}>{label}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* Filters */}
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.filters}>
|
||||
{[
|
||||
['todas', 'Melhor avaliação'],
|
||||
['top', 'Top avaliadas'],
|
||||
['servicos', 'Mais serviços'],
|
||||
].map(([id, label]) => (
|
||||
<TouchableOpacity
|
||||
key={id}
|
||||
onPress={() => setFilter(id as typeof filter)}
|
||||
style={[styles.chip, filter === id && styles.chipActive]}
|
||||
>
|
||||
<Text style={[styles.chipText, filter === id && styles.chipTextActive]}>{label}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
||||
{/* Results */}
|
||||
{!shopsReady ? (
|
||||
<View style={styles.emptyState}>
|
||||
<Text style={styles.emptyIcon}>⏳</Text>
|
||||
@@ -92,43 +88,83 @@ export default function Explore() {
|
||||
</Button>
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.list}>
|
||||
<Text style={styles.count}>{filtered.length} espaços disponíveis</Text>
|
||||
{filtered.map((shop) => (
|
||||
<TouchableOpacity
|
||||
key={shop.id}
|
||||
style={styles.shopCard}
|
||||
activeOpacity={0.85}
|
||||
onPress={() => navigation.navigate('ShopDetails', { shopId: shop.id })}
|
||||
>
|
||||
{shop.imageUrl ? (
|
||||
<Image source={{ uri: shop.imageUrl }} style={styles.shopImage} />
|
||||
) : (
|
||||
<View style={styles.shopImageFallback}>
|
||||
<Text style={styles.shopFallbackText}>{shop.name.charAt(0)}</Text>
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.shopOverlay} />
|
||||
<View style={styles.shopBody}>
|
||||
<View style={styles.shopRow}>
|
||||
<Text style={styles.shopName} numberOfLines={1}>{shop.name}</Text>
|
||||
<View style={styles.ratingPill}>
|
||||
<Text style={styles.ratingText}>★ {(shop.rating || 0).toFixed(1)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={styles.address} numberOfLines={1}>{shop.address || 'Endereço indisponível'}</Text>
|
||||
<View style={styles.metaRow}>
|
||||
<View style={styles.metaPill}>
|
||||
<Text style={styles.metaText}>{(shop.services || []).length} serviços</Text>
|
||||
</View>
|
||||
<View style={styles.metaPill}>
|
||||
<Text style={styles.metaText}>{(shop.barbers || []).length} barbeiros</Text>
|
||||
</View>
|
||||
</View>
|
||||
<>
|
||||
{featuredShops.length > 0 && (
|
||||
<View style={styles.featuredBlock}>
|
||||
<View style={styles.sectionRow}>
|
||||
<Text style={styles.sectionTitle}>Em destaque</Text>
|
||||
<Text style={styles.count}>{filtered.length} espaços</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.featuredList}>
|
||||
{featuredShops.map((shop) => (
|
||||
<TouchableOpacity
|
||||
key={shop.id}
|
||||
style={styles.featuredCard}
|
||||
activeOpacity={0.86}
|
||||
onPress={() => navigation.navigate('ShopDetails', { shopId: shop.id })}
|
||||
>
|
||||
{shop.imageUrl ? (
|
||||
<Image source={{ uri: shop.imageUrl }} style={styles.featuredImage} />
|
||||
) : (
|
||||
<View style={styles.featuredFallback}>
|
||||
<Text style={styles.shopFallbackText}>{shop.name.charAt(0)}</Text>
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.featuredBody}>
|
||||
<Text style={styles.shopName} numberOfLines={1}>{shop.name}</Text>
|
||||
<Text style={styles.address} numberOfLines={1}>{shop.address || 'Endereço indisponível'}</Text>
|
||||
<View style={styles.metaRow}>
|
||||
<View style={styles.ratingPill}>
|
||||
<Text style={styles.ratingText}>★ {(shop.rating || 0).toFixed(1)}</Text>
|
||||
</View>
|
||||
<Text style={styles.metaInline}>{(shop.services || []).length} serviços</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.list}>
|
||||
<View style={styles.sectionRow}>
|
||||
<Text style={styles.sectionTitle}>Todos os espaços</Text>
|
||||
<Text style={styles.count}>{filtered.length}</Text>
|
||||
</View>
|
||||
{filtered.map((shop) => (
|
||||
<TouchableOpacity
|
||||
key={shop.id}
|
||||
style={styles.shopCard}
|
||||
activeOpacity={0.85}
|
||||
onPress={() => navigation.navigate('ShopDetails', { shopId: shop.id })}
|
||||
>
|
||||
{shop.imageUrl ? (
|
||||
<Image source={{ uri: shop.imageUrl }} style={styles.shopImage} />
|
||||
) : (
|
||||
<View style={styles.shopImageFallback}>
|
||||
<Text style={styles.shopFallbackText}>{shop.name.charAt(0)}</Text>
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.shopBody}>
|
||||
<View style={styles.shopRow}>
|
||||
<Text style={styles.shopName} numberOfLines={1}>{shop.name}</Text>
|
||||
<View style={styles.ratingPill}>
|
||||
<Text style={styles.ratingText}>★ {(shop.rating || 0).toFixed(1)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={styles.address} numberOfLines={1}>{shop.address || 'Endereço indisponível'}</Text>
|
||||
<View style={styles.metaRow}>
|
||||
<Text style={styles.metaInline}>{(shop.services || []).length} serviços</Text>
|
||||
<Text style={styles.metaInline}>{(shop.barbers || []).length} barbeiros</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.openIcon}>
|
||||
<Text style={styles.openIconText}>›</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
@@ -145,27 +181,25 @@ const styles = StyleSheet.create({
|
||||
gap: 18,
|
||||
paddingBottom: 32,
|
||||
},
|
||||
heroPanel: {
|
||||
backgroundColor: colors.surfaceDark,
|
||||
borderRadius: radius.sm,
|
||||
padding: 18,
|
||||
gap: 18,
|
||||
...shadows.card,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
alignItems: 'center',
|
||||
gap: 16,
|
||||
},
|
||||
headerCopy: {
|
||||
flex: 1,
|
||||
},
|
||||
greeting: {
|
||||
color: '#b7c4bf',
|
||||
color: colors.textMuted,
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
marginBottom: 2,
|
||||
marginBottom: 4,
|
||||
},
|
||||
headline: {
|
||||
color: colors.textInverse,
|
||||
fontSize: 32,
|
||||
color: colors.text,
|
||||
fontSize: 30,
|
||||
lineHeight: 34,
|
||||
fontWeight: '900',
|
||||
},
|
||||
brandPill: {
|
||||
@@ -186,13 +220,22 @@ const styles = StyleSheet.create({
|
||||
searchBox: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.surface,
|
||||
backgroundColor: colors.surfaceMuted,
|
||||
borderRadius: radius.md,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.14)',
|
||||
borderColor: colors.border,
|
||||
paddingHorizontal: 16,
|
||||
gap: 10,
|
||||
},
|
||||
searchPanel: {
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: radius.sm,
|
||||
padding: 12,
|
||||
gap: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
...shadows.soft,
|
||||
},
|
||||
searchIcon: {
|
||||
color: colors.primary,
|
||||
fontSize: 20,
|
||||
@@ -228,7 +271,25 @@ const styles = StyleSheet.create({
|
||||
color: colors.textInverse,
|
||||
},
|
||||
list: {
|
||||
gap: 16,
|
||||
gap: 12,
|
||||
},
|
||||
featuredBlock: {
|
||||
gap: 12,
|
||||
},
|
||||
featuredList: {
|
||||
gap: 12,
|
||||
paddingRight: 4,
|
||||
},
|
||||
sectionRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 12,
|
||||
},
|
||||
sectionTitle: {
|
||||
color: colors.text,
|
||||
fontSize: 18,
|
||||
fontWeight: '900',
|
||||
},
|
||||
count: {
|
||||
color: colors.textMuted,
|
||||
@@ -238,20 +299,25 @@ const styles = StyleSheet.create({
|
||||
letterSpacing: 0,
|
||||
},
|
||||
shopCard: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderRadius: radius.sm,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: colors.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
padding: 10,
|
||||
gap: 12,
|
||||
...shadows.card,
|
||||
},
|
||||
shopImage: {
|
||||
width: '100%',
|
||||
height: 160,
|
||||
width: 88,
|
||||
height: 88,
|
||||
borderRadius: radius.sm,
|
||||
},
|
||||
shopImageFallback: {
|
||||
width: '100%',
|
||||
height: 160,
|
||||
width: 88,
|
||||
height: 88,
|
||||
borderRadius: radius.sm,
|
||||
backgroundColor: colors.primarySoft,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -262,10 +328,9 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '900',
|
||||
opacity: 0.5,
|
||||
},
|
||||
shopOverlay: {},
|
||||
shopBody: {
|
||||
padding: 18,
|
||||
gap: 10,
|
||||
flex: 1,
|
||||
gap: 8,
|
||||
},
|
||||
shopRow: {
|
||||
flexDirection: 'row',
|
||||
@@ -282,8 +347,8 @@ const styles = StyleSheet.create({
|
||||
ratingPill: {
|
||||
backgroundColor: colors.accentSoft,
|
||||
borderRadius: radius.pill,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 5,
|
||||
paddingHorizontal: 9,
|
||||
paddingVertical: 4,
|
||||
},
|
||||
ratingText: {
|
||||
color: '#7a4310',
|
||||
@@ -297,19 +362,51 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
metaRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
},
|
||||
metaPill: {
|
||||
backgroundColor: colors.surfaceMuted,
|
||||
borderRadius: radius.xs,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 5,
|
||||
},
|
||||
metaText: {
|
||||
metaInline: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 11,
|
||||
fontSize: 12,
|
||||
fontWeight: '700',
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
openIcon: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: radius.pill,
|
||||
backgroundColor: colors.surfaceMuted,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
openIconText: {
|
||||
color: colors.primary,
|
||||
fontSize: 22,
|
||||
fontWeight: '800',
|
||||
lineHeight: 24,
|
||||
},
|
||||
featuredCard: {
|
||||
width: 260,
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: radius.sm,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
overflow: 'hidden',
|
||||
...shadows.card,
|
||||
},
|
||||
featuredImage: {
|
||||
width: '100%',
|
||||
height: 122,
|
||||
},
|
||||
featuredFallback: {
|
||||
width: '100%',
|
||||
height: 122,
|
||||
backgroundColor: colors.primarySoft,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
featuredBody: {
|
||||
padding: 14,
|
||||
gap: 8,
|
||||
},
|
||||
emptyState: {
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -94,6 +94,8 @@ export default function Profile() {
|
||||
: [],
|
||||
[notifications, user?.id]
|
||||
);
|
||||
const activeAppointmentsCount = myAppointments.filter((a) => a.status !== 'cancelado').length;
|
||||
const totalOrdersValue = myOrders.reduce((sum, o) => sum + o.total, 0);
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
@@ -129,7 +131,21 @@ export default function Profile() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Notificações (Horizontal) */}
|
||||
<View style={styles.statsStrip}>
|
||||
<View style={styles.profileStat}>
|
||||
<Text style={styles.profileStatValue}>{activeAppointmentsCount}</Text>
|
||||
<Text style={styles.profileStatLabel}>{isBarber ? 'Marcações' : 'Agenda'}</Text>
|
||||
</View>
|
||||
<View style={styles.profileStat}>
|
||||
<Text style={styles.profileStatValue}>{isBarber ? shopReviews.length : favoriteShops.length}</Text>
|
||||
<Text style={styles.profileStatLabel}>{isBarber ? 'Reviews' : 'Favoritos'}</Text>
|
||||
</View>
|
||||
<View style={styles.profileStat}>
|
||||
<Text style={styles.profileStatValue}>{isBarber ? myShop?.rating.toFixed(1) || '0.0' : currency(totalOrdersValue)}</Text>
|
||||
<Text style={styles.profileStatLabel}>{isBarber ? 'Rating' : 'Compras'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{myNotifications.length > 0 && (
|
||||
<View style={styles.notifSection}>
|
||||
<Text style={styles.sectionTitle}>Notificações</Text>
|
||||
@@ -148,8 +164,7 @@ export default function Profile() {
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Tabs Role-Based */}
|
||||
<View style={styles.tabBar}>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.tabBar}>
|
||||
{(isBarber ? [
|
||||
['agenda_pessoal', 'Agenda Global'],
|
||||
['reviews', 'Avaliações'],
|
||||
@@ -165,10 +180,9 @@ export default function Profile() {
|
||||
style={[styles.tabItem, activeTab === id && styles.tabActive]}
|
||||
>
|
||||
<Text style={[styles.tabText, activeTab === id && styles.tabTextActive]}>{label}</Text>
|
||||
{activeTab === id && <View style={styles.tabIndicator} />}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* Conteúdo das Tabs */}
|
||||
<View style={styles.tabContent}>
|
||||
@@ -209,7 +223,7 @@ export default function Profile() {
|
||||
|
||||
{activeTab === 'favoritos' && (
|
||||
<View style={styles.listContainer}>
|
||||
{favoriteShops.map(s => (
|
||||
{favoriteShops.length > 0 ? favoriteShops.map(s => (
|
||||
<TouchableOpacity key={s.id} onPress={() => navigation.navigate('ShopDetails', { shopId: s.id })}>
|
||||
<Card style={styles.shopCard}>
|
||||
<View style={styles.shopIcon}><Text style={styles.shopIconText}>{s.name.charAt(0)}</Text></View>
|
||||
@@ -217,7 +231,29 @@ export default function Profile() {
|
||||
<Text style={styles.shopRating}>★ {s.rating.toFixed(1)}</Text>
|
||||
</Card>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
)) : <Text style={styles.emptyTxt}>Ainda não tens favoritos.</Text>}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{activeTab === 'pedidos' && (
|
||||
<View style={styles.listContainer}>
|
||||
{myOrders.length > 0 ? myOrders.map(order => {
|
||||
const shop = shops.find((s) => s.id === order.shopId);
|
||||
return (
|
||||
<Card key={order.id} style={styles.orderCard}>
|
||||
<View style={styles.orderTop}>
|
||||
<View>
|
||||
<Text style={styles.orderShop}>{shop?.name || 'Barbearia'}</Text>
|
||||
<Text style={styles.orderMeta}>{new Date(order.createdAt).toLocaleDateString('pt-PT')} · {order.items.length} item(s)</Text>
|
||||
</View>
|
||||
<Text style={styles.orderTotal}>{currency(order.total)}</Text>
|
||||
</View>
|
||||
<View style={[styles.statusTag, { backgroundColor: statusColor[order.status] + '20', alignSelf: 'flex-start' }]}>
|
||||
<Text style={[styles.statusText, { color: statusColor[order.status] }]}>{statusLabel[order.status]}</Text>
|
||||
</View>
|
||||
</Card>
|
||||
);
|
||||
}) : <Text style={styles.emptyTxt}>Ainda não tens compras.</Text>}
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -308,7 +344,7 @@ export default function Profile() {
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: colors.background },
|
||||
content: { padding: 20, paddingBottom: 100 },
|
||||
profileHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 28, gap: 16, backgroundColor: colors.surfaceDark, borderRadius: radius.sm, padding: 18, ...shadows.card },
|
||||
profileHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 12, gap: 16, backgroundColor: colors.surfaceDark, borderRadius: radius.sm, padding: 18, ...shadows.card },
|
||||
avatarBox: { width: 64, height: 64, borderRadius: radius.lg, backgroundColor: colors.accentSoft, alignItems: 'center', justifyContent: 'center', borderWidth: 1, borderColor: 'rgba(255,255,255,0.16)' },
|
||||
avatarText: { color: '#7a4310', fontSize: 24, fontWeight: '900' },
|
||||
profileInfo: { flex: 1, gap: 2 },
|
||||
@@ -316,6 +352,10 @@ const styles = StyleSheet.create({
|
||||
profileEmail: { color: '#b7c4bf', fontSize: 14, fontWeight: '500' },
|
||||
roleBadge: { alignSelf: 'flex-start', backgroundColor: colors.primarySoft, borderRadius: radius.pill, paddingHorizontal: 10, paddingVertical: 4, marginTop: 6 },
|
||||
roleText: { color: colors.primaryDark, fontSize: 10, fontWeight: '800', textTransform: 'uppercase' },
|
||||
statsStrip: { flexDirection: 'row', gap: 10, marginBottom: 28 },
|
||||
profileStat: { flex: 1, backgroundColor: colors.surface, borderRadius: radius.sm, padding: 12, borderWidth: 1, borderColor: colors.border, ...shadows.soft },
|
||||
profileStatValue: { color: colors.text, fontSize: 18, fontWeight: '900' },
|
||||
profileStatLabel: { color: colors.textMuted, fontSize: 11, fontWeight: '800', textTransform: 'uppercase', marginTop: 2 },
|
||||
reviewActions: { flexDirection: 'row', gap: 14 },
|
||||
sectionTitle: { color: colors.text, fontSize: 18, fontWeight: '800', marginBottom: 12 },
|
||||
notifSection: { marginBottom: 32 },
|
||||
@@ -324,12 +364,11 @@ const styles = StyleSheet.create({
|
||||
notifUnread: { borderColor: colors.primary, backgroundColor: colors.primarySoft },
|
||||
notifMsg: { color: colors.textMuted, fontSize: 13, lineHeight: 18, fontWeight: '500' },
|
||||
markRead: { color: colors.primaryDark, fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0 },
|
||||
tabBar: { flexDirection: 'row', marginBottom: 24, borderBottomWidth: 1, borderBottomColor: colors.border },
|
||||
tabItem: { paddingVertical: 14, marginRight: 24, position: 'relative' },
|
||||
tabActive: {},
|
||||
tabBar: { flexDirection: 'row', gap: 8, paddingBottom: 18 },
|
||||
tabItem: { paddingVertical: 10, paddingHorizontal: 14, borderRadius: radius.pill, backgroundColor: colors.surface, borderWidth: 1, borderColor: colors.border },
|
||||
tabActive: { backgroundColor: colors.primary, borderColor: colors.primary },
|
||||
tabText: { color: colors.textMuted, fontSize: 15, fontWeight: '700' },
|
||||
tabTextActive: { color: colors.text },
|
||||
tabIndicator: { position: 'absolute', bottom: -1, left: 0, right: 0, height: 3, backgroundColor: colors.primary, borderRadius: radius.pill },
|
||||
tabTextActive: { color: colors.textInverse },
|
||||
tabContent: { minHeight: 200 },
|
||||
listContainer: { gap: 16 },
|
||||
emptyTxt: { color: colors.textMuted, textAlign: 'center', marginTop: 20, fontWeight: '600' },
|
||||
@@ -351,6 +390,11 @@ const styles = StyleSheet.create({
|
||||
shopName: { color: colors.text, fontSize: 16, fontWeight: '800' },
|
||||
shopAddr: { color: colors.textMuted, fontSize: 12, fontWeight: '500' },
|
||||
shopRating: { color: colors.star, fontWeight: '900', fontSize: 14 },
|
||||
orderCard: { padding: 16, gap: 12, backgroundColor: colors.surface, borderRadius: radius.sm, borderWidth: 1, borderColor: colors.border },
|
||||
orderTop: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 },
|
||||
orderShop: { color: colors.text, fontSize: 16, fontWeight: '900' },
|
||||
orderMeta: { color: colors.textMuted, fontSize: 12, fontWeight: '600', marginTop: 4 },
|
||||
orderTotal: { color: colors.primary, fontSize: 18, fontWeight: '900' },
|
||||
reviewCard: { padding: 18, gap: 10, backgroundColor: colors.surface, borderRadius: radius.sm },
|
||||
reviewHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
|
||||
reviewStars: { color: colors.star, fontSize: 15 },
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Card } from '../components/ui/Card';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { currency } from '../lib/format';
|
||||
import { RootStackParamList } from '../navigation/types';
|
||||
import { colors, radius } from '../lib/theme';
|
||||
import { colors, radius, shadows } from '../lib/theme';
|
||||
|
||||
type Tab = 'servicos' | 'barbeiros' | 'produtos' | 'detalhes';
|
||||
|
||||
@@ -73,7 +73,6 @@ export default function ShopDetails() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ScrollView bounces={false} contentContainerStyle={styles.scrollContent}>
|
||||
{/* Imagem Hero e Overlay */}
|
||||
<View style={styles.hero}>
|
||||
{shop.imageUrl ? (
|
||||
<Image source={{ uri: shop.imageUrl }} style={styles.heroImage} />
|
||||
@@ -92,19 +91,40 @@ export default function ShopDetails() {
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
|
||||
<View style={styles.heroBody}>
|
||||
<View style={styles.ratingBox}>
|
||||
<Text style={styles.ratingValue}>★ {shop.rating.toFixed(1)}</Text>
|
||||
<View style={styles.summaryWrap}>
|
||||
<View style={styles.summaryCard}>
|
||||
<View style={styles.summaryTop}>
|
||||
<View style={styles.ratingBox}>
|
||||
<Text style={styles.ratingValue}>★ {shop.rating.toFixed(1)}</Text>
|
||||
</View>
|
||||
<Text style={styles.summaryMeta}>{shop.services.length} serviços</Text>
|
||||
</View>
|
||||
<Text style={styles.shopName}>{shop.name}</Text>
|
||||
<TouchableOpacity onPress={openMap} style={styles.addrBox}>
|
||||
<Text style={styles.shopAddr} numberOfLines={2}>📍 {shop.address}</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.quickStats}>
|
||||
<View style={styles.quickStat}>
|
||||
<Text style={styles.quickValue}>{shop.barbers.length}</Text>
|
||||
<Text style={styles.quickLabel}>Equipa</Text>
|
||||
</View>
|
||||
<View style={styles.quickStat}>
|
||||
<Text style={styles.quickValue}>{shop.products.length}</Text>
|
||||
<Text style={styles.quickLabel}>Produtos</Text>
|
||||
</View>
|
||||
<View style={styles.quickStat}>
|
||||
<Text style={styles.quickValue}>{schedule[currentDayIndex]?.closed ? 'Fechado' : 'Aberto'}</Text>
|
||||
<Text style={styles.quickLabel}>Hoje</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Button onPress={() => reserveService(shop.services[0]?.id)} disabled={shop.services.length === 0}>
|
||||
Agendar agora
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Tabs Estilizadas */}
|
||||
<View style={styles.tabSection}>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.tabsScroll}>
|
||||
{[
|
||||
@@ -206,7 +226,7 @@ const styles = StyleSheet.create({
|
||||
paddingBottom: 40,
|
||||
},
|
||||
hero: {
|
||||
height: 360,
|
||||
height: 230,
|
||||
position: 'relative',
|
||||
},
|
||||
heroImage: {
|
||||
@@ -269,12 +289,30 @@ const styles = StyleSheet.create({
|
||||
favActive: {
|
||||
color: colors.danger,
|
||||
},
|
||||
heroBody: {
|
||||
position: 'absolute',
|
||||
bottom: 30,
|
||||
left: 20,
|
||||
right: 20,
|
||||
gap: 10,
|
||||
summaryWrap: {
|
||||
paddingHorizontal: 20,
|
||||
marginTop: -36,
|
||||
zIndex: 2,
|
||||
},
|
||||
summaryCard: {
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: radius.sm,
|
||||
padding: 18,
|
||||
gap: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
...shadows.card,
|
||||
},
|
||||
summaryTop: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
summaryMeta: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 12,
|
||||
fontWeight: '800',
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
ratingBox: {
|
||||
alignSelf: 'flex-start',
|
||||
@@ -289,23 +327,47 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '900',
|
||||
},
|
||||
shopName: {
|
||||
color: colors.textInverse,
|
||||
fontSize: 32,
|
||||
color: colors.text,
|
||||
fontSize: 28,
|
||||
lineHeight: 32,
|
||||
fontWeight: '900',
|
||||
},
|
||||
addrBox: {
|
||||
opacity: 0.8,
|
||||
},
|
||||
shopAddr: {
|
||||
color: colors.textInverse,
|
||||
color: colors.textMuted,
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
},
|
||||
quickStats: {
|
||||
flexDirection: 'row',
|
||||
gap: 10,
|
||||
},
|
||||
quickStat: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.surfaceMuted,
|
||||
borderRadius: radius.sm,
|
||||
padding: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
quickValue: {
|
||||
color: colors.text,
|
||||
fontSize: 16,
|
||||
fontWeight: '900',
|
||||
},
|
||||
quickLabel: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 11,
|
||||
fontWeight: '700',
|
||||
marginTop: 2,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
tabSection: {
|
||||
backgroundColor: colors.surface,
|
||||
backgroundColor: colors.background,
|
||||
paddingVertical: 10,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border,
|
||||
marginTop: 6,
|
||||
},
|
||||
tabsScroll: {
|
||||
paddingHorizontal: 20,
|
||||
|
||||
Reference in New Issue
Block a user