Luisnguyen1 commited on
Commit
936232e
·
1 Parent(s): 0912c84

Update space

Browse files
Files changed (3) hide show
  1. index.html +159 -18
  2. main.js +438 -0
  3. style.css +433 -18
index.html CHANGED
@@ -1,19 +1,160 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>IoT Dashboard - Quản lý LED & Cảm biến</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
9
+ <link rel="stylesheet" href="style.css">
10
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
11
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
12
+ </head>
13
+ <body class="bg-black text-white overflow-x-hidden">
14
+ <div id="vanta-bg" class="absolute inset-0 -z-10"></div>
15
+
16
+ <!-- Connection Status -->
17
+ <div class="connection-status">
18
+ <span id="status-indicator" class="status-indicator status-offline"></span>
19
+ <span id="connection-status">Connecting...</span>
20
+ </div>
21
+
22
+ <div class="dashboard-container">
23
+ <!-- Header -->
24
+ <header class="text-center" data-aos="fade-down">
25
+ <h1 class="text-4xl font-bold text-cyan-400 drop-shadow-lg mb-2">
26
+ <i class="fas fa-microchip mr-3"></i>
27
+ IoT Dashboard
28
+ </h1>
29
+ <p class="text-cyan-300 text-lg">Hệ thống quản lý LED & giám sát cảm biến thời gian thực</p>
30
+ </header>
31
+
32
+ <!-- Main Control Grid -->
33
+ <div class="main-grid">
34
+ <!-- LED Controls Section -->
35
+ <div class="led-section" data-aos="fade-right">
36
+ <h2 class="text-2xl font-bold text-cyan-300 mb-4">
37
+ <i class="fas fa-lightbulb mr-2"></i>
38
+ Điều khiển LED
39
+ </h2>
40
+
41
+ <div class="led-controls">
42
+ <!-- LED 1 -->
43
+ <div class="led-card" data-aos="zoom-in" data-aos-delay="100">
44
+ <div class="led-info">
45
+ <h3 class="text-xl font-bold text-cyan-300">
46
+ <i class="fas fa-circle mr-2"></i>
47
+ LED 1
48
+ </h3>
49
+ <p class="text-sm mt-2">
50
+ Trạng thái: <span id="led1-status" class="font-bold text-red-400">OFF</span>
51
+ </p>
52
+ </div>
53
+ <div class="led-control">
54
+ <div id="led1-toggle" class="led-toggle off"></div>
55
+ </div>
56
+ </div>
57
+ </div>
58
+
59
+ <!-- Time Display Section -->
60
+ <div class="time-section" data-aos="fade-up" data-aos-delay="200">
61
+ <h2 class="text-2xl font-bold text-cyan-300 mb-4">
62
+ <i class="fas fa-clock mr-2"></i>
63
+ Thời gian hệ thống
64
+ </h2>
65
+
66
+ <div class="time-panel">
67
+ <div class="time-display">
68
+ <div class="current-time">
69
+ <div class="time-label">
70
+ <i class="fas fa-hourglass-half mr-2"></i>
71
+ Giờ hiện tại
72
+ </div>
73
+ <div class="time-value" id="current-time">
74
+ --:--:--
75
+ </div>
76
+ </div>
77
+
78
+ <div class="current-date">
79
+ <div class="date-label">
80
+ <i class="fas fa-calendar-alt mr-2"></i>
81
+ Ngày tháng năm
82
+ </div>
83
+ <div class="date-value" id="current-date">
84
+ --/--/----
85
+ </div>
86
+ </div>
87
+ </div>
88
+ </div>
89
+ </div>
90
+ </div>
91
+
92
+ <!-- Sensor Panel -->
93
+ <div class="sensor-section" data-aos="fade-up">
94
+ <h2 class="text-2xl font-bold text-cyan-300 mb-4">
95
+ <i class="fas fa-thermometer-half mr-2"></i>
96
+ Cảm biến Khu A
97
+ </h2>
98
+
99
+ <div class="sensor-panel">
100
+ <div class="sensor-grid">
101
+ <!-- Temperature -->
102
+ <div class="sensor-item" data-aos="flip-left" data-aos-delay="100">
103
+ <div class="sensor-label">
104
+ <i class="fas fa-temperature-high mr-2"></i>
105
+ Nhiệt độ
106
+ </div>
107
+ <div class="sensor-value">
108
+ <span id="temperature-value">--</span>
109
+ <span class="sensor-unit">°C</span>
110
+ </div>
111
+ </div>
112
+
113
+ <!-- Humidity -->
114
+ <div class="sensor-item" data-aos="flip-left" data-aos-delay="200">
115
+ <div class="sensor-label">
116
+ <i class="fas fa-tint mr-2"></i>
117
+ Độ ẩm
118
+ </div>
119
+ <div class="sensor-value">
120
+ <span id="humidity-value">--</span>
121
+ <span class="sensor-unit">%</span>
122
+ </div>
123
+ </div>
124
+
125
+ <!-- Gas Sensor -->
126
+ <div class="sensor-item" data-aos="flip-left" data-aos-delay="300">
127
+ <div class="sensor-label">
128
+ <i class="fas fa-smog mr-2"></i>
129
+ Khí Gas
130
+ </div>
131
+ <div class="sensor-value">
132
+ <span id="gas-value">--</span>
133
+ <span class="sensor-unit">ppm</span>
134
+ </div>
135
+ </div>
136
+ </div>
137
+ </div>
138
+ </div>
139
+
140
+ <!-- Chart Section -->
141
+ <div class="chart-section" data-aos="fade-left">
142
+ <h2 class="text-2xl font-bold text-cyan-300 mb-4">
143
+ <i class="fas fa-chart-line mr-2"></i>
144
+ Biểu đồ theo dõi
145
+ </h2>
146
+
147
+ <div class="chart-panel">
148
+ <canvas id="sensorChart" width="400" height="300"></canvas>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ </div>
153
+
154
+ <!-- Scripts -->
155
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r121/three.min.js"></script>
156
+ <script src="https://cdn.jsdelivr.net/npm/vanta@latest/dist/vanta.net.min.js"></script>
157
+ <script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script>
158
+ <script src="main.js"></script>
159
+ </body>
160
  </html>
