14/02/2025

This commit is contained in:
damarrsyh 2025-02-14 23:48:37 +07:00
parent 760aec0638
commit a58c9b35a4
14 changed files with 802 additions and 239 deletions

View File

@ -2,8 +2,6 @@
body { body {
font-family: 'Poppins', sans-serif !important; font-family: 'Poppins', sans-serif !important;
transition: background-color 0.3s, color 0.3s; transition: background-color 0.3s, color 0.3s;
padding-top: 70px; /* Sesuaikan dengan tinggi Navbar */
padding-left: 250px;
} }
body.light { body.light {
@ -16,31 +14,169 @@ body.dark {
color: #ffffff; color: #ffffff;
} }
.theme-transition {
opacity: 0;
animation: fadeIn 0.5s ease-in-out forwards;
}
@keyframes fadeIn {
from {
opacity: 0.2;
}
to {
opacity: 1;
}
}
.theme-toggle-btn {
background-color: transparent;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 50%;
transition: background-color 0.3s, transform 0.2s ease-in-out;
}
/* Light Mode */
.light .theme-toggle-btn {
background-color: rgba(0, 0, 0, 0.1);
color: #1d1d1d;
}
/* Dark Mode */
.dark .theme-toggle-btn {
background-color: rgba(255, 255, 255, 0.1);
color: #ffffff;
}
/* Hover Effect */
.theme-toggle-btn:hover {
transform: scale(1.1);
opacity: 0.8;
}
@media (max-width: 768px) {
.sidebar-app {
display: none;
}
.main-content {
margin-left: 0; /* Pastikan kontennya full width */
}
}
/* Styling untuk tombol modal hanya tampil di bawah 768px */
@media (min-width: 768px) {
.menu-toggle-btn, .sidebar-toggle-btn {
display: none;
}
.main-content {
margin-top: 75px;
margin-left: 250px; /* Pastikan kontennya full width */
}
}
/* Styling modal */
/* Styling untuk tombol modal hanya tampil di bawah 768px */
@media (min-width: 768px) {
.menu-toggle-btn,
.sidebar-toggle-btn {
display: none;
}
}
/* Styling modal menyesuaikan dengan tema */
.sidebar-modal .modal-content {
transition: background-color 0.3s, color 0.3s;
border-radius: 8px;
}
.light .sidebar-modal .modal-content {
background-color: #ffffff;
color: #1d1d1d;
}
.dark .sidebar-modal .modal-content {
background-color: #161616;
color: white;
}
.sidebar-modal .modal-header {
border-bottom: 1px solid #444;
}
.light .sidebar-modal .modal-header {
border-bottom: 1px solid #ddd;
}
.sidebar-modal .modal-title {
font-size: 1.5rem;
}
.sidebar-modal .modal-body {
padding: 20px;
}
.sidebar-modal .nav-link {
padding: 10px;
border-radius: 5px;
text-decoration: none;
transition: background-color 0.3s;
}
.light .sidebar-modal .nav-link {
color: #1d1d1d;
}
.dark .sidebar-modal .nav-link {
color: white;
}
.light .sidebar-modal .nav-link:hover {
background-color: rgba(0, 0, 0, 0.1);
}
.dark .sidebar-modal .nav-link:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.navbar-app { .navbar-app {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding-top: 10px; padding-top: 10px;
padding-bottom: 10px; padding-bottom: 10px;
transition: background-color 0.3s, color 0.3s, box-shadow 0.3s;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); /* Default shadow */
} }
/* Light Mode Navbar */
.light .navbar-app { .light .navbar-app {
background-color: #ffffff; background-color: #ffffff;
transition: background-color 0.3s, color 0.3s; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); /* Soft shadow */
} }
.light .navbar-brand { .light .navbar-brand {
color: #161616; color: #161616;
transition: background-color 0.3s, color 0.3s;
} }
/* Dark Mode Navbar */
.dark .navbar-app { .dark .navbar-app {
background-color: #161616; background-color: #161616;
transition: background-color 0.3s, color 0.3s; box-shadow: 0 4px 12px rgba(255, 255, 255, 0.1); /* Lebih jelas di dark mode */
}
.dark .navbar-app:hover {
box-shadow: 0 6px 18px rgba(255, 255, 255, 0.2); /* Efek hover agar terlihat lebih elegan */
} }
.dark .navbar-brand { .dark .navbar-brand {
transition: background-color 0.3s, color 0.3s;
color: #ffffff; color: #ffffff;
} }
@ -70,13 +206,30 @@ button:hover {
/* border-right: 1px solid #414141; */ /* border-right: 1px solid #414141; */
} }
.sidebar-app.closed {
transform: translateX(-100%);
}
.sidebar-toggle-btn {
background: none;
border: none;
cursor: pointer;
color: inherit;
}
.sidebar-dropdown-toggle {
background-color: transparent;
border: none;
color: inherit;
cursor: pointer;
}
/* Styling Default Link */ /* Styling Default Link */
.sidebar-app .nav-link { .sidebar-app .nav-link {
margin: 5px 15px; margin: 5px 15px;
font-size: 16px; font-size: 16px;
padding: 12px 15px; padding: 12px 15px;
border-radius: 5px; border-radius: 5px;
transition: background 0.3s, color 0.3s;
text-decoration: none; /* Hilangkan underline */ text-decoration: none; /* Hilangkan underline */
} }
@ -91,3 +244,380 @@ button:hover {
color: white !important; color: white !important;
font-weight: bold; font-weight: bold;
} }
/* LIGHT MODE - Sidebar */
.light .sidebar-app {
background-color: #f8f9fa;
color: #1d1d1d;
border-right: 1px solid #ddd;
}
/* DARK MODE - Sidebar */
.dark .sidebar-app {
background-color: #161616;
color: #ffffff;
border-right: 1px solid #444;
}
/* LIGHT MODE - Sidebar Link */
.light .nav-link {
color: #1d1d1d;
background-color: transparent;
}
/* DARK MODE - Sidebar Link */
.dark .nav-link {
color: #ffffff;
background-color: transparent;
}
/* Hover Effect */
.light .nav-link:hover {
background-color: rgba(0, 0, 0, 0.1);
}
.dark .nav-link:hover {
background-color: rgba(255, 255, 255, 0.1);
}
/* Aktif Link */
.light .nav-link.active {
background-color:#d32f2f;
color:rgb(255, 255, 255) !important;
font-weight: bold;
}
.dark .nav-link.active {
background-color:#EF5350;
color: white !important;
font-weight: bold;
}
/* ======================= */
/* CARD STYLING - START */
/* ======================= */
.card {
border-radius: 12px;
transition: background-color 0.3s, color 0.3s;
}
@media (max-width: 768px) {
.card {
width: 100%;
margin-bottom: 10px;
}
}
.light .card {
background-color: #ffffff;
color: #1d1d1d;
border: 1px solid #ddd;
}
.light .card-header {
background-color: #f8f9fa;
color: #1d1d1d;
font-weight: bold;
border-bottom: 1px solid #ddd;
}
.dark .card {
background-color: #252525;
color: #ffffff;
border: 1px solid #444;
}
.dark .card-header {
background-color: #333333;
color: #ffffff;
font-weight: bold;
border-bottom: 1px solid #444;
}
/* Styling untuk Card Header */
.card-header {
font-size: 1rem;
font-weight: bold;
text-align: left;
padding: 12px 16px;
border-bottom: none;
}
/* Styling untuk Card Body */
.card-body {
padding: 20px;
}
/* Styling untuk Card Title */
.card-title {
font-size: 1.5rem;
font-weight: bold;
margin-top: 10px;
}
/* Hover Effect */
.card:hover {
transform: scale(1.03);
transition: transform 0.2s ease-in-out;
}
/* ======================= */
/* CARD STYLING - END */
/* ======================= */
/* ======================= */
/* METRICS GRID TABLE */
/* ======================= */
.metrics-container {
display: flex;
flex-direction: column;
border-radius: 8px;
overflow: hidden;
margin-bottom: 20px;
transition: background-color 0.3s, color 0.3s;
}
.metrics-header, .metrics-row {
display: grid;
grid-template-columns: 1fr 2fr 1fr 1fr 1fr 1fr;
gap: 10px;
padding: 12px;
text-align: center;
}
/* Header Styling */
.metrics-header {
font-weight: bold;
}
/* LIGHT MODE */
.light .metrics-container {
background-color: #ffffff;
color: #1d1d1d;
border: 1px solid #ddd;
}
.light .metrics-header {
background-color: #f5f5f5;
color: #000;
}
.light .metrics-body .metrics-row {
background-color: #ffffff;
color: #1d1d1d;
}
.light .metrics-body .metrics-row:nth-child(even) {
background-color: #eeeeee;
}
/* DARK MODE */
.dark .metrics-container {
background-color: #161616;
color: #ffffff;
border: 1px solid #444;
}
.dark .metrics-header {
background-color: #333333;
color: #ffffff;
}
.dark .metrics-body .metrics-row {
background-color: #222222;
color: #ffffff;
}
.dark .metrics-body .metrics-row:nth-child(even) {
background-color: #2a2a2a;
}
/* Link Styling */
.metrics-row a {
color: #4CAF50;
text-decoration: none;
}
.metrics-row a:hover {
text-decoration: underline;
}
/* Responsive Design */
@media (max-width: 768px) {
.metrics-header, .metrics-row {
grid-template-columns: 1fr 1fr 1fr; /* Kurangi jumlah kolom agar lebih rapi di mobile */
font-size: 14px;
}
}
/* ======================= */
/* DATA TABLE GRID STYLING */
/* ======================= */
.data-table-container {
display: flex;
flex-direction: column;
border-radius: 8px;
overflow: hidden;
margin-bottom: 20px;
transition: background-color 0.3s, color 0.3s;
}
.data-table-header, .data-table-row {
display: grid;
grid-template-columns: 1fr 2fr 2fr 2fr 2fr 2fr 3fr 3fr;
gap: 10px;
padding: 12px;
text-align: center;
align-items: center;
}
/* HEADER */
.data-table-header {
font-weight: bold;
}
/* LIGHT MODE */
.light .data-table-container {
background-color: #ffffff;
color: #1d1d1d;
border: 1px solid #ddd;
}
.light .data-table-header {
background-color: #f5f5f5;
color: #000;
}
.light .data-table-body .data-table-row {
background-color: #ffffff;
color: #1d1d1d;
}
.light .data-table-body .data-table-row:nth-child(even) {
background-color: #eeeeee;
}
/* DARK MODE */
.dark .data-table-container {
background-color: #161616;
color: #ffffff;
border: 1px solid #444;
}
.dark .data-table-header {
background-color: #333333;
color: #ffffff;
}
.dark .data-table-body .data-table-row {
background-color: #222222;
color: #ffffff;
}
.dark .data-table-body .data-table-row:nth-child(even) {
background-color: #2a2a2a;
}
/* Responsive Design */
@media (max-width: 768px) {
.data-table-header, .data-table-row {
grid-template-columns: 1fr 1fr 1fr 1fr; /* Kurangi jumlah kolom di mobile */
font-size: 14px;
}
}
/* ======================= */
/* DEVICE TABLE GRID STYLE */
/* ======================= */
.device-table-container {
display: flex;
flex-direction: column;
border-radius: 8px;
overflow: hidden;
margin-bottom: 20px;
transition: background-color 0.3s, color 0.3s;
}
.device-table-header, .device-table-row {
display: grid;
grid-template-columns: 3fr 3fr 2fr;
gap: 10px;
padding: 12px;
text-align: center;
align-items: center;
}
/* HEADER */
.device-table-header {
font-weight: bold;
}
/* LIGHT MODE */
.light .device-table-container {
background-color: #ffffff;
color: #1d1d1d;
border: 1px solid #ddd;
}
.light .device-table-header {
background-color: #f5f5f5;
color: #000;
}
.light .device-table-body .device-table-row {
background-color: #ffffff;
color: #1d1d1d;
}
.light .device-table-body .device-table-row:nth-child(even) {
background-color: #eeeeee;
}
/* DARK MODE */
.dark .device-table-container {
background-color: #161616;
color: #ffffff;
border: 1px solid #444;
}
.dark .device-table-header {
background-color: #333333;
color: #ffffff;
}
.dark .device-table-body .device-table-row {
background-color: #222222;
color: #ffffff;
}
.dark .device-table-body .device-table-row:nth-child(even) {
background-color: #2a2a2a;
}
/* Status Styling */
.status {
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
.online {
color: green;
}
.offline {
color: red;
}
/* Responsive Design */
@media (max-width: 768px) {
.device-table-header, .device-table-row {
grid-template-columns: 1fr 1fr;
font-size: 14px;
}
}

View File

@ -1,7 +1,7 @@
import "bootstrap/dist/css/bootstrap.min.css" import "bootstrap/dist/css/bootstrap.min.css"
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import Sidebar from './components/layout/Sidebar'; import Sidebar from './components/layout/Sidebar';
import Navbar from './components/layout/Navbar'; import AppNavbar from './components/layout/Navbar';
import Dashboard from './pages/Dashboard'; import Dashboard from './pages/Dashboard';
import DevicesPage from './pages/DevicesPage'; import DevicesPage from './pages/DevicesPage';
import ReportPage from "./pages/ReportPage"; import ReportPage from "./pages/ReportPage";
@ -10,8 +10,8 @@ import { useEffect, useState } from "react";
import './App.css'; import './App.css';
function App() { function App() {
const [theme, setTheme] = useState(localStorage.getItem("theme") || "light"); const [theme, setTheme] = useState(localStorage.getItem("theme") || "light");
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
useEffect(() => { useEffect(() => {
document.body.className = theme; document.body.className = theme;
@ -22,18 +22,22 @@ function App() {
setTheme(prevTheme => (prevTheme === "light" ? "dark" : "light")); setTheme(prevTheme => (prevTheme === "light" ? "dark" : "light"));
}; };
const toggleSidebar = () => {
setIsSidebarOpen(!isSidebarOpen);
};
return ( return (
<Router> <Router>
<div className={`app-container ${theme}`}> <div className={`app-container ${theme} theme-transition`}>
<Navbar toggleTheme={toggleTheme} theme={theme} className={`${theme}`}/> <AppNavbar toggleTheme={toggleTheme} theme={theme} toggleSidebar={toggleSidebar} />
<Sidebar /> <Sidebar theme={theme} isOpen={isSidebarOpen} />
<div className={`main-content ${theme}`}> <div className={`main-content ${theme}`}>
<Routes> <Routes>
<Route path="/" element={<Navigate to="/dashboard"/>} /> <Route path="/" element={<Navigate to="/dashboard"/>} />
<Route path="/dashboard" element={<Dashboard />}/> <Route path="/dashboard" element={<Dashboard theme={theme}/>}/>
<Route path="/devices" element={<DevicesPage />}/> <Route path="/devices" element={<DevicesPage theme={theme}/>} />
<Route path="/reports" element={<ReportPage />}/> <Route path="/reports" element={<ReportPage theme={theme}/>} />
<Route path="/settings" element={<SettingsPage />}/> <Route path="/settings" element={<SettingsPage theme={theme}/>} />
</Routes> </Routes>
</div> </div>
</div> </div>

View File

@ -1,6 +1,9 @@
import ReactApexChart from 'react-apexcharts'; import ReactApexChart from 'react-apexcharts';
const DownloadChart = ({ data }) => { const DownloadChart = ({ data, theme }) => {
const isDark = theme === 'dark';
const chartData = { const chartData = {
series: [ series: [
{ {
@ -15,39 +18,39 @@ const DownloadChart = ({ data }) => {
chart: { chart: {
height: 250, height: 250,
type: 'area', type: 'area',
background: '#1e1e1e', background: isDark ? '#1e1e1e' : '#ffffff',
toolbar: { show: false }, toolbar: { show: false },
}, },
dataLabels: { enabled: false }, dataLabels: { enabled: false },
stroke: { stroke: {
curve: 'smooth', curve: 'smooth',
colors: ['#4CAF50'], colors: [isDark ? '#4CAF50' : '#28a745'],
}, },
xaxis: { xaxis: {
type: 'datetime', type: 'datetime',
labels: { style: { fontFamily: 'Poppins, sans-serif', colors: '#ffffff' } }, labels: { style: { fontFamily: 'Poppins, sans-serif', colors: isDark ? '#ffffff' : '#333333'} },
axisBorder: { color: '#777777' }, axisBorder: { color: isDark ? '#777777' : '#cccccc' },
axisTicks: { color: '#777777' }, axisTicks: { color: isDark ? '#777777' : '#cccccc' },
}, },
yaxis: { yaxis: {
labels: { style: { fontFamily: 'Poppins, sans-serif', colors: '#ffffff' } }, labels: { style: { fontFamily: 'Poppins, sans-serif', colors: isDark ? '#ffffff' : '#333333' } },
}, },
grid: { grid: {
borderColor: '#777777', borderColor: '#777777',
strokeDashArray: 4, strokeDashArray: 4,
}, },
tooltip: { tooltip: {
theme: 'dark', theme: isDark ? 'dark' : 'light',
x: { format: 'dd/MM/yy HH:mm' }, x: { format: 'dd/MM/yy HH:mm' },
y: { formatter: (val) => `${val} Mbps` }, y: { formatter: (val) => `${val} Mbps` },
}, },
colors: ['#4CAF50'], colors: [ isDark ? '#4CAF50' : '#28a745'],
fill: { fill: {
type: 'gradient', type: 'gradient',
gradient: { gradient: {
shade: 'dark', shade: isDark ? 'dark' : 'light',
type: 'vertical', type: 'vertical',
gradientToColors: ['#28a745'], gradientToColors: [ isDark ? '#28a745' : '#4CAF50'],
stops: [0, 100], stops: [0, 100],
}, },
}, },

View File

@ -1,7 +1,10 @@
/* eslint-disable react/prop-types */ /* eslint-disable react/prop-types */
import ReactApexChart from 'react-apexcharts'; import ReactApexChart from 'react-apexcharts';
const PingChart = ({ data }) => { const PingChart = ({ data, theme }) => {
const isDark = theme === 'dark';
const chartData = { const chartData = {
series: [ series: [
{ {
@ -16,39 +19,39 @@ const PingChart = ({ data }) => {
chart: { chart: {
height: 250, height: 250,
type: 'area', type: 'area',
background: '#1e1e1e', background: isDark ? '#1e1e1e' : '#ffffff',
toolbar: { show: false }, toolbar: { show: false },
}, },
dataLabels: { enabled: false }, dataLabels: { enabled: false },
stroke: { stroke: {
curve: 'smooth', curve: 'smooth',
colors: ['#ffc107'], colors: [ isDark ? '#ffc107' : '#d4a307'],
}, },
xaxis: { xaxis: {
type: 'datetime', type: 'datetime',
labels: { style: { fontFamily: 'Poppins, sans-serif', colors: '#ffffff' } }, labels: { style: { fontFamily: 'Poppins, sans-serif', colors: isDark ? '#ffffff' : '#333333' } },
axisBorder: { color: '#777777' }, axisBorder: { color: isDark ? '#777777' : '#cccccc' },
axisTicks: { color: '#777777' }, axisTicks: { color: isDark ? '#777777' : '#cccccc' },
}, },
yaxis: { yaxis: {
labels: { style: { fontFamily: 'Poppins, sans-serif', colors: '#ffffff' } }, labels: { style: { fontFamily: 'Poppins, sans-serif', colors: isDark ? '#ffffff' : '#333333' } },
}, },
grid: { grid: {
borderColor: '#777777', borderColor: isDark ? '#777777' : '#cccccc',
strokeDashArray: 4, strokeDashArray: 4,
}, },
tooltip: { tooltip: {
theme: 'dark', theme: isDark ? 'dark' : 'light',
x: { format: 'dd/MM/yy HH:mm' }, x: { format: 'dd/MM/yy HH:mm' },
y: { formatter: (val) => `${val} Mbps` }, y: { formatter: (val) => `${val} Mbps` },
}, },
colors: ['#ffc107'], colors: [ isDark ? '#ffc107' : '#d4a307'],
fill: { fill: {
type: 'gradient', type: 'gradient',
gradient: { gradient: {
shade: 'dark', shade: isDark ? 'dark' : 'light',
type: 'vertical', type: 'vertical',
gradientToColors: ['#ffc107'], gradientToColors: [ isDark ? '#ffc107' : '#d4a307'],
stops: [0, 100], stops: [0, 100],
}, },
}, },

View File

@ -1,6 +1,9 @@
import ReactApexChart from 'react-apexcharts'; import ReactApexChart from 'react-apexcharts';
const UploadChart = ({ data }) => { const UploadChart = ({ data, theme }) => {
const isDark = theme === 'dark';
const chartData = { const chartData = {
series: [ series: [
{ {
@ -15,39 +18,39 @@ const UploadChart = ({ data }) => {
chart: { chart: {
height: 250, height: 250,
type: 'area', type: 'area',
background: '#1e1e1e', background: isDark ? '#1e1e1e' : '#ffffff',
toolbar: { show: false }, toolbar: { show: false },
}, },
dataLabels: { enabled: false }, dataLabels: { enabled: false },
stroke: { stroke: {
curve: 'smooth', curve: 'smooth',
colors: ['#00bcd4'], colors: [ isDark ? '#00bcd4' : '#0097a7'],
}, },
xaxis: { xaxis: {
type: 'datetime', type: 'datetime',
labels: { style: { fontFamily: 'Poppins, sans-serif', colors: '#ffffff' } }, labels: { style: { fontFamily: 'Poppins, sans-serif', colors: isDark ? '#ffffff' : '#333333' } },
axisBorder: { color: '#777777' }, axisBorder: { color: isDark ? '#777777' : '#cccccc' },
axisTicks: { color: '#777777' }, axisTicks: { color: isDark ? '#777777' : '#cccccc' },
}, },
yaxis: { yaxis: {
labels: { style: { fontFamily: 'Poppins, sans-serif', colors: '#ffffff' } }, labels: { style: { fontFamily: 'Poppins, sans-serif', colors: isDark ? '#ffffff' : '#333333' } },
}, },
grid: { grid: {
borderColor: '#777777', borderColor: isDark ? '#777777' : '#cccccc',
strokeDashArray: 4, strokeDashArray: 4,
}, },
tooltip: { tooltip: {
theme: 'dark', theme: isDark ? 'dark' : 'light',
x: { format: 'dd/MM/yy HH:mm' }, x: { format: 'dd/MM/yy HH:mm' },
y: { formatter: (val) => `${val} Mbps` }, y: { formatter: (val) => `${val} Mbps` },
}, },
colors: ['#00bcd4'], colors: [ isDark ? '#00bcd4' : '#0097a7'],
fill: { fill: {
type: 'gradient', type: 'gradient',
gradient: { gradient: {
shade: 'dark', shade: isDark ? 'dark' : 'light',
type: 'vertical', type: 'vertical',
gradientToColors: ['#007bff'], gradientToColors: [ isDark ? '#0097a7' : '#00bcd4'],
stops: [0, 100], stops: [0, 100],
}, },
}, },

View File

@ -1,41 +1,43 @@
/* eslint-disable react/prop-types */ /* eslint-disable react/prop-types */
import { Table } from 'react-bootstrap';
import { FaCheckCircle, FaTimesCircle } from "react-icons/fa"; import { FaCheckCircle, FaTimesCircle } from "react-icons/fa";
const AppDevice = ({ devices }) => { const AppDevice = ({ devices, theme }) => {
return ( return (
<> <div className={`device-table-container ${theme}`}>
<Table striped bordered hover responsive> {/* Header */}
<thead> <div className="device-table-header">
<tr> <div className="header-item">IP Address</div>
<th>IP Address</th> <div className="header-item">Device Name</div>
<th>Device Name</th> <div className="header-item">Status</div>
<th>Status</th> </div>
</tr>
</thead> {/* Body */}
<tbody> <div className="device-table-body">
{devices.map((device) => ( {devices.map((device) => (
<tr key={device.id}> <div className="device-table-row" key={device.id}>
<td>{device.ip}</td> <div className="row-item">{device.ip}</div>
<td>{device.name}</td> <div className="row-item">{device.name}</div>
<td style={{ color: device.is_online === "Online" ? "green" : "red", display: "flex", alignItems: "center" }}> <div
className={`row-item status ${
device.is_online === "Online" ? "online" : "offline"
}`}
>
{device.is_online === "Online" ? ( {device.is_online === "Online" ? (
<> <>
<FaCheckCircle className="me-2" color="green" /> <FaCheckCircle className="me-2" />
Online Online
</> </>
) : ( ) : (
<> <>
<FaTimesCircle className="me-2" color="red" /> <FaTimesCircle className="me-2" />
Offline Offline
</> </>
)} )}
</td> </div>
</tr> </div>
))} ))}
</tbody> </div>
</Table> </div>
</>
); );
}; };

View File

@ -2,7 +2,7 @@
import { Card, Col, Row } from 'react-bootstrap'; import { Card, Col, Row } from 'react-bootstrap';
import { FaDownload, FaUpload, FaTachometerAlt } from 'react-icons/fa'; import { FaDownload, FaUpload, FaTachometerAlt } from 'react-icons/fa';
const AverageData = ({ data }) => { const AverageData = ({ data, theme }) => {
const average = { const average = {
download: 0, download: 0,
upload: 0, upload: 0,
@ -24,7 +24,7 @@ const AverageData = ({ data }) => {
return ( return (
<Row> <Row>
<Col md={4}> <Col md={4}>
<Card className="mb-4 shadow-sm bg-dark text-light"> <Card className={`card ${theme} mb-4 shadow-sm`}>
<Card.Header as="h6"> <Card.Header as="h6">
<FaDownload style={{ marginRight: '10px', width: '15px', color: '#28a745' }} /> <FaDownload style={{ marginRight: '10px', width: '15px', color: '#28a745' }} />
Average Download Average Download
@ -35,7 +35,7 @@ const AverageData = ({ data }) => {
</Card> </Card>
</Col> </Col>
<Col md={4}> <Col md={4}>
<Card className="mb-4 shadow-sm bg-dark text-light"> <Card className={`card ${theme} mb-4 shadow-sm`}>
<Card.Header as="h6"> <Card.Header as="h6">
<FaUpload style={{ marginRight: '10px', width: '15px', color: '#00bcd4' }} /> <FaUpload style={{ marginRight: '10px', width: '15px', color: '#00bcd4' }} />
Average Upload Average Upload
@ -46,7 +46,7 @@ const AverageData = ({ data }) => {
</Card> </Card>
</Col> </Col>
<Col md={4}> <Col md={4}>
<Card className="mb-4 shadow-sm bg-dark text-light"> <Card className={`card ${theme} mb-4 shadow-sm`}>
<Card.Header as="h6"> <Card.Header as="h6">
<FaTachometerAlt style={{ marginRight: '10px', width: '15px', color: '#ffc107' }} /> <FaTachometerAlt style={{ marginRight: '10px', width: '15px', color: '#ffc107' }} />
Average Ping Average Ping

View File

@ -10,35 +10,35 @@ const Chart = ({ data, theme }) => {
<> <>
<Row> <Row>
<Col md="4"> <Col md="4">
<Card className={`mb-4 shadow-sm ${theme}`}> <Card className="mb-4 shadow-sm">
<Card.Body> <Card.Body>
<Card.Title> <Card.Title>
<FaDownload style={{ marginRight: '10px', color: '#28a745'}} /> <FaDownload style={{ marginRight: '10px', color: '#28a745'}} />
Download Download
</Card.Title> </Card.Title>
<DownloadChart data={data} title="Download Speed" type="download" /> <DownloadChart theme={theme} data={data} title="Download Speed" type="download" />
</Card.Body> </Card.Body>
</Card> </Card>
</Col> </Col>
<Col md="4"> <Col md="4">
<Card className={`mb-4 shadow-sm ${theme}`}> <Card className="mb-4 shadow-sm">
<Card.Body> <Card.Body>
<Card.Title> <Card.Title>
<FaUpload style={{ marginRight: '10px', width: '15px', color: '#00bcd4' }} /> <FaUpload style={{ marginRight: '10px', width: '15px', color: '#00bcd4' }} />
Upload Upload
</Card.Title> </Card.Title>
<UploadChart data={data} title="Upload Speed" type="upload" /> <UploadChart theme={theme} data={data} title="Upload Speed" type="upload" />
</Card.Body> </Card.Body>
</Card> </Card>
</Col> </Col>
<Col md="4"> <Col md="4">
<Card className={`mb-4 shadow-sm ${theme}`}> <Card className="mb-4 shadow-sm">
<Card.Body> <Card.Body>
<Card.Title> <Card.Title>
<FaTachometerAlt style={{ marginRight: '10px', width: '15px', color: '#ffc107' }} /> <FaTachometerAlt style={{ marginRight: '10px', width: '15px', color: '#ffc107' }} />
Ping Ping
</Card.Title> </Card.Title>
<PingChart data={data} title="Ping" type="ping" /> <PingChart theme={theme} data={data} title="Ping" type="ping" />
</Card.Body> </Card.Body>
</Card> </Card>
</Col> </Col>

View File

@ -1,36 +1,33 @@
import Table from 'react-bootstrap/Table';
const MetricsTable = ({ metrics = [] }) => { const MetricsTable = ({ metrics = [], theme }) => {
return ( return (
<div style={{ overflowX: "auto", maxHeight: "450px", overflowY: "auto", position: "relative", borderTop: "1px solid #444", backgroundColor: "#1e1e1e" }}> <div className={`metrics-container ${theme}`}>
<Table striped bordered hover responsive variant='dark' style={{ fontFamily: 'Poppins, sans-serif', minWidth: "900px", backgroundColor: "#1e1e1e", color: "#ffffff" }}> <div className="metrics-header">
<thead> <div>Monitor Name</div>
<tr style={{ backgroundColor: "#333333", color: "#ffffff" }}> <div>URL</div>
<th className="py-3 text-center align-middle">Monitor Name</th> <div>Response Time (ms)</div>
<th className="py-3 text-center align-middle">URL</th> <div>Status</div>
<th className="py-3 text-center align-middle">Response Time (ms)</th> <div>Certificate Valid</div>
<th className="py-3 text-center align-middle">Status</th> <div>Cert Days Remaining</div>
<th className="py-3 text-center align-middle">Certificate Valid</th> </div>
<th className="py-3 text-center align-middle">Cert Days Remaining</th> <div className="metrics-body">
</tr>
</thead>
<tbody>
{metrics.map((item, index) => ( {metrics.map((item, index) => (
<tr key={index} style={{ backgroundColor: "#222222", color: "#ffffff" }}> <div key={index} className="metrics-row">
<td>{item.monitor_name}</td> <div>{item.monitor_name}</div>
<td><a href={item.monitor_url} target="_blank" rel="noopener noreferrer" style={{ color: "#4CAF50" }}>{item.monitor_url}</a></td> <div>
<td>{item.response_time} ms</td> <a href={item.monitor_url} target="_blank" rel="noopener noreferrer">
<td> {item.monitor_url}
{item.status === 1 ? 'UP' : </a>
item.status === 0 ? 'DOWN' : </div>
item.status === 2 ? 'PENDING' : 'MAINTENANCE'} <div>{item.response_time} ms</div>
</td> <div>
<td>{item.cert_valid === 1 ? 'Yes' : 'No'}</td> {item.status === 1 ? "UP" : item.status === 0 ? "DOWN" : item.status === 2 ? "PENDING" : "MAINTENANCE"}
<td>{item.cert_days_remaining}</td> </div>
</tr> <div>{item.cert_valid === 1 ? "Yes" : "No"}</div>
<div>{item.cert_days_remaining}</div>
</div>
))} ))}
</tbody> </div>
</Table>
</div> </div>
); );
}; };

View File

@ -1,49 +1,59 @@
/* eslint-disable react/prop-types */ import { format } from "date-fns";
import { Table } from 'react-bootstrap';
import { format } from 'date-fns';
import { FaDownload, FaUpload, FaTachometerAlt } from "react-icons/fa"; import { FaDownload, FaUpload, FaTachometerAlt } from "react-icons/fa";
const DataTable = ({ data = [], theme }) => { const DataTable = ({ data = [], theme }) => {
return ( return (
<div style={{ overflowX: "auto", maxHeight: "400px", overflowY: "auto", position: "relative", marginBottom: "20px"}}> <div className={`data-table-container ${theme}`}>
<Table striped bordered hover responsive style={{ fontFamily: 'Poppins, sans-serif', minWidth: "900px"}} className={`${theme}`}> {/* Header */}
<thead className="bg-secondary text-center" style={{ position: "sticky", top: 0, zIndex: 10, backgroundColor: "#343a40" }}> <div className="data-table-header">
<tr> <div className="header-item">No</div>
<th className="py-3 text-center align-middle">No</th> <div className="header-item">
<th className="py-3 text-center align-middle" style={{ color: '#28a745' }}>
<FaDownload className="me-2" /> Bytes <FaDownload className="me-2" /> Bytes
</th> </div>
<th className="py-3 text-center align-middle" style={{ color: '#28a745' }}> <div className="header-item">
<FaDownload className="me-2" /> Elapsed <FaDownload className="me-2" /> Elapsed
</th> </div>
<th className="py-3 text-center align-middle" style={{ color: '#00bcd4' }}> <div className="header-item">
<FaUpload className="me-2" /> Bytes <FaUpload className="me-2" /> Bytes
</th> </div>
<th className="py-3 text-center align-middle" style={{ color: '#00bcd4' }}> <div className="header-item">
<FaUpload className="me-2" /> Elapsed <FaUpload className="me-2" /> Elapsed
</th> </div>
<th className="py-3 text-center align-middle" style={{ color: '#ffc107' }}> <div className="header-item">
<FaTachometerAlt className="me-2" /> Jitter <FaTachometerAlt className="me-2" /> Jitter
</th> </div>
<th className="py-3 text-center align-middle">Time</th> <div className="header-item">Time</div>
<th className="py-3 text-center align-middle">Server</th> <div className="header-item">Server</div>
</tr> </div>
</thead>
<tbody> {/* Body */}
<div className="data-table-body">
{data.map((item, index) => ( {data.map((item, index) => (
<tr key={item._id}> <div className="data-table-row" key={item._id}>
<td className="py-3 text-center">{index + 1}</td> <div className="row-item">{index + 1}</div>
<td className="py-3 text-center">{(item.data.download.bytes / 1048576).toFixed(1)} Mbps</td> <div className="row-item">
<td className="py-3 text-center">{(item.data.download.elapsed / 1000).toFixed(2)}s</td> {(item.data.download.bytes / 1048576).toFixed(1)} Mbps
<td className="py-3 text-center">{(item.data.upload.bytes / 1048576).toFixed(1)} Mbps</td> </div>
<td className="py-3 text-center">{(item.data.upload.elapsed / 1000).toFixed(2)}s</td> <div className="row-item">
<td className="py-3 text-center">{(item.data.ping.jitter).toFixed(1)}ms</td> {(item.data.download.elapsed / 1000).toFixed(2)}s
<td className="py-3 text-center">{format(new Date(item.datetime), 'eeee, d MMMM yyyy HH:mm:ss')}</td> </div>
<td className="py-3 text-center">{item.data.isp} ({item.data.server.name}, {item.data.server.location})</td> <div className="row-item">
</tr> {(item.data.upload.bytes / 1048576).toFixed(1)} Mbps
</div>
<div className="row-item">
{(item.data.upload.elapsed / 1000).toFixed(2)}s
</div>
<div className="row-item">{item.data.ping.jitter.toFixed(1)}ms</div>
<div className="row-item">
{format(new Date(item.datetime), "eeee, d MMMM yyyy HH:mm:ss")}
</div>
<div className="row-item">
{item.data.isp} ({item.data.server.name},{" "}
{item.data.server.location})
</div>
</div>
))} ))}
</tbody> </div>
</Table>
</div> </div>
); );
}; };

View File

@ -1,22 +1,52 @@
import { Navbar, Nav } from 'react-bootstrap'; import { Navbar, Nav, Modal, Button } from 'react-bootstrap';
import { FaChartLine, FaSun, FaMoon } from 'react-icons/fa'; import { FaChartLine, FaSun, FaMoon, FaBars } from 'react-icons/fa';
import { useState } from 'react';
import { Link } from 'react-router-dom';
const AppNavbar = ({ toggleTheme, theme }) => { const AppNavbar = ({ toggleTheme, theme }) => {
const [showModal, setShowModal] = useState(false);
return ( return (
<Navbar <>
className={`${theme} navbar-app fixed-top w-100 shadow-sm d-flex justify-content-between`} <Navbar className={`${theme} navbar-app fixed-top w-100 shadow-sm d-flex justify-content-between`} style={{ zIndex: 1030, padding: '15px 20px' }}>
style={{ zIndex: 1030, padding: '15px 20px' }} <div className="d-flex align-items-center">
> <Navbar.Brand className={`${theme}`} href="#home" style={{ marginLeft: '10px' }}>
<Navbar.Brand className={`${theme}`} href="#home" style={{ marginLeft: '20px'}}> <FaChartLine style={{ marginRight: '10px', color: '#b31e1e' }} /> Speedtest
<FaChartLine style={{ marginRight: '10px', color: '#b31e1e' }} />
Speedtest Tracker
</Navbar.Brand> </Navbar.Brand>
<Nav className="ml-auto" style={{ marginRight: '20px' }}> </div>
<button onClick={toggleTheme} className="theme-toggle-btn"> <Nav className="ml-auto d-flex align-items-center" style={{ marginRight: '20px' }}>
<button onClick={toggleTheme} className="theme-toggle-btn me-3">
{theme === 'light' ? <FaMoon size={20} /> : <FaSun size={20} />} {theme === 'light' ? <FaMoon size={20} /> : <FaSun size={20} />}
</button> </button>
<button className="sidebar-toggle-btn" onClick={() => setShowModal(true)}>
<FaBars size={20} />
</button>
</Nav> </Nav>
</Navbar> </Navbar>
{/* Modal Sidebar */}
<Modal className={`${theme}`} show={showModal} onHide={() => setShowModal(false)} centered>
<Modal.Header className={`${theme}`} closeButton>
<Modal.Title className={`${theme}`}>Menu</Modal.Title>
</Modal.Header>
<Modal.Body className={`${theme}`}>
<Nav className={`${theme} flex-column`}>
<Nav.Link as={Link} to="/dashboard" onClick={() => setShowModal(false)} className={`${theme}`}>
Dashboard
</Nav.Link>
<Nav.Link as={Link} to="/devices" onClick={() => setShowModal(false)} className={`${theme}`}>
Devices
</Nav.Link>
<Nav.Link as={Link} to="/reports" onClick={() => setShowModal(false)} className={`${theme}`}>
Reports
</Nav.Link>
<Nav.Link as={Link} to="/settings" onClick={() => setShowModal(false)} className={`${theme}`}>
Settings
</Nav.Link>
</Nav>
</Modal.Body>
</Modal>
</>
); );
}; };

View File

@ -1,48 +1,29 @@
import { Navbar, Nav } from 'react-bootstrap'; import { Navbar, Nav } from 'react-bootstrap';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { FaTachometerAlt, FaDesktop, FaChartBar, FaCog } from "react-icons/fa"; import { FaTachometerAlt, FaDesktop, FaChartBar, FaCog, FaBars } from "react-icons/fa";
import { useState, useEffect } from 'react';
const Sidebar = ({ theme }) => { const Sidebar = ({ theme }) => {
const location = useLocation(); const location = useLocation();
return ( return (
<Navbar expand="lg" className={`sidebar-app flex-column ${theme}`}> <div className={`sidebar-app ${theme}`}>
<Nav className="flex-column w-100"> <Nav className="flex-column w-100">
<Nav.Link <Nav.Link as={Link} to="/dashboard" className={location.pathname === "/dashboard" ? "active" : ""}>
as={Link}
to="/dashboard"
className={location.pathname === "/dashboard" ? "active" : ""}
>
<FaTachometerAlt className="me-2" /> Dashboard <FaTachometerAlt className="me-2" /> Dashboard
</Nav.Link> </Nav.Link>
<Nav.Link as={Link} to="/devices" className={location.pathname === "/devices" ? "active" : ""}>
<Nav.Link
as={Link}
to="/devices"
className={location.pathname === "/devices" ? "active" : ""}
>
<FaDesktop className="me-2" /> Devices <FaDesktop className="me-2" /> Devices
</Nav.Link> </Nav.Link>
<Nav.Link as={Link} to="/reports" className={location.pathname === "/reports" ? "active" : ""}>
<Nav.Link
as={Link}
to="/reports"
className={location.pathname === "/reports" ? "active" : ""}
>
<FaChartBar className="me-2" /> Reports <FaChartBar className="me-2" /> Reports
</Nav.Link> </Nav.Link>
<Nav.Link as={Link} to="/settings" className={location.pathname === "/settings" ? "active" : ""}>
<Nav.Link
as={Link}
to="/settings"
className={location.pathname === "/settings" ? "active" : ""}
>
<FaCog className="me-2" /> Settings <FaCog className="me-2" /> Settings
</Nav.Link> </Nav.Link>
</Nav> </Nav>
</Navbar> </div>
); );
} };
export default Sidebar; export default Sidebar;

View File

@ -1,11 +1,11 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Container, Spinner, Alert, Row, Col } from 'react-bootstrap'; import { Container, Spinner, Alert, Row, Col } from 'react-bootstrap';
import { fetchSpeedTestData, fetchMetricsData } from '../services/api'; import { fetchSpeedTestData } from '../services/api';
import AverageData from '../components/dashboard/AverageData'; import AverageData from '../components/dashboard/AverageData';
import Chart from '../components/dashboard/ChartData'; import Chart from '../components/dashboard/ChartData';
import DataTable from '../components/dashboard/TableData'; import DataTable from '../components/dashboard/TableData';
const Dashboard = () => { const Dashboard = ({theme}) => {
const [data, setData] = useState([]); const [data, setData] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
@ -42,18 +42,18 @@ const Dashboard = () => {
} }
return ( return (
<Container fluid className='flex'> <Container fluid className={`flex dashboard-page ${theme}`}>
<Col className="p-3 ms-auto"> <Col className="p-3 ms-auto">
<h4 className='my-3'>Dashboard</h4> <h4 className='my-3'>Dashboard</h4>
<Row> <Row>
<Col> <Col>
<AverageData data={data} /> <AverageData data={data} theme={theme}/>
<Chart data={data} /> <Chart data={data} theme={theme}/>
</Col> </Col>
</Row> </Row>
<Row> <Row>
<Col> <Col>
<DataTable data={data} /> <DataTable data={data} theme={theme}/>
</Col> </Col>
</Row> </Row>
</Col> </Col>

View File

@ -6,7 +6,7 @@ import MetricsTable from '../components/dashboard/MetricsTable';
import AppDevice from '../components/dashboard/AppDevice'; import AppDevice from '../components/dashboard/AppDevice';
function DevicesPage() { function DevicesPage({theme}) {
const [metrics, setMetrics] = useState([]); const [metrics, setMetrics] = useState([]);
const [devices, setDevices] = useState([]); const [devices, setDevices] = useState([]);
@ -36,7 +36,7 @@ function DevicesPage() {
if (loading) { if (loading) {
return ( return (
<Container fluid className="text-center mt-5"> <Container fluid className="text-center mt-5">
<Spinner animation="border" variant="info"/> <Spinner animation="border" variant="danger"/>
</Container> </Container>
); );
} }
@ -50,11 +50,11 @@ function DevicesPage() {
} }
return ( return (
<Container fluid className='d-flex' style={{ fontFamily: 'Poppins, sans-serif'}}> <Container fluid className={`device-page flex {$theme}`} style={{ fontFamily: 'Poppins, sans-serif'}}>
<Col className="p-3 ms-auto"> <Col className="p-3 ms-auto">
<h4 className='my-3'>Devices Page</h4> <h4 className='my-3'>Devices Page</h4>
<AppDevice devices={devices}/> <AppDevice devices={devices} theme={theme}/>
<MetricsTable metrics={metrics}/> <MetricsTable metrics={metrics} theme={theme}/>
</Col> </Col>
</Container> </Container>
); );