first commit

This commit is contained in:
2026-01-07 10:35:00 +00:00
parent 13745ac89e
commit 3c7190bca4
53 changed files with 5538 additions and 531 deletions

162
web/OPCOES_CORES.md Normal file
View File

@@ -0,0 +1,162 @@
# Opções de Cores para a Landing Page
Este documento lista diferentes paletas de cores que podem ser aplicadas à aplicação.
## 🎨 Paleta Atual: Azul/Indigo
**Status:** ✅ Aplicada
- **Primária:** Indigo 500-600
- **Secundária:** Blue 600-700
- **Background:** Blue 50/30
- **Estilo:** Profissional, moderno, confiável
**Cores principais:**
- Hero: `from-indigo-600 via-blue-600 to-indigo-700`
- Botões: `from-indigo-500 to-blue-600`
- Hover: `indigo-50`, `indigo-600`
---
## 🟡 Opção 1: Âmbar/Amarelo (Original)
**Status:** Disponível
- **Primária:** Amber 500-600
- **Secundária:** Amber 600-700
- **Background:** Amber 50/30
- **Estilo:** Quente, acolhedor, energético
**Cores principais:**
- Hero: `from-amber-500 via-amber-600 to-amber-700`
- Botões: `from-amber-500 to-amber-600`
- Hover: `amber-50`, `amber-600`
---
## 🟢 Opção 2: Verde/Emerald
**Status:** Disponível
- **Primária:** Emerald 500-600
- **Secundária:** Green 600-700
- **Background:** Emerald 50/30
- **Estilo:** Natural, fresco, crescimento
**Cores principais:**
- Hero: `from-emerald-500 via-emerald-600 to-emerald-700`
- Botões: `from-emerald-500 to-emerald-600`
- Hover: `emerald-50`, `emerald-600`
**Substituições necessárias:**
- `indigo``emerald`
- `blue``green`
- `indigo-50``emerald-50`
---
## 🟣 Opção 3: Roxo/Violet
**Status:** Disponível
- **Primária:** Purple 500-600
- **Secundária:** Violet 600-700
- **Background:** Purple 50/30
- **Estilo:** Criativo, luxuoso, inovador
**Cores principais:**
- Hero: `from-purple-500 via-purple-600 to-purple-700`
- Botões: `from-purple-500 to-purple-600`
- Hover: `purple-50`, `purple-600`
**Substituições necessárias:**
- `indigo``purple`
- `blue``violet`
- `indigo-50``purple-50`
---
## 🔴 Opção 4: Vermelho/Rose
**Status:** Disponível
- **Primária:** Rose 500-600
- **Secundária:** Red 600-700
- **Background:** Rose 50/30
- **Estilo:** Vibrante, apaixonado, dinâmico
**Cores principais:**
- Hero: `from-rose-500 via-rose-600 to-rose-700`
- Botões: `from-rose-500 to-rose-600`
- Hover: `rose-50`, `rose-600`
**Substituições necessárias:**
- `indigo``rose`
- `blue``red`
- `indigo-50``rose-50`
---
## 🔵 Opção 5: Ciano/Sky
**Status:** Disponível
- **Primária:** Sky 500-600
- **Secundária:** Cyan 600-700
- **Background:** Sky 50/30
- **Estilo:** Fresco, moderno, tecnológico
**Cores principais:**
- Hero: `from-sky-500 via-sky-600 to-sky-700`
- Botões: `from-sky-500 to-sky-600`
- Hover: `sky-50`, `sky-600`
**Substituições necessárias:**
- `indigo``sky`
- `blue``cyan`
- `indigo-50``sky-50`
---
## 🟠 Opção 6: Laranja/Orange
**Status:** Disponível
- **Primária:** Orange 500-600
- **Secundária:** Amber 600-700
- **Background:** Orange 50/30
- **Estilo:** Energético, entusiasta, criativo
**Cores principais:**
- Hero: `from-orange-500 via-orange-600 to-orange-700`
- Botões: `from-orange-500 to-orange-600`
- Hover: `orange-50`, `orange-600`
**Substituições necessárias:**
- `indigo``orange`
- `blue``amber`
- `indigo-50``orange-50`
---
## 📝 Como Aplicar uma Nova Paleta
Para aplicar uma nova paleta, você precisa substituir as cores nos seguintes arquivos:
1. **`src/pages/Landing.tsx`** - Hero section, seções, CTAs
2. **`src/components/ui/button.tsx`** - Variantes de botões
3. **`src/components/layout/Header.tsx`** - Navegação e links
4. **`src/components/ShopCard.tsx`** - Cards de barbearias
5. **`src/index.css`** - Background do body
6. **`src/components/ui/card.tsx`** - Hover states
### Buscar e Substituir:
- `indigo-500``[nova-cor]-500`
- `indigo-600``[nova-cor]-600`
- `indigo-50``[nova-cor]-50`
- `blue-600``[cor-secundaria]-600`
- `blue-50``[cor-secundaria]-50`
---
## 💡 Recomendações
- **Barbearias/Beleza:** Âmbar ou Laranja (quente, acolhedor)
- **Tecnologia/Profissional:** Azul/Indigo (confiável, moderno)
- **Sustentabilidade:** Verde/Emerald (natural, fresco)
- **Luxo/Criatividade:** Roxo/Violet (inovador, premium)
- **Energia/Dinamismo:** Vermelho/Rose (vibrante, apaixonado)

31
web/README.md Normal file
View File

@@ -0,0 +1,31 @@
## Smart Agenda (Web, mobile-first)
Aplicação React + TypeScript + Vite + Tailwind com React Router v6, Context API, localStorage, Recharts e lucide-react.
### Requisitos
- Node 18+ e npm
### Instalação e execução
```bash
cd web
npm install
npm run dev
```
O Vite arrancará por defeito em `http://localhost:5173`.
### Scripts úteis
- `npm run dev` — modo desenvolvimento
- `npm run build` — build de produção
- `npm run preview` — servir o build localmente
- `npm run lint` — verificação TypeScript
### Credenciais demo
- Cliente: `cliente@demo.com` / `123`
- Barbearia: `barber@demo.com` / `123`
### Notas de implementação
- Estado global via Context API com persistência em `localStorage`.
- Dados mock de barbearias/serviços/produtos já incluídos; registo de barbearia cria barbearia nova automaticamente.
- UI mobile-first em Tailwind com paleta âmbar + slate e componentes reutilizáveis (botões, cards, badges, tabs, modais base).

View File