main.js ADDED
@@ -0,0 +1,438 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Firebase Database URL
2
+ const FIREBASE_URL = 'https://dht22-3b177-default-rtdb.firebaseio.com';
3
+
4
+ // Global variables
5
+ let firebaseData = {};
6
+ let connectionStatus = false;
7
+ let sensorChart = null;
8
+ let chartData = {
9
+ temperature: [],
10
+ humidity: [],
11
+ gas: [],
12
+ timestamps: []
13
+ };
14
+
15
+ // Initialize the dashboard
16
+ document.addEventListener('DOMContentLoaded', function() {
17
+ initializeVantaBackground();
18
+ initializeAOS();
19
+ initializeChart();
20
+ startFirebaseListener();
21
+ setupLEDControls();
22
+ startTimeDisplay();
23
+
24
+ console.log('IoT Dashboard initialized');
25
+ });
26
+
27
+ // Initialize Vanta.js background
28
+ function initializeVantaBackground() {
29
+ if (typeof VANTA !== 'undefined') {
30
+ VANTA.NET({
31
+ el: "#vanta-bg",
32
+ mouseControls: true,
33
+ touchControls: true,
34
+ gyroControls: false,
35
+ minHeight: 200.00,
36
+ minWidth: 200.00,
37
+ scale: 1.00,
38
+ scaleMobile: 1.00,
39
+ color: 0x00ffff,
40
+ backgroundColor: 0x000000,
41
+ points: 10.00,
42
+ maxDistance: 25.00,
43
+ spacing: 20.00
44
+ });
45
+ }
46
+ }
47
+
48
+ // Initialize AOS animations
49
+ function initializeAOS() {
50
+ if (typeof AOS !== 'undefined') {
51
+ AOS.init({
52
+ duration: 1000,
53
+ once: true,
54
+ offset: 100
55
+ });
56
+ }
57
+ }
58
+
59
+ // Initialize Chart
60
+ function initializeChart() {
61
+ const ctx = document.getElementById('sensorChart').getContext('2d');
62
+
63
+ sensorChart = new Chart(ctx, {
64
+ type: 'line',
65
+ data: {
66
+ labels: chartData.timestamps,
67
+ datasets: [
68
+ {
69
+ label: 'Nhiệt độ (°C)',
70
+ data: chartData.temperature,
71
+ borderColor: '#ff6b6b',
72
+ backgroundColor: 'rgba(255, 107, 107, 0.1)',
73
+ borderWidth: 2,
74
+ fill: false,
75
+ tension: 0.4
76
+ },
77
+ {
78
+ label: 'Độ ẩm (%)',
79
+ data: chartData.humidity,
80
+ borderColor: '#4ecdc4',
81
+ backgroundColor: 'rgba(78, 205, 196, 0.1)',
82
+ borderWidth: 2,
83
+ fill: false,
84
+ tension: 0.4
85
+ },
86
+ {
87
+ label: 'Khí Gas (ppm)',
88
+ data: chartData.gas,
89
+ borderColor: '#45b7d1',
90
+ backgroundColor: 'rgba(69, 183, 209, 0.1)',
91
+ borderWidth: 2,
92
+ fill: false,
93
+ tension: 0.4
94
+ }
95
+ ]
96
+ },
97
+ options: {
98
+ responsive: true,
99
+ maintainAspectRatio: false,
100
+ aspectRatio: 1.5,
101
+ scales: {
102
+ x: {
103
+ display: true,
104
+ title: {
105
+ display: true,
106
+ text: 'Thời gian',
107
+ color: '#00ffff'
108
+ },
109
+ ticks: {
110
+ color: '#ffffff'
111
+ },
112
+ grid: {
113
+ color: 'rgba(0, 255, 255, 0.1)'
114
+ }
115
+ },
116
+ y: {
117
+ display: true,
118
+ title: {
119
+ display: true,
120
+ text: 'Giá trị',
121
+ color: '#00ffff'
122
+ },
123
+ ticks: {
124
+ color: '#ffffff'
125
+ },
126
+ grid: {
127
+ color: 'rgba(0, 255, 255, 0.1)'
128
+ }
129
+ }
130
+ },
131
+ plugins: {
132
+ legend: {
133
+ labels: {
134
+ color: '#ffffff'
135
+ }
136
+ }
137
+ },
138
+ elements: {
139
+ point: {
140
+ radius: 4,
141
+ hoverRadius: 6
142
+ }
143
+ }
144
+ }
145
+ });
146
+ }
147
+
148
+ // Setup LED control buttons
149
+ function setupLEDControls() {
150
+ const toggle = document.getElementById('led1-toggle');
151
+ if (toggle) {
152
+ toggle.addEventListener('click', () => toggleLED(1));
153
+ }
154
+ }
155
+
156
+ // Toggle LED state
157
+ async function toggleLED(ledNumber) {
158
+ const toggle = document.getElementById(`led${ledNumber}-toggle`);
159
+ const currentState = toggle.classList.contains('on') ? 1 : 0;
160
+ const newState = currentState === 1 ? 0 : 1;
161
+
162
+ try {
163
+ // Update Firebase
164
+ const response = await fetch(`${FIREBASE_URL}/Control/LED${ledNumber}.json`, {
165
+ method: 'PUT',
166
+ headers: {
167
+ 'Content-Type': 'application/json',
168
+ },
169
+ body: JSON.stringify(newState)
170
+ });
171
+
172
+ if (response.ok) {
173
+ updateLEDUI(ledNumber, newState);
174
+ console.log(`LED${ledNumber} updated to ${newState}`);
175
+ } else {
176
+ console.error(`Failed to update LED${ledNumber}`);
177
+ }
178
+ } catch (error) {
179
+ console.error('Error updating LED:', error);
180
+ showConnectionError();
181
+ }
182
+ }
183
+
184
+ // Update LED UI
185
+ function updateLEDUI(ledNumber, state) {
186
+ const toggle = document.getElementById(`led${ledNumber}-toggle`);
187
+ const status = document.getElementById(`led${ledNumber}-status`);
188
+
189
+ if (toggle && status) {
190
+ if (state === 1) {
191
+ toggle.classList.remove('off');
192
+ toggle.classList.add('on');
193
+ status.textContent = 'ON';
194
+ status.style.color = '#00ff00';
195
+ } else {
196
+ toggle.classList.remove('on');
197
+ toggle.classList.add('off');
198
+ status.textContent = 'OFF';
199
+ status.style.color = '#ff4444';
200
+ }
201
+ }
202
+ }
203
+
204
+ // Start Firebase real-time listener
205
+ function startFirebaseListener() {
206
+ // Listen for data changes
207
+ setInterval(fetchFirebaseData, 2000); // Update every 2 seconds
208
+ fetchFirebaseData(); // Initial fetch
209
+ }
210
+
211
+ // Fetch data from Firebase
212
+ async function fetchFirebaseData() {
213
+ try {
214
+ const response = await fetch(`${FIREBASE_URL}/.json`);
215
+
216
+ if (response.ok) {
217
+ const data = await response.json();
218
+ firebaseData = data;
219
+ updateConnectionStatus(true);
220
+ updateUI(data);
221
+ } else {
222
+ updateConnectionStatus(false);
223
+ console.error('Failed to fetch Firebase data');
224
+ }
225
+ } catch (error) {
226
+ updateConnectionStatus(false);
227
+ console.error('Firebase connection error:', error);
228
+ }
229
+ }
230
+
231
+ // Update UI with Firebase data
232
+ function updateUI(data) {
233
+ // Update LED states
234
+ if (data.Control) {
235
+ const ledState = data.Control.LED1;
236
+ if (ledState !== undefined) {
237
+ updateLEDUI(1, ledState);
238
+ }
239
+ }
240
+
241
+ // Update sensor data
242
+ if (data.cambien && data.cambien.KhuA) {
243
+ const sensorData = data.cambien.KhuA;
244
+ updateSensorUI(sensorData);
245
+ }
246
+ }
247
+
248
+ // Update sensor UI
249
+ function updateSensorUI(sensorData) {
250
+ // Temperature
251
+ const tempElement = document.getElementById('temperature-value');
252
+ if (tempElement && sensorData.temperature !== undefined) {
253
+ tempElement.textContent = sensorData.temperature.toFixed(1);
254
+ animateValueChange(tempElement);
255
+ }
256
+
257
+ // Humidity
258
+ const humidityElement = document.getElementById('humidity-value');
259
+ if (humidityElement && sensorData.humidity !== undefined) {
260
+ humidityElement.textContent = sensorData.humidity.toFixed(1);
261
+ animateValueChange(humidityElement);
262
+ }
263
+
264
+ // Air Quality Index
265
+ const aqiElement = document.getElementById('aqi-value');
266
+ if (aqiElement && sensorData.aqi !== undefined) {
267
+ aqiElement.textContent = sensorData.aqi.toFixed(1);
268
+ animateValueChange(aqiElement);
269
+
270
+ // Update AQI color based on value
271
+ updateAQIColor(aqiElement, sensorData.aqi);
272
+ }
273
+
274
+ // Gas Sensor
275
+ const gasElement = document.getElementById('gas-value');
276
+ if (gasElement && sensorData.gas !== undefined) {
277
+ gasElement.textContent = sensorData.gas.toFixed(1);
278
+ animateValueChange(gasElement);
279
+
280
+ // Update gas color based on value
281
+ updateGasColor(gasElement, sensorData.gas);
282
+ }
283
+
284
+ // Update chart data
285
+ updateChartData(sensorData);
286
+ }
287
+
288
+ // Update chart with new sensor data
289
+ function updateChartData(sensorData) {
290
+ const now = new Date();
291
+ const timeString = now.toLocaleTimeString('vi-VN', {
292
+ hour: '2-digit',
293
+ minute: '2-digit'
294
+ });
295
+
296
+ // Add new data
297
+ chartData.timestamps.push(timeString);
298
+ chartData.temperature.push(sensorData.temperature || 0);
299
+ chartData.humidity.push(sensorData.humidity || 0);
300
+ chartData.gas.push(sensorData.gas || 0);
301
+
302
+ // Keep only last 10 data points
303
+ const maxDataPoints = 10;
304
+ if (chartData.timestamps.length > maxDataPoints) {
305
+ chartData.timestamps.shift();
306
+ chartData.temperature.shift();
307
+ chartData.humidity.shift();
308
+ chartData.gas.shift();
309
+ }
310
+
311
+ // Update chart
312
+ if (sensorChart) {
313
+ sensorChart.data.labels = chartData.timestamps;
314
+ sensorChart.data.datasets[0].data = chartData.temperature;
315
+ sensorChart.data.datasets[1].data = chartData.humidity;
316
+ sensorChart.data.datasets[2].data = chartData.gas;
317
+ sensorChart.update('none'); // No animation for smooth updates
318
+ }
319
+ }
320
+
321
+ // Update AQI color based on value
322
+ function updateAQIColor(element, value) {
323
+ let color = '#00ff00'; // Good (0-50)
324
+
325
+ if (value > 100) {
326
+ color = '#ff4444'; // Unhealthy (101-150)
327
+ } else if (value > 50) {
328
+ color = '#ffaa00'; // Moderate (51-100)
329
+ }
330
+
331
+ element.style.color = color;
332
+ element.style.textShadow = `0 0 10px ${color}`;
333
+ }
334
+
335
+ // Update Gas color based on value (ppm)
336
+ function updateGasColor(element, value) {
337
+ let color = '#00ff00'; // Safe (0-30)
338
+
339
+ if (value > 100) {
340
+ color = '#ff4444'; // Dangerous (>100 ppm)
341
+ } else if (value > 50) {
342
+ color = '#ffaa00'; // Warning (51-100 ppm)
343
+ } else if (value > 30) {
344
+ color = '#ffdd00'; // Caution (31-50 ppm)
345
+ }
346
+
347
+ element.style.color = color;
348
+ element.style.textShadow = `0 0 10px ${color}`;
349
+ }
350
+
351
+ // Animate value change
352
+ function animateValueChange(element) {
353
+ element.style.transform = 'scale(1.1)';
354
+ element.style.transition = 'transform 0.2s ease';
355
+
356
+ setTimeout(() => {
357
+ element.style.transform = 'scale(1)';
358
+ }, 200);
359
+ }
360
+
361
+ // Update connection status
362
+ function updateConnectionStatus(isConnected) {
363
+ const statusElement = document.getElementById('connection-status');
364
+ const statusIndicator = document.getElementById('status-indicator');
365
+
366
+ if (statusElement && statusIndicator) {
367
+ if (isConnected) {
368
+ connectionStatus = true;
369
+ statusElement.textContent = 'Connected';
370
+ statusIndicator.className = 'status-indicator status-online';
371
+ } else {
372
+ connectionStatus = false;
373
+ statusElement.textContent = 'Disconnected';
374
+ statusIndicator.className = 'status-indicator status-offline';
375
+ }
376
+ }
377
+ }
378
+
379
+ // Show connection error
380
+ function showConnectionError() {
381
+ console.error('Connection error - retrying...');
382
+ updateConnectionStatus(false);
383
+
384
+ // Retry connection after 3 seconds
385
+ setTimeout(() => {
386
+ fetchFirebaseData();
387
+ }, 3000);
388
+ }
389
+
390
+ // Start time display
391
+ function startTimeDisplay() {
392
+ updateTimeDisplay(); // Initial update
393
+ setInterval(updateTimeDisplay, 1000); // Update every second
394
+ }
395
+
396
+ // Update time display
397
+ function updateTimeDisplay() {
398
+ const now = new Date();
399
+
400
+ // Format time (HH:MM:SS)
401
+ const timeString = now.toLocaleTimeString('vi-VN', {
402
+ hour: '2-digit',
403
+ minute: '2-digit',
404
+ second: '2-digit'
405
+ });
406
+
407
+ // Format date (DD/MM/YYYY)
408
+ const dateString = now.toLocaleDateString('vi-VN', {
409
+ day: '2-digit',
410
+ month: '2-digit',
411
+ year: 'numeric'
412
+ });
413
+
414
+ // Update display
415
+ const timeElement = document.getElementById('current-time');
416
+ const dateElement = document.getElementById('current-date');
417
+
418
+ if (timeElement) {
419
+ timeElement.textContent = timeString;
420
+ // Removed animation effect for time
421
+ }
422
+
423
+ if (dateElement) {
424
+ dateElement.textContent = dateString;
425
+ }
426
+ }
427
+
428
+ // Utility function to format numbers
429
+ function formatNumber(value, decimals = 1) {
430
+ return parseFloat(value).toFixed(decimals);
431
+ }
432
+
433
+ // Export functions for global access
434
+ window.IoTDashboard = {
435
+ toggleLED,
436
+ fetchFirebaseData,
437
+ updateConnectionStatus
438
+ };
style.css CHANGED
@@ -1,28 +1,443 @@
 
1
  body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
 
4
  }
5
 
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
 
9
  }
10
 
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
  }
17
 
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
 
24
  }
25
 
26
- .card p:last-child {
27
- margin-bottom: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  }
 
1
+ /* Custom Styles for IoT Dashboard */
2
  body {
3
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
4
+ margin: 0;
5
+ padding: 0;
6
  }
7
 
8
+ @media (min-width: 1025px) {
9
+ body {
10
+ overflow: hidden;
11
+ }
12
  }
13
 
14
+ @media (max-width: 1024px) {
15
+ body {
16
+ overflow-x: hidden;
17
+ overflow-y: auto;
18
+ }
19
  }
20
 
21
+ .dashboard-container {
22
+ height: 100vh;
23
+ display: grid;
24
+ grid-template-rows: auto 1fr;
25
+ gap: 1rem;
26
+ padding: 1rem;
27
+ box-sizing: border-box;
28
  }
29
 
30
+ @media (max-width: 1024px) {
31
+ .dashboard-container {
32
+ height: auto;
33
+ min-height: 100vh;
34
+ padding: 1rem;
35
+ }
36
+ }
37
+
38
+ .control-grid {
39
+ display: grid;
40
+ grid-template-columns: 2fr 1fr;
41
+ gap: 1.5rem;
42
+ height: 100%;
43
+ }
44
+
45
+ .main-grid {
46
+ display: grid;
47
+ grid-template-columns: 1fr 1fr 1fr;
48
+ gap: 1.5rem;
49
+ height: calc(100vh - 200px);
50
+ align-items: stretch;
51
+ }
52
+
53
+ .led-section, .sensor-section, .chart-section {
54
+ display: flex;
55
+ flex-direction: column;
56
+ height: 100%;
57
+ min-height: 0;
58
+ }
59
+
60
+ .led-section > *, .sensor-section > *, .chart-section > * {
61
+ flex-shrink: 0;
62
+ }
63
+
64
+ /* LED section specific - chia đều giữa LED control và Time */
65
+ .led-section {
66
+ justify-content: space-between;
67
+ }
68
+
69
+ .led-section > h2:first-child {
70
+ flex: 0 0 auto;
71
+ }
72
+
73
+ .led-controls {
74
+ flex: 0 0 auto;
75
+ }
76
+
77
+ .time-section {
78
+ flex: 1;
79
+ display: flex;
80
+ flex-direction: column;
81
+ justify-content: flex-end;
82
+ margin-top: 1.5rem;
83
+ }
84
+
85
+ /* Sensor section - panel chiếm toàn bộ chiều cao còn lại */
86
+ .sensor-section > h2:first-child {
87
+ flex: 0 0 auto;
88
+ }
89
+
90
+ .sensor-panel {
91
+ flex: 1;
92
+ min-height: 0;
93
+ }
94
+
95
+ /* Chart section - panel chiếm toàn bộ chiều cao còn lại */
96
+ .chart-section > h2:first-child {
97
+ flex: 0 0 auto;
98
+ }
99
+
100
+ .chart-panel {
101
+ flex: 1;
102
+ min-height: 0;
103
+ }
104
+
105
+
106
+ .led-controls {
107
+ display: flex;
108
+ justify-content: center;
109
+ gap: 1rem;
110
+ height: fit-content;
111
+ width: 100%;
112
+ }
113
+
114
+ .led-card {
115
+ background: rgba(255, 255, 255, 0.1);
116
+ backdrop-filter: blur(20px);
117
+ border: 1px solid rgba(0, 255, 255, 0.3);
118
+ border-radius: 15px;
119
+ padding: 1.5rem;
120
+ transition: all 0.3s ease;
121
+ position: relative;
122
+ overflow: hidden;
123
+ width: 100%;
124
+ display: grid;
125
+ grid-template-columns: 2fr 1fr;
126
+ gap: 1rem;
127
+ align-items: center;
128
+ }
129
+
130
+ .led-info {
131
+ display: flex;
132
+ flex-direction: column;
133
+ justify-content: center;
134
+ }
135
+
136
+ .led-control {
137
+ display: flex;
138
+ justify-content: center;
139
+ align-items: center;
140
+ }
141
+
142
+ .led-card::before {
143
+ content: '';
144
+ position: absolute;
145
+ top: 0;
146
+ left: -100%;
147
+ width: 100%;
148
+ height: 100%;
149
+ background: linear-gradient(90deg, transparent, rgba(0, 255, 255, 0.1), transparent);
150
+ transition: left 0.5s;
151
+ }
152
+
153
+ .led-card:hover::before {
154
+ left: 100%;
155
+ }
156
+
157
+ .led-card:hover {
158
+ transform: translateY(-5px);
159
+ border-color: rgba(0, 255, 255, 0.6);
160
+ box-shadow: 0 10px 30px rgba(0, 255, 255, 0.2);
161
+ }
162
+
163
+ .led-toggle {
164
+ width: 80px;
165
+ height: 40px;
166
+ background: #333;
167
+ border-radius: 20px;
168
+ position: relative;
169
+ cursor: pointer;
170
+ transition: all 0.3s ease;
171
+ border: 2px solid #555;
172
+ }
173
+
174
+ .led-toggle.on {
175
+ background: linear-gradient(45deg, #00ff00, #00cc00);
176
+ border-color: #00ff00;
177
+ box-shadow: 0 0 20px rgba(0, 255, 0, 0.5);
178
+ }
179
+
180
+ .led-toggle.off {
181
+ background: linear-gradient(45deg, #ff4444, #cc0000);
182
+ border-color: #ff4444;
183
+ box-shadow: 0 0 20px rgba(255, 68, 68, 0.5);
184
+ }
185
+
186
+ .led-toggle::after {
187
+ content: '';
188
+ position: absolute;
189
+ width: 32px;
190
+ height: 32px;
191
+ background: white;
192
+ border-radius: 50%;
193
+ top: 2px;
194
+ left: 2px;
195
+ transition: all 0.3s ease;
196
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
197
+ }
198
+
199
+ .led-toggle.on::after {
200
+ transform: translateX(40px);
201
+ }
202
+
203
+ .sensor-panel {
204
+ background: rgba(255, 255, 255, 0.1);
205
+ backdrop-filter: blur(20px);
206
+ border: 1px solid rgba(0, 255, 255, 0.3);
207
+ border-radius: 15px;
208
+ padding: 1.5rem;
209
+ display: flex;
210
+ flex-direction: column;
211
+ overflow: hidden;
212
+ }
213
+
214
+ .chart-panel {
215
+ background: rgba(255, 255, 255, 0.1);
216
+ backdrop-filter: blur(20px);
217
+ border: 1px solid rgba(0, 255, 255, 0.3);
218
+ border-radius: 15px;
219
+ padding: 1.5rem;
220
+ display: flex;
221
+ flex-direction: column;
222
+ overflow: hidden;
223
+ }
224
+
225
+ .chart-panel canvas {
226
+ max-width: 100%;
227
+ height: 100% !important;
228
+ flex: 1;
229
+ min-height: 0;
230
+ }
231
+
232
+ .time-panel {
233
+ background: rgba(255, 255, 255, 0.1);
234
+ backdrop-filter: blur(20px);
235
+ border: 1px solid rgba(0, 255, 255, 0.3);
236
+ border-radius: 15px;
237
+ padding: 1.5rem;
238
+ display: flex;
239
+ flex-direction: column;
240
+ justify-content: center;
241
+ flex: 1;
242
+ }
243
+
244
+ .time-display {
245
+ display: grid;
246
+ gap: 1rem;
247
+ }
248
+
249
+ .current-time, .current-date {
250
+ background: rgba(0, 0, 0, 0.3);
251
+ padding: 1rem;
252
+ border-radius: 10px;
253
+ border: 1px solid rgba(0, 255, 255, 0.2);
254
+ position: relative;
255
+ overflow: hidden;
256
+ }
257
+
258
+ .current-time::before, .current-date::before {
259
+ content: '';
260
+ position: absolute;
261
+ top: 0;
262
+ left: 0;
263
+ right: 0;
264
+ height: 3px;
265
+ background: linear-gradient(90deg, #00ffff, #0080ff, #00ffff);
266
+ background-size: 200% 100%;
267
+ animation: flow 2s linear infinite;
268
+ }
269
+
270
+ .time-label, .date-label {
271
+ font-size: 0.9rem;
272
+ color: #00ffff;
273
+ margin-bottom: 0.5rem;
274
+ text-transform: uppercase;
275
+ letter-spacing: 1px;
276
+ }
277
+
278
+ .time-value, .date-value {
279
+ font-size: 1.5rem;
280
+ font-weight: bold;
281
+ color: white;
282
+ text-shadow: 0 0 10px rgba(0, 255, 255, 0.5);
283
+ font-family: 'Courier New', monospace;
284
+ }
285
+
286
+ .sensor-grid {
287
+ display: grid;
288
+ gap: 1rem;
289
+ margin-top: 1rem;
290
+ flex: 1;
291
+ align-content: start;
292
+ }
293
+
294
+ .sensor-item {
295
+ background: rgba(0, 0, 0, 0.3);
296
+ padding: 1rem;
297
+ border-radius: 10px;
298
+ border: 1px solid rgba(0, 255, 255, 0.2);
299
+ position: relative;
300
+ overflow: hidden;
301
+ }
302
+
303
+ .sensor-item::before {
304
+ content: '';
305
+ position: absolute;
306
+ top: 0;
307
+ left: 0;
308
+ right: 0;
309
+ height: 3px;
310
+ background: linear-gradient(90deg, #00ffff, #0080ff, #00ffff);
311
+ background-size: 200% 100%;
312
+ animation: flow 2s linear infinite;
313
+ }
314
+
315
+ @keyframes flow {
316
+ 0% { background-position: -200% 0; }
317
+ 100% { background-position: 200% 0; }
318
+ }
319
+
320
+ .sensor-label {
321
+ font-size: 0.9rem;
322
+ color: #00ffff;
323
+ margin-bottom: 0.5rem;
324
+ text-transform: uppercase;
325
+ letter-spacing: 1px;
326
+ }
327
+
328
+ .sensor-value {
329
+ font-size: 1.8rem;
330
+ font-weight: bold;
331
+ color: white;
332
+ text-shadow: 0 0 10px rgba(0, 255, 255, 0.5);
333
+ }
334
+
335
+ .sensor-unit {
336
+ font-size: 1rem;
337
+ color: #888;
338
+ margin-left: 0.5rem;
339
+ }
340
+
341
+ .status-indicator {
342
+ display: inline-block;
343
+ width: 12px;
344
+ height: 12px;
345
+ border-radius: 50%;
346
+ margin-right: 0.5rem;
347
+ animation: pulse 2s infinite;
348
+ }
349
+
350
+ .status-online {
351
+ background: #00ff00;
352
+ box-shadow: 0 0 10px rgba(0, 255, 0, 0.5);
353
+ }
354
+
355
+ .status-offline {
356
+ background: #ff4444;
357
+ box-shadow: 0 0 10px rgba(255, 68, 68, 0.5);
358
+ }
359
+
360
+ @keyframes pulse {
361
+ 0%, 100% { opacity: 1; }
362
+ 50% { opacity: 0.5; }
363
+ }
364
+
365
+ .connection-status {
366
+ position: absolute;
367
+ top: 1rem;
368
+ right: 1rem;
369
+ font-size: 0.8rem;
370
+ color: #00ffff;
371
+ z-index: 10;
372
+ }
373
+
374
+ .loading {
375
+ display: inline-block;
376
+ width: 20px;
377
+ height: 20px;
378
+ border: 3px solid rgba(0, 255, 255, 0.3);
379
+ border-radius: 50%;
380
+ border-top-color: #00ffff;
381
+ animation: spin 1s ease-in-out infinite;
382
+ }
383
+
384
+ @keyframes spin {
385
+ to { transform: rotate(360deg); }
386
+ }
387
+
388
+ /* Responsive Design */
389
+ @media (max-width: 1200px) {
390
+ .main-grid {
391
+ grid-template-columns: 1fr;
392
+ grid-template-rows: auto auto auto;
393
+ height: auto;
394
+ min-height: 100vh;
395
+ }
396
+
397
+ .led-section, .sensor-section, .chart-section {
398
+ height: auto;
399
+ max-height: none;
400
+ }
401
+
402
+ .sensor-panel, .chart-panel {
403
+ height: auto;
404
+ max-height: none;
405
+ min-height: 400px;
406
+ }
407
+ }
408
+
409
+ @media (max-width: 1024px) {
410
+ .control-grid {
411
+ grid-template-columns: 1fr;
412
+ grid-template-rows: auto auto;
413
+ }
414
+
415
+ .led-card {
416
+ grid-template-columns: 1fr 1fr;
417
+ }
418
+
419
+ .time-display {
420
+ grid-template-columns: 1fr 1fr;
421
+ }
422
+ }
423
+
424
+ @media (max-width: 768px) {
425
+ .led-card {
426
+ grid-template-columns: 1fr;
427
+ text-align: center;
428
+ gap: 1.5rem;
429
+ }
430
+
431
+ .led-info {
432
+ align-items: center;
433
+ text-align: center;
434
+ }
435
+
436
+ .time-display {
437
+ grid-template-columns: 1fr;
438
+ }
439
+
440
+ .dashboard-container {
441
+ padding: 0.5rem;
442
+ }
443
  }