From 50edd54561ff7e20fee7e1bb2455132d42a1a642 Mon Sep 17 00:00:00 2001 From: Sidney Date: Sun, 19 Jul 2026 21:09:20 -0300 Subject: [PATCH] feat: substituir alertas nativos do navegador por sistema de toast notifications customizado em toda a plataforma e admin --- admin/src/app/globals.css | 99 +++++++++++++++++++++++++++++++++ admin/src/app/page.tsx | 2 + admin/src/components/Toast.tsx | 54 ++++++++++++++++++ admin/src/lib/toast.ts | 16 ++++++ src/app/[slug]/agendar/page.tsx | 5 +- src/app/globals.css | 99 +++++++++++++++++++++++++++++++++ src/app/page.tsx | 2 + src/app/register/page.tsx | 7 ++- src/components/Agenda.tsx | 5 +- src/components/Services.tsx | 3 +- src/components/Subscription.tsx | 7 ++- src/components/Team.tsx | 3 +- src/components/Toast.tsx | 54 ++++++++++++++++++ src/lib/toast.ts | 16 ++++++ 14 files changed, 362 insertions(+), 10 deletions(-) create mode 100644 admin/src/components/Toast.tsx create mode 100644 admin/src/lib/toast.ts create mode 100644 src/components/Toast.tsx create mode 100644 src/lib/toast.ts diff --git a/admin/src/app/globals.css b/admin/src/app/globals.css index 76addc3..923a345 100644 --- a/admin/src/app/globals.css +++ b/admin/src/app/globals.css @@ -188,3 +188,102 @@ tr:hover td { background: var(--bg-card-hover); } .revenue-chart { display: flex; align-items: flex-end; gap: 6px; height: 160px; padding: 16px 0; } .chart-bar { flex: 1; border-radius: 4px 4px 0 0; background: var(--gradient-primary); transition: all 0.3s; min-width: 20px; cursor: pointer; position: relative; } .chart-bar:hover { opacity: 0.8; } + +/* === TOAST NOTIFICATIONS === */ +.toast-container { + position: fixed; + top: 24px; + right: 24px; + z-index: 9999; + display: flex; + flex-direction: column; + gap: 10px; + max-width: 420px; + width: calc(100% - 48px); + pointer-events: none; +} + +.toast-item { + pointer-events: auto; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 14px 18px; + background: var(--bg-card, #12121c); + border: 1px solid var(--border-color, rgba(255,255,255,0.12)); + border-radius: 10px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); + color: #fff; + font-size: 14px; + font-weight: 500; + animation: toastSlideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1); + backdrop-filter: blur(12px); +} + +.toast-content { + display: flex; + align-items: center; + gap: 10px; + flex: 1; +} + +.toast-icon { flex-shrink: 0; } +.toast-icon.success { color: #10b981; } +.toast-icon.error { color: #ef4444; } +.toast-icon.warning { color: #f59e0b; } +.toast-icon.info { color: #6366f1; } + +.toast-error { + border-left: 4px solid #ef4444; + background: linear-gradient(90deg, rgba(239,68,68,0.15) 0%, rgba(18,18,28,0.95) 100%); +} + +.toast-warning { + border-left: 4px solid #f59e0b; + background: linear-gradient(90deg, rgba(245,158,11,0.15) 0%, rgba(18,18,28,0.95) 100%); +} + +.toast-success { + border-left: 4px solid #10b981; + background: linear-gradient(90deg, rgba(16,185,129,0.15) 0%, rgba(18,18,28,0.95) 100%); +} + +.toast-info { + border-left: 4px solid #6366f1; + background: linear-gradient(90deg, rgba(99,102,241,0.15) 0%, rgba(18,18,28,0.95) 100%); +} + +.toast-message { + line-height: 1.4; + color: var(--text-primary, #f3f4f6); +} + +.toast-close { + background: transparent; + border: none; + color: var(--text-secondary, #9ca3af); + cursor: pointer; + padding: 4px; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; +} + +.toast-close:hover { + color: #fff; + background: rgba(255, 255, 255, 0.1); +} + +@keyframes toastSlideIn { + from { + opacity: 0; + transform: translateY(-20px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} diff --git a/admin/src/app/page.tsx b/admin/src/app/page.tsx index fbd1c06..2008556 100644 --- a/admin/src/app/page.tsx +++ b/admin/src/app/page.tsx @@ -7,6 +7,7 @@ import Tenants from '@/components/Tenants'; import Plans from '@/components/Plans'; import Payments from '@/components/Payments'; import AdminSettings from '@/components/AdminSettings'; +import ToastContainer from '@/components/Toast'; const navItems = [ { section: 'Visão Geral' }, @@ -291,6 +292,7 @@ export default function AdminPage() { {renderPage()} + ); } diff --git a/admin/src/components/Toast.tsx b/admin/src/components/Toast.tsx new file mode 100644 index 0000000..cc47c30 --- /dev/null +++ b/admin/src/components/Toast.tsx @@ -0,0 +1,54 @@ +'use client'; +import { useState, useEffect } from 'react'; +import { CheckCircle2, AlertTriangle, XCircle, Info, X } from 'lucide-react'; +import { ToastData } from '@/lib/toast'; + +export default function ToastContainer() { + const [toasts, setToasts] = useState([]); + + useEffect(() => { + const handleToast = (e: Event) => { + const customEvent = e as CustomEvent; + const newToast = customEvent.detail; + setToasts(prev => [...prev, newToast]); + + setTimeout(() => { + setToasts(prev => prev.filter(t => t.id !== newToast.id)); + }, 5000); + }; + + window.addEventListener('agendapro-toast', handleToast); + return () => window.removeEventListener('agendapro-toast', handleToast); + }, []); + + const removeToast = (id: string) => { + setToasts(prev => prev.filter(t => t.id !== id)); + }; + + if (toasts.length === 0) return null; + + return ( +
+ {toasts.map(t => { + const icons = { + success: , + error: , + warning: , + info: + }; + + return ( +
+
+ {icons[t.type]} + {t.message} +
+ +
+ ); + })} +
+ ); +} diff --git a/admin/src/lib/toast.ts b/admin/src/lib/toast.ts new file mode 100644 index 0000000..e5b0ba7 --- /dev/null +++ b/admin/src/lib/toast.ts @@ -0,0 +1,16 @@ +export type ToastType = 'success' | 'error' | 'warning' | 'info'; + +export interface ToastData { + id: string; + message: string; + type: ToastType; +} + +export function showToast(message: string, type: ToastType = 'info') { + if (typeof window !== 'undefined') { + const event = new CustomEvent('agendapro-toast', { + detail: { message, type, id: Math.random().toString(36).substring(2, 9) } + }); + window.dispatchEvent(event); + } +} diff --git a/src/app/[slug]/agendar/page.tsx b/src/app/[slug]/agendar/page.tsx index 775ffa8..afefce2 100644 --- a/src/app/[slug]/agendar/page.tsx +++ b/src/app/[slug]/agendar/page.tsx @@ -1,6 +1,8 @@ 'use client'; import { useState, useEffect, use } from 'react'; import { Calendar, Clock, User, Phone, Mail, ChevronLeft, ChevronRight, Check, Scissors, MapPin, Star, Loader } from 'lucide-react'; +import { showToast } from '@/lib/toast'; +import ToastContainer from '@/components/Toast'; export default function PublicBookingPage({ params }: { params: Promise<{ slug: string }> }) { const { slug } = use(params); @@ -147,7 +149,7 @@ export default function PublicBookingPage({ params }: { params: Promise<{ slug: setBooked(true); } else { const errData = await res.json(); - alert(errData.error || 'Erro ao realizar o agendamento.'); + showToast(errData.error || 'Erro ao realizar o agendamento.', 'warning'); } } catch (e) { console.error(e); @@ -416,6 +418,7 @@ export default function PublicBookingPage({ params }: { params: Promise<{ slug: Powered by AgendaPRO

+ ); } diff --git a/src/app/globals.css b/src/app/globals.css index 496873d..b6b7ebf 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -898,3 +898,102 @@ tr:hover td { background: var(--bg-card-hover); } .page-content { padding: 16px; } .pricing-grid { grid-template-columns: 1fr; } } + +/* === TOAST NOTIFICATIONS === */ +.toast-container { + position: fixed; + top: 24px; + right: 24px; + z-index: 9999; + display: flex; + flex-direction: column; + gap: 10px; + max-width: 420px; + width: calc(100% - 48px); + pointer-events: none; +} + +.toast-item { + pointer-events: auto; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 14px 18px; + background: var(--bg-card, #12121c); + border: 1px solid var(--border-color, rgba(255,255,255,0.12)); + border-radius: 10px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); + color: #fff; + font-size: 14px; + font-weight: 500; + animation: toastSlideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1); + backdrop-filter: blur(12px); +} + +.toast-content { + display: flex; + align-items: center; + gap: 10px; + flex: 1; +} + +.toast-icon { flex-shrink: 0; } +.toast-icon.success { color: #10b981; } +.toast-icon.error { color: #ef4444; } +.toast-icon.warning { color: #f59e0b; } +.toast-icon.info { color: #6366f1; } + +.toast-error { + border-left: 4px solid #ef4444; + background: linear-gradient(90deg, rgba(239,68,68,0.15) 0%, rgba(18,18,28,0.95) 100%); +} + +.toast-warning { + border-left: 4px solid #f59e0b; + background: linear-gradient(90deg, rgba(245,158,11,0.15) 0%, rgba(18,18,28,0.95) 100%); +} + +.toast-success { + border-left: 4px solid #10b981; + background: linear-gradient(90deg, rgba(16,185,129,0.15) 0%, rgba(18,18,28,0.95) 100%); +} + +.toast-info { + border-left: 4px solid #6366f1; + background: linear-gradient(90deg, rgba(99,102,241,0.15) 0%, rgba(18,18,28,0.95) 100%); +} + +.toast-message { + line-height: 1.4; + color: var(--text-primary, #f3f4f6); +} + +.toast-close { + background: transparent; + border: none; + color: var(--text-secondary, #9ca3af); + cursor: pointer; + padding: 4px; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; +} + +.toast-close:hover { + color: #fff; + background: rgba(255, 255, 255, 0.1); +} + +@keyframes toastSlideIn { + from { + opacity: 0; + transform: translateY(-20px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} diff --git a/src/app/page.tsx b/src/app/page.tsx index f12bc72..f776a62 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -11,6 +11,7 @@ import Services from '@/components/Services'; import Team from '@/components/Team'; import Settings from '@/components/Settings'; import Subscription from '@/components/Subscription'; +import ToastContainer from '@/components/Toast'; // Helper para cookies const getCookie = (name: string): string => { @@ -149,6 +150,7 @@ export default function Home() { {renderPage()} + ); } diff --git a/src/app/register/page.tsx b/src/app/register/page.tsx index fee3a3e..661c35a 100644 --- a/src/app/register/page.tsx +++ b/src/app/register/page.tsx @@ -2,6 +2,8 @@ import { useState } from 'react'; import { Mail, Lock, Shield, ArrowRight, Building, Globe } from 'lucide-react'; import { useRouter } from 'next/navigation'; +import { showToast } from '@/lib/toast'; +import ToastContainer from '@/components/Toast'; export default function RegisterPage() { const [name, setName] = useState(''); @@ -33,10 +35,10 @@ export default function RegisterPage() { // Redireciona para o painel principal do app router.push('/'); } else { - alert(data.error || 'Erro ao criar conta'); + showToast(data.error || 'Erro ao criar conta', 'error'); } } catch (error) { - alert('Erro de conexão ao criar conta'); + showToast('Erro de conexão ao criar conta', 'error'); } finally { setLoading(false); } @@ -116,6 +118,7 @@ export default function RegisterPage() {

+ ); } diff --git a/src/components/Agenda.tsx b/src/components/Agenda.tsx index 44be192..305f7d0 100644 --- a/src/components/Agenda.tsx +++ b/src/components/Agenda.tsx @@ -1,5 +1,6 @@ import { useState, useEffect } from 'react'; -import { Plus, ChevronLeft, ChevronRight, X, Clock, User, Calendar } from 'lucide-react'; +import { ChevronLeft, ChevronRight, Plus, Calendar, Clock, User, Phone, CheckCircle, AlertCircle, X, Mail } from 'lucide-react'; +import { showToast } from '@/lib/toast'; const hours = Array.from({ length: 12 }, (_, i) => i + 8); @@ -289,7 +290,7 @@ export default function Agenda() { setNewNotes(''); } else { const errData = await res.json(); - alert(errData.error || 'Erro ao realizar agendamento.'); + showToast(errData.error || 'Erro ao realizar agendamento.', 'warning'); } } catch (e) { console.error(e); diff --git a/src/components/Services.tsx b/src/components/Services.tsx index fa1c378..23f0906 100644 --- a/src/components/Services.tsx +++ b/src/components/Services.tsx @@ -1,5 +1,6 @@ import { useState, useEffect } from 'react'; import { Plus, Search, Clock, DollarSign, X, Edit, Trash2, AlertTriangle, Layers } from 'lucide-react'; +import { showToast } from '@/lib/toast'; interface Service { id: string; name: string; category: string; price: number; duration: number; type: 'fixed' | 'starting' | 'variable'; description: string; @@ -75,7 +76,7 @@ export default function Services() { setEditingService(null); } else { const errData = await res.json(); - alert(errData.error || 'Erro ao salvar serviço.'); + showToast(errData.error || 'Erro ao salvar serviço.', 'warning'); } } catch (e) { console.error(e); diff --git a/src/components/Subscription.tsx b/src/components/Subscription.tsx index 342797f..bc83510 100644 --- a/src/components/Subscription.tsx +++ b/src/components/Subscription.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState, useEffect } from 'react'; import { Check, Crown, Zap, Building2, Rocket, AlertTriangle, ShieldAlert, CreditCard, QrCode, FileText, CheckCircle2, ShieldCheck } from 'lucide-react'; +import { showToast } from '@/lib/toast'; const staticPlans = [ { @@ -234,14 +235,14 @@ export default function Subscription({ currentPlan, onPlanChange, isLocked = fal body: JSON.stringify({ slug, status: 'active' }), }); - alert('Pagamento simulado com sucesso! O sistema foi desbloqueado.'); + showToast('Pagamento simulado com sucesso! O sistema foi desbloqueado.', 'success'); setSelectedPlan(null); if (onPaymentSuccess) { onPaymentSuccess(); } } catch (err) { console.warn('Erro ao conectar com API de simulação, utilizando fallback local de homologação.', err); - alert('Pagamento simulado localmente! O sistema foi desbloqueado (modo offline).'); + showToast('Pagamento simulado localmente! O sistema foi desbloqueado (modo offline).', 'success'); setSelectedPlan(null); if (onPaymentSuccess) { onPaymentSuccess(); @@ -466,7 +467,7 @@ export default function Subscription({ currentPlan, onPlanChange, isLocked = fal
- +
diff --git a/src/components/Team.tsx b/src/components/Team.tsx index 2ce60d0..a89d84f 100644 --- a/src/components/Team.tsx +++ b/src/components/Team.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState, useEffect } from 'react'; import { Plus, X, Clock, Edit, Trash2, Star, AlertTriangle } from 'lucide-react'; +import { showToast } from '@/lib/toast'; interface Professional { id: string; name: string; role: string; phone: string; email: string; commission: number; @@ -71,7 +72,7 @@ export default function Team() { setEditingProf(null); } else { const errData = await res.json(); - alert(errData.error || 'Erro ao salvar profissional.'); + showToast(errData.error || 'Erro ao salvar profissional.', 'warning'); } } catch (e) { console.error(e); diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx new file mode 100644 index 0000000..cc47c30 --- /dev/null +++ b/src/components/Toast.tsx @@ -0,0 +1,54 @@ +'use client'; +import { useState, useEffect } from 'react'; +import { CheckCircle2, AlertTriangle, XCircle, Info, X } from 'lucide-react'; +import { ToastData } from '@/lib/toast'; + +export default function ToastContainer() { + const [toasts, setToasts] = useState([]); + + useEffect(() => { + const handleToast = (e: Event) => { + const customEvent = e as CustomEvent; + const newToast = customEvent.detail; + setToasts(prev => [...prev, newToast]); + + setTimeout(() => { + setToasts(prev => prev.filter(t => t.id !== newToast.id)); + }, 5000); + }; + + window.addEventListener('agendapro-toast', handleToast); + return () => window.removeEventListener('agendapro-toast', handleToast); + }, []); + + const removeToast = (id: string) => { + setToasts(prev => prev.filter(t => t.id !== id)); + }; + + if (toasts.length === 0) return null; + + return ( +
+ {toasts.map(t => { + const icons = { + success: , + error: , + warning: , + info: + }; + + return ( +
+
+ {icons[t.type]} + {t.message} +
+ +
+ ); + })} +
+ ); +} diff --git a/src/lib/toast.ts b/src/lib/toast.ts new file mode 100644 index 0000000..e5b0ba7 --- /dev/null +++ b/src/lib/toast.ts @@ -0,0 +1,16 @@ +export type ToastType = 'success' | 'error' | 'warning' | 'info'; + +export interface ToastData { + id: string; + message: string; + type: ToastType; +} + +export function showToast(message: string, type: ToastType = 'info') { + if (typeof window !== 'undefined') { + const event = new CustomEvent('agendapro-toast', { + detail: { message, type, id: Math.random().toString(36).substring(2, 9) } + }); + window.dispatchEvent(event); + } +}