first commit

This commit is contained in:
2026-01-07 10:35:00 +00:00
parent 13745ac89e
commit 3c7190bca4
53 changed files with 5538 additions and 531 deletions

View File

@@ -0,0 +1,51 @@
import React from 'react';
import { View, Text, StyleSheet, ViewStyle } from 'react-native';
type Props = {
children: React.ReactNode;
color?: 'amber' | 'slate' | 'green' | 'red' | 'blue';
variant?: 'solid' | 'soft';
style?: ViewStyle;
};
const colorMap = {
solid: {
amber: { bg: '#f59e0b', text: '#fff' },
slate: { bg: '#475569', text: '#fff' },
green: { bg: '#10b981', text: '#fff' },
red: { bg: '#ef4444', text: '#fff' },
blue: { bg: '#3b82f6', text: '#fff' },
},
soft: {
amber: { bg: '#fef3c7', text: '#92400e' },
slate: { bg: '#f1f5f9', text: '#475569' },
green: { bg: '#d1fae5', text: '#065f46' },
red: { bg: '#fee2e2', text: '#991b1b' },
blue: { bg: '#dbeafe', text: '#1e40af' },
},
};
export const Badge = ({ children, color = 'amber', variant = 'soft', style }: Props) => {
const colors = colorMap[variant][color];
return (
<View style={[styles.badge, { backgroundColor: colors.bg }, style]}>
<Text style={[styles.text, { color: colors.text }]}>{children}</Text>
</View>
);
};
const styles = StyleSheet.create({
badge: {
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 12,
alignSelf: 'flex-start',
},
text: {
fontSize: 12,
fontWeight: '600',
},
});

View File

@@ -0,0 +1,103 @@
import React from 'react';
import { TouchableOpacity, Text, StyleSheet, ActivityIndicator, ViewStyle, TextStyle } from 'react-native';
type Props = {
children: React.ReactNode;
onPress?: () => void;
variant?: 'solid' | 'outline' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
loading?: boolean;
style?: ViewStyle;
textStyle?: TextStyle;
};
export const Button = ({ children, onPress, variant = 'solid', size = 'md', disabled, loading, style, textStyle }: Props) => {
const buttonStyle = [
styles.base,
styles[variant],
styles[`size_${size}`],
(disabled || loading) && styles.disabled,
style,
];
const textStyles = [
styles.text,
styles[`text_${variant}`],
styles[`textSize_${size}`],
textStyle,
];
return (
<TouchableOpacity
style={buttonStyle}
onPress={onPress}
disabled={disabled || loading}
activeOpacity={0.7}
>
{loading ? (
<ActivityIndicator color={variant === 'solid' ? '#fff' : '#f59e0b'} />
) : (
<Text style={textStyles}>{children}</Text>
)}
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
base: {
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
},
solid: {
backgroundColor: '#f59e0b',
},
outline: {
backgroundColor: 'transparent',
borderWidth: 2,
borderColor: '#f59e0b',
},
ghost: {
backgroundColor: 'transparent',
},
size_sm: {
paddingHorizontal: 12,
paddingVertical: 8,
},
size_md: {
paddingHorizontal: 16,
paddingVertical: 10,
},
size_lg: {
paddingHorizontal: 24,
paddingVertical: 14,
},
disabled: {
opacity: 0.5,
},
text: {
fontWeight: '600',
},
text_solid: {
color: '#fff',
},
text_outline: {
color: '#f59e0b',
},
text_ghost: {
color: '#f59e0b',
},
textSize_sm: {
fontSize: 12,
},
textSize_md: {
fontSize: 14,
},
textSize_lg: {
fontSize: 16,
},
});

View File

@@ -0,0 +1,28 @@
import React from 'react';
import { View, StyleSheet, ViewStyle } from 'react-native';
type Props = {
children: React.ReactNode;
style?: ViewStyle;
};
export const Card = ({ children, style }: Props) => {
return <View style={[styles.card, style]}>{children}</View>;
};
const styles = StyleSheet.create({
card: {
backgroundColor: '#fff',
borderRadius: 12,
padding: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
elevation: 2,
borderWidth: 1,
borderColor: '#e2e8f0',
},
});

View File

@@ -0,0 +1,53 @@
import React from 'react';
import { TextInput, View, Text, StyleSheet, TextInputProps } from 'react-native';
type Props = TextInputProps & {
label?: string;
error?: string;
};
export const Input = ({ label, error, style, ...props }: Props) => {
return (
<View style={styles.container}>
{label && <Text style={styles.label}>{label}</Text>}
<TextInput
style={[styles.input, error && styles.inputError, style]}
placeholderTextColor="#94a3b8"
{...props}
/>
{error && <Text style={styles.error}>{error}</Text>}
</View>
);
};
const styles = StyleSheet.create({
container: {
marginBottom: 16,
},
label: {
fontSize: 14,
fontWeight: '600',
color: '#334155',
marginBottom: 6,
},
input: {
borderWidth: 1,
borderColor: '#cbd5e1',
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
fontSize: 14,
color: '#0f172a',
backgroundColor: '#fff',
},
inputError: {
borderColor: '#ef4444',
},
error: {
fontSize: 12,
color: '#ef4444',
marginTop: 4,
},
});

323
src/context/AppContext.tsx Normal file
View File

