refactor: simplify booking flow and update status bar appearance
This commit is contained in:
2
App.tsx
2
App.tsx
@@ -9,7 +9,7 @@ export default function App() {
|
||||
<SafeAreaProvider>
|
||||
<AppProvider>
|
||||
<AppNavigator />
|
||||
<StatusBar style="auto" />
|
||||
<StatusBar style="light" />
|
||||
</AppProvider>
|
||||
</SafeAreaProvider>
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user