Refactor: Update header component with a new visual design, color scheme, and minor code simplifications.
This commit is contained in:
@@ -3,19 +3,17 @@ import { Star, MapPin, Scissors, Heart, Calendar, Users } from 'lucide-react';
|
||||
import { BarberShop } from '../types';
|
||||
import { useApp } from '../context/AppContext';
|
||||
|
||||
// Paleta de gradientes para barbearias sem foto
|
||||
const gradients = [
|
||||
'from-emerald-600 to-teal-700',
|
||||
'from-violet-600 to-indigo-700',
|
||||
'from-amber-500 to-orange-600',
|
||||
'from-emerald-500 to-teal-700',
|
||||
'from-rose-500 to-pink-700',
|
||||
'from-sky-500 to-blue-700',
|
||||
'from-slate-600 to-slate-800',
|
||||
'from-sky-600 to-blue-700',
|
||||
'from-rose-600 to-pink-700',
|
||||
'from-teal-500 to-cyan-700',
|
||||
];
|
||||
|
||||
function shopGradient(name: string) {
|
||||
const idx = name.charCodeAt(0) % gradients.length;
|
||||
return gradients[idx];
|
||||
return gradients[name.charCodeAt(0) % gradients.length];
|
||||
}
|
||||
|
||||
export const ShopCard = ({ shop }: { shop: BarberShop }) => {
|
||||
@@ -24,77 +22,67 @@ export const ShopCard = ({ shop }: { shop: BarberShop }) => {
|
||||
const initials = shop.name.slice(0, 2).toUpperCase();
|
||||
|
||||
return (
|
||||
<div className="group flex flex-col bg-white rounded-2xl border border-slate-100 hover:border-amber-200 shadow-sm hover:shadow-md transition-all duration-200 overflow-hidden">
|
||||
{/* Top accent strip + avatar */}
|
||||
<div className="flex items-start gap-4 p-4">
|
||||
<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-16 h-16 rounded-xl bg-gradient-to-br ${shopGradient(shop.name)} overflow-hidden flex items-center justify-center shadow-md`}>
|
||||
<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-xl">{initials}</span>
|
||||
<span className="text-white font-black text-lg select-none">{initials}</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Rating pill */}
|
||||
<div className="absolute -bottom-1.5 -right-1.5 bg-white border border-slate-100 rounded-full px-1.5 py-0.5 flex items-center gap-0.5 shadow-sm">
|
||||
<Star size={9} className="fill-amber-400 text-amber-400" />
|
||||
{/* 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>
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
{/* Name + address */}
|
||||
<div className="flex-1 min-w-0 pt-0.5">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<h2 className="font-bold text-slate-900 text-base leading-tight group-hover:text-amber-700 transition-colors truncate">
|
||||
<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>
|
||||
{/* Favorite */}
|
||||
<button
|
||||
onClick={(e) => { e.preventDefault(); toggleFavorite(shop.id); }}
|
||||
className={`flex-shrink-0 p-1 rounded-full transition-all ${favorite ? 'text-rose-500 bg-rose-50' : 'text-slate-300 hover:text-rose-400 hover:bg-rose-50'
|
||||
}`}
|
||||
className={`flex-shrink-0 p-1 rounded-full transition-all ${favorite ? 'text-rose-500' : 'text-slate-300 hover:text-rose-400'}`}
|
||||
>
|
||||
<Heart size={15} className={favorite ? 'fill-rose-500' : ''} />
|
||||
<Heart size={14} className={favorite ? 'fill-rose-500' : ''} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 mt-1 text-slate-500">
|
||||
<MapPin size={11} className="flex-shrink-0 text-amber-500" />
|
||||
<p className="text-xs truncate">{shop.address || 'Endereço não disponível'}</p>
|
||||
<div 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-2 text-xs text-slate-400 font-medium">
|
||||
<span className="flex items-center gap-1">
|
||||
<Scissors size={10} />
|
||||
{(shop.services || []).length} serviços
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Users size={10} />
|
||||
{(shop.barbers || []).length} barbeiros
|
||||
</span>
|
||||
<div 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Separator */}
|
||||
{/* Divider */}
|
||||
<div className="h-px bg-slate-100 mx-4" />
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2 p-3">
|
||||
<Link
|
||||
to={`/barbearia/${shop.id}`}
|
||||
className="flex-1 text-center text-xs font-semibold py-2 px-3 rounded-xl border border-slate-200 text-slate-600 hover:border-amber-300 hover:text-amber-700 hover:bg-amber-50 transition-all"
|
||||
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-2 px-3 rounded-xl bg-amber-500 hover:bg-amber-600 text-white transition-all flex items-center justify-center gap-1.5 shadow-sm"
|
||||
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={11} />
|
||||
<Calendar size={10} />
|
||||
Agendar
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Link, useNavigate, useLocation } from 'react-router-dom'
|
||||
import { ShoppingCart, User, LogOut, Menu, X, Scissors, Search } from 'lucide-react'
|
||||
import { ShoppingCart, User, LogOut, Menu, X, Scissors } from 'lucide-react'
|
||||
import { useApp } from '../../context/AppContext'
|
||||
import { useState } from 'react'
|
||||
|
||||
@@ -7,54 +7,40 @@ export const Header = () => {
|
||||
const { user, cart, logout } = useApp()
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const handleLogout = () => {
|
||||
logout()
|
||||
navigate('/')
|
||||
setMobileMenuOpen(false)
|
||||
}
|
||||
const handleLogout = () => { logout(); navigate('/'); setOpen(false) }
|
||||
|
||||
const navLink = (to: string) =>
|
||||
`flex items-center gap-1.5 text-sm font-medium transition-colors px-3 py-1.5 rounded-lg ${location.pathname === to
|
||||
? 'bg-amber-50 text-amber-700'
|
||||
: 'text-slate-600 hover:text-amber-700 hover:bg-amber-50'
|
||||
}`
|
||||
const isActive = (path: string) => location.pathname === path
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-30 bg-white/90 backdrop-blur-md border-b border-slate-100 shadow-sm">
|
||||
<div className="mx-auto flex h-15 max-w-6xl items-center justify-between px-4 py-3">
|
||||
<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">
|
||||
{/* Logo */}
|
||||
<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" />
|
||||
<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" />
|
||||
</div>
|
||||
<span className="text-lg font-black text-slate-900 tracking-tight">Smart<span className="text-amber-500">Agenda</span></span>
|
||||
<span className="text-base font-bold text-slate-900 tracking-tight">Smart<span className="text-emerald-600">Agenda</span></span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
{/* Desktop nav */}
|
||||
<nav className="hidden md:flex items-center gap-1">
|
||||
{user?.role !== 'barbearia' && (
|
||||
<>
|
||||
<Link to="/explorar" className={navLink('/explorar')}>
|
||||
<Search size={15} />
|
||||
<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'}`}
|
||||
>
|
||||
Barbearias
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to="/carrinho"
|
||||
className={`relative p-2 rounded-lg transition-colors ${location.pathname === '/carrinho'
|
||||
? 'text-amber-700 bg-amber-50'
|
||||
: 'text-slate-600 hover:text-amber-700 hover:bg-amber-50'
|
||||
}`}
|
||||
className={`relative p-2 rounded-lg transition-colors ${isActive('/carrinho') ? 'text-emerald-700 bg-emerald-50' : 'text-slate-500 hover:bg-slate-100'}`}
|
||||
>
|
||||
<ShoppingCart size={18} />
|
||||
{cart.length > 0 && (
|
||||
<span className="absolute -right-1 -top-1 rounded-full bg-amber-500 px-1.5 py-0.5 text-[10px] font-bold text-white shadow-sm min-w-[18px] text-center leading-none flex items-center justify-center">
|
||||
<span className="absolute -right-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">
|
||||
{cart.length}
|
||||
</span>
|
||||
)}
|
||||
@@ -66,19 +52,17 @@ 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={navLink(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"
|
||||
type="button"
|
||||
>
|
||||
<div className="w-6 h-6 rounded-full bg-gradient-to-br from-amber-400 to-amber-600 flex items-center justify-center text-white text-xs font-bold">
|
||||
<div className="w-6 h-6 rounded-full bg-emerald-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} />
|
||||
@@ -87,85 +71,62 @@ export const Header = () => {
|
||||
) : (
|
||||
<Link
|
||||
to="/login"
|
||||
className="ml-2 inline-flex items-center gap-1.5 px-4 py-2 rounded-xl bg-gradient-to-r from-amber-500 to-orange-500 text-white text-sm font-semibold shadow-sm hover:shadow-md hover:from-amber-600 hover:to-orange-600 transition-all"
|
||||
className="ml-2 px-4 py-1.5 rounded-lg bg-emerald-600 text-white text-sm font-semibold hover:bg-emerald-700 transition-colors"
|
||||
>
|
||||
Entrar
|
||||
</Link>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
{/* Mobile right area */}
|
||||
{/* Mobile */}
|
||||
<div className="flex items-center gap-2 md:hidden">
|
||||
{user?.role !== 'barbearia' && (
|
||||
<Link to="/carrinho" className="relative p-2 text-slate-600">
|
||||
<ShoppingCart size={20} />
|
||||
<Link to="/carrinho" className="relative p-1.5 text-slate-600">
|
||||
<ShoppingCart size={19} />
|
||||
{cart.length > 0 && (
|
||||
<span className="absolute -right-0.5 -top-0.5 rounded-full bg-amber-500 w-4 h-4 text-[10px] font-bold text-white flex items-center justify-center">
|
||||
<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">
|
||||
{cart.length}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
className="p-2 text-slate-700 hover:text-amber-600 hover:bg-amber-50 rounded-lg transition-colors"
|
||||
type="button"
|
||||
>
|
||||
{mobileMenuOpen ? <X size={20} /> : <Menu size={20} />}
|
||||
<button 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu */}
|
||||
{mobileMenuOpen && (
|
||||
<div className="md:hidden border-t border-slate-100 bg-white/98 backdrop-blur-md">
|
||||
<nav className="px-4 py-3 space-y-1">
|
||||
{/* Mobile menu */}
|
||||
{open && (
|
||||
<div className="md:hidden border-t border-slate-200 bg-white">
|
||||
<div className="px-4 py-3 space-y-1">
|
||||
{user?.role !== 'barbearia' && (
|
||||
<Link
|
||||
to="/explorar"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="flex items-center gap-2 text-sm font-medium text-slate-700 hover:text-amber-700 px-3 py-2.5 rounded-xl hover:bg-amber-50 transition-colors"
|
||||
>
|
||||
<Search size={16} />
|
||||
<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">
|
||||
Barbearias
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{user ? (
|
||||
<>
|
||||
<button
|
||||
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"
|
||||
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"
|
||||
type="button"
|
||||
>
|
||||
<div className="w-6 h-6 rounded-full bg-gradient-to-br from-amber-400 to-amber-600 flex items-center justify-center text-white text-xs font-bold">
|
||||
<div className="w-6 h-6 rounded-full bg-emerald-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 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 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>
|
||||
</>
|
||||
) : (
|
||||
<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"
|
||||
>
|
||||
<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">
|
||||
Entrar
|
||||
</Link>
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap');
|
||||
@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');
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@@ -14,16 +14,14 @@
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
font-family: 'Inter', system-ui, 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;
|
||||
}
|
||||
@@ -35,19 +33,4 @@
|
||||
::-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,120 +1,106 @@
|
||||
/**
|
||||
* @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 { Input } from '../components/ui/input'
|
||||
import { Button } from '../components/ui/button'
|
||||
import { Card } from '../components/ui/card'
|
||||
import { LogIn } from 'lucide-react'
|
||||
import { LogIn, Scissors, Eye, EyeOff } 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 {
|
||||
// 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,
|
||||
})
|
||||
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
|
||||
if (role) {
|
||||
navigate(role === 'barbearia' ? '/painel' : '/explorar', { replace: true })
|
||||
}
|
||||
} catch (e: any) {
|
||||
setError('Credenciais inválidas ou email não confirmado')
|
||||
if (role) navigate(role === 'barbearia' ? '/painel' : '/explorar', { replace: true })
|
||||
} catch {
|
||||
setError('Email ou palavra-passe incorretos.')
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
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-4">
|
||||
<div className="w-full max-w-sm">
|
||||
{/* Logo top */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="inline-flex w-12 h-12 bg-emerald-600 rounded-2xl items-center justify-center mb-4 shadow-md">
|
||||
<Scissors size={22} className="text-white" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-slate-900">Entrar</h1>
|
||||
<p className="text-sm text-slate-600">Aceda à sua conta</p>
|
||||
<h1 className="text-2xl font-bold text-slate-900">Bem-vindo de volta</h1>
|
||||
<p className="text-sm text-slate-500 mt-1">Aceda à sua conta SmartAgenda</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-lg border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
|
||||
{error}
|
||||
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6">
|
||||
{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>
|
||||
)}
|
||||
|
||||
<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:ring-emerald-500 focus:border-transparent placeholder:text-slate-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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:ring-emerald-500 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>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full py-2.5 rounded-xl bg-emerald-600 text-white text-sm font-semibold hover:bg-emerald-700 disabled:opacity-60 transition-colors flex items-center justify-center gap-2 mt-2"
|
||||
>
|
||||
<LogIn size={16} />
|
||||
{loading ? 'A entrar...' : 'Entrar'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="text-center mt-5 pt-5 border-t border-slate-100">
|
||||
<p className="text-sm text-slate-500">
|
||||
Não tem conta?{' '}
|
||||
<Link to="/registo" className="text-emerald-700 font-semibold hover:text-emerald-800">
|
||||
Criar conta grátis
|
||||
</Link>
|
||||
</p>
|
||||
</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
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Senha"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
required
|
||||
/>
|
||||
|
||||
<Button type="submit" className="w-full" size="lg" disabled={loading}>
|
||||
{loading ? 'A entrar...' : 'Entrar'}
|
||||
</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
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,106 +1,86 @@
|
||||
/**
|
||||
* @file Explore.tsx
|
||||
*/
|
||||
import { useMemo, useState } from 'react';
|
||||
import { ShopCard } from '../components/ShopCard';
|
||||
import { useApp } from '../context/AppContext';
|
||||
import { Search, Heart, Scissors, SlidersHorizontal, X } from 'lucide-react';
|
||||
import { Search, Heart, X, 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 [sortBy, setSortBy] = useState<'relevancia' | 'avaliacao' | 'preco' | 'servicos'>('relevancia');
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
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;
|
||||
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;
|
||||
return true;
|
||||
};
|
||||
|
||||
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;
|
||||
});
|
||||
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);
|
||||
});
|
||||
}, [shops, query, filter, sortBy, favorites]);
|
||||
|
||||
const chips: { id: typeof filter; label: string }[] = [
|
||||
{ id: 'todas', label: 'Todas' },
|
||||
{ id: 'favoritas', label: `❤️ Favoritas${favorites.length > 0 ? ` (${favorites.length})` : ''}` },
|
||||
{ id: 'top', label: '⭐ Top avaliadas' },
|
||||
{ id: 'produtos', label: '🛍️ Com produtos' },
|
||||
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' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{/* Page header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-0.5">
|
||||
<Scissors size={16} className="text-amber-500" />
|
||||
<span className="text-xs font-semibold uppercase tracking-widest text-slate-400">Explorar</span>
|
||||
</div>
|
||||
<h1 className="text-2xl font-black text-slate-900">Barbearias</h1>
|
||||
</div>
|
||||
<span className="text-sm text-slate-500 font-medium">
|
||||
{filtered.length} {filtered.length === 1 ? 'resultado' : 'resultados'}
|
||||
</span>
|
||||
<div 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 bar */}
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<Search size={16} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-400" />
|
||||
<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 por nome ou endereço..."
|
||||
className="w-full pl-10 pr-10 py-3 rounded-2xl border border-slate-200 bg-white text-sm focus:outline-none focus:ring-2 focus:ring-amber-400 focus:border-transparent placeholder:text-slate-400 shadow-sm"
|
||||
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.5 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600">
|
||||
<X size={15} />
|
||||
<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 row */}
|
||||
{/* Filters */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{chips.map((chip) => (
|
||||
{chips.map(chip => (
|
||||
<button
|
||||
key={chip.id}
|
||||
onClick={() => setFilter(chip.id)}
|
||||
className={`px-4 py-2 rounded-full text-xs font-semibold transition-all ${filter === chip.id
|
||||
? 'bg-amber-500 text-white shadow-sm'
|
||||
: 'bg-white text-slate-600 border border-slate-200 hover:border-amber-300 hover:text-amber-700'
|
||||
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-2">
|
||||
<SlidersHorizontal size={14} className="text-slate-400" />
|
||||
<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="border border-slate-200 rounded-xl px-3 py-2 text-xs font-medium text-slate-700 bg-white focus:outline-none focus:ring-2 focus:ring-amber-400"
|
||||
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>
|
||||
@@ -110,26 +90,28 @@ export default function Explore() {
|
||||
</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>
|
||||
|
||||
{/* Grid */}
|
||||
{filtered.length === 0 ? (
|
||||
<div className="text-center py-16 bg-white rounded-2xl border border-slate-100">
|
||||
<div className="w-16 h-16 bg-slate-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
{filter === 'favoritas' ? <Heart size={28} className="text-slate-400" /> : <Search size={28} className="text-slate-400" />}
|
||||
<div 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>
|
||||
<p className="font-semibold text-slate-700 mb-1">
|
||||
{filter === 'favoritas' ? 'Ainda não tem favoritas' : 'Nenhuma barbearia encontrada'}
|
||||
<p className="font-semibold text-slate-700 text-sm">
|
||||
{filter === 'favoritas' ? 'Sem favoritas ainda' : 'Nenhuma barbearia encontrada'}
|
||||
</p>
|
||||
<p className="text-sm text-slate-400">
|
||||
{filter === 'favoritas'
|
||||
? 'Clique no ❤️ de qualquer barbearia para guardar aqui.'
|
||||
: 'Tente ajustar os filtros ou a pesquisa.'}
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
{filter === 'favoritas' ? 'Clique no ❤️ de qualquer barbearia.' : 'Tente ajustar os filtros.'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<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 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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,22 +1,7 @@
|
||||
/**
|
||||
* @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 { 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 { Calendar, ShoppingBag, BarChart3, Sparkles, Users, Clock, Shield, ArrowRight, Star, Scissors, CheckCircle2, Smartphone, TrendingUp } from 'lucide-react';
|
||||
import { useEffect } from 'react';
|
||||
import { useApp } from '../context/AppContext';
|
||||
import { mockShops } from '../data/mock';
|
||||
|
||||
export default function Landing() {
|
||||
const { user } = useApp();
|
||||
@@ -24,314 +9,184 @@ export default function Landing() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
const target = user.role === 'barbearia' ? '/painel' : '/explorar';
|
||||
navigate(target, { replace: true });
|
||||
navigate(user.role === 'barbearia' ? '/painel' : '/explorar', { replace: true });
|
||||
}, [user, navigate]);
|
||||
|
||||
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-20 pb-12">
|
||||
{/* Hero */}
|
||||
<section className="relative overflow-hidden rounded-2xl bg-slate-900 text-white px-6 py-16 md:px-14 md:py-24">
|
||||
<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%)]" />
|
||||
|
||||
<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 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
|
||||
</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-4xl md:text-6xl font-black leading-tight mb-5">
|
||||
Agendamentos e gestão{' '}
|
||||
<span className="text-emerald-400">num único lugar</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-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>
|
||||
|
||||
<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 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>
|
||||
|
||||
{/* 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>
|
||||
</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 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>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Features Grid */}
|
||||
{/* Features */}
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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-4">
|
||||
{[
|
||||
{
|
||||
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} />
|
||||
{ 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} />
|
||||
</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>
|
||||
</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>
|
||||
<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>
|
||||
))}
|
||||
</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
|
||||
</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>
|
||||
{/* 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>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{featuredShops.map((shop) => (
|
||||
<ShopCard key={shop.id} shop={shop} />
|
||||
<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>
|
||||
|
||||
<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} />
|
||||
<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>
|
||||
</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-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 className="text-center mb-8">
|
||||
<h2 className="text-3xl font-bold text-slate-900 mb-2">O que dizem os clientes</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
<div className="grid md:grid-cols-3 gap-4">
|
||||
{[
|
||||
{
|
||||
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" />
|
||||
))}
|
||||
{ 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" />)}
|
||||
</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>
|
||||
<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>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Final */}
|
||||
<section className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white px-6 py-16 md:px-12 md:py-20 shadow-2xl">
|
||||
<div className="absolute top-0 right-0 w-96 h-96 bg-indigo-500/10 rounded-full blur-3xl"></div>
|
||||
<div className="absolute bottom-0 left-0 w-96 h-96 bg-blue-500/10 rounded-full blur-3xl"></div>
|
||||
|
||||
<div className="relative text-center space-y-8 max-w-3xl mx-auto">
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-balance">
|
||||
Pronto para começar?
|
||||
</h2>
|
||||
<p className="text-xl text-slate-300 max-w-2xl mx-auto">
|
||||
Junte-se a centenas de barbearias que já estão usando a Smart Agenda
|
||||
para revolucionar seus negócios.
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-4 pt-4">
|
||||
<Button asChild size="lg" className="text-base px-8 py-4 bg-white text-slate-900 hover:bg-slate-100">
|
||||
<Link to="/registo" className="flex items-center gap-2">
|
||||
Criar conta grátis
|
||||
<ArrowRight size={18} />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg" className="text-base px-8 py-4 border-white/30 text-white hover:bg-white/10">
|
||||
<Link to="/explorar">Explorar agora</Link>
|
||||
</Button>
|
||||
</div>
|
||||
{/* CTA */}
|
||||
<section className="rounded-2xl bg-slate-900 text-white px-6 py-14 md:px-12 text-center">
|
||||
<div className="inline-flex w-12 h-12 bg-emerald-500/20 rounded-2xl items-center justify-center mb-5">
|
||||
<Scissors size={22} className="text-emerald-400" />
|
||||
</div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-3">Pronto para começar?</h2>
|
||||
<p className="text-slate-400 max-w-lg mx-auto mb-8">
|
||||
Junte-se a centenas de barbearias que já usam a SmartAgenda.
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-3">
|
||||
<Link to="/registo" 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">
|
||||
Criar conta grátis <ArrowRight size={16} />
|
||||
</Link>
|
||||
<Link to="/explorar" 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">
|
||||
Explorar barbearias
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user