From 4cca0a53ea0765fab2e060ade1530c2fe21b9ea0 Mon Sep 17 00:00:00 2001 From: RafaeL_Monteiro Date: Tue, 7 Oct 2025 22:40:14 -0300 Subject: [PATCH] Menu de acessibilidade --- src/App.js | 42 ++---- src/components/VlibrasWidget.jsx | 62 ++++++++ src/components/botaoacessibilidade.css | 198 +++++++++++++++++++++++++ src/components/botaoacessibilidade.jsx | 101 +++++++++++++ src/index.js | 4 +- src/pages/DoctorTable.jsx | 20 +-- src/pages/TablePaciente.jsx | 19 +-- 7 files changed, 396 insertions(+), 50 deletions(-) create mode 100644 src/components/VlibrasWidget.jsx create mode 100644 src/components/botaoacessibilidade.css create mode 100644 src/components/botaoacessibilidade.jsx diff --git a/src/App.js b/src/App.js index cb86164..1312003 100644 --- a/src/App.js +++ b/src/App.js @@ -1,6 +1,6 @@ import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; -import { useState } from "react"; +// Suas páginas import Login from "./pages/Login"; import Register from "./pages/Register"; import Forgot from "./pages/ForgotPassword"; @@ -10,29 +10,19 @@ import PerfilFinanceiro from "./perfis/perfil_financeiro/PerfilFinanceiro"; import Perfiladm from "./perfis/Perfil_adm/Perfiladm"; import PerfilMedico from "./perfis/Perfil_medico/PerfilMedico"; +// Componentes globais de acessibilidade +import VlibrasWidget from "./components/VlibrasWidget"; + +import BotaoAcessibilidade from "./components/botaoacessibilidade.jsx"; + function App() { - // O estado controla qual view mostrar: false = Landing Page, true = Dashboard - const [isInternalView, setIsInternalView] = useState(false); - // const [isSecretaria, setIsSecretaria] = useState(false); - - const handleEnterSystem = () => { - setIsInternalView(true); - }; - - const handleExitSystem = () => { - setIsInternalView(false); - }; - - // if (isSecretaria) { - // return setIsSecretaria(false)} />; - // } - - // Se não estiver na visualização interna, retorna a LandingPage. - if (!isInternalView) { - return ( + return ( + + + - } /> + } /> } /> } /> } /> @@ -43,14 +33,8 @@ function App() { Página não encontrada} /> - ) - } - - // Se estiver na visualização interna, retorna o PerfilSecretaria - return ( - // Passamos a função de saída (logout) - ); } -export default App; \ No newline at end of file +export default App; + diff --git a/src/components/VlibrasWidget.jsx b/src/components/VlibrasWidget.jsx new file mode 100644 index 0000000..81e8d48 --- /dev/null +++ b/src/components/VlibrasWidget.jsx @@ -0,0 +1,62 @@ +// src/components/VlibrasWidget.jsx + +import React, { useEffect } from 'react'; + +const VlibrasWidget = () => { + useEffect(() => { + // Cria o elemento div principal do Vlibras + const vwDiv = document.createElement('div'); + vwDiv.setAttribute('vw', ''); + vwDiv.classList.add('enabled'); + vwDiv.id = 'vlibras-div'; // 🔹 ADICIONADO: ID para remoção segura + + const vwAccessButton = document.createElement('div'); + vwAccessButton.setAttribute('vw-access-button', ''); + vwAccessButton.classList.add('active'); + + const vwPluginWrapper = document.createElement('div'); + vwPluginWrapper.setAttribute('vw-plugin-wrapper', ''); + + const vwPluginTopWrapper = document.createElement('div'); + vwPluginTopWrapper.classList.add('vw-plugin-top-wrapper'); + + vwPluginWrapper.appendChild(vwPluginTopWrapper); + vwDiv.appendChild(vwAccessButton); + vwDiv.appendChild(vwPluginWrapper); + + document.body.appendChild(vwDiv); + + // Adiciona o script principal do Vlibras + const script = document.createElement('script'); + script.src = 'https://vlibras.gov.br/app/vlibras-plugin.js'; + script.async = true; + script.id = 'vlibras-script'; // 🔹 ADICIONADO: ID para remoção segura + + script.onload = () => { + // Inicializa o widget após o script carregar + // Certifica-se que a API está disponível globalmente + if (window.VLibras) { + new window.VLibras.Widget('https://vlibras.gov.br/app'); + } + }; + + document.body.appendChild(script); + + // 🔹 ATUALIZADO: Função de limpeza para remover os elementos pelos IDs + return () => { + const existingVwDiv = document.getElementById('vlibras-div'); + if (existingVwDiv) { + document.body.removeChild(existingVwDiv); + } + + const existingScript = document.getElementById('vlibras-script'); + if (existingScript) { + document.body.removeChild(existingScript); + } + }; + }, []); // O array vazio [] garante que o useEffect rode apenas uma vez + + return null; // Este componente não renderiza nada visualmente +}; + +export default VlibrasWidget; \ No newline at end of file diff --git a/src/components/botaoacessibilidade.css b/src/components/botaoacessibilidade.css new file mode 100644 index 0000000..4fc533f --- /dev/null +++ b/src/components/botaoacessibilidade.css @@ -0,0 +1,198 @@ +/* --- ESTILO PARA ESCONDER O BOTÃO ORIGINAL DO VLIBRAS --- */ +[vw-access-button] { + display: none !important; +} + +/* --- ESTILOS GERAIS DO COMPONENTE --- */ +.container-acessibilidade { + position: fixed; + bottom: 20px; + right: 20px; + z-index: 99998; + display: flex; + flex-direction: column; + align-items: center; +} + +.botao-flutuante-acessibilidade { + position: relative; + z-index: 2; /* Acima do menu */ + background: linear-gradient(45deg, #007bff, #0056b3); + color: white; + border: none; + border-radius: 50%; + width: 60px; + height: 60px; + cursor: pointer; + box-shadow: 0 5px 15px rgba(0, 91, 179, 0.4); + display: flex; + justify-content: center; + align-items: center; + transition: transform 0.2s ease-in-out, box-shadow 0.2s ease; + margin-top: 15px; /* Distância do menu */ +} + +.botao-flutuante-acessibilidade:hover { + transform: scale(1.1); + box-shadow: 0 8px 20px rgba(0, 91, 179, 0.5); +} + +/* --- ESTILOS DO MENU "BALÃO" --- */ +.menu-opcoes { + background-color: #ffffff; + border-radius: 12px; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); + padding: 8px; + width: 280px; + z-index: 1; /* Abaixo do botão principal */ + border: 1px solid #e9ecef; + + /* Animação */ + transform-origin: bottom center; + transform: translateY(10px) scale(0.95); + opacity: 0; + visibility: hidden; + transition: all 0.2s ease; +} + +.menu-opcoes.aberto { + transform: translateY(0) scale(1); + opacity: 1; + visibility: visible; +} + +.menu-titulo { + font-size: 14px; + font-weight: 600; + color: #6c757d; + padding: 8px 12px; + border-bottom: 1px solid #f1f3f5; + margin-bottom: 5px; + transition: color 0.2s ease, border-bottom-color 0.2s ease; +} + +/* --- ESTILOS DOS BOTÕES E DA CHECKBOX NO MENU --- */ +.menu-opcoes button, +.checkbox-label-button { + display: flex; + align-items: center; + gap: 12px; + background-color: transparent; + border: none; + padding: 12px; + text-align: left; + cursor: pointer; + font-size: 16px; + color: #212529; + width: 100%; + border-radius: 8px; + transition: background-color 0.2s ease, color 0.2s ease; +} + +.menu-opcoes button:hover, +.checkbox-label-button:hover { + background-color: #f8f9fa; +} + +/* --- ESTILO DO INTERRUPTOR (CHECKBOX) --- */ +.checkbox-label-button { + justify-content: space-between; +} + +.checkbox-label-button input[type="checkbox"] { + appearance: none; + -webkit-appearance: none; + position: relative; + width: 44px; + height: 24px; + background-color: #ced4da; + border-radius: 12px; + cursor: pointer; + transition: background-color 0.3s ease-in-out; +} + +.checkbox-label-button input[type="checkbox"]::before { + content: ''; + position: absolute; + top: 2px; + left: 2px; + width: 20px; + height: 20px; + background-color: white; + border-radius: 50%; + box-shadow: 0 1px 3px rgba(0,0,0,0.2); + transition: transform 0.3s ease-in-out; +} + +.checkbox-label-button input[type="checkbox"]:checked { + background-color: #0d6efd; +} + +.checkbox-label-button input[type="checkbox"]:checked::before { + transform: translateX(20px); +} + + +/* --- ✨ NOVOS ESTILOS PARA O CONTROLE DE FONTE ✨ --- */ + +.font-size-control { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + color: #212529; + transition: color 0.2s ease; + border-top: 1px solid #f1f3f5; + margin-top: 5px; +} + +.font-size-label { + font-size: 16px; + display: flex; + align-items: center; + gap: 12px; +} + +.font-size-buttons { + display: flex; + align-items: center; + gap: 8px; +} + +.font-size-buttons button { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + font-weight: bold; + font-size: 16px; + background-color: #e9ecef; + color: #495057; + border: 1px solid #dee2e6; + border-radius: 6px; + width: 36px; + height: 32px; + padding: 0; + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + transition: background-color 0.2s ease, color 0.2s ease; +} + +.font-size-buttons button:hover { + background-color: #dee2e6; +} + +.font-size-buttons button:disabled { + background-color: #f8f9fa; + color: #adb5bd; + cursor: not-allowed; + border-color: #f1f3f5; +} + +.font-size-display { + font-size: 14px; + font-weight: 600; + color: #495057; + min-width: 45px; + text-align: center; + transition: color 0.2s ease; +} diff --git a/src/components/botaoacessibilidade.jsx b/src/components/botaoacessibilidade.jsx new file mode 100644 index 0000000..2ea8936 --- /dev/null +++ b/src/components/botaoacessibilidade.jsx @@ -0,0 +1,101 @@ +import React, { useState, useEffect, useRef } from 'react'; +import './botaoacessibilidade.css'; // Importando o CSS + +function BotaoAcessibilidade() { + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isReadOnHoverActive, setIsReadOnHoverActive] = useState(false); + const [isDarkMode, setIsDarkMode] = useState(false); + const lastSpokenTargetRef = useRef(null); + + useEffect(() => { + if (!isReadOnHoverActive) { + window.speechSynthesis.cancel(); + return; + } + const handleMouseOver = (event) => { + const target = event.target; + if (target && target !== lastSpokenTargetRef.current && target.innerText) { + const text = target.innerText.trim(); + if (text.length > 0 && ['P', 'H1', 'H2', 'H3', 'BUTTON', 'A', 'LI', 'LABEL'].includes(target.tagName)) { + lastSpokenTargetRef.current = target; + window.speechSynthesis.cancel(); + const utterance = new SpeechSynthesisUtterance(text); + utterance.lang = 'pt-BR'; + window.speechSynthesis.speak(utterance); + } + } + }; + document.body.addEventListener('mouseover', handleMouseOver); + return () => { + document.body.removeEventListener('mouseover', handleMouseOver); + window.speechSynthesis.cancel(); + }; + }, [isReadOnHoverActive]); + + const handleVlibrasClick = () => { + const originalVlibrasButton = document.querySelector('[vw-access-button]'); + if (originalVlibrasButton) { + originalVlibrasButton.click(); + } else { + alert("O Vlibras não pôde ser ativado."); + } + setIsMenuOpen(false); + }; + + const handleReadAloud = () => { + const selectedText = window.getSelection().toString().trim(); + if (selectedText) { + window.speechSynthesis.cancel(); + const utterance = new SpeechSynthesisUtterance(selectedText); + utterance.lang = 'pt-BR'; + window.speechSynthesis.speak(utterance); + } else { + alert("Por favor, selecione um texto para ler em voz alta."); + } + setIsMenuOpen(false); + }; + + return ( +
+
+
Acessibilidade
+ + + +
+ +
+ ); +} + +export default BotaoAcessibilidade; + diff --git a/src/index.js b/src/index.js index 4bfc68d..b3c6e84 100644 --- a/src/index.js +++ b/src/index.js @@ -7,10 +7,10 @@ import { AuthProvider } from "./components/utils/AuthProvider"; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( - + // - , + // , ); diff --git a/src/pages/DoctorTable.jsx b/src/pages/DoctorTable.jsx index 9d2e29c..019f7f7 100644 --- a/src/pages/DoctorTable.jsx +++ b/src/pages/DoctorTable.jsx @@ -10,13 +10,13 @@ function TableDoctor() { const [search, setSearch] = useState(""); const [filtroAniversariante, setFiltroAniversariante] = useState(false); - // estados do modal + // estados do modal const [showDeleteModal, setShowDeleteModal] = useState(false); const [selectedDoctorId, setSelectedDoctorId] = useState(null); // Função para excluir médicos const deleteDoctor = async (id) => { - + const authHeader = getAuthorizationHeader() console.log(id, 'teu id') @@ -56,11 +56,11 @@ function TableDoctor() { // Buscar médicos da API useEffect(() => { - const authHeader = getAuthorizationHeader() + const authHeader = getAuthorizationHeader() - console.log(authHeader, 'aqui autorização') + console.log(authHeader, 'aqui autorização') - var myHeaders = new Headers(); + var myHeaders = new Headers(); myHeaders.append("apikey", API_KEY); myHeaders.append("Authorization", `${authHeader}`); var requestOptions = { @@ -75,14 +75,14 @@ function TableDoctor() { .catch(error => console.log('error', error)); }, []); - // Filtrar médicos pelo campo de pesquisa e aniversariantes - const medicosFiltrados = medicos.filter( + // ✨ CORREÇÃO AQUI: Verificamos se 'medicos' é um array antes de filtrar. + const medicosFiltrados = Array.isArray(medicos) ? medicos.filter( (medico) => `${medico.nome} ${medico.cpf} ${medico.email} ${medico.telefone}` .toLowerCase() .includes(search.toLowerCase()) && (filtroAniversariante ? ehAniversariante(medico.data_nascimento) : true) - ); + ) : []; // Se não for um array, usamos um array vazio como fallback. return ( <> @@ -180,7 +180,7 @@ function TableDoctor() { - {/* Editar */} + {/* Editar */} - +