feat(mobile): alinhar cliente e barbearia ao web com tema, dashboard e reservas completos.
Paridade visual âmbar/slate, explore com ShopCard, painel barbearia com todas as tabs, reservas com fecho/lista de espera, perfil e detalhes da loja enriquecidos, e AppContext com users e notificações. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
176
src/components/ShopCard.tsx
Normal file
176
src/components/ShopCard.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import React from 'react';
|
||||
import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { BarberShop } from '../types';
|
||||
import { colors, radius, shadows } from '../lib/theme';
|
||||
|
||||
type Props = {
|
||||
shop: BarberShop;
|
||||
isFavorite?: boolean;
|
||||
onPress: () => void;
|
||||
onToggleFavorite?: () => void;
|
||||
};
|
||||
|
||||
export const ShopCard = ({ shop, isFavorite, onPress, onToggleFavorite }: Props) => (
|
||||
<TouchableOpacity style={styles.card} activeOpacity={0.92} onPress={onPress}>
|
||||
<View style={styles.imageWrap}>
|
||||
{shop.imageUrl ? (
|
||||
<Image source={{ uri: shop.imageUrl }} style={styles.image} />
|
||||
) : (
|
||||
<View style={styles.imageFallback}>
|
||||
<Ionicons name="cut-outline" size={36} color={colors.brandAccent} />
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.imageOverlay} />
|
||||
<View style={styles.ratingBadge}>
|
||||
<Ionicons name="star" size={12} color={colors.primary} />
|
||||
<Text style={styles.ratingText}>{(shop.rating || 0).toFixed(1)}</Text>
|
||||
</View>
|
||||
{onToggleFavorite && (
|
||||
<TouchableOpacity
|
||||
style={styles.favBtn}
|
||||
activeOpacity={0.8}
|
||||
onPress={(e) => {
|
||||
e.stopPropagation?.();
|
||||
onToggleFavorite();
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name={isFavorite ? 'heart' : 'heart-outline'}
|
||||
size={16}
|
||||
color={isFavorite ? colors.danger : colors.textInverse}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.body}>
|
||||
<Text style={styles.name} numberOfLines={1}>{shop.name}</Text>
|
||||
<View style={styles.addressRow}>
|
||||
<Ionicons name="location-outline" size={14} color={colors.primary} />
|
||||
<Text style={styles.address} numberOfLines={1}>
|
||||
{shop.address && shop.address !== 'Endereço a definir' ? shop.address : 'Endereço Indisponível'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<Text style={styles.barbersLabel}>+{(shop.barbers || []).length} Barbeiros</Text>
|
||||
<View style={styles.cta}>
|
||||
<Text style={styles.ctaText}>Reservar</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: radius.xl,
|
||||
overflow: 'hidden',
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
...shadows.card,
|
||||
},
|
||||
imageWrap: {
|
||||
height: 150,
|
||||
position: 'relative',
|
||||
},
|
||||
image: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
imageFallback: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: colors.surfaceDark,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
imageOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(15,23,42,0.25)',
|
||||
},
|
||||
ratingBadge: {
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
right: 12,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
backgroundColor: 'rgba(15,23,42,0.9)',
|
||||
borderRadius: radius.pill,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.1)',
|
||||
},
|
||||
ratingText: {
|
||||
color: colors.textInverse,
|
||||
fontSize: 11,
|
||||
fontWeight: '800',
|
||||
},
|
||||
favBtn: {
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
left: 12,
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
backgroundColor: 'rgba(15,23,42,0.55)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.12)',
|
||||
},
|
||||
body: {
|
||||
padding: 16,
|
||||
gap: 8,
|
||||
},
|
||||
name: {
|
||||
fontSize: 18,
|
||||
fontWeight: '900',
|
||||
color: colors.text,
|
||||
letterSpacing: -0.3,
|
||||
},
|
||||
addressRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
},
|
||||
address: {
|
||||
flex: 1,
|
||||
color: colors.textMuted,
|
||||
fontSize: 13,
|
||||
fontWeight: '500',
|
||||
},
|
||||
footer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: 4,
|
||||
gap: 8,
|
||||
},
|
||||
barbersLabel: {
|
||||
flex: 1,
|
||||
fontSize: 10,
|
||||
fontWeight: '800',
|
||||
color: colors.textSubtle,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.8,
|
||||
},
|
||||
cta: {
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 8,
|
||||
backgroundColor: colors.surfaceDark,
|
||||
borderRadius: radius.md,
|
||||
minHeight: 36,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
ctaText: {
|
||||
color: colors.brandAccent,
|
||||
fontSize: 12,
|
||||
fontWeight: '800',
|
||||
},
|
||||
});
|
||||
@@ -11,15 +11,15 @@ type Props = {
|
||||
|
||||
const colorMap = {
|
||||
solid: {
|
||||
amber: { bg: colors.accent, text: colors.textInverse },
|
||||
slate: { bg: colors.textSoft, text: colors.textInverse },
|
||||
amber: { bg: colors.primary, text: colors.textInverse },
|
||||
slate: { bg: colors.surfaceDark, text: colors.textInverse },
|
||||
green: { bg: colors.success, text: colors.textInverse },
|
||||
red: { bg: colors.danger, text: colors.textInverse },
|
||||
blue: { bg: colors.primary, text: colors.textInverse },
|
||||
indigo: { bg: colors.primary, text: colors.textInverse },
|
||||
},
|
||||
soft: {
|
||||
amber: { bg: colors.accentSoft, text: '#7a4310' },
|
||||
amber: { bg: colors.primarySoft, text: colors.primaryDark },
|
||||
slate: { bg: colors.backgroundAlt, text: colors.textSoft },
|
||||
green: { bg: colors.successSoft, text: '#166534' },
|
||||
red: { bg: colors.dangerSoft, text: '#991b1b' },
|
||||
@@ -29,11 +29,11 @@ const colorMap = {
|
||||
};
|
||||
|
||||
export const Badge = ({ children, color = 'indigo', variant = 'soft', style }: Props) => {
|
||||
const colors = colorMap[variant][color];
|
||||
const palette = colorMap[variant][color];
|
||||
|
||||
return (
|
||||
<View style={[styles.badge, { backgroundColor: colors.bg }, style]}>
|
||||
<Text style={[styles.text, { color: colors.text }]}>{children}</Text>
|
||||
<View style={[styles.badge, { backgroundColor: palette.bg }, style]}>
|
||||
<Text style={[styles.text, { color: palette.text }]}>{children}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -50,4 +50,3 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { colors, radius, shadows } from '../../lib/theme';
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
onPress?: () => void;
|
||||
variant?: 'solid' | 'outline' | 'ghost';
|
||||
variant?: 'solid' | 'outline' | 'ghost' | 'dark';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
@@ -29,6 +29,11 @@ export const Button = ({ children, onPress, variant = 'solid', size = 'md', disa
|
||||
textStyle,
|
||||
];
|
||||
|
||||
const spinnerColor =
|
||||
variant === 'solid' ? colors.textInverse :
|
||||
variant === 'dark' ? colors.brandAccent :
|
||||
colors.primaryDark;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={buttonStyle}
|
||||
@@ -37,7 +42,7 @@ export const Button = ({ children, onPress, variant = 'solid', size = 'md', disa
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator size="small" color={variant === 'solid' ? colors.textInverse : colors.primary} />
|
||||
<ActivityIndicator size="small" color={spinnerColor} />
|
||||
) : (
|
||||
<Text style={textStyles}>{children}</Text>
|
||||
)}
|
||||
@@ -57,10 +62,14 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: colors.primary,
|
||||
...shadows.soft,
|
||||
},
|
||||
dark: {
|
||||
backgroundColor: colors.surfaceDark,
|
||||
...shadows.soft,
|
||||
},
|
||||
outline: {
|
||||
backgroundColor: 'transparent',
|
||||
borderWidth: 1,
|
||||
borderColor: colors.borderStrong,
|
||||
backgroundColor: colors.surface,
|
||||
borderWidth: 2,
|
||||
borderColor: colors.primary,
|
||||
},
|
||||
ghost: {
|
||||
backgroundColor: 'transparent',
|
||||
@@ -86,6 +95,9 @@ const styles = StyleSheet.create({
|
||||
text_solid: {
|
||||
color: colors.textInverse,
|
||||
},
|
||||
text_dark: {
|
||||
color: colors.brandAccent,
|
||||
},
|
||||
text_outline: {
|
||||
color: colors.primaryDark,
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@ export const Card = ({ children, style }: Props) => {
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: radius.sm,
|
||||
borderRadius: radius.lg,
|
||||
padding: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Platform, Alert } from 'react-native';
|
||||
|
||||
type State = {
|
||||
user?: User;
|
||||
users: User[];
|
||||
shops: BarberShop[];
|
||||
shopsReady: boolean;
|
||||
cart: CartItem[];
|
||||
@@ -64,6 +65,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [waitlists, setWaitlists] = useState<WaitlistEntry[]>([]);
|
||||
const [notifications, setNotifications] = useState<AppNotification[]>([]);
|
||||
const [user, setUser] = useState<User | undefined>(undefined);
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [shopsReady, setShopsReady] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -166,11 +168,22 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const loadUser = async () => {
|
||||
const { data } = await supabase.auth.getUser();
|
||||
if (data.user) await applySupabaseUser(data.user);
|
||||
let mounted = true;
|
||||
|
||||
const { data: sub } = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
if (!mounted) return;
|
||||
if (session?.user) {
|
||||
applySupabaseUser(session.user).then(() => refreshShops());
|
||||
} else {
|
||||
setUser(undefined);
|
||||
}
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
sub.subscription.unsubscribe();
|
||||
};
|
||||
loadUser();
|
||||
}, []);
|
||||
|
||||
const refreshShops = async () => {
|
||||
@@ -185,6 +198,20 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const { data: waitlistsData } = await supabase.from('waitlist').select('*');
|
||||
const { data: notificationsData } = await supabase.from('notifications').select('*');
|
||||
const { data: reviewsData } = await supabase.from('reviews').select('shop_id, rating');
|
||||
const { data: profilesData } = await supabase.from('profiles').select('id, name, role, shop_id');
|
||||
|
||||
if (profilesData) {
|
||||
setUsers(
|
||||
profilesData.map((p: any) => ({
|
||||
id: p.id,
|
||||
name: p.name || 'Utilizador',
|
||||
email: '',
|
||||
password: '',
|
||||
role: p.role === 'barbearia' ? 'barbearia' : 'cliente',
|
||||
shopId: p.shop_id || undefined,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
if (shopsData) {
|
||||
const merged: BarberShop[] = shopsData.map((shop: any) => {
|
||||
@@ -203,6 +230,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
paymentMethods: shop.payment_methods || undefined,
|
||||
socialNetworks: shop.social_networks || undefined,
|
||||
contacts: shop.contacts || undefined,
|
||||
ownerId: shop.owner_id || shop.ownerId || undefined,
|
||||
services: (servicesData || []).filter((s: any) => s.shop_id === shop.id).map((s: any) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
@@ -395,6 +423,14 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
createdAt: data.created_at,
|
||||
};
|
||||
|
||||
const shopOwnerId = shop?.ownerId || shop?.id;
|
||||
if (shopOwnerId) {
|
||||
const clientName = user?.name || 'Um cliente';
|
||||
await supabase.from('notifications').insert([
|
||||
{ user_id: shopOwnerId, message: `${clientName} fez um novo pedido de produtos!` },
|
||||
]);
|
||||
}
|
||||
|
||||
await refreshShops();
|
||||
setCart((prev) => prev.filter((i) => i.shopId !== shopId));
|
||||
return newOrder;
|
||||
@@ -562,20 +598,51 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
};
|
||||
|
||||
const createAppointment = async (input: Omit<Appointment, 'id' | 'status' | 'total'>) => {
|
||||
const svc = shops.flatMap(s => s.services).find(s => s.id === input.serviceId);
|
||||
const total = svc ? svc.price : 0;
|
||||
const { data } = await supabase.from('appointments').insert([{
|
||||
shop_id: input.shopId,
|
||||
const shopRef = shops.find((s) => s.id === input.shopId);
|
||||
if (!shopRef) return null;
|
||||
const svc = shopRef.services.find((s) => s.id === input.serviceId);
|
||||
if (!svc) return null;
|
||||
|
||||
const exists = appointments.find(
|
||||
(ap) => ap.barberId === input.barberId && ap.date === input.date && ap.status !== 'cancelado'
|
||||
);
|
||||
if (exists) return null;
|
||||
|
||||
const total = svc.price;
|
||||
const { data, error } = await supabase.from('appointments').insert([{
|
||||
shop_id: input.shopId,
|
||||
service_id: input.serviceId,
|
||||
barber_id: input.barberId,
|
||||
customer_id: input.customerId,
|
||||
date: input.date,
|
||||
status: 'pendente',
|
||||
total,
|
||||
reminder_minutes: input.reminderMinutes
|
||||
reminder_minutes: input.reminderMinutes,
|
||||
}]).select().single();
|
||||
|
||||
if (error || !data) {
|
||||
console.error('Erro ao criar marcação:', error);
|
||||
return null;
|
||||
}
|
||||
|
||||
const shopOwnerId = shopRef.ownerId || shopRef.id;
|
||||
const clientName = user?.name || 'Um cliente';
|
||||
await supabase.from('notifications').insert([
|
||||
{ user_id: shopOwnerId, message: `${clientName} solicitou um novo agendamento para o dia ${input.date}!` },
|
||||
]);
|
||||
|
||||
await refreshShops();
|
||||
return data as any as Appointment;
|
||||
return {
|
||||
id: data.id,
|
||||
shopId: data.shop_id,
|
||||
serviceId: data.service_id,
|
||||
barberId: data.barber_id,
|
||||
customerId: data.customer_id,
|
||||
date: data.date,
|
||||
status: data.status as Appointment['status'],
|
||||
total: data.total,
|
||||
reminderMinutes: data.reminder_minutes,
|
||||
};
|
||||
};
|
||||
|
||||
const updateAppointmentStatus = async (id: string, status: Appointment['status']) => {
|
||||
@@ -655,6 +722,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const value: AppContextValue = useMemo(
|
||||
() => ({
|
||||
user,
|
||||
users,
|
||||
shops,
|
||||
shopsReady,
|
||||
cart,
|
||||
@@ -690,7 +758,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
markNotificationRead,
|
||||
refreshShops,
|
||||
}),
|
||||
[user, shops, shopsReady, cart, favorites, appointments, orders, waitlists, notifications]
|
||||
[user, users, shops, shopsReady, cart, favorites, appointments, orders, waitlists, notifications]
|
||||
);
|
||||
|
||||
if (loading) return null;
|
||||
|
||||
@@ -1,42 +1,47 @@
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
/** Paleta alinhada com web/src/index.css e tailwind (indigo → amber) */
|
||||
export const colors = {
|
||||
background: '#eef3ef',
|
||||
backgroundAlt: '#e5ebe7',
|
||||
background: '#f8fafc',
|
||||
backgroundAlt: '#f1f5f9',
|
||||
surface: '#ffffff',
|
||||
surfaceMuted: '#f7faf8',
|
||||
surfaceDark: '#14211d',
|
||||
surfaceDarkMuted: '#20312b',
|
||||
text: '#17211d',
|
||||
textSoft: '#35443e',
|
||||
textMuted: '#64736d',
|
||||
textSubtle: '#87948f',
|
||||
textInverse: '#f8fbf8',
|
||||
border: '#dce5df',
|
||||
borderStrong: '#c8d5ce',
|
||||
surfaceMuted: '#f1f5f9',
|
||||
surfaceDark: '#0f172a',
|
||||
surfaceDarkMuted: '#1e293b',
|
||||
slate950: '#020617',
|
||||
text: '#0f172a',
|
||||
textSoft: '#334155',
|
||||
textMuted: '#64748b',
|
||||
textSubtle: '#94a3b8',
|
||||
textInverse: '#ffffff',
|
||||
border: '#e2e8f0',
|
||||
borderStrong: '#cbd5e1',
|
||||
borderDark: 'rgba(255,255,255,0.12)',
|
||||
primary: '#0f766e',
|
||||
primaryDark: '#0b4f49',
|
||||
primarySoft: '#d9f1ed',
|
||||
accent: '#c87918',
|
||||
accentSoft: '#fff1d1',
|
||||
primary: '#f59e0b',
|
||||
primaryDark: '#b45309',
|
||||
primaryLight: '#fcd34d',
|
||||
primarySoft: '#fef3c7',
|
||||
brandAccent: '#fbbf24',
|
||||
accent: '#f59e0b',
|
||||
accentSoft: '#fef3c7',
|
||||
success: '#16a34a',
|
||||
successSoft: '#dcfce7',
|
||||
danger: '#dc2626',
|
||||
dangerSoft: '#fee2e2',
|
||||
danger: '#e11d48',
|
||||
dangerSoft: '#fff1f2',
|
||||
warning: '#d97706',
|
||||
warningSoft: '#fef3c7',
|
||||
star: '#f2a51a',
|
||||
overlay: 'rgba(12,20,17,0.56)',
|
||||
placeholder: '#8a9791',
|
||||
star: '#f59e0b',
|
||||
overlay: 'rgba(15,23,42,0.56)',
|
||||
placeholder: '#94a3b8',
|
||||
};
|
||||
|
||||
export const radius = {
|
||||
xs: 6,
|
||||
sm: 8,
|
||||
md: 10,
|
||||
lg: 12,
|
||||
xl: 16,
|
||||
md: 12,
|
||||
lg: 16,
|
||||
xl: 24,
|
||||
xxl: 32,
|
||||
pill: 999,
|
||||
};
|
||||
|
||||
@@ -46,7 +51,7 @@ export const spacing = {
|
||||
|
||||
const cardShadow = Platform.select({
|
||||
ios: {
|
||||
shadowColor: '#102018',
|
||||
shadowColor: '#0f172a',
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
shadowOpacity: 0.08,
|
||||
shadowRadius: 18,
|
||||
@@ -55,7 +60,7 @@ const cardShadow = Platform.select({
|
||||
elevation: 3,
|
||||
},
|
||||
default: {
|
||||
shadowColor: '#102018',
|
||||
shadowColor: '#0f172a',
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
shadowOpacity: 0.08,
|
||||
shadowRadius: 18,
|
||||
@@ -65,7 +70,7 @@ const cardShadow = Platform.select({
|
||||
|
||||
const softShadow = Platform.select({
|
||||
ios: {
|
||||
shadowColor: '#102018',
|
||||
shadowColor: '#0f172a',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.06,
|
||||
shadowRadius: 12,
|
||||
@@ -74,7 +79,7 @@ const softShadow = Platform.select({
|
||||
elevation: 2,
|
||||
},
|
||||
default: {
|
||||
shadowColor: '#102018',
|
||||
shadowColor: '#0f172a',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.06,
|
||||
shadowRadius: 12,
|
||||
|
||||
@@ -24,7 +24,7 @@ const TabIcon = ({ name, focused }: { name: any; focused: boolean }) => (
|
||||
<Ionicons
|
||||
name={focused ? name : `${name}-outline`}
|
||||
size={20}
|
||||
color={focused ? colors.textInverse : '#9cad9f'}
|
||||
color={focused ? colors.brandAccent : colors.textSubtle}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
@@ -67,8 +67,8 @@ function ClientTabs() {
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
tabBarStyle: tabStyles.bar,
|
||||
tabBarActiveTintColor: colors.textInverse,
|
||||
tabBarInactiveTintColor: '#9cad9f',
|
||||
tabBarActiveTintColor: colors.brandAccent,
|
||||
tabBarInactiveTintColor: colors.textSubtle,
|
||||
tabBarLabelStyle: tabStyles.label,
|
||||
}}
|
||||
>
|
||||
@@ -128,8 +128,8 @@ function BarbeariaTabs() {
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
tabBarStyle: tabStyles.bar,
|
||||
tabBarActiveTintColor: colors.textInverse,
|
||||
tabBarInactiveTintColor: '#9cad9f',
|
||||
tabBarActiveTintColor: colors.brandAccent,
|
||||
tabBarInactiveTintColor: colors.textSubtle,
|
||||
tabBarLabelStyle: tabStyles.label,
|
||||
}}
|
||||
>
|
||||
@@ -216,10 +216,10 @@ const iconStyles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
},
|
||||
wrapActive: {
|
||||
backgroundColor: colors.primary,
|
||||
backgroundColor: colors.surfaceDarkMuted,
|
||||
},
|
||||
emoji: {
|
||||
fontSize: 18,
|
||||
color: '#9cad9f',
|
||||
color: colors.textSubtle,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -76,7 +76,7 @@ export default function AuthLogin() {
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
|
||||
<Button onPress={handleSubmit} style={styles.submitButton} loading={loading}>
|
||||
<Button variant="dark" onPress={handleSubmit} style={styles.submitButton} loading={loading}>
|
||||
Entrar na Conta
|
||||
</Button>
|
||||
|
||||
@@ -110,11 +110,10 @@ const styles = StyleSheet.create({
|
||||
logoRow: {
|
||||
alignItems: 'center',
|
||||
marginBottom: 28,
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: radius.sm,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.surfaceDark,
|
||||
borderRadius: radius.lg,
|
||||
paddingVertical: 18,
|
||||
paddingHorizontal: 24,
|
||||
...shadows.card,
|
||||
},
|
||||
logoImage: {
|
||||
@@ -151,7 +150,6 @@ const styles = StyleSheet.create({
|
||||
submitButton: {
|
||||
width: '100%',
|
||||
marginTop: 4,
|
||||
backgroundColor: colors.primary,
|
||||
paddingVertical: 16,
|
||||
},
|
||||
guestButton: {
|
||||
|
||||
@@ -107,7 +107,7 @@ export default function AuthRegister() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button onPress={handleSubmit} style={styles.submitButton} loading={loading}>
|
||||
<Button variant="dark" onPress={handleSubmit} style={styles.submitButton} loading={loading}>
|
||||
Criar minha conta
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function Booking() {
|
||||
const route = useRoute();
|
||||
const navigation = useNavigation();
|
||||
const { shopId, serviceId: initialServiceId } = route.params as { shopId: string; serviceId?: string };
|
||||
const { shops, createAppointment, user, appointments, joinWaitlist } = useApp();
|
||||
const { shops, shopsReady, createAppointment, user, appointments, waitlists, joinWaitlist } = useApp();
|
||||
|
||||
const shop = useMemo(() => shops.find((s) => s.id === shopId), [shops, shopId]);
|
||||
|
||||
@@ -42,15 +42,69 @@ export default function Booking() {
|
||||
return dates;
|
||||
}, []);
|
||||
|
||||
const generateDefaultSlots = (): string[] => {
|
||||
const slots: string[] = [];
|
||||
for (let hour = 9; hour <= 18; hour += 1) {
|
||||
slots.push(`${hour.toString().padStart(2, '0')}:00`);
|
||||
if (hour < 18) slots.push(`${hour.toString().padStart(2, '0')}:30`);
|
||||
}
|
||||
return slots;
|
||||
};
|
||||
|
||||
const isShopClosedOnDate = useMemo(() => {
|
||||
if (!date || !shop) return false;
|
||||
const dateObj = new Date(`${date}T00:00:00`);
|
||||
const ptDays = ['Domingo', 'Segunda-feira', 'Terça-feira', 'Quarta-feira', 'Quinta-feira', 'Sexta-feira', 'Sábado'];
|
||||
const dayName = ptDays[dateObj.getDay()];
|
||||
|
||||
const exception = shop.schedule?.find((s) => s.isException && s.date === date);
|
||||
if (exception) return Boolean(exception.closed);
|
||||
|
||||
const defaultDay = shop.schedule?.find((s) => !s.isException && (s.day === dayName || s.day.startsWith(dayName.slice(0, 3))));
|
||||
if (defaultDay) return Boolean(defaultDay.closed);
|
||||
|
||||
return dayName === 'Domingo';
|
||||
}, [shop?.schedule, date]);
|
||||
|
||||
const processedSlots = useMemo(() => {
|
||||
if (!selectedBarber || !date) return [];
|
||||
const slots = ["09:00", "09:30", "10:00", "10:30", "11:00", "11:30", "12:00", "14:00", "14:30", "15:00", "15:30", "16:00", "16:30", "17:00", "17:30", "18:00"];
|
||||
const booked = appointments
|
||||
.filter(a => a.barberId === barberId && a.status !== 'cancelado' && a.date.startsWith(date))
|
||||
.map(a => a.date.split(' ')[1]);
|
||||
|
||||
return slots.map(t => ({ time: t, isBooked: booked.includes(t) }));
|
||||
}, [selectedBarber, date, barberId, appointments]);
|
||||
if (!selectedBarber || !date || isShopClosedOnDate) return [];
|
||||
|
||||
const specificSchedule = selectedBarber.schedule?.find((s) => s.day === date);
|
||||
const slots =
|
||||
specificSchedule && specificSchedule.slots.length > 0
|
||||
? [...specificSchedule.slots]
|
||||
: generateDefaultSlots();
|
||||
|
||||
const bookedSlots = appointments
|
||||
.filter((apt) => apt.barberId === barberId && apt.status !== 'cancelado' && apt.date.startsWith(date))
|
||||
.map((apt) => apt.date.split(' ')[1])
|
||||
.filter(Boolean);
|
||||
|
||||
const todayStr = new Date().toISOString().split('T')[0];
|
||||
const isToday = date === todayStr;
|
||||
const now = new Date();
|
||||
|
||||
return slots.map((time) => {
|
||||
let isPast = false;
|
||||
if (isToday) {
|
||||
const [h, m] = time.split(':').map(Number);
|
||||
if (h < now.getHours() || (h === now.getHours() && m <= now.getMinutes())) {
|
||||
isPast = true;
|
||||
}
|
||||
}
|
||||
const isBooked = bookedSlots.includes(time) || isPast;
|
||||
const waitlistedByMe = user
|
||||
? waitlists.some(
|
||||
(w) =>
|
||||
w.barberId === barberId &&
|
||||
w.date.startsWith(date) &&
|
||||
w.customerId === user.id &&
|
||||
w.status === 'pending'
|
||||
)
|
||||
: false;
|
||||
return { time, isBooked, waitlistedByMe };
|
||||
});
|
||||
}, [selectedBarber, date, barberId, appointments, user, waitlists, isShopClosedOnDate]);
|
||||
|
||||
const submit = async () => {
|
||||
if (!user) { navigation.navigate('Login' as never); return; }
|
||||
@@ -65,9 +119,19 @@ export default function Booking() {
|
||||
if (appt) {
|
||||
Alert.alert('Sucesso', 'O seu agendamento foi confirmado.');
|
||||
navigation.navigate('ExploreTab' as never);
|
||||
} else {
|
||||
Alert.alert('Erro', 'Não foi possível criar a marcação. O horário pode já estar ocupado.');
|
||||
}
|
||||
};
|
||||
|
||||
if (!shopsReady && !shop) {
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, styles.centered]}>
|
||||
<Text style={styles.loadingText}>A preparar reserva...</Text>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
if (!shop) return null;
|
||||
|
||||
return (
|
||||
@@ -171,20 +235,51 @@ export default function Booking() {
|
||||
</ScrollView>
|
||||
|
||||
<Text style={[styles.stepLabel, { marginTop: 20 }]}>Horários disponíveis</Text>
|
||||
<View style={styles.slotsGrid}>
|
||||
{processedSlots.map(s => (
|
||||
<TouchableOpacity
|
||||
key={s.time}
|
||||
disabled={s.isBooked}
|
||||
onPress={() => setSlot(s.time)}
|
||||
style={[styles.slotItem, slot === s.time && styles.slotActive, s.isBooked && styles.slotBooked]}
|
||||
>
|
||||
<Text style={[styles.slotText, slot === s.time && styles.slotTextActive, s.isBooked && styles.slotTextBooked]}>
|
||||
{s.time}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
{isShopClosedOnDate ? (
|
||||
<View style={styles.closedBox}>
|
||||
<Text style={styles.closedTitle}>O estabelecimento está fechado neste dia.</Text>
|
||||
<Text style={styles.closedHint}>Selecione outra data.</Text>
|
||||
</View>
|
||||
) : processedSlots.some((s) => !s.isBooked) ? (
|
||||
<View style={styles.slotsGrid}>
|
||||
{processedSlots.filter((s) => !s.isBooked).map((s) => (
|
||||
<TouchableOpacity
|
||||
key={s.time}
|
||||
onPress={() => setSlot(s.time)}
|
||||
style={[styles.slotItem, slot === s.time && styles.slotActive]}
|
||||
>
|
||||
<Text style={[styles.slotText, slot === s.time && styles.slotTextActive]}>{s.time}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
) : processedSlots.length > 0 ? (
|
||||
<View style={styles.closedBox}>
|
||||
<Text style={styles.closedTitle}>Todos os horários estão preenchidos.</Text>
|
||||
{processedSlots.some((s) => s.waitlistedByMe) ? (
|
||||
<Text style={styles.closedHint}>Já estás na lista de espera deste dia.</Text>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="dark"
|
||||
onPress={async () => {
|
||||
if (!user) {
|
||||
navigation.navigate('Login' as never);
|
||||
return;
|
||||
}
|
||||
const ok = await joinWaitlist(shop.id, serviceId, barberId, `${date} ${processedSlots[0]?.time || '09:00'}`);
|
||||
Alert.alert(
|
||||
ok ? 'Lista de espera' : 'Erro',
|
||||
ok ? 'Entraste na lista de espera! Serás notificado se houver desistências.' : 'Não foi possível entrar na lista de espera.'
|
||||
);
|
||||
}}
|
||||
>
|
||||
Entrar na Lista de Espera
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<Text style={styles.closedHint}>Sem horários para este dia.</Text>
|
||||
)}
|
||||
|
||||
{slot !== '' && (
|
||||
<View style={styles.footerAction}>
|
||||
@@ -231,6 +326,33 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
centered: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
loadingText: {
|
||||
color: colors.textMuted,
|
||||
fontWeight: '700',
|
||||
},
|
||||
closedBox: {
|
||||
backgroundColor: colors.dangerSoft,
|
||||
borderRadius: radius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: '#fecaca',
|
||||
padding: 20,
|
||||
gap: 10,
|
||||
alignItems: 'center',
|
||||
},
|
||||
closedTitle: {
|
||||
color: colors.danger,
|
||||
fontWeight: '800',
|
||||
textAlign: 'center',
|
||||
},
|
||||
closedHint: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 13,
|
||||
textAlign: 'center',
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,10 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Image, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
|
||||
import { ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { useApp } from '../context/AppContext';
|
||||
import { ShopCard } from '../components/ShopCard';
|
||||
import { Card } from '../components/ui/Card';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { RootStackParamList } from '../navigation/types';
|
||||
@@ -12,7 +13,7 @@ import { Ionicons } from '@expo/vector-icons';
|
||||
|
||||
export default function Explore() {
|
||||
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
||||
const { shops, shopsReady, user, toggleFavorite, isFavorite } = useApp();
|
||||
const { shops, shopsReady, toggleFavorite, isFavorite } = useApp();
|
||||
const [query, setQuery] = useState('');
|
||||
const [filter, setFilter] = useState<'todas' | 'top' | 'servicos'>('todas');
|
||||
|
||||
@@ -34,49 +35,32 @@ export default function Explore() {
|
||||
});
|
||||
}, [shops, query, filter]);
|
||||
|
||||
const featuredShops = filtered.slice(0, 3);
|
||||
|
||||
const goToProfile = () => {
|
||||
if (!user) {
|
||||
navigation.navigate('Login');
|
||||
return;
|
||||
}
|
||||
navigation.getParent()?.navigate('ProfileTab' as never);
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<ScrollView contentContainerStyle={styles.content} keyboardShouldPersistTaps="handled" showsVerticalScrollIndicator={false}>
|
||||
|
||||
{/* Modern Header Section */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerCopy}>
|
||||
<Text style={styles.greeting}>
|
||||
{user ? `Olá, ${user.name.split(' ')[0]} 👋` : 'Bem-vindo 👋'}
|
||||
</Text>
|
||||
<Text style={styles.headline}>Encontra um espaço</Text>
|
||||
<View style={styles.hero}>
|
||||
<View style={styles.kicker}>
|
||||
<Ionicons name="star" size={10} color={colors.primaryDark} />
|
||||
<Text style={styles.kickerText}>As Nossas Barbearias</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.avatarButton}
|
||||
activeOpacity={0.85}
|
||||
onPress={goToProfile}
|
||||
>
|
||||
<View style={styles.avatarGradient}>
|
||||
<Text style={styles.avatarText}>
|
||||
{user ? user.name.charAt(0).toUpperCase() : 'S'}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<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>
|
||||
<Text style={styles.countLabel}>
|
||||
<Text style={styles.countValue}>{filtered.length}</Text> Espaços Disponíveis
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Clean Integrated Search Box */}
|
||||
<View style={styles.searchSection}>
|
||||
<View style={styles.searchBox}>
|
||||
<Ionicons name="search-outline" size={20} color={colors.placeholder} />
|
||||
<Card style={styles.searchCard}>
|
||||
<View style={styles.searchRow}>
|
||||
<Ionicons name="search-outline" size={20} color={colors.textSubtle} />
|
||||
<TextInput
|
||||
value={query}
|
||||
onChangeText={setQuery}
|
||||
placeholder="Pesquisar nome, endereço ou serviço..."
|
||||
placeholder="Pesquisar por nome ou endereço..."
|
||||
placeholderTextColor={colors.placeholder}
|
||||
style={styles.searchInput}
|
||||
/>
|
||||
@@ -87,7 +71,6 @@ export default function Explore() {
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Quick Filters Scroll */}
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.filtersScroll}>
|
||||
{[
|
||||
['todas', 'Melhor avaliação'],
|
||||
@@ -104,7 +87,7 @@ export default function Explore() {
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</Card>
|
||||
|
||||
{!shopsReady ? (
|
||||
<View style={styles.emptyState}>
|
||||
@@ -121,155 +104,17 @@ export default function Explore() {
|
||||
</Button>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
{/* Featured Section */}
|
||||
{featuredShops.length > 0 && (
|
||||
<View style={styles.featuredSection}>
|
||||
<View style={styles.sectionHeaderRow}>
|
||||
<Text style={styles.sectionTitle}>Em destaque</Text>
|
||||
<Text style={styles.countBadge}>{filtered.length} espaços</Text>
|
||||
</View>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.featuredList}>
|
||||
{featuredShops.map((shop) => {
|
||||
const isFav = isFavorite(shop.id);
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={shop.id}
|
||||
style={styles.featuredCard}
|
||||
activeOpacity={0.9}
|
||||
onPress={() => navigation.navigate('ShopDetails', { shopId: shop.id })}
|
||||
>
|
||||
<View style={styles.featuredImageWrapper}>
|
||||
{shop.imageUrl ? (
|
||||
<Image source={{ uri: shop.imageUrl }} style={styles.featuredImage} />
|
||||
) : (
|
||||
<View style={styles.featuredFallback}>
|
||||
<Ionicons name="storefront-outline" size={40} color={colors.primary} style={{ opacity: 0.6 }} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Rating Badge Overlay */}
|
||||
<View style={styles.ratingBadgeOverlay}>
|
||||
<Ionicons name="star" size={12} color={colors.star} />
|
||||
<Text style={styles.ratingBadgeText}>{(shop.rating || 0).toFixed(1)}</Text>
|
||||
</View>
|
||||
|
||||
{/* Floating Favorite Heart Button */}
|
||||
<TouchableOpacity
|
||||
style={styles.favoriteBtnFloating}
|
||||
activeOpacity={0.7}
|
||||
onPress={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleFavorite(shop.id);
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name={isFav ? "heart" : "heart-outline"}
|
||||
size={18}
|
||||
color={isFav ? colors.danger : colors.textInverse}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.featuredCardBody}>
|
||||
<Text style={styles.featuredShopName} numberOfLines={1}>{shop.name}</Text>
|
||||
<View style={styles.infoRowInline}>
|
||||
<Ionicons name="location-outline" size={13} color={colors.textMuted} />
|
||||
<Text style={styles.addressText} numberOfLines={1}>
|
||||
{shop.address && shop.address !== 'Endereço a definir' ? shop.address : 'Morada por definir'}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.cardFooterRow}>
|
||||
<Text style={styles.featuredStatsText}>{(shop.services || []).length} serv. • {(shop.barbers || []).length} barb.</Text>
|
||||
<View style={styles.goBtn}>
|
||||
<Ionicons name="arrow-forward" size={14} color={colors.textInverse} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* All Spaces Section */}
|
||||
<View style={styles.allSpacesSection}>
|
||||
<View style={styles.sectionHeaderRow}>
|
||||
<Text style={styles.sectionTitle}>Todos os espaços</Text>
|
||||
<Text style={styles.countBadge}>{filtered.length}</Text>
|
||||
</View>
|
||||
<View style={styles.allSpacesList}>
|
||||
{filtered.map((shop) => {
|
||||
const isFav = isFavorite(shop.id);
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={shop.id}
|
||||
style={styles.shopCard}
|
||||
activeOpacity={0.9}
|
||||
onPress={() => navigation.navigate('ShopDetails', { shopId: shop.id })}
|
||||
>
|
||||
<View style={styles.shopImageWrapper}>
|
||||
{shop.imageUrl ? (
|
||||
<Image source={{ uri: shop.imageUrl }} style={styles.shopImage} />
|
||||
) : (
|
||||
<View style={styles.shopImageFallback}>
|
||||
<Ionicons name="storefront-outline" size={24} color={colors.primary} style={{ opacity: 0.5 }} />
|
||||
</View>
|
||||
)}
|
||||
{/* Floating Favorite Button on list image */}
|
||||
<TouchableOpacity
|
||||
style={styles.favoriteBtnListFloating}
|
||||
activeOpacity={0.7}
|
||||
onPress={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleFavorite(shop.id);
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name={isFav ? "heart" : "heart-outline"}
|
||||
size={14}
|
||||
color={isFav ? colors.danger : colors.textInverse}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.shopBody}>
|
||||
<View style={styles.shopNameRow}>
|
||||
<Text style={styles.shopName} numberOfLines={1}>{shop.name}</Text>
|
||||
<View style={styles.ratingBadgeSmall}>
|
||||
<Ionicons name="star" size={11} color={colors.star} />
|
||||
<Text style={styles.ratingTextSmall}>{(shop.rating || 0).toFixed(1)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.infoRowInline}>
|
||||
<Ionicons name="location-outline" size={13} color={colors.textMuted} />
|
||||
<Text style={styles.addressText} numberOfLines={1}>
|
||||
{shop.address && shop.address !== 'Endereço a definir' ? shop.address : 'Morada por definir'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.shopStatsRow}>
|
||||
<View style={styles.statTag}>
|
||||
<Ionicons name="cut-outline" size={10} color={colors.primary} />
|
||||
<Text style={styles.statTagText}>{(shop.services || []).length} serv.</Text>
|
||||
</View>
|
||||
<View style={styles.statTag}>
|
||||
<Ionicons name="people-outline" size={10} color={colors.primary} />
|
||||
<Text style={styles.statTagText}>{(shop.barbers || []).length} barb.</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.chevronIconContainer}>
|
||||
<Ionicons name="chevron-forward" size={18} color={colors.primary} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
<View style={styles.grid}>
|
||||
{filtered.map((shop) => (
|
||||
<ShopCard
|
||||
key={shop.id}
|
||||
shop={shop}
|
||||
isFavorite={isFavorite(shop.id)}
|
||||
onToggleFavorite={() => toggleFavorite(shop.id)}
|
||||
onPress={() => navigation.navigate('ShopDetails', { shopId: shop.id })}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
@@ -283,461 +128,105 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
content: {
|
||||
padding: 20,
|
||||
gap: 22,
|
||||
gap: 20,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
header: {
|
||||
hero: {
|
||||
gap: 8,
|
||||
},
|
||||
kicker: {
|
||||
alignSelf: 'flex-start',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginTop: 8,
|
||||
gap: 6,
|
||||
backgroundColor: colors.primarySoft,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 5,
|
||||
borderRadius: radius.pill,
|
||||
},
|
||||
headerCopy: {
|
||||
flex: 1,
|
||||
kickerText: {
|
||||
color: colors.primaryDark,
|
||||
fontSize: 10,
|
||||
fontWeight: '900',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 1.2,
|
||||
},
|
||||
greeting: {
|
||||
title: {
|
||||
color: colors.text,
|
||||
fontSize: 32,
|
||||
fontWeight: '900',
|
||||
letterSpacing: -0.8,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
titleAccent: {
|
||||
color: colors.primary,
|
||||
},
|
||||
subtitle: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
marginBottom: 4,
|
||||
letterSpacing: 0.3,
|
||||
fontWeight: '500',
|
||||
lineHeight: 20,
|
||||
maxWidth: 340,
|
||||
},
|
||||
headline: {
|
||||
color: colors.text,
|
||||
fontSize: 28,
|
||||
fontWeight: '900',
|
||||
letterSpacing: -0.5,
|
||||
},
|
||||
avatarButton: {
|
||||
shadowColor: colors.primary,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 8,
|
||||
elevation: 4,
|
||||
},
|
||||
avatarGradient: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
backgroundColor: colors.primary,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 2,
|
||||
borderColor: colors.surface,
|
||||
},
|
||||
avatarText: {
|
||||
color: colors.textInverse,
|
||||
fontSize: 18,
|
||||
countLabel: {
|
||||
marginTop: 4,
|
||||
color: colors.textSubtle,
|
||||
fontSize: 12,
|
||||
fontWeight: '800',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.8,
|
||||
},
|
||||
searchSection: {
|
||||
gap: 12,
|
||||
countValue: {
|
||||
color: colors.text,
|
||||
},
|
||||
searchBox: {
|
||||
searchCard: {
|
||||
padding: 8,
|
||||
borderRadius: radius.xl,
|
||||
borderWidth: 0,
|
||||
...shadows.card,
|
||||
},
|
||||
searchRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: radius.md,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.1)',
|
||||
paddingHorizontal: 16,
|
||||
gap: 10,
|
||||
height: 54,
|
||||
shadowColor: '#102018',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.04,
|
||||
shadowRadius: 8,
|
||||
elevation: 2,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 4,
|
||||
},
|
||||
searchInput: {
|
||||
flex: 1,
|
||||
height: '100%',
|
||||
height: 48,
|
||||
color: colors.text,
|
||||
fontSize: 15,
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
},
|
||||
filtersScroll: {
|
||||
gap: 8,
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 8,
|
||||
paddingBottom: 8,
|
||||
paddingTop: 4,
|
||||
},
|
||||
filterChip: {
|
||||
borderRadius: radius.pill,
|
||||
backgroundColor: colors.surface,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
backgroundColor: colors.backgroundAlt,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 9,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.08)',
|
||||
shadowColor: '#102018',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.02,
|
||||
shadowRadius: 4,
|
||||
elevation: 1,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
filterChipActive: {
|
||||
backgroundColor: colors.primary,
|
||||
borderColor: colors.primary,
|
||||
backgroundColor: colors.surfaceDark,
|
||||
borderColor: colors.surfaceDark,
|
||||
},
|
||||
filterChipText: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 13,
|
||||
fontSize: 12,
|
||||
fontWeight: '700',
|
||||
},
|
||||
filterChipTextActive: {
|
||||
color: colors.textInverse,
|
||||
color: colors.brandAccent,
|
||||
},
|
||||
categoriesSection: {
|
||||
gap: 12,
|
||||
marginTop: 4,
|
||||
},
|
||||
categoriesScroll: {
|
||||
gap: 14,
|
||||
paddingRight: 10,
|
||||
paddingVertical: 4,
|
||||
},
|
||||
categoryItem: {
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
width: 72,
|
||||
},
|
||||
categoryIconBg: {
|
||||
width: 58,
|
||||
height: 58,
|
||||
borderRadius: 29,
|
||||
backgroundColor: colors.surface,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.08)',
|
||||
shadowColor: '#102018',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 8,
|
||||
elevation: 2,
|
||||
},
|
||||
categoryIconBgActive: {
|
||||
backgroundColor: colors.primary,
|
||||
borderColor: colors.primary,
|
||||
},
|
||||
categoryLabel: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
color: colors.textMuted,
|
||||
textAlign: 'center',
|
||||
},
|
||||
categoryLabelActive: {
|
||||
color: colors.primary,
|
||||
fontWeight: '800',
|
||||
},
|
||||
promoBanner: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: colors.surfaceDark,
|
||||
borderRadius: radius.lg,
|
||||
overflow: 'hidden',
|
||||
shadowColor: colors.primaryDark,
|
||||
shadowOffset: { width: 0, height: 6 },
|
||||
shadowOpacity: 0.12,
|
||||
shadowRadius: 12,
|
||||
elevation: 5,
|
||||
marginTop: 6,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.04)',
|
||||
},
|
||||
promoContent: {
|
||||
flex: 1.2,
|
||||
padding: 20,
|
||||
gap: 8,
|
||||
},
|
||||
promoBadge: {
|
||||
alignSelf: 'flex-start',
|
||||
backgroundColor: 'rgba(255,255,255,0.12)',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 3,
|
||||
borderRadius: radius.xs,
|
||||
},
|
||||
promoBadgeText: {
|
||||
color: '#e2f4f1',
|
||||
fontSize: 10,
|
||||
fontWeight: '800',
|
||||
letterSpacing: 1,
|
||||
},
|
||||
promoTitle: {
|
||||
color: colors.textInverse,
|
||||
fontSize: 18,
|
||||
fontWeight: '900',
|
||||
lineHeight: 22,
|
||||
},
|
||||
promoSubtitle: {
|
||||
color: 'rgba(248,251,248,0.72)',
|
||||
fontSize: 12,
|
||||
lineHeight: 16,
|
||||
fontWeight: '500',
|
||||
},
|
||||
promoButton: {
|
||||
alignSelf: 'flex-start',
|
||||
backgroundColor: colors.surface,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 8,
|
||||
borderRadius: radius.sm,
|
||||
marginTop: 6,
|
||||
},
|
||||
promoButtonText: {
|
||||
color: colors.surfaceDark,
|
||||
fontSize: 12,
|
||||
fontWeight: '800',
|
||||
},
|
||||
promoVisual: {
|
||||
flex: 0.8,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
},
|
||||
promoBackgroundIcon: {
|
||||
position: 'absolute',
|
||||
right: -10,
|
||||
bottom: -10,
|
||||
},
|
||||
sectionHeaderRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 12,
|
||||
},
|
||||
sectionTitle: {
|
||||
color: colors.text,
|
||||
fontSize: 18,
|
||||
fontWeight: '900',
|
||||
letterSpacing: -0.3,
|
||||
},
|
||||
countBadge: {
|
||||
color: colors.primary,
|
||||
backgroundColor: colors.primarySoft,
|
||||
fontSize: 11,
|
||||
fontWeight: '800',
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
borderRadius: radius.pill,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
featuredSection: {
|
||||
marginTop: 4,
|
||||
},
|
||||
featuredList: {
|
||||
grid: {
|
||||
gap: 16,
|
||||
paddingRight: 10,
|
||||
paddingVertical: 4,
|
||||
},
|
||||
featuredCard: {
|
||||
width: 250,
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: radius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.06)',
|
||||
overflow: 'hidden',
|
||||
shadowColor: '#102018',
|
||||
shadowOffset: { width: 0, height: 6 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 10,
|
||||
elevation: 3,
|
||||
},
|
||||
featuredImageWrapper: {
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: 130,
|
||||
},
|
||||
featuredImage: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
featuredFallback: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: colors.primarySoft,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
ratingBadgeOverlay: {
|
||||
position: 'absolute',
|
||||
bottom: 10,
|
||||
left: 10,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(12,20,17,0.7)',
|
||||
borderRadius: radius.pill,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 3.5,
|
||||
gap: 4,
|
||||
borderWidth: 0.5,
|
||||
borderColor: 'rgba(255,255,255,0.15)',
|
||||
},
|
||||
ratingBadgeText: {
|
||||
color: colors.textInverse,
|
||||
fontSize: 11,
|
||||
fontWeight: '800',
|
||||
},
|
||||
favoriteBtnFloating: {
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
right: 10,
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
backgroundColor: 'rgba(12,20,17,0.5)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 0.5,
|
||||
borderColor: 'rgba(255,255,255,0.15)',
|
||||
},
|
||||
featuredCardBody: {
|
||||
padding: 14,
|
||||
gap: 6,
|
||||
},
|
||||
featuredShopName: {
|
||||
fontSize: 16,
|
||||
fontWeight: '800',
|
||||
color: colors.text,
|
||||
},
|
||||
infoRowInline: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 5,
|
||||
},
|
||||
addressText: {
|
||||
flex: 1,
|
||||
color: colors.textMuted,
|
||||
fontWeight: '500',
|
||||
fontSize: 13,
|
||||
},
|
||||
cardFooterRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: 4,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: 'rgba(15,118,110,0.05)',
|
||||
paddingTop: 8,
|
||||
},
|
||||
featuredStatsText: {
|
||||
color: colors.textSubtle,
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
},
|
||||
goBtn: {
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: 11,
|
||||
backgroundColor: colors.primary,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
allSpacesSection: {
|
||||
marginTop: 4,
|
||||
},
|
||||
allSpacesList: {
|
||||
gap: 12,
|
||||
},
|
||||
shopCard: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderRadius: radius.lg,
|
||||
backgroundColor: colors.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.06)',
|
||||
padding: 10,
|
||||
gap: 12,
|
||||
shadowColor: '#102018',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.03,
|
||||
shadowRadius: 8,
|
||||
elevation: 2,
|
||||
},
|
||||
shopImageWrapper: {
|
||||
position: 'relative',
|
||||
width: 82,
|
||||
height: 82,
|
||||
borderRadius: radius.md,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
shopImage: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
shopImageFallback: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: colors.primarySoft,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
favoriteBtnListFloating: {
|
||||
position: 'absolute',
|
||||
top: 4,
|
||||
right: 4,
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
backgroundColor: 'rgba(12,20,17,0.5)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 0.5,
|
||||
borderColor: 'rgba(255,255,255,0.15)',
|
||||
},
|
||||
shopBody: {
|
||||
flex: 1,
|
||||
gap: 4,
|
||||
},
|
||||
shopNameRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 8,
|
||||
},
|
||||
shopName: {
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
fontWeight: '800',
|
||||
color: colors.text,
|
||||
},
|
||||
ratingBadgeSmall: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.accentSoft,
|
||||
borderRadius: radius.pill,
|
||||
paddingHorizontal: 7,
|
||||
paddingVertical: 2,
|
||||
gap: 3,
|
||||
},
|
||||
ratingTextSmall: {
|
||||
color: '#7a4310',
|
||||
fontSize: 10,
|
||||
fontWeight: '800',
|
||||
},
|
||||
shopStatsRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
marginTop: 2,
|
||||
},
|
||||
statTag: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 3,
|
||||
backgroundColor: 'rgba(15,118,110,0.05)',
|
||||
borderRadius: radius.xs,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 2,
|
||||
},
|
||||
statTagText: {
|
||||
color: colors.primary,
|
||||
fontSize: 10,
|
||||
fontWeight: '700',
|
||||
},
|
||||
chevronIconContainer: {
|
||||
width: 26,
|
||||
height: 26,
|
||||
borderRadius: 13,
|
||||
backgroundColor: 'rgba(15,118,110,0.05)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
emptyState: {
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -294,7 +294,7 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '900',
|
||||
},
|
||||
rating: {
|
||||
color: '#7a4310',
|
||||
color: colors.primaryDark,
|
||||
backgroundColor: colors.accentSoft,
|
||||
borderRadius: radius.pill,
|
||||
paddingHorizontal: 10,
|
||||
|
||||
@@ -250,6 +250,9 @@ export default function Profile() {
|
||||
|
||||
<View style={styles.agendaMain}>
|
||||
<Text style={styles.agendaShopPremium} numberOfLines={1}>{shop?.name || 'Barbearia'}</Text>
|
||||
<Text style={styles.agendaServicePremium} numberOfLines={1}>
|
||||
{shop?.services.find((s) => s.id === appt.serviceId)?.name || 'Serviço'} · {shop?.services.find((s) => s.id === appt.serviceId)?.duration || '—'} min
|
||||
</Text>
|
||||
<View style={styles.agendaMetaRow}>
|
||||
<Ionicons name="time-outline" size={14} color={colors.textMuted} />
|
||||
<Text style={styles.agendaTimePremium}>{dateParts[1]} • {currency(appt.total)}</Text>
|
||||
@@ -558,7 +561,7 @@ const styles = StyleSheet.create({
|
||||
height: 72,
|
||||
borderRadius: 36,
|
||||
borderWidth: 2.5,
|
||||
borderColor: 'rgba(15,118,110,0.35)',
|
||||
borderColor: 'rgba(245,158,11,0.35)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 2,
|
||||
@@ -620,7 +623,7 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: radius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.06)',
|
||||
borderColor: colors.border,
|
||||
...shadows.soft,
|
||||
},
|
||||
profileStat: {
|
||||
@@ -688,12 +691,12 @@ const styles = StyleSheet.create({
|
||||
padding: 14,
|
||||
marginRight: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.06)',
|
||||
borderColor: colors.border,
|
||||
gap: 6,
|
||||
...shadows.soft,
|
||||
},
|
||||
notifUnread: {
|
||||
borderColor: 'rgba(15,118,110,0.2)',
|
||||
borderColor: 'rgba(245,158,11,0.2)',
|
||||
backgroundColor: colors.primarySoft,
|
||||
},
|
||||
notifHeader: {
|
||||
@@ -739,7 +742,7 @@ const styles = StyleSheet.create({
|
||||
borderRadius: radius.pill,
|
||||
backgroundColor: colors.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.06)',
|
||||
borderColor: colors.border,
|
||||
},
|
||||
tabActive: {
|
||||
backgroundColor: colors.primary,
|
||||
@@ -791,7 +794,7 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: radius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.06)',
|
||||
borderColor: colors.border,
|
||||
...shadows.soft,
|
||||
},
|
||||
agendaTop: {
|
||||
@@ -805,7 +808,7 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
minWidth: 52,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.06)',
|
||||
borderColor: colors.border,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
calendarHeader: {
|
||||
@@ -873,7 +876,7 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: radius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.06)',
|
||||
borderColor: colors.border,
|
||||
...shadows.soft,
|
||||
},
|
||||
shopIcon: {
|
||||
@@ -884,7 +887,7 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 0.5,
|
||||
borderColor: 'rgba(15,118,110,0.1)',
|
||||
borderColor: colors.borderStrong,
|
||||
},
|
||||
shopIconText: {
|
||||
color: colors.primary,
|
||||
@@ -921,7 +924,7 @@ const styles = StyleSheet.create({
|
||||
gap: 3,
|
||||
},
|
||||
shopRatingText: {
|
||||
color: '#7a4310',
|
||||
color: colors.primaryDark,
|
||||
fontWeight: '800',
|
||||
fontSize: 11,
|
||||
},
|
||||
@@ -933,7 +936,7 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: radius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.06)',
|
||||
borderColor: colors.border,
|
||||
...shadows.soft,
|
||||
},
|
||||
orderTop: {
|
||||
@@ -970,7 +973,7 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: radius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.06)',
|
||||
borderColor: colors.border,
|
||||
...shadows.soft,
|
||||
},
|
||||
reviewHeader: {
|
||||
@@ -1007,7 +1010,7 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: radius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.06)',
|
||||
borderColor: colors.border,
|
||||
...shadows.soft,
|
||||
},
|
||||
statCardFull: {
|
||||
@@ -1017,7 +1020,7 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: radius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.06)',
|
||||
borderColor: colors.border,
|
||||
...shadows.soft,
|
||||
},
|
||||
statIconContainer: {
|
||||
@@ -1071,7 +1074,7 @@ const styles = StyleSheet.create({
|
||||
height: 90,
|
||||
textAlignVertical: 'top',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.08)',
|
||||
borderColor: colors.borderStrong,
|
||||
},
|
||||
reviewActions: {
|
||||
flexDirection: 'row',
|
||||
@@ -1083,7 +1086,7 @@ const styles = StyleSheet.create({
|
||||
marginTop: 20,
|
||||
paddingTop: 20,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: 'rgba(15,118,110,0.08)',
|
||||
borderTopColor: colors.borderStrong,
|
||||
alignItems: 'center',
|
||||
},
|
||||
/* ── Premium Client Layout Styles ── */
|
||||
@@ -1105,7 +1108,7 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: radius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.06)',
|
||||
borderColor: colors.border,
|
||||
gap: 16,
|
||||
...shadows.soft,
|
||||
},
|
||||
@@ -1117,7 +1120,7 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.1)',
|
||||
borderColor: colors.borderStrong,
|
||||
},
|
||||
dateMonthPremium: {
|
||||
color: colors.primaryDark,
|
||||
@@ -1136,6 +1139,12 @@ const styles = StyleSheet.create({
|
||||
fontSize: 16,
|
||||
fontWeight: '800',
|
||||
},
|
||||
agendaServicePremium: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
marginTop: 2,
|
||||
},
|
||||
agendaTimePremium: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 13,
|
||||
@@ -1159,7 +1168,7 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: radius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.04)',
|
||||
borderColor: colors.backgroundAlt,
|
||||
gap: 12,
|
||||
},
|
||||
emptyIconCircle: {
|
||||
@@ -1183,7 +1192,7 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: radius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.06)',
|
||||
borderColor: colors.border,
|
||||
gap: 12,
|
||||
},
|
||||
shopImageMini: {
|
||||
@@ -1218,7 +1227,7 @@ const styles = StyleSheet.create({
|
||||
gap: 4,
|
||||
},
|
||||
shopRatingTextPremium: {
|
||||
color: '#7a4310',
|
||||
color: colors.primaryDark,
|
||||
fontSize: 12,
|
||||
fontWeight: '900',
|
||||
},
|
||||
@@ -1227,7 +1236,7 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: radius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.06)',
|
||||
borderColor: colors.border,
|
||||
},
|
||||
orderTopPremium: {
|
||||
flexDirection: 'row',
|
||||
|
||||
@@ -9,11 +9,12 @@ import { Button } from '../components/ui/Button';
|
||||
import { currency } from '../lib/format';
|
||||
import { RootStackParamList } from '../navigation/types';
|
||||
import { colors, radius, shadows } from '../lib/theme';
|
||||
import { ShopSchedule } from '../types';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
|
||||
type Tab = 'servicos' | 'barbeiros' | 'produtos' | 'detalhes';
|
||||
|
||||
const defaultSchedule = [
|
||||
const defaultSchedule: ShopSchedule[] = [
|
||||
{ day: 'Segunda-feira', open: '09:00', close: '19:30' },
|
||||
{ day: 'Terça-feira', open: '09:00', close: '19:30' },
|
||||
{ day: 'Quarta-feira', open: '09:00', close: '19:30' },
|
||||
@@ -312,6 +313,52 @@ export default function ShopDetails() {
|
||||
) : (
|
||||
<Text style={styles.noContactText}>Contacto telefónico não disponível.</Text>
|
||||
)}
|
||||
|
||||
{shop.contacts?.phone2 ? (
|
||||
<TouchableOpacity onPress={() => Linking.openURL(`tel:${shop.contacts?.phone2}`)} style={styles.phoneLinkBox} activeOpacity={0.7}>
|
||||
<Ionicons name="phone-portrait-outline" size={18} color={colors.primaryDark} />
|
||||
<Text style={styles.contactLink}>{shop.contacts.phone2}</Text>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
|
||||
{shop.paymentMethods && shop.paymentMethods.length > 0 && (
|
||||
<>
|
||||
<View style={styles.divider} />
|
||||
<View style={styles.detailTitleRow}>
|
||||
<Ionicons name="card-outline" size={18} color={colors.primary} />
|
||||
<Text style={styles.detailTitle}>Formas de Pagamento</Text>
|
||||
</View>
|
||||
<Text style={styles.detailBody}>{shop.paymentMethods.join(', ')}</Text>
|
||||
</>
|
||||
)}
|
||||
|
||||
{(shop.socialNetworks?.whatsapp || shop.socialNetworks?.instagram || shop.socialNetworks?.facebook) && (
|
||||
<>
|
||||
<View style={styles.divider} />
|
||||
<View style={styles.detailTitleRow}>
|
||||
<Ionicons name="globe-outline" size={18} color={colors.primary} />
|
||||
<Text style={styles.detailTitle}>Redes Sociais</Text>
|
||||
</View>
|
||||
{shop.socialNetworks?.whatsapp ? (
|
||||
<TouchableOpacity onPress={() => Linking.openURL(`https://wa.me/${shop.socialNetworks?.whatsapp}`)} style={styles.phoneLinkBox}>
|
||||
<Ionicons name="logo-whatsapp" size={18} color={colors.success} />
|
||||
<Text style={styles.contactLink}>WhatsApp</Text>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
{shop.socialNetworks?.instagram ? (
|
||||
<TouchableOpacity onPress={() => Linking.openURL(`https://instagram.com/${shop.socialNetworks?.instagram?.replace('@', '')}`)} style={styles.phoneLinkBox}>
|
||||
<Ionicons name="logo-instagram" size={18} color={colors.primaryDark} />
|
||||
<Text style={styles.contactLink}>Instagram</Text>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
{shop.socialNetworks?.facebook ? (
|
||||
<TouchableOpacity onPress={() => Linking.openURL(shop.socialNetworks!.facebook!)} style={styles.phoneLinkBox}>
|
||||
<Ionicons name="logo-facebook" size={18} color={colors.primary} />
|
||||
<Text style={styles.contactLink}>Facebook</Text>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
</View>
|
||||
@@ -388,8 +435,8 @@ const styles = StyleSheet.create({
|
||||
padding: 20,
|
||||
gap: 14,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.06)',
|
||||
shadowColor: '#102018',
|
||||
borderColor: colors.border,
|
||||
shadowColor: colors.surfaceDark,
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
shadowOpacity: 0.08,
|
||||
shadowRadius: 16,
|
||||
@@ -410,7 +457,7 @@ const styles = StyleSheet.create({
|
||||
gap: 4,
|
||||
},
|
||||
ratingValue: {
|
||||
color: '#7a4310',
|
||||
color: colors.primaryDark,
|
||||
fontSize: 12,
|
||||
fontWeight: '900',
|
||||
},
|
||||
@@ -457,7 +504,7 @@ const styles = StyleSheet.create({
|
||||
borderRadius: radius.md,
|
||||
padding: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.05)',
|
||||
borderColor: colors.border,
|
||||
},
|
||||
quickValue: {
|
||||
color: colors.text,
|
||||
@@ -498,7 +545,7 @@ const styles = StyleSheet.create({
|
||||
borderRadius: radius.pill,
|
||||
backgroundColor: colors.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.05)',
|
||||
borderColor: colors.border,
|
||||
},
|
||||
tabItemActive: {
|
||||
backgroundColor: colors.primary,
|
||||
@@ -525,9 +572,9 @@ const styles = StyleSheet.create({
|
||||
gap: 12,
|
||||
borderRadius: radius.md,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.06)',
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.surface,
|
||||
shadowColor: '#102018',
|
||||
shadowColor: colors.surfaceDark,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.02,
|
||||
shadowRadius: 6,
|
||||
@@ -576,7 +623,7 @@ const styles = StyleSheet.create({
|
||||
gap: 14,
|
||||
borderRadius: radius.md,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.06)',
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.surface,
|
||||
},
|
||||
barberAvatar: {
|
||||
@@ -587,7 +634,7 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.1)',
|
||||
borderColor: colors.borderStrong,
|
||||
},
|
||||
avatarTxt: {
|
||||
color: colors.primary,
|
||||
@@ -621,7 +668,7 @@ const styles = StyleSheet.create({
|
||||
gap: 10,
|
||||
borderRadius: radius.md,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.06)',
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.surface,
|
||||
},
|
||||
prodHeader: {
|
||||
@@ -672,7 +719,7 @@ const styles = StyleSheet.create({
|
||||
gap: 14,
|
||||
borderRadius: radius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(15,118,110,0.06)',
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.surface,
|
||||
},
|
||||
detailTitleRow: {
|
||||
@@ -686,6 +733,11 @@ const styles = StyleSheet.create({
|
||||
fontSize: 16,
|
||||
fontWeight: '800',
|
||||
},
|
||||
detailBody: {
|
||||
color: colors.textMuted,
|
||||
fontSize: 14,
|
||||
lineHeight: 20,
|
||||
},
|
||||
scheduleList: {
|
||||
gap: 8,
|
||||
},
|
||||
@@ -716,7 +768,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
divider: {
|
||||
height: 1,
|
||||
backgroundColor: 'rgba(15,118,110,0.08)',
|
||||
backgroundColor: colors.borderStrong,
|
||||
marginVertical: 6,
|
||||
},
|
||||
phoneLinkBox: {
|
||||
@@ -729,7 +781,7 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: 14,
|
||||
borderRadius: radius.sm,
|
||||
borderWidth: 0.5,
|
||||
borderColor: 'rgba(15,118,110,0.1)',
|
||||
borderColor: colors.borderStrong,
|
||||
},
|
||||
contactLink: {
|
||||
color: colors.primaryDark,
|
||||
|
||||
Reference in New Issue
Block a user