alterações design
This commit is contained in:
@@ -3,10 +3,27 @@ import { currency } from '../lib/format';
|
||||
import { Card } from './ui/card';
|
||||
import { Button } from './ui/button';
|
||||
import { useApp } from '../context/AppContext';
|
||||
import { ShoppingBag, Trash2, ShoppingCart, ArrowRight, Package, Scissors } from 'lucide-react';
|
||||
|
||||
export const CartPanel = () => {
|
||||
const { cart, shops, removeFromCart, placeOrder, user } = useApp();
|
||||
if (!cart.length) return <Card className="p-4">Carrinho vazio</Card>;
|
||||
|
||||
if (!cart.length) {
|
||||
return (
|
||||
<div className="py-20 text-center space-y-6 animate-in fade-in zoom-in duration-500">
|
||||
<div className="w-24 h-24 bg-slate-50 rounded-full flex items-center justify-center mx-auto text-slate-200 shadow-inner">
|
||||
<ShoppingCart size={48} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-2xl font-black text-slate-900 uppercase italic tracking-tighter">O seu carrinho está vazio</h3>
|
||||
<p className="text-slate-500 font-medium italic">Explore os melhores produtos e serviços de luxo.</p>
|
||||
</div>
|
||||
<Button asChild className="h-14 px-8 bg-slate-900 hover:bg-slate-800 text-amber-500 font-black rounded-2xl uppercase tracking-widest text-xs italic">
|
||||
<Link to="/explorar">Começar a Explorar</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const grouped = cart.reduce<Record<string, typeof cart>>((acc, item) => {
|
||||
acc[item.shopId] = acc[item.shopId] || [];
|
||||
@@ -29,7 +46,7 @@ export const CartPanel = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-10 pb-10">
|
||||
{Object.entries(grouped).map(([shopId, items]) => {
|
||||
const shop = shops.find((s) => s.id === shopId);
|
||||
const total = items.reduce((sum, i) => {
|
||||
@@ -39,42 +56,69 @@ export const CartPanel = () => {
|
||||
: shop?.products.find((p) => p.id === i.refId)?.price ?? 0;
|
||||
return sum + price * i.qty;
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<Card key={shopId} className="p-4 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-slate-900">{shop?.name ?? 'Barbearia'}</p>
|
||||
<p className="text-xs text-slate-500">{shop?.address}</p>
|
||||
</div>
|
||||
<div className="text-sm font-semibold text-amber-700">{currency(total)}</div>
|
||||
<div key={shopId} className="space-y-4 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<div className="flex items-center gap-2 px-2">
|
||||
<ShoppingBag size={14} className="text-amber-600" />
|
||||
<h3 className="text-[10px] font-black uppercase tracking-[0.3em] text-slate-400">Origem: <span className="text-slate-900">{shop?.name ?? 'Barbearia'}</span></h3>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{items.map((i) => {
|
||||
const ref =
|
||||
i.type === 'service'
|
||||
? shop?.services.find((s) => s.id === i.refId)
|
||||
: shop?.products.find((p) => p.id === i.refId);
|
||||
return (
|
||||
<div key={i.refId} className="flex items-center justify-between text-sm">
|
||||
<span>
|
||||
{i.type === 'service' ? 'Serviço: ' : 'Produto: '}
|
||||
{ref?.name ?? 'Item'} x{i.qty}
|
||||
</span>
|
||||
<button className="text-amber-700 text-xs" onClick={() => removeFromCart(i.refId)}>
|
||||
Remover
|
||||
</button>
|
||||
|
||||
<Card className="p-2 border-none glass-card rounded-[2.5rem] shadow-xl shadow-slate-200/50 overflow-hidden">
|
||||
<div className="p-6 md:p-8 space-y-6">
|
||||
<div className="space-y-4">
|
||||
{items.map((i) => {
|
||||
const ref =
|
||||
i.type === 'service'
|
||||
? shop?.services.find((s) => s.id === i.refId)
|
||||
: shop?.products.find((p) => p.id === i.refId);
|
||||
|
||||
return (
|
||||
<div key={i.refId} className="flex items-center justify-between group py-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-slate-50 rounded-xl flex items-center justify-center text-slate-400 border border-slate-100 italic font-black text-xs">
|
||||
{i.type === 'service' ? <Scissors size={20} /> : <Package size={20} />}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-black text-slate-900 uppercase italic tracking-tight">{ref?.name ?? 'Item'}</p>
|
||||
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest">
|
||||
{i.qty} unidade{i.qty > 1 ? 's' : ''} • {currency(ref?.price || 0)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="w-10 h-10 rounded-full flex items-center justify-center text-slate-300 hover:text-rose-500 hover:bg-rose-50 transition-all active:scale-90"
|
||||
onClick={() => removeFromCart(i.refId)}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{user ? (
|
||||
<Button onClick={() => handleCheckout(shopId)}>Finalizar pedido</Button>
|
||||
) : (
|
||||
<Button asChild>
|
||||
<Link to="/login">Entrar para finalizar</Link>
|
||||
</Button>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<div className="pt-6 border-t border-slate-100 flex items-end justify-between">
|
||||
<div>
|
||||
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">Total do Pedido</p>
|
||||
<p className="text-3xl font-black text-slate-900 tracking-tighter italic">{currency(total)}</p>
|
||||
</div>
|
||||
{user ? (
|
||||
<Button
|
||||
onClick={() => handleCheckout(shopId)}
|
||||
className="h-14 px-8 bg-slate-900 hover:bg-slate-800 text-amber-500 font-black rounded-2xl uppercase tracking-widest text-xs italic shadow-lg active:scale-95 transition-all"
|
||||
>
|
||||
Finalizar Compra
|
||||
<ArrowRight size={14} className="ml-2" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button asChild className="h-14 px-8 bg-slate-100 text-slate-900 hover:bg-slate-200 font-black rounded-2xl uppercase tracking-widest text-xs italic">
|
||||
<Link to="/login">Entrar para Comprar</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -12,36 +12,47 @@ export const ProductList = ({
|
||||
products: Product[];
|
||||
onAdd?: (id: string) => void;
|
||||
}) => (
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
||||
{products.map((p) => {
|
||||
const lowStock = p.stock <= 3;
|
||||
return (
|
||||
<Card key={p.id} hover className={`p-5 flex flex-col gap-3 ${lowStock ? 'border-amber-300 bg-amber-50/30' : ''}`}>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="font-bold text-slate-900 text-lg">{p.name}</h3>
|
||||
{lowStock && (
|
||||
<Badge color="amber" variant="solid" className="text-[10px] px-1.5 py-0">
|
||||
<AlertCircle size={10} className="mr-1" />
|
||||
Stock baixo
|
||||
</Badge>
|
||||
)}
|
||||
<Card key={p.id} className="relative overflow-hidden border-none glass-card rounded-[2rem] premium-shadow group hover:shadow-2xl transition-all duration-300 flex flex-col">
|
||||
<div className="aspect-square bg-slate-50 flex items-center justify-center p-8 group-hover:bg-amber-50 transition-colors">
|
||||
<Package size={48} className="text-slate-200 group-hover:text-amber-200 transition-all group-hover:scale-110 duration-500" />
|
||||
|
||||
{lowStock && (
|
||||
<div className="absolute top-4 left-4">
|
||||
<Badge color="amber" variant="solid" className="text-[9px] px-2 py-0.5 font-black uppercase tracking-widest shadow-lg animate-pulse">
|
||||
Últimas Unidades
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-slate-600">
|
||||
<Package size={14} />
|
||||
<span className={lowStock ? 'font-semibold text-amber-700' : ''}>
|
||||
{p.stock} {p.stock === 1 ? 'unidade' : 'unidades'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-5 flex-1 flex flex-col gap-4">
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-black text-slate-900 text-base tracking-tight leading-tight group-hover:text-amber-600 transition-colors uppercase italic truncate">{p.name}</h3>
|
||||
<div className="text-xs font-bold text-slate-400 uppercase tracking-widest">
|
||||
{p.stock} em stock
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-amber-600">{currency(p.price)}</div>
|
||||
|
||||
<div className="mt-auto pt-4 border-t border-slate-50 flex flex-col gap-3">
|
||||
<div className="text-xl font-black text-slate-900 tracking-tighter">
|
||||
{currency(p.price)}
|
||||
</div>
|
||||
|
||||
{onAdd && (
|
||||
<Button
|
||||
onClick={() => onAdd(p.id)}
|
||||
disabled={p.stock <= 0}
|
||||
className="w-full h-10 bg-slate-900 hover:bg-slate-800 text-amber-500 font-black rounded-xl shadow-lg shadow-slate-200 transition-all active:scale-95 uppercase tracking-widest text-[10px]"
|
||||
>
|
||||
{p.stock > 0 ? 'Adicionar' : 'Esgotado'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{onAdd && (
|
||||
<Button onClick={() => onAdd(p.id)} disabled={p.stock <= 0} size="sm" className="w-full" variant={lowStock ? 'solid' : 'solid'}>
|
||||
{p.stock > 0 ? 'Adicionar ao carrinho' : 'Sem stock'}
|
||||
</Button>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -11,23 +11,36 @@ export const ServiceList = ({
|
||||
services: Service[];
|
||||
onSelect?: (id: string) => void;
|
||||
}) => (
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{services.map((s) => (
|
||||
<Card key={s.id} hover className="p-5 flex flex-col gap-3 group">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<h3 className="font-bold text-slate-900 text-lg mb-1 group-hover:text-amber-700 transition-colors">{s.name}</h3>
|
||||
<div className="flex items-center gap-2 text-sm text-slate-600">
|
||||
<Clock size={14} />
|
||||
<span>{s.duration} minutos</span>
|
||||
<Card key={s.id} className="p-6 border-none glass-card rounded-[2rem] premium-shadow group hover:shadow-2xl transition-all duration-300">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 space-y-2">
|
||||
<h3 className="font-black text-slate-900 text-xl tracking-tight leading-tight group-hover:text-amber-600 transition-colors uppercase italic">{s.name}</h3>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1.5 px-2 py-1 bg-slate-100 rounded-lg text-xs font-bold text-slate-500 uppercase">
|
||||
<Clock size={12} />
|
||||
<span>{s.duration} min</span>
|
||||
</div>
|
||||
<div className="w-1 h-1 bg-slate-300 rounded-full" />
|
||||
<span className="text-xs font-black text-slate-400 uppercase tracking-widest italic">Corte Profissional</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-amber-600">{currency(s.price)}</div>
|
||||
<div className="text-2xl font-black text-slate-900 tracking-tighter bg-amber-50 px-3 py-1 rounded-xl border border-amber-100">
|
||||
{currency(s.price)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{onSelect && (
|
||||
<Button onClick={() => onSelect(s.id)} size="sm" className="w-full">
|
||||
Agendar
|
||||
</Button>
|
||||
<div className="mt-6 pt-6 border-t border-slate-100 flex items-center justify-between gap-4">
|
||||
<p className="text-xs font-medium text-slate-400 max-w-[150px]">Lugar disponível hoje</p>
|
||||
<Button
|
||||
onClick={() => onSelect(s.id)}
|
||||
className="flex-1 h-11 bg-slate-900 hover:bg-slate-800 text-amber-500 font-black rounded-xl shadow-lg shadow-slate-200 transition-all active:scale-95 uppercase tracking-widest text-xs"
|
||||
>
|
||||
Reservar Agora
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
|
||||
@@ -1,58 +1,66 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Star, MapPin, Scissors } from 'lucide-react';
|
||||
import { Star, MapPin, Scissors, User } from 'lucide-react';
|
||||
import { BarberShop } from '../types';
|
||||
import { Card } from './ui/card';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
export const ShopCard = ({ shop }: { shop: BarberShop }) => {
|
||||
return (
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<Card className="overflow-hidden border-none glass-card rounded-[2rem] premium-shadow group hover:shadow-2xl hover:shadow-slate-200/60 transition-all duration-500">
|
||||
<div className="relative h-44 overflow-hidden">
|
||||
{shop.imageUrl ? (
|
||||
<img
|
||||
src={shop.imageUrl}
|
||||
alt={shop.name}
|
||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full bg-slate-900 flex items-center justify-center text-amber-500">
|
||||
<Scissors size={40} />
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-slate-900/80 via-transparent to-transparent opacity-60 group-hover:opacity-40 transition-opacity" />
|
||||
|
||||
{/* Rating Badge */}
|
||||
<div className="absolute top-4 right-4 bg-slate-900/90 backdrop-blur-md border border-white/10 px-3 py-1 rounded-full flex items-center gap-1.5 shadow-xl">
|
||||
<Star size={14} className="fill-amber-500 text-amber-500" />
|
||||
<span className="text-white text-xs font-black tracking-wider">
|
||||
{shop.rating ? shop.rating.toFixed(1) : '0.0'}
|
||||
</span>
|
||||
</div>
|
||||
</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>
|
||||
<div className="p-6 space-y-4">
|
||||
<div>
|
||||
<h2 className="text-slate-900 text-xl font-black tracking-tight group-hover:text-amber-600 transition-colors truncate">
|
||||
{shop.name}
|
||||
</h2>
|
||||
<div className="flex items-center gap-1.5 text-slate-500 mt-1">
|
||||
<MapPin size={14} className="text-amber-600" />
|
||||
<p className="text-sm font-medium line-clamp-1">
|
||||
{shop.address || 'Endereço Indisponível'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex -space-x-2">
|
||||
{[1, 2, 3].map(i => (
|
||||
<div key={i} className="w-7 h-7 rounded-full border-2 border-white bg-slate-100 flex items-center justify-center overflow-hidden">
|
||||
<User size={12} className="text-slate-400" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-xs font-bold text-slate-400 uppercase tracking-widest">
|
||||
+{(shop.barbers || []).length} Barbeiros
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Button asChild className="rounded-xl bg-slate-900 hover:bg-slate-800 text-amber-500 font-bold px-5 h-10 shadow-lg shadow-slate-200 transition-all active:scale-95">
|
||||
<Link to={`/barbearia/${shop.id}`}>Reservar</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -15,35 +15,38 @@ export const Header = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<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">
|
||||
<header className="sticky top-0 z-30 bg-white/70 backdrop-blur-lg border-b border-slate-200/50 shadow-sm shadow-slate-200/20">
|
||||
<div className="mx-auto flex h-20 max-w-6xl items-center justify-between px-6">
|
||||
<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"
|
||||
className="text-2xl font-black tracking-tighter text-slate-900 group flex items-center gap-2"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
Smart Agenda
|
||||
<div className="w-8 h-8 rounded-lg bg-slate-900 flex items-center justify-center text-amber-500 shadow-lg shadow-slate-200">
|
||||
<User size={18} fill="currentColor" />
|
||||
</div>
|
||||
<span className="group-hover:text-amber-600 transition-colors">Smart Agenda</span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<nav className="hidden md:flex items-center gap-4">
|
||||
<nav className="hidden md:flex items-center gap-8">
|
||||
{user?.role !== 'barbearia' && (
|
||||
<>
|
||||
<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"
|
||||
className="text-sm font-bold text-slate-600 hover:text-slate-900 transition-all flex items-center gap-2"
|
||||
>
|
||||
<MapPin size={16} />
|
||||
<span>Barbearias</span>
|
||||
<MapPin size={16} className="text-amber-600" />
|
||||
<span>Explorar</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to="/carrinho"
|
||||
className="relative text-slate-700 hover:text-indigo-600 transition-colors p-2 rounded-lg hover:bg-indigo-50"
|
||||
className="relative text-slate-600 hover:text-slate-900 transition-all p-2 rounded-xl hover:bg-slate-50"
|
||||
>
|
||||
<ShoppingCart size={18} />
|
||||
<ShoppingCart size={20} />
|
||||
{cart.length > 0 && (
|
||||
<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">
|
||||
<span className="absolute -right-1 -top-1 rounded-full bg-slate-900 px-1.5 py-0.5 text-[10px] font-black text-amber-500 shadow-md min-w-[18px] text-center">
|
||||
{cart.length}
|
||||
</span>
|
||||
)}
|
||||
@@ -51,40 +54,52 @@ export const Header = () => {
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="h-6 w-px bg-slate-200 mx-1" />
|
||||
|
||||
{user ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => navigate(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"
|
||||
className="flex items-center gap-3 bg-slate-50 hover:bg-slate-100 border border-slate-200/60 pl-3 pr-4 py-1.5 rounded-full transition-all group"
|
||||
type="button"
|
||||
>
|
||||
<User size={16} />
|
||||
<span className="max-w-[120px] truncate">{user.name}</span>
|
||||
<div className="w-7 h-7 rounded-full bg-slate-900 flex items-center justify-center text-amber-500 shadow-sm">
|
||||
<User size={14} fill="currentColor" />
|
||||
</div>
|
||||
<span className="text-sm font-bold text-slate-700 group-hover:text-slate-900 max-w-[120px] truncate">{user.name}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="p-2 text-slate-600 hover:text-rose-600 hover:bg-rose-50 rounded-lg transition-colors"
|
||||
className="p-2 text-slate-400 hover:text-rose-600 hover:bg-rose-50 rounded-xl transition-all"
|
||||
title="Sair"
|
||||
type="button"
|
||||
>
|
||||
<LogOut size={16} />
|
||||
<LogOut size={18} />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
to="/login"
|
||||
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>
|
||||
<div className="flex items-center gap-3">
|
||||
<Link
|
||||
to="/login"
|
||||
className="text-sm font-bold text-slate-600 hover:text-slate-900 px-4 py-2 transition-colors"
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
<Link
|
||||
to="/registro"
|
||||
className="inline-flex items-center justify-center rounded-xl bg-slate-900 px-5 py-2 text-sm font-bold text-amber-500 shadow-lg shadow-slate-200 hover:bg-slate-800 transition-all"
|
||||
>
|
||||
Criar Conta
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
{/* 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"
|
||||
className="md:hidden p-2.5 bg-slate-50 border border-slate-200 rounded-xl text-slate-900 transition-all"
|
||||
type="button"
|
||||
>
|
||||
{mobileMenuOpen ? <X size={20} /> : <Menu size={20} />}
|
||||
@@ -93,28 +108,28 @@ export const Header = () => {
|
||||
|
||||
{/* Mobile Menu */}
|
||||
{mobileMenuOpen && (
|
||||
<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">
|
||||
<div className="md:hidden border-t border-slate-100 bg-white shadow-2xl animate-in slide-in-from-top-4 duration-300">
|
||||
<nav className="px-6 py-6 space-y-4">
|
||||
{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-600 transition-colors px-3 py-2 rounded-lg hover:bg-amber-50"
|
||||
className="flex items-center gap-3 text-base font-bold text-slate-700 hover:text-amber-600 p-3 rounded-2xl bg-slate-50 transition-all"
|
||||
>
|
||||
<MapPin size={16} />
|
||||
<MapPin size={18} className="text-amber-600" />
|
||||
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"
|
||||
className="flex items-center gap-3 text-base font-bold text-slate-700 hover:text-amber-600 p-3 rounded-2xl bg-slate-50 transition-all"
|
||||
>
|
||||
<ShoppingCart size={16} />
|
||||
Carrinho
|
||||
<ShoppingCart size={18} className="text-amber-600" />
|
||||
Meu Carrinho
|
||||
{cart.length > 0 && (
|
||||
<span className="ml-auto rounded-full bg-amber-500 px-2 py-0.5 text-[10px] font-bold text-white">
|
||||
<span className="ml-auto rounded-full bg-slate-900 px-2 py-0.5 text-[10px] font-black text-amber-500">
|
||||
{cart.length}
|
||||
</span>
|
||||
)}
|
||||
@@ -122,6 +137,8 @@ export const Header = () => {
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="h-px bg-slate-100 w-full" />
|
||||
|
||||
{user ? (
|
||||
<>
|
||||
<button
|
||||
@@ -129,30 +146,39 @@ 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-600 transition-colors px-3 py-2 rounded-lg hover:bg-amber-50 text-left"
|
||||
className="w-full flex items-center gap-3 text-base font-bold text-slate-700 p-3 rounded-2xl hover:bg-slate-50 transition-all text-left"
|
||||
type="button"
|
||||
>
|
||||
<User size={16} />
|
||||
<User size={18} className="text-slate-400" />
|
||||
{user.name}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
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"
|
||||
className="w-full flex items-center gap-3 text-base font-bold text-rose-600 p-3 rounded-2xl hover:bg-rose-50 transition-all text-left"
|
||||
type="button"
|
||||
>
|
||||
<LogOut size={16} />
|
||||
Sair
|
||||
<LogOut size={18} />
|
||||
Sair da Conta
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<Link
|
||||
to="/login"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
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>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Link
|
||||
to="/login"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="flex items-center justify-center rounded-2xl border border-slate-200 bg-white py-3 text-sm font-bold text-slate-700"
|
||||
>
|
||||
Entrar
|
||||
</Link>
|
||||
<Link
|
||||
to="/registro"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="flex items-center justify-center rounded-2xl bg-slate-900 py-3 text-sm font-bold text-amber-500 shadow-lg shadow-slate-200"
|
||||
>
|
||||
Criar Conta
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { cn } from '../../lib/cn';
|
||||
|
||||
export const Chip = ({ children, active, onClick }: { children: React.ReactNode; active?: boolean; onClick?: () => void }) => (
|
||||
export const Chip = ({ children, active, onClick, className }: { children: React.ReactNode; active?: boolean; onClick?: () => void; className?: string }) => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
'px-3 py-1.5 rounded-full border text-sm transition',
|
||||
active ? 'border-amber-500 bg-amber-50 text-amber-700' : 'border-slate-200 text-slate-700 hover:bg-slate-100'
|
||||
'px-3 py-1.5 rounded-full border text-sm transition font-medium',
|
||||
active ? 'border-amber-500 bg-amber-50 text-amber-700' : 'border-slate-200 text-slate-700 hover:bg-slate-100',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import { cn } from '../../lib/cn';
|
||||
|
||||
type Tab = { id: string; label: string; badge?: number };
|
||||
|
||||
export const Tabs = ({ tabs, active, onChange }: { tabs: Tab[]; active: string; onChange: (id: string) => void }) => (
|
||||
<div className="flex gap-1 border-b border-slate-200 overflow-x-auto">
|
||||
export const Tabs = ({ tabs, active, onChange, className }: { tabs: Tab[]; active: string; onChange: (id: string) => void; className?: string }) => (
|
||||
<div className={cn("flex gap-2 p-1 bg-slate-100 rounded-2xl w-fit", className)}>
|
||||
{tabs.map((t) => (
|
||||
<button
|
||||
key={t.id}
|
||||
onClick={() => onChange(t.id)}
|
||||
className={`flex items-center gap-2 px-4 py-3 text-sm font-semibold transition-all whitespace-nowrap ${active === t.id
|
||||
? 'text-amber-600 border-b-2 border-amber-500 bg-amber-50/50'
|
||||
: 'text-slate-600 hover:text-amber-600 hover:bg-amber-50/30'
|
||||
}`}
|
||||
className={cn(
|
||||
"px-6 py-2.5 text-sm font-black uppercase tracking-widest transition-all rounded-xl whitespace-nowrap",
|
||||
active === t.id
|
||||
? "bg-slate-900 text-amber-500 shadow-xl"
|
||||
: "text-slate-500 hover:text-slate-900 hover:bg-white/50"
|
||||
)}
|
||||
>
|
||||
{t.label}
|
||||
{t.badge && (
|
||||
<span className="flex items-center justify-center w-5 h-5 text-xs font-bold text-white bg-red-500 rounded-full">
|
||||
<span className="ml-2 inline-flex items-center justify-center w-5 h-5 text-[10px] font-black text-white bg-slate-900 rounded-full border border-amber-500/50">
|
||||
{t.badge}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--brand-gold: #d97706;
|
||||
--brand-gold-light: #fbbf24;
|
||||
--obsidian: #0f172a;
|
||||
--obsidian-light: #1e293b;
|
||||
--slate-950: #020617;
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
@@ -12,12 +17,16 @@
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-gradient-to-br from-slate-50 via-white to-blue-50/30 text-slate-900 font-sans antialiased;
|
||||
@apply bg-[#f8fafc] text-slate-900 font-sans antialiased;
|
||||
background-image:
|
||||
radial-gradient(at 0% 0%, rgba(217, 119, 6, 0.03) 0px, transparent 50%),
|
||||
radial-gradient(at 100% 0%, rgba(15, 23, 42, 0.03) 0px, transparent 50%);
|
||||
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
a {
|
||||
@apply text-inherit no-underline;
|
||||
@apply text-inherit no-underline transition-colors;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
@@ -38,6 +47,22 @@
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
@apply bg-white/80 backdrop-blur-md border border-white/20 shadow-xl shadow-slate-200/50;
|
||||
}
|
||||
|
||||
.premium-shadow {
|
||||
box-shadow: 0 10px 40px -10px rgba(15, 23, 42, 0.1);
|
||||
}
|
||||
|
||||
.gold-gradient {
|
||||
@apply bg-gradient-to-br from-amber-500 via-amber-600 to-amber-700;
|
||||
}
|
||||
|
||||
.obsidian-gradient {
|
||||
@apply bg-gradient-to-br from-slate-800 via-slate-900 to-slate-950;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -66,51 +66,62 @@ export default function AuthLogin() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-md mx-auto py-8">
|
||||
<Card className="p-8 space-y-6">
|
||||
<div className="text-center space-y-2">
|
||||
<div className="inline-flex p-3 bg-gradient-to-br from-amber-500 to-amber-600 rounded-xl text-white shadow-lg mb-2">
|
||||
<LogIn size={24} />
|
||||
<div className="min-h-[80vh] flex items-center justify-center px-6 py-12">
|
||||
<Card className="w-full max-w-[440px] p-10 space-y-8 glass-card border-none rounded-[2rem] premium-shadow animate-in fade-in zoom-in duration-500">
|
||||
<div className="text-center space-y-4">
|
||||
<Link to="/" className="inline-flex p-4 bg-slate-900 rounded-2xl text-amber-500 shadow-xl mb-2 hover:scale-105 transition-transform">
|
||||
<LogIn size={32} />
|
||||
</Link>
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-3xl font-black text-slate-900 tracking-tight">Bem-vindo</h1>
|
||||
<p className="text-slate-500 font-medium">Aceda à sua conta Smart Agenda</p>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-slate-900">Entrar</h1>
|
||||
<p className="text-sm text-slate-600">Aceda à sua conta</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-lg border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
|
||||
<div className="rounded-2xl border border-rose-100 bg-rose-50 px-4 py-3 text-sm text-rose-600 font-medium animate-shake">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form className="space-y-4" onSubmit={handleLogin}>
|
||||
<Input
|
||||
label="Email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="seu@email.com"
|
||||
required
|
||||
/>
|
||||
<form className="space-y-5" onSubmit={handleLogin}>
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
label="Email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="exemplo@email.com"
|
||||
required
|
||||
className="rounded-xl border-slate-200/60 focus:ring-amber-500/20"
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Senha"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
label="Palavra-passe"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
required
|
||||
className="rounded-xl border-slate-200/60 focus:ring-amber-500/20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full" size="lg" disabled={loading}>
|
||||
{loading ? 'A entrar...' : 'Entrar'}
|
||||
<Button type="submit" className="w-full h-12 bg-slate-900 hover:bg-slate-800 text-amber-500 font-bold rounded-xl shadow-lg shadow-slate-200 transition-all active:scale-[0.98]" disabled={loading}>
|
||||
{loading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 border-2 border-amber-500 border-t-transparent rounded-full animate-spin" />
|
||||
<span>A entrar...</span>
|
||||
</div>
|
||||
) : 'Entrar na Conta'}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="text-center pt-4 border-t border-slate-200">
|
||||
<p className="text-sm text-slate-600">
|
||||
Não tem conta?{' '}
|
||||
<Link to="/registo" className="text-amber-700 font-semibold hover:text-amber-800 transition-colors">
|
||||
Criar conta
|
||||
<div className="text-center pt-6 border-t border-slate-100">
|
||||
<p className="text-sm text-slate-500 font-medium">
|
||||
Ainda não tem conta?{' '}
|
||||
<Link to="/registo" className="text-amber-600 font-bold hover:text-amber-700 underline-offset-4 hover:underline transition-all">
|
||||
Criar conta grátis
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -108,31 +108,29 @@ export default function AuthRegister() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-md mx-auto py-8">
|
||||
<Card className="p-8 space-y-6">
|
||||
<div className="text-center space-y-2">
|
||||
<div className="inline-flex p-3 bg-gradient-to-br from-amber-500 to-amber-600 rounded-xl text-white shadow-lg mb-2">
|
||||
<UserPlus size={24} />
|
||||
<div className="min-h-[80vh] flex items-center justify-center px-6 py-12">
|
||||
<Card className="w-full max-w-[500px] p-10 space-y-8 glass-card border-none rounded-[2rem] premium-shadow animate-in fade-in zoom-in duration-500">
|
||||
<div className="text-center space-y-4">
|
||||
<div className="inline-flex p-4 bg-slate-900 rounded-2xl text-amber-500 shadow-xl mb-2">
|
||||
<UserPlus size={32} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-3xl font-black text-slate-900 tracking-tight">Criar Conta</h1>
|
||||
<p className="text-slate-500 font-medium">Junte-se à Smart Agenda</p>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-slate-900">
|
||||
Criar conta
|
||||
</h1>
|
||||
<p className="text-sm text-slate-600">
|
||||
Escolha o tipo de acesso
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-lg border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
|
||||
<div className="rounded-2xl border border-rose-100 bg-rose-50 px-4 py-3 text-sm text-rose-600 font-medium">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form className="space-y-5" onSubmit={onSubmit}>
|
||||
<form className="space-y-6" onSubmit={onSubmit}>
|
||||
{/* Tipo de conta */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-700">
|
||||
Tipo de conta
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-bold text-slate-900 uppercase tracking-wider ml-1">
|
||||
Eu sou...
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{(['cliente', 'barbearia'] as const).map((r) => (
|
||||
@@ -143,18 +141,18 @@ export default function AuthRegister() {
|
||||
setRole(r)
|
||||
setError('')
|
||||
}}
|
||||
className={`p-4 rounded-xl border-2 transition-all ${role === r
|
||||
? 'border-amber-500 bg-amber-50 shadow-md'
|
||||
: 'border-slate-200 hover:border-amber-300'
|
||||
className={`p-4 rounded-2xl border-2 transition-all group ${role === r
|
||||
? 'border-slate-900 bg-slate-900 text-amber-500 shadow-lg'
|
||||
: 'border-slate-100 bg-slate-50/50 hover:border-slate-200 text-slate-500'
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
{r === 'cliente' ? (
|
||||
<User size={20} className={role === r ? 'text-amber-600' : 'text-slate-400'} />
|
||||
<User size={20} className={role === r ? 'text-amber-500' : 'text-slate-400 group-hover:text-slate-600'} />
|
||||
) : (
|
||||
<Scissors size={20} className={role === r ? 'text-amber-600' : 'text-slate-400'} />
|
||||
<Scissors size={20} className={role === r ? 'text-amber-500' : 'text-slate-400 group-hover:text-slate-600'} />
|
||||
)}
|
||||
<span className="text-sm font-semibold">
|
||||
<span className="text-sm font-bold uppercase tracking-tight">
|
||||
{r === 'cliente' ? 'Cliente' : 'Barbearia'}
|
||||
</span>
|
||||
</div>
|
||||
@@ -163,55 +161,63 @@ export default function AuthRegister() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
label="Nome completo"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="João Silva"
|
||||
required
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="seu@email.com"
|
||||
required
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Senha"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
required
|
||||
/>
|
||||
|
||||
{role === 'barbearia' && (
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
label="Nome da barbearia"
|
||||
value={shopName}
|
||||
onChange={(e) => setShopName(e.target.value)}
|
||||
placeholder="Barbearia XPTO"
|
||||
label="Nome completo"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Ex: João Silva"
|
||||
required
|
||||
className="rounded-xl border-slate-200 focus:ring-amber-500/20"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button type="submit" className="w-full" size="lg" disabled={loading}>
|
||||
{loading ? 'A criar conta…' : 'Criar conta'}
|
||||
<Input
|
||||
label="Email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="exemplo@email.com"
|
||||
required
|
||||
className="rounded-xl border-slate-200 focus:ring-amber-500/20"
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Palavra-passe"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
required
|
||||
className="rounded-xl border-slate-200 focus:ring-amber-500/20"
|
||||
/>
|
||||
|
||||
{role === 'barbearia' && (
|
||||
<Input
|
||||
label="Nome da barbearia"
|
||||
value={shopName}
|
||||
onChange={(e) => setShopName(e.target.value)}
|
||||
placeholder="Ex: Barbearia Estilo"
|
||||
required
|
||||
className="rounded-xl border-slate-200 focus:ring-amber-500/20 animate-in slide-in-from-top-2"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full h-12 bg-slate-900 hover:bg-slate-800 text-amber-500 font-bold rounded-xl shadow-lg shadow-slate-200 transition-all active:scale-[0.98]" disabled={loading}>
|
||||
{loading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 border-2 border-amber-500 border-t-transparent rounded-full animate-spin" />
|
||||
<span>A processar...</span>
|
||||
</div>
|
||||
) : 'Criar minha conta'}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="text-center pt-4 border-t border-slate-200">
|
||||
<p className="text-sm text-slate-600">
|
||||
Já tem conta?{' '}
|
||||
<Link
|
||||
to="/login"
|
||||
className="text-amber-700 font-semibold hover:text-amber-800"
|
||||
>
|
||||
Entrar
|
||||
<div className="text-center pt-6 border-t border-slate-100">
|
||||
<p className="text-sm text-slate-500 font-medium">
|
||||
Já tem uma conta?{' '}
|
||||
<Link to="/login" className="text-amber-600 font-bold hover:text-amber-700 underline-offset-4 hover:underline transition-all">
|
||||
Fazer Login
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
/**
|
||||
* @file Booking.tsx
|
||||
* @description Página de Agendamento da versão Web.
|
||||
* Gere um formulário multi-passo unificado para selecionar o Serviço,
|
||||
* Barbeiro, Data e Horário. Cruza disponibilidades em tempo real.
|
||||
*/
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { useNavigate, useParams, useSearchParams, Link } from 'react-router-dom';
|
||||
import { useMemo, useState, useEffect } from 'react';
|
||||
import { Card } from '../components/ui/card';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Input } from '../components/ui/input';
|
||||
import { useApp } from '../context/AppContext';
|
||||
import { Calendar, Clock, Scissors, User, CheckCircle2 } from 'lucide-react';
|
||||
import { Calendar, Clock, Scissors, User, CheckCircle2, MapPin, ArrowLeft, ArrowRight } from 'lucide-react';
|
||||
import { currency } from '../lib/format';
|
||||
|
||||
export default function Booking() {
|
||||
@@ -47,35 +41,21 @@ export default function Booking() {
|
||||
const nextStep = () => setStep((s) => Math.min(s + 1, 4));
|
||||
const prevStep = () => setStep((s) => Math.max(s - 1, 1));
|
||||
|
||||
/**
|
||||
* Função para gerar horários padrão se não houver horários específicos predefinidos
|
||||
* pelo barbeiro na Base de Dados.
|
||||
* @returns {string[]} Lista de horários de 1 em 1 hora.
|
||||
*/
|
||||
const generateDefaultSlots = (): string[] => {
|
||||
const slots: string[] = [];
|
||||
// Horário de trabalho padrão estipulado: 09:00 às 18:00
|
||||
for (let hour = 9; hour <= 18; hour++) {
|
||||
slots.push(`${hour.toString().padStart(2, '0')}:00`);
|
||||
}
|
||||
return slots;
|
||||
};
|
||||
|
||||
/**
|
||||
* Deriva reativamente a lista exata de horários disponíveis.
|
||||
* Elimina os slots que já estejam formalmente ocupados ('appointments' não cancelados) na BD.
|
||||
*/
|
||||
// Buscar horários disponíveis para a data selecionada interagindo com dados transacionais
|
||||
const availableSlots = useMemo(() => {
|
||||
if (!selectedBarber || !date) return [];
|
||||
|
||||
// Primeiro, tenta encontrar horários específicos configurados para a data
|
||||
const specificSchedule = selectedBarber.schedule?.find((s) => s.day === date);
|
||||
let slots = specificSchedule && specificSchedule.slots.length > 0
|
||||
? [...specificSchedule.slots]
|
||||
: generateDefaultSlots();
|
||||
|
||||
// Filtra agendamentos atuais já alocados para impedir duplo-agendamento ('Double Booking')
|
||||
const bookedSlots = appointments
|
||||
.filter((apt) =>
|
||||
apt.barberId === barberId &&
|
||||
@@ -83,34 +63,25 @@ export default function Booking() {
|
||||
apt.date.startsWith(date)
|
||||
)
|
||||
.map((apt) => {
|
||||
// Separação para identificar a secção das "horas" na data ISO/String ("YYYY-MM-DD HH:MM")
|
||||
const parts = apt.date.split(' ');
|
||||
return parts.length > 1 ? parts[1] : '';
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
// Devolve diferença de conjuntos
|
||||
return slots.filter((slot) => !bookedSlots.includes(slot));
|
||||
}, [selectedBarber, date, barberId, appointments]);
|
||||
|
||||
if (!shop) return <div className="text-center py-12 text-slate-600">Barbearia não encontrada</div>;
|
||||
if (!shop) return <div className="text-center py-24 text-slate-500 font-black uppercase tracking-widest italic">Barbearia não encontrada</div>;
|
||||
|
||||
const canSubmit = serviceId && barberId && date && slot;
|
||||
|
||||
/**
|
||||
* Dispara a ação de guardar a nova marcação na base de dados Supabase via Context API.
|
||||
*/
|
||||
const submit = async () => {
|
||||
if (!user) {
|
||||
// Bloqueia ações de clientes anónimos exigindo Sessão iniciada
|
||||
navigate('/login');
|
||||
return;
|
||||
}
|
||||
if (!canSubmit) return;
|
||||
|
||||
// O método 'createAppointment' fará internamente um pedido `supabase.from('appointments').insert(...)`
|
||||
const appt = await createAppointment({ shopId: shop.id, serviceId, barberId, customerId: user.id, date: `${date} ${slot}` });
|
||||
|
||||
if (appt) {
|
||||
navigate('/perfil');
|
||||
} else {
|
||||
@@ -126,89 +97,75 @@ export default function Booking() {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900 mb-1">Agendar em {shop.name}</h1>
|
||||
<p className="text-sm text-slate-600">{shop.address}</p>
|
||||
<div className="max-w-4xl mx-auto space-y-10 py-4 pb-20">
|
||||
<header className="space-y-4 text-center">
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-amber-100 text-amber-700 text-[10px] font-black uppercase tracking-widest">
|
||||
<Calendar size={10} fill="currentColor" />
|
||||
<span>Reserva Exclusiva</span>
|
||||
</div>
|
||||
{step > 1 && (
|
||||
<Button variant="outline" size="sm" onClick={prevStep}>
|
||||
Voltar
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-4xl md:text-5xl font-black text-slate-900 tracking-tighter uppercase italic">
|
||||
Agendar em <span className="text-amber-600 block md:inline">{shop.name}</span>
|
||||
</h1>
|
||||
<div className="flex items-center justify-center gap-2 text-slate-500 font-medium">
|
||||
<MapPin size={14} className="text-amber-600" />
|
||||
<p className="text-sm">{shop.address}</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Progress Steps */}
|
||||
<div className="flex items-center justify-between max-w-2xl bg-white p-4 rounded-xl shadow-sm border border-slate-100">
|
||||
{steps.map((s, idx) => (
|
||||
<div key={s.id} className="flex items-center flex-1 last:flex-none">
|
||||
<div className="flex flex-col items-center flex-1">
|
||||
{/* Progress Steps - Premium Stepper */}
|
||||
<div className="relative">
|
||||
<div className="absolute top-1/2 left-0 w-full h-px bg-slate-200 -translate-y-1/2 z-0" />
|
||||
<div className="relative flex items-center justify-between z-10 px-4">
|
||||
{steps.map((s) => (
|
||||
<div key={s.id} className="flex flex-col items-center">
|
||||
<button
|
||||
onClick={() => {
|
||||
if (s.completed || s.id < step) setStep(s.id);
|
||||
}}
|
||||
disabled={!s.completed && s.id > step}
|
||||
className={`w-10 h-10 rounded-full flex items-center justify-center border-2 transition-all ${s.active
|
||||
? 'bg-amber-600 border-amber-600 text-white shadow-lg ring-4 ring-amber-100'
|
||||
: s.completed
|
||||
className={`w-12 h-12 rounded-2xl flex items-center justify-center border-2 transition-all duration-500 scale-100 active:scale-90 ${
|
||||
s.active
|
||||
? 'bg-slate-900 border-slate-900 text-amber-500 shadow-2xl shadow-slate-300 -translate-y-2'
|
||||
: s.completed
|
||||
? 'bg-amber-500 border-amber-500 text-white'
|
||||
: 'bg-white border-slate-200 text-slate-400'
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
{s.completed && !s.active ? <CheckCircle2 size={18} /> : <s.icon size={18} />}
|
||||
{s.completed && !s.active ? <CheckCircle2 size={24} /> : <s.icon size={24} />}
|
||||
</button>
|
||||
<span className={`text-[10px] mt-2 font-bold uppercase tracking-wider ${s.active ? 'text-amber-700' : 'text-slate-500'}`}>
|
||||
<div className={`mt-3 text-[10px] font-black uppercase tracking-widest ${s.active ? 'text-slate-900' : 'text-slate-400'}`}>
|
||||
{s.label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{idx < steps.length - 1 && (
|
||||
<div className={`h-1 flex-1 mx-2 rounded-full ${s.completed ? 'bg-amber-500' : 'bg-slate-100'}`} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="p-6">
|
||||
{/* Step Info Summary (Show what was already selected) */}
|
||||
{step > 1 && selectedService && (
|
||||
<div className="mb-6 p-3 bg-amber-50 border border-amber-100 rounded-lg flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-white rounded-full flex items-center justify-center text-amber-600 shadow-sm">
|
||||
<Scissors size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-amber-600 font-bold uppercase">Serviço Selecionado</p>
|
||||
<p className="text-sm font-bold text-slate-900">{selectedService.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-lg font-bold text-amber-600">{currency(selectedService.price)}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step > 2 && selectedBarber && (
|
||||
<div className="mb-6 p-3 bg-indigo-50 border border-indigo-100 rounded-lg flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-white rounded-full flex items-center justify-center text-indigo-600 shadow-sm">
|
||||
<User size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-indigo-600 font-bold uppercase">Barbeiro</p>
|
||||
<p className="text-sm font-bold text-slate-900">{selectedBarber.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Card className="p-2 border-none glass-card rounded-[3rem] shadow-2xl shadow-slate-200/50 overflow-hidden">
|
||||
{/* Dynamic Step Content */}
|
||||
<div className="space-y-6">
|
||||
<div className="p-8 md:p-12 space-y-10">
|
||||
|
||||
{/* Step Back Button */}
|
||||
{step > 1 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={prevStep}
|
||||
className="px-0 h-auto font-black text-[10px] uppercase tracking-[0.2em] text-slate-400 hover:text-slate-900 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<ArrowLeft size={12} />
|
||||
Voltar ao passo anterior
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{step === 1 && (
|
||||
<div className="space-y-4 animate-in fade-in slide-in-from-bottom-2 duration-300">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-8 h-8 bg-amber-600 text-white rounded-lg flex items-center justify-center text-sm font-bold">1</div>
|
||||
<h3 className="text-lg font-bold text-slate-900">Escolha o serviço</h3>
|
||||
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<div className="space-y-2 text-center md:text-left">
|
||||
<h3 className="text-3xl font-black text-slate-900 tracking-tighter uppercase italic">1. Selecione o <span className="text-amber-600">Serviço</span></h3>
|
||||
<p className="text-slate-500 font-medium italic">O primeiro passo para a sua transformação de elite.</p>
|
||||
</div>
|
||||
<div className="grid md:grid-cols-2 gap-3">
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{shop.services.map((s) => (
|
||||
<button
|
||||
key={s.id}
|
||||
@@ -216,16 +173,26 @@ export default function Booking() {
|
||||
setService(s.id);
|
||||
setStep(2);
|
||||
}}
|
||||
className={`p-4 rounded-xl border-2 text-left transition-all ${serviceId === s.id
|
||||
? 'border-amber-500 bg-amber-50/50 shadow-md ring-2 ring-amber-200'
|
||||
: 'border-slate-100 hover:border-amber-300 hover:bg-amber-50/30'
|
||||
}`}
|
||||
className={`group p-6 rounded-[2rem] border-2 text-left transition-all duration-300 flex flex-col gap-4 ${
|
||||
serviceId === s.id
|
||||
? 'border-slate-900 bg-slate-900 text-white shadow-2xl translate-y-[-4px]'
|
||||
: 'border-slate-50 bg-slate-50 hover:border-amber-200 hover:bg-amber-50/50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="font-bold text-slate-900">{s.name}</div>
|
||||
<div className="text-sm font-bold text-amber-600">{currency(s.price)}</div>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-1">
|
||||
<div className={`font-black text-xl tracking-tight uppercase italic ${serviceId === s.id ? 'text-amber-500' : 'text-slate-900 group-hover:text-amber-600'}`}>
|
||||
{s.name}
|
||||
</div>
|
||||
<div className={`flex items-center gap-2 text-xs font-bold uppercase tracking-widest ${serviceId === s.id ? 'text-slate-400' : 'text-slate-500'}`}>
|
||||
<Clock size={12} />
|
||||
{s.duration} MIN
|
||||
</div>
|
||||
</div>
|
||||
<div className={`text-2xl font-black tracking-tighter px-3 py-1 rounded-xl ${serviceId === s.id ? 'bg-white/10 text-white' : 'bg-white text-slate-900 shadow-sm'}`}>
|
||||
{currency(s.price)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-slate-500">Duração: {s.duration} min</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -233,12 +200,12 @@ export default function Booking() {
|
||||
)}
|
||||
|
||||
{step === 2 && (
|
||||
<div className="space-y-4 animate-in fade-in slide-in-from-bottom-2 duration-300">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-8 h-8 bg-amber-600 text-white rounded-lg flex items-center justify-center text-sm font-bold">2</div>
|
||||
<h3 className="text-lg font-bold text-slate-900">Escolha o barbeiro</h3>
|
||||
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<div className="space-y-2 text-center md:text-left">
|
||||
<h3 className="text-3xl font-black text-slate-900 tracking-tighter uppercase italic">2. Escolha o <span className="text-amber-600">Mestre</span></h3>
|
||||
<p className="text-slate-500 font-medium italic">Selecione o artista que cuidará do seu visual.</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-6">
|
||||
{shop.barbers.map((b) => (
|
||||
<button
|
||||
key={b.id}
|
||||
@@ -246,25 +213,26 @@ export default function Booking() {
|
||||
setBarber(b.id);
|
||||
setStep(3);
|
||||
}}
|
||||
className={`p-4 rounded-xl border-2 text-center transition-all flex flex-col items-center gap-3 ${barberId === b.id
|
||||
? 'border-amber-500 bg-amber-50/50 shadow-md ring-2 ring-amber-200'
|
||||
: 'border-slate-100 hover:border-amber-300 hover:bg-amber-50/30'
|
||||
}`}
|
||||
className={`group p-6 rounded-[2.5rem] border-2 text-center transition-all duration-300 flex flex-col items-center gap-5 ${
|
||||
barberId === b.id
|
||||
? 'border-slate-900 bg-slate-900 text-white shadow-2xl translate-y-[-4px]'
|
||||
: 'border-slate-50 bg-slate-50 hover:border-amber-200 hover:bg-amber-50/50'
|
||||
}`}
|
||||
>
|
||||
<div className="w-full aspect-square rounded-2xl overflow-hidden border-2 border-slate-200 bg-slate-50">
|
||||
<div className={`w-32 h-32 rounded-[2rem] overflow-hidden border-4 transition-all duration-500 ${barberId === b.id ? 'border-amber-500 rotate-3' : 'border-white group-hover:border-amber-100'}`}>
|
||||
{b.imageUrl ? (
|
||||
<img src={b.imageUrl} alt={b.name} className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-slate-400">
|
||||
<User size={32} />
|
||||
<div className="w-full h-full flex items-center justify-center bg-slate-100 text-slate-300">
|
||||
<User size={48} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-bold text-slate-900">{b.name}</p>
|
||||
{b.specialties.length > 0 && (
|
||||
<p className="text-xs text-slate-500 mt-1">{b.specialties[0]}</p>
|
||||
)}
|
||||
<div className="space-y-1">
|
||||
<p className={`font-black text-lg uppercase italic tracking-tight ${barberId === b.id ? 'text-amber-500' : 'text-slate-900 group-hover:text-amber-600'}`}>{b.name}</p>
|
||||
<p className={`text-[10px] font-black uppercase tracking-[0.2em] ${barberId === b.id ? 'text-slate-400' : 'text-slate-400'}`}>
|
||||
{b.specialties[0] || 'Elite Barber'}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
@@ -273,12 +241,15 @@ export default function Booking() {
|
||||
)}
|
||||
|
||||
{step === 3 && (
|
||||
<div className="space-y-4 animate-in fade-in slide-in-from-bottom-2 duration-300">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-8 h-8 bg-amber-600 text-white rounded-lg flex items-center justify-center text-sm font-bold">3</div>
|
||||
<h3 className="text-lg font-bold text-slate-900">Escolha a data</h3>
|
||||
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<div className="space-y-2 text-center md:text-left">
|
||||
<h3 className="text-3xl font-black text-slate-900 tracking-tighter uppercase italic">3. Defina o <span className="text-amber-600">Momento</span></h3>
|
||||
<p className="text-slate-500 font-medium italic">Seu tempo é valioso. Escolha a data perfeita.</p>
|
||||
</div>
|
||||
<div className="max-w-md mx-auto">
|
||||
<div className="max-w-md mx-auto relative">
|
||||
<div className="absolute left-6 top-1/2 -translate-y-1/2 text-amber-600 pointer-events-none z-10">
|
||||
<Calendar size={20} />
|
||||
</div>
|
||||
<Input
|
||||
type="date"
|
||||
value={date}
|
||||
@@ -287,62 +258,84 @@ export default function Booking() {
|
||||
if (e.target.value) setStep(4);
|
||||
}}
|
||||
min={new Date().toISOString().split('T')[0]}
|
||||
className="text-lg py-6"
|
||||
className="h-16 pl-14 pr-6 bg-slate-50 border-none rounded-2xl text-lg font-black uppercase tracking-widest text-slate-900 focus:ring-2 focus:ring-amber-500/20 transition-all shadow-inner"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 4 && (
|
||||
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-2 duration-300">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-8 h-8 bg-amber-600 text-white rounded-lg flex items-center justify-center text-sm font-bold">4</div>
|
||||
<h3 className="text-lg font-bold text-slate-900">Escolha o horário</h3>
|
||||
<div className="space-y-10 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<div className="space-y-2 text-center md:text-left">
|
||||
<h3 className="text-3xl font-black text-slate-900 tracking-tighter uppercase italic">4. Escolha o <span className="text-amber-600">Horário</span></h3>
|
||||
<p className="text-slate-500 font-medium italic">A pontualidade é a cortesia dos reis.</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-slate-50 rounded-xl mb-4 flex items-center justify-between border border-slate-100">
|
||||
<div className="flex items-center gap-2 text-slate-700">
|
||||
<Calendar size={18} className="text-amber-600" />
|
||||
<span className="font-bold">{new Date(date).toLocaleDateString('pt-PT', { day: 'numeric', month: 'long', year: 'numeric' })}</span>
|
||||
<div className="flex flex-col md:flex-row gap-6">
|
||||
{/* Left Side: Summary Sidebar */}
|
||||
<div className="w-full md:w-80 space-y-4">
|
||||
<div className="p-6 bg-slate-900 text-white rounded-[2rem] space-y-6 shadow-xl relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-amber-500/5 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2" />
|
||||
<div className="space-y-4 relative z-10">
|
||||
<div className="space-y-1 border-b border-white/10 pb-4">
|
||||
<p className="text-[10px] font-black text-slate-500 uppercase tracking-widest">Serviço</p>
|
||||
<p className="text-lg font-black uppercase italic tracking-tight">{selectedService?.name}</p>
|
||||
<p className="text-xl font-black text-amber-500 tracking-tighter">{currency(selectedService?.price || 0)}</p>
|
||||
</div>
|
||||
<div className="space-y-1 border-b border-white/10 pb-4">
|
||||
<p className="text-[10px] font-black text-slate-500 uppercase tracking-widest">Mestre</p>
|
||||
<p className="font-black uppercase italic text-sm">{selectedBarber?.name}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-[10px] font-black text-slate-500 uppercase tracking-widest">Data</p>
|
||||
<p className="font-black uppercase italic text-sm">
|
||||
{new Date(date).toLocaleDateString('pt-PT', { day: 'numeric', month: 'long' })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={() => setStep(3)}>Alterar</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 md:grid-cols-4 gap-2">
|
||||
{availableSlots.length > 0 ? (
|
||||
availableSlots.map((h) => (
|
||||
<button
|
||||
key={h}
|
||||
onClick={() => setSlot(h)}
|
||||
className={`py-3 rounded-lg border-2 text-sm font-bold transition-all ${slot === h
|
||||
? 'border-amber-600 bg-amber-600 text-white shadow-md'
|
||||
: 'border-slate-200 text-slate-700 hover:border-amber-400 hover:bg-amber-50'
|
||||
}`}
|
||||
>
|
||||
{h}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className="col-span-full py-8 text-center bg-rose-50 rounded-xl border border-rose-100">
|
||||
<p className="text-sm text-rose-600 font-bold">Infelizmente não há horários livres para este dia.</p>
|
||||
{/* Right Side: Slots Grid */}
|
||||
<div className="flex-1 space-y-6">
|
||||
<div className="grid grid-cols-3 sm:grid-cols-4 gap-3">
|
||||
{availableSlots.length > 0 ? (
|
||||
availableSlots.map((h) => (
|
||||
<button
|
||||
key={h}
|
||||
onClick={() => setSlot(h)}
|
||||
className={`h-14 rounded-2xl border-2 text-sm font-black tracking-widest transition-all duration-300 ${
|
||||
slot === h
|
||||
? 'border-slate-900 bg-slate-900 text-amber-500 shadow-xl scale-105 z-10'
|
||||
: 'border-slate-50 bg-slate-50 text-slate-600 hover:border-amber-200 hover:bg-amber-50'
|
||||
}`}
|
||||
>
|
||||
{h}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className="col-span-full py-12 text-center bg-rose-50 rounded-[2rem] border border-rose-100">
|
||||
<p className="text-sm text-rose-600 font-black uppercase tracking-widest italic">Sem disponibilidade para este dia</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Final Summary & Confirmation */}
|
||||
{canSubmit && (
|
||||
<div className="pt-6 border-t border-slate-200 mt-8 space-y-4">
|
||||
<div className="flex justify-between items-end">
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 font-bold uppercase mb-1">Total a pagar</p>
|
||||
<p className="text-2xl font-black text-slate-900">{currency(selectedService?.price || 0)}</p>
|
||||
{canSubmit && (
|
||||
<div className="pt-6 animate-in slide-in-from-right-4 duration-500">
|
||||
<Button
|
||||
onClick={submit}
|
||||
size="lg"
|
||||
className="w-full h-16 bg-slate-900 hover:bg-slate-800 text-amber-500 font-black rounded-2xl shadow-2xl transition-all active:scale-95 uppercase tracking-[0.2em] text-sm italic"
|
||||
>
|
||||
Confirmar Experiência de Elite
|
||||
</Button>
|
||||
<p className="text-center mt-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest">
|
||||
Pagamento realizado após o serviço
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={submit} size="lg" className="px-10 bg-indigo-600 hover:bg-indigo-700 shadow-lg shadow-indigo-100">
|
||||
Confirmar Marcação
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,8 @@ 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 } from 'lucide-react';
|
||||
import { Search, Star } from 'lucide-react';
|
||||
import { Button } from '../components/ui/button';
|
||||
|
||||
export default function Explore() {
|
||||
const { shops } = useApp();
|
||||
@@ -52,66 +53,94 @@ export default function Explore() {
|
||||
}, [shops, query, filter, sortBy]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<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 className="max-w-6xl mx-auto space-y-10 py-6">
|
||||
<section className="space-y-4 text-center md:text-left">
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
||||
<div className="space-y-1">
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-amber-100 text-amber-700 text-[10px] font-black uppercase tracking-widest mb-2">
|
||||
<Star size={10} fill="currentColor" />
|
||||
<span>As melhores Barbearias</span>
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-5xl font-black text-slate-900 tracking-tighter">
|
||||
Explorar <span className="text-amber-600">Espaços</span>
|
||||
</h1>
|
||||
<p className="text-slate-500 font-medium max-w-md">Descubra barbearias exclusivas e reserve o seu próximo corte em segundos.</p>
|
||||
</div>
|
||||
<div className="hidden md:block text-sm font-bold text-slate-400 uppercase tracking-widest">
|
||||
<span className="text-slate-900">{filtered.length}</span> Espaços Disponíveis
|
||||
</div>
|
||||
<div className="text-sm text-slate-500">{filtered.length} resultados</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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="pl-11"
|
||||
/>
|
||||
<Search size={18} className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400" />
|
||||
<div className="grid gap-8">
|
||||
<Card className="p-2 border-none glass-card rounded-[2.5rem] shadow-2xl shadow-slate-200/50">
|
||||
<div className="flex flex-col md:flex-row items-center gap-2 p-1">
|
||||
<div className="relative flex-1 w-full">
|
||||
<Search size={20} className="absolute left-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="h-14 pl-14 pr-6 bg-transparent border-none text-lg font-medium focus:ring-0 placeholder:text-slate-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="h-10 w-px bg-slate-200 hidden md:block" />
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2 px-4 py-2 w-full md:w-auto">
|
||||
<Chip
|
||||
active={filter === 'todas'}
|
||||
onClick={() => setFilter('todas')}
|
||||
className={`h-11 px-6 rounded-2xl font-bold uppercase tracking-tight transition-all ${filter === 'todas' ? '!bg-slate-900 !text-amber-500 border-none shadow-lg' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'}`}
|
||||
>
|
||||
Todas
|
||||
</Chip>
|
||||
<Chip
|
||||
active={filter === 'top'}
|
||||
onClick={() => setFilter('top')}
|
||||
className={`h-11 px-6 rounded-2xl font-bold uppercase tracking-tight transition-all ${filter === 'top' ? '!bg-slate-900 !text-amber-500 border-none shadow-lg' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'}`}
|
||||
>
|
||||
Top Avaliadas
|
||||
</Chip>
|
||||
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value as typeof sortBy)}
|
||||
className="h-11 rounded-2xl border-none bg-slate-100 px-4 text-sm font-bold text-slate-700 focus:ring-2 focus:ring-amber-500/20"
|
||||
>
|
||||
<option value="avaliacao">Melhor avaliação</option>
|
||||
<option value="servicos">Mais serviços</option>
|
||||
</select>
|
||||
</div>
|
||||
</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="avaliacao">Melhor avaliação</option>
|
||||
<option value="servicos">Mais serviços</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{!useApp().shopsReady ? (
|
||||
<div className="py-20 text-center">
|
||||
<div className="inline-block w-8 h-8 border-4 border-slate-200 border-t-indigo-600 rounded-full animate-spin mb-2" />
|
||||
<p className="text-sm text-slate-500">A carregar barbearias...</p>
|
||||
</div>
|
||||
) : filtered.length === 0 ? (
|
||||
<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 md:grid-cols-2 gap-4">
|
||||
{filtered.map((shop) => (
|
||||
<ShopCard key={shop.id} shop={shop} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!useApp().shopsReady ? (
|
||||
<div className="py-24 text-center">
|
||||
<div className="inline-block w-12 h-12 border-4 border-slate-200 border-t-amber-600 rounded-full animate-spin mb-4" />
|
||||
<p className="text-slate-500 font-bold uppercase tracking-widest text-xs">A carregar espaços...</p>
|
||||
</div>
|
||||
) : filtered.length === 0 ? (
|
||||
<Card className="p-16 text-center space-y-4 border-none glass-card rounded-[2rem]">
|
||||
<div className="w-16 h-16 bg-slate-100 rounded-full flex items-center justify-center mx-auto text-slate-400">
|
||||
<Search size={32} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-2xl font-black text-slate-900 tracking-tight">Nenhuma barbearia encontrada</p>
|
||||
<p className="text-slate-500 font-medium">Tente ajustar o termo de pesquisa ou os filtros ativos.</p>
|
||||
</div>
|
||||
<Button variant="ghost" onClick={() => {setQuery(''); setFilter('todas');}} className="font-bold text-amber-600 hover:text-amber-700">
|
||||
Limpar Tudo
|
||||
</Button>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-2 gap-8 lg:gap-10">
|
||||
{filtered.map((shop) => (
|
||||
<ShopCard key={shop.id} shop={shop} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,299 +31,209 @@ export default function Landing() {
|
||||
const featuredShops = mockShops.slice(0, 3);
|
||||
|
||||
return (
|
||||
<div className="space-y-16 md:space-y-24 pb-12">
|
||||
{/* Hero Section */}
|
||||
<section className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-indigo-600 via-blue-600 to-indigo-700 text-white px-6 py-16 md:px-12 md:py-24 shadow-2xl">
|
||||
<div className="absolute inset-0 bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxnIGZpbGw9IiNmZmYiIGZpbGwtb3BhY2l0eT0iMC4xIj48Y2lyY2xlIGN4PSIzMCIgY3k9IjMwIiByPSIyIi8+PC9nPjwvZz48L3N2Zz4=')] opacity-20"></div>
|
||||
<div className="absolute top-0 right-0 w-96 h-96 bg-blue-400/20 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2"></div>
|
||||
<div className="absolute bottom-0 left-0 w-96 h-96 bg-indigo-500/20 rounded-full blur-3xl translate-y-1/2 -translate-x-1/2"></div>
|
||||
<div className="space-y-24 md:space-y-32 pb-24">
|
||||
{/* Hero Section - Midnight Luxury Style */}
|
||||
<section className="relative overflow-hidden rounded-[3rem] obsidian-gradient text-white px-8 py-20 md:px-16 md:py-32 shadow-[0_20px_50px_rgba(0,0,0,0.3)] border border-white/5">
|
||||
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/carbon-fibre.png')] opacity-20 pointer-events-none"></div>
|
||||
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-amber-500/10 rounded-full blur-[120px] -translate-y-1/2 translate-x-1/2"></div>
|
||||
<div className="absolute bottom-0 left-0 w-[500px] h-[500px] bg-slate-500/10 rounded-full blur-[120px] translate-y-1/2 -translate-x-1/2"></div>
|
||||
|
||||
<div className="relative space-y-8 max-w-4xl">
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 bg-white/20 backdrop-blur-sm rounded-full text-sm font-semibold w-fit border border-white/30">
|
||||
<Sparkles size={16} />
|
||||
<span>Revolucione sua barbearia</span>
|
||||
<div className="relative z-10 space-y-10 max-w-5xl">
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 bg-white/5 backdrop-blur-md rounded-full text-[10px] font-black uppercase tracking-[0.3em] w-fit border border-white/10 animate-fade-in text-amber-500">
|
||||
<Sparkles size={14} className="animate-pulse" />
|
||||
<span>O Novo Standard do Cuidado Masculino</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-5xl md:text-6xl lg:text-7xl font-bold leading-tight text-balance">
|
||||
Agendamentos, produtos e gestão em um{' '}
|
||||
<span className="text-blue-100">único lugar</span>
|
||||
<h1 className="text-6xl md:text-8xl font-black leading-[0.9] tracking-tighter text-balance uppercase italic">
|
||||
Elegância em cada <br />
|
||||
<span className="gold-gradient bg-clip-text text-transparent italic">Agendamento</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-xl md:text-2xl text-blue-50/90 max-w-3xl leading-relaxed">
|
||||
Experiência mobile-first para clientes e painel completo para barbearias.
|
||||
Simplifique a gestão do seu negócio e aumente sua receita.
|
||||
<p className="text-xl md:text-2xl text-slate-300 max-w-2xl leading-relaxed font-medium">
|
||||
Transforme a rotina da sua barbearia com uma experiência digital digna de um cavalheiro.
|
||||
Mobile-first, premium e inteligente.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-4 pt-4">
|
||||
<Button asChild size="lg" className="text-base px-8 py-4">
|
||||
<Link to="/explorar" className="flex items-center gap-2">
|
||||
Explorar barbearias
|
||||
<div className="flex flex-wrap gap-6 pt-6">
|
||||
<Button asChild size="lg" className="h-16 px-10 bg-white text-slate-950 hover:bg-amber-500 hover:text-white font-black uppercase tracking-widest text-xs transition-all duration-300 rounded-2xl shadow-2xl">
|
||||
<Link to="/explorar" className="flex items-center gap-3">
|
||||
Explorar Espaços
|
||||
<ArrowRight size={18} />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg" className="bg-white/10 backdrop-blur-sm text-white border-white/30 hover:bg-white/20 text-base px-8 py-4">
|
||||
<Link to="/registo">Criar conta grátis</Link>
|
||||
<Button asChild variant="outline" size="lg" className="h-16 px-10 bg-transparent text-white border-white/20 hover:bg-white/5 hover:border-white/40 font-black uppercase tracking-widest text-xs rounded-2xl backdrop-blur-sm">
|
||||
<Link to="/registo">Parceiro Profissional</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-3 gap-6 pt-8 border-t border-white/20">
|
||||
<div>
|
||||
<div className="text-3xl md:text-4xl font-bold">500+</div>
|
||||
<div className="text-sm text-blue-100/80 mt-1">Barbearias</div>
|
||||
{/* Stats Bar */}
|
||||
<div className="grid grid-cols-3 gap-10 pt-12 border-t border-white/10 max-w-2xl">
|
||||
<div className="space-y-1">
|
||||
<div className="text-4xl md:text-5xl font-black tracking-tighter italic">500+</div>
|
||||
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Espaços de Luxo</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-3xl md:text-4xl font-bold">10k+</div>
|
||||
<div className="text-sm text-blue-100/80 mt-1">Agendamentos</div>
|
||||
<div className="space-y-1">
|
||||
<div className="text-4xl md:text-5xl font-black tracking-tighter italic">10K+</div>
|
||||
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Cortes Marcados</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-3xl md:text-4xl font-bold">4.8</div>
|
||||
<div className="text-sm text-blue-100/80 mt-1">Avaliação média</div>
|
||||
<div className="space-y-1">
|
||||
<div className="text-4xl md:text-5xl font-black tracking-tighter gold-gradient bg-clip-text text-transparent italic">4.9</div>
|
||||
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Rating de Elite</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Features Grid */}
|
||||
<section>
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-slate-900 mb-4">
|
||||
Tudo que você precisa
|
||||
</h2>
|
||||
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
|
||||
Funcionalidades poderosas para clientes e barbearias
|
||||
</p>
|
||||
{/* Hero Image Mockup (Place with a generated-like look) */}
|
||||
<section className="relative -mt-32 px-6">
|
||||
<div className="max-w-6xl mx-auto rounded-[3rem] overflow-hidden shadow-[0_50px_100px_rgba(0,0,0,0.5)] border-4 border-white/10 glass-card">
|
||||
<div className="aspect-video bg-slate-900 flex items-center justify-center relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-slate-900 via-slate-800 to-slate-950 flex items-center justify-center opacity-80" />
|
||||
<div className="relative z-10 text-center space-y-6">
|
||||
<Scissors size={80} className="text-amber-500 mx-auto mb-4" />
|
||||
<h3 className="text-2xl font-black text-white uppercase italic tracking-tighter">Smart Agenda Elite Edition</h3>
|
||||
<div className="flex gap-4 justify-center">
|
||||
<div className="w-12 h-1 bg-amber-500 rounded-full" />
|
||||
<div className="w-12 h-1 bg-white/20 rounded-full" />
|
||||
<div className="w-12 h-1 bg-white/20 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Features - Minimalist & Bold */}
|
||||
<section className="max-w-7xl mx-auto px-6">
|
||||
<div className="text-center md:text-left mb-16 flex flex-col md:flex-row md:items-end justify-between gap-6">
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-5xl md:text-6xl font-black text-slate-900 tracking-tighter uppercase italic pr-8 border-l-[12px] border-amber-500 pl-8">
|
||||
Ecossistema <br /> <span className="text-amber-600">Completo</span>
|
||||
</h2>
|
||||
<p className="text-xl text-slate-500 font-medium max-w-xl">
|
||||
Tudo o que a sua barbearia precisa para escalar com sofisticação.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-10">
|
||||
{[
|
||||
{
|
||||
icon: Calendar,
|
||||
title: 'Agendamentos Inteligentes',
|
||||
desc: 'Escolha serviço, barbeiro, data e horário com validação de slots em tempo real. Notificações automáticas.',
|
||||
color: 'from-blue-500 to-blue-600'
|
||||
icon: Clock,
|
||||
title: 'Gestão Cirúrgica',
|
||||
desc: 'Controle de horários com precisão absoluta. Slot management inteligente e automação de reserva.',
|
||||
color: 'bg-slate-950 shadow-[0_15px_35px_rgba(0,0,0,0.15)]'
|
||||
},
|
||||
{
|
||||
icon: ShoppingBag,
|
||||
title: 'Carrinho Inteligente',
|
||||
desc: 'Produtos e serviços agrupados por barbearia, checkout rápido e seguro. Histórico completo de compras.',
|
||||
color: 'from-emerald-500 to-emerald-600'
|
||||
title: 'Curadoria de Produtos',
|
||||
desc: 'Venda produtos de elite diretamente no ecossistema. Gestão de stock e carrinho omnicanal.',
|
||||
color: 'bg-white border-2 border-slate-50'
|
||||
},
|
||||
{
|
||||
icon: BarChart3,
|
||||
title: 'Painel Completo',
|
||||
desc: 'Faturamento, agendamentos, pedidos e análises detalhadas. Tudo no controle da sua barbearia.',
|
||||
color: 'from-purple-500 to-purple-600'
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: 'Gestão de Barbeiros',
|
||||
desc: 'Gerencie horários, especialidades e disponibilidade de cada barbeiro. Calendário integrado.',
|
||||
color: 'from-indigo-500 to-indigo-600'
|
||||
},
|
||||
{
|
||||
icon: Clock,
|
||||
title: 'Horários Flexíveis',
|
||||
desc: 'Configure horários de funcionamento, intervalos e disponibilidade. Sistema automático de bloqueio.',
|
||||
color: 'from-orange-500 to-orange-600'
|
||||
},
|
||||
{
|
||||
icon: Shield,
|
||||
title: 'Seguro e Confiável',
|
||||
desc: 'Dados protegidos, pagamentos seguros e backup automático. Conformidade com LGPD.',
|
||||
color: 'from-rose-500 to-rose-600'
|
||||
title: 'Analytics de Luxo',
|
||||
desc: 'Relatórios detalhados de faturamento, performance de barbeiros e taxas de retenção.',
|
||||
color: 'bg-white border-2 border-slate-50'
|
||||
},
|
||||
].map((feature) => (
|
||||
<Card key={feature.title} hover className="p-6 space-y-4 group">
|
||||
<div className={`inline-flex p-3 rounded-xl bg-gradient-to-br ${feature.color} text-white shadow-lg group-hover:scale-110 transition-transform duration-200`}>
|
||||
<feature.icon size={24} />
|
||||
<Card key={feature.title} className={`p-10 space-y-6 rounded-[2.5rem] transition-all duration-500 hover:-translate-y-2 group ${feature.color}`}>
|
||||
<div className={`w-16 h-16 rounded-2xl flex items-center justify-center shadow-inner ${feature.icon === Clock ? 'bg-amber-500 text-slate-900' : 'bg-slate-900 text-amber-500'}`}>
|
||||
<feature.icon size={32} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-slate-900 mb-2">{feature.title}</h3>
|
||||
<p className="text-sm text-slate-600 leading-relaxed">{feature.desc}</p>
|
||||
<div className="space-y-3">
|
||||
<h3 className={`text-2xl font-black tracking-tight uppercase italic ${feature.icon === Clock ? 'text-white' : 'text-slate-900'}`}>{feature.title}</h3>
|
||||
<p className={`text-sm leading-relaxed font-medium ${feature.icon === Clock ? 'text-slate-400' : 'text-slate-500'}`}>{feature.desc}</p>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* How it Works */}
|
||||
<section className="bg-gradient-to-br from-slate-50 to-blue-50/30 rounded-2xl p-8 md:p-12">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-slate-900 mb-4">
|
||||
Como funciona
|
||||
</h2>
|
||||
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
|
||||
Simples, rápido e eficiente em 3 passos
|
||||
</p>
|
||||
</div>
|
||||
{/* How it Works - Immersive */}
|
||||
<section className="bg-slate-950 py-24 md:py-32 overflow-hidden relative">
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-slate-900 via-slate-950 to-slate-950 opacity-50" />
|
||||
<div className="max-w-6xl mx-auto px-6 relative z-10">
|
||||
<div className="text-center mb-20">
|
||||
<h2 className="text-5xl md:text-6xl font-black text-white tracking-tighter uppercase italic mb-6">
|
||||
A Jornada do <span className="gold-gradient bg-clip-text text-transparent">Cavalheiro</span>
|
||||
</h2>
|
||||
<div className="w-24 h-1 bg-amber-500 mx-auto rounded-full" />
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-8 max-w-4xl mx-auto">
|
||||
{[
|
||||
{ step: '1', title: 'Explore', desc: 'Navegue pelas barbearias disponíveis, veja avaliações e serviços oferecidos.' },
|
||||
{ step: '2', title: 'Agende', desc: 'Escolha o serviço, barbeiro e horário que melhor se adequa à sua agenda.' },
|
||||
{ step: '3', title: 'Aproveite', desc: 'Compareça no horário agendado e aproveite um serviço de qualidade.' },
|
||||
].map((item) => (
|
||||
<div key={item.step} className="text-center space-y-4">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-gradient-to-br from-indigo-500 to-blue-600 text-white text-2xl font-bold shadow-lg">
|
||||
{item.step}
|
||||
<div className="grid md:grid-cols-3 gap-16 relative">
|
||||
{[
|
||||
{ step: '01', title: 'Descobrir', desc: 'Encontre os espaços mais exclusivos da cidade com avaliações reais.' },
|
||||
{ step: '02', title: 'Personalizar', desc: 'Escolha o seu barbeiro de confiança e o seu horário preferido.' },
|
||||
{ step: '03', title: 'Vivenciar', desc: 'Receba o tratamento de elite que você merece, sem esperas.' },
|
||||
].map((item, idx) => (
|
||||
<div key={item.step} className="text-center space-y-8 relative group">
|
||||
<div className="relative">
|
||||
<div className="text-8xl font-black text-white/5 absolute -top-12 left-1/2 -translate-x-1/2 select-none group-hover:text-amber-500/10 transition-colors duration-700">
|
||||
{item.step}
|
||||
</div>
|
||||
<div className="w-20 h-20 mx-auto rounded-full obsidian-gradient border-2 border-white/10 flex items-center justify-center text-amber-500 text-2xl font-black italic shadow-[0_0_30px_rgba(245,158,11,0.2)]">
|
||||
{item.step}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-2xl font-black text-white uppercase italic tracking-widest">{item.title}</h3>
|
||||
<p className="text-slate-400 font-medium leading-relaxed">{item.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-slate-900">{item.title}</h3>
|
||||
<p className="text-slate-600">{item.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Featured Shops */}
|
||||
<section>
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-slate-900 mb-2">
|
||||
Barbearias em destaque
|
||||
{/* Featured Shops - Premium Row */}
|
||||
<section className="max-w-7xl mx-auto px-6">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between mb-16 gap-6">
|
||||
<div className="space-y-2 text-center md:text-left">
|
||||
<h2 className="text-4xl md:text-5xl font-black text-slate-900 tracking-tighter uppercase italic">
|
||||
Clubes <span className="text-amber-600">Membros</span>
|
||||
</h2>
|
||||
<p className="text-slate-600">
|
||||
Conheça algumas das melhores barbearias da plataforma
|
||||
</p>
|
||||
<p className="text-slate-500 font-bold uppercase tracking-[0.2em] text-xs">As melhores barbearias do país</p>
|
||||
</div>
|
||||
<Button asChild variant="ghost" className="hidden md:flex">
|
||||
<Button asChild variant="ghost" className="h-14 px-8 rounded-2xl bg-slate-50 border border-slate-100 font-black text-slate-900 uppercase tracking-widest text-xs hover:bg-slate-100 transition-all">
|
||||
<Link to="/explorar" className="flex items-center gap-2">
|
||||
Ver todas
|
||||
Ver Catálogo Completo
|
||||
<ArrowRight size={16} />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-10">
|
||||
{featuredShops.map((shop) => (
|
||||
<ShopCard key={shop.id} shop={shop} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="text-center mt-8">
|
||||
<Button asChild size="lg">
|
||||
<Link to="/explorar">Ver todas as barbearias</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Benefits */}
|
||||
<section className="grid md:grid-cols-2 gap-8">
|
||||
<Card className="p-8 md:p-10 space-y-6">
|
||||
<div className="inline-flex p-3 rounded-xl bg-gradient-to-br from-indigo-500 to-blue-600 text-white shadow-lg">
|
||||
<Smartphone size={28} />
|
||||
</div>
|
||||
<h3 className="text-2xl md:text-3xl font-bold text-slate-900">
|
||||
Mobile-First
|
||||
</h3>
|
||||
<p className="text-slate-600 leading-relaxed">
|
||||
Interface otimizada para dispositivos móveis. Agende de qualquer lugar,
|
||||
a qualquer hora. Experiência fluida e responsiva.
|
||||
</p>
|
||||
<ul className="space-y-3">
|
||||
{['Design responsivo', 'Carregamento rápido', 'Interface intuitiva'].map((item) => (
|
||||
<li key={item} className="flex items-center gap-2 text-slate-700">
|
||||
<CheckCircle2 size={18} className="text-indigo-600 flex-shrink-0" />
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Card>
|
||||
{/* Final CTA - Immersive Dark */}
|
||||
<section className="px-6">
|
||||
<div className="max-w-6xl mx-auto relative overflow-hidden rounded-[4rem] obsidian-gradient text-white px-8 py-20 md:px-20 md:py-24 shadow-2xl border border-white/5">
|
||||
<div className="absolute top-0 right-0 w-[400px] h-[400px] bg-amber-500/10 rounded-full blur-[100px]" />
|
||||
<div className="absolute bottom-0 left-0 w-[400px] h-[400px] bg-slate-500/10 rounded-full blur-[100px]" />
|
||||
|
||||
<Card className="p-8 md:p-10 space-y-6">
|
||||
<div className="inline-flex p-3 rounded-xl bg-gradient-to-br from-purple-500 to-purple-600 text-white shadow-lg">
|
||||
<TrendingUp size={28} />
|
||||
</div>
|
||||
<h3 className="text-2xl md:text-3xl font-bold text-slate-900">
|
||||
Aumente sua Receita
|
||||
</h3>
|
||||
<p className="text-slate-600 leading-relaxed">
|
||||
Ferramentas poderosas para gerenciar seu negócio. Análises detalhadas,
|
||||
gestão de estoque e muito mais.
|
||||
</p>
|
||||
<ul className="space-y-3">
|
||||
{['Análises em tempo real', 'Gestão de estoque', 'Relatórios detalhados'].map((item) => (
|
||||
<li key={item} className="flex items-center gap-2 text-slate-700">
|
||||
<CheckCircle2 size={18} className="text-purple-600 flex-shrink-0" />
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
{/* Testimonials */}
|
||||
<section>
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-slate-900 mb-4">
|
||||
O que nossos clientes dizem
|
||||
</h2>
|
||||
<p className="text-lg text-slate-600">
|
||||
Depoimentos reais de quem usa a plataforma
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
{[
|
||||
{
|
||||
name: 'João Silva',
|
||||
role: 'Cliente',
|
||||
text: 'Facilita muito agendar meu corte. Interface simples e rápida. Recomendo!',
|
||||
rating: 5
|
||||
},
|
||||
{
|
||||
name: 'Carlos Mendes',
|
||||
role: 'Proprietário',
|
||||
text: 'O painel é completo e me ajuda muito na gestão. Aumentou minha organização.',
|
||||
rating: 5
|
||||
},
|
||||
{
|
||||
name: 'Miguel Santos',
|
||||
role: 'Cliente',
|
||||
text: 'Nunca mais perco horário. As notificações são muito úteis.',
|
||||
rating: 5
|
||||
},
|
||||
].map((testimonial) => (
|
||||
<Card key={testimonial.name} className="p-6 space-y-4">
|
||||
<div className="flex items-center gap-1">
|
||||
{[...Array(testimonial.rating)].map((_, i) => (
|
||||
<Star key={i} size={16} className="fill-indigo-500 text-indigo-500" />
|
||||
))}
|
||||
</div>
|
||||
<Quote className="text-indigo-500/50" size={24} />
|
||||
<p className="text-slate-700 leading-relaxed">{testimonial.text}</p>
|
||||
<div className="pt-2 border-t border-slate-100">
|
||||
<div className="font-semibold text-slate-900">{testimonial.name}</div>
|
||||
<div className="text-sm text-slate-500">{testimonial.role}</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Final */}
|
||||
<section className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white px-6 py-16 md:px-12 md:py-20 shadow-2xl">
|
||||
<div className="absolute top-0 right-0 w-96 h-96 bg-indigo-500/10 rounded-full blur-3xl"></div>
|
||||
<div className="absolute bottom-0 left-0 w-96 h-96 bg-blue-500/10 rounded-full blur-3xl"></div>
|
||||
|
||||
<div className="relative text-center space-y-8 max-w-3xl mx-auto">
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-balance">
|
||||
Pronto para começar?
|
||||
</h2>
|
||||
<p className="text-xl text-slate-300 max-w-2xl mx-auto">
|
||||
Junte-se a centenas de barbearias que já estão usando a Smart Agenda
|
||||
para revolucionar seus negócios.
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-4 pt-4">
|
||||
<Button asChild size="lg" className="text-base px-8 py-4 bg-white text-slate-900 hover:bg-slate-100">
|
||||
<Link to="/registo" className="flex items-center gap-2">
|
||||
Criar conta grátis
|
||||
<ArrowRight size={18} />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg" className="text-base px-8 py-4 border-white/30 text-white hover:bg-white/10">
|
||||
<Link to="/explorar">Explorar agora</Link>
|
||||
</Button>
|
||||
<div className="relative text-center space-y-12 max-w-3xl mx-auto">
|
||||
<h2 className="text-5xl md:text-7xl font-black tracking-tighter uppercase italic leading-[0.9]">
|
||||
Faça Parte <br /> do <span className="gold-gradient bg-clip-text text-transparent">Legado</span>
|
||||
</h2>
|
||||
<p className="text-xl text-slate-400 font-medium leading-relaxed">
|
||||
Centenas de profissionais já elevaram o seu negócio ao próximo nível.
|
||||
A sua barbearia merece o melhor.
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-6 pt-4">
|
||||
<Button asChild size="lg" className="h-16 px-10 bg-white text-slate-950 hover:bg-amber-500 hover:text-white font-black uppercase tracking-widest text-xs rounded-2xl transition-all shadow-2xl">
|
||||
<Link to="/registo" className="flex items-center gap-3">
|
||||
Criar Conta Grátis
|
||||
<ArrowRight size={18} />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg" className="h-16 px-10 border-white/20 text-white font-black uppercase tracking-widest text-xs rounded-2xl hover:bg-white/5">
|
||||
<Link to="/explorar">Navegar agora</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -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, MapPin } from 'lucide-react'
|
||||
import { Calendar, ShoppingBag, User, Clock, Heart, Star, MapPin, CheckCircle2 } from 'lucide-react'
|
||||
import { supabase } from '../lib/supabase'
|
||||
import { ReviewModal } from '../components/ReviewModal'
|
||||
|
||||
@@ -130,60 +130,67 @@ export default function Profile() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="space-y-8">
|
||||
{/* Profile Header */}
|
||||
<Card className="p-6 bg-gradient-to-br from-amber-50 via-white to-orange-50 border-amber-100">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-amber-400 to-amber-600 rounded-2xl flex items-center justify-center text-white shadow-lg flex-shrink-0">
|
||||
<User size={28} />
|
||||
<div className="max-w-4xl mx-auto space-y-12 pb-20">
|
||||
{/* Profile Header - Luxury Style */}
|
||||
<section className="relative overflow-hidden rounded-[3rem] obsidian-gradient text-white p-8 md:p-12 shadow-2xl border border-white/5">
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-amber-500/10 rounded-full blur-[80px] -translate-y-1/2 translate-x-1/2" />
|
||||
<div className="relative z-10 flex flex-col md:flex-row items-center gap-8 md:text-left text-center">
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-0 bg-amber-500 blur-2xl opacity-20 group-hover:opacity-40 transition-opacity" />
|
||||
<div className="w-24 h-24 bg-white/10 backdrop-blur-xl border-2 border-white/20 rounded-[2rem] flex items-center justify-center text-amber-500 shadow-2xl relative z-10 transition-transform duration-500 hover:rotate-6">
|
||||
<User size={48} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h1 className="text-2xl font-bold text-slate-900 truncate">Olá, {displayName}!</h1>
|
||||
<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 className="space-y-3">
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 bg-white/5 border border-white/10 rounded-full text-[10px] font-black uppercase tracking-[0.2em] text-amber-500">
|
||||
<Star size={12} fill="currentColor" />
|
||||
<span>Membro de Elite</span>
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-5xl font-black tracking-tighter uppercase italic leading-[0.9]">
|
||||
{displayName}
|
||||
</h1>
|
||||
<p className="text-slate-400 font-medium italic">{authEmail}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
{/* ❤️ Barbearias Favoritas */}
|
||||
{/* ❤️ Barbearias Favoritas - Horizontal Scroll or Grid */}
|
||||
{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>
|
||||
<section className="space-y-6">
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<div className="flex items-center gap-2 text-slate-900">
|
||||
<Heart size={16} className="text-rose-500 fill-rose-500" />
|
||||
<h2 className="text-sm font-black uppercase tracking-[0.3em]">Cofre de Favoritos</h2>
|
||||
</div>
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">{favoriteShops.length} Espaços</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
{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" />
|
||||
<Card className="p-2 border-none glass-card rounded-[2rem] shadow-lg shadow-slate-200/50 hover:-translate-y-1 transition-all duration-300 group">
|
||||
<div className="flex items-center gap-4 p-4">
|
||||
<div className="w-16 h-16 rounded-2xl overflow-hidden border-2 border-slate-50 shadow-inner group-hover:border-amber-200 transition-colors">
|
||||
{shop.imageUrl ? (
|
||||
<img src={shop.imageUrl} alt={shop.name} className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<div className="w-full h-full bg-slate-100 flex items-center justify-center text-slate-300">
|
||||
<User size={24} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-black text-slate-900 uppercase italic tracking-tight group-hover:text-amber-600 transition-colors truncate">{shop.name}</p>
|
||||
<div className="flex items-center gap-3 mt-1">
|
||||
<div className="flex items-center gap-1 text-[10px] font-black text-amber-600 uppercase tracking-widest">
|
||||
<Star size={10} className="fill-amber-500" />
|
||||
{shop.rating.toFixed(1)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-[10px] font-black text-slate-400 uppercase tracking-widest truncate">
|
||||
<MapPin size={10} />
|
||||
{shop.address.split(',')[0]}
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
@@ -192,117 +199,123 @@ export default function Profile() {
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Agendamentos */}
|
||||
<section className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar size={20} className="text-amber-600" />
|
||||
<h2 className="text-xl font-bold text-slate-900">Agendamentos</h2>
|
||||
<Badge color="slate" variant="soft">{myAppointments.length}</Badge>
|
||||
</div>
|
||||
|
||||
{!myAppointments.length ? (
|
||||
<Card className="p-8 text-center">
|
||||
<Calendar size={48} className="mx-auto text-slate-300 mb-3" />
|
||||
<p className="text-slate-600 font-medium">Nenhum agendamento ainda</p>
|
||||
<p className="text-sm text-slate-500 mt-1">Explore barbearias e agende o seu primeiro serviço!</p>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{myAppointments.map((a) => {
|
||||
const shop = shops.find((s) => s.id === a.shopId)
|
||||
const service = shop?.services.find((s) => s.id === a.serviceId)
|
||||
const canReview = a.status === 'concluido' && !reviewedAppointments.has(a.id)
|
||||
return (
|
||||
<Card key={a.id} hover className="p-5">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 space-y-1.5">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h3 className="font-bold text-slate-900">{shop?.name}</h3>
|
||||
<Badge color={statusColor[a.status]} variant="soft">
|
||||
{statusLabel[a.status]}
|
||||
</Badge>
|
||||
</div>
|
||||
{service && (
|
||||
<p className="text-sm text-slate-600 flex items-center gap-1">
|
||||
<Clock size={13} />
|
||||
{service.name} · {service.duration} min
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs text-slate-500">{a.date}</p>
|
||||
{/* Botão de avaliar */}
|
||||
{canReview && (
|
||||
<button
|
||||
onClick={() => setReviewTarget({ appointmentId: a.id, shopId: a.shopId, shopName: shop?.name ?? 'Barbearia' })}
|
||||
className="flex items-center gap-1.5 mt-2 text-xs font-semibold text-amber-600 hover:text-amber-700 bg-amber-50 hover:bg-amber-100 px-3 py-1.5 rounded-lg transition-colors"
|
||||
>
|
||||
<Star size={12} className="fill-amber-400 text-amber-400" />
|
||||
Avaliar atendimento
|
||||
</button>
|
||||
)}
|
||||
{a.status === 'concluido' && reviewedAppointments.has(a.id) && (
|
||||
<p className="text-xs text-green-600 flex items-center gap-1 mt-1">
|
||||
✓ Avaliado
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right flex-shrink-0">
|
||||
<p className="text-lg font-bold text-amber-600">{currency(a.total)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
<div className="grid lg:grid-cols-5 gap-10">
|
||||
{/* Main Column: Appointments */}
|
||||
<section className="lg:col-span-3 space-y-6">
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<div className="flex items-center gap-2 text-slate-900">
|
||||
<Calendar size={16} className="text-amber-600" />
|
||||
<h2 className="text-sm font-black uppercase tracking-[0.3em]">Minha Agenda</h2>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Pedidos */}
|
||||
<section className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShoppingBag size={20} className="text-amber-600" />
|
||||
<h2 className="text-xl font-bold text-slate-900">Pedidos</h2>
|
||||
<Badge color="slate" variant="soft">{myOrders.length}</Badge>
|
||||
</div>
|
||||
{!myAppointments.length ? (
|
||||
<Card className="p-16 text-center border-none glass-card rounded-[3rem] shadow-xl">
|
||||
<Calendar size={64} className="mx-auto text-slate-100 mb-6" />
|
||||
<h3 className="text-xl font-black text-slate-900 uppercase italic tracking-tight">Sem Reservas</h3>
|
||||
<p className="text-slate-400 font-medium italic mt-2">Sua jornada de estilo ainda não começou.</p>
|
||||
<Button asChild className="mt-8 h-12 px-8 bg-slate-900 text-amber-500 font-black rounded-2xl uppercase tracking-widest text-[10px] italic">
|
||||
<Link to="/explorar">Agendar Agora</Link>
|
||||
</Button>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{myAppointments.map((a) => {
|
||||
const shop = shops.find((s) => s.id === a.shopId)
|
||||
const service = shop?.services.find((s) => s.id === a.serviceId)
|
||||
const canReview = a.status === 'concluido' && !reviewedAppointments.has(a.id)
|
||||
|
||||
return (
|
||||
<Card key={a.id} className="p-2 border-none glass-card rounded-[2.5rem] shadow-lg shadow-slate-200/50">
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="font-black text-xl text-slate-900 uppercase italic tracking-tighter">{shop?.name}</h3>
|
||||
<div className={`px-2 py-0.5 rounded-full text-[8px] font-black uppercase tracking-widest bg-${statusColor[a.status]}-100 text-${statusColor[a.status]}-700`}>
|
||||
{statusLabel[a.status]}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs font-black text-slate-400 uppercase tracking-[0.2em]">{a.date}</p>
|
||||
</div>
|
||||
<div className="text-2xl font-black text-slate-900 tracking-tighter italic">
|
||||
{currency(a.total)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!myOrders.length ? (
|
||||
<Card className="p-8 text-center">
|
||||
<ShoppingBag size={48} className="mx-auto text-slate-300 mb-3" />
|
||||
<p className="text-slate-600 font-medium">Nenhum pedido ainda</p>
|
||||
<p className="text-sm text-slate-500 mt-1">Adicione produtos ao carrinho e finalize o primeiro pedido!</p>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{myOrders.map((o) => {
|
||||
const shop = shops.find((s) => s.id === o.shopId)
|
||||
return (
|
||||
<Card key={o.id} hover className="p-5">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 space-y-1.5">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h3 className="font-bold text-slate-900">{shop?.name}</h3>
|
||||
<Badge color={statusColor[o.status]} variant="soft">
|
||||
{statusLabel[o.status]}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500">
|
||||
{new Date(o.createdAt).toLocaleDateString('pt-PT', {
|
||||
day: '2-digit', month: 'long', year: 'numeric'
|
||||
})}
|
||||
</p>
|
||||
<p className="text-xs text-slate-600">
|
||||
{o.items.length} {o.items.length === 1 ? 'item' : 'itens'}
|
||||
</p>
|
||||
<div className="flex items-center justify-between pt-4 border-t border-slate-50">
|
||||
{service && (
|
||||
<div className="flex items-center gap-2 text-[10px] font-black text-slate-500 uppercase tracking-widest">
|
||||
<Clock size={12} className="text-amber-600" />
|
||||
{service.name} · {service.duration} MIN
|
||||
</div>
|
||||
)}
|
||||
|
||||
{canReview && (
|
||||
<button
|
||||
onClick={() => setReviewTarget({ appointmentId: a.id, shopId: a.shopId, shopName: shop?.name ?? 'Barbearia' })}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-amber-500 hover:bg-slate-900 text-white hover:text-amber-500 rounded-xl transition-all duration-300 transform active:scale-95 shadow-lg shadow-amber-500/20"
|
||||
>
|
||||
<Star size={12} className="fill-current" />
|
||||
<span className="text-[10px] font-black uppercase tracking-widest">Avaliar Experiência</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{a.status === 'concluido' && reviewedAppointments.has(a.id) && (
|
||||
<div className="flex items-center gap-1 text-[10px] font-black text-green-600 uppercase tracking-widest">
|
||||
<CheckCircle2 size={12} />
|
||||
Avaliado
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right flex-shrink-0">
|
||||
<p className="text-lg font-bold text-amber-600">{currency(o.total)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Side Column: Orders */}
|
||||
<section className="lg:col-span-2 space-y-6">
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<div className="flex items-center gap-2 text-slate-900">
|
||||
<ShoppingBag size={16} className="text-amber-600" />
|
||||
<h2 className="text-sm font-black uppercase tracking-[0.3em]">Pedidos</h2>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{!myOrders.length ? (
|
||||
<Card className="p-12 text-center border-none glass-card rounded-[3rem] shadow-lg">
|
||||
<p className="text-slate-400 font-medium italic">Sem encomendas efetuadas.</p>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{myOrders.map((o) => {
|
||||
const shop = shops.find((s) => s.id === o.shopId)
|
||||
return (
|
||||
<Card key={o.id} className="p-4 border-none glass-card rounded-[2rem] shadow shadow-slate-200/50">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-black text-slate-900 uppercase italic tracking-tight truncate max-w-[120px]">{shop?.name}</h3>
|
||||
<div className="text-lg font-black text-amber-600 tracking-tighter">{currency(o.total)}</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between pt-2 border-t border-slate-50">
|
||||
<div className="text-[9px] font-black text-slate-400 uppercase tracking-widest">
|
||||
{o.items.length} {o.items.length === 1 ? 'Item' : 'Itens'}
|
||||
</div>
|
||||
<div className={`px-2 py-0.5 rounded-full text-[7px] font-black uppercase tracking-widest bg-${statusColor[o.status]}-100 text-${statusColor[o.status]}-700`}>
|
||||
{statusLabel[o.status]}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -37,87 +37,97 @@ export default function ShopDetails() {
|
||||
)}`;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="relative overflow-hidden rounded-2xl border border-slate-200 bg-slate-100">
|
||||
<div className="max-w-6xl mx-auto space-y-8 py-4">
|
||||
<div className="relative overflow-hidden rounded-[2.5rem] border-none shadow-2xl shadow-slate-200/50">
|
||||
{shop.imageUrl ? (
|
||||
<img src={shop.imageUrl} alt={`Foto de ${shop.name}`} className="h-48 w-full object-cover md:h-64" />
|
||||
<img src={shop.imageUrl} alt={`Foto de ${shop.name}`} className="h-64 w-full object-cover md:h-96 transition-transform duration-1000 hover:scale-105" />
|
||||
) : (
|
||||
<div className="h-48 w-full bg-gradient-to-br from-slate-900 via-slate-700 to-slate-500 md:h-64" />
|
||||
<div className="h-64 w-full obsidian-gradient md:h-96" />
|
||||
)}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-slate-900/70 via-slate-900/15 to-transparent" />
|
||||
<div className="absolute right-4 top-4 flex gap-2">
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-slate-950/80 via-slate-950/20 to-transparent" />
|
||||
|
||||
<div className="absolute right-6 top-6 flex gap-3">
|
||||
<a
|
||||
href={mapUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex h-10 w-10 items-center justify-center rounded-full bg-white/90 text-slate-800 shadow-md hover:bg-white"
|
||||
aria-label="Abrir no Google Maps"
|
||||
className="w-12 h-12 flex items-center justify-center rounded-2xl bg-white/90 backdrop-blur-md text-slate-900 shadow-xl hover:bg-white hover:scale-110 transition-all"
|
||||
title="Ver no Mapa"
|
||||
>
|
||||
<MapPin size={18} />
|
||||
<MapPin size={22} />
|
||||
</a>
|
||||
<button
|
||||
onClick={() => toggleFavorite(shop.id)}
|
||||
className="inline-flex h-10 w-10 items-center justify-center rounded-full bg-white/90 text-slate-800 shadow-md hover:bg-white"
|
||||
aria-label={isFavorite(shop.id) ? 'Remover dos favoritos' : 'Adicionar aos favoritos'}
|
||||
className="w-12 h-12 flex items-center justify-center rounded-2xl bg-white/90 backdrop-blur-md text-slate-900 shadow-xl hover:bg-white hover:scale-110 transition-all font-bold"
|
||||
type="button"
|
||||
>
|
||||
<Heart
|
||||
size={18}
|
||||
className={isFavorite(shop.id) ? 'fill-rose-500 text-rose-500' : 'text-slate-700'}
|
||||
size={22}
|
||||
className={isFavorite(shop.id) ? 'fill-rose-500 text-rose-500' : 'text-slate-900'}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setImageOpen(true)}
|
||||
className="inline-flex h-10 w-10 items-center justify-center rounded-full bg-white/90 text-slate-800 shadow-md hover:bg-white"
|
||||
aria-label="Ampliar foto"
|
||||
className="w-12 h-12 flex items-center justify-center rounded-2xl bg-white/90 backdrop-blur-md text-slate-900 shadow-xl hover:bg-white hover:scale-110 transition-all"
|
||||
type="button"
|
||||
>
|
||||
<Maximize2 size={18} />
|
||||
<Maximize2 size={22} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="absolute bottom-4 left-4 space-y-1 text-white">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Star size={14} className="fill-amber-400 text-amber-400" />
|
||||
<span className="font-semibold">{(shop.rating || 0).toFixed(1)}</span>
|
||||
|
||||
<div className="absolute bottom-10 left-10 space-y-3">
|
||||
<div className="flex items-center gap-2 bg-slate-900/40 backdrop-blur-md border border-white/20 w-fit px-3 py-1 rounded-full">
|
||||
<Star size={14} className="fill-amber-500 text-amber-500" />
|
||||
<span className="text-white text-xs font-black tracking-widest">{(shop.rating || 0).toFixed(1)} EXCELENTE</span>
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-5xl font-black text-white tracking-tighter">{shop.name}</h1>
|
||||
<div className="flex items-center gap-2 text-white/90">
|
||||
<MapPin size={16} className="text-amber-500" />
|
||||
<p className="text-base font-medium">{shop.address}</p>
|
||||
</div>
|
||||
<h1 className="text-2xl font-semibold">{shop.name}</h1>
|
||||
<p className="text-sm text-white/80">{shop.address}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-slate-600">
|
||||
{(shop.services || []).length} serviços · {(shop.barbers || []).length} barbeiros
|
||||
<div className="grid gap-8">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 bg-white/50 backdrop-blur-sm p-2 rounded-[2rem] border border-white/50">
|
||||
<Tabs
|
||||
tabs={[
|
||||
{ id: 'servicos', label: 'Serviços' },
|
||||
{ id: 'barbeiros', label: 'Barbeiros' },
|
||||
{ id: 'produtos', label: 'Produtos' },
|
||||
]}
|
||||
active={tab}
|
||||
onChange={(v) => setTab(v as typeof tab)}
|
||||
className="border-none bg-transparent"
|
||||
/>
|
||||
<div className="px-6 py-2 text-xs font-black text-slate-400 uppercase tracking-widest bg-white rounded-2xl border border-slate-100 shadow-sm">
|
||||
{(shop.services || []).length} SERVIÇOS · {(shop.barbers || []).length} BARBEIROS
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
{tab === 'servicos' ? (
|
||||
<ServiceList
|
||||
services={shop.services}
|
||||
onSelect={(sid) => {
|
||||
window.location.href = `/agendar/${shop.id}?service=${sid}`;
|
||||
}}
|
||||
/>
|
||||
) : tab === 'barbeiros' ? (
|
||||
<div className="bg-white/30 backdrop-blur-md p-8 rounded-[3rem] border border-white/50">
|
||||
<BarberList barbers={shop.barbers} />
|
||||
</div>
|
||||
) : (
|
||||
<ProductList products={shop.products} onAdd={(pid) => addToCart({ shopId: shop.id, type: 'product', refId: pid, qty: 1 })} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Tabs
|
||||
tabs={[
|
||||
{ id: 'servicos', label: 'Serviços' },
|
||||
{ id: 'barbeiros', label: 'Barbeiros' },
|
||||
{ id: 'produtos', label: 'Produtos' },
|
||||
]}
|
||||
active={tab}
|
||||
onChange={(v) => setTab(v as typeof tab)}
|
||||
/>
|
||||
{tab === 'servicos' ? (
|
||||
<ServiceList
|
||||
services={shop.services}
|
||||
onSelect={(sid) => {
|
||||
// Navega para a página de agendamento com o serviço pré-selecionado
|
||||
window.location.href = `/agendar/${shop.id}?service=${sid}`;
|
||||
}}
|
||||
/>
|
||||
) : tab === 'barbeiros' ? (
|
||||
<BarberList barbers={shop.barbers} />
|
||||
) : (
|
||||
<ProductList products={shop.products} onAdd={(pid) => addToCart({ shopId: shop.id, type: 'product', refId: pid, qty: 1 })} />
|
||||
)}
|
||||
|
||||
<Dialog open={imageOpen} title={shop.name} onClose={() => setImageOpen(false)}>
|
||||
{shop.imageUrl ? (
|
||||
<img src={shop.imageUrl} alt={`Foto de ${shop.name}`} className="w-full rounded-lg object-cover" />
|
||||
<img src={shop.imageUrl} alt={`Foto de ${shop.name}`} className="w-full rounded-[2rem] object-cover shadow-2xl" />
|
||||
) : (
|
||||
<div className="h-64 w-full rounded-lg bg-gradient-to-br from-slate-900 via-slate-700 to-slate-500" />
|
||||
<div className="h-96 w-full rounded-[2rem] obsidian-gradient" />
|
||||
)}
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user