- Implemented error handling for unhandled promise rejections related to message channels, improving stability during push notification operations. - Updated the PushNotificationManager component to manage push subscription registration with timeouts, preventing application hangs. - Enhanced the sidebar and chat components to display user avatars, improving user experience and visual consistency. - Refactored email processing logic to support scheduled email sending, integrating new backend functionalities for better email management. - Improved overall error handling and logging across components to reduce console spam and enhance debugging capabilities.
442 lines
14 KiB
Svelte
442 lines
14 KiB
Svelte
<script lang="ts">
|
|
import {
|
|
chatAberto,
|
|
chatMinimizado,
|
|
conversaAtiva,
|
|
fecharChat,
|
|
minimizarChat,
|
|
maximizarChat,
|
|
abrirChat,
|
|
} from "$lib/stores/chatStore";
|
|
import { useQuery } from "convex-svelte";
|
|
import { api } from "@sgse-app/backend/convex/_generated/api";
|
|
import ChatList from "./ChatList.svelte";
|
|
import ChatWindow from "./ChatWindow.svelte";
|
|
import { authStore } from "$lib/stores/auth.svelte";
|
|
import { getAvatarUrl } from "$lib/utils/avatarGenerator";
|
|
|
|
const count = useQuery(api.chat.contarNotificacoesNaoLidas, {});
|
|
|
|
let isOpen = $state(false);
|
|
let isMinimized = $state(false);
|
|
let activeConversation = $state<string | null>(null);
|
|
|
|
// Função para obter a URL do avatar/foto do usuário logado
|
|
const avatarUrlDoUsuario = $derived(() => {
|
|
const usuario = authStore.usuario;
|
|
if (!usuario) return null;
|
|
|
|
// Prioridade: fotoPerfilUrl > avatar > fallback com nome
|
|
if (usuario.fotoPerfilUrl) {
|
|
return usuario.fotoPerfilUrl;
|
|
}
|
|
if (usuario.avatar) {
|
|
return getAvatarUrl(usuario.avatar);
|
|
}
|
|
// Fallback: gerar avatar baseado no nome
|
|
return getAvatarUrl(usuario.nome);
|
|
});
|
|
|
|
// Posição do widget (arrastável)
|
|
let position = $state({ x: 0, y: 0 });
|
|
let isDragging = $state(false);
|
|
let dragStart = $state({ x: 0, y: 0 });
|
|
let isAnimating = $state(false);
|
|
|
|
// Sincronizar com stores
|
|
$effect(() => {
|
|
isOpen = $chatAberto;
|
|
});
|
|
|
|
$effect(() => {
|
|
isMinimized = $chatMinimizado;
|
|
});
|
|
|
|
$effect(() => {
|
|
activeConversation = $conversaAtiva;
|
|
});
|
|
|
|
function handleToggle() {
|
|
if (isOpen && !isMinimized) {
|
|
minimizarChat();
|
|
} else {
|
|
abrirChat();
|
|
}
|
|
}
|
|
|
|
function handleClose() {
|
|
fecharChat();
|
|
}
|
|
|
|
function handleMinimize() {
|
|
minimizarChat();
|
|
}
|
|
|
|
function handleMaximize() {
|
|
maximizarChat();
|
|
}
|
|
|
|
// Funcionalidade de arrastar
|
|
function handleMouseDown(e: MouseEvent) {
|
|
if (e.button !== 0) return; // Apenas botão esquerdo
|
|
isDragging = true;
|
|
dragStart = {
|
|
x: e.clientX - position.x,
|
|
y: e.clientY - position.y,
|
|
};
|
|
document.body.classList.add('dragging');
|
|
e.preventDefault();
|
|
}
|
|
|
|
function handleMouseMove(e: MouseEvent) {
|
|
if (!isDragging) return;
|
|
|
|
const newX = e.clientX - dragStart.x;
|
|
const newY = e.clientY - dragStart.y;
|
|
|
|
// Dimensões do widget
|
|
const widgetWidth = isOpen && !isMinimized ? 440 : 72;
|
|
const widgetHeight = isOpen && !isMinimized ? 680 : 72;
|
|
|
|
// Limites da tela com margem de segurança
|
|
const minX = -(widgetWidth - 100); // Permitir até 100px visíveis
|
|
const maxX = window.innerWidth - 100; // Manter 100px dentro da tela
|
|
const minY = -(widgetHeight - 100);
|
|
const maxY = window.innerHeight - 100;
|
|
|
|
position = {
|
|
x: Math.max(minX, Math.min(newX, maxX)),
|
|
y: Math.max(minY, Math.min(newY, maxY)),
|
|
};
|
|
}
|
|
|
|
function handleMouseUp() {
|
|
if (isDragging) {
|
|
isDragging = false;
|
|
document.body.classList.remove('dragging');
|
|
// Garantir que está dentro dos limites ao soltar
|
|
ajustarPosicao();
|
|
}
|
|
}
|
|
|
|
function ajustarPosicao() {
|
|
isAnimating = true;
|
|
|
|
// Dimensões do widget
|
|
const widgetWidth = isOpen && !isMinimized ? 440 : 72;
|
|
const widgetHeight = isOpen && !isMinimized ? 680 : 72;
|
|
|
|
// Verificar se está fora dos limites
|
|
let newX = position.x;
|
|
let newY = position.y;
|
|
|
|
// Ajustar X
|
|
if (newX < -(widgetWidth - 100)) {
|
|
newX = -(widgetWidth - 100);
|
|
} else if (newX > window.innerWidth - 100) {
|
|
newX = window.innerWidth - 100;
|
|
}
|
|
|
|
// Ajustar Y
|
|
if (newY < -(widgetHeight - 100)) {
|
|
newY = -(widgetHeight - 100);
|
|
} else if (newY > window.innerHeight - 100) {
|
|
newY = window.innerHeight - 100;
|
|
}
|
|
|
|
position = { x: newX, y: newY };
|
|
|
|
setTimeout(() => {
|
|
isAnimating = false;
|
|
}, 300);
|
|
}
|
|
|
|
// Event listeners globais
|
|
if (typeof window !== 'undefined') {
|
|
window.addEventListener('mousemove', handleMouseMove);
|
|
window.addEventListener('mouseup', handleMouseUp);
|
|
}
|
|
</script>
|
|
|
|
<!-- Botão flutuante MODERNO E ARRASTÁVEL -->
|
|
{#if !isOpen || isMinimized}
|
|
<button
|
|
type="button"
|
|
class="fixed group relative border-0 backdrop-blur-xl"
|
|
style="
|
|
z-index: 99999 !important;
|
|
width: 4.5rem;
|
|
height: 4.5rem;
|
|
bottom: {position.y === 0 ? '1.5rem' : `${window.innerHeight - position.y - 72}px`};
|
|
right: {position.x === 0 ? '1.5rem' : `${window.innerWidth - position.x - 72}px`};
|
|
position: fixed !important;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
|
|
box-shadow:
|
|
0 20px 60px -10px rgba(102, 126, 234, 0.5),
|
|
0 10px 30px -5px rgba(118, 75, 162, 0.4),
|
|
0 0 0 1px rgba(255, 255, 255, 0.1) inset;
|
|
border-radius: 50%;
|
|
cursor: {isDragging ? 'grabbing' : 'grab'};
|
|
transform: {isDragging ? 'scale(1.05)' : 'scale(1)'};
|
|
transition: {isAnimating ? 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)' : 'transform 0.2s, box-shadow 0.3s'};
|
|
"
|
|
onclick={handleToggle}
|
|
onmousedown={handleMouseDown}
|
|
aria-label="Abrir chat"
|
|
>
|
|
<!-- Anel de brilho rotativo -->
|
|
<div class="absolute inset-0 rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-500"
|
|
style="background: conic-gradient(from 0deg, transparent 0%, rgba(255,255,255,0.3) 50%, transparent 100%); animation: rotate 3s linear infinite;">
|
|
</div>
|
|
|
|
<!-- Ondas de pulso -->
|
|
<div class="absolute inset-0 rounded-full" style="animation: pulse-ring 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;"></div>
|
|
|
|
<!-- Ícone de chat moderno com efeito 3D -->
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
class="w-7 h-7 relative z-10 text-white group-hover:scale-110 transition-all duration-500"
|
|
style="filter: drop-shadow(0 4px 12px rgba(0,0,0,0.4));"
|
|
>
|
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
|
<circle cx="9" cy="10" r="1" fill="currentColor"/>
|
|
<circle cx="12" cy="10" r="1" fill="currentColor"/>
|
|
<circle cx="15" cy="10" r="1" fill="currentColor"/>
|
|
</svg>
|
|
|
|
<!-- Badge ULTRA PREMIUM com gradiente e brilho -->
|
|
{#if count?.data && count.data > 0}
|
|
<span
|
|
class="absolute -top-1 -right-1 flex h-8 w-8 items-center justify-center rounded-full text-white text-xs font-black z-20"
|
|
style="
|
|
background: linear-gradient(135deg, #ff416c, #ff4b2b);
|
|
box-shadow:
|
|
0 8px 24px -4px rgba(255, 65, 108, 0.6),
|
|
0 4px 12px -2px rgba(255, 75, 43, 0.4),
|
|
0 0 0 3px rgba(255, 255, 255, 0.3),
|
|
0 0 0 5px rgba(255, 65, 108, 0.2);
|
|
animation: badge-bounce 2s ease-in-out infinite;
|
|
"
|
|
>
|
|
{count.data > 9 ? "9+" : count.data}
|
|
</span>
|
|
{/if}
|
|
|
|
<!-- Indicador de arrastável -->
|
|
<div class="absolute -bottom-2 left-1/2 transform -translate-x-1/2 flex gap-1 opacity-50 group-hover:opacity-100 transition-opacity">
|
|
<div class="w-1 h-1 rounded-full bg-white"></div>
|
|
<div class="w-1 h-1 rounded-full bg-white"></div>
|
|
<div class="w-1 h-1 rounded-full bg-white"></div>
|
|
</div>
|
|
</button>
|
|
{/if}
|
|
|
|
<!-- Janela do Chat ULTRA MODERNA E ARRASTÁVEL -->
|
|
{#if isOpen && !isMinimized}
|
|
<div
|
|
class="fixed flex flex-col overflow-hidden backdrop-blur-2xl"
|
|
style="
|
|
z-index: 99999 !important;
|
|
bottom: {position.y === 0 ? '1.5rem' : `${window.innerHeight - position.y - 680}px`};
|
|
right: {position.x === 0 ? '1.5rem' : `${window.innerWidth - position.x - 440}px`};
|
|
width: 440px;
|
|
height: 680px;
|
|
max-width: calc(100vw - 3rem);
|
|
max-height: calc(100vh - 3rem);
|
|
position: fixed !important;
|
|
background: linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(249,250,251,0.98) 100%);
|
|
border-radius: 24px;
|
|
box-shadow:
|
|
0 32px 64px -12px rgba(0, 0, 0, 0.15),
|
|
0 16px 32px -8px rgba(0, 0, 0, 0.1),
|
|
0 0 0 1px rgba(0, 0, 0, 0.05),
|
|
0 0 0 1px rgba(255, 255, 255, 0.5) inset;
|
|
animation: slideInScale 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
transition: {isAnimating ? 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)' : 'none'};
|
|
"
|
|
>
|
|
<!-- Header ULTRA PREMIUM com gradiente glassmorphism -->
|
|
<div
|
|
class="flex items-center justify-between px-6 py-5 text-white relative overflow-hidden"
|
|
style="
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
|
|
box-shadow: 0 8px 32px -4px rgba(102, 126, 234, 0.3);
|
|
cursor: {isDragging ? 'grabbing' : 'grab'};
|
|
"
|
|
onmousedown={handleMouseDown}
|
|
role="button"
|
|
tabindex="0"
|
|
aria-label="Arrastar janela do chat"
|
|
>
|
|
<!-- Efeitos de fundo animados -->
|
|
<div class="absolute inset-0 opacity-30" style="background: radial-gradient(circle at 20% 50%, rgba(255,255,255,0.3) 0%, transparent 50%);"></div>
|
|
<div class="absolute inset-0 opacity-20" style="background: linear-gradient(45deg, transparent 30%, rgba(255,255,255,0.1) 50%, transparent 70%); animation: shimmer 3s infinite;"></div>
|
|
<!-- Título com avatar/foto do usuário logado -->
|
|
<h2 class="text-xl font-bold flex items-center gap-3 relative z-10">
|
|
<!-- Avatar/Foto do usuário logado com efeito glassmorphism -->
|
|
<div class="relative flex items-center justify-center w-10 h-10 rounded-xl overflow-hidden" style="background: rgba(255,255,255,0.2); backdrop-filter: blur(10px); box-shadow: 0 4px 12px rgba(0,0,0,0.1), 0 0 0 1px rgba(255,255,255,0.2) inset;">
|
|
{#if avatarUrlDoUsuario()}
|
|
<img
|
|
src={avatarUrlDoUsuario()}
|
|
alt={authStore.usuario?.nome || "Usuário"}
|
|
class="w-full h-full object-cover"
|
|
/>
|
|
{:else}
|
|
<!-- Fallback: ícone de chat genérico -->
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
class="w-5 h-5"
|
|
style="filter: drop-shadow(0 2px 6px rgba(0,0,0,0.2));"
|
|
>
|
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
|
<line x1="9" y1="10" x2="15" y2="10"/>
|
|
<line x1="9" y1="14" x2="13" y2="14"/>
|
|
</svg>
|
|
{/if}
|
|
</div>
|
|
<span class="tracking-wide font-extrabold" style="text-shadow: 0 2px 8px rgba(0,0,0,0.3); letter-spacing: 0.02em;">Mensagens</span>
|
|
</h2>
|
|
|
|
<!-- Botões de controle modernos -->
|
|
<div class="flex items-center gap-2 relative z-10">
|
|
<!-- Botão minimizar MODERNO -->
|
|
<button
|
|
type="button"
|
|
class="flex items-center justify-center w-10 h-10 rounded-xl transition-all duration-300 group relative overflow-hidden"
|
|
style="background: rgba(255,255,255,0.15); backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.2);"
|
|
onclick={handleMinimize}
|
|
aria-label="Minimizar"
|
|
>
|
|
<div class="absolute inset-0 bg-white/0 group-hover:bg-white/20 transition-colors duration-300"></div>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2.5"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
class="w-5 h-5 relative z-10 group-hover:scale-110 transition-transform duration-300"
|
|
style="filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));"
|
|
>
|
|
<line x1="5" y1="12" x2="19" y2="12"/>
|
|
</svg>
|
|
</button>
|
|
|
|
<!-- Botão fechar MODERNO -->
|
|
<button
|
|
type="button"
|
|
class="flex items-center justify-center w-10 h-10 rounded-xl transition-all duration-300 group relative overflow-hidden"
|
|
style="background: rgba(255,255,255,0.15); backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.2);"
|
|
onclick={handleClose}
|
|
aria-label="Fechar"
|
|
>
|
|
<div class="absolute inset-0 bg-red-500/0 group-hover:bg-red-500/30 transition-colors duration-300"></div>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2.5"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
class="w-5 h-5 relative z-10 group-hover:scale-110 group-hover:rotate-90 transition-all duration-300"
|
|
style="filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));"
|
|
>
|
|
<line x1="18" y1="6" x2="6" y2="18"/>
|
|
<line x1="6" y1="6" x2="18" y2="18"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Conteúdo -->
|
|
<div class="flex-1 overflow-hidden">
|
|
{#if !activeConversation}
|
|
<ChatList />
|
|
{:else}
|
|
<ChatWindow conversaId={activeConversation} />
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<style>
|
|
/* Animação do badge com bounce suave */
|
|
@keyframes badge-bounce {
|
|
0%, 100% {
|
|
transform: scale(1) translateY(0);
|
|
}
|
|
50% {
|
|
transform: scale(1.08) translateY(-2px);
|
|
}
|
|
}
|
|
|
|
/* Animação de entrada da janela com escala e bounce */
|
|
@keyframes slideInScale {
|
|
0% {
|
|
opacity: 0;
|
|
transform: translateY(30px) scale(0.9);
|
|
}
|
|
60% {
|
|
transform: translateY(-5px) scale(1.02);
|
|
}
|
|
100% {
|
|
opacity: 1;
|
|
transform: translateY(0) scale(1);
|
|
}
|
|
}
|
|
|
|
/* Ondas de pulso para o botão flutuante */
|
|
@keyframes pulse-ring {
|
|
0% {
|
|
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0.5);
|
|
}
|
|
50% {
|
|
box-shadow: 0 0 0 15px rgba(102, 126, 234, 0);
|
|
}
|
|
100% {
|
|
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0);
|
|
}
|
|
}
|
|
|
|
/* Rotação para anel de brilho */
|
|
@keyframes rotate {
|
|
from {
|
|
transform: rotate(0deg);
|
|
}
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
/* Efeito shimmer para o header */
|
|
@keyframes shimmer {
|
|
0% {
|
|
transform: translateX(-100%);
|
|
}
|
|
100% {
|
|
transform: translateX(100%);
|
|
}
|
|
}
|
|
|
|
/* Suavizar transições */
|
|
* {
|
|
-webkit-font-smoothing: antialiased;
|
|
-moz-osx-font-smoothing: grayscale;
|
|
}
|
|
</style>
|
|
|