This commit is contained in:
2026-05-21 10:39:51 +01:00
parent 8d5e706521
commit 7de75be897
5 changed files with 567 additions and 145 deletions

View File

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

View File

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

View File

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

View File

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

View File

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