This commit is contained in:
2026-03-12 15:32:50 +00:00
parent ca27e2acd5
commit 75d8634091
2 changed files with 145 additions and 148 deletions

View File

@@ -1,90 +1,102 @@
import { Link } from 'react-router-dom';
import { Star, MapPin, Scissors, Heart, Calendar } from 'lucide-react';
import { Star, MapPin, Scissors, Heart, Calendar, Users } from 'lucide-react';
import { BarberShop } from '../types';
import { useApp } from '../context/AppContext';
export const ShopCard = ({ shop, compact = false }: { shop: BarberShop; compact?: boolean }) => {
// Paleta de gradientes para barbearias sem foto
const gradients = [
'from-violet-600 to-indigo-700',
'from-amber-500 to-orange-600',
'from-emerald-500 to-teal-700',
'from-rose-500 to-pink-700',
'from-sky-500 to-blue-700',
'from-slate-600 to-slate-800',
];
function shopGradient(name: string) {
const idx = name.charCodeAt(0) % gradients.length;
return gradients[idx];
}
export const ShopCard = ({ shop }: { shop: BarberShop }) => {
const { toggleFavorite, isFavorite } = useApp();
const favorite = isFavorite(shop.id);
const initials = shop.name.slice(0, 2).toUpperCase();
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>
<div className="group flex flex-col bg-white rounded-2xl border border-slate-100 hover:border-amber-200 shadow-sm hover:shadow-md transition-all duration-200 overflow-hidden">
{/* Top accent strip + avatar */}
<div className="flex items-start gap-4 p-4">
{/* Avatar */}
<div className="relative flex-shrink-0">
<div className={`w-16 h-16 rounded-xl bg-gradient-to-br ${shopGradient(shop.name)} overflow-hidden flex items-center justify-center shadow-md`}>
{shop.imageUrl ? (
<img src={shop.imageUrl} alt={shop.name} className="w-full h-full object-cover" />
) : (
<span className="text-white font-black text-xl">{initials}</span>
)}
</div>
{/* Rating pill */}
<div className="absolute -bottom-1.5 -right-1.5 bg-white border border-slate-100 rounded-full px-1.5 py-0.5 flex items-center gap-0.5 shadow-sm">
<Star size={9} className="fill-amber-400 text-amber-400" />
<span className="text-[10px] font-bold text-slate-700">
{shop.rating ? shop.rating.toFixed(1) : '—'}
</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>
{/* Info */}
<div className="flex-1 min-w-0 pt-0.5">
<div className="flex items-start justify-between gap-2">
<h2 className="font-bold text-slate-900 text-base leading-tight group-hover:text-amber-700 transition-colors truncate">
{shop.name}
</h2>
{/* Favorite */}
<button
onClick={(e) => { e.preventDefault(); toggleFavorite(shop.id); }}
className={`flex-shrink-0 p-1 rounded-full transition-all ${favorite ? 'text-rose-500 bg-rose-50' : 'text-slate-300 hover:text-rose-400 hover:bg-rose-50'
}`}
>
<Heart size={15} className={favorite ? 'fill-rose-500' : ''} />
</button>
</div>
{/* 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">
{shop.name}
</h2>
<div className="flex items-center gap-1 mt-1 text-slate-500">
<MapPin size={11} className="flex-shrink-0 text-amber-500" />
<p className="text-xs truncate">{shop.address || 'Endereço não disponível'}</p>
</div>
<div className="flex items-center gap-3 mt-2 text-xs text-slate-400 font-medium">
<span className="flex items-center gap-1">
<Scissors size={10} />
{(shop.services || []).length} serviços
</span>
<span className="flex items-center gap-1">
<Users size={10} />
{(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>
{/* Separator */}
<div className="h-px bg-slate-100 mx-4" />
<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>
{/* Actions */}
<div className="flex gap-2 p-3">
<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-600 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-amber-500 hover:bg-amber-600 text-white transition-all flex items-center justify-center gap-1.5 shadow-sm"
>
<Calendar size={11} />
Agendar
</Link>
</div>
</div>
);

View File

@@ -4,17 +4,15 @@
import { useMemo, useState } from 'react';
import { ShopCard } from '../components/ShopCard';
import { useApp } from '../context/AppContext';
import { Search, Heart, Compass, SlidersHorizontal } from 'lucide-react';
import { Search, Heart, Scissors, SlidersHorizontal, X } from 'lucide-react';
export default function Explore() {
const { shops, favorites } = useApp();
const [query, setQuery] = useState('');
const [filter, setFilter] = useState<'todas' | 'top' | 'produtos' | 'barbeiros' | 'favoritas'>('todas');
const [filter, setFilter] = useState<'todas' | 'top' | 'produtos' | 'favoritas'>('todas');
const [sortBy, setSortBy] = useState<'relevancia' | 'avaliacao' | 'preco' | 'servicos'>('relevancia');
const favoriteShops = useMemo(() => shops.filter((s) => favorites.includes(s.id)), [shops, favorites]);
const filtered = useMemo(() => {
const normalized = query.trim().toLowerCase();
const matchesQuery = (name: string, address: string) =>
@@ -24,7 +22,6 @@ export default function Explore() {
if (filter === 'favoritas') return favorites.includes(shop.id);
if (filter === 'top') return (shop.rating || 0) >= 4;
if (filter === 'produtos') return (shop.products || []).length > 0;
if (filter === 'barbeiros') return (shop.barbers || []).length >= 2;
return true;
};
@@ -44,100 +41,88 @@ export default function Explore() {
});
}, [shops, query, filter, sortBy, favorites]);
const chips: { id: typeof filter; label: string; icon?: React.ReactNode }[] = [
const chips: { id: typeof filter; label: string }[] = [
{ 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' },
{ id: 'favoritas', label: `❤️ Favoritas${favorites.length > 0 ? ` (${favorites.length})` : ''}` },
{ id: 'top', label: 'Top avaliadas' },
{ id: 'produtos', label: '🛍️ Com produtos' },
];
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>
<div className="space-y-5">
{/* Page header */}
<div className="flex items-center justify-between">
<div>
<div className="flex items-center gap-2 mb-0.5">
<Scissors size={16} className="text-amber-500" />
<span className="text-xs font-semibold uppercase tracking-widest text-slate-400">Explorar</span>
</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>
<h1 className="text-2xl font-black text-slate-900">Barbearias</h1>
</div>
<span className="text-sm text-slate-500 font-medium">
{filtered.length} {filtered.length === 1 ? 'resultado' : 'resultados'}
</span>
</div>
{/* 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
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"
/>
</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>
</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>
{/* Search bar */}
<div className="relative">
<Search size={16} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-400" />
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Pesquisar por nome ou endereço..."
className="w-full pl-10 pr-10 py-3 rounded-2xl border border-slate-200 bg-white text-sm focus:outline-none focus:ring-2 focus:ring-amber-400 focus:border-transparent placeholder:text-slate-400 shadow-sm"
/>
{query && (
<button onClick={() => setQuery('')} className="absolute right-3.5 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600">
<X size={15} />
</button>
)}
</div>
{/* 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>
{/* Filters row */}
<div className="flex items-center gap-2 flex-wrap">
{chips.map((chip) => (
<button
key={chip.id}
onClick={() => setFilter(chip.id)}
className={`px-4 py-2 rounded-full text-xs font-semibold transition-all ${filter === chip.id
? 'bg-amber-500 text-white shadow-sm'
: 'bg-white text-slate-600 border border-slate-200 hover:border-amber-300 hover:text-amber-700'
}`}
>
{chip.label}
</button>
))}
<div className="ml-auto flex items-center gap-2">
<SlidersHorizontal size={14} 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 text-xs font-medium text-slate-700 bg-white focus:outline-none focus:ring-2 focus:ring-amber-400"
>
<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>
</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" />
}
{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.'}
? 'Clique no ❤️ de qualquer barbearia para guardar aqui.'
: 'Tente ajustar os filtros ou a pesquisa.'}
</p>
</div>
) : (