diff options
Diffstat (limited to 'frontend')
| -rw-r--r-- | frontend/index.html | 124 | ||||
| -rw-r--r-- | frontend/script.js | 331 | ||||
| -rw-r--r-- | frontend/style.css | 253 |
3 files changed, 708 insertions, 0 deletions
diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..237f2f7 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,124 @@ +<!DOCTYPE html> +<html lang="pt-br"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Dashboard</title> + + <link rel="preconnect" href="https://fonts.googleapis.com"> + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> + <link href="https://fonts.googleapis.com/css2?family=Sora:wght@300;400;600;700&display=swap" rel="stylesheet"> + + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"> + + <link rel="stylesheet" href="style.css"> +</head> +<body> + + <div class="dashboard-container"> + + <aside class="sidebar"> + <header class="sidebar-header"> + <h2>Dashboard</h2> + <span>Iot Monitoration</span> + </header> + + <nav class="sidebar-nav"> + <h3><i class="fas fa-satellite-dish"></i> Dados Atuais</h3> + + <div class="data-widget"> + <div class="widget-icon icon-temp"> + <i class="fas fa-thermometer-half"></i> + </div> + <div class="widget-info"> + <span id="currentTemp">--.- °C</span> + <p>Temperatura</p> + </div> + </div> + + <div class="data-widget"> + <div class="widget-icon icon-humidity"> + <i class="fas fa-tint"></i> + </div> + <div class="widget-info"> + <span id="currentHumidity">-- %</span> + <p>Umidade</p> + </div> + </div> + + <div class="data-widget"> + <div class="widget-icon icon-wind"> + <i class="fas fa-wind"></i> + </div> + <div class="widget-info"> + <span id="currentWind">--.- km/h</span> + <p>Vento</p> + </div> + </div> + + <div class="data-widget"> + <div class="widget-icon icon-pressure"> + <i class="fas fa-tachometer-alt"></i> + </div> + <div class="widget-info"> + <span id="currentPressure">---- hPa</span> + <p>Pressão</p> + </div> + </div> + + </nav> + + <footer class="sidebar-footer"> + <button id="toggleUpdatesBtn"> + <i class="fas fa-pause"></i> <span>Pausar</span> + </button> + <p>Status: <span id="statusText">Monitorando...</span></p> + </footer> + </aside> + + <main class="main-content"> + <h1>Painel de Monitoramento</h1> + + <section class="charts-grid"> + + <div class="chart-card large"> + <h3>Temperatura & Pressão (Últimos 30s)</h3> + <canvas id="graficoTempPressao"></canvas> + </div> + + <div class="chart-card"> + <h3>Umidade</h3> + <div class="gauge-container"> + <canvas id="graficoUmidade"></canvas> + <div id="umidadeText" class="gauge-text">--%</div> + </div> + </div> + + <div class="chart-card"> + <h3>Índice UV</h3> + <div class="gauge-container"> + <canvas id="graficoUv"></canvas> + <div id="uvText" class="gauge-text">--</div> + </div> + </div> + + <div class="chart-card"> + <h3>Velocidade do Vento</h3> + <canvas id="graficoVento"></canvas> + </div> + + <div class="chart-card"> + <h3>Visibilidade</h3> + <canvas id="graficoVisibilidade"></canvas> + </div> + + </section> + </main> + </div> + + <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> + <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns"></script> + + <script src="script.js"></script> +</body> +</html>
\ No newline at end of file diff --git a/frontend/script.js b/frontend/script.js new file mode 100644 index 0000000..5a3e77c --- /dev/null +++ b/frontend/script.js @@ -0,0 +1,331 @@ +document.addEventListener("DOMContentLoaded", () => { + // --- Configurações Globais --- + const updateInterval = 2000; // 2 segundos + let intervalId = null; + let isPaused = false; + const maxDataPoints = 15; // 15 pontos * 2s = 30s de dados + + // --- Elementos do DOM --- + const toggleUpdatesBtn = document.getElementById('toggleUpdatesBtn'); + const statusText = document.getElementById('statusText'); + + // Widgets da Sidebar + const currentTempSpan = document.getElementById('currentTemp'); + const currentHumiditySpan = document.getElementById('currentHumidity'); + const currentWindSpan = document.getElementById('currentWind'); + const currentPressureSpan = document.getElementById('currentPressure'); + + // Texto dos Medidores + const umidadeText = document.getElementById('umidadeText'); + const uvText = document.getElementById('uvText'); + + // --- Configurações Globais do Chart.js para o Tema Dark --- + Chart.defaults.color = '#aaaaaa'; // Cor da fonte (eixos, legendas) + Chart.defaults.borderColor = 'rgba(255, 255, 255, 0.1)'; // Cor das linhas do grid + + // --- Helper para Gradiente --- + function createChartGradient(ctx, color) { + const gradient = ctx.createLinearGradient(0, 0, 0, 400); + + // Pega a cor das variáveis CSS. Ex: 'var(--temp-color)' + let colorHex = getComputedStyle(document.documentElement).getPropertyValue(color.match(/\((.*?)\)/)[1]).trim(); + + // Converte hex para rgba + const r = parseInt(colorHex.slice(1, 3), 16); + const g = parseInt(colorHex.slice(3, 5), 16); + const b = parseInt(colorHex.slice(5, 7), 16); + + gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, 0.6)`); + gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0.05)`); + return gradient; + } + + // --- PARTE 1: SIMULAÇÃO DA API --- + // QUANDO FOR USAR A API REAL, VOCÊ VAI APAGAR ESTA FUNÇÃO + function simularApiDaEstacao() { + const temperatura = (Math.random() * 10 + 15).toFixed(1); // 15.0 - 25.0 + const umidade = (Math.random() * 30 + 50).toFixed(0); // 50 - 80 + const vento = (Math.random() * 15 + 5).toFixed(1); // 5.0 - 20.0 + const pressao = (Math.random() * 20 + 1000).toFixed(0); // 1000 - 1020 + const uv = Math.floor(Math.random() * 11); // 0 - 10 + const visibilidade = (Math.random() * 15 + 5).toFixed(1); // 5.0 - 20.0 + + return { + timestamp: new Date(), + temperatura: parseFloat(temperatura), + umidade: parseInt(umidade), + vento: parseFloat(vento), + pressao: parseInt(pressao), + uv: parseInt(uv), + visibilidade: parseFloat(visibilidade) + }; + } + + // API REAL + /* + async function fetchApiReal() { + const url = 'http://URL_DA_SUA_API_PYTHON/dados'; + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Erro na API: ${response.statusText}`); + } + const data = await response.json(); + + // Adiciona o timestamp, já que a API pode não mandar + data.timestamp = new Date(); + return data; + + } catch (error) { + console.error("Falha ao buscar dados da API", error); + // Retorna nulo para não quebrar o dashboard + return null; + } + } + */ + + + // --- PARTE 2: INICIALIZAÇÃO DOS GRÁFICOS --- + +// // 1. Gráfico de Linha (Temperatura & Pressão) - 2 eixos Y +// const ctxTempPressao = document.getElementById('graficoTempPressao').getContext('2d'); +// const graficoTempPressao = new Chart(ctxTempPressao, { +// type: 'line', +// data: { +// labels: [], +// datasets: [ +// { +// label: 'Temperatura', +// data: [], +// borderColor: 'var(--temp-color)', +// backgroundColor: createChartGradient(ctxTempPressao, 'var(--temp-color)'), +// fill: true, +// tension: 0.4, +// yAxisID: 'yTemp' // Associa ao eixo Y da temperatura +// }, +// { +// label: 'Pressão', +// data: [], +// borderColor: 'var(--pressure-color)', +// backgroundColor: createChartGradient(ctxTempPressao, 'var(--pressure-color)'), +// fill: true, +// tension: 0.4, +// yAxisID: 'yPressure' // Associa ao eixo Y da pressão +// } +// ] +// }, +// options: { +// responsive: true, +// maintainAspectRatio: false, +// interaction: { mode: 'index', intersect: false }, +// scales: { +// x: { +// type: 'time', +// time: { unit: 'second', displayFormats: { second: 'HH:mm:ss' } }, +// ticks: { maxRotation: 0, autoSkip: true, maxTicksLimit: 6 } +// }, +// yTemp: { // Eixo Y da Esquerda (Temperatura) +// type: 'linear', +// position: 'left', +// grid: { drawOnChartArea: false }, // Remove grid deste eixo +// ticks: { callback: value => `${value} °C` } +// }, +// yPressure: { // Eixo Y da Direita (Pressão) +// type: 'linear', +// position: 'right', +// ticks: { callback: value => `${value} hPa` } +// } +// }, +// plugins: { +// legend: { position: 'top' }, +// tooltip: { +// backgroundColor: '#000', +// titleFont: { weight: 'bold' }, +// bodySpacing: 4, +// padding: 10, +// borderColor: 'var(--border-color)', +// borderWidth: 1 +// } +// } +// } +// }); + +// // 2. Medidor de Umidade (Doughnut) +// const graficoUmidade = new Chart(document.getElementById('graficoUmidade').getContext('2d'), { +// type: 'doughnut', +// data: { +// datasets: [{ +// data: [0, 100], // Valor, Restante +// backgroundColor: ['var(--humidity-color)', '#2f2f2f'], // Cor da umidade, Cor do fundo +// borderWidth: 0, +// borderRadius: 5 +// }] +// }, +// options: { +// responsive: true, +// maintainAspectRatio: false, +// circumference: 270, // Arco de 270 graus +// rotation: 225, // Começa no canto inferior esquerdo +// cutout: '80%', +// plugins: { legend: { display: false }, tooltip: { enabled: false } } +// } +// }); + +// // 3. Medidor de UV (Doughnut) +// const graficoUv = new Chart(document.getElementById('graficoUv').getContext('2d'), { +// type: 'doughnut', +// data: { +// datasets: [{ +// data: [0, 12], // Valor, Max (escala UV vai ~12) +// backgroundColor: ['var(--uv-color)', '#2f2f2f'], // Cor UV, Cor do fundo +// borderWidth: 0, +// borderRadius: 5 +// }] +// }, +// options: { +// responsive: true, +// maintainAspectRatio: false, +// circumference: 270, +// rotation: 225, +// cutout: '80%', +// plugins: { legend: { display: false }, tooltip: { enabled: false } } +// } +// }); + +// // 4. Gráfico de Vento (Barra Vertical) +// const graficoVento = new Chart(document.getElementById('graficoVento').getContext('2d'), { +// type: 'bar', +// data: { +// labels: ['Vento'], +// datasets: [{ +// label: 'km/h', +// data: [0], +// backgroundColor: 'var(--accent-color)', // Cor de destaque +// borderRadius: 4 +// }] +// }, +// options: { +// responsive: true, +// maintainAspectRatio: false, +// scales: { +// x: { display: false }, +// y: { beginAtZero: true, max: 30 } +// }, +// plugins: { legend: { display: false } } +// } +// }); + +// // 5. Gráfico de Visibilidade (Barra Horizontal) +// const graficoVisibilidade = new Chart(document.getElementById('graficoVisibilidade').getContext('2d'), { +// type: 'bar', +// data: { +// labels: ['Visibilidade'], +// datasets: [{ +// label: 'km', +// data: [0], +// backgroundColor: '#9c27b0', // Roxo +// borderRadius: 4 +// }] +// }, +// options: { +// indexAxis: 'y', // Barra horizontal +// responsive: true, +// maintainAspectRatio: false, +// scales: { +// y: { display: false }, +// x: { beginAtZero: true, max: 25 } +// }, +// plugins: { legend: { display: false } } +// } +// }); + + +// // --- PARTE 3: FUNÇÃO DE ATUALIZAÇÃO --- + +// // Esta função será chamada a cada X segundos +// async function atualizarDashboard() { + +// // Mude aqui para usar a API real +// // const dados = await fetchApiReal(); +// const dados = simularApiDaEstacao(); // Usando simulação por enquanto + +// // Se a API falhar, 'dados' será nulo. Pulamos a atualização. +// if (!dados) { +// return; +// } + +// // Atualiza Widgets da Sidebar +// currentTempSpan.textContent = `${dados.temperatura.toFixed(1)} °C`; +// currentHumiditySpan.textContent = `${dados.umidade} %`; +// currentWindSpan.textContent = `${dados.vento.toFixed(1)} km/h`; +// currentPressureSpan.textContent = `${dados.pressao} hPa`; + +// // Atualiza Gráfico de Linha (Temp & Pressão) +// const labels = graficoTempPressao.data.labels; +// const tempAtivos = graficoTempPressao.data.datasets[0].data; +// const pressAtivos = graficoTempPressao.data.datasets[1].data; + +// labels.push(dados.timestamp); +// tempAtivos.push(dados.temperatura); +// pressAtivos.push(dados.pressao); + +// if (labels.length > maxDataPoints) { +// labels.shift(); +// tempAtivos.shift(); +// pressAtivos.shift(); +// } +// graficoTempPressao.update(); + +// // Atualiza Medidor de Umidade +// umidadeText.textContent = `${dados.umidade}%`; +// graficoUmidade.data.datasets[0].data = [dados.umidade, 100 - dados.umidade]; +// graficoUmidade.update(); + +// // Atualiza Medidor de UV (com cor dinâmica) +// let uvColor; +// if (dados.uv <= 2) { uvColor = '#4caf50'; } // Verde +// else if (dados.uv <= 5) { uvColor = '#fdd835'; } // Amarelo +// else if (dados.uv <= 7) { uvColor = '#ff9800'; } // Laranja +// else { uvColor = '#f44336'; } // Vermelho + +// uvText.textContent = dados.uv; +// graficoUv.data.datasets[0].data = [dados.uv, 12 - dados.uv]; +// graficoUv.data.datasets[0].backgroundColor[0] = uvColor; +// graficoUv.update(); + +// // Atualiza Gráfico de Vento +// graficoVento.data.datasets[0].data = [dados.vento]; +// graficoVento.update(); + +// // Atualiza Gráfico de Visibilidade +// graficoVisibilidade.data.datasets[0].data = [dados.visibilidade]; +// graficoVisibilidade.update(); +// } + +// // --- PARTE 4: CONTROLE DE ATUALIZAÇÃO --- + +// toggleUpdatesBtn.addEventListener('click', () => { +// isPaused = !isPaused; +// if (isPaused) { +// clearInterval(intervalId); +// intervalId = null; +// toggleUpdatesBtn.innerHTML = '<i class="fas fa-play"></i> <span>Retomar</span>'; +// statusText.textContent = 'Pausado'; +// statusText.style.color = '#ff9800'; +// } else { +// startUpdates(); +// toggleUpdatesBtn.innerHTML = '<i class="fas fa-pause"></i> <span>Pausar</span>'; +// statusText.textContent = 'Monitorando...'; +// statusText.style.color = 'var(--accent-color)'; +// } +// }); + +// function startUpdates() { +// if (intervalId === null) { +// atualizarDashboard(); // Roda uma vez imediatamente +// intervalId = setInterval(atualizarDashboard, updateInterval); +// } +// } + +// startUpdates(); // Inicia ao carregar +// });
\ No newline at end of file diff --git a/frontend/style.css b/frontend/style.css new file mode 100644 index 0000000..be965cb --- /dev/null +++ b/frontend/style.css @@ -0,0 +1,253 @@ +/* --- Variáveis de Cor (Paleta "Neon Tech") --- */ +:root { + --bg-color: #121212; /* Fundo principal (preto) */ + --card-color: #1e1e1e; /* Fundo dos cards (grafite) */ + --border-color: #2f2f2f; /* Bordas sutis */ + --text-primary: #e0e0e0; /* Texto principal (cinza claro) */ + --text-secondary: #aaaaaa; /* Texto secundário (cinza) */ + --accent-color: #00ff9a; /* Destaque (verde neon) */ + --accent-color-dark: #00b36e; + --accent-color-transparent: rgba(0, 255, 154, 0.1); + + --temp-color: #ff5252; /* Vermelho para temperatura */ + --pressure-color: #448aff; /* Azul para pressão */ + --humidity-color: #29b6f6; /* Ciano para umidade */ + --uv-color: #fdd835; /* Amarelo para UV */ + + --font-family: 'Sora', 'Segoe UI', system-ui, sans-serif; +} + +/* --- Resets e Estilos Globais --- */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: var(--font-family); + background-color: var(--bg-color); + color: var(--text-primary); + display: flex; + height: 100vh; + overflow: hidden; /* Impede o scroll da página inteira */ +} + +h1, h2, h3 { + font-weight: 600; +} + +/* --- Layout Principal --- */ +.dashboard-container { + display: flex; + width: 100%; + height: 100%; +} + +/* --- Barra Lateral (Sidebar) --- */ +.sidebar { + width: 280px; + flex-shrink: 0; + background-color: var(--card-color); + border-right: 1px solid var(--border-color); + display: flex; + flex-direction: column; + padding: 20px; + overflow-y: auto; /* Permite scroll na sidebar se precisar */ +} + +.sidebar-header { + text-align: center; + margin-bottom: 30px; + border-bottom: 1px solid var(--border-color); + padding-bottom: 20px; +} +.sidebar-header h2 { + color: var(--accent-color); + font-weight: 700; + letter-spacing: 1px; +} +.sidebar-header span { + font-size: 0.9rem; + color: var(--text-secondary); +} + +.sidebar-nav { + flex-grow: 1; +} +.sidebar-nav h3 { + color: var(--text-secondary); + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 15px; + display: flex; + align-items: center; + gap: 8px; +} + +/* Widgets de Dados Atuais */ +.data-widget { + display: flex; + align-items: center; + margin-bottom: 20px; + background-color: var(--bg-color); + padding: 12px; + border-radius: 8px; + border: 1px solid var(--border-color); + transition: border-color 0.3s ease; +} +.data-widget:hover { + border-color: var(--accent-color); +} + +.widget-icon { + font-size: 1.5rem; + margin-right: 15px; + padding: 10px; + border-radius: 50%; + width: 48px; + height: 48px; + display: grid; + place-items: center; +} +.widget-info span { + font-size: 1.4rem; + font-weight: 600; + color: var(--text-primary); +} +.widget-info p { + font-size: 0.9rem; + color: var(--text-secondary); +} + +/* Cores dos ícones dos widgets */ +.icon-temp { background-color: rgba(255, 82, 82, 0.1); color: var(--temp-color); } +.icon-humidity { background-color: rgba(41, 182, 246, 0.1); color: var(--humidity-color); } +.icon-wind { background-color: rgba(0, 255, 154, 0.1); color: var(--accent-color); } +.icon-pressure { background-color: rgba(68, 138, 255, 0.1); color: var(--pressure-color); } + +.sidebar-footer { + border-top: 1px solid var(--border-color); + padding-top: 20px; + text-align: center; +} +.sidebar-footer button { + background-color: var(--accent-color); + color: #000; + border: none; + padding: 12px 20px; + border-radius: 5px; + cursor: pointer; + font-family: var(--font-family); + font-weight: 600; + font-size: 1rem; + width: 100%; + margin-bottom: 10px; + transition: background-color 0.3s ease; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} +.sidebar-footer button:hover { + background-color: var(--accent-color-dark); +} +.sidebar-footer p { + font-size: 0.8rem; + color: var(--text-secondary); +} +#statusText { + color: var(--accent-color); + font-weight: 600; +} + +/* --- Conteúdo Principal (Gráficos) --- */ +.main-content { + flex-grow: 1; + padding: 30px; + overflow-y: auto; /* Permite scroll se os gráficos não couberem */ +} + +.main-content h1 { + font-size: 2rem; + margin-bottom: 30px; + color: var(--text-primary); +} + +.charts-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 20px; +} + +.chart-card { + background-color: var(--card-color); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 20px; + min-height: 350px; + display: flex; + flex-direction: column; + + /* A CORREÇÃO VITAL: + Isso "prende" o canvas do Chart.js dentro do card, + impedindo que ele tente criar um canvas infinito + quando executado no Live Server. + */ + position: relative; +} +.chart-card.large { + grid-column: span 2; /* Faz o gráfico ocupar 2 colunas */ +} +.chart-card h3 { + margin-bottom: 20px; + text-align: center; + color: var(--text-secondary); + font-weight: 400; + font-size: 1rem; +} + +/* Contêiner para Medidores (Gauge) */ +.gauge-container { + position: relative; + flex-grow: 1; + display: grid; + place-items: center; +} +.gauge-text { + position: absolute; + font-size: 2.5rem; + font-weight: 700; + color: var(--text-primary); +} +#uvText.gauge-text { + font-size: 3rem; /* UV é só um número, pode ser maior */ +} + +/* --- Responsividade --- */ +@media (max-width: 1200px) { + .chart-card.large { + grid-column: span 1; /* Em telas menores, o gráfico largo ocupa 1 coluna */ + } +} +@media (max-width: 900px) { + body { + height: auto; + overflow: auto; /* Permite scroll em telas pequenas */ + } + .dashboard-container { + flex-direction: column; + } + .sidebar { + width: 100%; + height: auto; + border-right: none; + border-bottom: 1px solid var(--border-color); + overflow-y: visible; + } + .main-content { + height: auto; + overflow-y: visible; + } +}
\ No newline at end of file |