@@ -0,0 +1,323 @@
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { nanoid } from 'nanoid';
import { Appointment, Barber, BarberShop, CartItem, Order, Product, Service, User } from '../types';
import { mockShops, mockUsers } from '../data/mock';
import { storage } from '../lib/storage';
type State = {
user?: User;
users: User[];
shops: BarberShop[];
appointments: Appointment[];
orders: Order[];
cart: CartItem[];
};
type AppContextValue = State & {
login: (email: string, password: string) => boolean;
logout: () => void;
register: (payload: Omit<User, 'id' | 'shopId'> & { shopName?: string }) => boolean;
addToCart: (item: CartItem) => void;
removeFromCart: (refId: string) => void;
clearCart: () => void;
createAppointment: (input: Omit<Appointment, 'id' | 'status' | 'total'>) => Appointment | null;
placeOrder: (customerId: string, shopId?: string) => Order | null;
updateAppointmentStatus: (id: string, status: Appointment['status']) => void;
updateOrderStatus: (id: string, status: Order['status']) => void;
addService: (shopId: string, service: Omit<Service, 'id'>) => void;
updateService: (shopId: string, service: Service) => void;
deleteService: (shopId: string, serviceId: string) => void;
addProduct: (shopId: string, product: Omit<Product, 'id'>) => void;
updateProduct: (shopId: string, product: Product) => void;
deleteProduct: (shopId: string, productId: string) => void;
addBarber: (shopId: string, barber: Omit<Barber, 'id'>) => void;
updateBarber: (shopId: string, barber: Barber) => void;
deleteBarber: (shopId: string, barberId: string) => void;
};
const initialState: State = {
user: undefined,
users: mockUsers,
shops: mockShops,
appointments: [],
orders: [],
cart: [],
};
const AppContext = createContext<AppContextValue | undefined>(undefined);
export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const [state, setState] = useState<State>(initialState);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const loadData = async () => {
try {
const saved = await storage.get('smart-agenda', initialState);
setState(saved);
} catch (err) {
console.error('Error loading data:', err);
} finally {
setIsLoading(false);
}
};
loadData();
}, []);
useEffect(() => {
if (!isLoading) {
storage.set('smart-agenda', state);
}
}, [state, isLoading]);
const login = (email: string, password: string) => {
const found = state.users.find((u) => u.email === email && u.password === password);
if (found) {
setState((s) => ({ ...s, user: found }));
return true;
}
return false;
};
const logout = () => setState((s) => ({ ...s, user: undefined }));
const register: AppContextValue['register'] = ({ shopName, ...payload }) => {
const exists = state.users.some((u) => u.email === payload.email);
if (exists) return false;
if (payload.role === 'barbearia') {
const shopId = nanoid();
const shop: BarberShop = {
id: shopId,
name: shopName || `Barbearia ${payload.name}`,
address: 'Endereço a definir',
rating: 0,
barbers: [],
services: [],
products: [],
};
const user: User = { ...payload, id: nanoid(), role: 'barbearia', shopId };
setState((s) => ({
...s,
user,
users: [...s.users, user],
shops: [...s.shops, shop],
}));
return true;
}
const user: User = { ...payload, id: nanoid(), role: 'cliente' };
setState((s) => ({
...s,
user,
users: [...s.users, user],
}));
return true;
};
const addToCart: AppContextValue['addToCart'] = (item) => {
setState((s) => {
const cart = [...s.cart];
const idx = cart.findIndex((c) => c.refId === item.refId && c.type === item.type && c.shopId === item.shopId);
if (idx >= 0) cart[idx].qty += item.qty;
else cart.push(item);
return { ...s, cart };
});
};
const removeFromCart: AppContextValue['removeFromCart'] = (refId) => {
setState((s) => ({ ...s, cart: s.cart.filter((c) => c.refId !== refId) }));
};
const clearCart = () => setState((s) => ({ ...s, cart: [] }));
const createAppointment: AppContextValue['createAppointment'] = (input) => {
const shop = state.shops.find((s) => s.id === input.shopId);
if (!shop) return null;
const svc = shop.services.find((s) => s.id === input.serviceId);
if (!svc) return null;
const exists = state.appointments.find(
(ap) => ap.barberId === input.barberId && ap.date === input.date && ap.status !== 'cancelado'
);
if (exists) return null;
const appointment: Appointment = {
...input,
id: nanoid(),
status: 'pendente',
total: svc.price,
};
setState((s) => ({ ...s, appointments: [...s.appointments, appointment] }));
return appointment;
};
const placeOrder: AppContextValue['placeOrder'] = (customerId, onlyShopId) => {
if (!state.cart.length) return null;
const grouped = state.cart.reduce<Record<string, CartItem[]>>((acc, item) => {
acc[item.shopId] = acc[item.shopId] || [];
acc[item.shopId].push(item);
return acc;
}, {});
const entries = Object.entries(grouped).filter(([shopId]) => (onlyShopId ? shopId === onlyShopId : true));
const newOrders: Order[] = entries.map(([shopId, items]) => {
const total = items.reduce((sum, item) => {
const shop = state.shops.find((s) => s.id === item.shopId);
if (!shop) return sum;
const price =
item.type === 'service'
? shop.services.find((s) => s.id === item.refId)?.price ?? 0
: shop.products.find((p) => p.id === item.refId)?.price ?? 0;
return sum + price * item.qty;
}, 0);
return {
id: nanoid(),
shopId,
customerId,
items,
total,
status: 'pendente',
createdAt: new Date().toISOString(),
};
});
setState((s) => ({ ...s, orders: [...s.orders, ...newOrders], cart: [] }));
return newOrders[0] ?? null;
};
const updateAppointmentStatus: AppContextValue['updateAppointmentStatus'] = (id, status) => {
setState((s) => ({
...s,
appointments: s.appointments.map((a) => (a.id === id ? { ...a, status } : a)),
}));
};
const updateOrderStatus: AppContextValue['updateOrderStatus'] = (id, status) => {
setState((s) => ({
...s,
orders: s.orders.map((o) => (o.id === id ? { ...o, status } : o)),
}));
};
const addService: AppContextValue['addService'] = (shopId, service) => {
const entry: Service = { ...service, id: nanoid() };
setState((s) => ({
...s,
shops: s.shops.map((shop) => (shop.id === shopId ? { ...shop, services: [...shop.services, entry] } : shop)),
}));
};
const updateService: AppContextValue['updateService'] = (shopId, service) => {
setState((s) => ({
...s,
shops: s.shops.map((shop) =>
shop.id === shopId ? { ...shop, services: shop.services.map((sv) => (sv.id === service.id ? service : sv)) } : shop
),
}));
};
const deleteService: AppContextValue['deleteService'] = (shopId, serviceId) => {
setState((s) => ({
...s,
shops: s.shops.map((shop) =>
shop.id === shopId ? { ...shop, services: shop.services.filter((sv) => sv.id !== serviceId) } : shop
),
}));
};
const addProduct: AppContextValue['addProduct'] = (shopId, product) => {
const entry: Product = { ...product, id: nanoid() };
setState((s) => ({
...s,
shops: s.shops.map((shop) => (shop.id === shopId ? { ...shop, products: [...shop.products, entry] } : shop)),
}));
};
const updateProduct: AppContextValue['updateProduct'] = (shopId, product) => {
setState((s) => ({
...s,
shops: s.shops.map((shop) =>
shop.id === shopId ? { ...shop, products: shop.products.map((p) => (p.id === product.id ? product : p)) } : shop
),
}));
};
const deleteProduct: AppContextValue['deleteProduct'] = (shopId, productId) => {
setState((s) => ({
...s,
shops: s.shops.map((shop) =>
shop.id === shopId ? { ...shop, products: shop.products.filter((p) => p.id !== productId) } : shop
),
}));
};
const addBarber: AppContextValue['addBarber'] = (shopId, barber) => {
const entry: Barber = { ...barber, id: nanoid() };
setState((s) => ({
...s,
shops: s.shops.map((shop) => (shop.id === shopId ? { ...shop, barbers: [...shop.barbers, entry] } : shop)),
}));
};
const updateBarber: AppContextValue['updateBarber'] = (shopId, barber) => {
setState((s) => ({
...s,
shops: s.shops.map((shop) =>
shop.id === shopId ? { ...shop, barbers: shop.barbers.map((b) => (b.id === barber.id ? barber : b)) } : shop
),
}));
};
const deleteBarber: AppContextValue['deleteBarber'] = (shopId, barberId) => {
setState((s) => ({
...s,
shops: s.shops.map((shop) =>
shop.id === shopId ? { ...shop, barbers: shop.barbers.filter((b) => b.id !== barberId) } : shop
),
}));
};
const value: AppContextValue = useMemo(
() => ({
...state,
login,
logout,
register,
addToCart,
removeFromCart,
clearCart,
createAppointment,
placeOrder,
updateAppointmentStatus,
updateOrderStatus,
addService,
updateService,
deleteService,
addProduct,
updateProduct,
deleteProduct,
addBarber,
updateBarber,
deleteBarber,
}),
[state]
);
if (isLoading) {
return null; // Ou um componente de loading
}
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};
export const useApp = () => {
const ctx = useContext(AppContext);
if (!ctx) throw new Error('useApp deve ser usado dentro de AppProvider');
return ctx;
};

40
src/data/mock.ts Normal file
View File

@@ -0,0 +1,40 @@
import { BarberShop, User } from '../types';
export const mockUsers: User[] = [
{ id: 'u1', name: 'Cliente Demo', email: 'cliente@demo.com', password: '123', role: 'cliente' },
{ id: 'u2', name: 'Barbearia Demo', email: 'barber@demo.com', password: '123', role: 'barbearia', shopId: 's1' },
];
export const mockShops: BarberShop[] = [
{
id: 's1',
name: 'Barbearia Central',
address: 'Rua Principal, 123',
rating: 4.7,
barbers: [
{ id: 'b1', name: 'João', specialties: ['Fade', 'Navalha'], schedule: [{ day: '2025-01-01', slots: ['09:00', '10:00', '11:00'] }] },
{ id: 'b2', name: 'Carlos', specialties: ['Barba', 'Clássico'], schedule: [{ day: '2025-01-01', slots: ['14:00', '15:00'] }] },
],
services: [
{ id: 'sv1', name: 'Corte Fade', price: 60, duration: 45, barberIds: ['b1'] },
{ id: 'sv2', name: 'Barba Completa', price: 40, duration: 30, barberIds: ['b2'] },
],
products: [
{ id: 'p1', name: 'Pomada Matte', price: 35, stock: 8 },
{ id: 'p2', name: 'Óleo para Barba', price: 45, stock: 5 },
],
},
{
id: 's2',
name: 'Barbearia Bairro',
address: 'Av. Verde, 45',
rating: 4.5,
barbers: [
{ id: 'b3', name: 'Miguel', specialties: ['Clássico', 'Fade'], schedule: [{ day: '2025-01-01', slots: ['09:30', '10:30'] }] },
],
services: [{ id: 'sv3', name: 'Corte Clássico', price: 50, duration: 40, barberIds: ['b3'] }],
products: [{ id: 'p3', name: 'Shampoo Masculino', price: 30, stock: 10 }],
},
];

