correção

This commit is contained in:
2026-03-12 16:02:47 +00:00
parent b1344a1257
commit 9d4e04a411
5 changed files with 242 additions and 257 deletions

View File

@@ -1,91 +1,62 @@
import { Link } from 'react-router-dom';
import { Star, MapPin, Scissors, Heart, Calendar } from 'lucide-react';
import { Star, MapPin, Scissors } from 'lucide-react';
import { BarberShop } from '../types';
import { useApp } from '../context/AppContext';
export const ShopCard = ({ shop, compact = false }: { shop: BarberShop; compact?: boolean }) => {
const { toggleFavorite, isFavorite } = useApp();
const favorite = isFavorite(shop.id);
import { Card } from './ui/card';
import { Button } from './ui/button';
export const ShopCard = ({ shop }: { shop: BarberShop }) => {
return (
<div className="group relative flex flex-col bg-white rounded-2xl overflow-hidden shadow-sm hover:shadow-lg border border-slate-100 hover:border-amber-200 transition-all duration-300">
{/* Cover image */}
<div className="relative h-40 bg-gradient-to-br from-slate-800 to-slate-900 overflow-hidden flex-shrink-0">
{shop.imageUrl ? (
<img
src={shop.imageUrl}
alt={shop.name}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
/>
) : (
<div className="w-full h-full flex flex-col items-center justify-center">
<Scissors size={36} className="text-slate-600 mb-2" />
<p className="text-slate-600 text-xs font-medium">Sem foto de capa</p>
<Card hover className="p-4 sm:p-5 flex flex-col w-full group">
<div className="flex gap-4">
{/* Avatar Circular com Badge de Rating */}
<div className="relative shrink-0 mt-1">
<div className="w-16 h-16 md:w-20 md:h-20 rounded-full border-2 border-slate-100 overflow-hidden bg-white flex items-center justify-center shadow-sm">
{shop.imageUrl ? (
<img src={shop.imageUrl} alt={shop.name} className="w-full h-full object-cover" />
) : (
<div className="text-slate-400 font-black text-center leading-none flex flex-col items-center justify-center h-full w-full bg-slate-50">
<Scissors size={24} className="text-slate-400 mb-1" />
</div>
)}
</div>
{/* Rating Badge - Posicionado em cima à direita como na imagem base */}
<div className="absolute -top-1 -right-2 bg-slate-800 border border-slate-700 px-2 py-0.5 rounded-full flex items-center gap-[2px] shadow-sm z-10">
<Star size={11} className="fill-amber-400 text-amber-400" />
<span className="text-white text-[11px] font-semibold tracking-wide">
{shop.rating ? shop.rating.toFixed(1) : '0.0'}
</span>
</div>
)}
{/* Gradient overlay */}
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent" />
{/* Rating badge */}
<div className="absolute top-3 left-3 flex items-center gap-1 bg-black/70 backdrop-blur-sm px-2.5 py-1 rounded-full">
<Star size={11} className="fill-amber-400 text-amber-400" />
<span className="text-white text-xs font-bold">
{shop.rating ? shop.rating.toFixed(1) : '—'}
</span>
</div>
{/* Favorite button */}
<button
onClick={(e) => { e.preventDefault(); toggleFavorite(shop.id); }}
className={`absolute top-3 right-3 p-1.5 rounded-full backdrop-blur-sm transition-all ${favorite
? 'bg-rose-500 text-white shadow-md scale-110'
: 'bg-black/40 text-white hover:bg-rose-500'
}`}
>
<Heart size={14} className={favorite ? 'fill-white' : ''} />
</button>
{/* Shop name on image */}
<div className="absolute bottom-0 left-0 right-0 p-4">
<h2 className="text-white font-bold text-base leading-tight truncate drop-shadow-md">
{/* Informações da Barbearia */}
<div className="flex flex-col flex-1 py-1">
<h2 className="text-slate-900 text-base md:text-lg font-bold uppercase tracking-wide truncate mb-1.5 group-hover:text-amber-600 transition-colors">
{shop.name}
</h2>
<div className="flex items-start gap-1.5 text-slate-500 mb-2">
<MapPin size={16} className="shrink-0 mt-0.5 text-amber-600" />
<p className="text-sm leading-snug line-clamp-2 pr-1">
{shop.address || 'Endereço Indisponível'}
</p>
</div>
<div className="flex items-center gap-2 text-xs text-slate-500 mt-auto font-medium">
<span>{(shop.services || []).length} serviços</span>
<span className="text-slate-300"></span>
<span>{(shop.barbers || []).length} barbeiros</span>
</div>
</div>
</div>
{/* Info section */}
<div className="p-4 flex flex-col gap-3 flex-1">
<div className="flex items-start gap-1.5 text-slate-500">
<MapPin size={13} className="shrink-0 mt-0.5 text-amber-500" />
<p className="text-xs leading-snug line-clamp-1">{shop.address || 'Endereço não disponível'}</p>
</div>
<div className="flex items-center gap-3 text-xs text-slate-500 font-medium">
<span className="flex items-center gap-1">
<Scissors size={11} className="text-slate-400" />
{(shop.services || []).length} serviços
</span>
<span className="text-slate-300"></span>
<span>{(shop.barbers || []).length} barbeiros</span>
</div>
{/* Action buttons */}
<div className="flex gap-2 mt-auto pt-1">
<Link
to={`/barbearia/${shop.id}`}
className="flex-1 text-center text-xs font-semibold py-2 px-3 rounded-xl border border-slate-200 text-slate-700 hover:border-amber-300 hover:text-amber-700 hover:bg-amber-50 transition-all"
>
Ver detalhes
</Link>
<Link
to={`/agendar/${shop.id}`}
className="flex-1 text-center text-xs font-semibold py-2 px-3 rounded-xl bg-gradient-to-r from-amber-500 to-orange-500 text-white hover:from-amber-600 hover:to-orange-600 transition-all flex items-center justify-center gap-1.5 shadow-sm hover:shadow-md"
>
<Calendar size={12} />
Agendar
</Link>
</div>
{/* Botões de Ação na base */}
<div className="flex gap-2 pt-4 mt-4 border-t border-slate-100">
<Button asChild variant="outline" size="sm" className="flex-1">
<Link to={`/barbearia/${shop.id}`}>Ver detalhes</Link>
</Button>
<Button asChild size="sm" className="flex-1 bg-amber-600 hover:bg-amber-700 border-0">
<Link to={`/agendar/${shop.id}`}>Agendar</Link>
</Button>
</div>
</div>
</Card>
);
};

View File

@@ -1,12 +1,11 @@
import { Link, useNavigate, useLocation } from 'react-router-dom'
import { ShoppingCart, User, LogOut, Menu, X, Scissors, Search } from 'lucide-react'
import { Link, useNavigate } from 'react-router-dom'
import { MapPin, ShoppingCart, User, LogOut, Menu, X } from 'lucide-react'
import { useApp } from '../../context/AppContext'
import { useState } from 'react'
export const Header = () => {
const { user, cart, logout } = useApp()
const navigate = useNavigate()
const location = useLocation()
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const handleLogout = () => {
@@ -15,46 +14,36 @@ export const Header = () => {
setMobileMenuOpen(false)
}
const navLink = (to: string) =>
`flex items-center gap-1.5 text-sm font-medium transition-colors px-3 py-1.5 rounded-lg ${location.pathname === to
? 'bg-amber-50 text-amber-700'
: 'text-slate-600 hover:text-amber-700 hover:bg-amber-50'
}`
return (
<header className="sticky top-0 z-30 bg-white/90 backdrop-blur-md border-b border-slate-100 shadow-sm">
<div className="mx-auto flex h-15 max-w-6xl items-center justify-between px-4 py-3">
{/* Logo */}
<header className="sticky top-0 z-30 bg-white/80 backdrop-blur-md border-b border-slate-200/60 shadow-sm">
<div className="mx-auto flex h-16 max-w-5xl items-center justify-between px-4">
<Link
to="/"
className="text-xl font-bold bg-gradient-to-r from-indigo-600 to-blue-700 bg-clip-text text-transparent hover:from-indigo-700 hover:to-blue-800 transition-all"
onClick={() => setMobileMenuOpen(false)}
className="flex items-center gap-2 group"
>
<div className="w-8 h-8 bg-gradient-to-br from-amber-400 to-orange-500 rounded-lg flex items-center justify-center shadow-sm group-hover:shadow-md transition-shadow">
<Scissors size={16} className="text-white" />
</div>
<span className="text-lg font-black text-slate-900 tracking-tight">Smart<span className="text-amber-500">Agenda</span></span>
Smart Agenda
</Link>
{/* Desktop Navigation */}
<nav className="hidden md:flex items-center gap-1">
<nav className="hidden md:flex items-center gap-4">
{user?.role !== 'barbearia' && (
<>
<Link to="/explorar" className={navLink('/explorar')}>
<Search size={15} />
Barbearias
<Link
to="/explorar"
className="flex items-center gap-1.5 text-sm font-medium text-slate-700 hover:text-indigo-600 transition-colors px-3 py-1.5 rounded-lg hover:bg-indigo-50"
>
<MapPin size={16} />
<span>Barbearias</span>
</Link>
<Link
to="/carrinho"
className={`relative p-2 rounded-lg transition-colors ${location.pathname === '/carrinho'
? 'text-amber-700 bg-amber-50'
: 'text-slate-600 hover:text-amber-700 hover:bg-amber-50'
}`}
className="relative text-slate-700 hover:text-indigo-600 transition-colors p-2 rounded-lg hover:bg-indigo-50"
>
<ShoppingCart size={18} />
{cart.length > 0 && (
<span className="absolute -right-1 -top-1 rounded-full bg-amber-500 px-1.5 py-0.5 text-[10px] font-bold text-white shadow-sm min-w-[18px] text-center leading-none flex items-center justify-center">
<span className="absolute -right-1 -top-1 rounded-full bg-gradient-to-r from-indigo-500 to-blue-600 px-1.5 py-0.5 text-[10px] font-bold text-white shadow-sm min-w-[18px] text-center">
{cart.length}
</span>
)}
@@ -63,72 +52,74 @@ export const Header = () => {
)}
{user ? (
<div className="flex items-center gap-1 ml-2 pl-2 border-l border-slate-200">
<div className="flex items-center gap-2">
<button
onClick={() => navigate(user.role === 'barbearia' ? '/painel' : '/perfil')}
className={navLink(user.role === 'barbearia' ? '/painel' : '/perfil')}
className="flex items-center gap-1.5 text-sm font-medium text-slate-700 hover:text-indigo-600 transition-colors px-3 py-1.5 rounded-lg hover:bg-indigo-50"
type="button"
>
<div className="w-6 h-6 rounded-full bg-gradient-to-br from-amber-400 to-amber-600 flex items-center justify-center text-white text-xs font-bold">
{(user.name?.[0] ?? 'U').toUpperCase()}
</div>
<span className="max-w-[100px] truncate">{user.name}</span>
<User size={16} />
<span className="max-w-[120px] truncate">{user.name}</span>
</button>
<button
onClick={handleLogout}
className="p-2 text-slate-400 hover:text-rose-500 hover:bg-rose-50 rounded-lg transition-colors"
className="p-2 text-slate-600 hover:text-rose-600 hover:bg-rose-50 rounded-lg transition-colors"
title="Sair"
type="button"
>
<LogOut size={15} />
<LogOut size={16} />
</button>
</div>
) : (
<Link
to="/login"
className="ml-2 inline-flex items-center gap-1.5 px-4 py-2 rounded-xl bg-gradient-to-r from-amber-500 to-orange-500 text-white text-sm font-semibold shadow-sm hover:shadow-md hover:from-amber-600 hover:to-orange-600 transition-all"
className="inline-flex items-center justify-center rounded-md border border-slate-200 bg-white px-3 py-1.5 text-sm font-medium text-slate-800 shadow-sm hover:bg-slate-50 transition-colors"
>
Entrar
</Link>
)}
</nav>
{/* Mobile right area */}
<div className="flex items-center gap-2 md:hidden">
{user?.role !== 'barbearia' && (
<Link to="/carrinho" className="relative p-2 text-slate-600">
<ShoppingCart size={20} />
{cart.length > 0 && (
<span className="absolute -right-0.5 -top-0.5 rounded-full bg-amber-500 w-4 h-4 text-[10px] font-bold text-white flex items-center justify-center">
{cart.length}
</span>
)}
</Link>
)}
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="p-2 text-slate-700 hover:text-amber-600 hover:bg-amber-50 rounded-lg transition-colors"
type="button"
>
{mobileMenuOpen ? <X size={20} /> : <Menu size={20} />}
</button>
</div>
{/* Mobile Menu Button */}
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="md:hidden p-2 text-slate-700 hover:text-amber-600 hover:bg-amber-50 rounded-lg transition-colors"
type="button"
>
{mobileMenuOpen ? <X size={20} /> : <Menu size={20} />}
</button>
</div>
{/* Mobile Menu */}
{mobileMenuOpen && (
<div className="md:hidden border-t border-slate-100 bg-white/98 backdrop-blur-md">
<nav className="px-4 py-3 space-y-1">
<div className="md:hidden border-t border-slate-200/60 bg-white/95 backdrop-blur-md animate-in slide-in-from-top">
<nav className="px-4 py-3 space-y-2">
{user?.role !== 'barbearia' && (
<Link
to="/explorar"
onClick={() => setMobileMenuOpen(false)}
className="flex items-center gap-2 text-sm font-medium text-slate-700 hover:text-amber-700 px-3 py-2.5 rounded-xl hover:bg-amber-50 transition-colors"
>
<Search size={16} />
Barbearias
</Link>
<>
<Link
to="/explorar"
onClick={() => setMobileMenuOpen(false)}
className="flex items-center gap-2 text-sm font-medium text-slate-700 hover:text-amber-600 transition-colors px-3 py-2 rounded-lg hover:bg-amber-50"
>
<MapPin size={16} />
Barbearias
</Link>
<Link
to="/carrinho"
onClick={() => setMobileMenuOpen(false)}
className="flex items-center gap-2 text-sm font-medium text-slate-700 hover:text-amber-600 transition-colors px-3 py-2 rounded-lg hover:bg-amber-50"
>
<ShoppingCart size={16} />
Carrinho
{cart.length > 0 && (
<span className="ml-auto rounded-full bg-amber-500 px-2 py-0.5 text-[10px] font-bold text-white">
{cart.length}
</span>
)}
</Link>
</>
)}
{user ? (
@@ -138,18 +129,16 @@ export const Header = () => {
navigate(user.role === 'barbearia' ? '/painel' : '/perfil')
setMobileMenuOpen(false)
}}
className="w-full flex items-center gap-2 text-sm font-medium text-slate-700 hover:text-amber-700 px-3 py-2.5 rounded-xl hover:bg-amber-50 transition-colors text-left"
className="w-full flex items-center gap-2 text-sm font-medium text-slate-700 hover:text-amber-600 transition-colors px-3 py-2 rounded-lg hover:bg-amber-50 text-left"
type="button"
>
<div className="w-6 h-6 rounded-full bg-gradient-to-br from-amber-400 to-amber-600 flex items-center justify-center text-white text-xs font-bold">
{(user.name?.[0] ?? 'U').toUpperCase()}
</div>
<User size={16} />
{user.name}
</button>
<button
onClick={handleLogout}
className="w-full flex items-center gap-2 text-sm font-medium text-rose-500 hover:bg-rose-50 px-3 py-2.5 rounded-xl transition-colors text-left"
className="w-full flex items-center gap-2 text-sm font-medium text-rose-600 hover:bg-rose-50 transition-colors px-3 py-2 rounded-lg text-left"
type="button"
>
<LogOut size={16} />
@@ -160,7 +149,7 @@ export const Header = () => {
<Link
to="/login"
onClick={() => setMobileMenuOpen(false)}
className="flex items-center justify-center gap-2 py-2.5 px-3 rounded-xl bg-gradient-to-r from-amber-500 to-orange-500 text-white text-sm font-semibold"
className="inline-flex w-full items-center justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-medium text-white hover:bg-indigo-700 transition-colors"
>
Entrar
</Link>

View File

@@ -1,5 +1,3 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
@@ -14,8 +12,7 @@
}
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
@apply bg-slate-50 text-slate-900 antialiased;
@apply bg-gradient-to-br from-slate-50 via-white to-blue-50/30 text-slate-900 font-sans antialiased;
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
}
@@ -25,11 +22,11 @@
/* Scrollbar styling */
::-webkit-scrollbar {
@apply w-1.5 h-1.5;
@apply w-2 h-2;
}
::-webkit-scrollbar-track {
@apply bg-transparent;
@apply bg-slate-100;
}
::-webkit-scrollbar-thumb {
@@ -41,13 +38,9 @@
.text-balance {
text-wrap: balance;
}
/* Smooth fade-in animation for modals */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fadeIn {
animation: fadeIn 0.25s ease-out forwards;
}
}

View File

@@ -1,147 +1,129 @@
/**
* @file Explore.tsx
* @description View de Exploração de barbearias na versão Web.
* Consome do estado global (`useApp`) a lista consolidada das "shops" populadas
* pela base de dados e aplica filtros do lado do cliente baseados na query, tipo e ordenação.
*/
import { useMemo, useState } from 'react';
import { ShopCard } from '../components/ShopCard';
import { Card } from '../components/ui/card';
import { Chip } from '../components/ui/chip';
import { Input } from '../components/ui/input';
import { useApp } from '../context/AppContext';
import { Search, Heart, Compass, SlidersHorizontal } from 'lucide-react';
import { Search } from 'lucide-react';
export default function Explore() {
const { shops, favorites } = useApp();
const { shops } = useApp();
// Estados para manter as seleções de filtragem
const [query, setQuery] = useState('');
const [filter, setFilter] = useState<'todas' | 'top' | 'produtos' | 'barbeiros' | 'favoritas'>('todas');
const [filter, setFilter] = useState<'todas' | 'top' | 'produtos' | 'barbeiros' | 'servicos'>('todas');
const [sortBy, setSortBy] = useState<'relevancia' | 'avaliacao' | 'preco' | 'servicos'>('relevancia');
const favoriteShops = useMemo(() => shops.filter((s) => favorites.includes(s.id)), [shops, favorites]);
/**
* Deriva a lista de Shops tratada a partir do conjunto mestre global.
* Só recalcula quando os raw `shops` ou os critérios de pesquisa se alteram.
*/
const filtered = useMemo(() => {
const normalized = query.trim().toLowerCase();
// Regra 1: Combinação livre por correspondência parcial textual (Nome/Morada)
const matchesQuery = (name: string, address: string) =>
!normalized || name.toLowerCase().includes(normalized) || address.toLowerCase().includes(normalized);
// Regra 2: Restrições de Chip
const passesFilter = (shop: (typeof shops)[number]) => {
if (filter === 'favoritas') return favorites.includes(shop.id);
if (filter === 'top') return (shop.rating || 0) >= 4;
if (filter === 'top') return (shop.rating || 0) >= 4.7;
if (filter === 'produtos') return (shop.products || []).length > 0;
if (filter === 'barbeiros') return (shop.barbers || []).length >= 2;
if (filter === 'servicos') return (shop.services || []).length >= 2;
return true;
};
return [...shops]
.filter((s) => matchesQuery(s.name, s.address || ''))
// Aplicação condicional com Sort
const sorted = [...shops]
.filter((shop) => matchesQuery(shop.name, shop.address || ''))
.filter(passesFilter)
.sort((a, b) => {
if (sortBy === 'avaliacao') return (b.rating || 0) - (a.rating || 0);
if (sortBy === 'servicos') return (b.services || []).length - (a.services || []).length;
if (sortBy === 'preco') {
const aMin = a.services?.length ? Math.min(...a.services.map((s) => s.price)) : Infinity;
const bMin = b.services?.length ? Math.min(...b.services.map((s) => s.price)) : Infinity;
// Extrai o preço mínimo nos serviços oferecidos e compara
const aMin = (a.services || []).length ? Math.min(...a.services.map((s) => s.price)) : Infinity;
const bMin = (b.services || []).length ? Math.min(...b.services.map((s) => s.price)) : Infinity;
return aMin - bMin;
}
// Critério por defeito ou quebra de empate: Avaliação descendente
if (b.rating !== a.rating) return (b.rating || 0) - (a.rating || 0);
return (b.services || []).length - (a.services || []).length;
});
}, [shops, query, filter, sortBy, favorites]);
const chips: { id: typeof filter; label: string; icon?: React.ReactNode }[] = [
{ id: 'todas', label: 'Todas' },
{ id: 'favoritas', label: `Favoritas${favorites.length > 0 ? ` (${favorites.length})` : ''}`, icon: <Heart size={12} className="fill-current" /> },
{ id: 'top', label: 'Top avaliadas' },
{ id: 'produtos', label: 'Com produtos' },
{ id: 'barbeiros', label: 'Mais barbeiros' },
];
return sorted;
}, [shops, query, filter, sortBy]);
return (
<div className="space-y-6">
{/* Hero Header */}
<div className="relative rounded-2xl overflow-hidden bg-gradient-to-br from-slate-900 via-slate-800 to-amber-900 p-6 md:p-8">
<div className="absolute inset-0 opacity-10 bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-amber-400 to-transparent" />
<div className="relative">
<div className="flex items-center gap-2 mb-2">
<Compass size={18} className="text-amber-400" />
<span className="text-amber-400 text-xs font-semibold uppercase tracking-widest">Explorar</span>
<section className="space-y-2">
<p className="text-xs uppercase tracking-[0.2em] text-slate-500">Explorar</p>
<div className="flex flex-col gap-2 md:flex-row md:items-end md:justify-between">
<div>
<h1 className="text-2xl md:text-3xl font-semibold text-slate-900">Barbearias</h1>
<p className="text-sm text-slate-600">Escolha a sua favorita e agende em minutos.</p>
</div>
<h1 className="text-2xl md:text-3xl font-black text-white mb-1">Barbearias</h1>
<p className="text-slate-400 text-sm">Encontre a sua favorita e agende em minutos.</p>
<div className="text-sm text-slate-500">{filtered.length} resultados</div>
</div>
</div>
</section>
{/* Search & Sort bar */}
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-4 space-y-3">
<div className="flex gap-3">
<div className="relative flex-1">
<Search size={16} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-400" />
<input
<Card className="p-4 md:p-5">
<div className="grid gap-3 md:grid-cols-[1.3fr_auto] md:items-center">
<div className="relative">
<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Pesquisar por nome ou endereço..."
className="w-full pl-10 pr-4 py-2.5 rounded-xl border border-slate-200 text-sm focus:outline-none focus:ring-2 focus:ring-amber-400 focus:border-transparent placeholder:text-slate-400"
className="pl-11"
/>
<Search size={18} className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400" />
</div>
<div className="flex items-center gap-2">
<SlidersHorizontal size={15} className="text-slate-400" />
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as typeof sortBy)}
className="border border-slate-200 rounded-xl px-3 py-2.5 text-sm text-slate-700 focus:outline-none focus:ring-2 focus:ring-amber-400 bg-white"
>
<option value="relevancia">Relevância</option>
<option value="avaliacao">Melhor avaliação</option>
<option value="preco">Menor preço</option>
<option value="servicos">Mais serviços</option>
</select>
</div>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as typeof sortBy)}
className="rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm text-slate-700 shadow-sm"
>
<option value="relevancia">Relevância</option>
<option value="avaliacao">Melhor avaliação</option>
<option value="preco">Menor preço</option>
<option value="servicos">Mais serviços</option>
</select>
</div>
{/* Filter chips */}
<div className="flex gap-2 flex-wrap">
{chips.map((chip) => (
<button
key={chip.id}
onClick={() => setFilter(chip.id)}
className={`flex items-center gap-1.5 px-3.5 py-1.5 rounded-full text-xs font-semibold transition-all ${filter === chip.id
? chip.id === 'favoritas'
? 'bg-rose-500 text-white shadow-sm'
: 'bg-amber-500 text-white shadow-sm'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
}`}
>
{chip.icon}
{chip.label}
</button>
))}
<div className="mt-3 flex flex-wrap gap-2">
<Chip active={filter === 'todas'} onClick={() => setFilter('todas')}>
Todas
</Chip>
<Chip active={filter === 'top'} onClick={() => setFilter('top')}>
Top avaliadas
</Chip>
<Chip active={filter === 'produtos'} onClick={() => setFilter('produtos')}>
Com produtos
</Chip>
<Chip active={filter === 'barbeiros'} onClick={() => setFilter('barbeiros')}>
Mais barbeiros
</Chip>
<Chip active={filter === 'servicos'} onClick={() => setFilter('servicos')}>
Mais serviços
</Chip>
</div>
</div>
</Card>
{/* Results count */}
<div className="flex items-center justify-between px-1">
<p className="text-sm text-slate-500">
<span className="font-semibold text-slate-900">{filtered.length}</span>{' '}
{filtered.length === 1 ? 'barbearia encontrada' : 'barbearias encontradas'}
</p>
</div>
{/* Grid */}
{filtered.length === 0 ? (
<div className="text-center py-16 bg-white rounded-2xl border border-slate-100">
<div className="w-16 h-16 bg-slate-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
{filter === 'favoritas'
? <Heart size={28} className="text-slate-400" />
: <Search size={28} className="text-slate-400" />
}
</div>
<p className="font-semibold text-slate-700 mb-1">
{filter === 'favoritas' ? 'Ainda não tem favoritas' : 'Nenhuma barbearia encontrada'}
</p>
<p className="text-sm text-slate-400">
{filter === 'favoritas'
? 'Clique no ❤️ em qualquer barbearia para a guardar aqui.'
: 'Tente ajustar a pesquisa ou os filtros.'}
</p>
</div>
<Card className="p-8 text-center space-y-2">
<p className="text-lg font-semibold text-slate-900">Nenhuma barbearia encontrada</p>
<p className="text-sm text-slate-600">Tente ajustar a pesquisa ou limpar os filtros.</p>
</Card>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="grid md:grid-cols-2 gap-4">
{filtered.map((shop) => (
<ShopCard key={shop.id} shop={shop} />
))}
@@ -150,3 +132,5 @@ export default function Explore() {
</div>
);
}

View File

@@ -10,7 +10,7 @@ import { Badge } from '../components/ui/badge'
import { Button } from '../components/ui/button'
import { currency } from '../lib/format'
import { useApp } from '../context/AppContext'
import { Calendar, ShoppingBag, User, Clock, Heart, Star } from 'lucide-react'
import { Calendar, ShoppingBag, User, Clock, Heart, Star, MapPin } from 'lucide-react'
import { supabase } from '../lib/supabase'
import { ReviewModal } from '../components/ReviewModal'
@@ -29,7 +29,7 @@ const statusLabel: Record<string, string> = {
}
export default function Profile() {
const { appointments, orders, shops, submitReview } = useApp()
const { appointments, orders, shops, favorites, submitReview } = useApp()
const navigate = useNavigate()
const [authEmail, setAuthEmail] = useState<string>('')
@@ -75,6 +75,9 @@ export default function Profile() {
return orders.filter((o) => o.customerId === authId)
}, [orders, authId])
const favoriteShops = useMemo(() => {
return shops.filter((s) => favorites.includes(s.id))
}, [shops, favorites])
const handleReviewSubmit = async (rating: number, comment: string) => {
if (!reviewTarget) return
@@ -139,11 +142,56 @@ export default function Profile() {
<p className="text-sm text-slate-500 truncate">{authEmail}</p>
<div className="flex items-center gap-2 mt-2">
<Badge color="amber" variant="soft">Cliente</Badge>
{favoriteShops.length > 0 && (
<span className="flex items-center gap-1 text-xs text-rose-500 font-medium">
<Heart size={12} className="fill-rose-500" /> {favoriteShops.length} favorita{favoriteShops.length > 1 ? 's' : ''}
</span>
)}
</div>
</div>
</div>
</Card>
{/* ❤️ Barbearias Favoritas */}
{favoriteShops.length > 0 && (
<section className="space-y-3">
<div className="flex items-center gap-2">
<Heart size={20} className="text-rose-500 fill-rose-500" />
<h2 className="text-xl font-bold text-slate-900">Barbearias Favoritas</h2>
<Badge color="red" variant="soft">{favoriteShops.length}</Badge>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{favoriteShops.map((shop) => (
<Link key={shop.id} to={`/barbearia/${shop.id}`}>
<Card hover className="p-4 flex items-center gap-3 group">
{shop.imageUrl ? (
<img src={shop.imageUrl} alt={shop.name} className="w-14 h-14 rounded-xl object-cover flex-shrink-0" />
) : (
<div className="w-14 h-14 bg-gradient-to-br from-slate-100 to-slate-200 rounded-xl flex items-center justify-center flex-shrink-0">
<User size={20} className="text-slate-400" />
</div>
)}
<div className="flex-1 min-w-0">
<p className="font-bold text-slate-900 truncate group-hover:text-amber-700 transition-colors">{shop.name}</p>
{shop.address && (
<p className="text-xs text-slate-500 flex items-center gap-1 mt-0.5 truncate">
<MapPin size={10} /> {shop.address}
</p>
)}
{shop.rating > 0 && (
<p className="text-xs text-amber-600 flex items-center gap-1 mt-1">
<Star size={10} className="fill-amber-400 text-amber-400" />
{shop.rating.toFixed(1)}
</p>
)}
</div>
</Card>
</Link>
))}
</div>
</section>
)}
{/* Agendamentos */}
<section className="space-y-3">
<div className="flex items-center gap-2">