refactor: implement bottom tab navigation and update UI component design system

This commit is contained in:
2026-05-08 10:40:12 +01:00
parent 8f5a88788c
commit 20e9e23938
8 changed files with 593 additions and 487 deletions

View File

@@ -36,7 +36,7 @@ export const Button = ({ children, onPress, variant = 'solid', size = 'md', disa
activeOpacity={0.7}
>
{loading ? (
<ActivityIndicator size="small" color={variant === 'solid' ? '#fff' : '#6366f1'} />
<ActivityIndicator size="small" color={variant === 'solid' ? '#fff' : '#818cf8'} />
) : (
<Text style={textStyles}>{children}</Text>
)}
@@ -46,7 +46,7 @@ export const Button = ({ children, onPress, variant = 'solid', size = 'md', disa
const styles = StyleSheet.create({
base: {
borderRadius: 8,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
@@ -56,38 +56,38 @@ const styles = StyleSheet.create({
},
outline: {
backgroundColor: 'transparent',
borderWidth: 2,
borderColor: '#6366f1',
borderWidth: 1.5,
borderColor: 'rgba(99,102,241,0.4)',
},
ghost: {
backgroundColor: 'transparent',
},
size_sm: {
paddingHorizontal: 12,
paddingHorizontal: 14,
paddingVertical: 8,
},
size_md: {
paddingHorizontal: 16,
paddingVertical: 10,
paddingHorizontal: 18,
paddingVertical: 12,
},
size_lg: {
paddingHorizontal: 24,
paddingVertical: 14,
paddingVertical: 16,
},
disabled: {
opacity: 0.5,
opacity: 0.4,
},
text: {
fontWeight: '600',
fontWeight: '700',
},
text_solid: {
color: '#fff',
},
text_outline: {
color: '#6366f1',
color: '#a5b4fc',
},
text_ghost: {
color: '#6366f1',
color: '#a5b4fc',
},
textSize_sm: {
fontSize: 12,
@@ -99,5 +99,3 @@ const styles = StyleSheet.create({
fontSize: 16,
},
});

View File

@@ -12,16 +12,10 @@ export const Card = ({ children, style }: Props) => {
const styles = StyleSheet.create({
card: {
backgroundColor: '#fff',
borderRadius: 12,
backgroundColor: '#141420',
borderRadius: 20,
padding: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
elevation: 2,
borderWidth: 1,
borderColor: '#e2e8f0',
borderColor: 'rgba(255,255,255,0.06)',
},
});

View File

@@ -12,7 +12,7 @@ export const Input = ({ label, error, style, ...props }: Props) => {
{label && <Text style={styles.label}>{label}</Text>}
<TextInput
style={[styles.input, error && styles.inputError, style]}
placeholderTextColor="#94a3b8"
placeholderTextColor="#475569"
{...props}
/>
{error && <Text style={styles.error}>{error}</Text>}
@@ -25,20 +25,22 @@ const styles = StyleSheet.create({
marginBottom: 16,
},
label: {
fontSize: 14,
fontWeight: '600',
color: '#334155',
marginBottom: 6,
fontSize: 12,
fontWeight: '700',
color: '#94a3b8',
textTransform: 'uppercase',
letterSpacing: 0.5,
marginBottom: 8,
},
input: {
borderWidth: 1,
borderColor: '#cbd5e1',
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
fontSize: 14,
color: '#0f172a',
backgroundColor: '#fff',
borderColor: 'rgba(255,255,255,0.08)',
borderRadius: 14,
paddingHorizontal: 16,
paddingVertical: 14,
fontSize: 15,
color: '#f8fafc',
backgroundColor: '#1c1c2e',
},
inputError: {
borderColor: '#ef4444',
@@ -46,8 +48,6 @@ const styles = StyleSheet.create({
error: {
fontSize: 12,
color: '#ef4444',
marginTop: 4,
marginTop: 6,
},
});

View File

@@ -1,8 +1,9 @@
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { View, Text, StyleSheet, Platform } from 'react-native';
import { useApp } from '../context/AppContext';
import Landing from '../pages/Landing';
import AuthLogin from '../pages/AuthLogin';
import AuthRegister from '../pages/AuthRegister';
import Explore from '../pages/Explore';
@@ -12,48 +13,204 @@ import Cart from '../pages/Cart';
import Profile from '../pages/Profile';
import Dashboard from '../pages/Dashboard';
import EventsCreate from '../pages/EventsCreate';
import { RootStackParamList } from './types';
const Stack = createNativeStackNavigator<RootStackParamList>();
const Stack = createNativeStackNavigator();
const Tab = createBottomTabNavigator();
const TabIcon = ({ icon, focused }: { icon: string; focused: boolean }) => (
<View style={[iconStyles.wrap, focused && iconStyles.wrapActive]}>
<Text style={iconStyles.emoji}>{icon}</Text>
</View>
);
/* ── Client Tab Stacks ── */
function ExploreStack() {
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Explore" component={Explore} />
<Stack.Screen name="ShopDetails" component={ShopDetails} />
<Stack.Screen name="Booking" component={Booking} />
</Stack.Navigator>
);
}
function CartStack() {
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Cart" component={Cart} />
</Stack.Navigator>
);
}
function ClientProfileStack() {
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Profile" component={Profile} />
<Stack.Screen name="EventsCreate" component={EventsCreate} />
<Stack.Screen name="ShopDetails" component={ShopDetails} />
<Stack.Screen name="Booking" component={Booking} />
</Stack.Navigator>
);
}
function ClientTabs() {
const { cart } = useApp();
return (
<Tab.Navigator
screenOptions={{
headerShown: false,
tabBarStyle: tabStyles.bar,
tabBarActiveTintColor: '#a5b4fc',
tabBarInactiveTintColor: '#475569',
tabBarLabelStyle: tabStyles.label,
}}
>
<Tab.Screen
name="ExploreTab"
component={ExploreStack}
options={{
tabBarLabel: 'Explorar',
tabBarIcon: ({ focused }) => <TabIcon icon="◉" focused={focused} />,
}}
/>
<Tab.Screen
name="CartTab"
component={CartStack}
options={{
tabBarLabel: 'Carrinho',
tabBarIcon: ({ focused }) => <TabIcon icon="◫" focused={focused} />,
tabBarBadge: cart.length > 0 ? cart.length : undefined,
tabBarBadgeStyle: tabStyles.badge,
}}
/>
<Tab.Screen
name="ProfileTab"
component={ClientProfileStack}
options={{
tabBarLabel: 'Perfil',
tabBarIcon: ({ focused }) => <TabIcon icon="◎" focused={focused} />,
}}
/>
</Tab.Navigator>
);
}
/* ── Barbearia Tab Stacks ── */
function DashboardStack() {
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Dashboard" component={Dashboard} />
<Stack.Screen name="EventsCreate" component={EventsCreate} />
</Stack.Navigator>
);
}
function BarberProfileStack() {
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Profile" component={Profile} />
<Stack.Screen name="EventsCreate" component={EventsCreate} />
</Stack.Navigator>
);
}
function BarbeariaTabs() {
return (
<Tab.Navigator
screenOptions={{
headerShown: false,
tabBarStyle: tabStyles.bar,
tabBarActiveTintColor: '#a5b4fc',
tabBarInactiveTintColor: '#475569',
tabBarLabelStyle: tabStyles.label,
}}
>
<Tab.Screen
name="DashboardTab"
component={DashboardStack}
options={{
tabBarLabel: 'Painel',
tabBarIcon: ({ focused }) => <TabIcon icon="▦" focused={focused} />,
}}
/>
<Tab.Screen
name="BarberProfileTab"
component={BarberProfileStack}
options={{
tabBarLabel: 'Perfil',
tabBarIcon: ({ focused }) => <TabIcon icon="◎" focused={focused} />,
}}
/>
</Tab.Navigator>
);
}
/* ── Root Navigator ── */
export default function AppNavigator() {
const { user } = useApp();
return (
<NavigationContainer>
<Stack.Navigator
screenOptions={{
headerStyle: { backgroundColor: '#6366f1' },
headerTintColor: '#fff',
headerTitleStyle: { fontWeight: '700' },
}}
>
{!user ? (
<>
<Stack.Screen name="Landing" component={Landing} options={{ headerShown: false }} />
<Stack.Screen name="Login" component={AuthLogin} options={{ title: 'Entrar' }} />
<Stack.Screen name="Register" component={AuthRegister} options={{ title: 'Criar Conta' }} />
<Stack.Screen name="Explore" component={Explore} options={{ title: 'Explorar' }} />
<Stack.Screen name="ShopDetails" component={ShopDetails} options={{ title: 'Detalhes' }} />
</>
) : user.role === 'barbearia' ? (
<>
<Stack.Screen name="Dashboard" component={Dashboard} options={{ title: 'Painel', headerShown: false }} />
<Stack.Screen name="Profile" component={Profile} options={{ title: 'Perfil' }} />
<Stack.Screen name="EventsCreate" component={EventsCreate} options={{ title: 'Criar evento' }} />
</>
) : (
<>
<Stack.Screen name="Explore" component={Explore} options={{ title: 'Explorar' }} />
<Stack.Screen name="ShopDetails" component={ShopDetails} options={{ title: 'Detalhes' }} />
<Stack.Screen name="Booking" component={Booking} options={{ title: 'Agendar' }} />
<Stack.Screen name="Cart" component={Cart} options={{ title: 'Carrinho' }} />
<Stack.Screen name="Profile" component={Profile} options={{ title: 'Perfil' }} />
<Stack.Screen name="EventsCreate" component={EventsCreate} options={{ title: 'Criar evento' }} />
</>
)}
</Stack.Navigator>
{!user ? (
<Stack.Navigator
screenOptions={{
headerStyle: { backgroundColor: '#0a0a0f' },
headerTintColor: '#f8fafc',
headerTitleStyle: { fontWeight: '700' },
}}
>
<Stack.Screen name="Login" component={AuthLogin} options={{ headerShown: false }} />
<Stack.Screen name="Register" component={AuthRegister} options={{ headerShown: false }} />
<Stack.Screen name="Explore" component={Explore} options={{ headerShown: false }} />
<Stack.Screen name="ShopDetails" component={ShopDetails} options={{ headerShown: false }} />
</Stack.Navigator>
) : user.role === 'barbearia' ? (
<BarbeariaTabs />
) : (
<ClientTabs />
)}
</NavigationContainer>
);
}
const tabStyles = StyleSheet.create({
bar: {
backgroundColor: '#0a0a0f',
borderTopColor: 'rgba(255,255,255,0.06)',
borderTopWidth: 1,
height: Platform.OS === 'ios' ? 88 : 64,
paddingBottom: Platform.OS === 'ios' ? 28 : 8,
paddingTop: 8,
},
label: {
fontSize: 10,
fontWeight: '700',
letterSpacing: 0.5,
},
badge: {
backgroundColor: '#6366f1',
color: '#fff',
fontSize: 10,
fontWeight: '800',
minWidth: 18,
height: 18,
lineHeight: 14,
},
});
const iconStyles = StyleSheet.create({
wrap: {
width: 32,
height: 32,
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center',
},
wrapActive: {
backgroundColor: 'rgba(99,102,241,0.15)',
},
emoji: {
fontSize: 18,
color: '#94a3b8',
},
});

View File

@@ -1,5 +1,4 @@
export type RootStackParamList = {
Landing: undefined;
Login: undefined;
Register: undefined;
Explore: undefined;

View File

@@ -6,7 +6,6 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useApp } from '../context/AppContext';
import { Button } from '../components/ui/Button';
import { Input } from '../components/ui/Input';
import { Card } from '../components/ui/Card';
import { RootStackParamList } from '../navigation/types';
export default function AuthLogin() {
@@ -23,7 +22,6 @@ export default function AuthLogin() {
setError('Preenche email e palavra-passe');
return;
}
try {
setLoading(true);
const nextUser = await login(email.trim(), password);
@@ -31,8 +29,6 @@ export default function AuthLogin() {
setError('Credenciais inválidas ou email não confirmado');
return;
}
navigation.replace(nextUser.role === 'barbearia' ? 'Dashboard' : 'Explore');
} catch (e: any) {
const message = e?.message || 'Credenciais inválidas ou email não confirmado';
setError(message);
@@ -45,50 +41,54 @@ export default function AuthLogin() {
return (
<SafeAreaView style={styles.container}>
<ScrollView contentContainerStyle={styles.content} keyboardShouldPersistTaps="handled">
<Card style={styles.card}>
<TouchableOpacity style={styles.iconBox} onPress={() => navigation.navigate('Landing')}>
<Text style={styles.iconText}>SA</Text>
</TouchableOpacity>
<Text style={styles.title}>Bem-vindo</Text>
<Text style={styles.subtitle}>Aceda à sua conta Smart Agenda</Text>
{!!error && <Text style={styles.error}>{error}</Text>}
<Input
label="Email"
value={email}
onChangeText={(text) => {
setEmail(text);
setError('');
}}
keyboardType="email-address"
autoCapitalize="none"
placeholder="exemplo@email.com"
/>
<Input
label="Palavra-passe"
value={password}
onChangeText={(text) => {
setPassword(text);
setError('');
}}
secureTextEntry
placeholder="••••••••"
/>
<Button onPress={handleSubmit} style={styles.submitButton} loading={loading}>
Entrar na Conta
</Button>
<View style={styles.footer}>
<Text style={styles.footerText}>Ainda não tem conta? </Text>
<Text style={styles.footerLink} onPress={() => navigation.navigate('Register')}>
Criar conta grátis
</Text>
<View style={styles.logoRow}>
<View style={styles.logoBox}>
<Text style={styles.logoText}>SA</Text>
</View>
</Card>
</View>
<Text style={styles.title}>Bem-vindo</Text>
<Text style={styles.subtitle}>Aceda à sua conta Smart Agenda</Text>
{!!error && (
<View style={styles.errorBox}>
<Text style={styles.errorText}>{error}</Text>
</View>
)}
<Input
label="Email"
value={email}
onChangeText={(text) => { setEmail(text); setError(''); }}
keyboardType="email-address"
autoCapitalize="none"
placeholder="exemplo@email.com"
/>
<Input
label="Palavra-passe"
value={password}
onChangeText={(text) => { setPassword(text); setError(''); }}
secureTextEntry
placeholder="••••••••"
/>
<Button onPress={handleSubmit} style={styles.submitButton} loading={loading}>
Entrar na Conta
</Button>
<TouchableOpacity style={styles.guestButton} onPress={() => navigation.navigate('Explore')}>
<Text style={styles.guestText}>Explorar sem conta</Text>
</TouchableOpacity>
<View style={styles.divider} />
<View style={styles.footer}>
<Text style={styles.footerText}>Ainda não tem conta? </Text>
<Text style={styles.footerLink} onPress={() => navigation.navigate('Register')}>
Criar conta grátis
</Text>
</View>
</ScrollView>
</SafeAreaView>
);
@@ -97,68 +97,84 @@ export default function AuthLogin() {
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
backgroundColor: '#0a0a0f',
},
content: {
padding: 16,
padding: 24,
justifyContent: 'center',
minHeight: '100%',
},
card: {
padding: 24,
logoRow: {
alignItems: 'center',
marginBottom: 32,
},
iconBox: {
width: 64,
height: 64,
borderRadius: 18,
backgroundColor: '#0f172a',
logoBox: {
width: 72,
height: 72,
borderRadius: 22,
backgroundColor: '#141420',
borderWidth: 1,
borderColor: 'rgba(99,102,241,0.3)',
alignItems: 'center',
justifyContent: 'center',
alignSelf: 'center',
marginBottom: 18,
},
iconText: {
logoText: {
color: '#818cf8',
fontSize: 18,
fontSize: 22,
fontWeight: '900',
},
title: {
fontSize: 30,
fontSize: 34,
fontWeight: '900',
color: '#0f172a',
color: '#f8fafc',
textAlign: 'center',
marginBottom: 6,
},
subtitle: {
fontSize: 14,
fontSize: 15,
color: '#64748b',
marginBottom: 24,
marginBottom: 32,
textAlign: 'center',
fontWeight: '500',
},
error: {
backgroundColor: '#fff1f2',
borderColor: '#fecdd3',
errorBox: {
backgroundColor: 'rgba(239,68,68,0.1)',
borderColor: 'rgba(239,68,68,0.3)',
borderWidth: 1,
borderRadius: 14,
color: '#be123c',
padding: 12,
padding: 14,
marginBottom: 16,
},
errorText: {
color: '#fca5a5',
fontWeight: '600',
fontSize: 13,
},
submitButton: {
width: '100%',
marginTop: 8,
backgroundColor: '#0f172a',
marginTop: 4,
backgroundColor: '#6366f1',
paddingVertical: 16,
},
guestButton: {
alignSelf: 'center',
marginTop: 16,
paddingVertical: 10,
},
guestText: {
color: '#64748b',
fontWeight: '700',
fontSize: 14,
},
divider: {
height: 1,
backgroundColor: 'rgba(255,255,255,0.06)',
marginVertical: 24,
},
footer: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'center',
marginTop: 24,
paddingTop: 24,
borderTopWidth: 1,
borderTopColor: '#e2e8f0',
},
footerText: {
fontSize: 14,
@@ -166,7 +182,7 @@ const styles = StyleSheet.create({
},
footerLink: {
fontSize: 14,
color: '#4f46e5',
color: '#818cf8',
fontWeight: '800',
},
});

View File

@@ -6,7 +6,6 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useApp } from '../context/AppContext';
import { Button } from '../components/ui/Button';
import { Input } from '../components/ui/Input';
import { Card } from '../components/ui/Card';
import { RootStackParamList } from '../navigation/types';
export default function AuthRegister() {
@@ -42,8 +41,6 @@ export default function AuthRegister() {
navigation.replace('Login');
return;
}
navigation.replace(nextUser.role === 'barbearia' ? 'Dashboard' : 'Explore');
} catch (e: any) {
const message = e?.message || 'Erro ao criar conta';
setError(message);
@@ -56,67 +53,69 @@ export default function AuthRegister() {
return (
<SafeAreaView style={styles.container}>
<ScrollView contentContainerStyle={styles.content} keyboardShouldPersistTaps="handled">
<Card style={styles.card}>
<View style={styles.iconBox}>
<Text style={styles.iconText}>SA</Text>
<View style={styles.logoRow}>
<View style={styles.logoBox}>
<Text style={styles.logoText}>SA</Text>
</View>
<Text style={styles.title}>Criar Conta</Text>
<Text style={styles.subtitle}>Junte-se à Smart Agenda</Text>
</View>
{!!error && <Text style={styles.error}>{error}</Text>}
<Text style={styles.title}>Criar Conta</Text>
<Text style={styles.subtitle}>Junte-se à Smart Agenda</Text>
<Text style={styles.label}>Eu sou...</Text>
<View style={styles.roleContainer}>
{(['cliente', 'barbearia'] as const).map((item) => (
<TouchableOpacity
key={item}
style={[styles.roleButton, role === item && styles.roleButtonActive]}
onPress={() => {
setRole(item);
setError('');
}}
>
<Text style={[styles.roleText, role === item && styles.roleTextActive]}>
{item === 'cliente' ? 'Cliente' : 'Barbearia'}
</Text>
</TouchableOpacity>
))}
{!!error && (
<View style={styles.errorBox}>
<Text style={styles.errorText}>{error}</Text>
</View>
)}
<Input label="Nome completo" value={name} onChangeText={setName} placeholder="Ex: João Silva" />
<Text style={styles.label}>Eu sou...</Text>
<View style={styles.roleContainer}>
{(['cliente', 'barbearia'] as const).map((item) => (
<TouchableOpacity
key={item}
style={[styles.roleButton, role === item && styles.roleButtonActive]}
onPress={() => { setRole(item); setError(''); }}
>
<Text style={[styles.roleEmoji]}>{item === 'cliente' ? '✂️' : '💈'}</Text>
<Text style={[styles.roleText, role === item && styles.roleTextActive]}>
{item === 'cliente' ? 'Cliente' : 'Barbearia'}
</Text>
</TouchableOpacity>
))}
</View>
<Input label="Nome completo" value={name} onChangeText={setName} placeholder="Ex: João Silva" />
<Input
label="Email"
value={email}
onChangeText={(text) => { setEmail(text); setError(''); }}
keyboardType="email-address"
autoCapitalize="none"
placeholder="exemplo@email.com"
/>
<Input label="Palavra-passe" value={password} onChangeText={setPassword} secureTextEntry placeholder="••••••••" />
{role === 'barbearia' && (
<Input
label="Email"
value={email}
onChangeText={(text) => {
setEmail(text);
setError('');
}}
keyboardType="email-address"
autoCapitalize="none"
placeholder="exemplo@email.com"
label="Nome da barbearia"
value={shopName}
onChangeText={setShopName}
placeholder="Ex: Barbearia Estilo"
/>
<Input label="Palavra-passe" value={password} onChangeText={setPassword} secureTextEntry placeholder="••••••••" />
)}
{role === 'barbearia' && (
<Input
label="Nome da barbearia"
value={shopName}
onChangeText={setShopName}
placeholder="Ex: Barbearia Estilo"
/>
)}
<Button onPress={handleSubmit} style={styles.submitButton} loading={loading}>
Criar minha conta
</Button>
<Button onPress={handleSubmit} style={styles.submitButton} loading={loading}>
Criar minha conta
</Button>
<View style={styles.divider} />
<View style={styles.footer}>
<Text style={styles.footerText}> tem uma conta? </Text>
<Text style={styles.footerLink} onPress={() => navigation.navigate('Login')}>
Fazer Login
</Text>
</View>
</Card>
<View style={styles.footer}>
<Text style={styles.footerText}> tem uma conta? </Text>
<Text style={styles.footerLink} onPress={() => navigation.navigate('Login')}>
Fazer Login
</Text>
</View>
</ScrollView>
</SafeAreaView>
);
@@ -125,102 +124,113 @@ export default function AuthRegister() {
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
backgroundColor: '#0a0a0f',
},
content: {
padding: 16,
padding: 24,
justifyContent: 'center',
minHeight: '100%',
},
card: {
padding: 24,
logoRow: {
alignItems: 'center',
marginBottom: 28,
},
iconBox: {
width: 64,
height: 64,
borderRadius: 18,
backgroundColor: '#0f172a',
logoBox: {
width: 72,
height: 72,
borderRadius: 22,
backgroundColor: '#141420',
borderWidth: 1,
borderColor: 'rgba(99,102,241,0.3)',
alignItems: 'center',
justifyContent: 'center',
alignSelf: 'center',
marginBottom: 18,
},
iconText: {
logoText: {
color: '#818cf8',
fontSize: 18,
fontSize: 22,
fontWeight: '900',
},
title: {
fontSize: 30,
fontSize: 34,
fontWeight: '900',
color: '#0f172a',
color: '#f8fafc',
textAlign: 'center',
marginBottom: 6,
},
subtitle: {
fontSize: 14,
fontSize: 15,
color: '#64748b',
marginBottom: 24,
marginBottom: 28,
textAlign: 'center',
fontWeight: '500',
},
error: {
backgroundColor: '#fff1f2',
borderColor: '#fecdd3',
errorBox: {
backgroundColor: 'rgba(239,68,68,0.1)',
borderColor: 'rgba(239,68,68,0.3)',
borderWidth: 1,
borderRadius: 14,
color: '#be123c',
padding: 12,
padding: 14,
marginBottom: 16,
},
errorText: {
color: '#fca5a5',
fontWeight: '600',
fontSize: 13,
},
label: {
fontSize: 12,
color: '#0f172a',
fontWeight: '900',
color: '#94a3b8',
fontWeight: '700',
textTransform: 'uppercase',
letterSpacing: 0.5,
marginBottom: 10,
},
roleContainer: {
flexDirection: 'row',
gap: 12,
marginBottom: 18,
marginBottom: 20,
},
roleButton: {
flex: 1,
padding: 16,
padding: 18,
borderRadius: 16,
borderWidth: 2,
borderColor: '#f1f5f9',
backgroundColor: '#f8fafc',
borderWidth: 1.5,
borderColor: 'rgba(255,255,255,0.06)',
backgroundColor: '#141420',
alignItems: 'center',
gap: 8,
},
roleButtonActive: {
borderColor: '#0f172a',
backgroundColor: '#0f172a',
borderColor: '#6366f1',
backgroundColor: 'rgba(99,102,241,0.1)',
},
roleEmoji: {
fontSize: 24,
},
roleText: {
fontSize: 13,
fontWeight: '900',
fontWeight: '800',
color: '#64748b',
textTransform: 'uppercase',
},
roleTextActive: {
color: '#818cf8',
color: '#a5b4fc',
},
submitButton: {
width: '100%',
marginTop: 8,
backgroundColor: '#0f172a',
marginTop: 4,
backgroundColor: '#6366f1',
paddingVertical: 16,
},
divider: {
height: 1,
backgroundColor: 'rgba(255,255,255,0.06)',
marginVertical: 24,
},
footer: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'center',
marginTop: 24,
paddingTop: 24,
borderTopWidth: 1,
borderTopColor: '#e2e8f0',
},
footerText: {
fontSize: 14,
@@ -228,7 +238,7 @@ const styles = StyleSheet.create({
},
footerLink: {
fontSize: 14,
color: '#4f46e5',
color: '#818cf8',
fontWeight: '800',
},
});

View File

@@ -10,7 +10,7 @@ import { RootStackParamList } from '../navigation/types';
export default function Explore() {
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const { shops, shopsReady, cart, user, logout } = useApp();
const { shops, shopsReady, user } = useApp();
const [query, setQuery] = useState('');
const [filter, setFilter] = useState<'todas' | 'top' | 'servicos'>('todas');
@@ -28,111 +28,102 @@ export default function Explore() {
});
}, [shops, query, filter]);
const goToProfile = () => {
if (!user) navigation.navigate('Login');
else navigation.navigate(user.role === 'barbearia' ? 'Dashboard' : 'Profile');
};
return (
<SafeAreaView style={styles.container}>
<ScrollView contentContainerStyle={styles.content} keyboardShouldPersistTaps="handled">
<View style={styles.topBar}>
<TouchableOpacity onPress={() => navigation.navigate(user ? 'Explore' : 'Landing')} style={styles.brand}>
<View style={styles.brandIcon}>
<Text style={styles.brandIconText}>SA</Text>
</View>
<Text style={styles.brandText}>Smart Agenda</Text>
</TouchableOpacity>
<View style={styles.topActions}>
{user?.role !== 'barbearia' && (
<TouchableOpacity style={styles.cartButton} onPress={() => (user ? navigation.navigate('Cart') : navigation.navigate('Login'))}>
<Text style={styles.cartText}>Carrinho</Text>
{cart.length > 0 && <Text style={styles.cartBadge}>{cart.length}</Text>}
</TouchableOpacity>
)}
<TouchableOpacity style={styles.profileButton} onPress={goToProfile}>
<Text style={styles.profileText}>{user ? user.name.charAt(0).toUpperCase() : 'Entrar'}</Text>
</TouchableOpacity>
</View>
</View>
{/* Header */}
<View style={styles.header}>
<View style={styles.kicker}>
<Text style={styles.kickerText}>As nossas barbearias</Text>
<View>
<Text style={styles.greeting}>
{user ? `Olá, ${user.name.split(' ')[0]}` : 'Descobre'}
</Text>
<Text style={styles.headline}>Barbearias</Text>
</View>
<View style={styles.brandPill}>
<Text style={styles.brandText}>SA</Text>
</View>
<Text style={styles.title}>Ver <Text style={styles.titleAccent}>Barbearias</Text></Text>
<Text style={styles.subtitle}>Descubra barbearias exclusivas e reserve o seu próximo corte em segundos.</Text>
{user && (
<TouchableOpacity onPress={logout} style={styles.logoutButton}>
<Text style={styles.logoutText}>Sair da conta</Text>
</TouchableOpacity>
)}
</View>
<Card style={styles.searchCard}>
{/* Search */}
<View style={styles.searchBox}>
<Text style={styles.searchIcon}></Text>
<TextInput
value={query}
onChangeText={setQuery}
placeholder="Pesquisar por nome ou endereço..."
placeholderTextColor="#94a3b8"
placeholder="Pesquisar nome ou endereço..."
placeholderTextColor="#475569"
style={styles.searchInput}
/>
<View style={styles.filters}>
{[
['todas', 'Melhor avaliação'],
['top', 'Top avaliadas'],
['servicos', 'Mais serviços'],
].map(([id, label]) => (
<TouchableOpacity
key={id}
onPress={() => setFilter(id as typeof filter)}
style={[styles.filterChip, filter === id && styles.filterChipActive]}
>
<Text style={[styles.filterText, filter === id && styles.filterTextActive]}>{label}</Text>
</TouchableOpacity>
))}
</View>
</Card>
</View>
{/* Filters */}
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.filters}>
{[
['todas', 'Melhor avaliação'],
['top', 'Top avaliadas'],
['servicos', 'Mais serviços'],
].map(([id, label]) => (
<TouchableOpacity
key={id}
onPress={() => setFilter(id as typeof filter)}
style={[styles.chip, filter === id && styles.chipActive]}
>
<Text style={[styles.chipText, filter === id && styles.chipTextActive]}>{label}</Text>
</TouchableOpacity>
))}
</ScrollView>
{/* Results */}
{!shopsReady ? (
<Card style={styles.emptyCard}>
<View style={styles.emptyState}>
<Text style={styles.emptyIcon}></Text>
<Text style={styles.emptyTitle}>A carregar espaços...</Text>
</Card>
</View>
) : filtered.length === 0 ? (
<Card style={styles.emptyCard}>
<View style={styles.emptyState}>
<Text style={styles.emptyIcon}>🔍</Text>
<Text style={styles.emptyTitle}>Nenhuma barbearia encontrada</Text>
<Text style={styles.emptyText}>Tente ajustar a pesquisa ou os filtros ativos.</Text>
<Button variant="ghost" onPress={() => { setQuery(''); setFilter('todas'); }}>
<Text style={styles.emptyText}>Tente ajustar a pesquisa ou filtros.</Text>
<Button variant="outline" onPress={() => { setQuery(''); setFilter('todas'); }}>
Limpar Tudo
</Button>
</Card>
</View>
) : (
<View style={styles.list}>
<Text style={styles.count}>{filtered.length} espaços disponíveis</Text>
{filtered.map((shop) => (
<Card key={shop.id} style={styles.shopCard}>
<TouchableOpacity
key={shop.id}
style={styles.shopCard}
activeOpacity={0.85}
onPress={() => navigation.navigate('ShopDetails', { shopId: shop.id })}
>
{shop.imageUrl ? (
<Image source={{ uri: shop.imageUrl }} style={styles.shopImage} />
) : (
<View style={styles.shopImageFallback}>
<Text style={styles.shopImageFallbackText}>SA</Text>
<Text style={styles.shopFallbackText}>{shop.name.charAt(0)}</Text>
</View>
)}
<View style={styles.shopOverlay} />
<View style={styles.shopBody}>
<View style={styles.shopRow}>
<Text style={styles.shopName} numberOfLines={1}>{shop.name}</Text>
<Text style={styles.rating}>{(shop.rating || 0).toFixed(1)}</Text>
<View style={styles.ratingPill}>
<Text style={styles.ratingText}> {(shop.rating || 0).toFixed(1)}</Text>
</View>
</View>
<Text style={styles.address} numberOfLines={1}>{shop.address || 'Endereço indisponível'}</Text>
<View style={styles.metaRow}>
<Text style={styles.metaText}>{(shop.services || []).length} serviços</Text>
<Text style={styles.metaText}>{(shop.barbers || []).length} barbeiros</Text>
<View style={styles.metaPill}>
<Text style={styles.metaText}>{(shop.services || []).length} serviços</Text>
</View>
<View style={styles.metaPill}>
<Text style={styles.metaText}>{(shop.barbers || []).length} barbeiros</Text>
</View>
</View>
<Button style={styles.reserveButton} onPress={() => navigation.navigate('ShopDetails', { shopId: shop.id })}>
Reservar
</Button>
</View>
</Card>
</TouchableOpacity>
))}
</View>
)}
@@ -144,243 +135,184 @@ export default function Explore() {
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
backgroundColor: '#0a0a0f',
},
content: {
padding: 16,
gap: 18,
},
topBar: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: 12,
},
brand: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
flex: 1,
},
brandIcon: {
width: 34,
height: 34,
borderRadius: 10,
backgroundColor: '#4f46e5',
alignItems: 'center',
justifyContent: 'center',
},
brandIconText: {
color: '#fff',
fontSize: 12,
fontWeight: '900',
},
brandText: {
color: '#0f172a',
fontSize: 18,
fontWeight: '900',
textTransform: 'uppercase',
},
topActions: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
cartButton: {
minHeight: 36,
borderRadius: 12,
backgroundColor: '#f1f5f9',
paddingHorizontal: 10,
alignItems: 'center',
justifyContent: 'center',
},
cartText: {
color: '#334155',
fontSize: 11,
fontWeight: '900',
},
cartBadge: {
position: 'absolute',
right: -5,
top: -5,
minWidth: 18,
textAlign: 'center',
borderRadius: 999,
backgroundColor: '#0f172a',
color: '#818cf8',
fontSize: 10,
fontWeight: '900',
paddingHorizontal: 4,
},
profileButton: {
minWidth: 38,
height: 38,
borderRadius: 999,
backgroundColor: '#0f172a',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 10,
},
profileText: {
color: '#818cf8',
fontSize: 12,
fontWeight: '900',
padding: 20,
gap: 20,
paddingBottom: 32,
},
header: {
gap: 8,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
},
kicker: {
alignSelf: 'flex-start',
backgroundColor: '#e0e7ff',
borderRadius: 999,
paddingHorizontal: 10,
paddingVertical: 5,
},
kickerText: {
color: '#4338ca',
fontSize: 10,
fontWeight: '900',
textTransform: 'uppercase',
},
title: {
fontSize: 36,
lineHeight: 38,
fontWeight: '900',
color: '#0f172a',
textTransform: 'uppercase',
},
titleAccent: {
color: '#4f46e5',
},
subtitle: {
greeting: {
color: '#64748b',
fontSize: 15,
lineHeight: 22,
fontWeight: '500',
},
logoutButton: {
alignSelf: 'flex-start',
paddingVertical: 6,
},
logoutText: {
color: '#e11d48',
fontWeight: '800',
},
searchCard: {
padding: 12,
gap: 12,
},
searchInput: {
minHeight: 48,
borderRadius: 16,
backgroundColor: '#f8fafc',
color: '#0f172a',
paddingHorizontal: 14,
fontSize: 15,
fontWeight: '600',
marginBottom: 2,
},
headline: {
color: '#f8fafc',
fontSize: 32,
fontWeight: '900',
},
brandPill: {
width: 44,
height: 44,
borderRadius: 14,
backgroundColor: '#141420',
borderWidth: 1,
borderColor: 'rgba(99,102,241,0.25)',
alignItems: 'center',
justifyContent: 'center',
},
brandText: {
color: '#818cf8',
fontSize: 14,
fontWeight: '900',
},
searchBox: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#141420',
borderRadius: 16,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.06)',
paddingHorizontal: 16,
gap: 10,
},
searchIcon: {
color: '#475569',
fontSize: 20,
},
searchInput: {
flex: 1,
height: 52,
color: '#f8fafc',
fontSize: 15,
fontWeight: '500',
},
filters: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
},
filterChip: {
borderRadius: 999,
backgroundColor: '#f1f5f9',
paddingHorizontal: 12,
paddingVertical: 8,
chip: {
borderRadius: 12,
backgroundColor: '#141420',
paddingHorizontal: 16,
paddingVertical: 10,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.04)',
},
filterChipActive: {
backgroundColor: '#0f172a',
chipActive: {
backgroundColor: '#6366f1',
borderColor: '#6366f1',
},
filterText: {
color: '#475569',
fontSize: 11,
fontWeight: '900',
chipText: {
color: '#94a3b8',
fontSize: 13,
fontWeight: '700',
},
filterTextActive: {
color: '#818cf8',
chipTextActive: {
color: '#fff',
},
list: {
gap: 12,
gap: 16,
},
count: {
color: '#64748b',
color: '#475569',
fontSize: 12,
fontWeight: '900',
fontWeight: '700',
textTransform: 'uppercase',
letterSpacing: 0.5,
},
shopCard: {
padding: 0,
borderRadius: 22,
overflow: 'hidden',
backgroundColor: '#141420',
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.04)',
},
shopImage: {
width: '100%',
height: 150,
height: 160,
},
shopImageFallback: {
width: '100%',
height: 150,
backgroundColor: '#0f172a',
height: 160,
backgroundColor: '#1c1c2e',
alignItems: 'center',
justifyContent: 'center',
},
shopImageFallbackText: {
color: '#818cf8',
fontSize: 24,
shopFallbackText: {
color: '#6366f1',
fontSize: 42,
fontWeight: '900',
opacity: 0.5,
},
shopOverlay: {},
shopBody: {
padding: 16,
padding: 18,
gap: 10,
},
shopRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: 10,
},
shopName: {
flex: 1,
fontSize: 20,
fontWeight: '900',
color: '#0f172a',
fontSize: 18,
fontWeight: '800',
color: '#f8fafc',
},
rating: {
color: '#fff',
backgroundColor: '#0f172a',
borderRadius: 999,
paddingHorizontal: 9,
paddingVertical: 4,
ratingPill: {
backgroundColor: 'rgba(99,102,241,0.15)',
borderRadius: 10,
paddingHorizontal: 10,
paddingVertical: 5,
},
ratingText: {
color: '#a5b4fc',
fontSize: 12,
fontWeight: '900',
fontWeight: '800',
},
address: {
color: '#64748b',
fontWeight: '500',
fontSize: 14,
},
metaRow: {
flexDirection: 'row',
gap: 10,
gap: 8,
},
metaPill: {
backgroundColor: 'rgba(255,255,255,0.04)',
borderRadius: 8,
paddingHorizontal: 10,
paddingVertical: 5,
},
metaText: {
color: '#94a3b8',
color: '#475569',
fontSize: 11,
fontWeight: '900',
fontWeight: '700',
textTransform: 'uppercase',
},
reserveButton: {
backgroundColor: '#0f172a',
marginTop: 4,
},
emptyCard: {
padding: 28,
emptyState: {
alignItems: 'center',
gap: 10,
padding: 40,
gap: 12,
},
emptyIcon: {
fontSize: 48,
marginBottom: 8,
},
emptyTitle: {
color: '#0f172a',
color: '#f8fafc',
fontSize: 18,
fontWeight: '900',
fontWeight: '800',
textAlign: 'center',
},
emptyText: {