-
+
+
+
+
+
-
Bem-vindo de volta
-
Aceda à sua conta SmartAgenda
+
Entrar
+
Aceda à sua conta
-
- {error && (
-
- {error}
-
- )}
+ {error && (
+
+ {error}
+
+ )}
-
+
+
-
+
+
Não tem conta?{' '}
-
- Criar conta grátis
+
+ Criar conta
-
+
)
}
diff --git a/web/src/pages/AuthRegister.tsx b/web/src/pages/AuthRegister.tsx
index 129d264..9d2a5a8 100644
--- a/web/src/pages/AuthRegister.tsx
+++ b/web/src/pages/AuthRegister.tsx
@@ -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) => (
-
-
- {node}
-
- )
- 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 (
-
-
- {/* Logo */}
-
-
-
+
+
+
+
+
-
Criar conta
-
Escolha o seu perfil e comece já
+
+ Criar conta
+
+
+ Escolha o tipo de acesso
+
-
- {error && (
-
- {error}
+ {error && (
+
+ {error}
+
+ )}
+
+
- {field('Nome completo',
-
setName(e.target.value)} placeholder="João Silva" required className={inputCls} />
- )}
- {field('Email',
-
setEmail(e.target.value)} placeholder="seu@email.com" required className={inputCls} />
- )}
- {field('Palavra-passe',
-
setPassword(e.target.value)} placeholder="••••••••" required className={inputCls} />
- )}
- {role === 'barbearia' && field('Nome da barbearia',
-
setShopName(e.target.value)} placeholder="Barbearia XPTO" required className={inputCls} />
- )}
-
-
-
-
-
+
+
Já tem conta?{' '}
-
+
Entrar
-
+
)
}
diff --git a/web/src/pages/Explore.tsx b/web/src/pages/Explore.tsx
index 447909a..bb72084 100644
--- a/web/src/pages/Explore.tsx
+++ b/web/src/pages/Explore.tsx
@@ -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: },
+ { id: 'top', label: 'Top avaliadas' },
+ { id: 'produtos', label: 'Com produtos' },
+ { id: 'barbeiros', label: 'Mais barbeiros' },
];
return (
-
- {/* Header */}
-
-
Barbearias
-
Encontre e agende na sua barbearia favorita
-
-
- {/* Search */}
-
-
- 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 && (
-
- )}
-
-
- {/* Filters */}
-
- {chips.map(chip => (
-
- ))}
-
-
-
+
+ {/* Hero Header */}
+
+
+
+
+
+ Explorar
+
+
Barbearias
+
Encontre a sua favorita e agende em minutos.
- {/* Count */}
-
- {filtered.length}{' '}
- {filtered.length === 1 ? 'barbearia' : 'barbearias'}
-
+ {/* Search & Sort bar */}
+
+
+
+
+ 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"
+ />
+
+
+
+
+
+
+
+ {/* Filter chips */}
+
+ {chips.map((chip) => (
+
+ ))}
+
+
+
+ {/* Results count */}
+
+
+ {filtered.length}{' '}
+ {filtered.length === 1 ? 'barbearia encontrada' : 'barbearias encontradas'}
+
+
{/* Grid */}
{filtered.length === 0 ? (
-
-
- {filter === 'favoritas' ?
:
}
+
+
+ {filter === 'favoritas'
+ ?
+ :
+ }
-
- {filter === 'favoritas' ? 'Sem favoritas ainda' : 'Nenhuma barbearia encontrada'}
+
+ {filter === 'favoritas' ? 'Ainda não tem favoritas' : 'Nenhuma barbearia encontrada'}
-
- {filter === 'favoritas' ? 'Clique no ❤️ de qualquer barbearia.' : 'Tente ajustar os filtros.'}
+
+ {filter === 'favoritas'
+ ? 'Clique no ❤️ em qualquer barbearia para a guardar aqui.'
+ : 'Tente ajustar a pesquisa ou os filtros.'}
) : (
-
- {filtered.map(shop =>
)}
+
+ {filtered.map((shop) => (
+
+ ))}
)}
diff --git a/web/src/pages/Landing.tsx b/web/src/pages/Landing.tsx
index beb6a21..ad5396a 100644
--- a/web/src/pages/Landing.tsx
+++ b/web/src/pages/Landing.tsx
@@ -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 (
-
- {/* Hero */}
-
-
-
+ const featuredShops = mockShops.slice(0, 3);
-
-
-
- Plataforma de gestão para barbearias
+ return (
+
+ {/* Hero Section */}
+
+
+
+
+
+
+
+
+ Revolucione sua barbearia
-
- Agendamentos e gestão{' '}
- num único lugar
+
+ Agendamentos, produtos e gestão em um{' '}
+ único lugar
-
- A plataforma completa para barbearias e clientes — agende, gira e cresce sem complicações.
+
+ Experiência mobile-first para clientes e painel completo para barbearias.
+ Simplifique a gestão do seu negócio e aumente sua receita.
-
-
- Explorar barbearias
-
-
- Criar conta grátis
-
+
+
+
{/* Stats */}
-
- {[['500+', 'Barbearias'], ['10k+', 'Agendamentos'], ['4.8 ⭐', 'Avaliação média']].map(([v, l]) => (
-
- ))}
+
+
+
+
+
4.8
+
Avaliação média
+
- {/* Features */}
+ {/* Features Grid */}
-
-
Tudo o que precisa
-
Funcionalidades para clientes e barbearias
+
+
+ Tudo que você precisa
+
+
+ Funcionalidades poderosas para clientes e barbearias
+
-
+
+
{[
- { 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 => (
-
-
-
+ {
+ 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) => (
+
+
+
- {f.title}
- {f.desc}
+
+
{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}
))}
- {/* How it works */}
-
-
-
Como funciona
-
Simples e rápido em 3 passos
+ {/* Featured Shops */}
+
+
+
+
+ Barbearias em destaque
+
+
+ Conheça algumas das melhores barbearias da plataforma
+
+
+
-
- {[
- { 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}
-
+
+
+ {featuredShops.map((shop) => (
+
))}
+
+
+
+
{/* 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 dizem os clientes
+
+
+ O que nossos clientes dizem
+
+
+ Depoimentos reais de quem usa a plataforma
+
-
+
+
{[
- { 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) =>
)}
+ {
+ 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) => (
+
+ ))}
- "{t.text}"
-
-
{t.name}
-
{t.role}
+
+
{testimonial.text}
+
+
{testimonial.name}
+
{testimonial.role}
-
+
))}
- {/* CTA */}
-
-
-
+ {/* 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.
+
+
+
+
+
- Pronto para começar?
-
- Junte-se a centenas de barbearias que já usam a SmartAgenda.
-
-
- Criar conta grátis
-
);
}
+
+
+
+
+