- Updated Makefile to include `--build` flag for `docker-compose.dev.yml` and `--no-cache` for `docker-compose.prod.yml` to ensure fresh builds. - Added new CSS styles for Leaflet tooltips and popups to utilize theme colors, enhancing visual consistency. - Enhanced MapView component with new props for markers and current zoom level, improving marker management and zoom functionality. - Introduced new icons for copy and info actions to improve user interface clarity. - Updated MapBookmarks and MapControls components to support new features and improve user experience with bookmarks and zoom controls. - Refactored MapSearch to display coordinates and improve marker search functionality.
251 lines
9.0 KiB
Vue
251 lines
9.0 KiB
Vue
<template>
|
|
<div class="flex flex-col gap-4">
|
|
<!-- Search -->
|
|
<MapSearch
|
|
v-if="currentMapId != null && currentCoords != null"
|
|
:maps="maps"
|
|
:quest-givers="questGivers"
|
|
:markers="markers"
|
|
:overlay-map-id="props.overlayMapId"
|
|
:current-map-id="currentMapId"
|
|
:current-coords="currentCoords"
|
|
:touch-friendly="touchFriendly"
|
|
@jump-to-marker="$emit('jumpToMarker', $event)"
|
|
/>
|
|
<!-- Zoom -->
|
|
<section class="flex flex-col gap-2">
|
|
<h3 class="text-xs font-semibold uppercase tracking-wider text-base-content/70 flex items-center gap-1.5">
|
|
<icons-icon-zoom-in class="size-3.5 opacity-80" aria-hidden="true" />
|
|
Zoom
|
|
</h3>
|
|
<div class="flex items-center gap-2">
|
|
<button
|
|
type="button"
|
|
class="btn btn-outline btn-sm btn-square transition-all duration-200 hover:scale-105 touch-manipulation"
|
|
:class="touchFriendly ? 'min-h-10 min-w-10' : ''"
|
|
title="Zoom in"
|
|
aria-label="Zoom in"
|
|
@click="$emit('zoomIn')"
|
|
>
|
|
<icons-icon-zoom-in />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-outline btn-sm btn-square transition-all duration-200 hover:scale-105 touch-manipulation"
|
|
:class="touchFriendly ? 'min-h-10 min-w-10' : ''"
|
|
title="Zoom out"
|
|
aria-label="Zoom out"
|
|
@click="$emit('zoomOut')"
|
|
>
|
|
<icons-icon-zoom-out />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-outline btn-sm btn-square transition-all duration-200 hover:scale-105 touch-manipulation"
|
|
:class="touchFriendly ? 'min-h-10 min-w-10' : ''"
|
|
title="Reset view — center map and minimum zoom"
|
|
aria-label="Reset view"
|
|
@click="$emit('resetView')"
|
|
>
|
|
<icons-icon-home />
|
|
</button>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<input
|
|
type="range"
|
|
:min="zoomMin"
|
|
:max="zoomMax"
|
|
:value="currentZoom"
|
|
class="range range-primary range-sm flex-1"
|
|
:class="touchFriendly ? 'range-lg' : ''"
|
|
aria-label="Zoom level"
|
|
@input="onZoomSliderInput($event)"
|
|
>
|
|
<span class="text-xs font-mono w-6 text-right" aria-hidden="true">{{ currentZoom }}</span>
|
|
</div>
|
|
</section>
|
|
<!-- Display -->
|
|
<section class="flex flex-col gap-2">
|
|
<h3 class="text-xs font-semibold uppercase tracking-wider text-base-content/70 flex items-center gap-1.5">
|
|
<icons-icon-eye class="size-3.5 opacity-80" aria-hidden="true" />
|
|
Display
|
|
</h3>
|
|
<label class="label cursor-pointer justify-start gap-2 py-0 hover:bg-base-200/50 rounded-lg px-2 -mx-2 touch-manipulation" :class="touchFriendly ? 'min-h-11' : ''">
|
|
<input v-model="hideMarkers" type="checkbox" class="checkbox checkbox-sm" />
|
|
<span>Hide markers</span>
|
|
</label>
|
|
</section>
|
|
<!-- Navigation -->
|
|
<section class="flex flex-col gap-3">
|
|
<h3 class="text-xs font-semibold uppercase tracking-wider text-base-content/70 flex items-center gap-1.5">
|
|
<icons-icon-map-pin class="size-3.5 opacity-80" aria-hidden="true" />
|
|
Navigation
|
|
</h3>
|
|
<fieldset class="fieldset">
|
|
<label class="label py-0"><span>Jump to Map</span></label>
|
|
<select
|
|
v-model="selectedMapIdSelect"
|
|
class="select select-sm w-full focus:ring-2 focus:ring-primary touch-manipulation"
|
|
:class="touchFriendly ? 'min-h-11 text-base' : ''"
|
|
>
|
|
<option value="">Select map</option>
|
|
<option v-for="m in maps" :key="m.ID" :value="String(m.ID)">{{ m.Name }}</option>
|
|
</select>
|
|
</fieldset>
|
|
<fieldset class="fieldset">
|
|
<label class="label py-0 flex items-center gap-1.5">
|
|
<span>Overlay Map</span>
|
|
<span
|
|
class="inline-flex text-base-content/60 cursor-help"
|
|
title="Overlay shows markers from another map on top of the current one."
|
|
aria-label="Overlay shows markers from another map on top of the current one."
|
|
>
|
|
<icons-icon-info class="size-3.5" />
|
|
</span>
|
|
</label>
|
|
<select
|
|
v-model="overlayMapId"
|
|
class="select select-sm w-full focus:ring-2 focus:ring-primary touch-manipulation"
|
|
:class="touchFriendly ? 'min-h-11 text-base' : ''"
|
|
>
|
|
<option value="-1">None</option>
|
|
<option v-for="m in maps" :key="'overlay-' + m.ID" :value="String(m.ID)">{{ m.Name }}</option>
|
|
</select>
|
|
</fieldset>
|
|
<fieldset class="fieldset">
|
|
<label class="label py-0"><span>Jump to</span></label>
|
|
<div class="join w-full flex">
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm join-item flex-1 touch-manipulation"
|
|
:class="[jumpToTab === 'quest' ? 'btn-active' : 'btn-ghost', touchFriendly ? 'min-h-11 text-base' : '']"
|
|
aria-pressed="jumpToTab === 'quest'"
|
|
@click="jumpToTab = 'quest'"
|
|
>
|
|
Quest giver
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm join-item flex-1 touch-manipulation"
|
|
:class="[jumpToTab === 'player' ? 'btn-active' : 'btn-ghost', touchFriendly ? 'min-h-11 text-base' : '']"
|
|
aria-pressed="jumpToTab === 'player'"
|
|
@click="jumpToTab = 'player'"
|
|
>
|
|
Player
|
|
</button>
|
|
</div>
|
|
<select
|
|
v-if="jumpToTab === 'quest'"
|
|
v-model="selectedMarkerIdSelect"
|
|
class="select select-sm w-full focus:ring-2 focus:ring-primary touch-manipulation mt-1"
|
|
:class="touchFriendly ? 'min-h-11 text-base' : ''"
|
|
aria-label="Select quest giver"
|
|
>
|
|
<option value="">Select quest giver</option>
|
|
<option v-for="q in questGivers" :key="q.id" :value="String(q.id)">{{ q.name }}</option>
|
|
</select>
|
|
<select
|
|
v-else
|
|
v-model="selectedPlayerIdSelect"
|
|
class="select select-sm w-full focus:ring-2 focus:ring-primary touch-manipulation mt-1"
|
|
:class="touchFriendly ? 'min-h-11 text-base' : ''"
|
|
aria-label="Select player"
|
|
>
|
|
<option value="">Select player</option>
|
|
<option v-for="p in players" :key="p.id" :value="String(p.id)">{{ p.name }}</option>
|
|
</select>
|
|
</fieldset>
|
|
</section>
|
|
<!-- Saved locations -->
|
|
<MapBookmarks
|
|
:maps="maps"
|
|
:current-map-id="currentMapId ?? null"
|
|
:current-coords="currentCoords ?? null"
|
|
:selected-marker-for-bookmark="selectedMarkerForBookmark ?? null"
|
|
:touch-friendly="touchFriendly"
|
|
/>
|
|
<p class="text-xs text-base-content/50 mt-2">
|
|
<kbd class="kbd kbd-xs">?</kbd> for shortcuts
|
|
</p>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { MapInfo, Marker as ApiMarker } from '~/types/api'
|
|
import type { SelectedMarkerForBookmark } from '~/components/map/MapBookmarks.vue'
|
|
import MapBookmarks from '~/components/map/MapBookmarks.vue'
|
|
import { HnHMinZoom, HnHMaxZoom } from '~/lib/LeafletCustomTypes'
|
|
|
|
interface QuestGiver {
|
|
id: number
|
|
name: string
|
|
}
|
|
|
|
interface Player {
|
|
id: number
|
|
name: string
|
|
}
|
|
|
|
const zoomMin = HnHMinZoom
|
|
const zoomMax = HnHMaxZoom
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
maps: MapInfo[]
|
|
questGivers: QuestGiver[]
|
|
players: Player[]
|
|
markers?: ApiMarker[]
|
|
touchFriendly?: boolean
|
|
selectedMapIdSelect: string
|
|
overlayMapId: number
|
|
selectedMarkerIdSelect: string
|
|
selectedPlayerIdSelect: string
|
|
currentZoom?: number
|
|
currentMapId?: number
|
|
currentCoords?: { x: number; y: number; z: number } | null
|
|
selectedMarkerForBookmark?: SelectedMarkerForBookmark
|
|
}>(),
|
|
{ touchFriendly: false, markers: () => [], currentZoom: 1, currentMapId: 0, currentCoords: null, selectedMarkerForBookmark: null }
|
|
)
|
|
|
|
const emit = defineEmits<{
|
|
zoomIn: []
|
|
zoomOut: []
|
|
resetView: []
|
|
setZoom: [level: number]
|
|
jumpToMarker: [id: number]
|
|
'update:hideMarkers': [v: boolean]
|
|
'update:selectedMapIdSelect': [v: string]
|
|
'update:overlayMapId': [v: number]
|
|
'update:selectedMarkerIdSelect': [v: string]
|
|
'update:selectedPlayerIdSelect': [v: string]
|
|
}>()
|
|
|
|
function onZoomSliderInput(event: Event) {
|
|
const value = (event.target as HTMLInputElement).value
|
|
const level = Number(value)
|
|
if (!Number.isNaN(level)) emit('setZoom', level)
|
|
}
|
|
|
|
const hideMarkers = defineModel<boolean>('hideMarkers', { required: true })
|
|
|
|
const jumpToTab = ref<'quest' | 'player'>('quest')
|
|
|
|
const selectedMapIdSelect = computed({
|
|
get: () => props.selectedMapIdSelect,
|
|
set: (v) => emit('update:selectedMapIdSelect', v),
|
|
})
|
|
const overlayMapId = computed({
|
|
get: () => String(props.overlayMapId),
|
|
set: (v) => emit('update:overlayMapId', v === '' ? -1 : Number(v)),
|
|
})
|
|
const selectedMarkerIdSelect = computed({
|
|
get: () => props.selectedMarkerIdSelect,
|
|
set: (v) => emit('update:selectedMarkerIdSelect', v),
|
|
})
|
|
const selectedPlayerIdSelect = computed({
|
|
get: () => props.selectedPlayerIdSelect,
|
|
set: (v) => emit('update:selectedPlayerIdSelect', v),
|
|
})
|
|
</script>
|