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);
+ }
+}