diff --git a/src/components/ShopCard.tsx b/src/components/ShopCard.tsx
new file mode 100644
index 0000000..5ea59f5
--- /dev/null
+++ b/src/components/ShopCard.tsx
@@ -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) => (
+
+
+ {shop.imageUrl ? (
+
+ ) : (
+
+
+
+ )}
+
+
+
+ {(shop.rating || 0).toFixed(1)}
+
+ {onToggleFavorite && (
+ {
+ e.stopPropagation?.();
+ onToggleFavorite();
+ }}
+ >
+
+
+ )}
+
+
+
+ {shop.name}
+
+
+
+ {shop.address && shop.address !== 'Endereço a definir' ? shop.address : 'Endereço Indisponível'}
+
+
+
+
+ +{(shop.barbers || []).length} Barbeiros
+
+ Reservar
+
+
+
+
+);
+
+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',
+ },
+});
diff --git a/src/components/ui/Badge.tsx b/src/components/ui/Badge.tsx
index 9df35b0..d9a5e84 100644
--- a/src/components/ui/Badge.tsx
+++ b/src/components/ui/Badge.tsx
@@ -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 (
-
- {children}
+
+ {children}
);
};
@@ -50,4 +50,3 @@ const styles = StyleSheet.create({
fontWeight: '600',
},
});
-
diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx
index 00ebd11..349132c 100644
--- a/src/components/ui/Button.tsx
+++ b/src/components/ui/Button.tsx
@@ -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 (
{loading ? (
-
+
) : (
{children}
)}
@@ -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,
},
diff --git a/src/components/ui/Card.tsx b/src/components/ui/Card.tsx
index 72195b2..ecb8969 100644
--- a/src/components/ui/Card.tsx
+++ b/src/components/ui/Card.tsx
@@ -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,
diff --git a/src/context/AppContext.tsx b/src/context/AppContext.tsx
index f9ecdf1..04cd246 100644
--- a/src/context/AppContext.tsx
+++ b/src/context/AppContext.tsx
@@ -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([]);
const [notifications, setNotifications] = useState([]);
const [user, setUser] = useState(undefined);
+ const [users, setUsers] = useState([]);
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) => {
- 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;
diff --git a/src/lib/theme.ts b/src/lib/theme.ts
index c7b7606..7352691 100644
--- a/src/lib/theme.ts
+++ b/src/lib/theme.ts
@@ -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,
diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx
index 462587c..939ccb8 100644
--- a/src/navigation/AppNavigator.tsx
+++ b/src/navigation/AppNavigator.tsx
@@ -24,7 +24,7 @@ const TabIcon = ({ name, focused }: { name: any; focused: boolean }) => (
);
@@ -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,
},
});
diff --git a/src/pages/AuthLogin.tsx b/src/pages/AuthLogin.tsx
index b434e28..c298cbe 100644
--- a/src/pages/AuthLogin.tsx
+++ b/src/pages/AuthLogin.tsx
@@ -76,7 +76,7 @@ export default function AuthLogin() {
placeholder="••••••••"
/>
-