fix: corrigir build do docker e notificaçoes
Build and Deploy (Gitea Actions) / build-and-deploy (push) Successful in 2m36s
Details
Build and Deploy (Gitea Actions) / build-and-deploy (push) Successful in 2m36s
Details
This commit is contained in:
parent
d8ae863327
commit
2bf14b0669
|
|
@ -5,7 +5,8 @@ RUN npm ci --omit=dev
|
||||||
|
|
||||||
FROM node:20-alpine AS builder
|
FROM node:20-alpine AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY package.json package-lock.json* ./
|
||||||
|
RUN npm ci
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^22.15.21",
|
"@types/node": "^22.15.21",
|
||||||
|
"@types/pg": "^8.11.10",
|
||||||
"@types/react": "^19.1.4",
|
"@types/react": "^19.1.4",
|
||||||
"@types/react-dom": "^19.1.5",
|
"@types/react-dom": "^19.1.5",
|
||||||
"lucide-react": "^0.511.0",
|
"lucide-react": "^0.511.0",
|
||||||
|
|
@ -647,6 +648,17 @@
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/pg": {
|
||||||
|
"version": "8.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz",
|
||||||
|
"integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*",
|
||||||
|
"pg-protocol": "*",
|
||||||
|
"pg-types": "^2.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.2.15",
|
"version": "19.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz",
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
"@types/node": "^22.15.21",
|
"@types/node": "^22.15.21",
|
||||||
"@types/react": "^19.1.4",
|
"@types/react": "^19.1.4",
|
||||||
"@types/react-dom": "^19.1.5",
|
"@types/react-dom": "^19.1.5",
|
||||||
|
"@types/pg": "^8.11.10",
|
||||||
"lucide-react": "^0.511.0",
|
"lucide-react": "^0.511.0",
|
||||||
"pg": "^8.16.0"
|
"pg": "^8.16.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { useState } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { LayoutDashboard, Users, CreditCard, Package, Settings, Shield, TrendingUp, Bell, LogOut } from 'lucide-react';
|
import { LayoutDashboard, Users, CreditCard, Package, Settings, Shield, TrendingUp, Bell, LogOut, User, CheckCircle, AlertCircle } from 'lucide-react';
|
||||||
import AdminDashboard from '@/components/AdminDashboard';
|
import AdminDashboard from '@/components/AdminDashboard';
|
||||||
import Tenants from '@/components/Tenants';
|
import Tenants from '@/components/Tenants';
|
||||||
import Plans from '@/components/Plans';
|
import Plans from '@/components/Plans';
|
||||||
|
|
@ -26,8 +26,79 @@ const pages: Record<string, { title: string; subtitle: string }> = {
|
||||||
settings: { title: 'Configurações', subtitle: 'Configurações do sistema' },
|
settings: { title: 'Configurações', subtitle: 'Configurações do sistema' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockNotifications = [
|
||||||
|
{ id: 1, title: 'Nova Assinatura', message: 'Barbearia do Zé contratou o plano Starter', type: 'success', time: '5 min atrás', read: false },
|
||||||
|
{ id: 2, title: 'Falha no Pagamento', message: 'Salão Glamour não conseguiu renovar a assinatura', type: 'warning', time: '1 hora atrás', read: true },
|
||||||
|
{ id: 3, title: 'Ticket de Suporte', message: 'Novo chamado aberto por Studio Bella Nails', type: 'info', time: '2 horas atrás', read: true },
|
||||||
|
];
|
||||||
|
|
||||||
export default function AdminPage() {
|
export default function AdminPage() {
|
||||||
const [currentPage, setCurrentPage] = useState('dashboard');
|
const [currentPage, setCurrentPage] = useState('dashboard');
|
||||||
|
|
||||||
|
const [showNotifications, setShowNotifications] = useState(false);
|
||||||
|
const [showProfile, setShowProfile] = useState(false);
|
||||||
|
const [notifications, setNotifications] = useState(mockNotifications);
|
||||||
|
|
||||||
|
const notifRef = useRef<HTMLDivElement>(null);
|
||||||
|
const profileRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
if (notifRef.current && !notifRef.current.contains(e.target as Node)) setShowNotifications(false);
|
||||||
|
if (profileRef.current && !profileRef.current.contains(e.target as Node)) setShowProfile(false);
|
||||||
|
};
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const playNotificationSound = () => {
|
||||||
|
try {
|
||||||
|
const audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||||
|
const oscillator = audioCtx.createOscillator();
|
||||||
|
const gainNode = audioCtx.createGain();
|
||||||
|
|
||||||
|
oscillator.type = 'sine';
|
||||||
|
oscillator.frequency.setValueAtTime(880, audioCtx.currentTime); // A5
|
||||||
|
oscillator.frequency.exponentialRampToValueAtTime(1760, audioCtx.currentTime + 0.1); // A6
|
||||||
|
|
||||||
|
gainNode.gain.setValueAtTime(0.1, audioCtx.currentTime);
|
||||||
|
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.3);
|
||||||
|
|
||||||
|
oscillator.connect(gainNode);
|
||||||
|
gainNode.connect(audioCtx.destination);
|
||||||
|
|
||||||
|
oscillator.start();
|
||||||
|
oscillator.stop(audioCtx.currentTime + 0.3);
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Audio not supported or blocked');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNotificationClick = () => {
|
||||||
|
if (!showNotifications) {
|
||||||
|
const unreadCount = notifications.filter(n => !n.read).length;
|
||||||
|
if (unreadCount > 0) {
|
||||||
|
playNotificationSound();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setShowNotifications(!showNotifications);
|
||||||
|
setShowProfile(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProfileClick = () => {
|
||||||
|
setShowProfile(!showProfile);
|
||||||
|
setShowNotifications(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const markAllAsRead = () => {
|
||||||
|
setNotifications(notifications.map(n => ({ ...n, read: true })));
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
window.location.href = '/';
|
||||||
|
};
|
||||||
|
|
||||||
|
const unreadCount = notifications.filter(n => !n.read).length;
|
||||||
|
|
||||||
const renderPage = () => {
|
const renderPage = () => {
|
||||||
switch (currentPage) {
|
switch (currentPage) {
|
||||||
|
|
@ -85,11 +156,77 @@ export default function AdminPage() {
|
||||||
<p>{pages[currentPage]?.subtitle}</p>
|
<p>{pages[currentPage]?.subtitle}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-gap">
|
<div className="flex-gap">
|
||||||
<button className="btn-ghost btn-icon" style={{ position: 'relative' }}>
|
<div style={{ position: 'relative' }} ref={notifRef}>
|
||||||
<Bell size={20} />
|
<button className="btn-ghost btn-icon" style={{ position: 'relative' }} onClick={handleNotificationClick}>
|
||||||
<span style={{ position: 'absolute', top: 4, right: 4, width: 8, height: 8, borderRadius: '50%', background: 'var(--danger)' }} />
|
<Bell size={20} />
|
||||||
</button>
|
{unreadCount > 0 && (
|
||||||
<div className="avatar" style={{ width: 36, height: 36, fontSize: 13 }}>SA</div>
|
<span style={{ position: 'absolute', top: 4, right: 4, width: 10, height: 10, borderRadius: '50%', background: 'var(--danger)', border: '2px solid var(--bg-card)' }} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showNotifications && (
|
||||||
|
<div className="card" style={{
|
||||||
|
position: 'absolute', top: '100%', right: 0, marginTop: '8px', width: '320px',
|
||||||
|
padding: 0, zIndex: 100, boxShadow: 'var(--shadow-lg)', border: '1px solid var(--border-color)',
|
||||||
|
animation: 'slideUp 0.2s ease-out forwards'
|
||||||
|
}}>
|
||||||
|
<div className="flex-between" style={{ padding: '12px 16px', borderBottom: '1px solid var(--border-color)' }}>
|
||||||
|
<h3 style={{ fontSize: '14px', fontWeight: 600 }}>Notificações</h3>
|
||||||
|
{unreadCount > 0 && (
|
||||||
|
<button className="btn-ghost" style={{ fontSize: '12px', padding: '4px 8px' }} onClick={markAllAsRead}>
|
||||||
|
Marcar lidas
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{ maxHeight: '300px', overflowY: 'auto' }}>
|
||||||
|
{notifications.map(n => (
|
||||||
|
<div key={n.id} style={{
|
||||||
|
padding: '12px 16px', borderBottom: '1px solid var(--border-color)',
|
||||||
|
background: n.read ? 'transparent' : 'rgba(59, 130, 246, 0.05)',
|
||||||
|
display: 'flex', gap: '12px', alignItems: 'flex-start'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
width: '32px', height: '32px', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
|
||||||
|
background: n.type === 'success' ? 'rgba(34, 197, 94, 0.1)' : n.type === 'warning' ? 'rgba(245, 158, 11, 0.1)' : 'rgba(59, 130, 246, 0.1)',
|
||||||
|
color: n.type === 'success' ? 'var(--success)' : n.type === 'warning' ? 'var(--warning)' : 'var(--primary)'
|
||||||
|
}}>
|
||||||
|
{n.type === 'success' ? <CheckCircle size={16} /> : n.type === 'warning' ? <AlertCircle size={16} /> : <Bell size={16} />}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '13px', fontWeight: n.read ? 500 : 600, color: 'var(--text-primary)' }}>{n.title}</div>
|
||||||
|
<div style={{ fontSize: '12px', color: 'var(--text-secondary)', marginTop: '2px', lineHeight: 1.4 }}>{n.message}</div>
|
||||||
|
<div style={{ fontSize: '11px', color: 'var(--text-muted)', marginTop: '4px' }}>{n.time}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ position: 'relative' }} ref={profileRef}>
|
||||||
|
<div className="avatar" style={{ width: 36, height: 36, fontSize: 13, cursor: 'pointer', transition: 'transform 0.2s' }} onClick={handleProfileClick}>SA</div>
|
||||||
|
|
||||||
|
{showProfile && (
|
||||||
|
<div className="card" style={{
|
||||||
|
position: 'absolute', top: '100%', right: 0, marginTop: '8px', width: '240px',
|
||||||
|
padding: '8px', zIndex: 100, boxShadow: 'var(--shadow-lg)', border: '1px solid var(--border-color)',
|
||||||
|
animation: 'slideUp 0.2s ease-out forwards'
|
||||||
|
}}>
|
||||||
|
<div style={{ padding: '12px', borderBottom: '1px solid var(--border-color)', marginBottom: '8px' }}>
|
||||||
|
<div style={{ fontWeight: 600, fontSize: '14px' }}>Super Admin</div>
|
||||||
|
<div style={{ color: 'var(--text-muted)', fontSize: '12px', marginTop: '2px' }}>admin@agendapro.com</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className="btn-ghost" style={{ width: '100%', justifyContent: 'flex-start', padding: '10px 12px', fontSize: '13px' }}>
|
||||||
|
<User size={16} style={{ marginRight: '8px' }} /> Meu Perfil
|
||||||
|
</button>
|
||||||
|
<button className="btn-ghost" onClick={logout} style={{ width: '100%', justifyContent: 'flex-start', padding: '10px 12px', fontSize: '13px', color: 'var(--danger)' }}>
|
||||||
|
<LogOut size={16} style={{ marginRight: '8px' }} /> Sair do Painel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className="page-content fade-in" key={currentPage}>
|
<div className="page-content fade-in" key={currentPage}>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { Bell, Menu, Search } from 'lucide-react';
|
import { Bell, Menu, Search, LogOut, User, CheckCircle, AlertCircle } from 'lucide-react';
|
||||||
|
|
||||||
interface TopbarProps {
|
interface TopbarProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -7,17 +7,94 @@ interface TopbarProps {
|
||||||
onMenuClick: () => void;
|
onMenuClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mockNotifications = [
|
||||||
|
{ id: 1, title: 'Novo Agendamento', message: 'João Silva marcou Cabelo às 14:00', type: 'info', time: '10 min atrás', read: false },
|
||||||
|
{ id: 2, title: 'Pagamento Recebido', message: 'Assinatura renovada com sucesso', type: 'success', time: '2 horas atrás', read: true },
|
||||||
|
{ id: 3, title: 'Estoque Baixo', message: 'Pomada modeladora abaixo de 5 unidades', type: 'warning', time: '1 dia atrás', read: true },
|
||||||
|
];
|
||||||
|
|
||||||
export default function Topbar({ title, subtitle, onMenuClick }: TopbarProps) {
|
export default function Topbar({ title, subtitle, onMenuClick }: TopbarProps) {
|
||||||
const [avatarInitials, setAvatarInitials] = useState('AD');
|
const [avatarInitials, setAvatarInitials] = useState('AD');
|
||||||
|
const [tenantName, setTenantName] = useState('Administrador');
|
||||||
|
const [email, setEmail] = useState('admin@agendapro.com');
|
||||||
|
|
||||||
|
const [showNotifications, setShowNotifications] = useState(false);
|
||||||
|
const [showProfile, setShowProfile] = useState(false);
|
||||||
|
const [notifications, setNotifications] = useState(mockNotifications);
|
||||||
|
|
||||||
|
const notifRef = useRef<HTMLDivElement>(null);
|
||||||
|
const profileRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedName = localStorage.getItem('agendapro_tenant_name');
|
const savedName = localStorage.getItem('agendapro_tenant_name');
|
||||||
if (savedName) {
|
if (savedName) {
|
||||||
|
setTenantName(savedName);
|
||||||
const initials = savedName.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase();
|
const initials = savedName.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase();
|
||||||
setAvatarInitials(initials || 'BJ');
|
setAvatarInitials(initials || 'BJ');
|
||||||
}
|
}
|
||||||
|
const savedEmail = localStorage.getItem('agendapro_email');
|
||||||
|
if (savedEmail) setEmail(savedEmail);
|
||||||
|
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
if (notifRef.current && !notifRef.current.contains(e.target as Node)) setShowNotifications(false);
|
||||||
|
if (profileRef.current && !profileRef.current.contains(e.target as Node)) setShowProfile(false);
|
||||||
|
};
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const playNotificationSound = () => {
|
||||||
|
try {
|
||||||
|
const audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||||
|
const oscillator = audioCtx.createOscillator();
|
||||||
|
const gainNode = audioCtx.createGain();
|
||||||
|
|
||||||
|
oscillator.type = 'sine';
|
||||||
|
oscillator.frequency.setValueAtTime(880, audioCtx.currentTime); // A5
|
||||||
|
oscillator.frequency.exponentialRampToValueAtTime(1760, audioCtx.currentTime + 0.1); // A6
|
||||||
|
|
||||||
|
gainNode.gain.setValueAtTime(0.1, audioCtx.currentTime);
|
||||||
|
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.3);
|
||||||
|
|
||||||
|
oscillator.connect(gainNode);
|
||||||
|
gainNode.connect(audioCtx.destination);
|
||||||
|
|
||||||
|
oscillator.start();
|
||||||
|
oscillator.stop(audioCtx.currentTime + 0.3);
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Audio not supported or blocked');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNotificationClick = () => {
|
||||||
|
if (!showNotifications) {
|
||||||
|
// Simulate receiving a new notification and playing sound the first time they open
|
||||||
|
const unreadCount = notifications.filter(n => !n.read).length;
|
||||||
|
if (unreadCount > 0) {
|
||||||
|
playNotificationSound();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setShowNotifications(!showNotifications);
|
||||||
|
setShowProfile(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProfileClick = () => {
|
||||||
|
setShowProfile(!showProfile);
|
||||||
|
setShowNotifications(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const markAllAsRead = () => {
|
||||||
|
setNotifications(notifications.map(n => ({ ...n, read: true })));
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
localStorage.removeItem('agendapro_tenant_slug');
|
||||||
|
localStorage.removeItem('agendapro_tenant_name');
|
||||||
|
window.location.href = '/';
|
||||||
|
};
|
||||||
|
|
||||||
|
const unreadCount = notifications.filter(n => !n.read).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="topbar">
|
<header className="topbar">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
||||||
|
|
@ -29,20 +106,90 @@ export default function Topbar({ title, subtitle, onMenuClick }: TopbarProps) {
|
||||||
<p>{subtitle}</p>
|
<p>{subtitle}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="topbar-right">
|
<div className="topbar-right">
|
||||||
<div className="search-box">
|
<div className="search-box">
|
||||||
<Search />
|
<Search size={16} />
|
||||||
<input className="form-input" placeholder="Buscar..." style={{ width: '240px', paddingLeft: '42px' }} />
|
<input className="form-input" placeholder="Buscar..." style={{ width: '240px', paddingLeft: '36px' }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ position: 'relative' }} ref={notifRef}>
|
||||||
|
<button className="btn-ghost btn-icon" style={{ position: 'relative' }} onClick={handleNotificationClick}>
|
||||||
|
<Bell size={20} />
|
||||||
|
{unreadCount > 0 && (
|
||||||
|
<span style={{
|
||||||
|
position: 'absolute', top: '4px', right: '4px',
|
||||||
|
width: '10px', height: '10px', borderRadius: '50%',
|
||||||
|
background: 'var(--danger)', border: '2px solid var(--bg-card)'
|
||||||
|
}} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showNotifications && (
|
||||||
|
<div className="card" style={{
|
||||||
|
position: 'absolute', top: '100%', right: 0, marginTop: '8px', width: '320px',
|
||||||
|
padding: 0, zIndex: 100, boxShadow: 'var(--shadow-lg)', border: '1px solid var(--border-color)',
|
||||||
|
animation: 'slideUp 0.2s ease-out forwards'
|
||||||
|
}}>
|
||||||
|
<div className="flex-between" style={{ padding: '12px 16px', borderBottom: '1px solid var(--border-color)' }}>
|
||||||
|
<h3 style={{ fontSize: '14px', fontWeight: 600 }}>Notificações</h3>
|
||||||
|
{unreadCount > 0 && (
|
||||||
|
<button className="btn-ghost" style={{ fontSize: '12px', padding: '4px 8px' }} onClick={markAllAsRead}>
|
||||||
|
Marcar lidas
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{ maxHeight: '300px', overflowY: 'auto' }}>
|
||||||
|
{notifications.map(n => (
|
||||||
|
<div key={n.id} style={{
|
||||||
|
padding: '12px 16px', borderBottom: '1px solid var(--border-color)',
|
||||||
|
background: n.read ? 'transparent' : 'rgba(59, 130, 246, 0.05)',
|
||||||
|
display: 'flex', gap: '12px', alignItems: 'flex-start'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
width: '32px', height: '32px', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
|
||||||
|
background: n.type === 'success' ? 'rgba(34, 197, 94, 0.1)' : n.type === 'warning' ? 'rgba(245, 158, 11, 0.1)' : 'rgba(59, 130, 246, 0.1)',
|
||||||
|
color: n.type === 'success' ? 'var(--success)' : n.type === 'warning' ? 'var(--warning)' : 'var(--primary)'
|
||||||
|
}}>
|
||||||
|
{n.type === 'success' ? <CheckCircle size={16} /> : n.type === 'warning' ? <AlertCircle size={16} /> : <Bell size={16} />}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '13px', fontWeight: n.read ? 500 : 600, color: 'var(--text-primary)' }}>{n.title}</div>
|
||||||
|
<div style={{ fontSize: '12px', color: 'var(--text-secondary)', marginTop: '2px', lineHeight: 1.4 }}>{n.message}</div>
|
||||||
|
<div style={{ fontSize: '11px', color: 'var(--text-muted)', marginTop: '4px' }}>{n.time}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ position: 'relative' }} ref={profileRef}>
|
||||||
|
<div className="avatar" style={{ cursor: 'pointer', transition: 'transform 0.2s' }} onClick={handleProfileClick}>
|
||||||
|
{avatarInitials}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showProfile && (
|
||||||
|
<div className="card" style={{
|
||||||
|
position: 'absolute', top: '100%', right: 0, marginTop: '8px', width: '240px',
|
||||||
|
padding: '8px', zIndex: 100, boxShadow: 'var(--shadow-lg)', border: '1px solid var(--border-color)',
|
||||||
|
animation: 'slideUp 0.2s ease-out forwards'
|
||||||
|
}}>
|
||||||
|
<div style={{ padding: '12px', borderBottom: '1px solid var(--border-color)', marginBottom: '8px' }}>
|
||||||
|
<div style={{ fontWeight: 600, fontSize: '14px' }}>{tenantName}</div>
|
||||||
|
<div style={{ color: 'var(--text-muted)', fontSize: '12px', marginTop: '2px' }}>{email}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className="btn-ghost" style={{ width: '100%', justifyContent: 'flex-start', padding: '10px 12px', fontSize: '13px' }}>
|
||||||
|
<User size={16} style={{ marginRight: '8px' }} /> Meu Perfil
|
||||||
|
</button>
|
||||||
|
<button className="btn-ghost" onClick={logout} style={{ width: '100%', justifyContent: 'flex-start', padding: '10px 12px', fontSize: '13px', color: 'var(--danger)' }}>
|
||||||
|
<LogOut size={16} style={{ marginRight: '8px' }} /> Sair da conta
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button className="btn-ghost btn-icon" style={{ position: 'relative' }}>
|
|
||||||
<Bell size={20} />
|
|
||||||
<span style={{
|
|
||||||
position: 'absolute', top: '4px', right: '4px',
|
|
||||||
width: '8px', height: '8px', borderRadius: '50%',
|
|
||||||
background: 'var(--danger)'
|
|
||||||
}} />
|
|
||||||
</button>
|
|
||||||
<div className="avatar">{avatarInitials}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue