Update terbaru
This commit is contained in:
parent
ddad0a4f2b
commit
c3beaf86a2
|
|
@ -1,5 +1,5 @@
|
||||||
class ApiConfig {
|
class ApiConfig {
|
||||||
static const String host = '192.168.1.10';
|
static const String host = '192.168.1.36';
|
||||||
static const String port = '8000';
|
static const String port = '8000';
|
||||||
|
|
||||||
static String get baseUrl {
|
static String get baseUrl {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Kategori;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\Wisata;
|
use App\Models\Wisata;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
@ -11,11 +12,30 @@ class DashboardController extends Controller
|
||||||
{
|
{
|
||||||
public function __invoke(): View
|
public function __invoke(): View
|
||||||
{
|
{
|
||||||
|
$wisatas = Wisata::query()
|
||||||
|
->with('kategori')
|
||||||
|
->whereNotNull('latitude')
|
||||||
|
->whereNotNull('longitude')
|
||||||
|
->orderBy('title')
|
||||||
|
->get();
|
||||||
|
|
||||||
return view('admin.dashboard.index', [
|
return view('admin.dashboard.index', [
|
||||||
'totalWisata' => Wisata::query()->count(),
|
'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(),
|
'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')
|
@extends('admin.layouts.app')
|
||||||
|
|
||||||
@section('title', 'Dashboard')
|
@section('title', 'Dashboard')
|
||||||
@section('page_title', 'Dashboard')
|
@section('page_title', 'Dashboard Admin')
|
||||||
@section('page_subtitle', 'Pusat manajemen data Explore Lumajang')
|
@section('page_subtitle', 'Ringkasan data dan peta persebaran wisata Kabupaten Lumajang')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="row g-3 g-xl-4">
|
<div class="row g-3 mb-4">
|
||||||
<div class="col-12 col-sm-6 col-xl-3">
|
<div class="col-12 col-md-4">
|
||||||
<div class="card soft-card dashboard-card h-100">
|
<div class="card soft-card h-100">
|
||||||
<div class="card-body">
|
<div class="card-body d-flex align-items-center justify-content-between gap-3">
|
||||||
<div class="d-flex justify-content-between align-items-start gap-3">
|
<div>
|
||||||
<div>
|
<small class="text-muted fw-semibold">Total Wisata</small>
|
||||||
<div class="text-muted small fw-semibold text-uppercase">Total Wisata</div>
|
<div class="display-6 fw-bold mb-0">{{ number_format($totalWisata) }}</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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
<div class="col-12 col-sm-6 col-xl-3">
|
<div class="card soft-card h-100">
|
||||||
<div class="card soft-card dashboard-card h-100">
|
<div class="card-body d-flex align-items-center justify-content-between gap-3">
|
||||||
<div class="card-body">
|
<div>
|
||||||
<div class="d-flex justify-content-between align-items-start gap-3">
|
<small class="text-muted fw-semibold">Total User</small>
|
||||||
<div>
|
<div class="display-6 fw-bold mb-0">{{ number_format($totalUser) }}</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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
<div class="col-12 col-sm-6 col-xl-3">
|
<div class="card soft-card h-100">
|
||||||
<div class="card soft-card dashboard-card h-100">
|
<div class="card-body d-flex align-items-center justify-content-between gap-3">
|
||||||
<div class="card-body">
|
<div>
|
||||||
<div class="d-flex justify-content-between align-items-start gap-3">
|
<small class="text-muted fw-semibold">Total Kategori</small>
|
||||||
<div>
|
<div class="display-6 fw-bold mb-0">{{ number_format($totalKategori) }}</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>
|
</div>
|
||||||
<div class="text-muted small mt-3">Destinasi yang menampilkan media video.</div>
|
<div class="stat-icon"><i class="bi bi-tags"></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 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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card soft-card mt-3 mt-xl-4">
|
<div class="card soft-card admin-map-card">
|
||||||
<div class="card-body d-flex flex-column flex-lg-row justify-content-between align-items-start align-items-lg-center gap-3">
|
<div class="card-body">
|
||||||
<div>
|
<div class="d-flex flex-column flex-xl-row justify-content-between gap-3 mb-3">
|
||||||
<h2 class="h6 mb-1">Kelola Data Wisata</h2>
|
<div>
|
||||||
<p class="text-muted mb-0">Tambah, ubah, dan lengkapi media destinasi untuk kebutuhan aplikasi Flutter.</p>
|
<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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@push('styles')
|
@push('styles')
|
||||||
<style>
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
||||||
.dashboard-card {
|
<link rel="stylesheet" href="{{ asset('css/admin-dashboard-map.css') }}">
|
||||||
transition: transform .18s ease, box-shadow .18s ease;
|
@endpush
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-card:hover {
|
@push('scripts')
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 16px 34px rgba(15, 76, 129, .11);
|
<script>
|
||||||
}
|
window.tourismMapData = @json($mapWisatas);
|
||||||
|
</script>
|
||||||
.dashboard-icon {
|
|
||||||
width: 48px;
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
height: 48px;
|
|
||||||
display: grid;
|
<script src="{{ asset('js/admin-dashboard-map.js') }}"></script>
|
||||||
place-items: center;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 1.35rem;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@endpush
|
@endpush
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue