diff --git a/src/pages/Explore.tsx b/src/pages/Explore.tsx index 7667e0a..8ed26d6 100644 --- a/src/pages/Explore.tsx +++ b/src/pages/Explore.tsx @@ -8,19 +8,32 @@ import { Card } from '../components/ui/Card'; import { Button } from '../components/ui/Button'; import { RootStackParamList } from '../navigation/types'; import { colors, radius, shadows } from '../lib/theme'; +import { Ionicons } from '@expo/vector-icons'; export default function Explore() { const navigation = useNavigation>(); - const { shops, shopsReady, user } = useApp(); + const { shops, shopsReady, user, toggleFavorite, isFavorite } = useApp(); const [query, setQuery] = useState(''); const [filter, setFilter] = useState<'todas' | 'top' | 'servicos'>('todas'); + const categories = useMemo(() => [ + { id: 'corte', label: 'Cortes', icon: 'cut-outline' as const, query: 'corte' }, + { id: 'barba', label: 'Barba', icon: 'sparkles-outline' as const, query: 'barba' }, + { id: 'lavagem', label: 'Lavagem', icon: 'water-outline' as const, query: 'lavar' }, + { id: 'tratamento', label: 'Tratamentos', icon: 'leaf-outline' as const, query: 'tratamento' }, + { id: 'rapido', label: 'Expresso', icon: 'flash-outline' as const, query: 'rápido' }, + ], []); + const filtered = useMemo(() => { const normalized = query.trim().toLowerCase(); return [...shops] .filter((shop) => { if (!normalized) return true; - return shop.name.toLowerCase().includes(normalized) || (shop.address || '').toLowerCase().includes(normalized); + return ( + shop.name.toLowerCase().includes(normalized) || + (shop.address || '').toLowerCase().includes(normalized) || + (shop.services || []).some(s => s.name.toLowerCase().includes(normalized)) + ); }) .filter((shop) => (filter === 'top' ? (shop.rating || 0) >= 4.7 : true)) .sort((a, b) => { @@ -28,141 +41,289 @@ export default function Explore() { return (b.rating || 0) - (a.rating || 0); }); }, [shops, query, filter]); + const featuredShops = filtered.slice(0, 3); + const handleCategoryPress = (catQuery: string) => { + if (query.toLowerCase() === catQuery.toLowerCase()) { + setQuery(''); + } else { + setQuery(catQuery); + } + }; + return ( - + + + {/* Modern Header Section */} - {user ? `Olá, ${user.name.split(' ')[0]}` : 'Descobre'} + {user ? `Olá, ${user.name.split(' ')[0]} 👋` : 'Bem-vindo 👋'} Encontra um espaço - - SA - + navigation.navigate('Profile' as any)} + > + + + {user ? user.name.charAt(0).toUpperCase() : 'S'} + + + - + {/* Clean Integrated Search Box */} + - + + {query.length > 0 && ( + setQuery('')}> + + + )} - + + {/* Quick Filters Scroll */} + {[ ['todas', 'Melhor avaliação'], - ['top', 'Top avaliadas'], + ['top', 'Top avaliadas (4.7+)'], ['servicos', 'Mais serviços'], ].map(([id, label]) => ( setFilter(id as typeof filter)} - style={[styles.chip, filter === id && styles.chipActive]} + style={[styles.filterChip, filter === id && styles.filterChipActive]} + activeOpacity={0.8} > - {label} + {label} ))} + {/* Categories Quick Grid */} + + Categorias Populares + + {categories.map((cat) => { + const isSelected = query.toLowerCase() === cat.query.toLowerCase(); + return ( + handleCategoryPress(cat.query)} + activeOpacity={0.75} + > + + + + {cat.label} + + ); + })} + + + + {/* Promo Hero Banner */} + + + + CAMPANHA + + Estilo Premium com 15% OFF + Agenda a tua primeira marcação online hoje e garante o teu desconto. + handleCategoryPress('corte')} + > + Ver Cortes ⚡ + + + + + + + {!shopsReady ? ( - + A carregar espaços... ) : filtered.length === 0 ? ( - 🔍 - Nenhuma barbearia encontrada - Tente ajustar a pesquisa ou filtros. + + Nenhum espaço encontrado + Tenta ajustar a pesquisa ou filtros. ) : ( <> + {/* Featured Section */} {featuredShops.length > 0 && ( - - + + Em destaque - {filtered.length} espaços + {filtered.length} espaços - {featuredShops.map((shop) => ( - navigation.navigate('ShopDetails', { shopId: shop.id })} - > - {shop.imageUrl ? ( - - ) : ( - - {shop.name.charAt(0)} - - )} - - {shop.name} - {shop.address || 'Endereço indisponível'} - - - ★ {(shop.rating || 0).toFixed(1)} + {featuredShops.map((shop) => { + const isFav = isFavorite(shop.id); + return ( + navigation.navigate('ShopDetails', { shopId: shop.id })} + > + + {shop.imageUrl ? ( + + ) : ( + + + + )} + + {/* Rating Badge Overlay */} + + + {(shop.rating || 0).toFixed(1)} - {(shop.services || []).length} serviços + + {/* Floating Favorite Heart Button */} + { + e.stopPropagation(); + toggleFavorite(shop.id); + }} + > + + - - - ))} + + {shop.name} + + + + {shop.address && shop.address !== 'Endereço a definir' ? shop.address : 'Morada por definir'} + + + + {(shop.services || []).length} serv. • {(shop.barbers || []).length} barb. + + + + + + + ); + })} )} - - + {/* All Spaces Section */} + + Todos os espaços - {filtered.length} + {filtered.length} - {filtered.map((shop) => ( - navigation.navigate('ShopDetails', { shopId: shop.id })} - > - {shop.imageUrl ? ( - - ) : ( - - {shop.name.charAt(0)} - - )} - - - {shop.name} - - ★ {(shop.rating || 0).toFixed(1)} + + {filtered.map((shop) => { + const isFav = isFavorite(shop.id); + return ( + navigation.navigate('ShopDetails', { shopId: shop.id })} + > + + {shop.imageUrl ? ( + + ) : ( + + + + )} + {/* Floating Favorite Button on list image */} + { + e.stopPropagation(); + toggleFavorite(shop.id); + }} + > + + - - {shop.address || 'Endereço indisponível'} - - {(shop.services || []).length} serviços - {(shop.barbers || []).length} barbeiros - - - - - - - ))} + + + + {shop.name} + + + {(shop.rating || 0).toFixed(1)} + + + + + + + {shop.address && shop.address !== 'Endereço a definir' ? shop.address : 'Morada por definir'} + + + + + + + {(shop.services || []).length} serv. + + + + {(shop.barbers || []).length} barb. + + + + + + + + + ); + })} + )} @@ -178,248 +339,471 @@ const styles = StyleSheet.create({ }, content: { padding: 20, - gap: 18, - paddingBottom: 32, + gap: 22, + paddingBottom: 40, }, header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', - gap: 16, + marginTop: 8, }, headerCopy: { flex: 1, }, greeting: { color: colors.textMuted, - fontSize: 15, + fontSize: 14, fontWeight: '600', marginBottom: 4, + letterSpacing: 0.3, }, headline: { color: colors.text, - fontSize: 30, - lineHeight: 34, + fontSize: 28, fontWeight: '900', + letterSpacing: -0.5, }, - brandPill: { + avatarButton: { + shadowColor: colors.primary, + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.15, + shadowRadius: 8, + elevation: 4, + }, + avatarGradient: { width: 44, height: 44, - borderRadius: radius.md, - backgroundColor: colors.accentSoft, - borderWidth: 1, - borderColor: 'rgba(255,255,255,0.18)', + borderRadius: 22, + backgroundColor: colors.primary, alignItems: 'center', justifyContent: 'center', + borderWidth: 2, + borderColor: colors.surface, }, - brandText: { - color: '#7a4310', - fontSize: 14, - fontWeight: '900', + avatarText: { + color: colors.textInverse, + fontSize: 18, + fontWeight: '800', + }, + searchSection: { + gap: 12, }, searchBox: { flexDirection: 'row', alignItems: 'center', - backgroundColor: colors.surfaceMuted, + backgroundColor: colors.surface, borderRadius: radius.md, borderWidth: 1, - borderColor: colors.border, + borderColor: 'rgba(15,118,110,0.1)', paddingHorizontal: 16, gap: 10, - }, - searchPanel: { - backgroundColor: colors.surface, - borderRadius: radius.sm, - padding: 12, - gap: 12, - borderWidth: 1, - borderColor: colors.border, - ...shadows.soft, - }, - searchIcon: { - color: colors.primary, - fontSize: 20, + height: 54, + shadowColor: '#102018', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.04, + shadowRadius: 8, + elevation: 2, }, searchInput: { flex: 1, - height: 52, + height: '100%', color: colors.text, fontSize: 15, fontWeight: '500', }, - filters: { + filtersScroll: { gap: 8, + paddingVertical: 2, }, - chip: { + filterChip: { borderRadius: radius.pill, backgroundColor: colors.surface, paddingHorizontal: 16, paddingVertical: 10, borderWidth: 1, - borderColor: colors.border, + borderColor: 'rgba(15,118,110,0.08)', + shadowColor: '#102018', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.02, + shadowRadius: 4, + elevation: 1, }, - chipActive: { + filterChipActive: { backgroundColor: colors.primary, borderColor: colors.primary, }, - chipText: { + filterChipText: { color: colors.textMuted, fontSize: 13, fontWeight: '700', }, - chipTextActive: { + filterChipTextActive: { color: colors.textInverse, }, - list: { + categoriesSection: { gap: 12, + marginTop: 4, }, - featuredBlock: { - gap: 12, + categoriesScroll: { + gap: 14, + paddingRight: 10, + paddingVertical: 4, }, - featuredList: { - gap: 12, - paddingRight: 4, + categoryItem: { + alignItems: 'center', + gap: 8, + width: 72, }, - sectionRow: { + 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', - gap: 12, + marginBottom: 12, }, sectionTitle: { color: colors.text, fontSize: 18, fontWeight: '900', + letterSpacing: -0.3, }, - count: { - color: colors.textMuted, - fontSize: 12, - fontWeight: '700', + countBadge: { + color: colors.primary, + backgroundColor: colors.primarySoft, + fontSize: 11, + fontWeight: '800', + paddingHorizontal: 10, + paddingVertical: 4, + borderRadius: radius.pill, textTransform: 'uppercase', - letterSpacing: 0, + }, + featuredSection: { + marginTop: 4, + }, + featuredList: { + 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.sm, + borderRadius: radius.lg, backgroundColor: colors.surface, borderWidth: 1, - borderColor: colors.border, + borderColor: 'rgba(15,118,110,0.06)', padding: 10, gap: 12, - ...shadows.card, + 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: 88, - height: 88, - borderRadius: radius.sm, + width: '100%', + height: '100%', }, shopImageFallback: { - width: 88, - height: 88, - borderRadius: radius.sm, + width: '100%', + height: '100%', backgroundColor: colors.primarySoft, alignItems: 'center', justifyContent: 'center', }, - shopFallbackText: { - color: colors.primary, - fontSize: 42, - fontWeight: '900', - opacity: 0.5, + 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: 8, + gap: 4, }, - shopRow: { + shopNameRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', - gap: 10, + gap: 8, }, shopName: { flex: 1, - fontSize: 18, + fontSize: 16, fontWeight: '800', color: colors.text, }, - ratingPill: { - backgroundColor: colors.accentSoft, - borderRadius: radius.pill, - paddingHorizontal: 9, - paddingVertical: 4, - }, - ratingText: { - color: '#7a4310', - fontSize: 12, - fontWeight: '800', - }, - address: { - color: colors.textMuted, - fontWeight: '500', - fontSize: 14, - }, - metaRow: { + ratingBadgeSmall: { flexDirection: 'row', alignItems: 'center', - gap: 10, + backgroundColor: colors.accentSoft, + borderRadius: radius.pill, + paddingHorizontal: 7, + paddingVertical: 2, + gap: 3, }, - metaInline: { - color: colors.textMuted, - fontSize: 12, + 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', }, - openIcon: { - width: 28, - height: 28, - borderRadius: radius.pill, - backgroundColor: colors.surfaceMuted, + chevronIconContainer: { + width: 26, + height: 26, + borderRadius: 13, + backgroundColor: 'rgba(15,118,110,0.05)', alignItems: 'center', justifyContent: 'center', }, - openIconText: { - color: colors.primary, - fontSize: 22, - fontWeight: '800', - lineHeight: 24, - }, - featuredCard: { - width: 260, - backgroundColor: colors.surface, - borderRadius: radius.sm, - borderWidth: 1, - borderColor: colors.border, - overflow: 'hidden', - ...shadows.card, - }, - featuredImage: { - width: '100%', - height: 122, - }, - featuredFallback: { - width: '100%', - height: 122, - backgroundColor: colors.primarySoft, - alignItems: 'center', - justifyContent: 'center', - }, - featuredBody: { - padding: 14, - gap: 8, - }, emptyState: { alignItems: 'center', - padding: 40, - gap: 12, - }, - emptyIcon: { - fontSize: 48, - marginBottom: 8, + paddingVertical: 60, + paddingHorizontal: 20, + gap: 14, }, emptyTitle: { color: colors.text, - fontSize: 18, + fontSize: 17, fontWeight: '800', textAlign: 'center', }, @@ -427,5 +811,10 @@ const styles = StyleSheet.create({ color: colors.textMuted, textAlign: 'center', fontWeight: '500', + fontSize: 14, + marginBottom: 6, + }, + loadingSpinner: { + marginBottom: 8, }, });