1077 lines
57 KiB
PHP
1077 lines
57 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('title', 'Dashboard')
|
|
|
|
@section('content')
|
|
<!-- SweetAlert2 CSS dan JS -->
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css">
|
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
|
|
|
<!-- Sensor Status Section - Card layout yang lebih responsif -->
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 mt-4 mb-6">
|
|
<!-- Card BH1750 - Dioptimalkan untuk layar kecil -->
|
|
<div class="bg-white rounded-lg shadow p-4 flex flex-col items-center hover:shadow-md transition-all duration-300">
|
|
<div class="w-16 h-16 sm:w-20 sm:h-20 rounded-full bg-blue-100 flex items-center justify-center mb-3">
|
|
<img src="{{ asset('images/bh1750-sensor.png') }}" alt="BH1750" class="w-12 h-12 sm:w-14 sm:h-14 object-contain">
|
|
</div>
|
|
<h3 class="text-base sm:text-lg font-semibold mb-1">Sensor BH1750</h3>
|
|
<div id="status-bh1750" class="text-gray-500 text-center">Memuat...</div>
|
|
</div>
|
|
|
|
<!-- Card DHT11 - Dioptimalkan untuk layar kecil -->
|
|
<div class="bg-white rounded-lg shadow p-4 flex flex-col items-center hover:shadow-md transition-all duration-300">
|
|
<div class="w-16 h-16 sm:w-20 sm:h-20 rounded-full bg-green-100 flex items-center justify-center mb-3">
|
|
<img src="{{ asset('images/dht11-sensor.png') }}" alt="DHT11" class="w-12 h-12 sm:w-14 sm:h-14 object-contain">
|
|
</div>
|
|
<h3 class="text-base sm:text-lg font-semibold mb-1">Sensor DHT11</h3>
|
|
<div id="status-dht11" class="text-gray-500 text-center">Memuat...</div>
|
|
</div>
|
|
|
|
<!-- Card Soil Moisture - Dioptimalkan untuk layar kecil -->
|
|
<div class="bg-white rounded-lg shadow p-4 flex flex-col items-center hover:shadow-md transition-all duration-300">
|
|
<div class="w-16 h-16 sm:w-20 sm:h-20 rounded-full bg-yellow-100 flex items-center justify-center mb-3">
|
|
<img src="{{ asset('images/soil-moisture-sensor.png') }}" alt="Soil Moisture" class="w-12 h-12 sm:w-14 sm:h-14 object-contain">
|
|
</div>
|
|
<h3 class="text-base sm:text-lg font-semibold mb-1">Soil Moisture</h3>
|
|
<div id="status-soil" class="text-gray-500 text-center">Memuat...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ESP32-CAM Viewer Section - Layout yang lebih baik untuk mobile -->
|
|
<div class="mb-6">
|
|
<div class="bg-white rounded-lg shadow-md overflow-hidden">
|
|
<div class="p-4 sm:p-6">
|
|
<h2 class="text-xl sm:text-2xl font-bold text-gray-800 mb-4">ESP32-CAM Monitoring</h2>
|
|
|
|
<!-- Status ESP32-CAM - Form yang lebih responsif -->
|
|
<div class="mb-4 bg-gray-50 rounded-lg p-3 sm:p-4">
|
|
<h3 class="text-base sm:text-lg font-semibold text-gray-700 mb-2">Status ESP32-CAM</h3>
|
|
<div class="flex flex-col space-y-3">
|
|
<form id="checkStatusForm" class="flex flex-col sm:flex-row items-start sm:items-center space-y-2 sm:space-y-0">
|
|
<div class="w-full sm:w-auto flex">
|
|
<input type="text" id="cameraIp" placeholder="192.168.240.201" value="192.168.240.201"
|
|
class="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-l-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
|
|
<button type="submit" class="px-3 py-2 bg-blue-500 text-white rounded-r-md hover:bg-blue-600 transition duration-300 whitespace-nowrap">
|
|
Cek Status
|
|
</button>
|
|
</div>
|
|
<div id="cameraStatus" class="flex items-center sm:ml-3 mt-2 sm:mt-0">
|
|
<span class="inline-block h-3 w-3 rounded-full mr-2 bg-gray-300"></span>
|
|
<span class="text-sm text-gray-500">Belum diperiksa</span>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Live Streaming Controls -->
|
|
<div class="mb-4 bg-blue-50 rounded-lg p-3 sm:p-4">
|
|
<h3 class="text-base sm:text-lg font-semibold text-gray-700 mb-2 flex items-center">
|
|
<svg class="w-5 h-5 mr-2 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
|
</svg>
|
|
Live Streaming
|
|
</h3>
|
|
<div class="flex flex-col space-y-3">
|
|
<div class="flex flex-col sm:flex-row items-start sm:items-center">
|
|
<div class="w-full sm:w-auto flex mb-2 sm:mb-0">
|
|
<input type="text" id="streamIp" placeholder="192.168.240.201" value="192.168.240.201"
|
|
class="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-l-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
|
|
<button id="startStreamBtn" type="button" class="px-3 py-2 bg-green-600 text-white rounded-r-md hover:bg-green-700 transition duration-300 whitespace-nowrap">
|
|
Mulai Stream
|
|
</button>
|
|
</div>
|
|
<button id="stopStreamBtn" type="button" class="px-3 py-2 sm:ml-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition duration-300 whitespace-nowrap disabled:opacity-50 disabled:cursor-not-allowed" disabled>
|
|
Hentikan Stream
|
|
</button>
|
|
</div>
|
|
<div id="streamStatus" class="text-sm text-gray-600">
|
|
Live stream tidak aktif
|
|
</div>
|
|
|
|
<!-- Pengaturan Kamera -->
|
|
<div class="pt-2 border-t border-gray-200 mt-2">
|
|
<h4 class="text-sm font-medium text-gray-700 mb-2">Pengaturan Kamera</h4>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
<div>
|
|
<label for="cameraResolution" class="block text-xs font-medium text-gray-700 mb-1">Resolusi</label>
|
|
<select id="cameraResolution" class="w-full px-3 py-2 text-sm border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
|
|
<option value="UXGA">UXGA (1600x1200)</option>
|
|
<option value="SXGA">SXGA (1280x1024)</option>
|
|
<option value="XGA">XGA (1024x768)</option>
|
|
<option value="SVGA" selected>SVGA (800x600)</option>
|
|
<option value="VGA">VGA (640x480)</option>
|
|
<option value="CIF">CIF (400x296)</option>
|
|
<option value="QVGA">QVGA (320x240)</option>
|
|
<option value="HQVGA">HQVGA (240x176)</option>
|
|
<option value="QQVGA">QQVGA (160x120)</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label for="cameraQuality" class="block text-xs font-medium text-gray-700 mb-1">Kualitas (0-63, rendah=kualitas tinggi)</label>
|
|
<input type="range" id="cameraQuality" min="0" max="63" value="10" class="w-full">
|
|
<div class="flex justify-between text-xs text-gray-500">
|
|
<span>Tinggi</span>
|
|
<span id="qualityValue">10</span>
|
|
<span>Rendah</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button id="applyCameraSettings" type="button" class="mt-2 px-3 py-1.5 bg-blue-500 text-white text-sm rounded-md hover:bg-blue-600 transition duration-300">
|
|
Terapkan Pengaturan
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stream Container -->
|
|
<div id="streamContainer" class="mb-4 hidden">
|
|
<div class="relative w-full">
|
|
<img id="streamImage" src="" alt="Live Stream" class="w-full h-auto rounded-lg shadow">
|
|
<div class="absolute bottom-0 left-0 right-0 bg-black bg-opacity-60 text-white p-2 rounded-b-lg">
|
|
<p class="text-xs sm:text-sm" id="streamInfo">
|
|
Live Stream - Aktif
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Image Container - Responsif untuk berbagai ukuran layar -->
|
|
<div class="flex flex-col items-center mb-4">
|
|
<?php
|
|
$uploadDir = 'storage/esp32cam/';
|
|
$files = glob(public_path($uploadDir) . '*.jpg');
|
|
|
|
if (count($files) > 0) {
|
|
usort($files, function($a, $b) {
|
|
return filemtime($b) - filemtime($a);
|
|
});
|
|
|
|
$latestImage = $uploadDir . basename($files[0]);
|
|
$imageTime = date("d-m-Y H:i:s", filemtime($files[0]));
|
|
$fileName = basename($files[0]);
|
|
|
|
// Mendapatkan timestamp dari nama file (format: esp32cam_YmdHis.jpg)
|
|
$timestampFromFilename = null;
|
|
if (preg_match('/esp32cam_(\d{8})_(\d{6})/', $fileName, $matches)) {
|
|
$dateStr = $matches[1];
|
|
$timeStr = $matches[2];
|
|
$timestampFromFilename = strtotime(
|
|
substr($dateStr, 0, 4) . '-' .
|
|
substr($dateStr, 4, 2) . '-' .
|
|
substr($dateStr, 6, 2) . ' ' .
|
|
substr($timeStr, 0, 2) . ':' .
|
|
substr($timeStr, 2, 2) . ':' .
|
|
substr($timeStr, 4, 2)
|
|
);
|
|
}
|
|
|
|
// Menggunakan timestamp dari nama file jika tersedia
|
|
if ($timestampFromFilename) {
|
|
$imageTime = date("d-m-Y H:i:s", $timestampFromFilename);
|
|
}
|
|
|
|
// Mendapatkan timestamp dari EXIF data jika tersedia
|
|
$exifData = @exif_read_data($files[0]);
|
|
$captureTime = $imageTime; // Default menggunakan waktu file
|
|
|
|
if ($exifData && isset($exifData['DateTimeOriginal'])) {
|
|
// Format EXIF datetime: YYYY:MM:DD HH:MM:SS
|
|
$exifTime = strtotime($exifData['DateTimeOriginal']);
|
|
if ($exifTime) {
|
|
$captureTime = date("d-m-Y H:i:s", $exifTime);
|
|
}
|
|
}
|
|
?>
|
|
<div class="relative w-full">
|
|
<img src="{{ asset($latestImage) }}"
|
|
alt="ESP32-CAM Image"
|
|
class="w-full h-auto rounded-lg shadow">
|
|
<div class="absolute bottom-0 left-0 right-0 bg-black bg-opacity-60 text-white p-2 rounded-b-lg">
|
|
<p class="text-xs sm:text-sm">
|
|
Foto terakhir: {{ $captureTime }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<?php } else { ?>
|
|
<div class="flex flex-col items-center justify-center w-full h-48 sm:h-64 bg-gray-100 rounded-lg">
|
|
<svg class="w-12 h-12 sm:w-16 sm:h-16 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
</svg>
|
|
<p class="mt-3 text-sm text-gray-500">Belum ada foto yang tersedia</p>
|
|
</div>
|
|
<?php } ?>
|
|
|
|
<!-- Tombol untuk mengambil gambar - Dioptimalkan untuk mobile -->
|
|
<div class="mt-3 w-full">
|
|
<form id="captureImageForm" class="flex flex-col sm:flex-row space-y-2 sm:space-y-0">
|
|
<div class="flex flex-1">
|
|
<input type="text" id="captureIp" placeholder="192.168.240.201" value="192.168.240.201"
|
|
class="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-l-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
|
|
<button type="submit" class="px-3 py-2 bg-green-600 text-white rounded-r-md hover:bg-green-700 transition duration-300 flex items-center">
|
|
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"></path>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
|
</svg>
|
|
Ambil Foto
|
|
</button>
|
|
</div>
|
|
</form>
|
|
<div id="captureResult" class="mt-2 text-xs text-gray-600 hidden"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Penjadwalan dan Pembersihan - Tampilan lebih compact -->
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4">
|
|
<!-- Penjadwalan -->
|
|
<div class="p-3 bg-gray-50 rounded-lg">
|
|
<h3 class="text-base font-semibold text-gray-700 mb-2 flex items-center">
|
|
<svg class="w-5 h-5 mr-1 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
Penjadwalan
|
|
</h3>
|
|
<p class="text-xs sm:text-sm text-gray-600 mb-2">
|
|
Sistem mengambil gambar otomatis setiap jam 6 pagi.
|
|
</p>
|
|
<div class="flex items-center">
|
|
<div class="h-3 w-3 rounded-full bg-green-500 mr-2"></div>
|
|
<span class="text-xs sm:text-sm text-gray-700">Aktif</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pembersihan Manual -->
|
|
<div class="p-3 bg-gray-50 rounded-lg">
|
|
<h3 class="text-base font-semibold text-gray-700 mb-2 flex items-center">
|
|
<svg class="w-5 h-5 mr-1 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
</svg>
|
|
Pembersihan Foto
|
|
</h3>
|
|
<p class="text-xs sm:text-sm text-gray-600 mb-2">
|
|
Menghapus foto lama (>7 hari).
|
|
</p>
|
|
<button id="cleanupButton" class="px-3 py-1.5 text-xs sm:text-sm bg-red-500 text-white rounded hover:bg-red-600 transition duration-300">
|
|
Bersihkan Foto Lama
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Image Gallery - Grid yang lebih responsif -->
|
|
<div>
|
|
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-3 gap-2">
|
|
<h3 class="text-lg font-semibold text-gray-700 flex items-center">
|
|
<svg class="w-5 h-5 mr-2 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
</svg>
|
|
Riwayat Foto
|
|
</h3>
|
|
|
|
<!-- Filter Foto Mini -->
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<?php
|
|
// Mengumpulkan tanggal unik dari file foto
|
|
$uniqueDates = [];
|
|
$uniqueMonths = [];
|
|
|
|
foreach ($files as $file) {
|
|
$fileName = basename($file);
|
|
$timestamp = filemtime($file);
|
|
|
|
// Mendapatkan timestamp dari nama file (format: esp32cam_YmdHis.jpg)
|
|
if (preg_match('/esp32cam_(\d{8})_(\d{6})/', $fileName, $matches)) {
|
|
$dateStr = $matches[1];
|
|
$timeStr = $matches[2];
|
|
$timestamp = strtotime(
|
|
substr($dateStr, 0, 4) . '-' .
|
|
substr($dateStr, 4, 2) . '-' .
|
|
substr($dateStr, 6, 2) . ' ' .
|
|
substr($timeStr, 0, 2) . ':' .
|
|
substr($timeStr, 2, 2) . ':' .
|
|
substr($timeStr, 4, 2)
|
|
);
|
|
}
|
|
|
|
// Mendapatkan timestamp dari EXIF data jika tersedia
|
|
$exifData = @exif_read_data($file);
|
|
if ($exifData && isset($exifData['DateTimeOriginal'])) {
|
|
$exifTime = strtotime($exifData['DateTimeOriginal']);
|
|
if ($exifTime) {
|
|
$timestamp = $exifTime;
|
|
}
|
|
}
|
|
|
|
$date = date('Y-m-d', $timestamp);
|
|
$month = date('Y-m', $timestamp);
|
|
|
|
$uniqueDates[$date] = date('d M Y', $timestamp);
|
|
$uniqueMonths[$month] = date('M Y', $timestamp);
|
|
}
|
|
|
|
// Urutkan tanggal dan bulan (terbaru dulu)
|
|
krsort($uniqueDates);
|
|
krsort($uniqueMonths);
|
|
?>
|
|
|
|
<select id="dashFilterDate" class="text-xs px-2 py-1 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
|
|
<option value="">Semua Tanggal</option>
|
|
<?php foreach(array_slice($uniqueDates, 0, 7) as $date => $displayDate): ?>
|
|
<option value="<?php echo $date; ?>"><?php echo $displayDate; ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
|
|
<select id="dashFilterMonth" class="text-xs px-2 py-1 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
|
|
<option value="">Semua Bulan</option>
|
|
<?php foreach($uniqueMonths as $month => $displayMonth): ?>
|
|
<option value="<?php echo $month; ?>"><?php echo $displayMonth; ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
|
|
<div class="flex space-x-1">
|
|
<button id="resetFilterBtn" onclick="resetDashboardFilters()" class="text-xs px-2 py-1 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition duration-300 flex items-center">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
</svg>
|
|
</button>
|
|
|
|
<a href="{{ route('photos.all') }}" class="text-xs px-2 py-1 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition duration-300">
|
|
Lihat Semua
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if (count($files) > 0) { ?>
|
|
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3" id="photosGrid">
|
|
<?php
|
|
foreach (array_slice($files, 0, 12) as $file) {
|
|
$fileName = basename($file);
|
|
$fileTime = date("d-m-Y H:i:s", filemtime($file));
|
|
$filePath = $uploadDir . $fileName;
|
|
$fileSize = round(filesize($file) / 1024, 2) . ' KB';
|
|
|
|
// Mendapatkan timestamp dari nama file (format: esp32cam_YmdHis.jpg)
|
|
$timestampFromFilename = null;
|
|
if (preg_match('/esp32cam_(\d{8})_(\d{6})/', $fileName, $matches)) {
|
|
$dateStr = $matches[1];
|
|
$timeStr = $matches[2];
|
|
$timestampFromFilename = strtotime(
|
|
substr($dateStr, 0, 4) . '-' .
|
|
substr($dateStr, 4, 2) . '-' .
|
|
substr($dateStr, 6, 2) . ' ' .
|
|
substr($timeStr, 0, 2) . ':' .
|
|
substr($timeStr, 2, 2) . ':' .
|
|
substr($timeStr, 4, 2)
|
|
);
|
|
}
|
|
|
|
// Menggunakan timestamp dari nama file jika tersedia
|
|
if ($timestampFromFilename) {
|
|
$fileTime = date("d-m-Y H:i:s", $timestampFromFilename);
|
|
}
|
|
|
|
// Mendapatkan timestamp dari EXIF data jika tersedia
|
|
$exifData = @exif_read_data($file);
|
|
$captureTime = $fileTime; // Default menggunakan waktu file
|
|
|
|
if ($exifData && isset($exifData['DateTimeOriginal'])) {
|
|
// Format EXIF datetime: YYYY:MM:DD HH:MM:SS
|
|
$exifTime = strtotime($exifData['DateTimeOriginal']);
|
|
if ($exifTime) {
|
|
$captureTime = date("d-m-Y H:i:s", $exifTime);
|
|
}
|
|
}
|
|
?>
|
|
<div class="bg-gray-50 rounded-lg overflow-hidden shadow hover:shadow-md transition duration-300 relative group">
|
|
<img src="{{ asset($filePath) }}"
|
|
alt="<?php echo $fileName; ?>"
|
|
class="w-full h-24 sm:h-28 object-cover rounded-t-lg cursor-pointer"
|
|
onclick="openPhotoModal('{{ asset($filePath) }}', '<?php echo $captureTime; ?>', '<?php echo $fileSize; ?>', '<?php echo $fileName; ?>')">
|
|
<div class="p-1.5 bg-white">
|
|
<p class="text-xs text-gray-500 truncate" title="<?php echo $captureTime; ?>"><?php echo $captureTime; ?></p>
|
|
<p class="text-xs text-gray-500"><?php echo $fileSize; ?></p>
|
|
</div>
|
|
<!-- Tombol hapus yang lebih mudah diakses di mobile -->
|
|
<button class="delete-photo absolute top-1 right-1 p-1 bg-red-500 text-white rounded-full opacity-80 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity duration-300 hover:bg-red-600 z-10"
|
|
data-filename="<?php echo $fileName; ?>">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<?php } ?>
|
|
</div>
|
|
|
|
<?php if (count($files) > 12) { ?>
|
|
<div class="mt-4 text-center">
|
|
<a href="{{ route('photos.all') }}" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition duration-300 inline-block text-sm">
|
|
Lihat Semua Foto
|
|
</a>
|
|
</div>
|
|
<?php } ?>
|
|
<?php } else { ?>
|
|
<div class="text-center py-6 bg-gray-50 rounded-lg">
|
|
<p class="text-gray-500 text-sm">Belum ada riwayat foto</p>
|
|
</div>
|
|
<?php } ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal untuk Photo Zoom -->
|
|
<div id="photoModal" class="fixed inset-0 z-50 hidden overflow-auto bg-black bg-opacity-80 flex items-center justify-center p-4">
|
|
<div class="relative bg-white rounded-lg shadow-xl max-w-4xl w-full">
|
|
<div class="flex justify-between items-center p-4 border-b">
|
|
<h3 class="text-lg font-semibold text-gray-900" id="modal-title">Detail Foto</h3>
|
|
<button type="button" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center" onclick="closePhotoModal()">
|
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
|
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div class="p-4 space-y-6">
|
|
<div class="relative">
|
|
<div id="zoom-container" class="overflow-hidden relative">
|
|
<img id="modal-photo" src="" alt="Photo" class="w-full transform transition-transform duration-300">
|
|
</div>
|
|
<div class="flex justify-center mt-4 space-x-2">
|
|
<button id="zoom-in" class="bg-gray-200 p-2 rounded-full hover:bg-gray-300 transition-colors">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd" />
|
|
</svg>
|
|
</button>
|
|
<button id="zoom-out" class="bg-gray-200 p-2 rounded-full hover:bg-gray-300 transition-colors">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fill-rule="evenodd" d="M5 10a1 1 0 011-1h8a1 1 0 110 2H6a1 1 0 01-1-1z" clip-rule="evenodd" />
|
|
</svg>
|
|
</button>
|
|
<button id="zoom-reset" class="bg-gray-200 p-2 rounded-full hover:bg-gray-300 transition-colors">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fill-rule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clip-rule="evenodd" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4">
|
|
<p class="text-sm text-gray-600"><span class="font-medium">Nama File:</span> <span id="modal-filename"></span></p>
|
|
<p class="text-sm text-gray-600"><span class="font-medium">Waktu Pengambilan:</span> <span id="modal-timestamp"></span></p>
|
|
<p class="text-sm text-gray-600"><span class="font-medium">Ukuran File:</span> <span id="modal-filesize"></span></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
|
const logsRef = firebase.database().ref('logs');
|
|
|
|
// Konfigurasi SweetAlert2 untuk menggunakan warna Tailwind
|
|
const mySwal = Swal.mixin({
|
|
customClass: {
|
|
confirmButton: 'bg-blue-600 text-white font-medium px-4 py-2 rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 mr-2',
|
|
cancelButton: 'bg-red-500 text-white font-medium px-4 py-2 rounded-md shadow-sm hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500'
|
|
},
|
|
buttonsStyling: false
|
|
});
|
|
|
|
function updateStatus(sensor, elementId) {
|
|
logsRef.child(sensor).once('value', function(snapshot) {
|
|
const data = snapshot.val();
|
|
const status = data && data.message ? data.message : 'Tidak ada data';
|
|
const el = document.getElementById(elementId);
|
|
el.textContent = status;
|
|
el.className = status.toLowerCase().includes('terhubung') ? 'text-green-600' : 'text-red-600';
|
|
});
|
|
}
|
|
|
|
// Real-time listener (utama)
|
|
logsRef.child('bh1750').on('value', function(snapshot) {
|
|
const data = snapshot.val();
|
|
const status = data && data.message ? data.message : 'Tidak ada data';
|
|
const el = document.getElementById('status-bh1750');
|
|
el.textContent = status;
|
|
el.className = status.toLowerCase().includes('terhubung') ? 'text-green-600' : 'text-red-600';
|
|
});
|
|
|
|
logsRef.child('dht11').on('value', function(snapshot) {
|
|
const data = snapshot.val();
|
|
const status = data && data.message ? data.message : 'Tidak ada data';
|
|
const el = document.getElementById('status-dht11');
|
|
el.textContent = status;
|
|
el.className = status.toLowerCase().includes('terhubung') ? 'text-green-600' : 'text-red-600';
|
|
});
|
|
|
|
logsRef.child('soil_moisture').on('value', function(snapshot) {
|
|
const data = snapshot.val();
|
|
const status = data && data.message ? data.message : 'Tidak ada data';
|
|
const el = document.getElementById('status-soil');
|
|
el.textContent = status;
|
|
el.className = status.toLowerCase().includes('terhubung') ? 'text-green-600' : 'text-red-600';
|
|
});
|
|
|
|
// Polling interval (cadangan, misal setiap 10 detik)
|
|
setInterval(function() {
|
|
updateStatus('bh1750', 'status-bh1750');
|
|
updateStatus('dht11', 'status-dht11');
|
|
updateStatus('soil_moisture', 'status-soil');
|
|
}, 10000); // 10000 ms = 10 detik
|
|
|
|
// Form cek status ESP32-CAM
|
|
document.getElementById('checkStatusForm').addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
|
|
const cameraIp = document.getElementById('cameraIp').value;
|
|
const statusElement = document.getElementById('cameraStatus');
|
|
|
|
if (!cameraIp) {
|
|
mySwal.fire({
|
|
title: 'Error!',
|
|
text: 'Masukkan IP address ESP32-CAM',
|
|
icon: 'error'
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Tampilkan loading
|
|
mySwal.fire({
|
|
title: 'Memeriksa status...',
|
|
html: 'Mohon tunggu sebentar...',
|
|
allowOutsideClick: false,
|
|
didOpen: () => {
|
|
mySwal.showLoading();
|
|
}
|
|
});
|
|
|
|
fetch(`https://${cameraIp}/status`)
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error: ${response.status}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
statusElement.innerHTML = `
|
|
<span class="inline-block h-3 w-3 rounded-full mr-2 bg-green-500"></span>
|
|
<span class="text-green-600">ESP32-CAM terhubung!</span>
|
|
<span class="ml-2 text-sm text-gray-500">
|
|
RSSI: ${data.wifi.rssi} dBm,
|
|
Uptime: ${data.uptime} detik
|
|
</span>
|
|
`;
|
|
|
|
mySwal.fire({
|
|
title: 'Terhubung!',
|
|
html: `ESP32-CAM terhubung dengan baik.<br>
|
|
<small>RSSI: ${data.wifi.rssi} dBm, Uptime: ${data.uptime} detik</small>`,
|
|
icon: 'success'
|
|
});
|
|
})
|
|
.catch(error => {
|
|
statusElement.innerHTML = `
|
|
<span class="inline-block h-3 w-3 rounded-full mr-2 bg-red-500"></span>
|
|
<span class="text-red-600">Tidak dapat terhubung ke ESP32-CAM</span>
|
|
<span class="ml-2 text-sm text-gray-500">${error.message}</span>
|
|
`;
|
|
|
|
mySwal.fire({
|
|
title: 'Gagal Terhubung!',
|
|
text: `Tidak dapat terhubung ke ESP32-CAM: ${error.message}`,
|
|
icon: 'error'
|
|
});
|
|
});
|
|
});
|
|
|
|
// Tombol cleanup
|
|
document.getElementById('cleanupButton').addEventListener('click', function() {
|
|
mySwal.fire({
|
|
title: 'Hapus foto lama?',
|
|
text: "Foto lama akan dihapus secara permanen!",
|
|
icon: 'warning',
|
|
showCancelButton: true,
|
|
confirmButtonText: 'Ya, bersihkan!',
|
|
cancelButtonText: 'Batal'
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
// Tampilkan loading
|
|
mySwal.fire({
|
|
title: 'Membersihkan...',
|
|
html: 'Mohon tunggu sebentar...',
|
|
allowOutsideClick: false,
|
|
didOpen: () => {
|
|
mySwal.showLoading();
|
|
}
|
|
});
|
|
|
|
fetch('/api/esp32cam/cleanup')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
mySwal.fire({
|
|
title: 'Berhasil!',
|
|
text: data.message,
|
|
icon: 'success',
|
|
timer: 1500,
|
|
showConfirmButton: false
|
|
});
|
|
} else {
|
|
mySwal.fire({
|
|
title: 'Gagal!',
|
|
text: data.message,
|
|
icon: 'error'
|
|
});
|
|
}
|
|
})
|
|
.catch(error => {
|
|
mySwal.fire({
|
|
title: 'Error!',
|
|
text: error.message,
|
|
icon: 'error'
|
|
});
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
// Form pengambilan gambar
|
|
document.getElementById('captureImageForm').addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
|
|
const cameraIp = document.getElementById('captureIp').value;
|
|
|
|
if (!cameraIp) {
|
|
mySwal.fire({
|
|
title: 'Error!',
|
|
text: 'Masukkan IP address ESP32-CAM',
|
|
icon: 'error'
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Tampilkan loading
|
|
mySwal.fire({
|
|
title: 'Mengambil gambar...',
|
|
html: 'Mohon tunggu sebentar...',
|
|
allowOutsideClick: false,
|
|
didOpen: () => {
|
|
mySwal.showLoading();
|
|
}
|
|
});
|
|
|
|
// Debug payload
|
|
const payload = {
|
|
camera_ip: cameraIp
|
|
};
|
|
console.log('Sending request with payload:', payload);
|
|
|
|
fetch('/api/esp32cam/fetch-from-camera', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': csrfToken
|
|
},
|
|
body: JSON.stringify(payload)
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
mySwal.fire({
|
|
title: 'Berhasil!',
|
|
text: `Gambar telah disimpan dengan nama ${data.filename}`,
|
|
icon: 'success',
|
|
showConfirmButton: true,
|
|
confirmButtonText: 'Muat Ulang Halaman',
|
|
showCancelButton: true,
|
|
cancelButtonText: 'Tetap di Halaman Ini'
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
window.location.reload();
|
|
}
|
|
});
|
|
} else {
|
|
mySwal.fire({
|
|
title: 'Gagal!',
|
|
text: data.message,
|
|
icon: 'error'
|
|
});
|
|
}
|
|
})
|
|
.catch(error => {
|
|
mySwal.fire({
|
|
title: 'Error!',
|
|
text: error.message,
|
|
icon: 'error'
|
|
});
|
|
});
|
|
});
|
|
|
|
// Tombol hapus foto
|
|
document.querySelectorAll('.delete-photo').forEach(button => {
|
|
button.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation(); // Prevent triggering photo click
|
|
const filename = this.getAttribute('data-filename');
|
|
const photoElement = this.closest('.bg-gray-50');
|
|
|
|
mySwal.fire({
|
|
title: 'Apakah Anda yakin?',
|
|
text: "Foto yang dihapus tidak dapat dikembalikan!",
|
|
icon: 'warning',
|
|
showCancelButton: true,
|
|
confirmButtonText: 'Ya, hapus!',
|
|
cancelButtonText: 'Batal'
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
// Tampilkan loading
|
|
mySwal.fire({
|
|
title: 'Menghapus...',
|
|
html: 'Mohon tunggu sebentar...',
|
|
allowOutsideClick: false,
|
|
didOpen: () => {
|
|
mySwal.showLoading();
|
|
}
|
|
});
|
|
|
|
fetch('/api/esp32cam/delete', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': csrfToken
|
|
},
|
|
body: JSON.stringify({
|
|
filename: filename,
|
|
delete_storage: true // Tambahkan flag untuk menghapus file di storage
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
// Tambahkan animasi sebelum menghapus
|
|
photoElement.style.transition = 'all 0.5s ease';
|
|
photoElement.style.opacity = '0';
|
|
photoElement.style.transform = 'scale(0.8)';
|
|
|
|
setTimeout(() => {
|
|
photoElement.remove();
|
|
mySwal.fire({
|
|
title: 'Terhapus!',
|
|
text: 'Foto berhasil dihapus dari sistem.',
|
|
icon: 'success',
|
|
timer: 1500,
|
|
showConfirmButton: false
|
|
});
|
|
}, 500);
|
|
} else {
|
|
mySwal.fire({
|
|
title: 'Error!',
|
|
text: 'Gagal menghapus foto: ' + data.message,
|
|
icon: 'error'
|
|
});
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
mySwal.fire({
|
|
title: 'Error!',
|
|
text: 'Terjadi kesalahan saat menghapus foto.',
|
|
icon: 'error'
|
|
});
|
|
});
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
// Live Streaming Functionality
|
|
const startStreamBtn = document.getElementById('startStreamBtn');
|
|
const stopStreamBtn = document.getElementById('stopStreamBtn');
|
|
const streamIp = document.getElementById('streamIp');
|
|
const streamStatus = document.getElementById('streamStatus');
|
|
const streamContainer = document.getElementById('streamContainer');
|
|
const streamImage = document.getElementById('streamImage');
|
|
const streamInfo = document.getElementById('streamInfo');
|
|
|
|
// Kamera Settings
|
|
const cameraResolution = document.getElementById('cameraResolution');
|
|
const cameraQuality = document.getElementById('cameraQuality');
|
|
const qualityValue = document.getElementById('qualityValue');
|
|
const applyCameraSettings = document.getElementById('applyCameraSettings');
|
|
|
|
// Update quality value display when slider changes
|
|
cameraQuality.addEventListener('input', function() {
|
|
qualityValue.textContent = this.value;
|
|
});
|
|
|
|
// Apply camera settings
|
|
applyCameraSettings.addEventListener('click', function() {
|
|
const ip = streamIp.value;
|
|
if (!ip) {
|
|
mySwal.fire({
|
|
title: 'Error!',
|
|
text: 'Masukkan IP address ESP32-CAM',
|
|
icon: 'error'
|
|
});
|
|
return;
|
|
}
|
|
|
|
const resolution = cameraResolution.value;
|
|
const quality = cameraQuality.value;
|
|
|
|
// Kirim permintaan ke ESP32-CAM untuk mengubah pengaturan
|
|
fetch(`https://${ip}/camera-settings?resolution=${resolution}&quality=${quality}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
mySwal.fire({
|
|
title: 'Berhasil!',
|
|
text: `Pengaturan kamera diubah: ${data.resolution}, kualitas ${data.quality}`,
|
|
icon: 'success'
|
|
});
|
|
})
|
|
.catch(error => {
|
|
console.error('Error mengubah pengaturan kamera:', error);
|
|
mySwal.fire({
|
|
title: 'Gagal!',
|
|
text: 'Tidak dapat mengubah pengaturan kamera. Periksa koneksi ke ESP32-CAM.',
|
|
icon: 'error'
|
|
});
|
|
});
|
|
});
|
|
|
|
let isStreaming = false;
|
|
let streamStartTime;
|
|
|
|
// Timer untuk memperbarui durasi streaming
|
|
let streamTimer = null;
|
|
|
|
function updateStreamDuration() {
|
|
if (isStreaming && streamStartTime) {
|
|
const duration = Math.floor((Date.now() - streamStartTime) / 1000);
|
|
const minutes = Math.floor(duration / 60);
|
|
const seconds = duration % 60;
|
|
streamInfo.textContent = `Live Stream - Aktif (${minutes}:${seconds.toString().padStart(2, '0')})`;
|
|
}
|
|
}
|
|
|
|
startStreamBtn.addEventListener('click', function() {
|
|
const ip = streamIp.value;
|
|
|
|
if (!ip) {
|
|
mySwal.fire({
|
|
title: 'Error!',
|
|
text: 'Masukkan IP address ESP32-CAM',
|
|
icon: 'error'
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Update UI
|
|
streamStatus.textContent = 'Menghubungkan ke stream...';
|
|
startStreamBtn.disabled = true;
|
|
|
|
// Set source untuk stream MJPEG
|
|
streamImage.src = `https://${ip}/stream`;
|
|
streamContainer.classList.remove('hidden');
|
|
|
|
// Periksa apakah streaming berhasil atau tidak
|
|
streamImage.onload = function() {
|
|
// Stream berhasil dimulai
|
|
isStreaming = true;
|
|
streamStatus.textContent = 'Live stream aktif';
|
|
streamStatus.className = 'text-sm text-green-600';
|
|
stopStreamBtn.disabled = false;
|
|
streamStartTime = Date.now();
|
|
|
|
// Mulai timer untuk durasi streaming
|
|
streamTimer = setInterval(updateStreamDuration, 1000);
|
|
};
|
|
|
|
streamImage.onerror = function() {
|
|
// Stream gagal dimulai
|
|
streamStatus.textContent = 'Gagal terhubung ke stream';
|
|
streamStatus.className = 'text-sm text-red-600';
|
|
startStreamBtn.disabled = false;
|
|
streamContainer.classList.add('hidden');
|
|
|
|
mySwal.fire({
|
|
title: 'Gagal!',
|
|
text: `Tidak dapat terhubung ke stream. Pastikan ESP32-CAM aktif dan dapat diakses di ${ip}`,
|
|
icon: 'error'
|
|
});
|
|
};
|
|
});
|
|
|
|
stopStreamBtn.addEventListener('click', function() {
|
|
const ip = streamIp.value;
|
|
|
|
// Hentikan stream dari sisi server ESP32-CAM
|
|
fetch(`https://${ip}/stopstream`)
|
|
.then(response => {
|
|
console.log('Stream dihentikan dari server:', response.ok);
|
|
})
|
|
.catch(error => {
|
|
console.error('Error menghentikan stream:', error);
|
|
});
|
|
|
|
// Update UI
|
|
isStreaming = false;
|
|
streamImage.src = '';
|
|
streamContainer.classList.add('hidden');
|
|
streamStatus.textContent = 'Live stream dihentikan';
|
|
streamStatus.className = 'text-sm text-gray-600';
|
|
startStreamBtn.disabled = false;
|
|
stopStreamBtn.disabled = true;
|
|
|
|
// Hentikan timer
|
|
if (streamTimer) {
|
|
clearInterval(streamTimer);
|
|
streamTimer = null;
|
|
}
|
|
});
|
|
|
|
// Tambahkan filter untuk riwayat foto di dashboard
|
|
const dashFilterDate = document.getElementById('dashFilterDate');
|
|
const dashFilterMonth = document.getElementById('dashFilterMonth');
|
|
const photosGrid = document.getElementById('photosGrid');
|
|
|
|
// Fungsi filter foto berdasarkan tanggal atau bulan
|
|
function filterDashboardPhotos() {
|
|
const selectedDate = dashFilterDate.value;
|
|
const selectedMonth = dashFilterMonth.value;
|
|
|
|
const photoItems = photosGrid.querySelectorAll('.bg-gray-50');
|
|
let visibleCount = 0;
|
|
|
|
photoItems.forEach(item => {
|
|
const timeElement = item.querySelector('.text-xs.text-gray-500[title]');
|
|
const displayTime = timeElement.getAttribute('title');
|
|
|
|
// Mengkonversi format tanggal dari dd-mm-yyyy menjadi yyyy-mm-dd untuk pembandingan
|
|
let photoDate = '';
|
|
let photoMonth = '';
|
|
|
|
if (displayTime) {
|
|
const parts = displayTime.split(' ');
|
|
if (parts.length === 2) {
|
|
const dateParts = parts[0].split('-');
|
|
if (dateParts.length === 3) {
|
|
// Format dari dd-mm-yyyy ke yyyy-mm-dd
|
|
photoDate = `${dateParts[2]}-${dateParts[1]}-${dateParts[0]}`;
|
|
photoMonth = `${dateParts[2]}-${dateParts[1]}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
let shouldShow = true;
|
|
|
|
if (selectedDate && photoDate !== selectedDate) {
|
|
shouldShow = false;
|
|
}
|
|
|
|
if (selectedMonth && photoMonth !== selectedMonth) {
|
|
shouldShow = false;
|
|
}
|
|
|
|
item.classList.toggle('hidden', !shouldShow);
|
|
|
|
if (shouldShow) {
|
|
visibleCount++;
|
|
}
|
|
});
|
|
|
|
// Jika tidak ada foto yang terlihat, tampilkan pesan
|
|
const noPhotosMessage = document.getElementById('noFilteredPhotos');
|
|
if (visibleCount === 0 && (selectedDate || selectedMonth)) {
|
|
if (!noPhotosMessage) {
|
|
const message = document.createElement('div');
|
|
message.id = 'noFilteredPhotos';
|
|
message.className = 'text-center py-8 bg-gray-50 rounded-lg col-span-full';
|
|
message.innerHTML = `
|
|
<p class="text-gray-500">Tidak ada foto untuk filter yang dipilih</p>
|
|
<button class="mt-2 px-3 py-1.5 text-sm bg-blue-500 text-white rounded-md hover:bg-blue-600" onclick="resetDashboardFilters()">
|
|
Reset Filter
|
|
</button>
|
|
`;
|
|
photosGrid.appendChild(message);
|
|
}
|
|
} else if (noPhotosMessage) {
|
|
noPhotosMessage.remove();
|
|
}
|
|
}
|
|
|
|
// Reset filter
|
|
function resetDashboardFilters() {
|
|
dashFilterDate.value = '';
|
|
dashFilterMonth.value = '';
|
|
filterDashboardPhotos();
|
|
}
|
|
|
|
// Tambahkan event listener
|
|
dashFilterDate.addEventListener('change', filterDashboardPhotos);
|
|
dashFilterMonth.addEventListener('change', filterDashboardPhotos);
|
|
});
|
|
|
|
// Variabel untuk zoom
|
|
let currentScale = 1;
|
|
|
|
// Fungsi untuk modal foto
|
|
function openPhotoModal(src, timestamp, filesize, filename) {
|
|
const modal = document.getElementById('photoModal');
|
|
const modalPhoto = document.getElementById('modal-photo');
|
|
const modalTimestamp = document.getElementById('modal-timestamp');
|
|
const modalFilesize = document.getElementById('modal-filesize');
|
|
const modalFilename = document.getElementById('modal-filename');
|
|
|
|
modalPhoto.src = src;
|
|
modalTimestamp.textContent = timestamp;
|
|
modalFilesize.textContent = filesize;
|
|
modalFilename.textContent = filename;
|
|
|
|
// Reset zoom setiap kali membuka modal
|
|
currentScale = 1;
|
|
modalPhoto.style.transform = 'scale(1)';
|
|
|
|
modal.classList.remove('hidden');
|
|
|
|
// Tambahkan event listeners untuk kontrol zoom
|
|
const zoomIn = document.getElementById('zoom-in');
|
|
const zoomOut = document.getElementById('zoom-out');
|
|
const zoomReset = document.getElementById('zoom-reset');
|
|
|
|
zoomIn.addEventListener('click', function() {
|
|
currentScale += 0.25;
|
|
modalPhoto.style.transform = `scale(${currentScale})`;
|
|
});
|
|
|
|
zoomOut.addEventListener('click', function() {
|
|
if (currentScale > 0.5) {
|
|
currentScale -= 0.25;
|
|
modalPhoto.style.transform = `scale(${currentScale})`;
|
|
}
|
|
});
|
|
|
|
zoomReset.addEventListener('click', function() {
|
|
currentScale = 1;
|
|
modalPhoto.style.transform = 'scale(1)';
|
|
});
|
|
}
|
|
|
|
function closePhotoModal() {
|
|
const modal = document.getElementById('photoModal');
|
|
modal.classList.add('hidden');
|
|
}
|
|
|
|
// Tutup modal dengan escape key
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape') {
|
|
closePhotoModal();
|
|
}
|
|
});
|
|
|
|
// Tutup modal dengan click di luar content
|
|
document.getElementById('photoModal')?.addEventListener('click', function(e) {
|
|
if (e.target === this) {
|
|
closePhotoModal();
|
|
}
|
|
});
|
|
</script>
|
|
@endsection
|