Update terbaru
This commit is contained in:
parent
ddad0a4f2b
commit
c3beaf86a2
|
|
@ -1,5 +1,5 @@
|
|||
class ApiConfig {
|
||||
static const String host = '192.168.1.10';
|
||||
static const String host = '192.168.1.36';
|
||||
static const String port = '8000';
|
||||
|
||||
static String get baseUrl {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Kategori;
|
||||
use App\Models\User;
|
||||
use App\Models\Wisata;
|
||||
use Illuminate\View\View;
|
||||
|
|
@ -11,11 +12,30 @@ class DashboardController extends Controller
|
|||
{
|
||||
public function __invoke(): View
|
||||
{
|
||||
$wisatas = Wisata::query()
|
||||
->with('kategori')
|
||||
->whereNotNull('latitude')
|
||||
->whereNotNull('longitude')
|
||||
->orderBy('title')
|
||||
->get();
|
||||
|
||||
return view('admin.dashboard.index', [
|
||||
'totalWisata' => Wisata::query()->count(),
|
||||
'totalWisataAr' => Wisata::query()->whereNotNull('model_path')->count(),
|
||||
'totalWisataVideo' => Wisata::query()->where('is_video', true)->count(),
|
||||
'totalUser' => User::query()->where('role', 'user')->count(),
|
||||
'totalKategori' => Kategori::query()->count(),
|
||||
'mapWisatas' => $wisatas->map(fn (Wisata $wisata): array => [
|
||||
'id' => $wisata->id,
|
||||
'title' => $wisata->title,
|
||||
'location' => $wisata->location,
|
||||
'latitude' => (float) $wisata->latitude,
|
||||
'longitude' => (float) $wisata->longitude,
|
||||
'imageUrl' => $wisata->image_url,
|
||||
'rating' => number_format((float) $wisata->rating, 1),
|
||||
'category' => $wisata->kategori?->nama ?? 'Tanpa Kategori',
|
||||
'mediaType' => $wisata->model_path ? 'ar' : ($wisata->video_path ? 'video' : 'ar'),
|
||||
'mediaLabel' => $wisata->model_path ? 'Augmented Reality' : ($wisata->video_path ? 'Video Wisata' : 'Augmented Reality'),
|
||||
'editUrl' => route('admin.wisata.edit', $wisata),
|
||||
])->values(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,205 @@
|
|||
.admin-map-card .card-body {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.map-actions {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 1fr) minmax(130px, .42fr) auto auto;
|
||||
gap: .75rem;
|
||||
width: min(900px, 100%);
|
||||
}
|
||||
|
||||
.map-shell {
|
||||
position: relative;
|
||||
height: 600px;
|
||||
min-height: 600px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
background: #e9f2f6;
|
||||
box-shadow: 0 18px 42px rgba(15, 76, 129, .1);
|
||||
}
|
||||
|
||||
.tourism-map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.leaflet-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tourism-map::before {
|
||||
content: attr(data-empty-message);
|
||||
display: none;
|
||||
place-items: center;
|
||||
height: 100%;
|
||||
padding: 2rem;
|
||||
color: #64748b;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tourism-map.is-empty::before {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.map-legend {
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
bottom: 14px;
|
||||
z-index: 500;
|
||||
width: min(300px, calc(100% - 28px));
|
||||
padding: .85rem 1rem;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, .96);
|
||||
border: 1px solid rgba(15, 76, 129, .14);
|
||||
box-shadow: 0 14px 34px rgba(15, 76, 129, .16);
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
box-shadow: 0 0 0 4px rgba(15, 76, 129, .08);
|
||||
}
|
||||
|
||||
.legend-dot-ar {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.legend-dot-video {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.map-stat {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
background: #fff;
|
||||
box-shadow: 0 10px 26px rgba(15, 76, 129, .07);
|
||||
}
|
||||
|
||||
.map-stat small {
|
||||
display: block;
|
||||
color: #64748b;
|
||||
font-weight: 700;
|
||||
margin-bottom: .35rem;
|
||||
}
|
||||
|
||||
.map-stat strong {
|
||||
display: block;
|
||||
color: var(--ink);
|
||||
font-size: 1.8rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tourism-marker {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.tourism-marker-pin {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50% 50% 50% 0;
|
||||
background: var(--marker-color);
|
||||
border: 3px solid #fff;
|
||||
transform: rotate(-45deg);
|
||||
box-shadow: 0 10px 24px rgba(15, 23, 42, .28);
|
||||
}
|
||||
|
||||
.tourism-marker-pin::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 7px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.leaflet-popup-content-wrapper {
|
||||
border-radius: 14px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 18px 42px rgba(15, 23, 42, .2);
|
||||
}
|
||||
|
||||
.leaflet-popup-content {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.map-popup-card {
|
||||
width: 280px;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.map-popup-card .card-img-top {
|
||||
width: 100%;
|
||||
height: 140px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.map-popup-card .card-body {
|
||||
padding: .9rem;
|
||||
}
|
||||
|
||||
.map-popup-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 800;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.map-popup-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: .45rem;
|
||||
color: #475569;
|
||||
font-size: .86rem;
|
||||
margin-top: .4rem;
|
||||
}
|
||||
|
||||
.map-popup-row i {
|
||||
color: var(--ocean);
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.fullscreen-control {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 0;
|
||||
border-bottom: 1px solid #ccc;
|
||||
background: #fff;
|
||||
color: #111827;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.fullscreen-control:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.map-actions {
|
||||
grid-template-columns: 1fr;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.map-shell {
|
||||
height: 600px;
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.map-legend {
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,328 @@
|
|||
(function () {
|
||||
const lumajangCenter = [-8.1269, 113.2248];
|
||||
const lumajangBounds = L.latLngBounds(
|
||||
L.latLng(-8.55, 112.75),
|
||||
L.latLng(-7.70, 113.55),
|
||||
);
|
||||
const defaultZoom = 10;
|
||||
const singleMarkerZoom = 14;
|
||||
const minZoom = 10;
|
||||
const maxZoom = 18;
|
||||
const markerColors = {
|
||||
ar: '#dc2626',
|
||||
video: '#2563eb',
|
||||
};
|
||||
|
||||
let map;
|
||||
let markerLayer;
|
||||
let markers = [];
|
||||
let activeMedia = 'all';
|
||||
let searchQuery = '';
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function createMarkerIcon(mediaType) {
|
||||
const color = markerColors[mediaType] || markerColors.ar;
|
||||
|
||||
return L.divIcon({
|
||||
className: 'tourism-marker',
|
||||
html: `<span class="tourism-marker-pin" style="--marker-color:${color}"></span>`,
|
||||
iconSize: [30, 42],
|
||||
iconAnchor: [15, 42],
|
||||
popupAnchor: [0, -38],
|
||||
});
|
||||
}
|
||||
|
||||
function buildPopupContent(item) {
|
||||
const isVideo = item.mediaType === 'video';
|
||||
const badgeClass = isVideo ? 'text-bg-primary' : 'text-bg-danger';
|
||||
|
||||
return `
|
||||
<div class="card map-popup-card border-0 shadow-sm">
|
||||
<img src="${escapeHtml(item.imageUrl)}" class="card-img-top" alt="${escapeHtml(item.title)}">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-start justify-content-between gap-2 mb-2">
|
||||
<h3 class="map-popup-title mb-0">${escapeHtml(item.title)}</h3>
|
||||
<span class="badge ${badgeClass}">${escapeHtml(item.mediaLabel)}</span>
|
||||
</div>
|
||||
<div class="map-popup-row"><i class="bi bi-geo-alt"></i><span>${escapeHtml(item.location)}</span></div>
|
||||
<div class="map-popup-row"><i class="bi bi-tag"></i><span>${escapeHtml(item.category)}</span></div>
|
||||
<a class="btn btn-sm btn-ocean w-100 mt-3" href="${escapeHtml(item.editUrl)}">Lihat Detail</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function markerMatches(marker) {
|
||||
const item = marker.tourismData;
|
||||
const matchesMedia = activeMedia === 'all' || item.mediaType === activeMedia;
|
||||
const haystack = `${item.title} ${item.location} ${item.category}`.toLowerCase();
|
||||
const matchesSearch = !searchQuery || haystack.includes(searchQuery);
|
||||
|
||||
return matchesMedia && matchesSearch;
|
||||
}
|
||||
|
||||
function getVisibleMarkers() {
|
||||
return markers.filter((marker) => markerLayer.hasLayer(marker));
|
||||
}
|
||||
|
||||
function setMarkerVisibility() {
|
||||
markers.forEach((marker) => {
|
||||
const shouldShow = markerMatches(marker);
|
||||
const isShown = markerLayer.hasLayer(marker);
|
||||
|
||||
if (shouldShow && !isShown) {
|
||||
markerLayer.addLayer(marker);
|
||||
}
|
||||
|
||||
if (!shouldShow && isShown) {
|
||||
markerLayer.removeLayer(marker);
|
||||
}
|
||||
});
|
||||
|
||||
const visibleMarkers = getVisibleMarkers();
|
||||
updateStats();
|
||||
|
||||
if (!visibleMarkers.length) {
|
||||
map.closePopup();
|
||||
return;
|
||||
}
|
||||
|
||||
const exactSearchMatch = searchQuery
|
||||
? visibleMarkers.find((marker) => marker.tourismData.title.toLowerCase().includes(searchQuery))
|
||||
: null;
|
||||
|
||||
if (exactSearchMatch) {
|
||||
focusMarker(exactSearchMatch);
|
||||
return;
|
||||
}
|
||||
|
||||
fitMarkers(visibleMarkers);
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
const totalElement = document.getElementById('mapTotalMarkers');
|
||||
const arElement = document.getElementById('mapArMarkers');
|
||||
const videoElement = document.getElementById('mapVideoMarkers');
|
||||
|
||||
const arCount = markers.filter((marker) => marker.tourismData.mediaType === 'ar').length;
|
||||
const videoCount = markers.filter((marker) => marker.tourismData.mediaType === 'video').length;
|
||||
|
||||
if (totalElement) totalElement.textContent = markers.length;
|
||||
if (arElement) arElement.textContent = arCount;
|
||||
if (videoElement) videoElement.textContent = videoCount;
|
||||
}
|
||||
|
||||
function fitMarkers(targetMarkers) {
|
||||
if (!targetMarkers.length) {
|
||||
map.setView(lumajangCenter, defaultZoom);
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetMarkers.length === 1) {
|
||||
map.setView(targetMarkers[0].getLatLng(), singleMarkerZoom);
|
||||
return;
|
||||
}
|
||||
|
||||
const group = L.featureGroup(targetMarkers);
|
||||
|
||||
map.fitBounds(group.getBounds().pad(0.2), {
|
||||
maxZoom,
|
||||
});
|
||||
}
|
||||
|
||||
function refreshMapSize(callback, delay = 140) {
|
||||
window.setTimeout(() => {
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
|
||||
map.invalidateSize(true);
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
|
||||
function scheduleMapInvalidation() {
|
||||
setTimeout(() => {
|
||||
map.invalidateSize(true);
|
||||
}, 500);
|
||||
|
||||
setTimeout(() => {
|
||||
map.invalidateSize(true);
|
||||
}, 1000);
|
||||
|
||||
setTimeout(() => {
|
||||
map.invalidateSize(true);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function focusMarker(marker) {
|
||||
map.setView(marker.getLatLng(), singleMarkerZoom);
|
||||
marker.openPopup();
|
||||
}
|
||||
|
||||
function resetView() {
|
||||
searchQuery = '';
|
||||
activeMedia = 'all';
|
||||
|
||||
const searchInput = document.getElementById('tourismMapSearch');
|
||||
const mediaSelect = document.getElementById('tourismMapMedia');
|
||||
|
||||
if (searchInput) searchInput.value = '';
|
||||
if (mediaSelect) mediaSelect.value = 'all';
|
||||
|
||||
markers.forEach((marker) => {
|
||||
if (!markerLayer.hasLayer(marker)) {
|
||||
markerLayer.addLayer(marker);
|
||||
}
|
||||
});
|
||||
|
||||
map.closePopup();
|
||||
updateStats();
|
||||
refreshMapSize(() => map.setView(lumajangCenter, defaultZoom), 120);
|
||||
}
|
||||
|
||||
function addFullscreenControl() {
|
||||
const FullscreenControl = L.Control.extend({
|
||||
options: { position: 'topleft' },
|
||||
onAdd() {
|
||||
const button = L.DomUtil.create('button', 'leaflet-control fullscreen-control');
|
||||
button.type = 'button';
|
||||
button.title = 'Fullscreen';
|
||||
button.setAttribute('aria-label', 'Fullscreen');
|
||||
button.innerHTML = '<i class="bi bi-fullscreen"></i>';
|
||||
|
||||
L.DomEvent.disableClickPropagation(button);
|
||||
L.DomEvent.on(button, 'click', () => {
|
||||
const container = map.getContainer();
|
||||
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen();
|
||||
} else if (container.requestFullscreen) {
|
||||
container.requestFullscreen();
|
||||
}
|
||||
});
|
||||
|
||||
return button;
|
||||
},
|
||||
});
|
||||
|
||||
map.addControl(new FullscreenControl());
|
||||
|
||||
document.addEventListener('fullscreenchange', () => {
|
||||
refreshMapSize(() => fitMarkers(getVisibleMarkers()), 180);
|
||||
});
|
||||
}
|
||||
|
||||
function bindControls() {
|
||||
document.getElementById('tourismMapSearch')?.addEventListener('input', (event) => {
|
||||
searchQuery = event.target.value.trim().toLowerCase();
|
||||
setMarkerVisibility();
|
||||
});
|
||||
|
||||
document.getElementById('tourismMapMedia')?.addEventListener('change', (event) => {
|
||||
activeMedia = event.target.value;
|
||||
setMarkerVisibility();
|
||||
});
|
||||
|
||||
document.getElementById('tourismMapFit')?.addEventListener('click', () => {
|
||||
fitMarkers(getVisibleMarkers());
|
||||
});
|
||||
|
||||
document.getElementById('tourismMapReset')?.addEventListener('click', resetView);
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
window.clearTimeout(window.tourismMapResizeTimer);
|
||||
window.tourismMapResizeTimer = window.setTimeout(() => {
|
||||
refreshMapSize(() => fitMarkers(getVisibleMarkers()), 40);
|
||||
}, 180);
|
||||
});
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
refreshMapSize(() => fitMarkers(getVisibleMarkers()), 220);
|
||||
});
|
||||
}
|
||||
|
||||
function initTourismMap() {
|
||||
const mapElement = document.getElementById('tourismMap');
|
||||
const data = Array.isArray(window.tourismMapData) ? window.tourismMapData : [];
|
||||
|
||||
if (!mapElement || typeof L === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mapElement._leaflet_id) return;
|
||||
|
||||
map = L.map(mapElement, {
|
||||
center: lumajangCenter,
|
||||
zoom: defaultZoom,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
maxBounds: lumajangBounds,
|
||||
maxBoundsViscosity: 1,
|
||||
zoomControl: true,
|
||||
scrollWheelZoom: true,
|
||||
});
|
||||
|
||||
map.setMaxBounds(lumajangBounds);
|
||||
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
|
||||
subdomains: 'abcd',
|
||||
maxZoom: 20,
|
||||
noWrap: true,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>',
|
||||
}).addTo(map);
|
||||
|
||||
scheduleMapInvalidation();
|
||||
|
||||
markerLayer = L.layerGroup().addTo(map);
|
||||
|
||||
markers = data.map((item) => {
|
||||
const marker = L.marker([Number(item.latitude), Number(item.longitude)], {
|
||||
icon: createMarkerIcon(item.mediaType),
|
||||
title: item.title,
|
||||
});
|
||||
|
||||
marker.tourismData = item;
|
||||
marker.bindPopup(buildPopupContent(item), {
|
||||
className: 'tourism-popup',
|
||||
maxWidth: 300,
|
||||
minWidth: 260,
|
||||
});
|
||||
markerLayer.addLayer(marker);
|
||||
|
||||
return marker;
|
||||
});
|
||||
|
||||
addFullscreenControl();
|
||||
bindControls();
|
||||
updateStats();
|
||||
|
||||
if (markers.length) {
|
||||
refreshMapSize(() => map.setView(lumajangCenter, defaultZoom), 180);
|
||||
} else {
|
||||
mapElement.classList.add('is-empty');
|
||||
refreshMapSize(() => map.setView(lumajangCenter, defaultZoom), 180);
|
||||
}
|
||||
}
|
||||
|
||||
function bootTourismMap() {
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initTourismMap, { once: true });
|
||||
} else {
|
||||
initTourismMap();
|
||||
}
|
||||
}
|
||||
|
||||
bootTourismMap();
|
||||
})();
|
||||
|
|
@ -1,112 +1,119 @@
|
|||
@extends('admin.layouts.app')
|
||||
|
||||
@section('title', 'Dashboard')
|
||||
@section('page_title', 'Dashboard')
|
||||
@section('page_subtitle', 'Pusat manajemen data Explore Lumajang')
|
||||
@section('page_title', 'Dashboard Admin')
|
||||
@section('page_subtitle', 'Ringkasan data dan peta persebaran wisata Kabupaten Lumajang')
|
||||
|
||||
@section('content')
|
||||
<div class="row g-3 g-xl-4">
|
||||
<div class="col-12 col-sm-6 col-xl-3">
|
||||
<div class="card soft-card dashboard-card h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start gap-3">
|
||||
<div>
|
||||
<div class="text-muted small fw-semibold text-uppercase">Total Wisata</div>
|
||||
<div class="display-6 fw-bold mb-0">{{ $totalWisata }}</div>
|
||||
</div>
|
||||
<div class="dashboard-icon text-bg-ocean">
|
||||
<i class="bi bi-map"></i>
|
||||
</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card soft-card h-100">
|
||||
<div class="card-body d-flex align-items-center justify-content-between gap-3">
|
||||
<div>
|
||||
<small class="text-muted fw-semibold">Total Wisata</small>
|
||||
<div class="display-6 fw-bold mb-0">{{ number_format($totalWisata) }}</div>
|
||||
</div>
|
||||
<div class="text-muted small mt-3">Semua destinasi yang tersedia di aplikasi.</div>
|
||||
<div class="stat-icon"><i class="bi bi-map"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-sm-6 col-xl-3">
|
||||
<div class="card soft-card dashboard-card h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start gap-3">
|
||||
<div>
|
||||
<div class="text-muted small fw-semibold text-uppercase">Total Wisata AR</div>
|
||||
<div class="display-6 fw-bold mb-0">{{ $totalWisataAr }}</div>
|
||||
</div>
|
||||
<div class="dashboard-icon text-bg-info">
|
||||
<i class="bi bi-badge-3d"></i>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card soft-card h-100">
|
||||
<div class="card-body d-flex align-items-center justify-content-between gap-3">
|
||||
<div>
|
||||
<small class="text-muted fw-semibold">Total User</small>
|
||||
<div class="display-6 fw-bold mb-0">{{ number_format($totalUser) }}</div>
|
||||
</div>
|
||||
<div class="text-muted small mt-3">Destinasi dengan model 3D untuk fitur AR.</div>
|
||||
<div class="stat-icon"><i class="bi bi-people"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-sm-6 col-xl-3">
|
||||
<div class="card soft-card dashboard-card h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start gap-3">
|
||||
<div>
|
||||
<div class="text-muted small fw-semibold text-uppercase">Total Wisata Video</div>
|
||||
<div class="display-6 fw-bold mb-0">{{ $totalWisataVideo }}</div>
|
||||
</div>
|
||||
<div class="dashboard-icon text-bg-dark">
|
||||
<i class="bi bi-play-btn"></i>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card soft-card h-100">
|
||||
<div class="card-body d-flex align-items-center justify-content-between gap-3">
|
||||
<div>
|
||||
<small class="text-muted fw-semibold">Total Kategori</small>
|
||||
<div class="display-6 fw-bold mb-0">{{ number_format($totalKategori) }}</div>
|
||||
</div>
|
||||
<div class="text-muted small mt-3">Destinasi yang menampilkan media video.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-sm-6 col-xl-3">
|
||||
<div class="card soft-card dashboard-card h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start gap-3">
|
||||
<div>
|
||||
<div class="text-muted small fw-semibold text-uppercase">Total User</div>
|
||||
<div class="display-6 fw-bold mb-0">{{ $totalUser }}</div>
|
||||
</div>
|
||||
<div class="dashboard-icon text-bg-primary">
|
||||
<i class="bi bi-people"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-muted small mt-3">Pengguna aplikasi Flutter yang terdaftar.</div>
|
||||
<div class="stat-icon"><i class="bi bi-tags"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card soft-card mt-3 mt-xl-4">
|
||||
<div class="card-body d-flex flex-column flex-lg-row justify-content-between align-items-start align-items-lg-center gap-3">
|
||||
<div>
|
||||
<h2 class="h6 mb-1">Kelola Data Wisata</h2>
|
||||
<p class="text-muted mb-0">Tambah, ubah, dan lengkapi media destinasi untuk kebutuhan aplikasi Flutter.</p>
|
||||
<div class="card soft-card admin-map-card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-column flex-xl-row justify-content-between gap-3 mb-3">
|
||||
<div>
|
||||
<h2 class="h5 fw-bold mb-1">Peta Persebaran Wisata Kabupaten Lumajang</h2>
|
||||
<p class="text-muted mb-0">Visualisasi lokasi seluruh destinasi wisata berdasarkan data yang tersimpan pada sistem.</p>
|
||||
</div>
|
||||
<div class="map-actions">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-white"><i class="bi bi-search"></i></span>
|
||||
<input class="form-control" id="tourismMapSearch" type="search" placeholder="Cari wisata">
|
||||
</div>
|
||||
<select class="form-select" id="tourismMapMedia" aria-label="Filter marker">
|
||||
<option value="all">Semua</option>
|
||||
<option value="ar">AR</option>
|
||||
<option value="video">Video</option>
|
||||
</select>
|
||||
<button class="btn btn-outline-ocean" id="tourismMapFit" type="button">
|
||||
<i class="bi bi-arrows-fullscreen me-1"></i>Fit Marker
|
||||
</button>
|
||||
<button class="btn btn-outline-ocean" id="tourismMapReset" type="button">
|
||||
<i class="bi bi-arrow-clockwise me-1"></i>Reset View
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="map-shell">
|
||||
<div id="tourismMap" class="tourism-map" data-empty-message="Belum ada wisata dengan koordinat latitude dan longitude."></div>
|
||||
<div class="map-legend">
|
||||
<div class="fw-semibold mb-2">Legenda</div>
|
||||
<div class="d-flex align-items-center gap-2 mb-1"><span class="legend-dot legend-dot-ar"></span><span>Wisata Augmented Reality</span></div>
|
||||
<div class="d-flex align-items-center gap-2"><span class="legend-dot legend-dot-video"></span><span>Wisata Video</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mt-1">
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="map-stat">
|
||||
<small>Jumlah Marker</small>
|
||||
<strong id="mapTotalMarkers">0</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="map-stat">
|
||||
<small>Jumlah Wisata AR</small>
|
||||
<strong id="mapArMarkers">0</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="map-stat">
|
||||
<small>Jumlah Wisata Video</small>
|
||||
<strong id="mapVideoMarkers">0</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn btn-nature" href="{{ route('admin.wisata.index') }}">
|
||||
<i class="bi bi-arrow-right-circle me-1"></i> Buka Data Wisata
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.dashboard-card {
|
||||
transition: transform .18s ease, box-shadow .18s ease;
|
||||
}
|
||||
|
||||
.dashboard-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 16px 34px rgba(15, 76, 129, .11);
|
||||
}
|
||||
|
||||
.dashboard-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 12px;
|
||||
font-size: 1.35rem;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
||||
<link rel="stylesheet" href="{{ asset('css/admin-dashboard-map.css') }}">
|
||||
@endpush
|
||||
|
||||
|
||||
@push('scripts')
|
||||
|
||||
<script>
|
||||
window.tourismMapData = @json($mapWisatas);
|
||||
</script>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
|
||||
<script src="{{ asset('js/admin-dashboard-map.js') }}"></script>
|
||||
|
||||
@endpush
|
||||
|
|
|
|||
Loading…
Reference in New Issue