3
src/lib/format.ts Normal file
View File

@@ -0,0 +1,3 @@
export const currency = (v: number) => v.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });

23
src/lib/storage.ts Normal file
View File

@@ -0,0 +1,23 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
export const storage = {
async get<T>(key: string, fallback: T): Promise<T> {
try {
const raw = await AsyncStorage.getItem(key);
if (!raw) return fallback;
return JSON.parse(raw) as T;
} catch (err) {
console.error('storage parse error', err);
return fallback;
}
},
async set<T>(key: string, value: T): Promise<void> {
try {
await AsyncStorage.setItem(key, JSON.stringify(value));
} catch (err) {
console.error('storage set error', err);
}
},
};

View File

@@ -0,0 +1,59 @@
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
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';
import ShopDetails from '../pages/ShopDetails';
import Booking from '../pages/Booking';
import Cart from '../pages/Cart';
import Profile from '../pages/Profile';
import Dashboard from '../pages/Dashboard';
import { RootStackParamList } from './types';
const Stack = createNativeStackNavigator<RootStackParamList>();
export default function AppNavigator() {
const { user } = useApp();
return (
<NavigationContainer>
<Stack.Navigator
screenOptions={{
headerStyle: { backgroundColor: '#f59e0b' },
headerTintColor: '#fff',
headerTitleStyle: { fontWeight: 'bold' },
}}
>
{!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' }} />
<Stack.Screen name="Booking" component={Booking} options={{ title: 'Agendar' }} />
<Stack.Screen name="Cart" component={Cart} options={{ title: 'Carrinho' }} />
</>
) : 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="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.Navigator>
</NavigationContainer>
);
}

19
src/navigation/types.ts Normal file
View File

@@ -0,0 +1,19 @@
export type RootStackParamList = {
Landing: undefined;
Login: undefined;
Register: undefined;
Explore: undefined;
ShopDetails: { shopId: string };
Booking: { shopId: string };
Cart: undefined;
Profile: undefined;
Dashboard: undefined;
};
declare global {
namespace ReactNavigation {
interface RootParamList extends RootStackParamList {}
}
}

148
src/pages/AuthLogin.tsx Normal file
View File

@@ -0,0 +1,148 @@
import React, { useState } from 'react';
import { View, Text, StyleSheet, ScrollView, Alert } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useApp } from '../context/AppContext';
import { Button } from '../components/ui/Button';
import { Input } from '../components/ui/Input';
import { Card } from '../components/ui/Card';
export default function AuthLogin() {
const navigation = useNavigation();
const { login } = useApp();
const [email, setEmail] = useState('cliente@demo.com');
const [password, setPassword] = useState('123');
const [error, setError] = useState('');
const handleSubmit = () => {
setError('');
const ok = login(email, password);
if (!ok) {
setError('Credenciais inválidas');
Alert.alert('Erro', 'Credenciais inválidas');
} else {
navigation.navigate('Explore' as never);
}
};
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<Card style={styles.card}>
<Text style={styles.title}>Bem-vindo de volta</Text>
<Text style={styles.subtitle}>Entre na sua conta para continuar</Text>
<View style={styles.demoBox}>
<Text style={styles.demoTitle}>💡 Conta demo:</Text>
<Text style={styles.demoText}>Cliente: cliente@demo.com / 123</Text>
<Text style={styles.demoText}>Barbearia: barber@demo.com / 123</Text>
</View>
<Input
label="Email"
value={email}
onChangeText={(text) => {
setEmail(text);
setError('');
}}
keyboardType="email-address"
autoCapitalize="none"
placeholder="seu@email.com"
/>
<Input
label="Senha"
value={password}
onChangeText={(text) => {
setPassword(text);
setError('');
}}
secureTextEntry
placeholder="••••••••"
error={error}
/>
<Button onPress={handleSubmit} style={styles.submitButton} size="lg">
Entrar
</Button>
<View style={styles.footer}>
<Text style={styles.footerText}>Não tem conta? </Text>
<Text
style={styles.footerLink}
onPress={() => navigation.navigate('Register' as never)}
>
Criar conta
</Text>
</View>
</Card>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
},
content: {
padding: 16,
justifyContent: 'center',
minHeight: '100%',
},
card: {
padding: 24,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#0f172a',
marginBottom: 8,
textAlign: 'center',
},
subtitle: {
fontSize: 14,
color: '#64748b',
marginBottom: 24,
textAlign: 'center',
},
demoBox: {
backgroundColor: '#fef3c7',
borderWidth: 1,
borderColor: '#fbbf24',
borderRadius: 8,
padding: 12,
marginBottom: 20,
},
demoTitle: {
fontSize: 12,
fontWeight: '600',
color: '#92400e',
marginBottom: 4,
},
demoText: {
fontSize: 11,
color: '#92400e',
},
submitButton: {
width: '100%',
marginTop: 8,
},
footer: {
flexDirection: 'row',
justifyContent: 'center',
marginTop: 24,
paddingTop: 24,
borderTopWidth: 1,
borderTopColor: '#e2e8f0',
},
footerText: {
fontSize: 14,
color: '#64748b',
},
footerLink: {
fontSize: 14,
color: '#f59e0b',
fontWeight: '600',
},
});

182
src/pages/AuthRegister.tsx Normal file
View File

@@ -0,0 +1,182 @@
import React, { useState } from 'react';
import { View, Text, StyleSheet, ScrollView, Alert, TouchableOpacity } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useApp } from '../context/AppContext';
import { Button } from '../components/ui/Button';
import { Input } from '../components/ui/Input';
import { Card } from '../components/ui/Card';
export default function AuthRegister() {
const navigation = useNavigation();
const { register } = useApp();
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [role, setRole] = useState<'cliente' | 'barbearia'>('cliente');
const [shopName, setShopName] = useState('');
const [error, setError] = useState('');
const handleSubmit = () => {
setError('');
const ok = register({ name, email, password, role, shopName });
if (!ok) {
setError('Email já registado');
Alert.alert('Erro', 'Email já registado');
} else {
navigation.navigate('Explore' as never);
}
};
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<Card style={styles.card}>
<Text style={styles.title}>Criar conta</Text>
<Text style={styles.subtitle}>Escolha o tipo de acesso</Text>
<View style={styles.roleContainer}>
<TouchableOpacity
style={[styles.roleButton, role === 'cliente' && styles.roleButtonActive]}
onPress={() => setRole('cliente')}
>
<Text style={[styles.roleText, role === 'cliente' && styles.roleTextActive]}>
Cliente
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.roleButton, role === 'barbearia' && styles.roleButtonActive]}
onPress={() => setRole('barbearia')}
>
<Text style={[styles.roleText, role === 'barbearia' && styles.roleTextActive]}>
Barbearia
</Text>
</TouchableOpacity>
</View>
<Input
label="Nome completo"
value={name}
onChangeText={setName}
placeholder="João Silva"
/>
<Input
label="Email"
value={email}
onChangeText={(text) => {
setEmail(text);
setError('');
}}
keyboardType="email-address"
autoCapitalize="none"
placeholder="seu@email.com"
error={error}
/>
<Input
label="Senha"
value={password}
onChangeText={setPassword}
secureTextEntry
placeholder="••••••••"
/>
{role === 'barbearia' && (
<Input
label="Nome da barbearia"
value={shopName}
onChangeText={setShopName}
placeholder="Barbearia XPTO"
/>
)}
<Button onPress={handleSubmit} style={styles.submitButton} size="lg">
Criar conta
</Button>
<View style={styles.footer}>
<Text style={styles.footerText}> tem conta? </Text>
<Text
style={styles.footerLink}
onPress={() => navigation.navigate('Login' as never)}
>
Entrar
</Text>
</View>
</Card>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
},
content: {
padding: 16,
},
card: {
padding: 24,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#0f172a',
marginBottom: 8,
textAlign: 'center',
},
subtitle: {
fontSize: 14,
color: '#64748b',
marginBottom: 24,
textAlign: 'center',
},
roleContainer: {
flexDirection: 'row',
gap: 12,
marginBottom: 20,
},
roleButton: {
flex: 1,
padding: 16,
borderRadius: 12,
borderWidth: 2,
borderColor: '#e2e8f0',
alignItems: 'center',
},
roleButtonActive: {
borderColor: '#f59e0b',
backgroundColor: '#fef3c7',
},
roleText: {
fontSize: 14,
fontWeight: '600',
color: '#64748b',
},
roleTextActive: {
color: '#f59e0b',
},
submitButton: {
width: '100%',
marginTop: 8,
},
footer: {
flexDirection: 'row',
justifyContent: 'center',
marginTop: 24,
paddingTop: 24,
borderTopWidth: 1,
borderTopColor: '#e2e8f0',
},
footerText: {
fontSize: 14,
color: '#64748b',
},
footerLink: {
fontSize: 14,
color: '#f59e0b',
fontWeight: '600',
},
});

