feat: substituir alertas nativos do navegador por sistema de toast notifications customizado em toda a plataforma e admin
Build and Deploy (Gitea Actions) / build-and-deploy (push) Successful in 2m40s
Details
Build and Deploy (Gitea Actions) / build-and-deploy (push) Successful in 2m40s
Details
This commit is contained in:
parent
e8a5dad8a6
commit
50edd54561
|
|
@ -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; }
|
.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 { 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; }
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import Tenants from '@/components/Tenants';
|
||||||
import Plans from '@/components/Plans';
|
import Plans from '@/components/Plans';
|
||||||
import Payments from '@/components/Payments';
|
import Payments from '@/components/Payments';
|
||||||
import AdminSettings from '@/components/AdminSettings';
|
import AdminSettings from '@/components/AdminSettings';
|
||||||
|
import ToastContainer from '@/components/Toast';
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ section: 'Visão Geral' },
|
{ section: 'Visão Geral' },
|
||||||
|
|
@ -291,6 +292,7 @@ export default function AdminPage() {
|
||||||
{renderPage()}
|
{renderPage()}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
<ToastContainer />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<ToastData[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleToast = (e: Event) => {
|
||||||
|
const customEvent = e as CustomEvent<ToastData>;
|
||||||
|
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 (
|
||||||
|
<div className="toast-container">
|
||||||
|
{toasts.map(t => {
|
||||||
|
const icons = {
|
||||||
|
success: <CheckCircle2 size={20} className="toast-icon success" />,
|
||||||
|
error: <XCircle size={20} className="toast-icon error" />,
|
||||||
|
warning: <AlertTriangle size={20} className="toast-icon warning" />,
|
||||||
|
info: <Info size={20} className="toast-icon info" />
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={t.id} className={`toast-item toast-${t.type}`}>
|
||||||
|
<div className="toast-content">
|
||||||
|
{icons[t.type]}
|
||||||
|
<span className="toast-message">{t.message}</span>
|
||||||
|
</div>
|
||||||
|
<button className="toast-close" onClick={() => removeToast(t.id)}>
|
||||||
|
<X size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { useState, useEffect, use } from 'react';
|
import { useState, useEffect, use } from 'react';
|
||||||
import { Calendar, Clock, User, Phone, Mail, ChevronLeft, ChevronRight, Check, Scissors, MapPin, Star, Loader } from 'lucide-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 }> }) {
|
export default function PublicBookingPage({ params }: { params: Promise<{ slug: string }> }) {
|
||||||
const { slug } = use(params);
|
const { slug } = use(params);
|
||||||
|
|
@ -147,7 +149,7 @@ export default function PublicBookingPage({ params }: { params: Promise<{ slug:
|
||||||
setBooked(true);
|
setBooked(true);
|
||||||
} else {
|
} else {
|
||||||
const errData = await res.json();
|
const errData = await res.json();
|
||||||
alert(errData.error || 'Erro ao realizar o agendamento.');
|
showToast(errData.error || 'Erro ao realizar o agendamento.', 'warning');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
@ -416,6 +418,7 @@ export default function PublicBookingPage({ params }: { params: Promise<{ slug:
|
||||||
Powered by <span style={{ fontWeight: 700, color: 'var(--accent)' }}>AgendaPRO</span>
|
Powered by <span style={{ fontWeight: 700, color: 'var(--accent)' }}>AgendaPRO</span>
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
<ToastContainer />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -898,3 +898,102 @@ tr:hover td { background: var(--bg-card-hover); }
|
||||||
.page-content { padding: 16px; }
|
.page-content { padding: 16px; }
|
||||||
.pricing-grid { grid-template-columns: 1fr; }
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import Services from '@/components/Services';
|
||||||
import Team from '@/components/Team';
|
import Team from '@/components/Team';
|
||||||
import Settings from '@/components/Settings';
|
import Settings from '@/components/Settings';
|
||||||
import Subscription from '@/components/Subscription';
|
import Subscription from '@/components/Subscription';
|
||||||
|
import ToastContainer from '@/components/Toast';
|
||||||
|
|
||||||
// Helper para cookies
|
// Helper para cookies
|
||||||
const getCookie = (name: string): string => {
|
const getCookie = (name: string): string => {
|
||||||
|
|
@ -149,6 +150,7 @@ export default function Home() {
|
||||||
{renderPage()}
|
{renderPage()}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
<ToastContainer />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Mail, Lock, Shield, ArrowRight, Building, Globe } from 'lucide-react';
|
import { Mail, Lock, Shield, ArrowRight, Building, Globe } from 'lucide-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { showToast } from '@/lib/toast';
|
||||||
|
import ToastContainer from '@/components/Toast';
|
||||||
|
|
||||||
export default function RegisterPage() {
|
export default function RegisterPage() {
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
|
|
@ -33,10 +35,10 @@ export default function RegisterPage() {
|
||||||
// Redireciona para o painel principal do app
|
// Redireciona para o painel principal do app
|
||||||
router.push('/');
|
router.push('/');
|
||||||
} else {
|
} else {
|
||||||
alert(data.error || 'Erro ao criar conta');
|
showToast(data.error || 'Erro ao criar conta', 'error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('Erro de conexão ao criar conta');
|
showToast('Erro de conexão ao criar conta', 'error');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -116,6 +118,7 @@ export default function RegisterPage() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ToastContainer />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useState, useEffect } from 'react';
|
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);
|
const hours = Array.from({ length: 12 }, (_, i) => i + 8);
|
||||||
|
|
||||||
|
|
@ -289,7 +290,7 @@ export default function Agenda() {
|
||||||
setNewNotes('');
|
setNewNotes('');
|
||||||
} else {
|
} else {
|
||||||
const errData = await res.json();
|
const errData = await res.json();
|
||||||
alert(errData.error || 'Erro ao realizar agendamento.');
|
showToast(errData.error || 'Erro ao realizar agendamento.', 'warning');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Plus, Search, Clock, DollarSign, X, Edit, Trash2, AlertTriangle, Layers } from 'lucide-react';
|
import { Plus, Search, Clock, DollarSign, X, Edit, Trash2, AlertTriangle, Layers } from 'lucide-react';
|
||||||
|
import { showToast } from '@/lib/toast';
|
||||||
|
|
||||||
interface Service {
|
interface Service {
|
||||||
id: string; name: string; category: string; price: number; duration: number; type: 'fixed' | 'starting' | 'variable'; description: string;
|
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);
|
setEditingService(null);
|
||||||
} else {
|
} else {
|
||||||
const errData = await res.json();
|
const errData = await res.json();
|
||||||
alert(errData.error || 'Erro ao salvar serviço.');
|
showToast(errData.error || 'Erro ao salvar serviço.', 'warning');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Check, Crown, Zap, Building2, Rocket, AlertTriangle, ShieldAlert, CreditCard, QrCode, FileText, CheckCircle2, ShieldCheck } from 'lucide-react';
|
import { Check, Crown, Zap, Building2, Rocket, AlertTriangle, ShieldAlert, CreditCard, QrCode, FileText, CheckCircle2, ShieldCheck } from 'lucide-react';
|
||||||
|
import { showToast } from '@/lib/toast';
|
||||||
|
|
||||||
const staticPlans = [
|
const staticPlans = [
|
||||||
{
|
{
|
||||||
|
|
@ -234,14 +235,14 @@ export default function Subscription({ currentPlan, onPlanChange, isLocked = fal
|
||||||
body: JSON.stringify({ slug, status: 'active' }),
|
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);
|
setSelectedPlan(null);
|
||||||
if (onPaymentSuccess) {
|
if (onPaymentSuccess) {
|
||||||
onPaymentSuccess();
|
onPaymentSuccess();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Erro ao conectar com API de simulação, utilizando fallback local de homologação.', 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);
|
setSelectedPlan(null);
|
||||||
if (onPaymentSuccess) {
|
if (onPaymentSuccess) {
|
||||||
onPaymentSuccess();
|
onPaymentSuccess();
|
||||||
|
|
@ -466,7 +467,7 @@ export default function Subscription({ currentPlan, onPlanChange, isLocked = fal
|
||||||
<label className="form-label" style={{ fontSize: '11px', textTransform: 'uppercase' }}>Código Copia e Cola</label>
|
<label className="form-label" style={{ fontSize: '11px', textTransform: 'uppercase' }}>Código Copia e Cola</label>
|
||||||
<div style={{ display: 'flex', gap: '8px' }}>
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
<input readOnly className="form-input" style={{ fontSize: '12px' }} value={successData.pixCode} />
|
<input readOnly className="form-input" style={{ fontSize: '12px' }} value={successData.pixCode} />
|
||||||
<button className="btn btn-secondary" onClick={() => { navigator.clipboard.writeText(successData.pixCode); alert('Código copiado!'); }}>Copiar</button>
|
<button className="btn btn-secondary" onClick={() => { navigator.clipboard.writeText(successData.pixCode); showToast('Código Pix copiado!', 'success'); }}>Copiar</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Plus, X, Clock, Edit, Trash2, Star, AlertTriangle } from 'lucide-react';
|
import { Plus, X, Clock, Edit, Trash2, Star, AlertTriangle } from 'lucide-react';
|
||||||
|
import { showToast } from '@/lib/toast';
|
||||||
|
|
||||||
interface Professional {
|
interface Professional {
|
||||||
id: string; name: string; role: string; phone: string; email: string; commission: number;
|
id: string; name: string; role: string; phone: string; email: string; commission: number;
|
||||||
|
|
@ -71,7 +72,7 @@ export default function Team() {
|
||||||
setEditingProf(null);
|
setEditingProf(null);
|
||||||
} else {
|
} else {
|
||||||
const errData = await res.json();
|
const errData = await res.json();
|
||||||
alert(errData.error || 'Erro ao salvar profissional.');
|
showToast(errData.error || 'Erro ao salvar profissional.', 'warning');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
|
||||||
|
|
@ -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<ToastData[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleToast = (e: Event) => {
|
||||||
|
const customEvent = e as CustomEvent<ToastData>;
|
||||||
|
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 (
|
||||||
|
<div className="toast-container">
|
||||||
|
{toasts.map(t => {
|
||||||
|
const icons = {
|
||||||
|
success: <CheckCircle2 size={20} className="toast-icon success" />,
|
||||||
|
error: <XCircle size={20} className="toast-icon error" />,
|
||||||
|
warning: <AlertTriangle size={20} className="toast-icon warning" />,
|
||||||
|
info: <Info size={20} className="toast-icon info" />
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={t.id} className={`toast-item toast-${t.type}`}>
|
||||||
|
<div className="toast-content">
|
||||||
|
{icons[t.type]}
|
||||||
|
<span className="toast-message">{t.message}</span>
|
||||||
|
</div>
|
||||||
|
<button className="toast-close" onClick={() => removeToast(t.id)}>
|
||||||
|
<X size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue