alterações design

This commit is contained in:
2026-03-12 17:54:19 +00:00
parent 50a265c29d
commit d63c27ec12
15 changed files with 1028 additions and 924 deletions

View File

@@ -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>

View File

@@ -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>
);
})}

View File

@@ -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>
))}

View File

@@ -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>
);

View File

@@ -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>

View File

@@ -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}

View File

@@ -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>
)}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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">
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">
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>

View File

@@ -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 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>

View File

@@ -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>
);
}

View File

@@ -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 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 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>

View File

@@ -10,7 +10,7 @@ import { Badge } from '../components/ui/badge'
import { Button } from '../components/ui/button'
import { currency } from '../lib/format'
import { useApp } from '../context/AppContext'
import { Calendar, ShoppingBag, User, Clock, Heart, Star, 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>
</>
)

View File

@@ -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>