298
src/pages/Booking.tsx Normal file
View File

@@ -0,0 +1,298 @@
import React, { useState, useMemo } from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Alert } from 'react-native';
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 { Input } from '../components/ui/Input';
import { Badge } from '../components/ui/Badge';
import { currency } from '../lib/format';
export default function Booking() {
const route = useRoute();
const navigation = useNavigation();
const { shopId } = route.params as { shopId: string };
const { shops, createAppointment, user, appointments } = useApp();
const shop = useMemo(() => shops.find((s) => s.id === shopId), [shops, shopId]);
const [serviceId, setService] = useState('');
const [barberId, setBarber] = useState('');
const [date, setDate] = useState('');
const [slot, setSlot] = useState('');
if (!shop) {
return (
<View style={styles.container}>
<Text>Barbearia não encontrada</Text>
</View>
);
}
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`);
}
return slots;
};
const availableSlots = 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) => {
const parts = apt.date.split(' ');
return parts.length > 1 ? parts[1] : '';
})
.filter(Boolean);
return slots.filter((slot) => !bookedSlots.includes(slot));
}, [selectedBarber, date, barberId, appointments]);
const canSubmit = serviceId && barberId && date && slot;
const submit = () => {
if (!user) {
Alert.alert('Login necessário', 'Faça login para agendar');
navigation.navigate('Login' as never);
return;
}
if (!canSubmit) return;
const appt = createAppointment({ shopId: shop.id, serviceId, barberId, customerId: user.id, date: `${date} ${slot}` });
if (appt) {
Alert.alert('Sucesso', 'Agendamento criado com sucesso!');
navigation.navigate('Profile' as never);
} else {
Alert.alert('Erro', 'Horário indisponível');
}
};
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<Text style={styles.title}>Agendar em {shop.name}</Text>
<Card style={styles.card}>
<Text style={styles.sectionTitle}>1. Escolha o serviço</Text>
<View style={styles.grid}>
{shop.services.map((s) => (
<TouchableOpacity
key={s.id}
style={[styles.serviceButton, serviceId === s.id && styles.serviceButtonActive]}
onPress={() => setService(s.id)}
>
<Text style={[styles.serviceText, serviceId === s.id && styles.serviceTextActive]}>{s.name}</Text>
<Text style={styles.servicePrice}>{currency(s.price)}</Text>
</TouchableOpacity>
))}
</View>
<Text style={styles.sectionTitle}>2. Escolha o barbeiro</Text>
<View style={styles.barberContainer}>
{shop.barbers.map((b) => (
<TouchableOpacity
key={b.id}
style={[styles.barberButton, barberId === b.id && styles.barberButtonActive]}
onPress={() => setBarber(b.id)}
>
<Text style={[styles.barberText, barberId === b.id && styles.barberTextActive]}>
{b.name}
</Text>
</TouchableOpacity>
))}
</View>
<Text style={styles.sectionTitle}>3. Escolha a data</Text>
<Input
value={date}
onChangeText={setDate}
placeholder="YYYY-MM-DD"
/>
<Text style={styles.sectionTitle}>4. Escolha o horário</Text>
<View style={styles.slotsContainer}>
{availableSlots.length > 0 ? (
availableSlots.map((h) => (
<TouchableOpacity
key={h}
style={[styles.slotButton, slot === h && styles.slotButtonActive]}
onPress={() => setSlot(h)}
>
<Text style={[styles.slotText, slot === h && styles.slotTextActive]}>{h}</Text>
</TouchableOpacity>
))
) : (
<Text style={styles.noSlots}>Escolha primeiro o barbeiro e a data</Text>
)}
</View>
{canSubmit && selectedService && (
<View style={styles.summary}>
<Text style={styles.summaryTitle}>Resumo</Text>
<Text style={styles.summaryText}>Serviço: {selectedService.name}</Text>
<Text style={styles.summaryText}>Barbeiro: {selectedBarber?.name}</Text>
<Text style={styles.summaryText}>Data: {date} às {slot}</Text>
<Text style={styles.summaryTotal}>Total: {currency(selectedService.price)}</Text>
</View>
)}
<Button onPress={submit} disabled={!canSubmit} style={styles.submitButton} size="lg">
{user ? 'Confirmar agendamento' : 'Entrar para agendar'}
</Button>
</Card>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
},
content: {
padding: 16,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#0f172a',
marginBottom: 16,
},
card: {
padding: 20,
},
sectionTitle: {
fontSize: 16,
fontWeight: 'bold',
color: '#0f172a',
marginTop: 16,
marginBottom: 12,
},
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
marginBottom: 16,
},
serviceButton: {
flex: 1,
minWidth: '45%',
padding: 16,
borderRadius: 8,
borderWidth: 2,
borderColor: '#e2e8f0',
marginBottom: 8,
},
serviceButtonActive: {
borderColor: '#f59e0b',
backgroundColor: '#fef3c7',
},
serviceText: {
fontSize: 14,
fontWeight: '600',
color: '#0f172a',
marginBottom: 4,
},
serviceTextActive: {
color: '#f59e0b',
},
servicePrice: {
fontSize: 12,
color: '#64748b',
},
barberContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
marginBottom: 16,
},
barberButton: {
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 20,
borderWidth: 2,
borderColor: '#e2e8f0',
},
barberButtonActive: {
borderColor: '#f59e0b',
backgroundColor: '#f59e0b',
},
barberText: {
fontSize: 14,
fontWeight: '600',
color: '#64748b',
},
barberTextActive: {
color: '#fff',
},
slotsContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
marginBottom: 16,
},
slotButton: {
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 8,
borderWidth: 2,
borderColor: '#e2e8f0',
},
slotButtonActive: {
borderColor: '#f59e0b',
backgroundColor: '#f59e0b',
},
slotText: {
fontSize: 14,
fontWeight: '600',
color: '#64748b',
},
slotTextActive: {
color: '#fff',
},
noSlots: {
fontSize: 14,
color: '#94a3b8',
fontStyle: 'italic',
},
summary: {
backgroundColor: '#f1f5f9',
padding: 16,
borderRadius: 8,
marginBottom: 16,
},
summaryTitle: {
fontSize: 16,
fontWeight: 'bold',
color: '#0f172a',
marginBottom: 8,
},
summaryText: {
fontSize: 14,
color: '#64748b',
marginBottom: 4,
},
summaryTotal: {
fontSize: 18,
fontWeight: 'bold',
color: '#f59e0b',
marginTop: 8,
},
submitButton: {
width: '100%',
marginTop: 16,
},
});

167
src/pages/Cart.tsx Normal file
View File

@@ -0,0 +1,167 @@
import React from 'react';
import { View, Text, StyleSheet, ScrollView, Alert } from 'react-native';
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();
const { cart, shops, removeFromCart, placeOrder, user } = useApp();
if (!cart.length) {
return (
<View style={styles.container}>
<Card style={styles.emptyCard}>
<Text style={styles.emptyText}>Carrinho vazio</Text>
</Card>
</View>
);
}
const grouped = cart.reduce<Record<string, typeof cart>>((acc, item) => {
acc[item.shopId] = acc[item.shopId] || [];
acc[item.shopId].push(item);
return acc;
}, {});
const handleCheckout = (shopId: string) => {
if (!user) {
Alert.alert('Login necessário', 'Faça login para finalizar o pedido');
navigation.navigate('Login' as never);
return;
}
const order = placeOrder(user.id, shopId);
if (order) {
Alert.alert('Sucesso', 'Pedido criado com sucesso!');
}
};
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<Text style={styles.title}>Carrinho</Text>
{Object.entries(grouped).map(([shopId, items]) => {
const shop = shops.find((s) => s.id === shopId);
const total = items.reduce((sum, i) => {
const price =
i.type === 'service'
? shop?.services.find((s) => s.id === i.refId)?.price ?? 0
: shop?.products.find((p) => p.id === i.refId)?.price ?? 0;
return sum + price * i.qty;
}, 0);
return (
<Card key={shopId} style={styles.shopCard}>
<View style={styles.shopHeader}>
<View>
<Text style={styles.shopName}>{shop?.name ?? 'Barbearia'}</Text>
<Text style={styles.shopAddress}>{shop?.address}</Text>
</View>
<Text style={styles.total}>{currency(total)}</Text>
</View>
{items.map((i) => {
const ref =
i.type === 'service'
? shop?.services.find((s) => s.id === i.refId)
: shop?.products.find((p) => p.id === i.refId);
return (
<View key={i.refId} style={styles.item}>
<Text style={styles.itemText}>
{i.type === 'service' ? 'Serviço: ' : 'Produto: '}
{ref?.name ?? 'Item'} x{i.qty}
</Text>
<Button
onPress={() => removeFromCart(i.refId)}
variant="ghost"
size="sm"
>
Remover
</Button>
</View>
);
})}
{user ? (
<Button onPress={() => handleCheckout(shopId)} style={styles.checkoutButton}>
Finalizar pedido
</Button>
) : (
<Button
onPress={() => navigation.navigate('Login' as never)}
style={styles.checkoutButton}
>
Entrar para finalizar
</Button>
)}
</Card>
);
})}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
},
content: {
padding: 16,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#0f172a',
marginBottom: 16,
},
emptyCard: {
padding: 32,
alignItems: 'center',
},
emptyText: {
fontSize: 16,
color: '#64748b',
},
shopCard: {
marginBottom: 16,
},
shopHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 12,
},
shopName: {
fontSize: 16,
fontWeight: 'bold',
color: '#0f172a',
},
shopAddress: {
fontSize: 12,
color: '#64748b',
},
total: {
fontSize: 18,
fontWeight: 'bold',
color: '#f59e0b',
},
item: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 8,
borderBottomWidth: 1,
borderBottomColor: '#e2e8f0',
},
itemText: {
fontSize: 14,
color: '#64748b',
flex: 1,
},
checkoutButton: {
width: '100%',
marginTop: 12,
},
});

541
src/pages/Dashboard.tsx Normal file
View File

@@ -0,0 +1,541 @@
import React, { useState } from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Alert } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useApp } from '../context/AppContext';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { Input } from '../components/ui/Input';
import { Badge } from '../components/ui/Badge';
import { currency } from '../lib/format';
export default function Dashboard() {
const navigation = useNavigation();
const {
user,
shops,
appointments,
orders,
updateAppointmentStatus,
updateOrderStatus,
addService,
addProduct,
addBarber,
updateProduct,
deleteProduct,
deleteService,
deleteBarber,
logout,
} = useApp();
const shop = shops.find((s) => s.id === user?.shopId);
const [activeTab, setActiveTab] = useState<'overview' | 'appointments' | 'orders' | 'services' | 'products' | 'barbers'>('overview');
const [svcName, setSvcName] = useState('');
const [svcPrice, setSvcPrice] = useState('50');
const [svcDuration, setSvcDuration] = useState('30');
const [prodName, setProdName] = useState('');
const [prodPrice, setProdPrice] = useState('30');
const [prodStock, setProdStock] = useState('10');
const [barberName, setBarberName] = useState('');
const [barberSpecs, setBarberSpecs] = useState('');
if (!user || user.role !== 'barbearia') {
return (
<View style={styles.container}>
<Text>Área exclusiva para barbearias</Text>
</View>
);
}
if (!shop) {
return (
<View style={styles.container}>
<Text>Barbearia não encontrada</Text>
</View>
);
}
const shopAppointments = appointments.filter((a) => a.shopId === shop.id);
const shopOrders = orders.filter((o) => o.shopId === shop.id);
const completedAppointments = shopAppointments.filter((a) => a.status === 'concluido');
const activeAppointments = shopAppointments.filter((a) => a.status !== 'concluido');
const productOrders = shopOrders.filter((o) => o.items.some((i) => i.type === 'product'));
const totalRevenue = shopOrders.reduce((s, o) => s + o.total, 0);
const lowStock = shop.products.filter((p) => p.stock <= 3);
const addNewService = () => {
if (!svcName.trim()) return;
addService(shop.id, { name: svcName, price: Number(svcPrice) || 0, duration: Number(svcDuration) || 30, barberIds: [] });
setSvcName('');
setSvcPrice('50');
setSvcDuration('30');
Alert.alert('Sucesso', 'Serviço adicionado');
};
const addNewProduct = () => {
if (!prodName.trim()) return;
addProduct(shop.id, { name: prodName, price: Number(prodPrice) || 0, stock: Number(prodStock) || 0 });
setProdName('');
setProdPrice('30');
setProdStock('10');
Alert.alert('Sucesso', 'Produto adicionado');
};
const addNewBarber = () => {
if (!barberName.trim()) return;
addBarber(shop.id, {
name: barberName,
specialties: barberSpecs.split(',').map((s) => s.trim()).filter(Boolean),
schedule: [],
});
setBarberName('');
setBarberSpecs('');
Alert.alert('Sucesso', 'Barbeiro adicionado');
};
const updateProductStock = (productId: string, delta: number) => {
const product = shop.products.find((p) => p.id === productId);
if (!product) return;
const next = { ...product, stock: Math.max(0, product.stock + delta) };
updateProduct(shop.id, next);
};
const tabs = [
{ id: 'overview', label: 'Visão Geral' },
{ id: 'appointments', label: 'Agendamentos' },
{ id: 'orders', label: 'Pedidos' },
{ id: 'services', label: 'Serviços' },
{ id: 'products', label: 'Produtos' },
{ id: 'barbers', label: 'Barbeiros' },
];
return (
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>{shop.name}</Text>
<Button onPress={logout} variant="ghost" size="sm">
Sair
</Button>
</View>
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.tabsContainer}>
{tabs.map((tab) => (
<TouchableOpacity
key={tab.id}
style={[styles.tab, activeTab === tab.id && styles.tabActive]}
onPress={() => setActiveTab(tab.id as any)}
>
<Text style={[styles.tabText, activeTab === tab.id && styles.tabTextActive]}>
{tab.label}
</Text>
</TouchableOpacity>
))}
</ScrollView>
<ScrollView style={styles.content} contentContainerStyle={styles.contentInner}>
{activeTab === 'overview' && (
<View>
<View style={styles.statsGrid}>
<Card style={styles.statCard}>
<Text style={styles.statLabel}>Faturamento</Text>
<Text style={styles.statValue}>{currency(totalRevenue)}</Text>
</Card>
<Card style={styles.statCard}>
<Text style={styles.statLabel}>Pendentes</Text>
<Text style={styles.statValue}>{activeAppointments.length}</Text>
</Card>
<Card style={styles.statCard}>
<Text style={styles.statLabel}>Concluídos</Text>
<Text style={styles.statValue}>{completedAppointments.length}</Text>
</Card>
<Card style={styles.statCard}>
<Text style={styles.statLabel}>Stock baixo</Text>
<Text style={[styles.statValue, lowStock.length > 0 && styles.statValueWarning]}>
{lowStock.length}
</Text>
</Card>
</View>
</View>
)}
{activeTab === 'appointments' && (
<View>
{activeAppointments.length > 0 ? (
activeAppointments.map((a) => {
const svc = shop.services.find((s) => s.id === a.serviceId);
const barber = shop.barbers.find((b) => b.id === a.barberId);
return (
<Card key={a.id} style={styles.itemCard}>
<View style={styles.itemHeader}>
<View>
<Text style={styles.itemName}>{svc?.name ?? 'Serviço'}</Text>
<Text style={styles.itemDesc}>{barber?.name} · {a.date}</Text>
</View>
<Badge color={a.status === 'pendente' ? 'amber' : a.status === 'confirmado' ? 'green' : 'red'}>
{a.status}
</Badge>
</View>
<View style={styles.statusSelector}>
<Text style={styles.selectorLabel}>Alterar status:</Text>
<View style={styles.statusButtons}>
{['pendente', 'confirmado', 'concluido', 'cancelado'].map((s) => (
<Button
key={s}
onPress={() => updateAppointmentStatus(a.id, s as any)}
variant={a.status === s ? 'solid' : 'outline'}
size="sm"
style={styles.statusButton}
>
{s}
</Button>
))}
</View>
</View>
</Card>
);
})
) : (
<Card style={styles.emptyCard}>
<Text style={styles.emptyText}>Nenhum agendamento ativo</Text>
</Card>
)}
</View>
)}
{activeTab === 'orders' && (
<View>
{productOrders.length > 0 ? (
productOrders.map((o) => (
<Card key={o.id} style={styles.itemCard}>
<View style={styles.itemHeader}>
<View>
<Text style={styles.itemName}>{currency(o.total)}</Text>
<Text style={styles.itemDesc}>{new Date(o.createdAt).toLocaleString('pt-BR')}</Text>
</View>
<Badge color={o.status === 'pendente' ? 'amber' : o.status === 'confirmado' ? 'green' : 'red'}>
{o.status}
</Badge>
</View>
<View style={styles.statusButtons}>
{['pendente', 'confirmado', 'concluido', 'cancelado'].map((s) => (
<Button
key={s}
onPress={() => updateOrderStatus(o.id, s as any)}
variant={o.status === s ? 'solid' : 'outline'}
size="sm"
style={styles.statusButton}
>
{s}
</Button>
))}
</View>
</Card>
))
) : (
<Card style={styles.emptyCard}>
<Text style={styles.emptyText}>Nenhum pedido de produtos</Text>
</Card>
)}
</View>
)}
{activeTab === 'services' && (
<View>
{shop.services.map((s) => (
<Card key={s.id} style={styles.itemCard}>
<View style={styles.itemHeader}>
<View>
<Text style={styles.itemName}>{s.name}</Text>
<Text style={styles.itemDesc}>Duração: {s.duration} min</Text>
</View>
<Text style={styles.itemPrice}>{currency(s.price)}</Text>
</View>
<Button
onPress={() => {
Alert.alert('Confirmar', 'Deseja remover este serviço?', [
{ text: 'Cancelar', style: 'cancel' },
{ text: 'Remover', style: 'destructive', onPress: () => deleteService(shop.id, s.id) },
]);
}}
variant="outline"
size="sm"
style={styles.deleteButton}
>
Remover
</Button>
</Card>
))}
<Card style={styles.formCard}>
<Text style={styles.formTitle}>Adicionar serviço</Text>
<Input label="Nome" value={svcName} onChangeText={setSvcName} placeholder="Ex: Corte Fade" />
<Input label="Preço" value={svcPrice} onChangeText={setSvcPrice} keyboardType="numeric" placeholder="50" />
<Input label="Duração (min)" value={svcDuration} onChangeText={setSvcDuration} keyboardType="numeric" placeholder="30" />
<Button onPress={addNewService} style={styles.addButton}>
Adicionar
</Button>
</Card>
</View>
)}
{activeTab === 'products' && (
<View>
{lowStock.length > 0 && (
<Card style={styles.alertCard}>
<Text style={styles.alertText}>
Atenção: {lowStock.length} {lowStock.length === 1 ? 'produto com stock baixo' : 'produtos com stock baixo'}
</Text>
</Card>
)}
{shop.products.map((p) => (
<Card key={p.id} style={[styles.itemCard, p.stock <= 3 && styles.itemCardWarning]}>
<View style={styles.itemHeader}>
<View>
<Text style={styles.itemName}>{p.name}</Text>
<Text style={styles.itemDesc}>Stock: {p.stock} unidades</Text>
</View>
<Text style={styles.itemPrice}>{currency(p.price)}</Text>
</View>
<View style={styles.stockControls}>
<Button onPress={() => updateProductStock(p.id, -1)} variant="outline" size="sm" style={styles.stockButton}>
-1
</Button>
<Button onPress={() => updateProductStock(p.id, 1)} variant="outline" size="sm" style={styles.stockButton}>
+1
</Button>
<Button
onPress={() => {
Alert.alert('Confirmar', 'Deseja remover este produto?', [
{ text: 'Cancelar', style: 'cancel' },
{ text: 'Remover', style: 'destructive', onPress: () => deleteProduct(shop.id, p.id) },
]);
}}
variant="outline"
size="sm"
style={styles.stockButton}
>
Remover
</Button>
</View>
</Card>
))}
<Card style={styles.formCard}>
<Text style={styles.formTitle}>Adicionar produto</Text>
<Input label="Nome" value={prodName} onChangeText={setProdName} placeholder="Ex: Pomada" />
<Input label="Preço" value={prodPrice} onChangeText={setProdPrice} keyboardType="numeric" placeholder="30" />
<Input label="Stock inicial" value={prodStock} onChangeText={setProdStock} keyboardType="numeric" placeholder="10" />
<Button onPress={addNewProduct} style={styles.addButton}>
Adicionar
</Button>
</Card>
</View>
)}
{activeTab === 'barbers' && (
<View>
{shop.barbers.map((b) => (
<Card key={b.id} style={styles.itemCard}>
<View style={styles.itemHeader}>
<View>
<Text style={styles.itemName}>{b.name}</Text>
<Text style={styles.itemDesc}>
Especialidades: {b.specialties.length > 0 ? b.specialties.join(', ') : 'Nenhuma'}
</Text>
</View>
</View>
<Button
onPress={() => {
Alert.alert('Confirmar', 'Deseja remover este barbeiro?', [
{ text: 'Cancelar', style: 'cancel' },
{ text: 'Remover', style: 'destructive', onPress: () => deleteBarber(shop.id, b.id) },
]);
}}
variant="outline"
size="sm"
style={styles.deleteButton}
>
Remover
</Button>
</Card>
))}
<Card style={styles.formCard}>
<Text style={styles.formTitle}>Adicionar barbeiro</Text>
<Input label="Nome" value={barberName} onChangeText={setBarberName} placeholder="Ex: João Silva" />
<Input label="Especialidades" value={barberSpecs} onChangeText={setBarberSpecs} placeholder="Fade, Navalha, Barba" />
<Button onPress={addNewBarber} style={styles.addButton}>
Adicionar
</Button>
</Card>
</View>
)}
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 16,
backgroundColor: '#fff',
borderBottomWidth: 1,
borderBottomColor: '#e2e8f0',
},
title: {
fontSize: 20,
fontWeight: 'bold',
color: '#0f172a',
},
tabsContainer: {
backgroundColor: '#fff',
borderBottomWidth: 1,
borderBottomColor: '#e2e8f0',
},
tab: {
paddingHorizontal: 16,
paddingVertical: 12,
borderBottomWidth: 2,
borderBottomColor: 'transparent',
},
tabActive: {
borderBottomColor: '#f59e0b',
},
tabText: {
fontSize: 14,
fontWeight: '600',
color: '#64748b',
},
tabTextActive: {
color: '#f59e0b',
},
content: {
flex: 1,
},
contentInner: {
padding: 16,
},
statsGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 12,
marginBottom: 16,
},
statCard: {
flex: 1,
minWidth: '45%',
padding: 16,
},
statLabel: {
fontSize: 12,
color: '#64748b',
marginBottom: 4,
},
statValue: {
fontSize: 20,
fontWeight: 'bold',
color: '#0f172a',
},
statValueWarning: {
color: '#f59e0b',
},
itemCard: {
marginBottom: 12,
padding: 16,
},
itemCardWarning: {
borderColor: '#fbbf24',
backgroundColor: '#fef3c7',
},
itemHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 12,
},
itemName: {
fontSize: 16,
fontWeight: 'bold',
color: '#0f172a',
flex: 1,
},
itemDesc: {
fontSize: 14,
color: '#64748b',
marginTop: 4,
},
itemPrice: {
fontSize: 18,
fontWeight: 'bold',
color: '#f59e0b',
},
statusSelector: {
marginTop: 8,
},
selectorLabel: {
fontSize: 12,
color: '#64748b',
marginBottom: 8,
},
statusButtons: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
},
statusButton: {
flex: 1,
minWidth: '22%',
},
emptyCard: {
padding: 32,
alignItems: 'center',
},
emptyText: {
fontSize: 14,
color: '#64748b',
},
alertCard: {
backgroundColor: '#fef3c7',
borderColor: '#fbbf24',
marginBottom: 16,
padding: 16,
},
alertText: {
fontSize: 14,
fontWeight: '600',
color: '#92400e',
},
formCard: {
marginTop: 16,
padding: 16,
},
formTitle: {
fontSize: 16,
fontWeight: 'bold',
color: '#0f172a',
marginBottom: 16,
},
addButton: {
width: '100%',
marginTop: 8,
},
deleteButton: {
width: '100%',
marginTop: 8,
},
stockControls: {
flexDirection: 'row',
gap: 8,
marginTop: 8,
},
stockButton: {
flex: 1,
},
});

109
src/pages/Explore.tsx Normal file
View File

@@ -0,0 +1,109 @@
import React from 'react';
import { View, Text, StyleSheet, ScrollView, FlatList } from 'react-native';
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 Explore() {
const navigation = useNavigation();
const { shops } = useApp();
return (
<View style={styles.container}>
<Text style={styles.title}>Explorar barbearias</Text>
<FlatList
data={shops}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.list}
renderItem={({ item: shop }) => (
<Card style={styles.shopCard}>
<View style={styles.shopHeader}>
<Text style={styles.shopName}>{shop.name}</Text>
<Badge color="amber">{shop.rating.toFixed(1)} </Badge>
</View>
<Text style={styles.shopAddress}>{shop.address}</Text>
<View style={styles.shopInfo}>
<Text style={styles.shopInfoText}>{shop.services.length} serviços</Text>
<Text style={styles.shopInfoText}></Text>
<Text style={styles.shopInfoText}>{shop.barbers.length} barbeiros</Text>
</View>
<View style={styles.buttons}>
<Button
onPress={() => navigation.navigate('ShopDetails' as never, { shopId: shop.id } as never)}
variant="outline"
style={styles.button}
>
Ver detalhes
</Button>
<Button
onPress={() => navigation.navigate('Booking' as never, { shopId: shop.id } as never)}
style={styles.button}
>
Agendar
</Button>
</View>
</Card>
)}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
padding: 16,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#0f172a',
marginBottom: 16,
},
list: {
gap: 16,
},
shopCard: {
marginBottom: 12,
},
shopHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
shopName: {
fontSize: 18,
fontWeight: 'bold',
color: '#0f172a',
flex: 1,
},
shopAddress: {
fontSize: 14,
color: '#64748b',
marginBottom: 8,
},
shopInfo: {
flexDirection: 'row',
gap: 8,
marginBottom: 12,
},
shopInfoText: {
fontSize: 12,
color: '#94a3b8',
},
buttons: {
flexDirection: 'row',
gap: 8,
marginTop: 8,
},
button: {
flex: 1,
},
});

119
src/pages/Landing.tsx Normal file
View File

@@ -0,0 +1,119 @@
import React from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { Button } from '../components/ui/Button';
import { Card } from '../components/ui/Card';
export default function Landing() {
const navigation = useNavigation();
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<View style={styles.hero}>
<Text style={styles.heroTitle}>Smart Agenda</Text>
<Text style={styles.heroSubtitle}>
Agendamentos, produtos e gestão em um único lugar.
</Text>
<Text style={styles.heroDesc}>
Experiência mobile-first para clientes e painel completo para barbearias.
</Text>
<View style={styles.buttons}>
<Button
onPress={() => navigation.navigate('Explore' as never)}
style={styles.button}
size="lg"
>
Explorar barbearias
</Button>
<Button
onPress={() => navigation.navigate('Register' as never)}
variant="outline"
style={styles.button}
size="lg"
>
Criar conta
</Button>
</View>
</View>
<View style={styles.features}>
<Card style={styles.featureCard}>
<Text style={styles.featureTitle}>Agendamentos</Text>
<Text style={styles.featureDesc}>
Escolha serviço, barbeiro, data e horário com validação de slots.
</Text>
</Card>
<Card style={styles.featureCard}>
<Text style={styles.featureTitle}>Carrinho</Text>
<Text style={styles.featureDesc}>
Produtos e serviços agrupados por barbearia, pagamento rápido.
</Text>
</Card>
<Card style={styles.featureCard}>
<Text style={styles.featureTitle}>Painel</Text>
<Text style={styles.featureDesc}>
Faturamento, agendamentos, pedidos, barbearia no controle.
</Text>
</Card>
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
},
content: {
padding: 16,
},
hero: {
backgroundColor: '#f59e0b',
borderRadius: 16,
padding: 24,
marginBottom: 24,
},
heroTitle: {
fontSize: 28,
fontWeight: 'bold',
color: '#fff',
marginBottom: 8,
},
heroSubtitle: {
fontSize: 20,
fontWeight: '600',
color: '#fff',
marginBottom: 8,
},
heroDesc: {
fontSize: 16,
color: '#fef3c7',
marginBottom: 20,
},
buttons: {
gap: 12,
},
button: {
width: '100%',
},
features: {
gap: 16,
},
featureCard: {
marginBottom: 12,
},
featureTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#0f172a',
marginBottom: 8,
},
featureDesc: {
fontSize: 14,
color: '#64748b',
lineHeight: 20,
},
});

165
src/pages/Profile.tsx Normal file
View File

@@ -0,0 +1,165 @@
import React from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useApp } from '../context/AppContext';
import { Card } from '../components/ui/Card';
import { Badge } from '../components/ui/Badge';
import { Button } from '../components/ui/Button';
import { currency } from '../lib/format';
const statusColor: Record<string, 'amber' | 'green' | 'slate' | 'red'> = {
pendente: 'amber',
confirmado: 'green',
concluido: 'green',
cancelado: 'red',
};
export default function Profile() {
const navigation = useNavigation();
const { user, appointments, orders, shops, logout } = useApp();
if (!user) {
return (
<View style={styles.container}>
<Text>Faça login para ver o perfil</Text>
</View>
);
}
const myAppointments = appointments.filter((a) => a.customerId === user.id);
const myOrders = orders.filter((o) => o.customerId === user.id);
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<Card style={styles.profileCard}>
<Text style={styles.profileName}>Olá, {user.name}</Text>
<Text style={styles.profileEmail}>{user.email}</Text>
<Badge color="amber" style={styles.roleBadge}>
{user.role === 'cliente' ? 'Cliente' : 'Barbearia'}
</Badge>
<Button onPress={logout} variant="outline" style={styles.logoutButton}>
Sair
</Button>
</Card>
<Text style={styles.sectionTitle}>Agendamentos</Text>
{myAppointments.length > 0 ? (
myAppointments.map((a) => {
const shop = shops.find((s) => s.id === a.shopId);
return (
<Card key={a.id} style={styles.itemCard}>
<View style={styles.itemHeader}>
<Text style={styles.itemName}>{shop?.name}</Text>
<Badge color={statusColor[a.status]}>{a.status}</Badge>
</View>
<Text style={styles.itemDate}>{a.date}</Text>
<Text style={styles.itemTotal}>{currency(a.total)}</Text>
</Card>
);
})
) : (
<Card style={styles.emptyCard}>
<Text style={styles.emptyText}>Nenhum agendamento ainda</Text>
</Card>
)}
<Text style={styles.sectionTitle}>Pedidos</Text>
{myOrders.length > 0 ? (
myOrders.map((o) => {
const shop = shops.find((s) => s.id === o.shopId);
return (
<Card key={o.id} style={styles.itemCard}>
<View style={styles.itemHeader}>
<Text style={styles.itemName}>{shop?.name}</Text>
<Badge color={statusColor[o.status]}>{o.status}</Badge>
</View>
<Text style={styles.itemDate}>
{new Date(o.createdAt).toLocaleString('pt-BR')}
</Text>
<Text style={styles.itemTotal}>{currency(o.total)}</Text>
</Card>
);
})
) : (
<Card style={styles.emptyCard}>
<Text style={styles.emptyText}>Nenhum pedido ainda</Text>
</Card>
)}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
},
content: {
padding: 16,
},
profileCard: {
marginBottom: 24,
padding: 20,
},
profileName: {
fontSize: 24,
fontWeight: 'bold',
color: '#0f172a',
marginBottom: 4,
},
profileEmail: {
fontSize: 14,
color: '#64748b',
marginBottom: 12,
},
roleBadge: {
alignSelf: 'flex-start',
marginBottom: 16,
},
logoutButton: {
width: '100%',
},
sectionTitle: {
fontSize: 20,
fontWeight: 'bold',
color: '#0f172a',
marginBottom: 12,
marginTop: 8,
},
itemCard: {
marginBottom: 12,
padding: 16,
},
itemHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
itemName: {
fontSize: 16,
fontWeight: 'bold',
color: '#0f172a',
flex: 1,
},
itemDate: {
fontSize: 14,
color: '#64748b',
marginBottom: 4,
},
itemTotal: {
fontSize: 16,
fontWeight: 'bold',
color: '#f59e0b',
},
emptyCard: {
padding: 32,
alignItems: 'center',
},
emptyText: {
fontSize: 14,
color: '#64748b',
},
});

184
src/pages/ShopDetails.tsx Normal file
View File

@@ -0,0 +1,184 @@
import React, { useState, useMemo } from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
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 { currency } from '../lib/format';
export default function ShopDetails() {
const route = useRoute();
const navigation = useNavigation();
const { shopId } = route.params as { shopId: string };
const { shops, addToCart } = useApp();
const shop = useMemo(() => shops.find((s) => s.id === shopId), [shops, shopId]);
const [tab, setTab] = useState<'servicos' | 'produtos'>('servicos');
if (!shop) {
return (
<View style={styles.container}>
<Text>Barbearia não encontrada</Text>
</View>
);
}
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<View style={styles.header}>
<Text style={styles.title}>{shop.name}</Text>
<Text style={styles.address}>{shop.address}</Text>
<Button
onPress={() => navigation.navigate('Booking' as never, { shopId: shop.id } as never)}
style={styles.bookButton}
>
Agendar
</Button>
</View>
<View style={styles.tabs}>
<TouchableOpacity
style={[styles.tab, tab === 'servicos' && styles.tabActive]}
onPress={() => setTab('servicos')}
>
<Text style={[styles.tabText, tab === 'servicos' && styles.tabTextActive]}>Serviços</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.tab, tab === 'produtos' && styles.tabActive]}
onPress={() => setTab('produtos')}
>
<Text style={[styles.tabText, tab === 'produtos' && styles.tabTextActive]}>Produtos</Text>
</TouchableOpacity>
</View>
{tab === 'servicos' ? (
<View style={styles.list}>
{shop.services.map((service) => (
<Card key={service.id} style={styles.itemCard}>
<View style={styles.itemHeader}>
<Text style={styles.itemName}>{service.name}</Text>
<Text style={styles.itemPrice}>{currency(service.price)}</Text>
</View>
<Text style={styles.itemDesc}>Duração: {service.duration} min</Text>
<Button
onPress={() => addToCart({ shopId: shop.id, type: 'service', refId: service.id, qty: 1 })}
size="sm"
style={styles.addButton}
>
Adicionar ao carrinho
</Button>
</Card>
))}
</View>
) : (
<View style={styles.list}>
{shop.products.map((product) => (
<Card key={product.id} style={styles.itemCard}>
<View style={styles.itemHeader}>
<Text style={styles.itemName}>{product.name}</Text>
<Text style={styles.itemPrice}>{currency(product.price)}</Text>
</View>
<Text style={styles.itemDesc}>Stock: {product.stock} unidades</Text>
{product.stock <= 3 && <Badge color="amber" style={styles.stockBadge}>Stock baixo</Badge>}
<Button
onPress={() => addToCart({ shopId: shop.id, type: 'product', refId: product.id, qty: 1 })}
size="sm"
style={styles.addButton}
disabled={product.stock <= 0}
>
{product.stock > 0 ? 'Adicionar ao carrinho' : 'Sem stock'}
</Button>
</Card>
))}
</View>
)}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
},
content: {
padding: 16,
},
header: {
marginBottom: 20,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#0f172a',
marginBottom: 8,
},
address: {
fontSize: 14,
color: '#64748b',
marginBottom: 16,
},
bookButton: {
width: '100%',
},
tabs: {
flexDirection: 'row',
gap: 8,
marginBottom: 16,
},
tab: {
flex: 1,
padding: 12,
borderRadius: 8,
borderWidth: 1,
borderColor: '#e2e8f0',
alignItems: 'center',
},
tabActive: {
borderColor: '#f59e0b',
backgroundColor: '#fef3c7',
},
tabText: {
fontSize: 14,
fontWeight: '600',
color: '#64748b',
},
tabTextActive: {
color: '#f59e0b',
},
list: {
gap: 12,
},
itemCard: {
marginBottom: 12,
},
itemHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 8,
},
itemName: {
fontSize: 16,
fontWeight: 'bold',
color: '#0f172a',
flex: 1,
},
itemPrice: {
fontSize: 16,
fontWeight: 'bold',
color: '#f59e0b',
},
itemDesc: {
fontSize: 14,
color: '#64748b',
marginBottom: 12,
},
stockBadge: {
marginBottom: 8,
},
addButton: {
width: '100%',
},
});

12
src/types.ts Normal file
View File

@@ -0,0 +1,12 @@
export type Barber = { id: string; name: string; specialties: string[]; schedule: { day: string; slots: string[] }[] };
export type Service = { id: string; name: string; price: number; duration: number; barberIds: string[] };
export type Product = { id: string; name: string; price: number; stock: number };
export type BarberShop = { id: string; name: string; address: string; rating: number; services: Service[]; products: Product[]; barbers: Barber[] };
export type AppointmentStatus = 'pendente' | 'confirmado' | 'concluido' | 'cancelado';
export type OrderStatus = 'pendente' | 'confirmado' | 'concluido' | 'cancelado';
export type Appointment = { id: string; shopId: string; serviceId: string; barberId: string; customerId: string; date: string; status: AppointmentStatus; total: number };
export type CartItem = { shopId: string; type: 'service' | 'product'; refId: string; qty: number };
export type Order = { id: string; shopId: string; customerId: string; items: CartItem[]; total: number; status: OrderStatus; createdAt: string };
export type User = { id: string; name: string; email: string; password: string; role: 'cliente' | 'barbearia'; shopId?: string };