This commit is contained in:
2026-03-12 16:00:10 +00:00
parent 30c2071841
commit b1344a1257
8 changed files with 795 additions and 475 deletions

View File

@@ -1,90 +1,90 @@
import { Link } from 'react-router-dom';
import { Star, MapPin, Scissors, Heart, Calendar, Users } from 'lucide-react';
import { Star, MapPin, Scissors, Heart, Calendar } from 'lucide-react';
import { BarberShop } from '../types';
import { useApp } from '../context/AppContext';
const gradients = [
'from-emerald-600 to-teal-700',
'from-violet-600 to-indigo-700',
'from-slate-600 to-slate-800',
'from-sky-600 to-blue-700',
'from-rose-600 to-pink-700',
'from-teal-500 to-cyan-700',
];
function shopGradient(name: string) {
return gradients[name.charCodeAt(0) % gradients.length];
}
export const ShopCard = ({ shop }: { shop: BarberShop }) => {
export const ShopCard = ({ shop, compact = false }: { shop: BarberShop; compact?: boolean }) => {
const { toggleFavorite, isFavorite } = useApp();
const favorite = isFavorite(shop.id);
const initials = shop.name.slice(0, 2).toUpperCase();
return (
<div className="group flex flex-col bg-white rounded-xl border border-slate-200 hover:border-emerald-300 hover:shadow-md transition-all duration-200 overflow-hidden">
{/* Header */}
<div className="flex items-start gap-3 p-4">
{/* Avatar */}
<div className="relative flex-shrink-0">
<div className={`w-14 h-14 rounded-xl bg-gradient-to-br ${shopGradient(shop.name)} overflow-hidden flex items-center justify-center shadow-sm`}>
{shop.imageUrl ? (
<img src={shop.imageUrl} alt={shop.name} className="w-full h-full object-cover" />
) : (
<span className="text-white font-black text-lg select-none">{initials}</span>
)}
</div>
{/* Rating */}
<div className="absolute -bottom-1.5 -right-1.5 bg-white border border-slate-200 rounded-full px-1.5 py-0.5 flex items-center gap-0.5 shadow-sm">
<Star size={9} className="fill-yellow-400 text-yellow-400" />
<span className="text-[10px] font-bold text-slate-700">
{shop.rating ? shop.rating.toFixed(1) : '—'}
</span>
<div className="group relative flex flex-col bg-white rounded-2xl overflow-hidden shadow-sm hover:shadow-lg border border-slate-100 hover:border-amber-200 transition-all duration-300">
{/* Cover image */}
<div className="relative h-40 bg-gradient-to-br from-slate-800 to-slate-900 overflow-hidden flex-shrink-0">
{shop.imageUrl ? (
<img
src={shop.imageUrl}
alt={shop.name}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
/>
) : (
<div className="w-full h-full flex flex-col items-center justify-center">
<Scissors size={36} className="text-slate-600 mb-2" />
<p className="text-slate-600 text-xs font-medium">Sem foto de capa</p>
</div>
)}
{/* Gradient overlay */}
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent" />
{/* Rating badge */}
<div className="absolute top-3 left-3 flex items-center gap-1 bg-black/70 backdrop-blur-sm px-2.5 py-1 rounded-full">
<Star size={11} className="fill-amber-400 text-amber-400" />
<span className="text-white text-xs font-bold">
{shop.rating ? shop.rating.toFixed(1) : '—'}
</span>
</div>
{/* Name + address */}
<div className="flex-1 min-w-0 pt-0.5">
<div className="flex items-start justify-between gap-1.5">
<h2 className="font-semibold text-slate-900 text-sm leading-tight truncate group-hover:text-emerald-700 transition-colors">
{shop.name}
</h2>
<button
onClick={(e) => { e.preventDefault(); toggleFavorite(shop.id); }}
className={`flex-shrink-0 p-1 rounded-full transition-all ${favorite ? 'text-rose-500' : 'text-slate-300 hover:text-rose-400'}`}
>
<Heart size={14} className={favorite ? 'fill-rose-500' : ''} />
</button>
</div>
<div className="flex items-center gap-1 mt-1">
<MapPin size={10} className="flex-shrink-0 text-slate-400" />
<p className="text-xs text-slate-500 truncate">{shop.address || 'Endereço não disponível'}</p>
</div>
<div className="flex items-center gap-3 mt-1.5 text-[11px] text-slate-400 font-medium">
<span className="flex items-center gap-0.5"><Scissors size={9} />{(shop.services || []).length} serviços</span>
<span className="flex items-center gap-0.5"><Users size={9} />{(shop.barbers || []).length} barb.</span>
</div>
{/* Favorite button */}
<button
onClick={(e) => { e.preventDefault(); toggleFavorite(shop.id); }}
className={`absolute top-3 right-3 p-1.5 rounded-full backdrop-blur-sm transition-all ${favorite
? 'bg-rose-500 text-white shadow-md scale-110'
: 'bg-black/40 text-white hover:bg-rose-500'
}`}
>
<Heart size={14} className={favorite ? 'fill-white' : ''} />
</button>
{/* Shop name on image */}
<div className="absolute bottom-0 left-0 right-0 p-4">
<h2 className="text-white font-bold text-base leading-tight truncate drop-shadow-md">
{shop.name}
</h2>
</div>
</div>
{/* Divider */}
<div className="h-px bg-slate-100 mx-4" />
{/* Info section */}
<div className="p-4 flex flex-col gap-3 flex-1">
<div className="flex items-start gap-1.5 text-slate-500">
<MapPin size={13} className="shrink-0 mt-0.5 text-amber-500" />
<p className="text-xs leading-snug line-clamp-1">{shop.address || 'Endereço não disponível'}</p>
</div>
{/* Actions */}
<div className="flex gap-2 p-3">
<Link
to={`/barbearia/${shop.id}`}
className="flex-1 text-center text-xs font-medium py-1.5 rounded-lg border border-slate-200 text-slate-600 hover:border-slate-300 hover:bg-slate-50 transition-colors"
>
Ver detalhes
</Link>
<Link
to={`/agendar/${shop.id}`}
className="flex-1 text-center text-xs font-semibold py-1.5 rounded-lg bg-emerald-600 text-white hover:bg-emerald-700 transition-colors flex items-center justify-center gap-1"
>
<Calendar size={10} />
Agendar
</Link>
<div className="flex items-center gap-3 text-xs text-slate-500 font-medium">
<span className="flex items-center gap-1">
<Scissors size={11} className="text-slate-400" />
{(shop.services || []).length} serviços
</span>
<span className="text-slate-300"></span>
<span>{(shop.barbers || []).length} barbeiros</span>
</div>
{/* Action buttons */}
<div className="flex gap-2 mt-auto pt-1">
<Link
to={`/barbearia/${shop.id}`}
className="flex-1 text-center text-xs font-semibold py-2 px-3 rounded-xl border border-slate-200 text-slate-700 hover:border-amber-300 hover:text-amber-700 hover:bg-amber-50 transition-all"
>
Ver detalhes
</Link>
<Link
to={`/agendar/${shop.id}`}
className="flex-1 text-center text-xs font-semibold py-2 px-3 rounded-xl bg-gradient-to-r from-amber-500 to-orange-500 text-white hover:from-amber-600 hover:to-orange-600 transition-all flex items-center justify-center gap-1.5 shadow-sm hover:shadow-md"
>
<Calendar size={12} />
Agendar
</Link>
</div>
</div>
</div>
);

View File

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

View File

@@ -5,7 +5,7 @@ export function Shell() {
return (
<>
<Header />
<main className="mx-auto max-w-7xl px-4 py-6">
<main className="mx-auto max-w-5xl px-4 py-6">
<Outlet />
</main>
</>

View File

@@ -1,4 +1,4 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,400;0,14..32,500;0,14..32,600;0,14..32,700;0,14..32,800;0,14..32,900;1,14..32,400&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap');
@tailwind base;
@tailwind components;
@@ -14,14 +14,16 @@
}
body {
font-family: 'Inter', system-ui, sans-serif;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
@apply bg-slate-50 text-slate-900 antialiased;
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
}
a {
@apply text-inherit no-underline;
}
/* Scrollbar styling */
::-webkit-scrollbar {
@apply w-1.5 h-1.5;
}
@@ -33,4 +35,19 @@
::-webkit-scrollbar-thumb {
@apply bg-slate-300 rounded-full hover:bg-slate-400;
}
}
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
/* Smooth fade-in animation for modals */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fadeIn {
animation: fadeIn 0.25s ease-out forwards;
}
}

View File

@@ -1,106 +1,120 @@
/**
* @file AuthLogin.tsx
* @description Página de Autenticação (Login) para a versão Web da aplicação.
* Permite aos utilizadores (clientes e barbearias) entrarem na sua conta
* através de credenciais de email e palavra-passe, ligando-se ao fluxo de
* sessões do Supabase.
*/
import { FormEvent, useEffect, useState } from 'react'
import { useNavigate, Link } from 'react-router-dom'
import { LogIn, Scissors, Eye, EyeOff } from 'lucide-react'
import { Input } from '../components/ui/input'
import { Button } from '../components/ui/button'
import { Card } from '../components/ui/card'
import { LogIn } from 'lucide-react'
import { supabase } from '../lib/supabase'
import { useApp } from '../context/AppContext'
export default function AuthLogin() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [showPw, setShowPw] = useState(false)
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
// Utilização do hook do react-router-dom para navegação entre páginas web
const navigate = useNavigate()
const { user } = useApp()
/**
* Hook executado na montagem inicial do componente.
* Interage diretamente com o AppContext para verificar
* de forma reativa se o utilizador já está logado.
* Se os dados do utilizador confirmarem autenticação ativa, redireciona o fluxo consoante a role.
*/
useEffect(() => {
if (user) navigate(user.role === 'barbearia' ? '/painel' : '/explorar', { replace: true })
if (user) {
navigate(user.role === 'barbearia' ? '/painel' : '/explorar', { replace: true })
}
}, [user, navigate])
/**
* Manipula a submissão do formulário na view para validar as credenciais.
* @param {FormEvent} e - Evento padrão de submissão form para prevenir comportamento `submit` comum.
*/
const handleLogin = async (e: FormEvent) => {
e.preventDefault()
setError('')
setLoading(true)
try {
const { data, error } = await supabase.auth.signInWithPassword({ email, password })
// Comunicação via API REST do Supabase para Login
// A biblioteca gera pedido POST com as credenciais (Email e JSON PW) para endpoint /token
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (error) throw error
// Sucesso na verificação origina redirecionamento baseado na rule (metadados guardados no Auth)
const role = data.user?.user_metadata?.role
navigate(role === 'barbearia' ? '/painel' : '/explorar', { replace: true })
} catch {
setError('Email ou palavra-passe incorretos.')
if (role) {
navigate(role === 'barbearia' ? '/painel' : '/explorar', { replace: true })
}
} catch (e: any) {
setError('Credenciais inválidas ou email não confirmado')
setLoading(false)
}
}
return (
<div className="flex items-center justify-center py-10 px-4">
<div className="w-full" style={{ maxWidth: 400 }}>
{/* Logo */}
<div className="text-center mb-7">
<div className="inline-flex w-11 h-11 rounded-xl items-center justify-center mb-3 shadow" style={{ background: '#059669' }}>
<Scissors size={20} className="text-white" />
<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>
<h1 className="text-xl font-bold text-slate-900">Bem-vindo de volta</h1>
<p className="text-sm text-slate-500 mt-0.5">Aceda à sua conta SmartAgenda</p>
<h1 className="text-2xl font-bold text-slate-900">Entrar</h1>
<p className="text-sm text-slate-600">Aceda à sua conta</p>
</div>
<div className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm">
{error && (
<div className="mb-4 rounded-xl bg-rose-50 border border-rose-200 px-4 py-3 text-sm text-rose-700">
{error}
</div>
)}
{error && (
<div className="rounded-lg border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
{error}
</div>
)}
<form className="space-y-4" onSubmit={handleLogin}>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1.5">Email</label>
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
placeholder="seu@email.com"
required
className="w-full px-3.5 py-2.5 rounded-xl border border-slate-200 text-sm focus:outline-none focus:ring-2 focus:border-transparent placeholder:text-slate-400"
style={{ '--tw-ring-color': '#10b981' } as any}
/>
</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
/>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1.5">Palavra-passe</label>
<div className="relative">
<input
type={showPw ? 'text' : 'password'}
value={password}
onChange={e => setPassword(e.target.value)}
placeholder="••••••••"
required
className="w-full px-3.5 py-2.5 pr-10 rounded-xl border border-slate-200 text-sm focus:outline-none focus:ring-2 focus:border-transparent placeholder:text-slate-400"
/>
<button type="button" onClick={() => setShowPw(!showPw)} className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600">
{showPw ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
</div>
<Input
label="Senha"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
/>
<button
type="submit"
disabled={loading}
className="w-full py-2.5 rounded-xl text-white text-sm font-semibold disabled:opacity-60 transition-all flex items-center justify-center gap-2"
style={{ background: loading ? '#6ee7b7' : '#059669' }}
>
<LogIn size={15} />
{loading ? 'A entrar...' : 'Entrar'}
</button>
</form>
<Button type="submit" className="w-full" size="lg" disabled={loading}>
{loading ? 'A entrar...' : 'Entrar'}
</Button>
</form>
<p className="text-center text-sm text-slate-500 mt-5">
<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="font-semibold hover:underline" style={{ color: '#059669' }}>
Criar conta grátis
<Link to="/registo" className="text-amber-700 font-semibold hover:text-amber-800 transition-colors">
Criar conta
</Link>
</p>
</div>
</div>
</Card>
</div>
)
}

View File

@@ -1,5 +1,14 @@
/**
* @file AuthRegister.tsx
* @description Página de Registo para a versão Web. Permite a criação de
* novas contas segmentadas por perfil ('cliente' ou 'barbearia'). Interage
* diretamente com o serviço de autenticação do Supabase.
*/
import { FormEvent, useEffect, useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { Input } from '../components/ui/input'
import { Button } from '../components/ui/button'
import { Card } from '../components/ui/card'
import { UserPlus, User, Scissors } from 'lucide-react'
import { supabase } from '../lib/supabase'
@@ -7,32 +16,58 @@ export default function AuthRegister() {
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
// Estado local para definir as permissões associadas à conta nova
const [role, setRole] = useState<'cliente' | 'barbearia'>('cliente')
// Condicional, só inserido no metadados se o tipo de utilizador for 'barbearia'
const [shopName, setShopName] = useState('')
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const navigate = useNavigate()
/**
* Previne acesso à página de registo por utilizadores com Sessão ativa.
* Utiliza um padrão `mounted` para não alterar state se o componente for desmontado.
*/
// 🔐 Se já estiver logado, não pode ver o registo
useEffect(() => {
let mounted = true
; (async () => {
// Efetua um fetch à sessão existente injetada pelo SDK da Supabase
const { data } = await supabase.auth.getSession()
if (!mounted) return
if (data.session) navigate('/explorar', { replace: true })
if (data.session) {
navigate('/explorar', { replace: true })
}
})()
return () => { mounted = false }
return () => {
mounted = false
}
}, [navigate])
/**
* Processa a criação e envio do novo perfil e das informações para a BD via Supabase Auth.
* @param {FormEvent} e - Evento de submissão do formulário.
*/
async function onSubmit(e: FormEvent) {
e.preventDefault()
setError('')
// Regras básicas de negócio à submissão (proteção pre-API)
if (!name.trim()) return setError('Preencha o nome completo')
if (!email.trim()) return setError('Preencha o email')
if (!password.trim()) return setError('Preencha a senha')
if (role === 'barbearia' && !shopName.trim()) return setError('Informe o nome da barbearia')
if (role === 'barbearia' && !shopName.trim()) {
return setError('Informe o nome da barbearia')
}
try {
setLoading(true)
// Registo propriamente dito perante serviço do Supabase
// Encaminha, adicionalmente, opções nos metadados (Data) da auth do serviço para mapeamento da "profile" table na BD
const { data, error } = await supabase.auth.signUp({
email,
password,
@@ -40,17 +75,31 @@ export default function AuthRegister() {
data: {
name: name.trim(),
role,
// Apenas vincula shopName no json object se a rule aplicável confirmar que a role o exige
// Envia ambas as formatações (camelCase para web, snake_case para o trigger SQL)
shopName: role === 'barbearia' ? shopName.trim() : null,
shop_name: role === 'barbearia' ? shopName.trim() : null,
},
},
})
if (error) throw error
// 🔔 Se confirmação de email estiver ON (verificação pendente via SMTP em Supabase Configs)
if (!data.session) {
navigate('/login', { replace: true, state: { msg: 'Conta criada! Confirme o email antes de fazer login.' } })
navigate('/login', {
replace: true,
state: {
msg: 'Conta criada! Confirma o email antes de fazer login.',
},
})
return
}
navigate(role === 'barbearia' ? '/painel' : '/explorar', { replace: true })
// ✅ Login automático: Rotas divergentes de acordo com a função Role
navigate(role === 'barbearia' ? '/painel' : '/explorar', {
replace: true,
})
} catch (err: any) {
setError(err?.message || 'Erro ao criar conta')
} finally {
@@ -58,91 +107,115 @@ export default function AuthRegister() {
}
}
const field = (label: string, node: React.ReactNode) => (
<div>
<label className="block text-sm font-medium text-slate-700 mb-1.5">{label}</label>
{node}
</div>
)
const inputCls = "w-full px-3.5 py-2.5 rounded-xl border border-slate-200 text-sm focus:outline-none placeholder:text-slate-400"
return (
<div className="flex items-center justify-center py-10 px-4">
<div className="w-full" style={{ maxWidth: 420 }}>
{/* Logo */}
<div className="text-center mb-7">
<div className="inline-flex w-11 h-11 rounded-xl items-center justify-center mb-3 shadow" style={{ background: '#059669' }}>
<UserPlus size={20} className="text-white" />
<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>
<h1 className="text-xl font-bold text-slate-900">Criar conta</h1>
<p className="text-sm text-slate-500 mt-0.5">Escolha o seu perfil e comece </p>
<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>
<div className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm">
{error && (
<div className="mb-4 rounded-xl bg-rose-50 border border-rose-200 px-4 py-3 text-sm text-rose-700">
{error}
{error && (
<div className="rounded-lg border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
{error}
</div>
)}
<form className="space-y-5" onSubmit={onSubmit}>
{/* Tipo de conta */}
<div className="space-y-2">
<label className="text-sm font-medium text-slate-700">
Tipo de conta
</label>
<div className="grid grid-cols-2 gap-3">
{(['cliente', 'barbearia'] as const).map((r) => (
<button
key={r}
type="button"
onClick={() => {
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'
}`}
>
<div className="flex flex-col items-center gap-2">
{r === 'cliente' ? (
<User size={20} className={role === r ? 'text-amber-600' : 'text-slate-400'} />
) : (
<Scissors size={20} className={role === r ? 'text-amber-600' : 'text-slate-400'} />
)}
<span className="text-sm font-semibold">
{r === 'cliente' ? 'Cliente' : 'Barbearia'}
</span>
</div>
</button>
))}
</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' && (
<Input
label="Nome da barbearia"
value={shopName}
onChange={(e) => setShopName(e.target.value)}
placeholder="Barbearia XPTO"
required
/>
)}
<form className="space-y-4" onSubmit={onSubmit}>
{/* Tipo de conta */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">Tipo de conta</label>
<div className="grid grid-cols-2 gap-2">
{(['cliente', 'barbearia'] as const).map(r => (
<button
key={r}
type="button"
onClick={() => { setRole(r); setError('') }}
className="flex flex-col items-center gap-1.5 py-3 px-2 rounded-xl border-2 transition-all text-sm font-medium"
style={{
borderColor: role === r ? '#059669' : '#e2e8f0',
background: role === r ? '#f0fdf4' : '#fff',
color: role === r ? '#065f46' : '#64748b',
}}
>
{r === 'cliente'
? <User size={18} style={{ color: role === r ? '#059669' : '#94a3b8' }} />
: <Scissors size={18} style={{ color: role === r ? '#059669' : '#94a3b8' }} />
}
{r === 'cliente' ? 'Cliente' : 'Barbearia'}
</button>
))}
</div>
</div>
<Button type="submit" className="w-full" size="lg" disabled={loading}>
{loading ? 'A criar conta…' : 'Criar conta'}
</Button>
</form>
{field('Nome completo',
<input value={name} onChange={e => setName(e.target.value)} placeholder="João Silva" required className={inputCls} />
)}
{field('Email',
<input type="email" value={email} onChange={e => setEmail(e.target.value)} placeholder="seu@email.com" required className={inputCls} />
)}
{field('Palavra-passe',
<input type="password" value={password} onChange={e => setPassword(e.target.value)} placeholder="••••••••" required className={inputCls} />
)}
{role === 'barbearia' && field('Nome da barbearia',
<input value={shopName} onChange={e => setShopName(e.target.value)} placeholder="Barbearia XPTO" required className={inputCls} />
)}
<button
type="submit"
disabled={loading}
className="w-full py-2.5 rounded-xl text-white text-sm font-semibold disabled:opacity-60 transition-all"
style={{ background: loading ? '#6ee7b7' : '#059669' }}
>
{loading ? 'A criar conta...' : 'Criar conta'}
</button>
</form>
<p className="text-center text-sm text-slate-500 mt-5">
<div className="text-center pt-4 border-t border-slate-200">
<p className="text-sm text-slate-600">
tem conta?{' '}
<Link to="/login" className="font-semibold hover:underline" style={{ color: '#059669' }}>
<Link
to="/login"
className="text-amber-700 font-semibold hover:text-amber-800"
>
Entrar
</Link>
</p>
</div>
</div>
</Card>
</div>
)
}

View File

@@ -1,117 +1,150 @@
/**
* @file Explore.tsx
*/
import { useMemo, useState } from 'react';
import { ShopCard } from '../components/ShopCard';
import { useApp } from '../context/AppContext';
import { Search, Heart, X, SlidersHorizontal } from 'lucide-react';
import { Search, Heart, Compass, SlidersHorizontal } from 'lucide-react';
export default function Explore() {
const { shops, favorites } = useApp();
const [query, setQuery] = useState('');
const [filter, setFilter] = useState<'todas' | 'top' | 'produtos' | 'favoritas'>('todas');
const [filter, setFilter] = useState<'todas' | 'top' | 'produtos' | 'barbeiros' | 'favoritas'>('todas');
const [sortBy, setSortBy] = useState<'relevancia' | 'avaliacao' | 'preco' | 'servicos'>('relevancia');
const favoriteShops = useMemo(() => shops.filter((s) => favorites.includes(s.id)), [shops, favorites]);
const filtered = useMemo(() => {
const norm = query.trim().toLowerCase();
const matchQ = (n: string, a: string) => !norm || n.toLowerCase().includes(norm) || a.toLowerCase().includes(norm);
const matchF = (s: (typeof shops)[0]) => {
if (filter === 'favoritas') return favorites.includes(s.id);
if (filter === 'top') return (s.rating || 0) >= 4;
if (filter === 'produtos') return (s.products || []).length > 0;
const normalized = query.trim().toLowerCase();
const matchesQuery = (name: string, address: string) =>
!normalized || name.toLowerCase().includes(normalized) || address.toLowerCase().includes(normalized);
const passesFilter = (shop: (typeof shops)[number]) => {
if (filter === 'favoritas') return favorites.includes(shop.id);
if (filter === 'top') return (shop.rating || 0) >= 4;
if (filter === 'produtos') return (shop.products || []).length > 0;
if (filter === 'barbeiros') return (shop.barbers || []).length >= 2;
return true;
};
return [...shops].filter(s => matchQ(s.name, s.address || '')).filter(matchF).sort((a, b) => {
if (sortBy === 'avaliacao') return (b.rating || 0) - (a.rating || 0);
if (sortBy === 'servicos') return (b.services || []).length - (a.services || []).length;
if (sortBy === 'preco') {
const aMin = a.services?.length ? Math.min(...a.services.map(s => s.price)) : Infinity;
const bMin = b.services?.length ? Math.min(...b.services.map(s => s.price)) : Infinity;
return aMin - bMin;
}
return (b.rating || 0) - (a.rating || 0);
});
return [...shops]
.filter((s) => matchesQuery(s.name, s.address || ''))
.filter(passesFilter)
.sort((a, b) => {
if (sortBy === 'avaliacao') return (b.rating || 0) - (a.rating || 0);
if (sortBy === 'servicos') return (b.services || []).length - (a.services || []).length;
if (sortBy === 'preco') {
const aMin = a.services?.length ? Math.min(...a.services.map((s) => s.price)) : Infinity;
const bMin = b.services?.length ? Math.min(...b.services.map((s) => s.price)) : Infinity;
return aMin - bMin;
}
if (b.rating !== a.rating) return (b.rating || 0) - (a.rating || 0);
return (b.services || []).length - (a.services || []).length;
});
}, [shops, query, filter, sortBy, favorites]);
const chips = [
{ id: 'todas' as const, label: 'Todas' },
{ id: 'favoritas' as const, label: `❤️ Favoritas${favorites.length > 0 ? ` (${favorites.length})` : ''}` },
{ id: 'top' as const, label: '⭐ Melhor avaliadas' },
{ id: 'produtos' as const, label: '🛍️ Com produtos' },
const chips: { id: typeof filter; label: string; icon?: React.ReactNode }[] = [
{ id: 'todas', label: 'Todas' },
{ id: 'favoritas', label: `Favoritas${favorites.length > 0 ? ` (${favorites.length})` : ''}`, icon: <Heart size={12} className="fill-current" /> },
{ id: 'top', label: 'Top avaliadas' },
{ id: 'produtos', label: 'Com produtos' },
{ id: 'barbeiros', label: 'Mais barbeiros' },
];
return (
<div className="space-y-5 max-w-6xl mx-auto">
{/* Header */}
<div>
<h1 className="text-2xl font-bold text-slate-900">Barbearias</h1>
<p className="text-sm text-slate-500 mt-0.5">Encontre e agende na sua barbearia favorita</p>
</div>
{/* Search */}
<div className="relative">
<Search size={15} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-400" />
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Pesquisar barbearia ou endereço..."
className="w-full pl-10 pr-9 py-2.5 rounded-xl border border-slate-200 bg-white text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent placeholder:text-slate-400"
/>
{query && (
<button onClick={() => setQuery('')} className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600">
<X size={14} />
</button>
)}
</div>
{/* Filters */}
<div className="flex items-center gap-2 flex-wrap">
{chips.map(chip => (
<button
key={chip.id}
onClick={() => setFilter(chip.id)}
className={`px-3.5 py-1.5 rounded-full text-xs font-semibold border transition-all ${filter === chip.id
? 'bg-emerald-600 text-white border-emerald-600'
: 'bg-white text-slate-600 border-slate-200 hover:border-emerald-300 hover:text-emerald-700'
}`}
>
{chip.label}
</button>
))}
<div className="ml-auto flex items-center gap-1.5">
<SlidersHorizontal size={13} className="text-slate-400" />
<select
value={sortBy}
onChange={e => setSortBy(e.target.value as typeof sortBy)}
className="text-xs border border-slate-200 rounded-lg px-2.5 py-1.5 bg-white text-slate-700 focus:outline-none focus:ring-2 focus:ring-emerald-500"
>
<option value="relevancia">Relevância</option>
<option value="avaliacao">Melhor avaliação</option>
<option value="preco">Menor preço</option>
<option value="servicos">Mais serviços</option>
</select>
<div className="space-y-6">
{/* Hero Header */}
<div className="relative rounded-2xl overflow-hidden bg-gradient-to-br from-slate-900 via-slate-800 to-amber-900 p-6 md:p-8">
<div className="absolute inset-0 opacity-10 bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-amber-400 to-transparent" />
<div className="relative">
<div className="flex items-center gap-2 mb-2">
<Compass size={18} className="text-amber-400" />
<span className="text-amber-400 text-xs font-semibold uppercase tracking-widest">Explorar</span>
</div>
<h1 className="text-2xl md:text-3xl font-black text-white mb-1">Barbearias</h1>
<p className="text-slate-400 text-sm">Encontre a sua favorita e agende em minutos.</p>
</div>
</div>
{/* Count */}
<p className="text-xs text-slate-500">
<span className="font-semibold text-slate-700">{filtered.length}</span>{' '}
{filtered.length === 1 ? 'barbearia' : 'barbearias'}
</p>
{/* Search & Sort bar */}
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-4 space-y-3">
<div className="flex gap-3">
<div className="relative flex-1">
<Search size={16} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-400" />
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Pesquisar por nome ou endereço..."
className="w-full pl-10 pr-4 py-2.5 rounded-xl border border-slate-200 text-sm focus:outline-none focus:ring-2 focus:ring-amber-400 focus:border-transparent placeholder:text-slate-400"
/>
</div>
<div className="flex items-center gap-2">
<SlidersHorizontal size={15} className="text-slate-400" />
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as typeof sortBy)}
className="border border-slate-200 rounded-xl px-3 py-2.5 text-sm text-slate-700 focus:outline-none focus:ring-2 focus:ring-amber-400 bg-white"
>
<option value="relevancia">Relevância</option>
<option value="avaliacao">Melhor avaliação</option>
<option value="preco">Menor preço</option>
<option value="servicos">Mais serviços</option>
</select>
</div>
</div>
{/* Filter chips */}
<div className="flex gap-2 flex-wrap">
{chips.map((chip) => (
<button
key={chip.id}
onClick={() => setFilter(chip.id)}
className={`flex items-center gap-1.5 px-3.5 py-1.5 rounded-full text-xs font-semibold transition-all ${filter === chip.id
? chip.id === 'favoritas'
? 'bg-rose-500 text-white shadow-sm'
: 'bg-amber-500 text-white shadow-sm'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
}`}
>
{chip.icon}
{chip.label}
</button>
))}
</div>
</div>
{/* Results count */}
<div className="flex items-center justify-between px-1">
<p className="text-sm text-slate-500">
<span className="font-semibold text-slate-900">{filtered.length}</span>{' '}
{filtered.length === 1 ? 'barbearia encontrada' : 'barbearias encontradas'}
</p>
</div>
{/* Grid */}
{filtered.length === 0 ? (
<div className="text-center py-16 bg-white rounded-xl border border-slate-200">
<div className="w-14 h-14 bg-slate-100 rounded-full flex items-center justify-center mx-auto mb-3">
{filter === 'favoritas' ? <Heart size={24} className="text-slate-400" /> : <Search size={24} className="text-slate-400" />}
<div className="text-center py-16 bg-white rounded-2xl border border-slate-100">
<div className="w-16 h-16 bg-slate-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
{filter === 'favoritas'
? <Heart size={28} className="text-slate-400" />
: <Search size={28} className="text-slate-400" />
}
</div>
<p className="font-semibold text-slate-700 text-sm">
{filter === 'favoritas' ? 'Sem favoritas ainda' : 'Nenhuma barbearia encontrada'}
<p className="font-semibold text-slate-700 mb-1">
{filter === 'favoritas' ? 'Ainda não tem favoritas' : 'Nenhuma barbearia encontrada'}
</p>
<p className="text-xs text-slate-400 mt-1">
{filter === 'favoritas' ? 'Clique no ❤️ de qualquer barbearia.' : 'Tente ajustar os filtros.'}
<p className="text-sm text-slate-400">
{filter === 'favoritas'
? 'Clique no ❤️ em qualquer barbearia para a guardar aqui.'
: 'Tente ajustar a pesquisa ou os filtros.'}
</p>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{filtered.map(shop => <ShopCard key={shop.id} shop={shop} />)}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{filtered.map((shop) => (
<ShopCard key={shop.id} shop={shop} />
))}
</div>
)}
</div>

View File

@@ -1,7 +1,22 @@
/**
* @file Landing.tsx
* @description Página de destino (Landing Page) da aplicação web.
* Serve como vitrine promocional e porta de entrada (Login/Registo - Call to Actions).
* Redireciona autonomamente utilizadores já autenticados para as suas áreas reservadas.
*/
import { Link, useNavigate } from 'react-router-dom';
import { Calendar, ShoppingBag, BarChart3, Sparkles, Users, Clock, Shield, ArrowRight, Star, Scissors, CheckCircle2, Smartphone, TrendingUp } from 'lucide-react';
import { Button } from '../components/ui/button';
import { Card } from '../components/ui/card';
import { ShopCard } from '../components/ShopCard';
import {
Calendar, ShoppingBag, BarChart3, Sparkles,
Users, Clock, Shield, TrendingUp, CheckCircle2,
ArrowRight, Star, Quote, Scissors, MapPin,
Zap, Smartphone, Globe
} from 'lucide-react';
import { useEffect } from 'react';
import { useApp } from '../context/AppContext';
import { mockShops } from '../data/mock';
export default function Landing() {
const { user } = useApp();
@@ -9,185 +24,314 @@ export default function Landing() {
useEffect(() => {
if (!user) return;
navigate(user.role === 'barbearia' ? '/painel' : '/explorar', { replace: true });
const target = user.role === 'barbearia' ? '/painel' : '/explorar';
navigate(target, { replace: true });
}, [user, navigate]);
return (
<div className="space-y-20 pb-12">
{/* Hero */}
<section
className="relative overflow-hidden rounded-2xl text-white px-6 py-16 md:px-14 md:py-24"
style={{ background: 'linear-gradient(135deg, #0f172a 0%, #134e4a 50%, #0f172a 100%)' }}
>
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_left,_theme(colors.emerald.900/60),_transparent_60%)]" />
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom_right,_theme(colors.teal.900/40),_transparent_60%)]" />
const featuredShops = mockShops.slice(0, 3);
<div className="relative max-w-3xl">
<div className="inline-flex items-center gap-2 px-3 py-1.5 bg-emerald-500/20 border border-emerald-500/30 rounded-full text-emerald-300 text-xs font-semibold mb-6">
<Sparkles size={13} />
Plataforma de gestão para barbearias
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="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>
<h1 className="text-4xl md:text-6xl font-black leading-tight mb-5">
Agendamentos e gestão{' '}
<span className="text-emerald-400">num único lugar</span>
<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>
<p className="text-lg text-slate-300 max-w-xl leading-relaxed mb-8">
A plataforma completa para barbearias e clientes agende, gira e cresce sem complicações.
<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>
<div className="flex flex-wrap gap-3">
<Link
to="/explorar"
className="inline-flex items-center gap-2 px-6 py-3 rounded-xl bg-emerald-500 text-white font-semibold text-sm hover:bg-emerald-400 transition-colors shadow-lg"
>
Explorar barbearias <ArrowRight size={16} />
</Link>
<Link
to="/registo"
className="inline-flex items-center gap-2 px-6 py-3 rounded-xl bg-white/10 border border-white/20 text-white font-semibold text-sm hover:bg-white/20 transition-colors"
>
Criar conta grátis
</Link>
<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
<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>
</div>
{/* Stats */}
<div className="flex flex-wrap gap-8 mt-12 pt-8 border-t border-white/10">
{[['500+', 'Barbearias'], ['10k+', 'Agendamentos'], ['4.8 ⭐', 'Avaliação média']].map(([v, l]) => (
<div key={l}>
<div className="text-2xl font-bold">{v}</div>
<div className="text-xs text-slate-400 mt-0.5">{l}</div>
</div>
))}
<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>
</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>
<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>
</div>
</div>
</section>
{/* Features */}
{/* Features Grid */}
<section>
<div className="text-center mb-10">
<h2 className="text-3xl font-bold text-slate-900 mb-2">Tudo o que precisa</h2>
<p className="text-slate-500">Funcionalidades para clientes e barbearias</p>
<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>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{[
{ icon: Calendar, title: 'Agendamentos', desc: 'Escolha serviço, barbeiro e horário em segundos. Acompanhe em tempo real.', color: 'text-emerald-600 bg-emerald-50' },
{ icon: ShoppingBag, title: 'Loja de Produtos', desc: 'Compre produtos da barbearia diretamente na plataforma. Entrega ou levantamento.', color: 'text-violet-600 bg-violet-50' },
{ icon: BarChart3, title: 'Painel de Gestão', desc: 'Relatórios, pedidos e agendamentos num painel completo e intuitivo.', color: 'text-sky-600 bg-sky-50' },
{ icon: Users, title: 'Gestão de Barbeiros', desc: 'Gira horários, especialidades e disponibilidade de cada barbeiro.', color: 'text-teal-600 bg-teal-50' },
{ icon: Clock, title: 'Horários Flexíveis', desc: 'Configure o funcionamento, intervalos e disponibilidade da sua barbearia.', color: 'text-indigo-600 bg-indigo-50' },
{ icon: Shield, title: 'Seguro e Fiável', desc: 'Dados protegidos com RLS e autenticação Supabase. Conformidade RGPD.', color: 'text-rose-600 bg-rose-50' },
].map(f => (
<div key={f.title} className="bg-white rounded-xl border border-slate-200 p-5 hover:border-emerald-200 hover:shadow-sm transition-all">
<div className={`inline-flex p-2.5 rounded-xl ${f.color} mb-3`}>
<f.icon size={20} />
{
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: 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'
},
{
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'
},
].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} />
</div>
<h3 className="font-bold text-slate-900 mb-1">{f.title}</h3>
<p className="text-sm text-slate-500 leading-relaxed">{f.desc}</p>
<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>
</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>
<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>
<h3 className="text-xl font-bold text-slate-900">{item.title}</h3>
<p className="text-slate-600">{item.desc}</p>
</div>
))}
</div>
</section>
{/* How it works */}
<section className="bg-slate-900 rounded-2xl p-8 md:p-12 text-white">
<div className="text-center mb-10">
<h2 className="text-3xl font-bold mb-2">Como funciona</h2>
<p className="text-slate-400">Simples e rápido em 3 passos</p>
{/* 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
</h2>
<p className="text-slate-600">
Conheça algumas das melhores barbearias da plataforma
</p>
</div>
<Button asChild variant="ghost" className="hidden md:flex">
<Link to="/explorar" className="flex items-center gap-2">
Ver todas
<ArrowRight size={16} />
</Link>
</Button>
</div>
<div className="grid md:grid-cols-3 gap-8">
{[
{ n: '01', t: 'Explore', d: 'Navegue pelas barbearias, veja avaliações e serviços disponíveis.' },
{ n: '02', t: 'Agende', d: 'Escolha o serviço, barbeiro e horário que se encaixa na sua agenda.' },
{ n: '03', t: 'Aproveite', d: 'Compareça no horário e desfrute de um serviço de qualidade.' },
].map(s => (
<div key={s.n} className="text-center">
<div className="text-4xl font-black text-emerald-500 mb-3">{s.n}</div>
<h3 className="text-lg font-bold mb-2">{s.t}</h3>
<p className="text-slate-400 text-sm leading-relaxed">{s.d}</p>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{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-6">
{[
{
icon: Smartphone, color: 'text-emerald-600 bg-emerald-50',
title: 'Mobile-First',
desc: 'Interface otimizada para telemóvel. Agende de qualquer lugar, a qualquer hora.',
items: ['Design responsivo', 'Carregamento rápido', 'Interface intuitiva'],
check: 'text-emerald-600'
},
{
icon: TrendingUp, color: 'text-violet-600 bg-violet-50',
title: 'Aumente a Receita',
desc: 'Ferramentas de análise para gerir e fazer crescer o seu negócio.',
items: ['Análises em tempo real', 'Gestão de stock', 'Relatórios detalhados'],
check: 'text-violet-600'
},
].map(b => (
<div key={b.title} className="bg-white border border-slate-200 rounded-2xl p-7">
<div className={`inline-flex p-3 rounded-xl ${b.color} mb-4`}>
<b.icon size={24} />
</div>
<h3 className="text-xl font-bold text-slate-900 mb-2">{b.title}</h3>
<p className="text-slate-500 text-sm mb-5 leading-relaxed">{b.desc}</p>
<ul className="space-y-2">
{b.items.map(i => (
<li key={i} className="flex items-center gap-2 text-sm text-slate-700">
<CheckCircle2 size={16} className={b.check} />
{i}
</li>
))}
</ul>
<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>
<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-8">
<h2 className="text-3xl font-bold text-slate-900 mb-2">O que dizem os clientes</h2>
<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-4">
<div className="grid md:grid-cols-3 gap-6">
{[
{ name: 'João Silva', role: 'Cliente', text: 'Facilita muito agendar o corte. Interface simples e rápida. Recomendo!' },
{ name: 'Carlos Mendes', role: 'Proprietário', text: 'O painel é completo e ajudou muito na organização da barbearia.' },
{ name: 'Miguel Santos', role: 'Cliente', text: 'Nunca mais perco horário. As notificações são muito úteis.' },
].map(t => (
<div key={t.name} className="bg-white border border-slate-200 rounded-xl p-5">
<div className="flex gap-0.5 mb-3">
{[...Array(5)].map((_, i) => <Star key={i} size={13} className="fill-yellow-400 text-yellow-400" />)}
{
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>
<p className="text-slate-600 text-sm leading-relaxed mb-4">"{t.text}"</p>
<div className="border-t border-slate-100 pt-3">
<p className="font-semibold text-slate-900 text-sm">{t.name}</p>
<p className="text-xs text-slate-400">{t.role}</p>
<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>
</div>
</Card>
))}
</div>
</section>
{/* CTA */}
<section
className="rounded-2xl text-white px-6 py-14 md:px-12 text-center"
style={{ background: 'linear-gradient(135deg, #0f172a 0%, #134e4a 50%, #0f172a 100%)' }}
>
<div className="inline-flex w-12 h-12 rounded-2xl items-center justify-center mb-5" style={{ background: 'rgba(16,185,129,0.2)' }}>
<Scissors size={22} className="text-emerald-400" />
{/* 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>
</div>
<h2 className="text-3xl md:text-4xl font-bold mb-3">Pronto para começar?</h2>
<p style={{ color: '#94a3b8' }} className="max-w-lg mx-auto mb-8">
Junte-se a centenas de barbearias que usam a SmartAgenda.
</p>
<Link to="/registo" className="inline-flex items-center gap-2 px-6 py-3 rounded-xl font-semibold text-sm transition-colors" style={{ background: '#10b981', color: '#fff' }}>
Criar conta grátis <ArrowRight size={16} />
</Link>
</section>
</div>
);
}