@@ -1,11 +1,11 @@
import { Link } from 'react-router-dom';
import { useAppStore } from '../store/useAppStore';
import { currency } from '../lib/format';
import { Card } from './ui/card';
import { Button } from './ui/button';
import { useApp } from '../context/AppContext';
export const CartPanel = () => {
const { cart, shops, removeFromCart, placeOrder, user } = useAppStore();
const { cart, shops, removeFromCart, placeOrder, user } = useApp();
if (!cart.length) return <Card className="p-4">Carrinho vazio</Card>;
const grouped = cart.reduce<Record<string, typeof cart>>((acc, item) => {
@@ -16,7 +16,7 @@ export const CartPanel = () => {
const handleCheckout = (shopId: string) => {
if (!user) return;
placeOrder(user.id);
placeOrder(user.id, shopId);
};
return (
@@ -74,3 +74,5 @@ export const CartPanel = () => {

View File

@@ -1,20 +1,28 @@
import { Card } from './ui/card';
import { currency } from '../lib/format';
import { useMemo } from 'react';
import { useAppStore } from '../store/useAppStore';
import { useApp } from '../context/AppContext';
import { BarChart, Bar, CartesianGrid, XAxis, Tooltip, ResponsiveContainer } from 'recharts';
export const DashboardCards = () => {
const { orders, appointments, shops, user } = useAppStore();
type Props = { periodFilter?: (date: Date) => boolean };
export const DashboardCards = ({ periodFilter }: Props) => {
const { orders, appointments, user } = useApp();
const shopId = user?.shopId;
const filteredOrders = useMemo(
() => (shopId ? orders.filter((o) => o.shopId === shopId) : orders),
[orders, shopId]
() =>
(shopId ? orders.filter((o) => o.shopId === shopId) : orders).filter((o) =>
periodFilter ? periodFilter(new Date(o.createdAt)) : true
),
[orders, shopId, periodFilter]
);
const filteredAppts = useMemo(
() => (shopId ? appointments.filter((a) => a.shopId === shopId) : appointments),
[appointments, shopId]
() =>
(shopId ? appointments.filter((a) => a.shopId === shopId) : appointments).filter((a) =>
periodFilter ? periodFilter(new Date(a.date.replace(' ', 'T'))) : true
),
[appointments, shopId, periodFilter]
);
const total = filteredOrders.reduce((s, o) => s + o.total, 0);
@@ -65,3 +73,5 @@ export const DashboardCards = () => {

View File

@@ -1,7 +1,9 @@
import { Button } from './ui/button';
import { Card } from './ui/card';
import { Badge } from './ui/badge';
import { currency } from '../lib/format';
import { Product } from '../types';
import { Package, AlertCircle } from 'lucide-react';
export const ProductList = ({
products,
@@ -10,25 +12,43 @@ export const ProductList = ({
products: Product[];
onAdd?: (id: string) => void;
}) => (
<div className="grid md:grid-cols-2 gap-3">
{products.map((p) => (
<Card key={p.id} className="p-4 flex flex-col gap-1">
<div className="flex items-center justify-between">
<div className="font-semibold text-slate-900">{p.name}</div>
<div className="text-sm text-amber-600">{currency(p.price)}</div>
</div>
<div className="text-sm text-slate-600">Stock: {p.stock}</div>
{onAdd && (
<div className="pt-2">
<Button onClick={() => onAdd(p.id)} disabled={p.stock <= 0}>
{p.stock > 0 ? 'Adicionar' : 'Sem stock'}
</Button>
<div className="grid md:grid-cols-2 gap-4">
{products.map((p) => {
const lowStock = p.stock <= 3;
return (
<Card key={p.id} hover className={`p-5 flex flex-col gap-3 ${lowStock ? 'border-amber-300 bg-amber-50/30' : ''}`}>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<h3 className="font-bold text-slate-900 text-lg">{p.name}</h3>
{lowStock && (
<Badge color="amber" variant="solid" className="text-[10px] px-1.5 py-0">
<AlertCircle size={10} className="mr-1" />
Stock baixo
</Badge>
)}
</div>
<div className="flex items-center gap-2 text-sm text-slate-600">
<Package size={14} />
<span className={lowStock ? 'font-semibold text-amber-700' : ''}>
{p.stock} {p.stock === 1 ? 'unidade' : 'unidades'}
</span>
</div>
</div>
<div className="text-xl font-bold text-amber-600">{currency(p.price)}</div>
</div>
)}
</Card>
))}
{onAdd && (
<Button onClick={() => onAdd(p.id)} disabled={p.stock <= 0} size="sm" className="w-full" variant={lowStock ? 'solid' : 'solid'}>
{p.stock > 0 ? 'Adicionar ao carrinho' : 'Sem stock'}
</Button>
)}
</Card>
);
})}
</div>
);

View File

@@ -2,6 +2,7 @@ import { Button } from './ui/button';
import { Card } from './ui/card';
import { currency } from '../lib/format';
import { Service } from '../types';
import { Clock } from 'lucide-react';
export const ServiceList = ({
services,
@@ -10,18 +11,23 @@ export const ServiceList = ({
services: Service[];
onSelect?: (id: string) => void;
}) => (
<div className="grid md:grid-cols-2 gap-3">
<div className="grid md:grid-cols-2 gap-4">
{services.map((s) => (
<Card key={s.id} className="p-4 flex flex-col gap-1">
<div className="flex items-center justify-between">
<div className="font-semibold text-slate-900">{s.name}</div>
<div className="text-sm text-amber-600">{currency(s.price)}</div>
</div>
<div className="text-sm text-slate-600">Duração: {s.duration} min</div>
{onSelect && (
<div className="pt-2">
<Button onClick={() => onSelect(s.id)}>Selecionar</Button>
<Card key={s.id} hover className="p-5 flex flex-col gap-3 group">
<div className="flex items-start justify-between">
<div className="flex-1">
<h3 className="font-bold text-slate-900 text-lg mb-1 group-hover:text-amber-700 transition-colors">{s.name}</h3>
<div className="flex items-center gap-2 text-sm text-slate-600">
<Clock size={14} />
<span>{s.duration} minutos</span>
</div>
</div>
<div className="text-xl font-bold text-amber-600">{currency(s.price)}</div>
</div>
{onSelect && (
<Button onClick={() => onSelect(s.id)} size="sm" className="w-full">
Adicionar ao carrinho
</Button>
)}
</Card>
))}

View File

@@ -1,27 +1,41 @@
import { Link } from 'react-router-dom';
import { Star, MapPin } from 'lucide-react';
import { Star, MapPin, Scissors } from 'lucide-react';
import { BarberShop } from '../types';
import { Card } from './ui/card';
import { Button } from './ui/button';
export const ShopCard = ({ shop }: { shop: BarberShop }) => (
<Card className="p-4 space-y-2">
<div className="flex items-center justify-between">
<h2 className="text-base font-semibold text-slate-900">{shop.name}</h2>
<span className="flex items-center gap-1 text-amber-600 text-sm">
<Star size={14} />
{shop.rating}
</span>
<Card hover className="p-6 space-y-4 group">
<div className="flex items-start justify-between">
<div className="space-y-2 flex-1">
<div className="flex items-center gap-2">
<div className="p-2 bg-gradient-to-br from-indigo-500 to-blue-600 rounded-lg text-white shadow-sm">
<Scissors size={18} />
</div>
<h2 className="text-lg font-bold text-slate-900 group-hover:text-indigo-700 transition-colors">{shop.name}</h2>
</div>
<div className="flex items-center gap-4 text-sm text-slate-600">
<span className="flex items-center gap-1 font-medium">
<MapPin size={14} />
{shop.address}
</span>
<span className="flex items-center gap-1 text-indigo-600 font-semibold">
<Star size={14} className="fill-indigo-500 text-indigo-500" />
{shop.rating.toFixed(1)}
</span>
</div>
<div className="flex items-center gap-3 text-xs text-slate-500 pt-1">
<span>{shop.services.length} serviços</span>
<span></span>
<span>{shop.barbers.length} barbeiros</span>
</div>
</div>
</div>
<p className="text-sm text-slate-600 flex items-center gap-1">
<MapPin size={14} />
{shop.address}
</p>
<div className="flex gap-2">
<Button asChild>
<div className="flex gap-2 pt-2 border-t border-slate-100">
<Button asChild variant="outline" size="sm" className="flex-1">
<Link to={`/barbearia/${shop.id}`}>Ver detalhes</Link>
</Button>
<Button asChild variant="outline">
<Button asChild size="sm" className="flex-1">
<Link to={`/agendar/${shop.id}`}>Agendar</Link>
</Button>
</div>
@@ -30,3 +44,5 @@ export const ShopCard = ({ shop }: { shop: BarberShop }) => (

View File

@@ -1,46 +1,138 @@
import { Link, useNavigate } from 'react-router-dom';
import { MapPin, ShoppingCart, User } from 'lucide-react';
import { MapPin, ShoppingCart, User, LogOut, Menu, X } from 'lucide-react';
import { Button } from '../ui/button';
import { useAppStore } from '../../store/useAppStore';
import { useApp } from '../../context/AppContext';
import { useState } from 'react';
export const Header = () => {
const { user, cart } = useAppStore();
const { user, cart, logout } = useApp();
const navigate = useNavigate();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const handleLogout = () => {
logout();
navigate('/');
setMobileMenuOpen(false);
};
return (
<header className="sticky top-0 z-30 bg-white/95 backdrop-blur border-b border-slate-200">
<div className="mx-auto flex h-14 max-w-5xl items-center justify-between px-4">
<Link to="/" className="text-lg font-bold text-amber-600">
<header className="sticky top-0 z-30 bg-white/80 backdrop-blur-md border-b border-slate-200/60 shadow-sm">
<div className="mx-auto flex h-16 max-w-5xl items-center justify-between px-4">
<Link to="/" className="text-xl font-bold bg-gradient-to-r from-indigo-600 to-blue-700 bg-clip-text text-transparent hover:from-indigo-700 hover:to-blue-800 transition-all">
Smart Agenda
</Link>
<div className="flex items-center gap-3">
<Link to="/explorar" className="flex items-center gap-1 text-sm text-slate-700">
{/* Desktop Navigation */}
<nav className="hidden md:flex items-center gap-4">
<Link
to="/explorar"
className="flex items-center gap-1.5 text-sm font-medium text-slate-700 hover:text-indigo-600 transition-colors px-3 py-1.5 rounded-lg hover:bg-indigo-50"
>
<MapPin size={16} />
Barbearias
<span>Barbearias</span>
</Link>
<Link to="/carrinho" className="relative text-slate-700">
<Link
to="/carrinho"
className="relative text-slate-700 hover:text-indigo-600 transition-colors p-2 rounded-lg hover:bg-indigo-50"
>
<ShoppingCart size={18} />
{cart.length > 0 && (
<span className="absolute -right-2 -top-2 rounded-full bg-amber-500 px-1 text-[11px] font-semibold text-white">
<span className="absolute -right-1 -top-1 rounded-full bg-gradient-to-r from-indigo-500 to-blue-600 px-1.5 py-0.5 text-[10px] font-bold text-white shadow-sm min-w-[18px] text-center">
{cart.length}
</span>
)}
</Link>
{user ? (
<button onClick={() => navigate(user.role === 'barbearia' ? '/painel' : '/perfil')} className="flex items-center gap-1 text-sm text-slate-700">
<User size={16} />
{user.name}
</button>
<div className="flex items-center gap-2">
<button
onClick={() => navigate(user.role === 'barbearia' ? '/painel' : '/perfil')}
className="flex items-center gap-1.5 text-sm font-medium text-slate-700 hover:text-indigo-600 transition-colors px-3 py-1.5 rounded-lg hover:bg-indigo-50"
>
<User size={16} />
<span className="max-w-[120px] truncate">{user.name}</span>
</button>
<button
onClick={handleLogout}
className="p-2 text-slate-600 hover:text-rose-600 hover:bg-rose-50 rounded-lg transition-colors"
title="Sair"
>
<LogOut size={16} />
</button>
</div>
) : (
<Button asChild variant="outline">
<Button asChild variant="outline" size="sm">
<Link to="/login">Entrar</Link>
</Button>
)}
</div>
</nav>
{/* Mobile Menu Button */}
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="md:hidden p-2 text-slate-700 hover:text-amber-600 hover:bg-amber-50 rounded-lg transition-colors"
>
{mobileMenuOpen ? <X size={20} /> : <Menu size={20} />}
</button>
</div>
{/* Mobile Menu */}
{mobileMenuOpen && (
<div className="md:hidden border-t border-slate-200/60 bg-white/95 backdrop-blur-md animate-in slide-in-from-top">
<nav className="px-4 py-3 space-y-2">
<Link
to="/explorar"
onClick={() => setMobileMenuOpen(false)}
className="flex items-center gap-2 text-sm font-medium text-slate-700 hover:text-amber-600 transition-colors px-3 py-2 rounded-lg hover:bg-amber-50"
>
<MapPin size={16} />
Barbearias
</Link>
<Link
to="/carrinho"
onClick={() => setMobileMenuOpen(false)}
className="flex items-center gap-2 text-sm font-medium text-slate-700 hover:text-amber-600 transition-colors px-3 py-2 rounded-lg hover:bg-amber-50"
>
<ShoppingCart size={16} />
Carrinho
{cart.length > 0 && (
<span className="ml-auto rounded-full bg-amber-500 px-2 py-0.5 text-[10px] font-bold text-white">
{cart.length}
</span>
)}
</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-600 transition-colors px-3 py-2 rounded-lg hover:bg-amber-50 text-left"
>
<User size={16} />
{user.name}
</button>
<button
onClick={handleLogout}
className="w-full flex items-center gap-2 text-sm font-medium text-rose-600 hover:bg-rose-50 transition-colors px-3 py-2 rounded-lg text-left"
>
<LogOut size={16} />
Sair
</button>
</>
) : (
<Button asChild variant="solid" size="sm" className="w-full">
<Link to="/login" onClick={() => setMobileMenuOpen(false)}>Entrar</Link>
</Button>
)}
</nav>
</div>
)}
</header>
);
};

View File

@@ -1,17 +1,31 @@
import { cn } from '../../lib/cn';
type Props = { children: React.ReactNode; color?: 'amber' | 'slate' | 'green' | 'red'; className?: string };
type Props = { children: React.ReactNode; color?: 'amber' | 'slate' | 'green' | 'red' | 'blue'; className?: string; variant?: 'solid' | 'soft' };
const colorMap = {
amber: 'bg-amber-100 text-amber-700 border border-amber-200',
slate: 'bg-slate-100 text-slate-700 border border-slate-200',
green: 'bg-emerald-100 text-emerald-700 border border-emerald-200',
red: 'bg-rose-100 text-rose-700 border border-rose-200',
solid: {
amber: 'bg-amber-500 text-white',
slate: 'bg-slate-600 text-white',
green: 'bg-emerald-500 text-white',
red: 'bg-rose-500 text-white',
blue: 'bg-blue-500 text-white',
},
soft: {
amber: 'bg-amber-50 text-amber-700 border border-amber-200/60',
slate: 'bg-slate-50 text-slate-700 border border-slate-200/60',
green: 'bg-emerald-50 text-emerald-700 border border-emerald-200/60',
red: 'bg-rose-50 text-rose-700 border border-rose-200/60',
blue: 'bg-blue-50 text-blue-700 border border-blue-200/60',
},
};
export const Badge = ({ children, color = 'amber', className }: Props) => (
<span className={cn('px-2 py-1 text-xs rounded-full font-semibold', colorMap[color], className)}>{children}</span>
export const Badge = ({ children, color = 'amber', variant = 'soft', className }: Props) => (
<span className={cn('inline-flex items-center px-2.5 py-1 text-xs rounded-full font-semibold transition-colors', colorMap[variant][color], className)}>
{children}
</span>
);

View File

@@ -2,18 +2,25 @@ import { cn } from '../../lib/cn';
import React from 'react';
type Props = React.ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: 'solid' | 'outline' | 'ghost';
variant?: 'solid' | 'outline' | 'ghost' | 'danger';
size?: 'sm' | 'md' | 'lg';
asChild?: boolean;
};
export const Button = ({ className, variant = 'solid', asChild, ...props }: Props) => {
const base = 'rounded-md px-4 py-2 text-sm font-semibold transition focus:outline-none focus:ring-2 focus:ring-amber-500/40';
export const Button = ({ className, variant = 'solid', size = 'md', asChild, ...props }: Props) => {
const base = 'inline-flex items-center justify-center font-semibold transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed';
const variants = {
solid: 'bg-amber-500 text-white hover:bg-amber-600',
outline: 'border border-amber-500 text-amber-700 hover:bg-amber-50',
ghost: 'text-amber-700 hover:bg-amber-50',
solid: 'bg-gradient-to-r from-indigo-500 to-blue-600 text-white hover:from-indigo-600 hover:to-blue-700 shadow-md hover:shadow-lg focus:ring-indigo-500/50 active:scale-[0.98]',
outline: 'border-2 border-indigo-500 text-indigo-700 bg-white hover:bg-indigo-50 hover:border-indigo-600 focus:ring-indigo-500/50 active:scale-[0.98]',
ghost: 'text-indigo-700 hover:bg-indigo-50 focus:ring-indigo-500/50 active:scale-[0.98]',
danger: 'bg-gradient-to-r from-rose-500 to-rose-600 text-white hover:from-rose-600 hover:to-rose-700 shadow-md hover:shadow-lg focus:ring-rose-500/50 active:scale-[0.98]',
};
const cls = cn(base, variants[variant], className);
const sizes = {
sm: 'text-xs px-3 py-1.5 rounded-lg',
md: 'text-sm px-4 py-2.5 rounded-lg',
lg: 'text-base px-6 py-3 rounded-xl',
};
const cls = cn(base, variants[variant], sizes[size], className);
if (asChild && React.isValidElement(props.children)) {
return React.cloneElement(props.children, {
...props,

View File

@@ -1,8 +1,18 @@
import { cn } from '../../lib/cn';
export const Card = ({ children, className = '' }: { children: React.ReactNode; className?: string }) => (
<div className={cn('bg-white rounded-xl shadow-sm border border-slate-200', className)}>{children}</div>
export const Card = ({ children, className = '', hover = false }: { children: React.ReactNode; className?: string; hover?: boolean }) => (
<div
className={cn(
'bg-white rounded-xl shadow-sm border border-slate-200/60 transition-all duration-200',
hover && 'hover:shadow-md hover:border-indigo-200/80',
className
)}
>
{children}
</div>
);

View File

@@ -1,17 +1,40 @@
import { cn } from '../../lib/cn';
import React from 'react';
type Props = React.InputHTMLAttributes<HTMLInputElement>;
type Props = React.InputHTMLAttributes<HTMLInputElement> & {
label?: string;
error?: string;
};
export const Input = ({ className, label, error, ...props }: Props) => {
const input = (
<input
className={cn(
'w-full rounded-lg border transition-all duration-200 bg-white px-4 py-2.5 text-sm text-slate-900',
'placeholder:text-slate-400',
error
? 'border-rose-300 focus:border-rose-500 focus:ring-2 focus:ring-rose-500/30'
: 'border-slate-300 focus:border-amber-500 focus:outline-none focus:ring-2 focus:ring-amber-500/30',
className
)}
{...props}
/>
);
if (label || error) {
return (
<div className="space-y-1.5">
{label && <label className="block text-sm font-medium text-slate-700">{label}</label>}
{input}
{error && <p className="text-xs text-rose-600">{error}</p>}
</div>
);
}
return input;
};
export const Input = ({ className, ...props }: Props) => (
<input
className={cn(
'w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 focus:border-amber-500 focus:outline-none focus:ring-2 focus:ring-amber-500/30',
className
)}
{...props}
/>
);

View File

@@ -1,13 +1,15 @@
type Tab = { id: string; label: string };
export const Tabs = ({ tabs, active, onChange }: { tabs: Tab[]; active: string; onChange: (id: string) => void }) => (
<div className="flex gap-2 border-b border-slate-200">
<div className="flex gap-1 border-b border-slate-200 overflow-x-auto">
{tabs.map((t) => (
<button
key={t.id}
onClick={() => onChange(t.id)}
className={`px-3 py-2 text-sm font-semibold ${
active === t.id ? 'text-amber-600 border-b-2 border-amber-500' : 'text-slate-600'
className={`px-4 py-3 text-sm font-semibold transition-all whitespace-nowrap ${
active === t.id
? 'text-amber-600 border-b-2 border-amber-500 bg-amber-50/50'
: 'text-slate-600 hover:text-amber-600 hover:bg-amber-50/30'
}`}
>
{t.label}
@@ -18,3 +20,5 @@ export const Tabs = ({ tabs, active, onChange }: { tabs: Tab[]; active: string;

View File

@@ -0,0 +1,302 @@
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { nanoid } from 'nanoid';
import { Appointment, Barber, BarberShop, CartItem, Order, Product, Service, User } from '../types';
import { mockShops, mockUsers } from '../data/mock';
import { storage } from '../lib/storage';
type State = {
user?: User;
users: User[];
shops: BarberShop[];
appointments: Appointment[];
orders: Order[];
cart: CartItem[];
};
type AppContextValue = State & {
login: (email: string, password: string) => boolean;
logout: () => void;
register: (payload: Omit<User, 'id' | 'shopId'> & { shopName?: string }) => boolean;
addToCart: (item: CartItem) => void;
removeFromCart: (refId: string) => void;
clearCart: () => void;
createAppointment: (input: Omit<Appointment, 'id' | 'status' | 'total'>) => Appointment | null;
placeOrder: (customerId: string, shopId?: string) => Order | null;
updateAppointmentStatus: (id: string, status: Appointment['status']) => void;
updateOrderStatus: (id: string, status: Order['status']) => void;
addService: (shopId: string, service: Omit<Service, 'id'>) => void;
updateService: (shopId: string, service: Service) => void;
deleteService: (shopId: string, serviceId: string) => void;
addProduct: (shopId: string, product: Omit<Product, 'id'>) => void;
updateProduct: (shopId: string, product: Product) => void;
deleteProduct: (shopId: string, productId: string) => void;
addBarber: (shopId: string, barber: Omit<Barber, 'id'>) => void;
updateBarber: (shopId: string, barber: Barber) => void;
deleteBarber: (shopId: string, barberId: string) => void;
};
const initialState: State = {
user: undefined,
users: mockUsers,
shops: mockShops,
appointments: [],
orders: [],
cart: [],
};
const AppContext = createContext<AppContextValue | undefined>(undefined);
export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const [state, setState] = useState<State>(() => storage.get('smart-agenda', initialState));
useEffect(() => {
storage.set('smart-agenda', state);
}, [state]);
const login = (email: string, password: string) => {
const found = state.users.find((u) => u.email === email && u.password === password);
if (found) {
setState((s) => ({ ...s, user: found }));
return true;
}
return false;
};
const logout = () => setState((s) => ({ ...s, user: undefined }));
const register: AppContextValue['register'] = ({ shopName, ...payload }) => {
const exists = state.users.some((u) => u.email === payload.email);
if (exists) return false;
if (payload.role === 'barbearia') {
const shopId = nanoid();
const shop: BarberShop = {
id: shopId,
name: shopName || `Barbearia ${payload.name}`,
address: 'Endereço a definir',
rating: 0,
barbers: [],
services: [],
products: [],
};
const user: User = { ...payload, id: nanoid(), role: 'barbearia', shopId };
setState((s) => ({
...s,
user,
users: [...s.users, user],
shops: [...s.shops, shop],
}));
return true;
}
const user: User = { ...payload, id: nanoid(), role: 'cliente' };
setState((s) => ({
...s,
user,
users: [...s.users, user],
}));
return true;
};
const addToCart: AppContextValue['addToCart'] = (item) => {
setState((s) => {
const cart = [...s.cart];
const idx = cart.findIndex((c) => c.refId === item.refId && c.type === item.type && c.shopId === item.shopId);
if (idx >= 0) cart[idx].qty += item.qty;
else cart.push(item);
return { ...s, cart };
});
};
const removeFromCart: AppContextValue['removeFromCart'] = (refId) => {
setState((s) => ({ ...s, cart: s.cart.filter((c) => c.refId !== refId) }));
};
const clearCart = () => setState((s) => ({ ...s, cart: [] }));
const createAppointment: AppContextValue['createAppointment'] = (input) => {
const shop = state.shops.find((s) => s.id === input.shopId);
if (!shop) return null;
const svc = shop.services.find((s) => s.id === input.serviceId);
if (!svc) return null;
const exists = state.appointments.find(
(ap) => ap.barberId === input.barberId && ap.date === input.date && ap.status !== 'cancelado'
);
if (exists) return null;
const appointment: Appointment = {
...input,
id: nanoid(),
status: 'pendente',
total: svc.price,
};
setState((s) => ({ ...s, appointments: [...s.appointments, appointment] }));
return appointment;
};
const placeOrder: AppContextValue['placeOrder'] = (customerId, onlyShopId) => {
if (!state.cart.length) return null;
const grouped = state.cart.reduce<Record<string, CartItem[]>>((acc, item) => {
acc[item.shopId] = acc[item.shopId] || [];
acc[item.shopId].push(item);
return acc;
}, {});
const entries = Object.entries(grouped).filter(([shopId]) => (onlyShopId ? shopId === onlyShopId : true));
const newOrders: Order[] = entries.map(([shopId, items]) => {
const total = items.reduce((sum, item) => {
const shop = state.shops.find((s) => s.id === item.shopId);
if (!shop) return sum;
const price =
item.type === 'service'
? shop.services.find((s) => s.id === item.refId)?.price ?? 0
: shop.products.find((p) => p.id === item.refId)?.price ?? 0;
return sum + price * item.qty;
}, 0);
return {
id: nanoid(),
shopId,
customerId,
items,
total,
status: 'pendente',
createdAt: new Date().toISOString(),
};
});
setState((s) => ({ ...s, orders: [...s.orders, ...newOrders], cart: [] }));
return newOrders[0] ?? null;
};
const updateAppointmentStatus: AppContextValue['updateAppointmentStatus'] = (id, status) => {
setState((s) => ({
...s,
appointments: s.appointments.map((a) => (a.id === id ? { ...a, status } : a)),
}));
};
const updateOrderStatus: AppContextValue['updateOrderStatus'] = (id, status) => {
setState((s) => ({
...s,
orders: s.orders.map((o) => (o.id === id ? { ...o, status } : o)),
}));
};
const addService: AppContextValue['addService'] = (shopId, service) => {
const entry: Service = { ...service, id: nanoid() };
setState((s) => ({
...s,
shops: s.shops.map((shop) => (shop.id === shopId ? { ...shop, services: [...shop.services, entry] } : shop)),
}));
};
const updateService: AppContextValue['updateService'] = (shopId, service) => {
setState((s) => ({
...s,
shops: s.shops.map((shop) =>
shop.id === shopId ? { ...shop, services: shop.services.map((sv) => (sv.id === service.id ? service : sv)) } : shop
),
}));
};
const deleteService: AppContextValue['deleteService'] = (shopId, serviceId) => {
setState((s) => ({
...s,
shops: s.shops.map((shop) =>
shop.id === shopId ? { ...shop, services: shop.services.filter((sv) => sv.id !== serviceId) } : shop
),
}));
};
const addProduct: AppContextValue['addProduct'] = (shopId, product) => {
const entry: Product = { ...product, id: nanoid() };
setState((s) => ({
...s,
shops: s.shops.map((shop) => (shop.id === shopId ? { ...shop, products: [...shop.products, entry] } : shop)),
}));
};
const updateProduct: AppContextValue['updateProduct'] = (shopId, product) => {
setState((s) => ({
...s,
shops: s.shops.map((shop) =>
shop.id === shopId ? { ...shop, products: shop.products.map((p) => (p.id === product.id ? product : p)) } : shop
),
}));
};
const deleteProduct: AppContextValue['deleteProduct'] = (shopId, productId) => {
setState((s) => ({
...s,
shops: s.shops.map((shop) =>
shop.id === shopId ? { ...shop, products: shop.products.filter((p) => p.id !== productId) } : shop
),
}));
};
const addBarber: AppContextValue['addBarber'] = (shopId, barber) => {
const entry: Barber = { ...barber, id: nanoid() };
setState((s) => ({
...s,
shops: s.shops.map((shop) => (shop.id === shopId ? { ...shop, barbers: [...shop.barbers, entry] } : shop)),
}));
};
const updateBarber: AppContextValue['updateBarber'] = (shopId, barber) => {
setState((s) => ({
...s,
shops: s.shops.map((shop) =>
shop.id === shopId ? { ...shop, barbers: shop.barbers.map((b) => (b.id === barber.id ? barber : b)) } : shop
),
}));
};
const deleteBarber: AppContextValue['deleteBarber'] = (shopId, barberId) => {
setState((s) => ({
...s,
shops: s.shops.map((shop) =>
shop.id === shopId ? { ...shop, barbers: shop.barbers.filter((b) => b.id !== barberId) } : shop
),
}));
};
const value: AppContextValue = useMemo(
() => ({
...state,
login,
logout,
register,
addToCart,
removeFromCart,
clearCart,
createAppointment,
placeOrder,
updateAppointmentStatus,
updateOrderStatus,
addService,
updateService,
deleteService,
addProduct,
updateProduct,
deleteProduct,
addBarber,
updateBarber,
deleteBarber,
}),
[state]
);
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};
export const useApp = () => {
const ctx = useContext(AppContext);
if (!ctx) throw new Error('useApp deve ser usado dentro de AppProvider');
return ctx;
};

View File

@@ -2,17 +2,45 @@
@tailwind components;
@tailwind utilities;
:root {
color-scheme: light;
@layer base {
:root {
color-scheme: light;
}
* {
@apply border-slate-200;
}
body {
@apply bg-gradient-to-br from-slate-50 via-white to-blue-50/30 text-slate-900 font-sans antialiased;
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
}
a {
@apply text-inherit no-underline;
}
/* Scrollbar styling */
::-webkit-scrollbar {
@apply w-2 h-2;
}
::-webkit-scrollbar-track {
@apply bg-slate-100;
}
::-webkit-scrollbar-thumb {
@apply bg-slate-300 rounded-full hover:bg-slate-400;
}
}
body {
@apply bg-slate-50 text-slate-900 font-sans;
}
a {
@apply text-inherit no-underline;
@layer utilities {
.text-balance {
text-wrap: balance;
}
}

View File

@@ -2,12 +2,17 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
import { AppProvider } from './context/AppContext';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
<AppProvider>
<App />
</AppProvider>
</React.StrictMode>
);

View File

@@ -1,51 +1,90 @@
import { FormEvent, useState } from 'react';
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 { useAppStore } from '../store/useAppStore';
import { useApp } from '../context/AppContext';
import { LogIn, Mail, Lock } from 'lucide-react';
export default function AuthLogin() {
const [email, setEmail] = useState('cliente@demo.com');
const [password, setPassword] = useState('123');
const [error, setError] = useState('');
const login = useAppStore((s) => s.login);
const { login, user } = useApp();
const navigate = useNavigate();
useEffect(() => {
if (!user) return;
const target = user.role === 'barbearia' ? '/painel' : '/explorar';
navigate(target, { replace: true });
}, [user, navigate]);
const onSubmit = (e: FormEvent) => {
e.preventDefault();
const ok = login(email, password);
if (!ok) {
setError('Credenciais inválidas');
} else {
navigate('/');
const target = user?.role === 'barbearia' ? '/painel' : '/explorar';
navigate(target);
}
};
return (
<div className="max-w-md mx-auto">
<Card className="p-6 space-y-4">
<div>
<h1 className="text-xl font-semibold text-slate-900">Entrar</h1>
<p className="text-sm text-slate-600">Use o demo: cliente@demo.com / 123</p>
<div className="max-w-md mx-auto py-8">
<Card className="p-8 space-y-6">
<div className="text-center space-y-2">
<div className="inline-flex p-3 bg-gradient-to-br from-amber-500 to-amber-600 rounded-xl text-white shadow-lg mb-2">
<LogIn size={24} />
</div>
<h1 className="text-2xl font-bold text-slate-900">Bem-vindo de volta</h1>
<p className="text-sm text-slate-600">Entre na sua conta para continuar</p>
</div>
<form className="space-y-3" onSubmit={onSubmit}>
<div>
<label className="text-sm font-medium text-slate-700">Email</label>
<Input value={email} onChange={(e) => setEmail(e.target.value)} type="email" required />
</div>
<div>
<label className="text-sm font-medium text-slate-700">Senha</label>
<Input value={password} onChange={(e) => setPassword(e.target.value)} type="password" required />
</div>
{error && <p className="text-sm text-rose-600">{error}</p>}
<Button type="submit" className="w-full">
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3 text-xs text-amber-800">
<p className="font-semibold mb-1">💡 Conta demo:</p>
<p>Cliente: cliente@demo.com / 123</p>
<p>Barbearia: barber@demo.com / 123</p>
</div>
<form className="space-y-4" onSubmit={onSubmit}>
<Input
label="Email"
type="email"
value={email}
onChange={(e) => {
setEmail(e.target.value);
setError('');
}}
required
error={error ? undefined : undefined}
placeholder="seu@email.com"
/>
<Input
label="Senha"
type="password"
value={password}
onChange={(e) => {
setPassword(e.target.value);
setError('');
}}
required
error={error}
placeholder="••••••••"
/>
<Button type="submit" className="w-full" size="lg">
Entrar
</Button>
</form>
<p className="text-sm text-slate-600">
Não tem conta? <Link to="/registo" className="text-amber-700 font-semibold">Registar</Link>
</p>
<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>
);
@@ -53,3 +92,5 @@ export default function AuthLogin() {

View File

@@ -1,62 +1,137 @@
import { FormEvent, useState } from 'react';
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 { useAppStore } from '../store/useAppStore';
import { useApp } from '../context/AppContext';
import { UserPlus, User, Scissors } from 'lucide-react';
export default function AuthRegister() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [role, setRole] = useState<'cliente' | 'barbearia'>('cliente');
const [shopName, setShopName] = useState('');
const [error, setError] = useState('');
const register = useAppStore((s) => s.register);
const { register, user } = useApp();
const navigate = useNavigate();
useEffect(() => {
if (!user) return;
const target = user.role === 'barbearia' ? '/painel' : '/explorar';
navigate(target, { replace: true });
}, [user, navigate]);
const onSubmit = (e: FormEvent) => {
e.preventDefault();
const ok = register({ name, email, password, role });
const ok = register({ name, email, password, role, shopName });
if (!ok) setError('Email já registado');
else navigate('/');
else {
const target = role === 'barbearia' ? '/painel' : '/explorar';
navigate(target);
}
};
return (
<div className="max-w-md mx-auto">
<Card className="p-6 space-y-4">
<div>
<h1 className="text-xl font-semibold text-slate-900">Criar conta</h1>
<p className="text-sm text-slate-600">Escolha o tipo de acesso.</p>
<div className="max-w-md mx-auto py-8">
<Card className="p-8 space-y-6">
<div className="text-center space-y-2">
<div className="inline-flex p-3 bg-gradient-to-br from-amber-500 to-amber-600 rounded-xl text-white shadow-lg mb-2">
<UserPlus size={24} />
</div>
<h1 className="text-2xl font-bold text-slate-900">Criar conta</h1>
<p className="text-sm text-slate-600">Escolha o tipo de acesso</p>
</div>
<form className="space-y-3" onSubmit={onSubmit}>
<div>
<label className="text-sm font-medium text-slate-700">Nome</label>
<Input value={name} onChange={(e) => setName(e.target.value)} required />
<form className="space-y-5" onSubmit={onSubmit}>
{/* Role Selection */}
<div className="space-y-2">
<label className="text-sm font-medium text-slate-700">Tipo de conta</label>
<div className="grid grid-cols-2 gap-3">
{(['cliente', 'barbearia'] as const).map((r) => (
<button
key={r}
type="button"
onClick={() => {
setRole(r);
setError('');
}}
className={`p-4 rounded-xl border-2 transition-all ${
role === r
? 'border-amber-500 bg-gradient-to-br from-amber-50 to-amber-100/50 shadow-md'
: 'border-slate-200 hover:border-amber-300 hover:bg-amber-50/50'
}`}
>
<div className="flex flex-col items-center gap-2">
{r === 'cliente' ? (
<User size={20} className={role === r ? 'text-amber-600' : 'text-slate-400'} />
) : (
<Scissors size={20} className={role === r ? 'text-amber-600' : 'text-slate-400'} />
)}
<span className={`text-sm font-semibold ${role === r ? 'text-amber-700' : 'text-slate-600'}`}>
{r === 'cliente' ? 'Cliente' : 'Barbearia'}
</span>
</div>
</button>
))}
</div>
</div>
<div>
<label className="text-sm font-medium text-slate-700">Email</label>
<Input value={email} onChange={(e) => setEmail(e.target.value)} type="email" required />
</div>
<div>
<label className="text-sm font-medium text-slate-700">Senha</label>
<Input value={password} onChange={(e) => setPassword(e.target.value)} type="password" required />
</div>
<div className="flex gap-3">
{(['cliente', 'barbearia'] as const).map((r) => (
<label key={r} className="flex items-center gap-2 text-sm text-slate-700">
<input type="radio" name="role" value={r} checked={role === r} onChange={() => setRole(r)} />
{r === 'cliente' ? 'Cliente' : 'Barbearia'}
</label>
))}
</div>
{error && <p className="text-sm text-rose-600">{error}</p>}
<Button type="submit" className="w-full">
<Input
label="Nome completo"
value={name}
onChange={(e) => {
setName(e.target.value);
setError('');
}}
required
placeholder="João Silva"
/>
<Input
label="Email"
type="email"
value={email}
onChange={(e) => {
setEmail(e.target.value);
setError('');
}}
required
placeholder="seu@email.com"
error={error}
/>
<Input
label="Senha"
type="password"
value={password}
onChange={(e) => {
setPassword(e.target.value);
setError('');
}}
required
placeholder="••••••••"
/>
{role === 'barbearia' && (
<Input
label="Nome da barbearia"
value={shopName}
onChange={(e) => setShopName(e.target.value)}
placeholder="Barbearia XPTO"
required
/>
)}
<Button type="submit" className="w-full" size="lg">
Criar conta
</Button>
</form>
<p className="text-sm text-slate-600">
tem conta? <Link to="/login" className="text-amber-700 font-semibold">Entrar</Link>
</p>
<div className="text-center pt-4 border-t border-slate-200">
<p className="text-sm text-slate-600">
tem conta?{' '}
<Link to="/login" className="text-amber-700 font-semibold hover:text-amber-800 transition-colors">
Entrar
</Link>
</p>
</div>
</Card>
</div>
);
@@ -64,3 +139,5 @@ export default function AuthRegister() {

View File

@@ -1,90 +1,294 @@
import { useNavigate, useParams } from 'react-router-dom';
import { useMemo, useState } from 'react';
import { useAppStore } from '../store/useAppStore';
import { Card } from '../components/ui/card';
import { Button } from '../components/ui/button';
import { Input } from '../components/ui/input';
import { useApp } from '../context/AppContext';
import { Calendar, Clock, Scissors, User, CheckCircle2 } from 'lucide-react';
import { currency } from '../lib/format';
export default function Booking() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { shops, createAppointment, user } = useAppStore();
const { shops, createAppointment, user, appointments } = useApp();
const shop = useMemo(() => shops.find((s) => s.id === id), [shops, id]);
const [serviceId, setService] = useState('');
const [barberId, setBarber] = useState('');
const [date, setDate] = useState('');
const [slot, setSlot] = useState('');
if (!shop) return <div>Barbearia não encontrada</div>;
if (!shop) return <div className="text-center py-12 text-slate-600">Barbearia não encontrada</div>;
const selectedService = shop.services.find((s) => s.id === serviceId);
const selectedBarber = shop.barbers.find((b) => b.id === barberId);
const availableSlots = selectedBarber?.schedule.find((s) => s.day === date)?.slots ?? [];
// Função para gerar horários padrão se não houver horários específicos para a data
const generateDefaultSlots = (): string[] => {
const slots: string[] = [];
// Horário de trabalho padrão: 09:00 às 18:00, de hora em hora
for (let hour = 9; hour <= 18; hour++) {
slots.push(`${hour.toString().padStart(2, '0')}:00`);
}
return slots;
};
// Buscar horários disponíveis para a data selecionada
const availableSlots = useMemo(() => {
if (!selectedBarber || !date) return [];
// Primeiro, tentar encontrar horários específicos para a data
const specificSchedule = selectedBarber.schedule.find((s) => s.day === date);
let slots = specificSchedule && specificSchedule.slots.length > 0
? [...specificSchedule.slots]
: generateDefaultSlots();
// Filtrar horários já ocupados
const bookedSlots = appointments
.filter((apt) =>
apt.barberId === barberId &&
apt.status !== 'cancelado' &&
apt.date.startsWith(date)
)
.map((apt) => {
// Extrair o horário da string de data (formato: "YYYY-MM-DD HH:MM")
const parts = apt.date.split(' ');
return parts.length > 1 ? parts[1] : '';
})
.filter(Boolean);
return slots.filter((slot) => !bookedSlots.includes(slot));
}, [selectedBarber, date, barberId, appointments]);
const canSubmit = serviceId && barberId && date && slot;
const submit = () => {
if (!user) {
navigate('/login');
return;
}
if (!serviceId || !barberId || !date || !slot) return;
if (!canSubmit) return;
const appt = createAppointment({ shopId: shop.id, serviceId, barberId, customerId: user.id, date: `${date} ${slot}` });
if (appt) navigate('/perfil');
else alert('Horário indisponível');
if (appt) {
navigate('/perfil');
} else {
alert('Horário indisponível');
}
};
// Determinar qual etapa mostrar
const currentStep = !serviceId ? 1 : !barberId ? 2 : 3;
const steps = [
{ id: 1, label: 'Serviço', icon: Scissors, completed: !!serviceId, current: currentStep === 1 },
{ id: 2, label: 'Barbeiro', icon: User, completed: !!barberId, current: currentStep === 2 },
{ id: 3, label: 'Data & Hora', icon: Calendar, completed: !!date && !!slot, current: currentStep === 3 },
];
return (
<div className="space-y-4">
<h1 className="text-xl font-semibold text-slate-900">Agendar em {shop.name}</h1>
<Card className="p-4 space-y-3">
<div>
<p className="text-sm font-semibold text-slate-700 mb-2">1. Serviço</p>
<div className="grid md:grid-cols-2 gap-2">
{shop.services.map((s) => (
<button
key={s.id}
onClick={() => setService(s.id)}
className={`p-3 rounded-md border text-left ${serviceId === s.id ? 'border-amber-500 bg-amber-50' : 'border-slate-200'}`}
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-slate-900 mb-2">Agendar em {shop.name}</h1>
<p className="text-sm text-slate-600">{shop.address}</p>
</div>
{/* Progress Steps */}
<div className="flex items-center justify-between max-w-2xl">
{steps.map((step, idx) => (
<div key={step.id} className="flex items-center flex-1">
<div className="flex flex-col items-center flex-1">
<div
className={`w-10 h-10 rounded-full flex items-center justify-center border-2 transition-all ${
step.completed
? 'bg-gradient-to-br from-indigo-500 to-blue-600 border-indigo-600 text-white shadow-md'
: step.current
? 'bg-gradient-to-br from-indigo-100 to-blue-100 border-indigo-500 text-indigo-600'
: 'bg-white border-slate-300 text-slate-400'
}`}
>
<div className="font-semibold text-slate-900">{s.name}</div>
<div className="text-sm text-slate-600">R$ {s.price}</div>
</button>
))}
{step.completed ? <CheckCircle2 size={18} /> : <step.icon size={18} />}
</div>
<span className={`text-xs mt-2 font-medium ${
step.completed ? 'text-indigo-700' : step.current ? 'text-indigo-600 font-semibold' : 'text-slate-500'
}`}>
{step.label}
</span>
</div>
{idx < steps.length - 1 && (
<div className={`h-0.5 flex-1 mx-2 ${step.completed ? 'bg-indigo-500' : 'bg-slate-200'}`} />
)}
</div>
</div>
<div>
<p className="text-sm font-semibold text-slate-700 mb-2">2. Barbeiro</p>
<div className="flex gap-2 flex-wrap">
{shop.barbers.map((b) => (
<button
key={b.id}
onClick={() => setBarber(b.id)}
className={`px-3 py-2 rounded-full border text-sm ${barberId === b.id ? 'border-amber-500 bg-amber-50' : 'border-slate-200'}`}
>
{b.name} · {b.specialties.join(', ')}
</button>
))}
</div>
</div>
<div className="grid md:grid-cols-2 gap-4">
<div>
<p className="text-sm font-semibold text-slate-700 mb-1">3. Data</p>
<input type="date" className="w-full border rounded-md px-3 py-2" value={date} onChange={(e) => setDate(e.target.value)} />
</div>
<div>
<p className="text-sm font-semibold text-slate-700 mb-1">4. Horário</p>
<div className="flex gap-2 flex-wrap">
{availableSlots.map((h) => (
))}
</div>
<Card className="p-6 space-y-6">
{/* Step 1: Service - Só aparece se não tiver serviço selecionado */}
{currentStep === 1 && (
<div className="space-y-3">
<div className="flex items-center gap-2">
<Scissors size={18} className="text-indigo-600" />
<h3 className="text-base font-bold text-slate-900">1. Escolha o serviço</h3>
</div>
<div className="grid md:grid-cols-2 gap-3">
{shop.services.map((s) => (
<button
key={h}
onClick={() => setSlot(h)}
className={`px-3 py-2 rounded-md border text-sm ${slot === h ? 'border-amber-500 bg-amber-50' : 'border-slate-200'}`}
key={s.id}
onClick={() => setService(s.id)}
className={`p-4 rounded-xl border-2 text-left transition-all ${
serviceId === s.id
? 'border-indigo-500 bg-gradient-to-br from-indigo-50 to-blue-100/50 shadow-md scale-[1.02]'
: 'border-slate-200 hover:border-indigo-300 hover:bg-indigo-50/50'
}`}
>
{h}
<div className="flex items-center justify-between mb-1">
<div className="font-bold text-slate-900">{s.name}</div>
<div className="text-sm font-bold text-indigo-600">{currency(s.price)}</div>
</div>
<div className="text-xs text-slate-500">Duração: {s.duration} min</div>
</button>
))}
{!availableSlots.length && <p className="text-sm text-slate-500">Escolha data e barbeiro.</p>}
</div>
</div>
</div>
<Button onClick={submit}>Confirmar agendamento</Button>
)}
{/* Step 2: Barber - Só aparece se tiver serviço mas não barbeiro */}
{currentStep === 2 && (
<div className="space-y-3">
<div className="flex items-center gap-2 mb-4">
<button
onClick={() => setService('')}
className="text-sm text-indigo-600 hover:text-indigo-700 font-medium flex items-center gap-1"
>
Voltar
</button>
</div>
<div className="flex items-center gap-2">
<User size={18} className="text-indigo-600" />
<h3 className="text-base font-bold text-slate-900">2. Escolha o barbeiro</h3>
</div>
{selectedService && (
<div className="mb-4 p-3 bg-indigo-50 rounded-lg border border-indigo-200">
<div className="text-sm text-slate-600">Serviço selecionado:</div>
<div className="font-semibold text-slate-900">{selectedService.name} - {currency(selectedService.price)}</div>
</div>
)}
<div className="flex gap-2 flex-wrap">
{shop.barbers.map((b) => (
<button
key={b.id}
onClick={() => setBarber(b.id)}
className={`px-4 py-2.5 rounded-full border-2 text-sm font-medium transition-all ${
barberId === b.id
? 'border-indigo-500 bg-gradient-to-r from-indigo-500 to-blue-600 text-white shadow-md'
: 'border-slate-200 text-slate-700 hover:border-indigo-300 hover:bg-indigo-50'
}`}
>
{b.name}
{b.specialties.length > 0 && (
<span className="ml-2 text-xs opacity-80">· {b.specialties[0]}</span>
)}
</button>
))}
</div>
</div>
)}
{/* Step 3: Date & Time - Só aparece se tiver serviço e barbeiro */}
{currentStep === 3 && (
<div className="space-y-6">
<div className="flex items-center gap-2 mb-4">
<button
onClick={() => setBarber('')}
className="text-sm text-indigo-600 hover:text-indigo-700 font-medium flex items-center gap-1"
>
Voltar
</button>
</div>
<div className="flex items-center gap-2 mb-4">
<Calendar size={18} className="text-indigo-600" />
<h3 className="text-base font-bold text-slate-900">3. Escolha a data e horário</h3>
</div>
{selectedService && selectedBarber && (
<div className="mb-4 p-3 bg-indigo-50 rounded-lg border border-indigo-200 space-y-1">
<div className="text-sm text-slate-600">Serviço: <span className="font-semibold text-slate-900">{selectedService.name}</span></div>
<div className="text-sm text-slate-600">Barbeiro: <span className="font-semibold text-slate-900">{selectedBarber.name}</span></div>
</div>
)}
<div className="grid md:grid-cols-2 gap-6">
<div className="space-y-3">
<div className="flex items-center gap-2">
<Calendar size={18} className="text-indigo-600" />
<h4 className="text-sm font-bold text-slate-900">Data</h4>
</div>
<Input
type="date"
value={date}
onChange={(e) => setDate(e.target.value)}
min={new Date().toISOString().split('T')[0]}
/>
</div>
<div className="space-y-3">
<div className="flex items-center gap-2">
<Clock size={18} className="text-indigo-600" />
<h4 className="text-sm font-bold text-slate-900">Horário</h4>
</div>
<div className="flex gap-2 flex-wrap">
{!date ? (
<p className="text-sm text-slate-500 py-2">Escolha primeiro a data.</p>
) : availableSlots.length > 0 ? (
availableSlots.map((h) => (
<button
key={h}
onClick={() => setSlot(h)}
className={`px-4 py-2 rounded-lg border-2 text-sm font-medium transition-all ${
slot === h
? 'border-indigo-500 bg-gradient-to-r from-indigo-500 to-blue-600 text-white shadow-md'
: 'border-slate-200 text-slate-700 hover:border-indigo-300 hover:bg-indigo-50'
}`}
>
{h}
</button>
))
) : (
<p className="text-sm text-indigo-600 py-2 font-medium">Nenhum horário disponível para esta data.</p>
)}
</div>
</div>
</div>
</div>
)}
{/* Summary */}
{canSubmit && selectedService && (
<div className="pt-4 border-t border-slate-200 space-y-3">
<h4 className="text-sm font-bold text-slate-900">Resumo do agendamento</h4>
<div className="bg-slate-50 rounded-lg p-4 space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-slate-600">Serviço:</span>
<span className="font-semibold text-slate-900">{selectedService.name}</span>
</div>
<div className="flex justify-between">
<span className="text-slate-600">Barbeiro:</span>
<span className="font-semibold text-slate-900">{selectedBarber?.name}</span>
</div>
<div className="flex justify-between">
<span className="text-slate-600">Data e hora:</span>
<span className="font-semibold text-slate-900">
{new Date(date).toLocaleDateString('pt-BR')} às {slot}
</span>
</div>
<div className="flex justify-between pt-2 border-t border-slate-200">
<span className="font-bold text-slate-900">Total:</span>
<span className="font-bold text-lg text-indigo-600">{currency(selectedService.price)}</span>
</div>
</div>
</div>
)}
<Button onClick={submit} disabled={!canSubmit} size="lg" className="w-full">
{user ? 'Confirmar agendamento' : 'Entrar para agendar'}
</Button>
</Card>
</div>
);
@@ -92,3 +296,5 @@ export default function Booking() {

View File

@@ -1,139 +1,640 @@
import { useState } from 'react';
import { useAppStore } from '../store/useAppStore';
import { useMemo, useState } from 'react';
import { Card } from '../components/ui/card';
import { Button } from '../components/ui/button';
import { Input } from '../components/ui/input';
import { Badge } from '../components/ui/badge';
import { DashboardCards } from '../components/DashboardCards';
import { Tabs } from '../components/ui/tabs';
import { currency } from '../lib/format';
import { nanoid } from 'nanoid';
import { useApp } from '../context/AppContext';
import { Product } from '../types';
import { BarChart, Bar, CartesianGrid, ResponsiveContainer, Tooltip, XAxis } from 'recharts';
import {
BarChart3,
Calendar,
ShoppingBag,
Scissors,
Package,
Users,
TrendingUp,
AlertTriangle,
Plus,
Trash2,
Minus,
Plus as PlusIcon,
History,
} from 'lucide-react';
const periods: Record<string, (date: Date) => boolean> = {
hoje: (d) => {
const now = new Date();
return d.toDateString() === now.toDateString();
},
semana: (d) => {
const now = new Date();
const diff = now.getTime() - d.getTime();
return diff <= 7 * 24 * 60 * 60 * 1000;
},
mes: (d) => {
const now = new Date();
return d.getMonth() === now.getMonth() && d.getFullYear() === now.getFullYear();
},
total: () => true,
};
const parseDate = (value: string) => new Date(value.replace(' ', 'T'));
type TabId = 'overview' | 'appointments' | 'history' | 'orders' | 'services' | 'products' | 'barbers';
export default function Dashboard() {
const { user, shops, appointments, orders, updateAppointmentStatus, updateOrderStatus, addService: addServiceStore } = useAppStore();
const {
user,
shops,
appointments,
orders,
updateAppointmentStatus,
updateOrderStatus,
addService,
addProduct,
addBarber,
updateProduct,
deleteProduct,
deleteService,
deleteBarber,
} = useApp();
const shop = shops.find((s) => s.id === user?.shopId);
const [activeTab, setActiveTab] = useState<TabId>('overview');
const [period, setPeriod] = useState<keyof typeof periods>('semana');
// Form states
const [svcName, setSvcName] = useState('');
const [svcPrice, setSvcPrice] = useState<number>(50);
const [svcDuration, setSvcDuration] = useState<number>(30);
const [prodName, setProdName] = useState('');
const [prodPrice, setProdPrice] = useState<number>(30);
const [prodStock, setProdStock] = useState<number>(10);
const [barberName, setBarberName] = useState('');
const [barberSpecs, setBarberSpecs] = useState('');
if (!user || user.role !== 'barbearia') return <div>Área exclusiva para barbearias.</div>;
if (!shop) return <div>Barbearia não encontrada.</div>;
const shopAppointments = appointments.filter((a) => a.shopId === shop.id);
const shopOrders = orders.filter((o) => o.shopId === shop.id);
const periodMatch = periods[period];
const allShopAppointments = appointments.filter((a) => a.shopId === shop.id && periodMatch(parseDate(a.date)));
// Agendamentos ativos (não concluídos)
const shopAppointments = allShopAppointments.filter((a) => a.status !== 'concluido');
// Agendamentos concluídos (histórico)
const completedAppointments = allShopAppointments.filter((a) => a.status === 'concluido');
// Pedidos apenas com produtos (não serviços)
const shopOrders = orders.filter(
(o) => o.shopId === shop.id && periodMatch(new Date(o.createdAt)) && o.items.some((item) => item.type === 'product')
);
const addService = () => {
addServiceStore(shop.id, { id: nanoid(), name: svcName || 'Novo Serviço', price: Number(svcPrice) || 0, duration: 30, barberIds: [] });
const totalRevenue = shopOrders.reduce((s, o) => s + o.total, 0);
const pendingAppts = shopAppointments.filter((a) => a.status === 'pendente').length;
const confirmedAppts = shopAppointments.filter((a) => a.status === 'confirmado').length;
const lowStock = shop.products.filter((p) => p.stock <= 3);
const comparisonData = useMemo(() => {
const totals = shopOrders.reduce(
(acc, order) => {
order.items.forEach((item) => {
if (item.type === 'service') {
acc.services += item.qty;
} else {
acc.products += item.qty;
}
});
return acc;
},
{ services: 0, products: 0 }
);
return [
{ name: 'Serviços', value: totals.services },
{ name: 'Produtos', value: totals.products },
];
}, [shopOrders]);
const topServices = useMemo(() => {
const map = new Map<string, { name: string; qty: number }>();
shopOrders.forEach((o) =>
o.items
.filter((i) => i.type === 'service')
.forEach((i) => {
const svc = shop.services.find((s) => s.id === i.refId);
if (!svc) return;
const prev = map.get(i.refId)?.qty ?? 0;
map.set(i.refId, { name: svc.name, qty: prev + i.qty });
})
);
return Array.from(map.values())
.sort((a, b) => b.qty - a.qty)
.slice(0, 5);
}, [shopOrders, shop.services]);
const topProducts = useMemo(() => {
const map = new Map<string, { name: string; qty: number }>();
shopOrders.forEach((o) =>
o.items
.filter((i) => i.type === 'product')
.forEach((i) => {
const prod = shop.products.find((p) => p.id === i.refId);
if (!prod) return;
const prev = map.get(i.refId)?.qty ?? 0;
map.set(i.refId, { name: prod.name, qty: prev + i.qty });
})
);
return Array.from(map.values())
.sort((a, b) => b.qty - a.qty)
.slice(0, 5);
}, [shopOrders, shop.products]);
const updateProductStock = (product: Product, delta: number) => {
const next = { ...product, stock: Math.max(0, product.stock + delta) };
updateProduct(shop.id, next);
};
const addNewService = () => {
if (!svcName.trim()) return;
addService(shop.id, { name: svcName, price: Number(svcPrice) || 0, duration: Number(svcDuration) || 30, barberIds: [] });
setSvcName('');
setSvcPrice(50);
setSvcDuration(30);
};
const addNewProduct = () => {
if (!prodName.trim()) return;
addProduct(shop.id, { name: prodName, price: Number(prodPrice) || 0, stock: Number(prodStock) || 0 });
setProdName('');
setProdPrice(30);
setProdStock(10);
};
const addNewBarber = () => {
if (!barberName.trim()) return;
addBarber(shop.id, {
name: barberName,
specialties: barberSpecs.split(',').map((s) => s.trim()).filter(Boolean),
schedule: [],
});
setBarberName('');
setBarberSpecs('');
};
const tabs = [
{ id: 'overview' as TabId, label: 'Visão Geral', icon: BarChart3 },
{ id: 'appointments' as TabId, label: 'Agendamentos', icon: Calendar },
{ id: 'history' as TabId, label: 'Histórico', icon: History },
{ id: 'orders' as TabId, label: 'Pedidos', icon: ShoppingBag },
{ id: 'services' as TabId, label: 'Serviços', icon: Scissors },
{ id: 'products' as TabId, label: 'Produtos', icon: Package },
{ id: 'barbers' as TabId, label: 'Barbeiros', icon: Users },
];
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
{/* Header */}
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<h1 className="text-xl font-semibold text-slate-900">Painel da {shop.name}</h1>
<h1 className="text-2xl font-bold text-slate-900">{shop.name}</h1>
<p className="text-sm text-slate-600">{shop.address}</p>
</div>
<Badge color="amber">Role: barbearia</Badge>
<div className="flex items-center gap-2">
{(['hoje', 'semana', 'mes', 'total'] as const).map((p) => (
<button
key={p}
onClick={() => setPeriod(p)}
className={`px-3 py-1.5 rounded-lg text-xs font-medium border transition-all ${
period === p
? 'border-amber-500 bg-amber-50 text-amber-700 shadow-sm'
: 'border-slate-200 text-slate-700 hover:border-amber-300 hover:bg-amber-50/50'
}`}
>
{p === 'mes' ? 'Mês' : p === 'hoje' ? 'Hoje' : p === 'semana' ? 'Semana' : 'Total'}
</button>
))}
</div>
</div>
<DashboardCards />
{/* Tabs */}
<Tabs tabs={tabs.map((t) => ({ id: t.id, label: t.label }))} active={activeTab} onChange={(v) => setActiveTab(v as TabId)} />
<section className="grid md:grid-cols-2 gap-4">
<Card className="p-4 space-y-2">
<div className="flex items-center justify-between">
<h3 className="text-base font-semibold text-slate-900">Agendamentos</h3>
</div>
<div className="space-y-2 max-h-80 overflow-auto">
{shopAppointments.map((a) => (
<div key={a.id} className="flex items-center justify-between rounded-md border border-slate-200 px-3 py-2">
<div className="text-sm">
<p className="font-semibold text-slate-900">{a.date}</p>
<p className="text-xs text-slate-600">Serviço: {a.serviceId}</p>
</div>
<div className="flex items-center gap-2">
<Badge>{a.status}</Badge>
<select
className="text-xs border border-slate-300 rounded-md px-2 py-1"
value={a.status}
onChange={(e) => updateAppointmentStatus(a.id, e.target.value as any)}
>
{['pendente', 'confirmado', 'concluido', 'cancelado'].map((s) => (
<option key={s} value={s}>
{s}
</option>
))}
</select>
{/* Tab Content */}
{activeTab === 'overview' && (
<div className="space-y-6">
{/* Stats Cards */}
<div className="grid md:grid-cols-4 gap-4">
<Card className="p-5 bg-gradient-to-br from-amber-50 to-white">
<div className="flex items-center justify-between mb-2">
<div className="p-2 bg-amber-500 rounded-lg text-white">
<TrendingUp size={20} />
</div>
<Badge color="amber" variant="soft">Período</Badge>
</div>
))}
{!shopAppointments.length && <p className="text-sm text-slate-600">Sem agendamentos.</p>}
<p className="text-sm text-slate-600 mb-1">Faturamento</p>
<p className="text-2xl font-bold text-amber-700">{currency(totalRevenue)}</p>
</Card>
<Card className="p-5">
<div className="flex items-center justify-between mb-2">
<div className="p-2 bg-blue-500 rounded-lg text-white">
<Calendar size={20} />
</div>
<Badge color="amber" variant="soft">{pendingAppts}</Badge>
</div>
<p className="text-sm text-slate-600 mb-1">Pendentes</p>
<p className="text-2xl font-bold text-slate-900">{pendingAppts}</p>
</Card>
<Card className="p-5">
<div className="flex items-center justify-between mb-2">
<div className="p-2 bg-emerald-500 rounded-lg text-white">
<Calendar size={20} />
</div>
<Badge color="green" variant="soft">{confirmedAppts}</Badge>
</div>
<p className="text-sm text-slate-600 mb-1">Confirmados</p>
<p className="text-2xl font-bold text-slate-900">{confirmedAppts}</p>
</Card>
<Card className={`p-5 ${lowStock.length > 0 ? 'bg-amber-50 border-amber-200' : ''}`}>
<div className="flex items-center justify-between mb-2">
<div className={`p-2 rounded-lg text-white ${lowStock.length > 0 ? 'bg-amber-500' : 'bg-slate-500'}`}>
<AlertTriangle size={20} />
</div>
{lowStock.length > 0 && <Badge color="amber" variant="solid">{lowStock.length}</Badge>}
</div>
<p className="text-sm text-slate-600 mb-1">Stock baixo</p>
<p className={`text-2xl font-bold ${lowStock.length > 0 ? 'text-amber-700' : 'text-slate-900'}`}>{lowStock.length}</p>
</Card>
</div>
{/* Charts */}
<div className="grid md:grid-cols-2 gap-6">
<Card className="p-6">
<h3 className="text-lg font-bold text-slate-900 mb-4">Serviços vs Produtos</h3>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={comparisonData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<Tooltip />
<Bar dataKey="value" fill="#f59e0b" radius={[6, 6, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</Card>
<div className="space-y-4">
<Card className="p-5">
<h3 className="text-base font-bold text-slate-900 mb-3">Top 5 Serviços</h3>
<div className="space-y-2">
{topServices.length > 0 ? (
topServices.map((s, idx) => (
<div key={s.name} className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2">
<span className="text-slate-400 font-bold">#{idx + 1}</span>
<span className="text-slate-700">{s.name}</span>
</div>
<Badge color="amber">{s.qty} vendas</Badge>
</div>
))
) : (
<p className="text-sm text-slate-500">Sem vendas no período</p>
)}
</div>
</Card>
<Card className="p-5">
<h3 className="text-base font-bold text-slate-900 mb-3">Top 5 Produtos</h3>
<div className="space-y-2">
{topProducts.length > 0 ? (
topProducts.map((p, idx) => (
<div key={p.name} className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2">
<span className="text-slate-400 font-bold">#{idx + 1}</span>
<span className="text-slate-700">{p.name}</span>
</div>
<Badge color="amber">{p.qty} vendas</Badge>
</div>
))
) : (
<p className="text-sm text-slate-500">Sem vendas no período</p>
)}
</div>
</Card>
</div>
</div>
</div>
)}
{activeTab === 'appointments' && (
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold text-slate-900">Agendamentos</h2>
<Badge color="slate" variant="soft">{shopAppointments.length} no período</Badge>
</div>
<div className="space-y-3">
{shopAppointments.length > 0 ? (
shopAppointments.map((a) => {
const svc = shop.services.find((s) => s.id === a.serviceId);
const barber = shop.barbers.find((b) => b.id === a.barberId);
return (
<div key={a.id} className="flex items-center justify-between p-4 border border-slate-200 rounded-lg hover:border-amber-300 transition-colors">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<p className="font-bold text-slate-900">{svc?.name ?? 'Serviço'}</p>
<Badge color={a.status === 'pendente' ? 'amber' : a.status === 'confirmado' ? 'green' : a.status === 'concluido' ? 'green' : 'red'}>
{a.status}
</Badge>
</div>
<p className="text-sm text-slate-600">{barber?.name ?? 'Barbeiro'} · {a.date}</p>
<p className="text-xs text-slate-500 mt-1">{currency(a.total)}</p>
</div>
<select
className="text-sm border border-slate-300 rounded-lg px-3 py-2 focus:border-amber-500 focus:ring-2 focus:ring-amber-500/30"
value={a.status}
onChange={(e) => updateAppointmentStatus(a.id, e.target.value as any)}
>
{['pendente', 'confirmado', 'concluido', 'cancelado'].map((s) => (
<option key={s} value={s}>
{s === 'pendente' ? 'Pendente' : s === 'confirmado' ? 'Confirmado' : s === 'concluido' ? 'Concluído' : 'Cancelado'}
</option>
))}
</select>
</div>
);
})
) : (
<div className="text-center py-12">
<Calendar size={48} className="mx-auto text-slate-300 mb-3" />
<p className="text-slate-600 font-medium">Nenhum agendamento no período</p>
</div>
)}
</div>
</Card>
)}
<Card className="p-4 space-y-2">
<div className="flex items-center justify-between">
<h3 className="text-base font-semibold text-slate-900">Pedidos</h3>
{activeTab === 'history' && (
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold text-slate-900">Histórico de Agendamentos</h2>
<Badge color="slate" variant="soft">{completedAppointments.length} concluídos</Badge>
</div>
<div className="space-y-2 max-h-80 overflow-auto">
{shopOrders.map((o) => (
<div key={o.id} className="flex items-center justify-between rounded-md border border-slate-200 px-3 py-2">
<div className="text-sm">
<p className="font-semibold text-slate-900">{currency(o.total)}</p>
<p className="text-xs text-slate-600">{new Date(o.createdAt).toLocaleString('pt-BR')}</p>
<div className="space-y-3">
{completedAppointments.length > 0 ? (
completedAppointments.map((a) => {
const svc = shop.services.find((s) => s.id === a.serviceId);
const barber = shop.barbers.find((b) => b.id === a.barberId);
return (
<div key={a.id} className="flex items-center justify-between p-4 border border-slate-200 rounded-lg bg-slate-50/50">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<p className="font-bold text-slate-900">{svc?.name ?? 'Serviço'}</p>
<Badge color="green" variant="soft">Concluído</Badge>
</div>
<p className="text-sm text-slate-600">{barber?.name ?? 'Barbeiro'} · {a.date}</p>
<p className="text-xs text-slate-500 mt-1">{currency(a.total)}</p>
</div>
</div>
);
})
) : (
<div className="text-center py-12">
<History size={48} className="mx-auto text-slate-300 mb-3" />
<p className="text-slate-600 font-medium">Nenhum agendamento concluído no período</p>
<p className="text-sm text-slate-500 mt-1">Os agendamentos concluídos aparecerão aqui</p>
</div>
)}
</div>
</Card>
)}
{activeTab === 'orders' && (
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold text-slate-900">Pedidos de Produtos</h2>
<Badge color="slate" variant="soft">{shopOrders.length} no período</Badge>
</div>
<div className="space-y-3">
{shopOrders.length > 0 ? (
shopOrders.map((o) => {
const productItems = o.items.filter((i) => i.type === 'product');
const productTotal = productItems.reduce((sum, item) => {
const prod = shop.products.find((p) => p.id === item.refId);
return sum + (prod?.price ?? 0) * item.qty;
}, 0);
return (
<div key={o.id} className="p-4 border border-slate-200 rounded-lg hover:border-amber-300 transition-colors">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<p className="font-bold text-amber-700">{currency(productTotal)}</p>
<Badge color={o.status === 'pendente' ? 'amber' : o.status === 'confirmado' ? 'green' : o.status === 'concluido' ? 'green' : 'red'}>
{o.status === 'pendente' ? 'Pendente' : o.status === 'confirmado' ? 'Confirmado' : o.status === 'concluido' ? 'Concluído' : 'Cancelado'}
</Badge>
</div>
<select
className="text-sm border border-slate-300 rounded-lg px-3 py-2 focus:border-amber-500 focus:ring-2 focus:ring-amber-500/30"
value={o.status}
onChange={(e) => updateOrderStatus(o.id, e.target.value as any)}
>
{['pendente', 'confirmado', 'concluido', 'cancelado'].map((s) => (
<option key={s} value={s}>
{s === 'pendente' ? 'Pendente' : s === 'confirmado' ? 'Confirmado' : s === 'concluido' ? 'Concluído' : 'Cancelado'}
</option>
))}
</select>
</div>
<p className="text-sm text-slate-600 mb-2">{new Date(o.createdAt).toLocaleString('pt-BR')}</p>
<div className="space-y-1">
{productItems.map((item) => {
const prod = shop.products.find((p) => p.id === item.refId);
return (
<div key={item.refId} className="flex items-center justify-between text-sm bg-slate-50 rounded px-2 py-1">
<span className="text-slate-700">{prod?.name ?? 'Produto'} x{item.qty}</span>
<span className="text-amber-600 font-semibold">{currency((prod?.price ?? 0) * item.qty)}</span>
</div>
);
})}
</div>
</div>
);
})
) : (
<div className="text-center py-12">
<ShoppingBag size={48} className="mx-auto text-slate-300 mb-3" />
<p className="text-slate-600 font-medium">Nenhum pedido de produtos no período</p>
<p className="text-sm text-slate-500 mt-1">Apenas pedidos com produtos aparecem aqui</p>
</div>
)}
</div>
</Card>
)}
{activeTab === 'services' && (
<div className="space-y-6">
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold text-slate-900">Serviços</h2>
<Badge color="slate" variant="soft">{shop.services.length} serviços</Badge>
</div>
<div className="space-y-3 mb-6">
{shop.services.map((s) => (
<div key={s.id} className="flex items-center justify-between p-4 border border-slate-200 rounded-lg">
<div className="flex-1">
<p className="font-bold text-slate-900">{s.name}</p>
<p className="text-sm text-slate-600">Duração: {s.duration} min</p>
</div>
<div className="flex items-center gap-4">
<span className="text-lg font-bold text-amber-600">{currency(s.price)}</span>
<Button variant="danger" size="sm" onClick={() => deleteService(shop.id, s.id)}>
<Trash2 size={16} />
</Button>
</div>
</div>
<div className="flex items-center gap-2">
<Badge>{o.status}</Badge>
<select
className="text-xs border border-slate-300 rounded-md px-2 py-1"
value={o.status}
onChange={(e) => updateOrderStatus(o.id, e.target.value as any)}
>
{['pendente', 'confirmado', 'concluido', 'cancelado'].map((s) => (
<option key={s} value={s}>
{s}
</option>
))}
</select>
))}
</div>
<div className="border-t border-slate-200 pt-4">
<h3 className="text-base font-semibold text-slate-900 mb-3">Adicionar novo serviço</h3>
<div className="grid md:grid-cols-4 gap-3">
<Input label="Nome" placeholder="Ex: Corte Fade" value={svcName} onChange={(e) => setSvcName(e.target.value)} />
<Input label="Preço" type="number" placeholder="50" value={svcPrice} onChange={(e) => setSvcPrice(Number(e.target.value))} />
<Input label="Duração (min)" type="number" placeholder="30" value={svcDuration} onChange={(e) => setSvcDuration(Number(e.target.value))} />
<div className="flex items-end">
<Button onClick={addNewService} className="w-full">
<PlusIcon size={16} className="mr-2" />
Adicionar
</Button>
</div>
</div>
))}
{!shopOrders.length && <p className="text-sm text-slate-600">Sem pedidos.</p>}
</div>
</Card>
</section>
</div>
</Card>
</div>
)}
<section className="grid md:grid-cols-2 gap-4">
<Card className="p-4 space-y-3">
<h3 className="text-base font-semibold text-slate-900">Serviços</h3>
<div className="space-y-2">
{shop.services.map((s) => (
<div key={s.id} className="flex items-center justify-between text-sm border border-slate-200 rounded-md px-3 py-2">
<span>{s.name}</span>
<span className="text-amber-700 font-semibold">{currency(s.price)}</span>
{activeTab === 'products' && (
<div className="space-y-6">
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold text-slate-900">Produtos</h2>
<Badge color="slate" variant="soft">{shop.products.length} produtos</Badge>
</div>
{lowStock.length > 0 && (
<div className="mb-4 p-3 bg-amber-50 border border-amber-200 rounded-lg">
<p className="text-sm font-semibold text-amber-800 flex items-center gap-2">
<AlertTriangle size={16} />
Atenção: {lowStock.length} {lowStock.length === 1 ? 'produto com stock baixo' : 'produtos com stock baixo'}
</p>
</div>
))}
</div>
<div className="flex gap-2">
<Input placeholder="Nome do serviço" value={svcName} onChange={(e) => setSvcName(e.target.value)} />
<Input type="number" value={svcPrice} onChange={(e) => setSvcPrice(Number(e.target.value))} />
<Button onClick={addService}>Adicionar</Button>
</div>
</Card>
)}
<div className="space-y-3 mb-6">
{shop.products.map((p) => (
<div
key={p.id}
className={`flex items-center justify-between p-4 border rounded-lg ${
p.stock <= 3 ? 'border-amber-300 bg-amber-50' : 'border-slate-200'
}`}
>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<p className="font-bold text-slate-900">{p.name}</p>
{p.stock <= 3 && <Badge color="amber" variant="solid">Stock baixo</Badge>}
</div>
<p className="text-sm text-slate-600">Stock: {p.stock} unidades</p>
</div>
<div className="flex items-center gap-4">
<span className="text-lg font-bold text-amber-600">{currency(p.price)}</span>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={() => updateProductStock(p, -1)}>
<Minus size={14} />
</Button>
<Button variant="outline" size="sm" onClick={() => updateProductStock(p, 1)}>
<Plus size={14} />
</Button>
<Button variant="danger" size="sm" onClick={() => deleteProduct(shop.id, p.id)}>
<Trash2 size={16} />
</Button>
</div>
</div>
</div>
))}
</div>
<div className="border-t border-slate-200 pt-4">
<h3 className="text-base font-semibold text-slate-900 mb-3">Adicionar novo produto</h3>
<div className="grid md:grid-cols-4 gap-3">
<Input label="Nome" placeholder="Ex: Pomada" value={prodName} onChange={(e) => setProdName(e.target.value)} />
<Input label="Preço" type="number" placeholder="30" value={prodPrice} onChange={(e) => setProdPrice(Number(e.target.value))} />
<Input label="Stock inicial" type="number" placeholder="10" value={prodStock} onChange={(e) => setProdStock(Number(e.target.value))} />
<div className="flex items-end">
<Button onClick={addNewProduct} className="w-full">
<PlusIcon size={16} className="mr-2" />
Adicionar
</Button>
</div>
</div>
</div>
</Card>
</div>
)}
<Card className="p-4 space-y-3">
<h3 className="text-base font-semibold text-slate-900">Produtos (stock)</h3>
<div className="space-y-2">
{shop.products.map((p) => (
<div key={p.id} className="flex items-center justify-between text-sm border border-slate-200 rounded-md px-3 py-2">
<span>{p.name}</span>
<span className="text-slate-700">Stock: {p.stock}</span>
{activeTab === 'barbers' && (
<div className="space-y-6">
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold text-slate-900">Barbeiros</h2>
<Badge color="slate" variant="soft">{shop.barbers.length} barbeiros</Badge>
</div>
<div className="space-y-3 mb-6">
{shop.barbers.map((b) => (
<div key={b.id} className="p-4 border border-slate-200 rounded-lg">
<div className="flex items-center justify-between mb-2">
<p className="font-bold text-slate-900 text-lg">{b.name}</p>
<Button variant="danger" size="sm" onClick={() => deleteBarber(shop.id, b.id)}>
<Trash2 size={16} />
</Button>
</div>
<div className="space-y-1">
<p className="text-sm text-slate-600">
<span className="font-medium">Especialidades:</span> {b.specialties.length > 0 ? b.specialties.join(', ') : 'Nenhuma'}
</p>
</div>
</div>
))}
{shop.barbers.length === 0 && (
<div className="text-center py-12">
<Users size={48} className="mx-auto text-slate-300 mb-3" />
<p className="text-slate-600 font-medium">Nenhum barbeiro registado</p>
</div>
)}
</div>
<div className="border-t border-slate-200 pt-4">
<h3 className="text-base font-semibold text-slate-900 mb-3">Adicionar novo barbeiro</h3>
<div className="grid md:grid-cols-3 gap-3">
<Input
label="Nome"
placeholder="Ex: João Silva"
value={barberName}
onChange={(e) => setBarberName(e.target.value)}
/>
<Input
label="Especialidades"
placeholder="Fade, Navalha, Barba"
value={barberSpecs}
onChange={(e) => setBarberSpecs(e.target.value)}
/>
<div className="flex items-end">
<Button onClick={addNewBarber} className="w-full">
<PlusIcon size={16} className="mr-2" />
Adicionar
</Button>
</div>
</div>
))}
</div>
<p className="text-xs text-slate-500">CRUD simplificado; ajuste de stock pode ser adicionado.</p>
</Card>
</section>
</div>
</Card>
</div>
)}
</div>
);
}

View File

@@ -1,8 +1,8 @@
import { useAppStore } from '../store/useAppStore';
import { ShopCard } from '../components/ShopCard';
import { useApp } from '../context/AppContext';
export default function Explore() {
const shops = useAppStore((s) => s.shops);
const { shops } = useApp();
return (
<div className="space-y-4">
@@ -18,3 +18,5 @@ export default function Explore() {

View File

@@ -1,35 +1,325 @@
import { Link } from 'react-router-dom';
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 { useEffect } from 'react';
import { useApp } from '../context/AppContext';
import { mockShops } from '../data/mock';
export default function Landing() {
const { user } = useApp();
const navigate = useNavigate();
useEffect(() => {
if (!user) return;
const target = user.role === 'barbearia' ? '/painel' : '/explorar';
navigate(target, { replace: true });
}, [user, navigate]);
const featuredShops = mockShops.slice(0, 3);
return (
<div className="space-y-10">
<section className="rounded-2xl bg-gradient-to-r from-amber-500 to-amber-600 text-white px-6 py-10 shadow-lg">
<div className="space-y-4 max-w-2xl">
<p className="text-sm uppercase tracking-wide font-semibold">Smart Agenda</p>
<h1 className="text-3xl font-bold leading-tight">Agendamentos, produtos e gestão em um único lugar.</h1>
<p className="text-lg text-amber-50">Experiência mobile-first para clientes e painel completo para barbearias.</p>
<div className="flex flex-wrap gap-3">
<Button asChild>
<Link to="/explorar">Explorar barbearias</Link>
<div className="space-y-16 md:space-y-24 pb-12">
{/* Hero Section */}
<section className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-indigo-600 via-blue-600 to-indigo-700 text-white px-6 py-16 md:px-12 md:py-24 shadow-2xl">
<div className="absolute inset-0 bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxnIGZpbGw9IiNmZmYiIGZpbGwtb3BhY2l0eT0iMC4xIj48Y2lyY2xlIGN4PSIzMCIgY3k9IjMwIiByPSIyIi8+PC9nPjwvZz48L3N2Zz4=')] opacity-20"></div>
<div className="absolute top-0 right-0 w-96 h-96 bg-blue-400/20 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2"></div>
<div className="absolute bottom-0 left-0 w-96 h-96 bg-indigo-500/20 rounded-full blur-3xl translate-y-1/2 -translate-x-1/2"></div>
<div className="relative space-y-8 max-w-4xl">
<div className="inline-flex items-center gap-2 px-4 py-2 bg-white/20 backdrop-blur-sm rounded-full text-sm font-semibold w-fit border border-white/30">
<Sparkles size={16} />
<span>Revolucione sua barbearia</span>
</div>
<h1 className="text-5xl md:text-6xl lg:text-7xl font-bold leading-tight text-balance">
Agendamentos, produtos e gestão em um{' '}
<span className="text-blue-100">único lugar</span>
</h1>
<p className="text-xl md:text-2xl text-blue-50/90 max-w-3xl leading-relaxed">
Experiência mobile-first para clientes e painel completo para barbearias.
Simplifique a gestão do seu negócio e aumente sua receita.
</p>
<div className="flex flex-wrap gap-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" className="bg-white text-amber-700 border-white">
<Link to="/registo">Criar conta</Link>
<Button asChild variant="outline" size="lg" className="bg-white/10 backdrop-blur-sm text-white border-white/30 hover:bg-white/20 text-base px-8 py-4">
<Link to="/registo">Criar conta grátis</Link>
</Button>
</div>
{/* Stats */}
<div className="grid grid-cols-3 gap-6 pt-8 border-t border-white/20">
<div>
<div className="text-3xl md:text-4xl font-bold">500+</div>
<div className="text-sm text-blue-100/80 mt-1">Barbearias</div>
</div>
<div>
<div className="text-3xl md:text-4xl font-bold">10k+</div>
<div className="text-sm text-blue-100/80 mt-1">Agendamentos</div>
</div>
<div>
<div className="text-3xl md:text-4xl font-bold">4.8</div>
<div className="text-sm text-blue-100/80 mt-1">Avaliação média</div>
</div>
</div>
</div>
</section>
<section className="grid md:grid-cols-3 gap-4">
{[
{ title: 'Agendamentos', desc: 'Escolha serviço, barbeiro, data e horário com validação de slots.' },
{ title: 'Carrinho', desc: 'Produtos e serviços agrupados por barbearia, pagamento rápido.' },
{ title: 'Painel', desc: 'Faturamento, agendamentos, pedidos, barbearia no controle.' },
].map((c) => (
<div key={c.title} className="rounded-xl bg-white border border-slate-200 p-4 shadow-sm">
<h3 className="text-lg font-semibold text-slate-900">{c.title}</h3>
<p className="text-sm text-slate-600">{c.desc}</p>
{/* Features Grid */}
<section>
<div className="text-center mb-12">
<h2 className="text-3xl md:text-4xl font-bold text-slate-900 mb-4">
Tudo que você precisa
</h2>
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
Funcionalidades poderosas para clientes e barbearias
</p>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{[
{
icon: Calendar,
title: 'Agendamentos Inteligentes',
desc: 'Escolha serviço, barbeiro, data e horário com validação de slots em tempo real. Notificações automáticas.',
color: 'from-blue-500 to-blue-600'
},
{
icon: ShoppingBag,
title: 'Carrinho Inteligente',
desc: 'Produtos e serviços agrupados por barbearia, checkout rápido e seguro. Histórico completo de compras.',
color: 'from-emerald-500 to-emerald-600'
},
{
icon: BarChart3,
title: 'Painel Completo',
desc: 'Faturamento, agendamentos, pedidos e análises detalhadas. Tudo no controle da sua barbearia.',
color: 'from-purple-500 to-purple-600'
},
{
icon: Users,
title: 'Gestão de Barbeiros',
desc: 'Gerencie horários, especialidades e disponibilidade de cada barbeiro. Calendário integrado.',
color: 'from-indigo-500 to-indigo-600'
},
{
icon: Clock,
title: 'Horários Flexíveis',
desc: 'Configure horários de funcionamento, intervalos e disponibilidade. Sistema automático de bloqueio.',
color: 'from-orange-500 to-orange-600'
},
{
icon: Shield,
title: 'Seguro e Confiável',
desc: 'Dados protegidos, pagamentos seguros e backup automático. Conformidade com LGPD.',
color: 'from-rose-500 to-rose-600'
},
].map((feature) => (
<Card key={feature.title} hover className="p-6 space-y-4 group">
<div className={`inline-flex p-3 rounded-xl bg-gradient-to-br ${feature.color} text-white shadow-lg group-hover:scale-110 transition-transform duration-200`}>
<feature.icon size={24} />
</div>
<div>
<h3 className="text-xl font-bold text-slate-900 mb-2">{feature.title}</h3>
<p className="text-sm text-slate-600 leading-relaxed">{feature.desc}</p>
</div>
</Card>
))}
</div>
</section>
{/* How it Works */}
<section className="bg-gradient-to-br from-slate-50 to-blue-50/30 rounded-2xl p-8 md:p-12">
<div className="text-center mb-12">
<h2 className="text-3xl md:text-4xl font-bold text-slate-900 mb-4">
Como funciona
</h2>
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
Simples, rápido e eficiente em 3 passos
</p>
</div>
<div className="grid md:grid-cols-3 gap-8 max-w-4xl mx-auto">
{[
{ step: '1', title: 'Explore', desc: 'Navegue pelas barbearias disponíveis, veja avaliações e serviços oferecidos.' },
{ step: '2', title: 'Agende', desc: 'Escolha o serviço, barbeiro e horário que melhor se adequa à sua agenda.' },
{ step: '3', title: 'Aproveite', desc: 'Compareça no horário agendado e aproveite um serviço de qualidade.' },
].map((item) => (
<div key={item.step} className="text-center space-y-4">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-gradient-to-br from-indigo-500 to-blue-600 text-white text-2xl font-bold shadow-lg">
{item.step}
</div>
<h3 className="text-xl font-bold text-slate-900">{item.title}</h3>
<p className="text-slate-600">{item.desc}</p>
</div>
))}
</div>
</section>
{/* Featured Shops */}
<section>
<div className="flex items-center justify-between mb-8">
<div>
<h2 className="text-3xl md:text-4xl font-bold text-slate-900 mb-2">
Barbearias em destaque
</h2>
<p className="text-slate-600">
Conheça algumas das melhores barbearias da plataforma
</p>
</div>
))}
<Button asChild variant="ghost" className="hidden md:flex">
<Link to="/explorar" className="flex items-center gap-2">
Ver todas
<ArrowRight size={16} />
</Link>
</Button>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{featuredShops.map((shop) => (
<ShopCard key={shop.id} shop={shop} />
))}
</div>
<div className="text-center mt-8">
<Button asChild size="lg">
<Link to="/explorar">Ver todas as barbearias</Link>
</Button>
</div>
</section>
{/* Benefits */}
<section className="grid md:grid-cols-2 gap-8">
<Card className="p-8 md:p-10 space-y-6">
<div className="inline-flex p-3 rounded-xl bg-gradient-to-br from-indigo-500 to-blue-600 text-white shadow-lg">
<Smartphone size={28} />
</div>
<h3 className="text-2xl md:text-3xl font-bold text-slate-900">
Mobile-First
</h3>
<p className="text-slate-600 leading-relaxed">
Interface otimizada para dispositivos móveis. Agende de qualquer lugar,
a qualquer hora. Experiência fluida e responsiva.
</p>
<ul className="space-y-3">
{['Design responsivo', 'Carregamento rápido', 'Interface intuitiva'].map((item) => (
<li key={item} className="flex items-center gap-2 text-slate-700">
<CheckCircle2 size={18} className="text-indigo-600 flex-shrink-0" />
<span>{item}</span>
</li>
))}
</ul>
</Card>
<Card className="p-8 md:p-10 space-y-6">
<div className="inline-flex p-3 rounded-xl bg-gradient-to-br from-purple-500 to-purple-600 text-white shadow-lg">
<TrendingUp size={28} />
</div>
<h3 className="text-2xl md:text-3xl font-bold text-slate-900">
Aumente sua Receita
</h3>
<p className="text-slate-600 leading-relaxed">
Ferramentas poderosas para gerenciar seu negócio. Análises detalhadas,
gestão de estoque e muito mais.
</p>
<ul className="space-y-3">
{['Análises em tempo real', 'Gestão de estoque', 'Relatórios detalhados'].map((item) => (
<li key={item} className="flex items-center gap-2 text-slate-700">
<CheckCircle2 size={18} className="text-purple-600 flex-shrink-0" />
<span>{item}</span>
</li>
))}
</ul>
</Card>
</section>
{/* Testimonials */}
<section>
<div className="text-center mb-12">
<h2 className="text-3xl md:text-4xl font-bold text-slate-900 mb-4">
O que nossos clientes dizem
</h2>
<p className="text-lg text-slate-600">
Depoimentos reais de quem usa a plataforma
</p>
</div>
<div className="grid md:grid-cols-3 gap-6">
{[
{
name: 'João Silva',
role: 'Cliente',
text: 'Facilita muito agendar meu corte. Interface simples e rápida. Recomendo!',
rating: 5
},
{
name: 'Carlos Mendes',
role: 'Proprietário',
text: 'O painel é completo e me ajuda muito na gestão. Aumentou minha organização.',
rating: 5
},
{
name: 'Miguel Santos',
role: 'Cliente',
text: 'Nunca mais perco horário. As notificações são muito úteis.',
rating: 5
},
].map((testimonial) => (
<Card key={testimonial.name} className="p-6 space-y-4">
<div className="flex items-center gap-1">
{[...Array(testimonial.rating)].map((_, i) => (
<Star key={i} size={16} className="fill-indigo-500 text-indigo-500" />
))}
</div>
<Quote className="text-indigo-500/50" size={24} />
<p className="text-slate-700 leading-relaxed">{testimonial.text}</p>
<div className="pt-2 border-t border-slate-100">
<div className="font-semibold text-slate-900">{testimonial.name}</div>
<div className="text-sm text-slate-500">{testimonial.role}</div>
</div>
</Card>
))}
</div>
</section>
{/* CTA Final */}
<section className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white px-6 py-16 md:px-12 md:py-20 shadow-2xl">
<div className="absolute top-0 right-0 w-96 h-96 bg-indigo-500/10 rounded-full blur-3xl"></div>
<div className="absolute bottom-0 left-0 w-96 h-96 bg-blue-500/10 rounded-full blur-3xl"></div>
<div className="relative text-center space-y-8 max-w-3xl mx-auto">
<h2 className="text-4xl md:text-5xl font-bold text-balance">
Pronto para começar?
</h2>
<p className="text-xl text-slate-300 max-w-2xl mx-auto">
Junte-se a centenas de barbearias que estão usando a Smart Agenda
para revolucionar seus negócios.
</p>
<div className="flex flex-wrap justify-center gap-4 pt-4">
<Button asChild size="lg" className="text-base px-8 py-4 bg-white text-slate-900 hover:bg-slate-100">
<Link to="/registo" className="flex items-center gap-2">
Criar conta grátis
<ArrowRight size={18} />
</Link>
</Button>
<Button asChild variant="outline" size="lg" className="text-base px-8 py-4 border-white/30 text-white hover:bg-white/10">
<Link to="/explorar">Explorar agora</Link>
</Button>
</div>
</div>
</section>
</div>
);
@@ -37,3 +327,5 @@ export default function Landing() {

View File

@@ -1,7 +1,8 @@
import { useAppStore } from '../store/useAppStore';
import { Card } from '../components/ui/card';
import { Badge } from '../components/ui/badge';
import { currency } from '../lib/format';
import { useApp } from '../context/AppContext';
import { Calendar, ShoppingBag, User, Clock } from 'lucide-react';
const statusColor: Record<string, 'amber' | 'green' | 'slate' | 'red'> = {
pendente: 'amber',
@@ -10,59 +11,138 @@ const statusColor: Record<string, 'amber' | 'green' | 'slate' | 'red'> = {
cancelado: 'red',
};
const statusLabel: Record<string, string> = {
pendente: 'Pendente',
confirmado: 'Confirmado',
concluido: 'Concluído',
cancelado: 'Cancelado',
};
export default function Profile() {
const { user, appointments, orders, shops } = useAppStore();
if (!user) return <div>Faça login para ver o perfil.</div>;
const { user, appointments, orders, shops } = useApp();
if (!user) {
return (
<div className="text-center py-12">
<p className="text-slate-600">Faça login para ver o perfil.</p>
</div>
);
}
const myAppointments = appointments.filter((a) => a.customerId === user.id);
const myOrders = orders.filter((o) => o.customerId === user.id);
return (
<div className="space-y-6">
<div>
<h1 className="text-xl font-semibold text-slate-900">Olá, {user.name}</h1>
<p className="text-sm text-slate-600">{user.email}</p>
</div>
<section className="space-y-2">
<h2 className="text-lg font-semibold text-slate-900">Agendamentos</h2>
{!myAppointments.length && <Card className="p-4 text-sm text-slate-600">Sem agendamentos.</Card>}
<div className="space-y-2">
{myAppointments.map((a) => {
const shop = shops.find((s) => s.id === a.shopId);
return (
<Card key={a.id} className="p-4 flex items-center justify-between">
<div>
<p className="text-sm font-semibold text-slate-900">{shop?.name}</p>
<p className="text-xs text-slate-600">{a.date}</p>
</div>
<Badge color={statusColor[a.status]}>{a.status}</Badge>
</Card>
);
})}
<div className="space-y-8">
{/* Profile Header */}
<Card className="p-6 bg-gradient-to-br from-amber-50 to-white">
<div className="flex items-center gap-4">
<div className="p-4 bg-gradient-to-br from-amber-500 to-amber-600 rounded-xl text-white shadow-lg">
<User size={24} />
</div>
<div>
<h1 className="text-2xl font-bold text-slate-900">Olá, {user.name}</h1>
<p className="text-sm text-slate-600">{user.email}</p>
<Badge color="amber" variant="soft" className="mt-2">
{user.role === 'cliente' ? 'Cliente' : 'Barbearia'}
</Badge>
</div>
</div>
</Card>
{/* Appointments Section */}
<section className="space-y-4">
<div className="flex items-center gap-2">
<Calendar size={20} className="text-amber-600" />
<h2 className="text-xl font-bold text-slate-900">Agendamentos</h2>
<Badge color="slate" variant="soft">{myAppointments.length}</Badge>
</div>
{!myAppointments.length ? (
<Card className="p-8 text-center">
<Calendar size={48} className="mx-auto text-slate-300 mb-3" />
<p className="text-slate-600 font-medium">Nenhum agendamento ainda</p>
<p className="text-sm text-slate-500 mt-1">Explore barbearias e agende seu primeiro serviço!</p>
</Card>
) : (
<div className="space-y-3">
{myAppointments.map((a) => {
const shop = shops.find((s) => s.id === a.shopId);
const service = shop?.services.find((s) => s.id === a.serviceId);
return (
<Card key={a.id} hover className="p-5">
<div className="flex items-start justify-between gap-4">
<div className="flex-1 space-y-2">
<div className="flex items-center gap-2">
<h3 className="font-bold text-slate-900">{shop?.name}</h3>
<Badge color={statusColor[a.status]} variant="soft">
{statusLabel[a.status]}
</Badge>
</div>
{service && (
<p className="text-sm text-slate-600 flex items-center gap-1">
<Clock size={14} />
{service.name} · {service.duration} min
</p>
)}
<p className="text-xs text-slate-500">{a.date}</p>
</div>
<div className="text-right">
<p className="text-lg font-bold text-amber-600">{currency(a.total)}</p>
</div>
</div>
</Card>
);
})}
</div>
)}
</section>
<section className="space-y-2">
<h2 className="text-lg font-semibold text-slate-900">Pedidos</h2>
{!myOrders.length && <Card className="p-4 text-sm text-slate-600">Sem pedidos.</Card>}
<div className="space-y-2">
{myOrders.map((o) => {
const shop = shops.find((s) => s.id === o.shopId);
return (
<Card key={o.id} className="p-4 flex items-center justify-between">
<div>
<p className="text-sm font-semibold text-slate-900">{shop?.name}</p>
<p className="text-xs text-slate-600">{new Date(o.createdAt).toLocaleString('pt-BR')}</p>
</div>
<div className="flex items-center gap-3">
<span className="text-sm font-semibold text-slate-900">{currency(o.total)}</span>
<Badge color={statusColor[o.status]}>{o.status}</Badge>
</div>
</Card>
);
})}
{/* Orders Section */}
<section className="space-y-4">
<div className="flex items-center gap-2">
<ShoppingBag size={20} className="text-amber-600" />
<h2 className="text-xl font-bold text-slate-900">Pedidos</h2>
<Badge color="slate" variant="soft">{myOrders.length}</Badge>
</div>
{!myOrders.length ? (
<Card className="p-8 text-center">
<ShoppingBag size={48} className="mx-auto text-slate-300 mb-3" />
<p className="text-slate-600 font-medium">Nenhum pedido ainda</p>
<p className="text-sm text-slate-500 mt-1">Adicione produtos ao carrinho e finalize seu primeiro pedido!</p>
</Card>
) : (
<div className="space-y-3">
{myOrders.map((o) => {
const shop = shops.find((s) => s.id === o.shopId);
return (
<Card key={o.id} hover className="p-5">
<div className="flex items-start justify-between gap-4">
<div className="flex-1 space-y-2">
<div className="flex items-center gap-2">
<h3 className="font-bold text-slate-900">{shop?.name}</h3>
<Badge color={statusColor[o.status]} variant="soft">
{statusLabel[o.status]}
</Badge>
</div>
<p className="text-xs text-slate-500">
{new Date(o.createdAt).toLocaleDateString('pt-BR', {
day: '2-digit',
month: 'long',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
})}
</p>
<p className="text-xs text-slate-600">{o.items.length} {o.items.length === 1 ? 'item' : 'itens'}</p>
</div>
<div className="text-right">
<p className="text-lg font-bold text-amber-600">{currency(o.total)}</p>
</div>
</div>
</Card>
);
})}
</div>
)}
</section>
</div>
);
@@ -70,3 +150,5 @@ export default function Profile() {

View File

@@ -1,15 +1,14 @@
import { useParams, Link } from 'react-router-dom';
import { useMemo, useState } from 'react';
import { useAppStore } from '../store/useAppStore';
import { Tabs } from '../components/ui/tabs';
import { ServiceList } from '../components/ServiceList';
import { ProductList } from '../components/ProductList';
import { Button } from '../components/ui/button';
import { useApp } from '../context/AppContext';
export default function ShopDetails() {
const { id } = useParams<{ id: string }>();
const shops = useAppStore((s) => s.shops);
const addToCart = useAppStore((s) => s.addToCart);
const { shops, addToCart } = useApp();
const shop = useMemo(() => shops.find((s) => s.id === id), [shops, id]);
const [tab, setTab] = useState<'servicos' | 'produtos'>('servicos');
@@ -48,3 +47,5 @@ export default function ShopDetails() {

View File

@@ -1,118 +0,0 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { nanoid } from 'nanoid';
import { Appointment, BarberShop, CartItem, Order, User } from '../types';
import { mockShops, mockUsers } from '../data/mock';
type State = {
user?: User;
shops: BarberShop[];
appointments: Appointment[];
orders: Order[];
cart: CartItem[];
};
type Actions = {
login: (email: string, password: string) => boolean;
register: (input: Omit<User, 'id'>) => boolean;
addToCart: (item: CartItem) => void;
removeFromCart: (refId: string) => void;
clearCart: () => void;
createAppointment: (a: Omit<Appointment, 'id' | 'status' | 'total'>) => Appointment | null;
placeOrder: (customerId: string) => Order | null;
updateAppointmentStatus: (id: string, status: Appointment['status']) => void;
updateOrderStatus: (id: string, status: Order['status']) => void;
addService: (shopId: string, service: BarberShop['services'][number]) => void;
};
export const useAppStore = create<State & Actions>()(
persist(
(set, get) => ({
user: undefined,
shops: mockShops,
appointments: [],
orders: [],
cart: [],
login: (email, password) => {
const found = mockUsers.find((u) => u.email === email && u.password === password);
if (found) {
set({ user: found });
return true;
}
return false;
},
addService: (shopId, service) => {
set({
shops: get().shops.map((s) => (s.id === shopId ? { ...s, services: [...s.services, service] } : s)),
});
},
register: (input) => {
const exists = mockUsers.some((u) => u.email === input.email);
if (exists) return false;
const nu: User = { ...input, id: nanoid() };
mockUsers.push(nu);
set({ user: nu });
return true;
},
addToCart: (item) => {
const cart = get().cart.slice();
const idx = cart.findIndex((c) => c.refId === item.refId && c.type === item.type);
if (idx >= 0) cart[idx].qty += item.qty;
else cart.push(item);
set({ cart });
},
removeFromCart: (refId) => set({ cart: get().cart.filter((c) => c.refId !== refId) }),
clearCart: () => set({ cart: [] }),
createAppointment: (a) => {
const shop = get().shops.find((s) => s.id === a.shopId);
if (!shop) return null;
const svc = shop.services.find((s) => s.id === a.serviceId);
if (!svc) return null;
const clash = get().appointments.find(
(ap) => ap.barberId === a.barberId && ap.date === a.date && ap.status !== 'cancelado'
);
if (clash) return null;
const appointment: Appointment = {
...a,
id: nanoid(),
status: 'pendente',
total: svc.price,
};
set({ appointments: [...get().appointments, appointment] });
return appointment;
},
placeOrder: (customerId) => {
const cart = get().cart;
if (!cart.length) return null;
const total = cart.reduce((sum, item) => {
const shop = get().shops.find((s) => s.id === item.shopId);
if (!shop) return sum;
const price =
item.type === 'service'
? shop.services.find((s) => s.id === item.refId)?.price ?? 0
: shop.products.find((p) => p.id === item.refId)?.price ?? 0;
return sum + price * item.qty;
}, 0);
const order: Order = {
id: nanoid(),
shopId: cart[0].shopId,
customerId,
items: cart,
total,
status: 'pendente',
createdAt: new Date().toISOString(),
};
set({ orders: [...get().orders, order], cart: [] });
return order;
},
updateAppointmentStatus: (id, status) => {
set({ appointments: get().appointments.map((a) => (a.id === id ? { ...a, status } : a)) });
},
updateOrderStatus: (id, status) => {
set({ orders: get().orders.map((o) => (o.id === id ? { ...o, status } : o)) });
},
}),
{ name: 'smart-agenda' }
)
);

View File

@@ -3,7 +3,11 @@ import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: { port: 5173 },
server: {
port: 5173,
host: '0.0.0.0', // Permite acesso de outras interfaces
strictPort: false, // Tenta outra porta se 5173 estiver ocupada
},
});