refactor: simplify booking flow and update status bar appearance

This commit is contained in:
2026-05-12 16:01:50 +01:00
parent 20e9e23938
commit d29cf7535b
6 changed files with 1621 additions and 2345 deletions

View File

@@ -9,7 +9,7 @@ export default function App() {
<SafeAreaProvider>
<AppProvider>
<AppNavigator />
<StatusBar style="auto" />
<StatusBar style="light" />
</AppProvider>
</SafeAreaProvider>
);

View File

@@ -1,321 +1,183 @@
/**
* @file Booking.tsx
* @description Página de agendamento reformulada para um fluxo multi-etapas (Wizard).
* Melhora a UX separando seleções e introduzindo seletor de data aprimorado.
*/
import React, { useState, useMemo, useEffect } from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Alert, FlatList } from 'react-native';
import React, { useState, useMemo } from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Alert } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRoute, useNavigation } from '@react-navigation/native';
import { useApp } from '../context/AppContext';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { Badge } from '../components/ui/Badge';
import { Stepper } from '../components/ui/Stepper';
import { currency } from '../lib/format';
export default function Booking() {
const route = useRoute();
const navigation = useNavigation();
const { shopId, serviceId: initialServiceId } = route.params as { shopId: string; serviceId?: string };
const { shops, createAppointment, user, appointments, waitlists, joinWaitlist } = useApp();
const { shops, createAppointment, user, appointments, joinWaitlist } = useApp();
const shop = useMemo(() => shops.find((s) => s.id === shopId), [shops, shopId]);
// Gestão de Steps
const [step, setStep] = useState(initialServiceId ? 2 : 1);
const steps = [
{ id: 1, label: 'Serviço' },
{ id: 2, label: 'Barbeiro' },
{ id: 3, label: 'Horário' },
{ id: 4, label: 'Confirmação' },
];
// Estados de Seleção
const [serviceId, setService] = useState(initialServiceId || '');
const [barberId, setBarber] = useState('');
const [date, setDate] = useState(new Date().toISOString().split('T')[0]);
const [slot, setSlot] = useState('');
const [reminderMinutes, setReminderMinutes] = useState(60); // 1h por padrão
const reminderOptions = [
{ label: '10 min', value: 10 },
{ label: '30 min', value: 30 },
{ label: '1 hora', value: 60 },
{ label: '24 horas', value: 1440 },
];
// Geração de datas (próximos 14 dias)
const availableDates = useMemo(() => {
const dates = [];
const today = new Date();
for (let i = 0; i < 14; i++) {
const d = new Date();
d.setDate(today.getDate() + i);
dates.push({
full: d.toISOString().split('T')[0],
day: d.getDate(),
weekday: d.toLocaleDateString('pt-PT', { weekday: 'short' }).replace('.', ''),
});
}
return dates;
}, []);
const [reminderMinutes, setReminderMinutes] = useState(60);
const selectedService = shop?.services.find((s) => s.id === serviceId);
const selectedBarber = shop?.barbers.find((b) => b.id === barberId);
const generateDefaultSlots = (): string[] => {
const slots: string[] = [];
for (let hour = 9; hour <= 18; hour++) {
slots.push(`${hour.toString().padStart(2, '0')}:00`);
const availableDates = useMemo(() => {
const dates = [];
const today = new Date();
for (let i = 0; i < 14; i++) {
const d = new Date();
d.setDate(today.getDate() + i);
dates.push({
full: d.toISOString().split('T')[0],
day: d.getDate(),
weekday: d.toLocaleDateString('pt-PT', { weekday: 'short' }).replace('.', '').toUpperCase(),
});
}
return slots;
};
return dates;
}, []);
const processedSlots = useMemo(() => {
if (!selectedBarber || !date) return [];
const specificSchedule = selectedBarber.schedule.find((s) => s.day === date);
let slots = specificSchedule && specificSchedule.slots.length > 0
? [...specificSchedule.slots]
: generateDefaultSlots();
const bookedSlots = appointments
.filter((apt) =>
apt.barberId === barberId &&
apt.status !== 'cancelado' &&
apt.date.startsWith(date)
)
.map((apt) => apt.date.split(' ')[1])
.filter(Boolean);
return slots.map(time => {
const isBooked = bookedSlots.includes(time);
const isSelected = slot === time;
return { time, isBooked, isSelected };
});
}, [selectedBarber, date, barberId, appointments, slot]);
if (!shop) {
return (
<SafeAreaView style={styles.container}>
<Text>Barbearia não encontrada</Text>
</SafeAreaView>
);
}
const canNext = () => {
if (step === 1) return !!serviceId;
if (step === 2) return !!barberId;
if (step === 3) return !!date && !!slot;
return true;
};
const slots = ["09:00", "09:30", "10:00", "10:30", "11:00", "11:30", "12:00", "14:00", "14:30", "15:00", "15:30", "16:00", "16:30", "17:00", "17:30", "18:00"];
const booked = appointments
.filter(a => a.barberId === barberId && a.status !== 'cancelado' && a.date.startsWith(date))
.map(a => a.date.split(' ')[1]);
return slots.map(t => ({ time: t, isBooked: booked.includes(t) }));
}, [selectedBarber, date, barberId, appointments]);
const submit = async () => {
if (!user) {
Alert.alert('Login necessário', 'Faça login para agendar');
navigation.navigate('Login' as never);
return;
}
if (!user) { navigation.navigate('Login' as never); return; }
const appt = await createAppointment({
shopId: shop.id,
shopId: shop!.id,
serviceId,
barberId,
customerId: user.id,
date: `${date} ${slot}`,
reminderMinutes
});
if (appt) {
Alert.alert('Sucesso', 'O seu agendamento foi confirmado. Receberá um alerta no telemóvel conforme configurado.');
Alert.alert('Sucesso', 'O seu agendamento foi confirmado.');
navigation.navigate('Profile' as never);
} else {
Alert.alert('Erro', 'Ocorreu um problema ao criar o agendamento.');
}
};
const renderStepContent = () => {
switch (step) {
case 1:
return (
<View style={styles.stepContent}>
<Text style={styles.stepTitle}>O que vamos fazer hoje?</Text>
<View style={styles.serviceGrid}>
{shop.services.map((s) => (
<TouchableOpacity
key={s.id}
style={[styles.serviceCard, serviceId === s.id && styles.serviceCardActive]}
onPress={() => setService(s.id)}
>
<View style={styles.serviceInfo}>
<Text style={[styles.serviceName, serviceId === s.id && styles.serviceNameActive]}>{s.name}</Text>
<Text style={styles.serviceDuration}>{s.duration} min</Text>
</View>
<Text style={[styles.servicePrice, serviceId === s.id && styles.servicePriceActive]}>{currency(s.price)}</Text>
</TouchableOpacity>
))}
</View>
</View>
);
case 2:
return (
<View style={styles.stepContent}>
<Text style={styles.stepTitle}>Com quem prefere?</Text>
<View style={styles.barberList}>
{shop.barbers.map((b) => (
<TouchableOpacity
key={b.id}
style={[styles.barberItem, barberId === b.id && styles.barberItemActive]}
onPress={() => setBarber(b.id)}
>
<View style={[styles.avatarPlaceholder, barberId === b.id && styles.avatarActive]}>
<Text style={styles.avatarText}>{b.name.charAt(0).toUpperCase()}</Text>
</View>
<View style={styles.barberDetails}>
<Text style={[styles.barberName, barberId === b.id && styles.barberNameActive]}>{b.name}</Text>
<Text style={styles.barberSpecialty}>{b.specialties.join(', ')}</Text>
</View>
{barberId === b.id && <Badge color="indigo"></Badge>}
</TouchableOpacity>
))}
</View>
</View>
);
case 3:
return (
<View style={styles.stepContent}>
<Text style={styles.stepTitle}>Quando?</Text>
{/* Seletor de Data Horizontal */}
<Text style={styles.subTitle}>Escolha o dia</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.dateScroll}>
{availableDates.map((d) => (
<TouchableOpacity
key={d.full}
style={[styles.dateButton, date === d.full && styles.dateButtonActive]}
onPress={() => { setDate(d.full); setSlot(''); }}
>
<Text style={[styles.dayName, date === d.full && styles.dayNameActive]}>{d.weekday}</Text>
<Text style={[styles.dayNum, date === d.full && styles.dayNumActive]}>{d.day}</Text>
</TouchableOpacity>
))}
</ScrollView>
<Text style={styles.subTitle}>Horários Disponíveis</Text>
<View style={styles.slotsGrid}>
{processedSlots.length > 0 ? (
processedSlots.map((s) => (
<TouchableOpacity
key={s.time}
disabled={s.isBooked}
style={[
styles.slotButton,
s.isSelected && styles.slotActive,
s.isBooked && styles.slotBooked
]}
onPress={() => setSlot(s.time)}
>
<Text style={[
styles.slotText,
s.isSelected && styles.slotTextActive,
s.isBooked && styles.slotTextBooked
]}>
{s.time}
</Text>
</TouchableOpacity>
))
) : (
<Text style={styles.emptyText}>Sem horários para este barbeiro neste dia.</Text>
)}
</View>
{processedSlots.length > 0 && !processedSlots.some(s => !s.isBooked) && (
<View style={styles.waitlistCard}>
<Text style={styles.waitlistText}>Esgotado! Queres ser avisado se alguém cancelar?</Text>
<Button
variant="outline"
onPress={async () => {
const ok = await joinWaitlist(shop.id, serviceId, barberId, date);
if (ok) Alert.alert('Lista de Espera', 'Estás na lista! Avisamos-te por push se abrir vaga.');
}}
>
Entrar na Lista de Espera
</Button>
</View>
)}
</View>
);
case 4:
return (
<View style={styles.stepContent}>
<Text style={styles.stepTitle}>Tudo certo?</Text>
<Card style={styles.summaryCard}>
<View style={styles.summaryRow}>
<Text style={styles.summaryLabel}>Serviço</Text>
<Text style={styles.summaryValue}>{selectedService?.name}</Text>
</View>
<View style={styles.summaryRow}>
<Text style={styles.summaryLabel}>Profissional</Text>
<Text style={styles.summaryValue}>{selectedBarber?.name}</Text>
</View>
<View style={styles.summaryRow}>
<Text style={styles.summaryLabel}>Data e Hora</Text>
<Text style={styles.summaryValue}>{date} às {slot}</Text>
</View>
<View style={styles.divider} />
<View style={styles.summaryRow}>
<Text style={styles.summaryLabel}>Total</Text>
<Text style={styles.summaryTotal}>{currency(selectedService?.price || 0)}</Text>
</View>
</Card>
<View style={styles.notificationSettings}>
<Text style={styles.subTitle}>Configurar Alerta Push</Text>
<Text style={styles.notifHelp}>Quando queres receber o lembrete no telemóvel?</Text>
<View style={styles.reminderOptions}>
{reminderOptions.map(opt => (
<TouchableOpacity
key={opt.value}
style={[styles.notifButton, reminderMinutes === opt.value && styles.notifButtonActive]}
onPress={() => setReminderMinutes(opt.value)}
>
<Text style={[styles.notifText, reminderMinutes === opt.value && styles.notifTextActive]}>
{opt.label}
</Text>
</TouchableOpacity>
))}
</View>
</View>
</View>
);
default:
return null;
}
};
if (!shop) return null;
return (
<SafeAreaView style={styles.container}>
<Stepper steps={steps} currentStep={step} />
<ScrollView style={styles.mainScroll} contentContainerStyle={styles.scrollContent}>
{renderStepContent()}
</ScrollView>
<View style={styles.footer}>
{step > 1 && (
<Button variant="ghost" onPress={() => setStep(s => s - 1)} style={styles.footerButton}>
Voltar
</Button>
)}
<Button
disabled={!canNext()}
onPress={step === 4 ? submit : () => setStep(s => s + 1)}
style={[styles.footerButton, { flex: 2 }]}
>
{step === 4 ? (user ? 'Confirmar Agendamento' : 'Login para Confirmar') : 'Próximo'}
</Button>
{/* Header Estilizado */}
<View style={styles.header}>
<TouchableOpacity onPress={() => step > 1 ? setStep(s => s - 1) : navigation.goBack()} style={styles.backBtn}>
<Text style={styles.backIcon}></Text>
</TouchableOpacity>
<View style={styles.headerTitleBox}>
<Text style={styles.headerTitle}>Agendamento</Text>
<Text style={styles.headerSubtitle}>Etapa {step} de 4</Text>
</View>
<View style={{ width: 40 }} />
</View>
<ScrollView contentContainerStyle={styles.scrollContent}>
{step === 1 && (
<View style={styles.stepBox}>
<Text style={styles.stepLabel}>Escolha o serviço</Text>
{shop.services.map(s => (
<TouchableOpacity key={s.id} onPress={() => { setService(s.id); setStep(2); }}>
<Card style={[styles.choiceCard, serviceId === s.id && styles.choiceCardActive]}>
<View style={{ flex: 1 }}>
<Text style={styles.choiceName}>{s.name}</Text>
<Text style={styles.choiceMeta}>{s.duration} min</Text>
</View>
<Text style={styles.choicePrice}>{currency(s.price)}</Text>
</Card>
</TouchableOpacity>
))}
</View>
)}
{step === 2 && (
<View style={styles.stepBox}>
<Text style={styles.stepLabel}>Com quem prefere?</Text>
{shop.barbers.map(b => (
<TouchableOpacity key={b.id} onPress={() => { setBarber(b.id); setStep(3); }}>
<Card style={[styles.choiceCard, barberId === b.id && styles.choiceCardActive]}>
<View style={styles.avatarMini}><Text style={styles.avatarTxt}>{b.name.charAt(0)}</Text></View>
<View style={{ flex: 1 }}>
<Text style={styles.choiceName}>{b.name}</Text>
<Text style={styles.choiceMeta}>{b.specialties[0] || 'Profissional'}</Text>
</View>
</Card>
</TouchableOpacity>
))}
</View>
)}
{step === 3 && (
<View style={styles.stepBox}>
<Text style={styles.stepLabel}>Escolha o dia</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.dateList}>
{availableDates.map(d => (
<TouchableOpacity key={d.full} onPress={() => setDate(d.full)} style={[styles.dateItem, date === d.full && styles.dateItemActive]}>
<Text style={[styles.dateWeek, date === d.full && styles.dateTextActive]}>{d.weekday}</Text>
<Text style={[styles.dateDay, date === d.full && styles.dateTextActive]}>{d.day}</Text>
</TouchableOpacity>
))}
</ScrollView>
<Text style={[styles.stepLabel, { marginTop: 20 }]}>Horários disponíveis</Text>
<View style={styles.slotsGrid}>
{processedSlots.map(s => (
<TouchableOpacity
key={s.time}
disabled={s.isBooked}
onPress={() => setSlot(s.time)}
style={[styles.slotItem, slot === s.time && styles.slotActive, s.isBooked && styles.slotBooked]}
>
<Text style={[styles.slotText, slot === s.time && styles.slotTextActive, s.isBooked && styles.slotTextBooked]}>
{s.time}
</Text>
</TouchableOpacity>
))}
</View>
{slot !== '' && (
<Button style={{ marginTop: 24 }} onPress={() => setStep(4)}>Continuar</Button>
)}
</View>
)}
{step === 4 && (
<View style={styles.stepBox}>
<Text style={styles.stepLabel}>Confirmar detalhes</Text>
<Card style={styles.summaryCard}>
<View style={styles.summaryRow}><Text style={styles.summaryLabel}>Serviço</Text><Text style={styles.summaryValue}>{selectedService?.name}</Text></View>
<View style={styles.summaryRow}><Text style={styles.summaryLabel}>Barbeiro</Text><Text style={styles.summaryValue}>{selectedBarber?.name}</Text></View>
<View style={styles.summaryRow}><Text style={styles.summaryLabel}>Data</Text><Text style={styles.summaryValue}>{date}</Text></View>
<View style={styles.summaryRow}><Text style={styles.summaryLabel}>Hora</Text><Text style={styles.summaryValue}>{slot}</Text></View>
<View style={styles.divider} />
<View style={styles.summaryRow}><Text style={styles.summaryLabel}>Total</Text><Text style={styles.summaryTotal}>{currency(selectedService?.price || 0)}</Text></View>
</Card>
<Text style={[styles.stepLabel, { marginTop: 24 }]}>Lembrete</Text>
<View style={styles.notifOptions}>
{[15, 30, 60, 120].map(m => (
<TouchableOpacity key={m} onPress={() => setReminderMinutes(m)} style={[styles.notifBtn, reminderMinutes === m && styles.notifBtnActive]}>
<Text style={[styles.notifBtnTxt, reminderMinutes === m && styles.notifBtnTxtActive]}>
{m >= 60 ? `${m/60}h antes` : `${m}m antes`}
</Text>
</TouchableOpacity>
))}
</View>
<Button style={{ marginTop: 32 }} onPress={submit}>Confirmar Agendamento</Button>
</View>
)}
</ScrollView>
</SafeAreaView>
);
}
@@ -323,167 +185,124 @@ export default function Booking() {
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
backgroundColor: '#0a0a0f',
},
mainScroll: {
flex: 1,
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingVertical: 10,
},
backBtn: {
width: 40,
height: 40,
borderRadius: 12,
backgroundColor: '#141420',
alignItems: 'center',
justifyContent: 'center',
},
backIcon: {
color: '#f8fafc',
fontSize: 20,
fontWeight: '700',
},
headerTitleBox: {
alignItems: 'center',
},
headerTitle: {
color: '#f8fafc',
fontSize: 18,
fontWeight: '800',
},
headerSubtitle: {
color: '#64748b',
fontSize: 12,
fontWeight: '600',
},
scrollContent: {
padding: 16,
padding: 20,
paddingBottom: 40,
},
stepContent: {
width: '100%',
stepBox: {
gap: 12,
},
stepTitle: {
fontSize: 22,
fontWeight: 'bold',
color: '#0f172a',
marginBottom: 20,
textAlign: 'center',
},
subTitle: {
fontSize: 14,
fontWeight: '700',
color: '#64748b',
stepLabel: {
color: '#94a3b8',
fontSize: 12,
fontWeight: '800',
textTransform: 'uppercase',
letterSpacing: 1,
marginTop: 10,
marginBottom: 12,
marginBottom: 8,
},
// Step 1 - Serviços
serviceGrid: {
gap: 12,
},
serviceCard: {
backgroundColor: '#fff',
borderRadius: 16,
padding: 20,
choiceCard: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
borderWidth: 2,
borderColor: 'transparent',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.05,
shadowRadius: 4,
elevation: 2,
},
serviceCardActive: {
borderColor: '#6366f1',
backgroundColor: '#f5f3ff',
},
serviceInfo: {
flex: 1,
},
serviceName: {
fontSize: 16,
fontWeight: '700',
color: '#1e293b',
},
serviceNameActive: {
color: '#6366f1',
},
serviceDuration: {
fontSize: 12,
color: '#94a3b8',
marginTop: 4,
},
servicePrice: {
fontSize: 16,
fontWeight: '800',
color: '#0f172a',
},
servicePriceActive: {
color: '#6366f1',
},
// Step 2 - Barbeiros
barberList: {
gap: 12,
},
barberItem: {
backgroundColor: '#fff',
borderRadius: 16,
padding: 16,
flexDirection: 'row',
alignItems: 'center',
borderWidth: 2,
gap: 14,
borderWidth: 1.5,
borderColor: 'transparent',
},
barberItemActive: {
choiceCardActive: {
borderColor: '#6366f1',
backgroundColor: '#f5f3ff',
backgroundColor: 'rgba(99,102,241,0.05)',
},
avatarPlaceholder: {
width: 50,
height: 50,
borderRadius: 25,
backgroundColor: '#f1f5f9',
alignItems: 'center',
justifyContent: 'center',
marginRight: 16,
},
avatarActive: {
backgroundColor: '#6366f1',
},
avatarText: {
fontSize: 20,
fontWeight: 'bold',
color: '#94a3b8',
},
barberDetails: {
flex: 1,
},
barberName: {
choiceName: {
color: '#f8fafc',
fontSize: 16,
fontWeight: '700',
color: '#1e293b',
},
barberNameActive: {
color: '#6366f1',
choiceMeta: {
color: '#64748b',
fontSize: 13,
},
barberSpecialty: {
fontSize: 12,
color: '#94a3b8',
choicePrice: {
color: '#f8fafc',
fontSize: 16,
fontWeight: '800',
},
// Step 3 - Data e Hora
dateScroll: {
marginBottom: 20,
paddingBottom: 4,
},
dateButton: {
width: 60,
height: 80,
backgroundColor: '#fff',
borderRadius: 16,
marginRight: 8,
avatarMini: {
width: 44,
height: 44,
borderRadius: 14,
backgroundColor: '#1c1c2e',
alignItems: 'center',
justifyContent: 'center',
borderWidth: 2,
borderColor: 'transparent',
},
dateButtonActive: {
borderColor: '#6366f1',
avatarTxt: {
color: '#6366f1',
fontSize: 18,
fontWeight: '900',
},
dateList: {
gap: 10,
},
dateItem: {
width: 64,
height: 80,
borderRadius: 16,
backgroundColor: '#141420',
alignItems: 'center',
justifyContent: 'center',
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.06)',
},
dateItemActive: {
backgroundColor: '#6366f1',
borderColor: '#6366f1',
},
dayName: {
dateWeek: {
color: '#475569',
fontSize: 10,
textTransform: 'uppercase',
color: '#94a3b8',
fontWeight: 'bold',
},
dayNameActive: {
color: '#fff',
opacity: 0.8,
},
dayNum: {
fontSize: 20,
fontWeight: '800',
color: '#1e293b',
},
dateDay: {
color: '#f8fafc',
fontSize: 20,
fontWeight: '900',
marginTop: 4,
},
dayNumActive: {
dateTextActive: {
color: '#fff',
},
slotsGrid: {
@@ -491,138 +310,84 @@ const styles = StyleSheet.create({
flexWrap: 'wrap',
gap: 10,
},
slotButton: {
width: '31%',
height: 50,
backgroundColor: '#fff',
slotItem: {
width: '22.5%',
height: 44,
borderRadius: 12,
backgroundColor: '#141420',
alignItems: 'center',
justifyContent: 'center',
borderWidth: 1,
borderColor: '#e2e8f0',
borderColor: 'rgba(255,255,255,0.06)',
},
slotActive: {
backgroundColor: '#1e293b',
borderColor: '#1e293b',
backgroundColor: '#6366f1',
borderColor: '#6366f1',
},
slotBooked: {
backgroundColor: '#f1f5f9',
borderColor: '#f1f5f9',
opacity: 0.5,
opacity: 0.2,
},
slotText: {
color: '#94a3b8',
fontSize: 14,
fontWeight: '700',
color: '#475569',
},
slotTextActive: {
color: '#fff',
},
slotTextBooked: {
textDecorationLine: 'line-through',
color: '#94a3b8',
},
emptyText: {
fontSize: 14,
color: '#94a3b8',
fontStyle: 'italic',
textAlign: 'center',
width: '100%',
padding: 20,
},
waitlistCard: {
marginTop: 30,
padding: 20,
backgroundColor: '#fff',
borderRadius: 20,
borderWidth: 1,
borderColor: '#fee2e2',
alignItems: 'center',
},
waitlistText: {
fontSize: 14,
color: '#b91c1c',
fontWeight: '600',
textAlign: 'center',
marginBottom: 16,
},
// Step 4 - Resumo
summaryCard: {
padding: 20,
marginTop: 0,
gap: 12,
},
summaryRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 12,
},
summaryLabel: {
fontSize: 14,
color: '#64748b',
fontSize: 14,
},
summaryValue: {
color: '#f8fafc',
fontSize: 14,
fontWeight: '700',
color: '#0f172a',
textAlign: 'right',
flex: 1,
marginLeft: 10,
},
divider: {
height: 1,
backgroundColor: '#e2e8f0',
marginVertical: 12,
backgroundColor: 'rgba(255,255,255,0.06)',
marginVertical: 4,
},
summaryTotal: {
color: '#a5b4fc',
fontSize: 22,
fontWeight: '900',
color: '#6366f1',
},
notificationSettings: {
marginTop: 24,
},
notifHelp: {
fontSize: 13,
color: '#64748b',
marginBottom: 16,
},
reminderOptions: {
notifOptions: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
gap: 10,
},
notifButton: {
paddingHorizontal: 12,
paddingVertical: 10,
notifBtn: {
flex: 1,
paddingVertical: 12,
borderRadius: 12,
backgroundColor: '#fff',
backgroundColor: '#141420',
alignItems: 'center',
borderWidth: 1,
borderColor: '#e2e8f0',
borderColor: 'rgba(255,255,255,0.06)',
},
notifButtonActive: {
backgroundColor: '#6366f1',
notifBtnActive: {
backgroundColor: 'rgba(99,102,241,0.15)',
borderColor: '#6366f1',
},
notifText: {
fontSize: 12,
fontWeight: 'bold',
notifBtnTxt: {
color: '#64748b',
fontSize: 12,
fontWeight: '700',
},
notifTextActive: {
color: '#fff',
},
// Footer
footer: {
padding: 16,
paddingBottom: 24,
backgroundColor: '#fff',
flexDirection: 'row',
gap: 12,
borderTopWidth: 1,
borderTopColor: '#e2e8f0',
},
footerButton: {
flex: 1,
notifBtnTxtActive: {
color: '#a5b4fc',
},
});

View File

@@ -1,55 +1,42 @@
import React from 'react';
import { View, Text, StyleSheet, ScrollView, Alert } from 'react-native';
import { View, Text, StyleSheet, ScrollView, Alert, TouchableOpacity } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useNavigation } from '@react-navigation/native';
import { useApp } from '../context/AppContext';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { Badge } from '../components/ui/Badge';
import { currency } from '../lib/format';
export default function Cart() {
const navigation = useNavigation();
// Obtém o estado global do carrinho, e as funções comunicadoras do AppContext (interface BD)
const { cart, shops, removeFromCart, placeOrder, user } = useApp();
// Renderiza um estado/view vazia intercetiva, caso o array "cart" esteja vazio
if (!cart.length) {
return (
<SafeAreaView style={styles.container}>
<Card style={styles.emptyCard}>
<Text style={styles.emptyText}>Sua Seleção está Deserta</Text>
</Card>
<View style={styles.emptyState}>
<Text style={styles.emptyIcon}>🛒</Text>
<Text style={styles.emptyTitle}>Carrinho vazio</Text>
<Text style={styles.emptyText}>Adicione serviços ou produtos para começar.</Text>
<Button variant="outline" style={{ marginTop: 20 }} onPress={() => navigation.navigate('Explore' as never)}>
Explorar Barbearias
</Button>
</View>
</SafeAreaView>
);
}
/**
* Lógica de Agrupamento de itens no Carrinho.
* A nível de negócio, como as encomendas (orders) são feitas por Barbearia (shopId),
* agrupamos para construir a interface dividida por Lojas caso de adicione itens mistos.
* @returns {Record<string, CartItem[]>} Dicionário indexado pelo shopId
*/
const grouped = cart.reduce<Record<string, typeof cart>>((acc, item) => {
acc[item.shopId] = acc[item.shopId] || [];
acc[item.shopId].push(item);
return acc;
}, {});
/**
* Acionado ao clicar em 'Finalizar pedido' para uma dada loja no JSX.
* Interliga através da função do Contexto à base de dados para materializar
* o agrupo de serviços selecionados e criar uma tupla na tabela de Pedidos/Marcações da db.
* @param {string} shopId - ID da loja que receberá o pedido final.
*/
const handleCheckout = async (shopId: string) => {
// Verificamos de forma segura pelo objeto user se o authState (sessão Supabase) existe
if (!user) {
Alert.alert('Sessão Necessária', 'Inicie sessão para confirmar o seu pedido');
navigation.navigate('Login' as never);
return;
}
// Gera a inserção na API (insert em tabelas de orders / ordem e dependentes)
const order = await placeOrder(user.id, shopId);
if (order) {
Alert.alert('Sucesso', 'Pedido criado com sucesso!');
@@ -59,17 +46,12 @@ export default function Cart() {
};
return (
// A página permite visibilidade escalonada num conteúdo flexível (ScrollView)
<SafeAreaView style={styles.container}>
<ScrollView contentContainerStyle={styles.content}>
<Text style={styles.title}>Minha Seleção</Text>
<Text style={styles.title}>Carrinho</Text>
{/* Renderiza dinamicamente 1 Card de Checkout por Loja agrupada no objeto `grouped` */}
{Object.entries(grouped).map(([shopId, items]) => {
// Mapeia o mock do objeto de loja baseado na primmary key `shopId`
const shop = shops.find((s) => s.id === shopId);
// Agregador quantitativo do array reduzindo o total financeiro calculado pelos preços da BD local
const total = items.reduce((sum, i) => {
const price =
i.type === 'service'
@@ -79,59 +61,57 @@ export default function Cart() {
}, 0);
return (
// Engloba os items duma só loja
<Card key={shopId} style={styles.shopCard}>
<View key={shopId} style={styles.shopCard}>
<View style={styles.shopHeader}>
<View>
{/* Consome o nome e morada do registo principal (Profile > Shop) na UI */}
<View style={styles.shopIcon}>
<Text style={styles.shopIconText}>{(shop?.name || 'B').charAt(0)}</Text>
</View>
<View style={{ flex: 1 }}>
<Text style={styles.shopName}>{shop?.name ?? 'Barbearia'}</Text>
<Text style={styles.shopAddress}>{shop?.address}</Text>
</View>
{/* Apresenta o custo transformado visualmente (ex: R$ / €) */}
<Text style={styles.total}>{currency(total)}</Text>
</View>
{/* Listagem linha a linha dos items (relacionados por foreign key 'refId') */}
<View style={styles.divider} />
{items.map((i) => {
// JOIN via frontend para resgatar o nome natural referenciado no menu original da Lojas
const ref =
i.type === 'service'
? shop?.services.find((s) => s.id === i.refId)
: shop?.products.find((p) => p.id === i.refId);
const price = ref?.price ?? 0;
return (
<View key={i.refId} style={styles.item}>
<Text style={styles.itemText}>
{/* Condicionamento estruturado na UI, mostra Serviço vs Produto perante a tipagem DB iterada */}
{i.type === 'service' ? 'Serviço: ' : 'Produto: '}
{ref?.name ?? 'Item'} x{i.qty}
</Text>
{/* Elimina de forma independente o registo não guardado da persistência AppContext/State */}
<Button
onPress={() => removeFromCart(i.refId)}
variant="ghost"
size="sm"
>
Remover
</Button>
<View style={styles.itemInfo}>
<View style={styles.typePill}>
<Text style={styles.typeText}>
{i.type === 'service' ? 'Serviço' : 'Produto'}
</Text>
</View>
<Text style={styles.itemName}>{ref?.name ?? 'Item'}</Text>
<Text style={styles.itemQty}>Qtd: {i.qty} · {currency(price * i.qty)}</Text>
</View>
<TouchableOpacity onPress={() => removeFromCart(i.refId)} style={styles.removeBtn}>
<Text style={styles.removeText}></Text>
</TouchableOpacity>
</View>
);
})}
{/* Renderização condicional no React para encaminhar fluxo para login se anónimo */}
{user ? (
<Button onPress={() => handleCheckout(shopId)} style={styles.checkoutButton}>
Finalizar Aquisição
</Button>
) : (
<Button
onPress={() => navigation.navigate('Login' as never)}
style={styles.checkoutButton}
>
Entrar para Adquirir
</Button>
)}
</Card>
<View style={styles.divider} />
<View style={styles.totalRow}>
<Text style={styles.totalLabel}>Total</Text>
<Text style={styles.totalValue}>{currency(total)}</Text>
</View>
<Button
onPress={() => (user ? handleCheckout(shopId) : navigation.navigate('Login' as never))}
style={styles.checkoutBtn}
>
{user ? 'Finalizar Aquisição' : 'Entrar para Adquirir'}
</Button>
</View>
);
})}
</ScrollView>
@@ -142,63 +122,150 @@ export default function Cart() {
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
backgroundColor: '#0a0a0f',
},
content: {
padding: 16,
padding: 20,
gap: 20,
paddingBottom: 32,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#0f172a',
marginBottom: 16,
fontSize: 32,
fontWeight: '900',
color: '#f8fafc',
marginBottom: 8,
},
emptyCard: {
padding: 32,
emptyState: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
padding: 40,
gap: 12,
},
emptyIcon: {
fontSize: 56,
marginBottom: 8,
},
emptyTitle: {
color: '#f8fafc',
fontSize: 22,
fontWeight: '800',
},
emptyText: {
fontSize: 16,
color: '#64748b',
fontSize: 15,
textAlign: 'center',
},
shopCard: {
backgroundColor: '#141420',
borderRadius: 24,
padding: 20,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.06)',
marginBottom: 16,
},
shopHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 12,
alignItems: 'center',
gap: 14,
},
shopIcon: {
width: 48,
height: 48,
borderRadius: 16,
backgroundColor: 'rgba(99,102,241,0.12)',
alignItems: 'center',
justifyContent: 'center',
},
shopIconText: {
color: '#818cf8',
fontSize: 18,
fontWeight: '900',
},
shopName: {
fontSize: 16,
fontWeight: 'bold',
color: '#0f172a',
fontSize: 18,
fontWeight: '800',
color: '#f8fafc',
},
shopAddress: {
fontSize: 12,
color: '#64748b',
fontSize: 13,
color: '#475569',
marginTop: 2,
},
total: {
fontSize: 18,
fontWeight: 'bold',
color: '#6366f1',
divider: {
height: 1,
backgroundColor: 'rgba(255,255,255,0.06)',
marginVertical: 16,
},
item: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 12,
},
itemInfo: {
flex: 1,
gap: 4,
},
typePill: {
alignSelf: 'flex-start',
backgroundColor: 'rgba(99,102,241,0.1)',
borderRadius: 8,
paddingHorizontal: 10,
paddingVertical: 3,
marginBottom: 4,
},
typeText: {
color: '#818cf8',
fontSize: 10,
fontWeight: '800',
textTransform: 'uppercase',
},
itemName: {
fontSize: 16,
fontWeight: '700',
color: '#f8fafc',
},
itemQty: {
fontSize: 13,
color: '#64748b',
fontWeight: '500',
},
removeBtn: {
width: 36,
height: 36,
borderRadius: 12,
backgroundColor: 'rgba(239,68,68,0.08)',
alignItems: 'center',
justifyContent: 'center',
marginLeft: 12,
},
removeText: {
color: '#ef4444',
fontSize: 14,
fontWeight: '800',
},
totalRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 8,
borderBottomWidth: 1,
borderBottomColor: '#e2e8f0',
marginBottom: 20,
},
itemText: {
totalLabel: {
color: '#94a3b8',
fontSize: 14,
color: '#64748b',
flex: 1,
fontWeight: '700',
textTransform: 'uppercase',
letterSpacing: 1,
},
checkoutButton: {
width: '100%',
marginTop: 12,
totalValue: {
color: '#a5b4fc',
fontSize: 26,
fontWeight: '900',
},
checkoutBtn: {
backgroundColor: '#6366f1',
borderRadius: 16,
paddingVertical: 16,
},
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,6 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useApp } from '../context/AppContext';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { Badge } from '../components/ui/Badge';
import { currency } from '../lib/format';
import { RootStackParamList } from '../navigation/types';
@@ -34,7 +33,7 @@ export default function ShopDetails() {
return (
<SafeAreaView style={styles.container}>
<View style={styles.centerState}>
<Text style={styles.centerTitle}>A carregar detalhes...</Text>
<Text style={styles.centerTitle}>Carregando...</Text>
</View>
</SafeAreaView>
);
@@ -44,11 +43,8 @@ export default function ShopDetails() {
return (
<SafeAreaView style={styles.container}>
<View style={styles.centerState}>
<Text style={styles.centerTitle}>Barbearia não encontrada</Text>
<Text style={styles.centerText}>O espaço pode ter sido removido ou o link está incorreto.</Text>
<Button style={styles.reserveButton} onPress={() => navigation.navigate('Explore')}>
Explorar outros espaços
</Button>
<Text style={styles.centerTitle}>Não encontrado</Text>
<Button onPress={() => navigation.navigate('Explore')}>Voltar</Button>
</View>
</SafeAreaView>
);
@@ -56,536 +52,402 @@ export default function ShopDetails() {
const openMap = () => {
const url = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(`${shop.name} ${shop.address}`)}`;
Linking.openURL(url).catch(() => Alert.alert('Erro', 'Não foi possível abrir o mapa.'));
Linking.openURL(url);
};
const reserveService = (serviceId?: string) => {
if (!user) {
navigation.navigate('Login');
return;
}
if (!user) { navigation.navigate('Login'); return; }
navigation.navigate('Booking', { shopId: shop.id, serviceId });
};
const addProduct = (productId: string) => {
if (!user) {
navigation.navigate('Login');
return;
}
if (!user) { navigation.navigate('Login'); return; }
addToCart({ shopId: shop.id, type: 'product', refId: productId, qty: 1 });
Alert.alert('Adicionado', 'Produto adicionado ao carrinho.');
Alert.alert('Sucesso', 'Produto adicionado ao carrinho.');
};
const schedule = shop.schedule || defaultSchedule;
const paymentMethods = shop.paymentMethods || ['Dinheiro', 'Cartão de Crédito', 'Cartão de Débito'];
const contacts = shop.contacts || { phone1: '252 048 754', phone2: '252 048 754' };
const currentDayIndex = new Date().getDay() === 0 ? 6 : new Date().getDay() - 1;
return (
<SafeAreaView style={styles.container}>
<ScrollView contentContainerStyle={styles.content}>
<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} />
) : (
<View style={styles.heroFallback}>
<Text style={styles.heroFallbackText}>SA</Text>
</View>
<View style={styles.heroPlaceholder}><Text style={styles.heroPlaceholderText}>💈</Text></View>
)}
<View style={styles.heroOverlay} />
<View style={styles.heroActions}>
<TouchableOpacity style={styles.heroAction} onPress={openMap}>
<Text style={styles.heroActionText}>Mapa</Text>
<SafeAreaView style={styles.heroHeader}>
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.backBtn}>
<Text style={styles.backIcon}></Text>
</TouchableOpacity>
<TouchableOpacity style={styles.heroAction} onPress={() => toggleFavorite(shop.id)}>
<Text style={[styles.heroActionText, isFavorite(shop.id) && styles.favoriteActive]}>
{isFavorite(shop.id) ? 'Favorito' : 'Guardar'}
<TouchableOpacity onPress={() => toggleFavorite(shop.id)} style={styles.favBtn}>
<Text style={[styles.favIcon, isFavorite(shop.id) && styles.favActive]}>
{isFavorite(shop.id) ? '' : ''}
</Text>
</TouchableOpacity>
</View>
<View style={styles.heroContent}>
<View style={styles.ratingPill}>
<Text style={styles.ratingText}>{(shop.rating || 0).toFixed(1)} Excelente</Text>
</SafeAreaView>
<View style={styles.heroBody}>
<View style={styles.ratingBox}>
<Text style={styles.ratingValue}> {shop.rating.toFixed(1)}</Text>
</View>
<Text style={styles.title} numberOfLines={2}>{shop.name}</Text>
<Text style={styles.address} numberOfLines={2}>{shop.address}</Text>
<Text style={styles.shopName}>{shop.name}</Text>
<TouchableOpacity onPress={openMap} style={styles.addrBox}>
<Text style={styles.shopAddr} numberOfLines={2}>📍 {shop.address}</Text>
</TouchableOpacity>
</View>
</View>
<View style={styles.tabShell}>
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.tabs}>
{/* Tabs Estilizadas */}
<View style={styles.tabSection}>
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.tabsScroll}>
{[
['servicos', 'Serviços'],
['barbeiros', 'Barbeiros'],
['produtos', 'Produtos'],
['detalhes', 'Detalhes'],
['barbeiros', 'Equipa'],
['produtos', 'Loja'],
['detalhes', 'Sobre'],
].map(([id, label]) => (
<TouchableOpacity
key={id}
onPress={() => setTab(id as Tab)}
style={[styles.tab, tab === id && styles.tabActive]}
style={[styles.tabItem, tab === id && styles.tabItemActive]}
>
<Text style={[styles.tabText, tab === id && styles.tabTextActive]}>{label}</Text>
</TouchableOpacity>
))}
</ScrollView>
<Text style={styles.summary}>{shop.services.length} serviços · {shop.barbers.length} barbeiros</Text>
</View>
{tab === 'servicos' && (
<View style={styles.grid}>
{shop.services.map((service) => (
<Card key={service.id} style={styles.serviceCard}>
<View style={styles.itemTop}>
<View style={styles.itemInfo}>
<Text style={styles.itemTitle}>{service.name}</Text>
<Text style={styles.itemMeta}>{service.duration} min · Lugar disponível hoje</Text>
{/* Listagem de Conteúdo */}
<View style={styles.contentArea}>
{tab === 'servicos' && (
<View style={styles.grid}>
{shop.services.map((s) => (
<Card key={s.id} style={styles.serviceCard}>
<View style={styles.svcInfo}>
<Text style={styles.svcName}>{s.name}</Text>
<Text style={styles.svcMeta}>{s.duration} min · {currency(s.price)}</Text>
</View>
<Text style={styles.price}>{currency(service.price)}</Text>
</View>
<Button style={styles.reserveButton} onPress={() => reserveService(service.id)}>
Reservar
</Button>
</Card>
))}
</View>
)}
<Button size="sm" onPress={() => reserveService(s.id)}>Agendar</Button>
</Card>
))}
</View>
)}
{tab === 'barbeiros' && (
<Card style={styles.panel}>
<View style={styles.barberGrid}>
{shop.barbers.length === 0 ? (
<Text style={styles.emptyText}>Esta barbearia ainda não registou barbeiros.</Text>
) : shop.barbers.map((barber) => (
<View key={barber.id} style={styles.barberItem}>
{barber.imageUrl ? (
<Image source={{ uri: barber.imageUrl }} style={styles.avatarImage} />
) : (
<View style={styles.avatar}>
<Text style={styles.avatarText}>{barber.name.charAt(0).toUpperCase()}</Text>
</View>
)}
<Text style={styles.barberName} numberOfLines={1}>{barber.name}</Text>
<Text style={styles.barberSpecialty} numberOfLines={1}>
{barber.specialties[0] || 'Especialista'}
{tab === 'barbeiros' && (
<View style={styles.barberList}>
{shop.barbers.map((b) => (
<Card key={b.id} style={styles.barberCard}>
<View style={styles.barberAvatar}>
<Text style={styles.avatarTxt}>{b.name.charAt(0)}</Text>
</View>
<View style={{ flex: 1 }}>
<Text style={styles.barberName}>{b.name}</Text>
<Text style={styles.barberSpecs}>{b.specialties.join(', ')}</Text>
</View>
</Card>
))}
</View>
)}
{tab === 'produtos' && (
<View style={styles.grid}>
{shop.products.map((p) => (
<Card key={p.id} style={styles.productCard}>
<View style={styles.prodHeader}>
<Text style={styles.prodName}>{p.name}</Text>
<Text style={styles.prodPrice}>{currency(p.price)}</Text>
</View>
<Text style={styles.prodStock}>{p.stock} em stock</Text>
<Button size="sm" variant="outline" onPress={() => addProduct(p.id)} disabled={p.stock <= 0}>
{p.stock > 0 ? 'Adicionar' : 'Esgotado'}
</Button>
</Card>
))}
</View>
)}
{tab === 'detalhes' && (
<Card style={styles.detailsBox}>
<Text style={styles.detailTitle}>Horário</Text>
{schedule.map((s, idx) => (
<View key={s.day} style={styles.schedRow}>
<Text style={[styles.schedDay, idx === currentDayIndex && styles.today]}>{s.day}</Text>
<Text style={[styles.schedTime, idx === currentDayIndex && styles.today]}>
{s.closed ? 'Fechado' : `${s.open} - ${s.close}`}
</Text>
</View>
))}
</View>
</Card>
)}
{tab === 'produtos' && (
<View style={styles.productGrid}>
{shop.products.map((product) => {
const lowStock = product.stock <= 3;
return (
<Card key={product.id} style={styles.productCard}>
<View style={styles.productIcon}>
<Text style={styles.productIconText}>P</Text>
{lowStock && <Badge color="indigo" style={styles.lowStock}>Últimas</Badge>}
</View>
<Text style={styles.productTitle} numberOfLines={1}>{product.name}</Text>
<Text style={styles.productStock}>{product.stock} em stock</Text>
<Text style={styles.productPrice}>{currency(product.price)}</Text>
<Button
size="sm"
style={styles.reserveButton}
disabled={product.stock <= 0}
onPress={() => addProduct(product.id)}
>
{product.stock > 0 ? 'Adicionar' : 'Esgotado'}
</Button>
</Card>
);
})}
</View>
)}
{tab === 'detalhes' && (
<Card style={styles.panel}>
<Text style={styles.detailTitle}>Horário de atendimento</Text>
{schedule.map((slot, index) => (
<View key={slot.day} style={styles.scheduleRow}>
<View style={styles.scheduleDay}>
<Text style={[styles.scheduleDayText, index === currentDayIndex && styles.todayText]}>{slot.day}</Text>
{index === currentDayIndex && <Text style={styles.todayBadge}>Hoje</Text>}
</View>
<Text style={styles.scheduleTime}>{slot.closed ? 'Fechado' : `${slot.open} - ${slot.close}`}</Text>
</View>
))}
<View style={styles.divider} />
<Text style={styles.detailTitle}>Formas de pagamento</Text>
<View style={styles.paymentList}>
{paymentMethods.map((method) => (
<Text key={method} style={styles.paymentChip}>{method}</Text>
))}
</View>
<View style={styles.divider} />
<Text style={styles.detailTitle}>Contacto</Text>
{[contacts.phone1, contacts.phone2].filter(Boolean).map((phone) => (
<TouchableOpacity key={phone} style={styles.phoneCard} onPress={() => Linking.openURL(`tel:${String(phone).replace(/\D/g, '')}`)}>
<Text style={styles.phoneText}>{phone}</Text>
<Text style={styles.phoneAction}>Ligar</Text>
<View style={styles.divider} />
<Text style={styles.detailTitle}>Contacto</Text>
<TouchableOpacity onPress={() => Linking.openURL(`tel:${shop.contacts?.phone1}`)}>
<Text style={styles.contactLink}>{shop.contacts?.phone1 || 'Não disponível'}</Text>
</TouchableOpacity>
))}
</Card>
)}
</Card>
)}
</View>
</ScrollView>
</SafeAreaView>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
backgroundColor: '#0a0a0f',
},
content: {
padding: 16,
gap: 16,
},
centerState: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
padding: 24,
gap: 12,
},
centerTitle: {
color: '#0f172a',
fontSize: 22,
fontWeight: '900',
textAlign: 'center',
},
centerText: {
color: '#64748b',
textAlign: 'center',
fontWeight: '500',
scrollContent: {
paddingBottom: 40,
},
hero: {
minHeight: 340,
borderRadius: 28,
overflow: 'hidden',
backgroundColor: '#0f172a',
height: 380,
position: 'relative',
},
heroImage: {
...StyleSheet.absoluteFillObject,
width: '100%',
height: '100%',
},
heroFallback: {
...StyleSheet.absoluteFillObject,
heroPlaceholder: {
width: '100%',
height: '100%',
backgroundColor: '#141420',
alignItems: 'center',
justifyContent: 'center',
},
heroFallbackText: {
color: '#818cf8',
fontSize: 42,
fontWeight: '900',
heroPlaceholderText: {
fontSize: 64,
},
heroOverlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(2,6,23,0.48)',
backgroundColor: 'rgba(10,10,15,0.5)',
},
heroActions: {
heroHeader: {
position: 'absolute',
right: 14,
top: 14,
top: 0,
left: 0,
right: 0,
flexDirection: 'row',
gap: 8,
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingTop: 10,
},
heroAction: {
backgroundColor: 'rgba(255,255,255,0.92)',
borderRadius: 14,
paddingHorizontal: 12,
paddingVertical: 9,
backBtn: {
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: 'rgba(255,255,255,0.1)',
alignItems: 'center',
justifyContent: 'center',
},
heroActionText: {
color: '#0f172a',
fontSize: 11,
fontWeight: '900',
backIcon: {
color: '#fff',
fontSize: 20,
fontWeight: '700',
},
favoriteActive: {
color: '#e11d48',
favBtn: {
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: 'rgba(255,255,255,0.1)',
alignItems: 'center',
justifyContent: 'center',
},
heroContent: {
favIcon: {
color: '#fff',
fontSize: 22,
},
favActive: {
color: '#ef4444',
},
heroBody: {
position: 'absolute',
bottom: 22,
left: 18,
right: 18,
gap: 8,
bottom: 30,
left: 20,
right: 20,
gap: 10,
},
ratingPill: {
ratingBox: {
alignSelf: 'flex-start',
backgroundColor: 'rgba(15,23,42,0.52)',
borderColor: 'rgba(255,255,255,0.18)',
borderWidth: 1,
borderRadius: 999,
backgroundColor: 'rgba(99,102,241,0.9)',
borderRadius: 8,
paddingHorizontal: 10,
paddingVertical: 5,
paddingVertical: 4,
},
ratingText: {
ratingValue: {
color: '#fff',
fontSize: 11,
fontWeight: '900',
textTransform: 'uppercase',
},
title: {
color: '#fff',
fontSize: 34,
lineHeight: 36,
fontWeight: '900',
},
address: {
color: 'rgba(255,255,255,0.9)',
fontSize: 15,
fontWeight: '600',
},
tabShell: {
backgroundColor: 'rgba(255,255,255,0.72)',
borderRadius: 24,
padding: 8,
gap: 8,
},
tabs: {
gap: 8,
},
tab: {
borderRadius: 16,
paddingHorizontal: 16,
paddingVertical: 11,
backgroundColor: '#f1f5f9',
},
tabActive: {
backgroundColor: '#0f172a',
},
tabText: {
color: '#475569',
fontSize: 12,
fontWeight: '900',
},
tabTextActive: {
color: '#818cf8',
},
summary: {
backgroundColor: '#fff',
color: '#94a3b8',
borderRadius: 16,
padding: 11,
fontSize: 11,
shopName: {
color: '#fff',
fontSize: 32,
fontWeight: '900',
textTransform: 'uppercase',
},
addrBox: {
opacity: 0.8,
},
shopAddr: {
color: '#f8fafc',
fontSize: 14,
fontWeight: '500',
},
tabSection: {
backgroundColor: '#141420',
paddingVertical: 6,
},
tabsScroll: {
paddingHorizontal: 20,
gap: 16,
},
tabItem: {
paddingVertical: 14,
paddingHorizontal: 4,
borderBottomWidth: 2,
borderBottomColor: 'transparent',
},
tabItemActive: {
borderBottomColor: '#6366f1',
},
tabText: {
color: '#475569',
fontSize: 14,
fontWeight: '700',
},
tabTextActive: {
color: '#f8fafc',
},
contentArea: {
padding: 20,
},
grid: {
gap: 12,
},
serviceCard: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
gap: 12,
},
svcInfo: {
flex: 1,
gap: 4,
},
svcName: {
color: '#f8fafc',
fontSize: 16,
fontWeight: '800',
},
svcMeta: {
color: '#64748b',
fontSize: 13,
},
barberList: {
gap: 12,
},
barberCard: {
flexDirection: 'row',
alignItems: 'center',
padding: 14,
gap: 14,
},
itemTop: {
flexDirection: 'row',
alignItems: 'flex-start',
gap: 10,
},
itemInfo: {
flex: 1,
gap: 6,
},
itemTitle: {
color: '#0f172a',
fontSize: 18,
fontWeight: '900',
textTransform: 'uppercase',
},
itemMeta: {
color: '#64748b',
fontSize: 12,
fontWeight: '700',
},
price: {
color: '#0f172a',
backgroundColor: '#eef2ff',
borderRadius: 12,
paddingHorizontal: 10,
paddingVertical: 6,
fontSize: 16,
fontWeight: '900',
},
reserveButton: {
backgroundColor: '#0f172a',
},
panel: {
padding: 18,
},
barberGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 16,
},
barberItem: {
width: '29%',
minWidth: 92,
alignItems: 'center',
gap: 6,
},
avatar: {
width: 74,
height: 74,
borderRadius: 999,
backgroundColor: '#e2e8f0',
barberAvatar: {
width: 50,
height: 50,
borderRadius: 16,
backgroundColor: '#1c1c2e',
alignItems: 'center',
justifyContent: 'center',
},
avatarImage: {
width: 74,
height: 74,
borderRadius: 999,
},
avatarText: {
color: '#64748b',
fontSize: 24,
avatarTxt: {
color: '#6366f1',
fontSize: 20,
fontWeight: '900',
},
barberName: {
color: '#0f172a',
fontWeight: '900',
textAlign: 'center',
color: '#f8fafc',
fontSize: 16,
fontWeight: '800',
},
barberSpecialty: {
color: '#94a3b8',
barberSpecs: {
color: '#475569',
fontSize: 12,
textAlign: 'center',
fontWeight: '600',
},
productGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 12,
},
productCard: {
width: '48%',
padding: 12,
padding: 16,
gap: 8,
},
productIcon: {
aspectRatio: 1,
borderRadius: 18,
backgroundColor: '#f8fafc',
alignItems: 'center',
justifyContent: 'center',
},
productIconText: {
color: '#cbd5e1',
fontSize: 36,
fontWeight: '900',
},
lowStock: {
position: 'absolute',
top: 8,
left: 8,
},
productTitle: {
color: '#0f172a',
fontSize: 14,
fontWeight: '900',
textTransform: 'uppercase',
},
productStock: {
color: '#94a3b8',
fontSize: 11,
fontWeight: '900',
textTransform: 'uppercase',
},
productPrice: {
color: '#0f172a',
fontSize: 18,
fontWeight: '900',
},
emptyText: {
color: '#64748b',
fontWeight: '600',
},
detailTitle: {
color: '#0f172a',
fontSize: 19,
fontWeight: '900',
marginBottom: 12,
},
scheduleRow: {
prodHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
gap: 12,
paddingVertical: 7,
alignItems: 'flex-start',
},
scheduleDay: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
prodName: {
color: '#f8fafc',
fontSize: 16,
fontWeight: '800',
flex: 1,
},
scheduleDayText: {
prodPrice: {
color: '#a5b4fc',
fontSize: 16,
fontWeight: '900',
},
prodStock: {
color: '#475569',
fontSize: 12,
marginBottom: 4,
},
detailsBox: {
padding: 20,
gap: 12,
},
detailTitle: {
color: '#f8fafc',
fontSize: 18,
fontWeight: '800',
marginBottom: 8,
},
schedRow: {
flexDirection: 'row',
justifyContent: 'space-between',
},
schedDay: {
color: '#64748b',
fontWeight: '700',
fontSize: 14,
},
todayText: {
color: '#4f46e5',
fontWeight: '900',
schedTime: {
color: '#f8fafc',
fontSize: 14,
fontWeight: '600',
},
todayBadge: {
color: '#4338ca',
backgroundColor: '#e0e7ff',
borderRadius: 8,
paddingHorizontal: 7,
paddingVertical: 2,
fontSize: 10,
fontWeight: '900',
},
scheduleTime: {
color: '#334155',
today: {
color: '#6366f1',
fontWeight: '800',
},
divider: {
height: 1,
backgroundColor: '#e2e8f0',
marginVertical: 18,
backgroundColor: 'rgba(255,255,255,0.06)',
marginVertical: 12,
},
paymentList: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
},
paymentChip: {
backgroundColor: '#fff',
borderColor: '#e2e8f0',
borderWidth: 1,
color: '#334155',
borderRadius: 999,
paddingHorizontal: 12,
paddingVertical: 8,
contactLink: {
color: '#818cf8',
fontSize: 16,
fontWeight: '700',
},
phoneCard: {
backgroundColor: '#fff',
borderColor: '#e2e8f0',
borderWidth: 1,
borderRadius: 16,
padding: 14,
flexDirection: 'row',
centerState: {
flex: 1,
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 10,
justifyContent: 'center',
padding: 40,
},
phoneText: {
color: '#334155',
fontSize: 16,
fontWeight: '900',
},
phoneAction: {
color: '#4f46e5',
fontWeight: '900',
centerTitle: {
color: '#f8fafc',
fontSize: 18,
fontWeight: '800',
},
});