+
{user?.role !== 'barbearia' && (
-
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"
- >
-
+
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
)}
-
{user ? (
<>
)}
diff --git a/web/src/index.css b/web/src/index.css
index daada6f..48ce529 100644
--- a/web/src/index.css
+++ b/web/src/index.css
@@ -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;
- }
-}
+}
\ No newline at end of file
diff --git a/web/src/pages/AuthLogin.tsx b/web/src/pages/AuthLogin.tsx
index 3a0c254..1eea857 100644
--- a/web/src/pages/AuthLogin.tsx
+++ b/web/src/pages/AuthLogin.tsx
@@ -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 (
-
-
-
-
-
+
+
+ {/* Logo top */}
+
+
+
-
Entrar
-
Aceda à sua conta
+
Bem-vindo de volta
+
Aceda à sua conta SmartAgenda
- {error && (
-
- {error}
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+ Não tem conta?{' '}
+
+ Criar conta grátis
+
+
- )}
-
-
-
-
-
- Não tem conta?{' '}
-
- Criar conta
-
-
-
+
)
}
diff --git a/web/src/pages/Explore.tsx b/web/src/pages/Explore.tsx
index ace7f3b..447909a 100644
--- a/web/src/pages/Explore.tsx
+++ b/web/src/pages/Explore.tsx
@@ -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 (
-
- {/* Page header */}
-
-
-
-
- Explorar
-
-
Barbearias
-
-
- {filtered.length} {filtered.length === 1 ? 'resultado' : 'resultados'}
-
+
+ {/* Header */}
+
+
Barbearias
+
Encontre e agende na sua barbearia favorita
- {/* Search bar */}
+ {/* Search */}
-
+
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 && (
- setQuery('')} className="absolute right-3.5 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600">
-
+ setQuery('')} className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600">
+
)}
- {/* Filters row */}
+ {/* Filters */}
- {chips.map((chip) => (
+ {chips.map(chip => (
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}
))}
-
-
-
+
+
+ {/* Count */}
+
+ {filtered.length}{' '}
+ {filtered.length === 1 ? 'barbearia' : 'barbearias'}
+
+
{/* Grid */}
{filtered.length === 0 ? (
-
-
- {filter === 'favoritas' ?
:
}
+
+
+ {filter === 'favoritas' ? : }
-
- {filter === 'favoritas' ? 'Ainda não tem favoritas' : 'Nenhuma barbearia encontrada'}
+
+ {filter === 'favoritas' ? 'Sem favoritas ainda' : 'Nenhuma barbearia encontrada'}
-
- {filter === 'favoritas'
- ? 'Clique no ❤️ de qualquer barbearia para guardar aqui.'
- : 'Tente ajustar os filtros ou a pesquisa.'}
+
+ {filter === 'favoritas' ? 'Clique no ❤️ de qualquer barbearia.' : 'Tente ajustar os filtros.'}
) : (
-
- {filtered.map((shop) => (
-
- ))}
+
+ {filtered.map(shop => )}
)}
diff --git a/web/src/pages/Landing.tsx b/web/src/pages/Landing.tsx
index ad5396a..09c9cf7 100644
--- a/web/src/pages/Landing.tsx
+++ b/web/src/pages/Landing.tsx
@@ -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 (
-
- {/* Hero Section */}
-
-
-
-
+
+ {/* Hero */}
+
+
+
-
-
-
-
Revolucione sua barbearia
+
+
+
+ Plataforma de gestão para barbearias
-
- Agendamentos, produtos e gestão em um{' '}
- único lugar
+
+ Agendamentos e gestão{' '}
+ num único lugar
-
- Experiência mobile-first para clientes e painel completo para barbearias.
- Simplifique a gestão do seu negócio e aumente sua receita.
+
+ A plataforma completa para barbearias e clientes — agende, gira e cresce sem complicações.
-
-
-
- Explorar barbearias
-
-
-
-
- Criar conta grátis
-
+
+
+ Explorar barbearias
+
+
+ Criar conta grátis
+
{/* Stats */}
-
-
-
-
-
4.8
-
Avaliação média
-
+
+ {[['500+', 'Barbearias'], ['10k+', 'Agendamentos'], ['4.8 ⭐', 'Avaliação média']].map(([v, l]) => (
+
+ ))}
- {/* Features Grid */}
+ {/* Features */}
-
-
- Tudo que você precisa
-
-
- Funcionalidades poderosas para clientes e barbearias
-
+
+
Tudo o que precisa
+
Funcionalidades para clientes e barbearias
-
-
+
{[
- {
- 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) => (
-
-
-
+ { 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 => (
+
+
+
-
-
{feature.title}
-
{feature.desc}
-
-
- ))}
-
-
-
- {/* How it Works */}
-
-
-
- Como funciona
-
-
- Simples, rápido e eficiente em 3 passos
-
-
-
-
- {[
- { 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) => (
-
-
- {item.step}
-
-
{item.title}
-
{item.desc}
+
{f.title}
+
{f.desc}
))}
- {/* Featured Shops */}
-
-
-
-
- Barbearias em destaque
-
-
- Conheça algumas das melhores barbearias da plataforma
-
-
-
-
- Ver todas
-
-
-
+ {/* How it works */}
+
+
+
Como funciona
+
Simples e rápido em 3 passos
-
-
- {featuredShops.map((shop) => (
-
+
+ {[
+ { 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 => (
+
+
{s.n}
+
{s.t}
+
{s.d}
+
))}
-
-
-
- Ver todas as barbearias
-
-
{/* Benefits */}
-
-
-
-
+
+ {[
+ {
+ 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 => (
+
+
+
+
+
{b.title}
+
{b.desc}
+
+ {b.items.map(i => (
+ -
+
+ {i}
+
+ ))}
+
-
- Mobile-First
-
-
- Interface otimizada para dispositivos móveis. Agende de qualquer lugar,
- a qualquer hora. Experiência fluida e responsiva.
-
-
- {['Design responsivo', 'Carregamento rápido', 'Interface intuitiva'].map((item) => (
- -
-
- {item}
-
- ))}
-
-
-
-
-
-
-
-
- Aumente sua Receita
-
-
- Ferramentas poderosas para gerenciar seu negócio. Análises detalhadas,
- gestão de estoque e muito mais.
-
-
- {['Análises em tempo real', 'Gestão de estoque', 'Relatórios detalhados'].map((item) => (
- -
-
- {item}
-
- ))}
-
-
+ ))}
{/* Testimonials */}
-
-
- O que nossos clientes dizem
-
-
- Depoimentos reais de quem usa a plataforma
-
+
+
O que dizem os clientes
-
-
+
{[
- {
- 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) => (
-
-
- {[...Array(testimonial.rating)].map((_, i) => (
-
- ))}
+ { 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 => (
+
+
+ {[...Array(5)].map((_, i) => )}
-
-
{testimonial.text}
-
-
{testimonial.name}
-
{testimonial.role}
+
"{t.text}"
+
-
+
))}
- {/* CTA Final */}
-
-
-
-
-
-
- Pronto para começar?
-
-
- Junte-se a centenas de barbearias que já estão usando a Smart Agenda
- para revolucionar seus negócios.
-
-
-
-
- Criar conta grátis
-
-
-
-
- Explorar agora
-
-
+ {/* CTA */}
+
+
+
+
+ Pronto para começar?
+
+ Junte-se a centenas de barbearias que já usam a SmartAgenda.
+
+
+
+ Criar conta grátis
+
+
+ Explorar barbearias
+
);
}
-
-
-
-
-