Content & marketing
Map Card code
Copy the implementation or inspect every file that belongs to this component unit.
resources/views/components/ui/map-card.blade.php
@props([
'mapName' => 'chernarus',
'mapSize' => 15360,
'tileUrl' => null,
'center' => [7680, 7680],
'zoom' => 2,
'minZoom' => 1,
'maxZoom' => 7,
'markers' => [],
'paths' => [],
'legend' => [],
'height' => 'lg',
'controls' => true,
'fitMarkers' => false,
'markerSidebar' => true,
'layout' => 'overlay',
'sidebarTitle' => 'Markers',
'allowPlacement' => false,
'placementLabel' => 'Waypoint',
'heatmap' => [],
'heatmapOptions' => [],
'liveTracking' => null,
'attribution' => '',
'assetUrls' => [],
])
@php
$heightClasses = [
'sm' => 'h-72 sm:h-80',
'base' => 'h-80 sm:h-96',
'lg' => 'h-96 sm:h-[30rem]',
'xl' => 'h-[28rem] sm:h-[36rem]',
];
// Split layout: map panel keeps a mobile height, the row stretches on lg.
$splitMapHeights = [
'sm' => 'h-64 sm:h-72',
'base' => 'h-72 sm:h-80',
'lg' => 'h-80 sm:h-96',
'xl' => 'h-96 sm:h-[28rem]',
];
$splitRowHeights = [
'sm' => 'lg:h-80',
'base' => 'lg:h-96',
'lg' => 'lg:h-[30rem]',
'xl' => 'lg:h-[36rem]',
];
$isSplit = $layout === 'split';
$markerItems = collect($markers)->values();
$pathItems = collect($paths)->values();
$showMarkerSidebar = ! $isSplit && $markerSidebar && ($markerItems->isNotEmpty() || $allowPlacement);
$showSplitSidebar = $isSplit && ($markerItems->isNotEmpty() || $allowPlacement || isset($sidebar) || isset($form));
$sidebarId = 'map-marker-sidebar-'.Str::random(8);
$mapLabel = trim((string) ($attributes->get('aria-label') ?: Str::headline((string) $mapName).' map'));
$resolvedTileUrl = $tileUrl ?: "/maps/{$mapName}/tiles/{z}/{x}/{y}.webp";
$resolvedAssetUrls = array_replace([
'css' => 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css',
'js' => 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js',
], $assetUrls);
$config = [
'mapName' => $mapName,
'mapSize' => (int) $mapSize,
'tileUrl' => $resolvedTileUrl,
'center' => $center,
'zoom' => (int) $zoom,
'minZoom' => (int) $minZoom,
'maxZoom' => (int) $maxZoom,
'controls' => (bool) $controls,
'fitMarkers' => (bool) $fitMarkers,
'attribution' => $attribution,
'markers' => $markerItems->all(),
'paths' => $pathItems->all(),
'allowPlacement' => (bool) $allowPlacement,
'placementLabel' => $placementLabel,
'heatmap' => collect($heatmap)->values()->all(),
'heatmapOptions' => $heatmapOptions,
'liveTracking' => $liveTracking,
'assetUrls' => $resolvedAssetUrls,
];
@endphp
<div
data-leaflet-map-card
data-map-config='@json($config)'
{{ $attributes->except('aria-label')->class([
'harbour-map-card overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900',
'flex flex-col lg:flex-row '.($splitRowHeights[$height] ?? $splitRowHeights['lg']) => $isSplit,
]) }}
>
<div
@class([
'relative bg-gray-950',
$heightClasses[$height] ?? $heightClasses['lg'] => ! $isSplit,
($splitMapHeights[$height] ?? $splitMapHeights['lg']).' lg:h-auto lg:w-2/3' => $isSplit,
])
@if ($showMarkerSidebar || $allowPlacement)
x-data="{ sidebarOpen: false, placing: @js($allowPlacement) }"
@keydown.escape.window="sidebarOpen = false"
:class="{ 'harbour-map-card--placing': placing }"
@endif
>
<div
data-map-target
role="application"
aria-label="{{ $mapLabel }}"
@class([
'absolute inset-0 z-0',
'harbour-map-card-target' => $allowPlacement,
])
></div>
@if ($allowPlacement)
<div
data-map-placement-toolbar
class="absolute start-3 top-3 z-20 flex flex-wrap items-center gap-2"
>
<button
type="button"
data-map-placement-toggle
class="inline-flex items-center gap-2 rounded-xl border border-brand-300 bg-brand-600 px-3 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-brand-500 dark:border-brand-700"
x-on:click="placing = ! placing"
:aria-pressed="placing"
>
<x-ui.icon name="map-pin" size="sm" />
<span x-text="placing ? 'Placing waypoints' : 'Place waypoints'">Place waypoints</span>
</button>
@if ($showMarkerSidebar)
<button
type="button"
data-map-marker-toggle
x-show="! sidebarOpen"
x-cloak
class="inline-flex items-center gap-2 rounded-xl border border-gray-200 bg-white/95 px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-gray-900/5 transition hover:border-brand-300 hover:text-brand-700 dark:border-gray-800 dark:bg-gray-900/95 dark:text-gray-100 dark:ring-white/5 dark:hover:border-brand-800 dark:hover:text-brand-300"
x-on:click="sidebarOpen = true"
:aria-expanded="sidebarOpen"
aria-controls="{{ $sidebarId }}"
>
<x-ui.icon name="list" size="sm" />
<span>Markers</span>
<x-ui.badge variant="brand" size="sm" data-map-marker-count>0</x-ui.badge>
</button>
@endif
<button
type="button"
data-map-clear-markers
class="inline-flex items-center gap-2 rounded-xl border border-gray-200 bg-white/95 px-3 py-2 text-sm font-semibold text-gray-700 shadow-sm ring-1 ring-gray-900/5 transition hover:border-red-300 hover:text-red-700 dark:border-gray-800 dark:bg-gray-900/95 dark:text-gray-200 dark:ring-white/5 dark:hover:border-red-800 dark:hover:text-red-300"
>
<x-ui.icon name="trash" size="sm" />
Clear
</button>
</div>
<p
data-map-placement-hint
x-show="placing"
x-cloak
class="pointer-events-none absolute inset-x-0 bottom-16 z-20 mx-auto w-fit rounded-full border border-white/10 bg-gray-950/80 px-4 py-2 text-xs font-semibold text-white shadow-lg backdrop-blur"
>
Click the map to drop {{ Str::lower($placementLabel) }}s
</p>
@endif
@if ($showMarkerSidebar)
<div
x-show="sidebarOpen"
x-cloak
x-transition.opacity
class="absolute inset-0 z-10 bg-gray-950/45 backdrop-blur-[1px]"
x-on:click="sidebarOpen = false"
></div>
<aside
data-map-marker-sidebar
x-show="sidebarOpen"
x-cloak
x-transition:enter="transform transition ease-out duration-200"
x-transition:enter-start="-translate-x-full"
x-transition:enter-end="translate-x-0"
x-transition:leave="transform transition ease-in duration-150"
x-transition:leave-start="translate-x-0"
x-transition:leave-end="-translate-x-full"
class="absolute inset-y-0 start-0 z-20 flex w-72 max-w-[85%] flex-col border-e border-gray-200 bg-white/96 shadow-2xl backdrop-blur dark:border-gray-800 dark:bg-gray-900/96"
aria-label="Map markers"
id="{{ $sidebarId }}"
>
<div class="flex items-center justify-between gap-3 border-b border-gray-200 px-4 py-3 dark:border-gray-800">
<div class="min-w-0">
<p class="text-sm font-semibold text-gray-900 dark:text-white">Markers</p>
<p class="text-xs text-gray-500 dark:text-gray-400" data-map-marker-summary>
@if ($allowPlacement)
0 locations
@else
{{ $markerItems->count() }} locations
@endif
@if ($pathItems->isNotEmpty())
· {{ $pathItems->count() }} {{ Str::plural('path', $pathItems->count()) }}
@endif
</p>
</div>
<x-ui.icon-button
icon="x"
label="Close marker list"
size="sm"
variant="ghost"
x-on:click="sidebarOpen = false"
/>
</div>
<div class="min-h-0 flex-1 overflow-y-auto p-2">
<x-ui.map-marker-list
:markers="$markerItems->all()"
:paths="$pathItems->all()"
:allow-placement="$allowPlacement"
:placement-label="$placementLabel"
/>
</div>
</aside>
@if (! $allowPlacement)
<button
type="button"
data-map-marker-toggle
x-show="! sidebarOpen"
x-cloak
class="absolute start-3 top-3 z-20 inline-flex items-center gap-2 rounded-xl border border-gray-200 bg-white/95 px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-gray-900/5 transition hover:border-brand-300 hover:text-brand-700 dark:border-gray-800 dark:bg-gray-900/95 dark:text-gray-100 dark:ring-white/5 dark:hover:border-brand-800 dark:hover:text-brand-300"
x-on:click="sidebarOpen = true"
:aria-expanded="sidebarOpen"
aria-controls="{{ $sidebarId }}"
>
<x-ui.icon name="list" size="sm" />
<span>Markers</span>
<x-ui.badge variant="brand" size="sm">{{ $markerItems->count() }}</x-ui.badge>
</button>
@endif
@endif
<div data-map-loading role="status" aria-live="polite" class="absolute inset-0 z-10 flex items-center justify-center bg-gray-950 text-sm font-medium text-gray-300">
<span class="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/10 px-3 py-1.5">
<x-ui.spinner size="sm" />
Loading map
</span>
</div>
<div data-map-error role="alert" class="absolute inset-0 z-10 hidden items-center justify-center bg-gray-950 p-6 text-center">
<div class="max-w-sm">
<div class="mx-auto flex h-10 w-10 items-center justify-center rounded-xl bg-red-500/10 text-red-300 ring-1 ring-red-500/20">
<x-ui.icon name="warning" size="xl" />
</div>
<p class="mt-3 text-sm font-semibold text-white">Map unavailable</p>
<p class="mt-1 text-sm text-gray-400">Leaflet or the configured tile layer could not load.</p>
</div>
</div>
</div>
@if ($showSplitSidebar)
<aside
class="flex min-h-0 w-full flex-col border-t border-gray-200 bg-white lg:w-1/3 lg:border-s lg:border-t-0 dark:border-gray-800 dark:bg-gray-900"
aria-label="{{ $sidebarTitle }}"
id="{{ $sidebarId }}"
>
<div class="flex items-center justify-between gap-3 border-b border-gray-200 px-4 py-3 dark:border-gray-800">
<div class="min-w-0">
<p class="text-sm font-semibold text-gray-900 dark:text-white">{{ $sidebarTitle }}</p>
<p class="text-xs text-gray-500 dark:text-gray-400" data-map-marker-summary>
@if ($allowPlacement)
0 locations
@else
{{ $markerItems->count() }} {{ Str::plural('location', $markerItems->count()) }}
@endif
@if ($pathItems->isNotEmpty())
· {{ $pathItems->count() }} {{ Str::plural('path', $pathItems->count()) }}
@endif
</p>
</div>
@unless ($allowPlacement)
<x-ui.badge variant="brand" size="sm" data-map-location-count>{{ $markerItems->count() }}</x-ui.badge>
@endunless
</div>
@isset($form)
<div class="border-b border-gray-200 p-4 dark:border-gray-800">
{{ $form }}
</div>
@endisset
@if ($markerItems->isNotEmpty() || $allowPlacement || isset($sidebar))
<div class="min-h-0 flex-1 overflow-y-auto p-2">
@isset($sidebar)
{{ $sidebar }}
@else
<x-ui.map-marker-list
:markers="$markerItems->all()"
:paths="$pathItems->all()"
:allow-placement="$allowPlacement"
:placement-label="$placementLabel"
/>
@endisset
</div>
@endif
</aside>
@endif
</div>