Refactor frontend components for improved functionality and accessibility
- Consolidated global error handling in app.vue to redirect users to the login page on API authentication failure. - Enhanced MapView component by reintroducing event listeners for selected map and marker updates, improving interactivity. - Updated PasswordInput and various modal components to ensure proper input handling and accessibility compliance. - Refactored MapControls and MapControlsContent to streamline prop management and enhance user experience. - Improved error handling in local storage operations within useMapBookmarks and useRecentLocations composables. - Standardized input elements across forms for consistency in user interaction.
This commit is contained in:
@@ -4,6 +4,16 @@
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Global error handling: on API auth failure, redirect to login
|
||||
const { onApiError } = useMapApi()
|
||||
const { fullUrl } = useAppPaths()
|
||||
const unsubscribe = onApiError(() => {
|
||||
if (import.meta.client) window.location.href = fullUrl('/login')
|
||||
})
|
||||
onUnmounted(() => unsubscribe())
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page-enter-active,
|
||||
.page-leave-active {
|
||||
@@ -14,13 +24,3 @@
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Global error handling: on API auth failure, redirect to login
|
||||
const { onApiError } = useMapApi()
|
||||
const { fullUrl } = useAppPaths()
|
||||
const unsubscribe = onApiError(() => {
|
||||
if (import.meta.client) window.location.href = fullUrl('/login')
|
||||
})
|
||||
onUnmounted(() => unsubscribe())
|
||||
</script>
|
||||
|
||||
@@ -88,15 +88,10 @@
|
||||
</div>
|
||||
<MapControls
|
||||
:hide-markers="mapLogic.state.hideMarkers.value"
|
||||
@update:hide-markers="(v) => (mapLogic.state.hideMarkers.value = v)"
|
||||
:selected-map-id="mapLogic.state.selectedMapId.value"
|
||||
@update:selected-map-id="(v) => (mapLogic.state.selectedMapId.value = v)"
|
||||
:overlay-map-id="mapLogic.state.overlayMapId.value"
|
||||
@update:overlay-map-id="(v) => (mapLogic.state.overlayMapId.value = v)"
|
||||
:selected-marker-id="mapLogic.state.selectedMarkerId.value"
|
||||
@update:selected-marker-id="(v) => (mapLogic.state.selectedMarkerId.value = v)"
|
||||
:selected-player-id="mapLogic.state.selectedPlayerId.value"
|
||||
@update:selected-player-id="(v) => (mapLogic.state.selectedPlayerId.value = v)"
|
||||
:maps="maps"
|
||||
:quest-givers="questGivers"
|
||||
:players="players"
|
||||
@@ -105,6 +100,11 @@
|
||||
:current-map-id="mapLogic.state.mapid.value"
|
||||
:current-coords="mapLogic.state.displayCoords.value"
|
||||
:selected-marker-for-bookmark="selectedMarkerForBookmark"
|
||||
@update:hide-markers="(v) => (mapLogic.state.hideMarkers.value = v)"
|
||||
@update:selected-map-id="(v) => (mapLogic.state.selectedMapId.value = v)"
|
||||
@update:overlay-map-id="(v) => (mapLogic.state.overlayMapId.value = v)"
|
||||
@update:selected-marker-id="(v) => (mapLogic.state.selectedMarkerId.value = v)"
|
||||
@update:selected-player-id="(v) => (mapLogic.state.selectedPlayerId.value = v)"
|
||||
@zoom-in="mapLogic.zoomIn(leafletMap)"
|
||||
@zoom-out="mapLogic.zoomOutControl(leafletMap)"
|
||||
@reset-view="mapLogic.resetView(leafletMap)"
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
:readonly="readonly"
|
||||
:aria-describedby="ariaDescribedby"
|
||||
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 btn btn-ghost btn-sm btn-square min-h-9 min-w-9 touch-manipulation"
|
||||
@@ -41,7 +41,14 @@ const props = withDefaults(
|
||||
inputId?: string
|
||||
ariaDescribedby?: string
|
||||
}>(),
|
||||
{ required: false, autocomplete: 'off', inputId: undefined, ariaDescribedby: undefined }
|
||||
{
|
||||
required: false,
|
||||
autocomplete: 'off',
|
||||
inputId: undefined,
|
||||
ariaDescribedby: undefined,
|
||||
label: undefined,
|
||||
placeholder: undefined,
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({ inheritAttrs: false })
|
||||
defineOptions({ name: 'AppSkeleton', inheritAttrs: false })
|
||||
</script>
|
||||
|
||||
@@ -30,7 +30,7 @@ const props = withDefaults(
|
||||
email?: string
|
||||
size?: number
|
||||
}>(),
|
||||
{ size: 32 }
|
||||
{ size: 32, email: undefined }
|
||||
)
|
||||
|
||||
const gravatarError = ref(false)
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
class="input input-bordered w-full"
|
||||
placeholder="Bookmark name"
|
||||
@keydown.enter.prevent="onSubmit"
|
||||
/>
|
||||
>
|
||||
</div>
|
||||
<div class="modal-action">
|
||||
<form method="dialog" @submit.prevent="onSubmit">
|
||||
|
||||
@@ -39,13 +39,9 @@
|
||||
<MapControlsContent
|
||||
v-model:hide-markers="hideMarkers"
|
||||
:selected-map-id-select="selectedMapIdSelect"
|
||||
@update:selected-map-id-select="(v) => (selectedMapIdSelect = v)"
|
||||
:overlay-map-id="overlayMapId"
|
||||
@update:overlay-map-id="(v) => (overlayMapId = v)"
|
||||
:selected-marker-id-select="selectedMarkerIdSelect"
|
||||
@update:selected-marker-id-select="(v) => (selectedMarkerIdSelect = v)"
|
||||
:selected-player-id-select="selectedPlayerIdSelect"
|
||||
@update:selected-player-id-select="(v) => (selectedPlayerIdSelect = v)"
|
||||
:maps="maps"
|
||||
:quest-givers="questGivers"
|
||||
:players="players"
|
||||
@@ -54,6 +50,10 @@
|
||||
:current-map-id="currentMapId ?? undefined"
|
||||
:current-coords="currentCoords"
|
||||
:selected-marker-for-bookmark="selectedMarkerForBookmark"
|
||||
@update:selected-map-id-select="(v) => (selectedMapIdSelect = v)"
|
||||
@update:overlay-map-id="(v) => (overlayMapId = v)"
|
||||
@update:selected-marker-id-select="(v) => (selectedMarkerIdSelect = v)"
|
||||
@update:selected-player-id-select="(v) => (selectedPlayerIdSelect = v)"
|
||||
@zoom-in="$emit('zoomIn')"
|
||||
@zoom-out="$emit('zoomOut')"
|
||||
@reset-view="$emit('resetView')"
|
||||
@@ -133,13 +133,9 @@
|
||||
<MapControlsContent
|
||||
v-model:hide-markers="hideMarkers"
|
||||
:selected-map-id-select="selectedMapIdSelect"
|
||||
@update:selected-map-id-select="(v) => (selectedMapIdSelect = v)"
|
||||
:overlay-map-id="overlayMapId"
|
||||
@update:overlay-map-id="(v) => (overlayMapId = v)"
|
||||
:selected-marker-id-select="selectedMarkerIdSelect"
|
||||
@update:selected-marker-id-select="(v) => (selectedMarkerIdSelect = v)"
|
||||
:selected-player-id-select="selectedPlayerIdSelect"
|
||||
@update:selected-player-id-select="(v) => (selectedPlayerIdSelect = v)"
|
||||
:maps="maps"
|
||||
:quest-givers="questGivers"
|
||||
:players="players"
|
||||
@@ -149,6 +145,10 @@
|
||||
:current-coords="currentCoords"
|
||||
:selected-marker-for-bookmark="selectedMarkerForBookmark"
|
||||
:touch-friendly="true"
|
||||
@update:selected-map-id-select="(v) => (selectedMapIdSelect = v)"
|
||||
@update:overlay-map-id="(v) => (overlayMapId = v)"
|
||||
@update:selected-marker-id-select="(v) => (selectedMarkerIdSelect = v)"
|
||||
@update:selected-player-id-select="(v) => (selectedPlayerIdSelect = v)"
|
||||
@zoom-in="$emit('zoomIn')"
|
||||
@zoom-out="$emit('zoomOut')"
|
||||
@reset-view="$emit('resetView')"
|
||||
@@ -191,9 +191,9 @@ interface Player {
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
maps: MapInfo[]
|
||||
questGivers: QuestGiver[]
|
||||
players: Player[]
|
||||
maps?: MapInfo[]
|
||||
questGivers?: QuestGiver[]
|
||||
players?: Player[]
|
||||
markers?: ApiMarker[]
|
||||
currentZoom?: number
|
||||
currentMapId?: number | null
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
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" />
|
||||
<input v-model="hideMarkers" type="checkbox" class="checkbox checkbox-sm" >
|
||||
<span>Hide markers</span>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
<h3 id="coord-set-modal-title" class="font-bold text-lg">Rewrite tile coords</h3>
|
||||
<p class="py-2">From ({{ coordSetFrom.x }}, {{ coordSetFrom.y }}) to:</p>
|
||||
<div class="flex gap-2">
|
||||
<input ref="firstInputRef" v-model.number="localTo.x" type="number" class="input flex-1" placeholder="X" />
|
||||
<input v-model.number="localTo.y" type="number" class="input flex-1" placeholder="Y" />
|
||||
<input ref="firstInputRef" v-model.number="localTo.x" type="number" class="input flex-1" placeholder="X" >
|
||||
<input v-model.number="localTo.y" type="number" class="input flex-1" placeholder="Y" >
|
||||
</div>
|
||||
<div class="modal-action">
|
||||
<form method="dialog" @submit.prevent="onSubmit">
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
@keydown.enter="onEnter"
|
||||
@keydown.down.prevent="moveHighlight(1)"
|
||||
@keydown.up.prevent="moveHighlight(-1)"
|
||||
/>
|
||||
>
|
||||
<button
|
||||
v-if="query"
|
||||
type="button"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
import { useAppPaths } from '../useAppPaths'
|
||||
|
||||
const useRuntimeConfigMock = vi.fn()
|
||||
vi.stubGlobal('useRuntimeConfig', useRuntimeConfigMock)
|
||||
|
||||
import { useAppPaths } from '../useAppPaths'
|
||||
|
||||
describe('useAppPaths with default base /', () => {
|
||||
beforeEach(() => {
|
||||
useRuntimeConfigMock.mockReturnValue({ app: { baseURL: '/' } })
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
|
||||
import { useMapApi } from '../useMapApi'
|
||||
|
||||
vi.stubGlobal('useRuntimeConfig', () => ({
|
||||
app: { baseURL: '/' },
|
||||
public: { apiBase: '/map/api' },
|
||||
}))
|
||||
|
||||
import { useMapApi } from '../useMapApi'
|
||||
|
||||
function mockFetch(status: number, body: unknown, contentType = 'application/json') {
|
||||
return vi.fn().mockResolvedValue({
|
||||
ok: status >= 200 && status < 300,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useMapBookmarks } from '../useMapBookmarks'
|
||||
|
||||
const stateByKey: Record<string, ReturnType<typeof ref>> = {}
|
||||
const useStateMock = vi.fn((key: string, init: () => unknown) => {
|
||||
if (!stateByKey[key]) {
|
||||
@@ -18,15 +20,13 @@ const localStorageMock = {
|
||||
storage[key] = value
|
||||
}),
|
||||
clear: vi.fn(() => {
|
||||
for (const k of Object.keys(storage)) delete storage[k]
|
||||
delete storage['hnh-map-bookmarks']
|
||||
}),
|
||||
}
|
||||
vi.stubGlobal('localStorage', localStorageMock)
|
||||
vi.stubGlobal('import.meta.server', false)
|
||||
vi.stubGlobal('import.meta.client', true)
|
||||
|
||||
import { useMapBookmarks } from '../useMapBookmarks'
|
||||
|
||||
describe('useMapBookmarks', () => {
|
||||
beforeEach(() => {
|
||||
storage['hnh-map-bookmarks'] = '[]'
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { ref, reactive } from 'vue'
|
||||
import type { Map } from 'leaflet'
|
||||
|
||||
import { useMapLogic } from '../useMapLogic'
|
||||
|
||||
vi.stubGlobal('ref', ref)
|
||||
vi.stubGlobal('reactive', reactive)
|
||||
|
||||
import { useMapLogic } from '../useMapLogic'
|
||||
|
||||
describe('useMapLogic', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@@ -27,7 +28,7 @@ describe('useMapLogic', () => {
|
||||
it('zoomIn calls map.zoomIn', () => {
|
||||
const { zoomIn } = useMapLogic()
|
||||
const mockMap = { zoomIn: vi.fn() }
|
||||
zoomIn(mockMap as unknown as import('leaflet').Map)
|
||||
zoomIn(mockMap as unknown as Map)
|
||||
expect(mockMap.zoomIn).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -39,7 +40,7 @@ describe('useMapLogic', () => {
|
||||
it('zoomOutControl calls map.zoomOut', () => {
|
||||
const { zoomOutControl } = useMapLogic()
|
||||
const mockMap = { zoomOut: vi.fn() }
|
||||
zoomOutControl(mockMap as unknown as import('leaflet').Map)
|
||||
zoomOutControl(mockMap as unknown as Map)
|
||||
expect(mockMap.zoomOut).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -47,7 +48,7 @@ describe('useMapLogic', () => {
|
||||
const { state, resetView } = useMapLogic()
|
||||
state.trackingCharacterId.value = 42
|
||||
const mockMap = { setView: vi.fn() }
|
||||
resetView(mockMap as unknown as import('leaflet').Map)
|
||||
resetView(mockMap as unknown as Map)
|
||||
expect(state.trackingCharacterId.value).toBe(-1)
|
||||
expect(mockMap.setView).toHaveBeenCalledWith([0, 0], 1, { animate: false })
|
||||
})
|
||||
@@ -59,7 +60,7 @@ describe('useMapLogic', () => {
|
||||
getCenter: vi.fn(() => ({ lat: 0, lng: 0 })),
|
||||
getZoom: vi.fn(() => 3),
|
||||
}
|
||||
updateDisplayCoords(mockMap as unknown as import('leaflet').Map)
|
||||
updateDisplayCoords(mockMap as unknown as Map)
|
||||
expect(state.displayCoords.value).toEqual({ x: 5, y: 3, z: 3 })
|
||||
})
|
||||
|
||||
@@ -72,7 +73,7 @@ describe('useMapLogic', () => {
|
||||
it('toLatLng calls map.unproject', () => {
|
||||
const { toLatLng } = useMapLogic()
|
||||
const mockMap = { unproject: vi.fn(() => ({ lat: 1, lng: 2 })) }
|
||||
const result = toLatLng(mockMap as unknown as import('leaflet').Map, 100, 200)
|
||||
const result = toLatLng(mockMap as unknown as Map, 100, 200)
|
||||
expect(mockMap.unproject).toHaveBeenCalledWith([100, 200], 6)
|
||||
expect(result).toEqual({ lat: 1, lng: 2 })
|
||||
})
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useToast } from '../useToast'
|
||||
|
||||
const stateByKey: Record<string, ReturnType<typeof ref>> = {}
|
||||
const useStateMock = vi.fn((key: string, init: () => unknown) => {
|
||||
if (!stateByKey[key]) {
|
||||
@@ -10,8 +12,6 @@ const useStateMock = vi.fn((key: string, init: () => unknown) => {
|
||||
})
|
||||
vi.stubGlobal('useState', useStateMock)
|
||||
|
||||
import { useToast } from '../useToast'
|
||||
|
||||
describe('useToast', () => {
|
||||
beforeEach(() => {
|
||||
stateByKey['hnh-map-toasts'] = ref([])
|
||||
|
||||
@@ -29,7 +29,7 @@ function saveBookmarks(bookmarks: MapBookmark[]) {
|
||||
if (import.meta.server) return
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(bookmarks.slice(0, MAX_BOOKMARKS)))
|
||||
} catch (_) {}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
export function useMapBookmarks() {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import type { SmartTileLayer } from '~/lib/SmartTileLayer'
|
||||
import type { Marker as ApiMarker, Character as ApiCharacter } from '~/types/api'
|
||||
|
||||
type LeafletModule = L
|
||||
type SmartTileLayerInstance = InstanceType<typeof SmartTileLayer>
|
||||
|
||||
function escapeHtml(s: string): string {
|
||||
@@ -25,7 +26,7 @@ function escapeHtml(s: string): string {
|
||||
|
||||
export interface MapLayersOptions {
|
||||
/** Leaflet API (from dynamic import). Required for creating markers and characters without static leaflet import. */
|
||||
L: typeof import('leaflet')
|
||||
L: LeafletModule
|
||||
map: L.Map
|
||||
markerLayer: L.LayerGroup
|
||||
layer: SmartTileLayerInstance
|
||||
@@ -33,7 +34,7 @@ export interface MapLayersOptions {
|
||||
getCurrentMapId: () => number
|
||||
setCurrentMapId: (id: number) => void
|
||||
setSelectedMapId: (id: number) => void
|
||||
getAuths: () => string[]
|
||||
getAuths?: () => string[]
|
||||
getTrackingCharacterId: () => number
|
||||
setTrackingCharacterId: (id: number) => void
|
||||
onMarkerContextMenu: (clientX: number, clientY: number, id: number, name: string) => void
|
||||
@@ -69,7 +70,7 @@ export function createMapLayers(options: MapLayersOptions): MapLayersManager {
|
||||
getCurrentMapId,
|
||||
setCurrentMapId,
|
||||
setSelectedMapId,
|
||||
getAuths,
|
||||
getAuths: _getAuths,
|
||||
getTrackingCharacterId,
|
||||
setTrackingCharacterId,
|
||||
onMarkerContextMenu,
|
||||
|
||||
@@ -26,7 +26,7 @@ function saveRecent(list: RecentLocation[]) {
|
||||
if (import.meta.server) return
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(list.slice(0, MAX_RECENT)))
|
||||
} catch (_) {}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
export function useRecentLocations() {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
type="checkbox"
|
||||
class="drawer-toggle"
|
||||
@change="onDrawerChange"
|
||||
/>
|
||||
>
|
||||
<div class="drawer-content flex flex-col h-screen overflow-hidden">
|
||||
<header class="navbar relative z-[1100] bg-base-100/80 backdrop-blur-xl border-b border-base-300/50 px-4 gap-2 shrink-0">
|
||||
<NuxtLink to="/" class="flex items-center gap-2 text-lg font-semibold hover:opacity-80 transition-all duration-200">
|
||||
@@ -87,7 +87,7 @@
|
||||
class="toggle toggle-sm toggle-primary shrink-0"
|
||||
:checked="dark"
|
||||
@change="onThemeToggle"
|
||||
/>
|
||||
>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
@@ -177,7 +177,7 @@
|
||||
class="toggle toggle-sm toggle-primary shrink-0"
|
||||
:checked="dark"
|
||||
@change="onThemeToggle"
|
||||
/>
|
||||
>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
@@ -296,7 +296,7 @@ async function loadConfig(loadToken: number) {
|
||||
const config = await useMapApi().getConfig()
|
||||
if (loadToken !== loadId) return
|
||||
if (config?.title) title.value = config.title
|
||||
} catch (_) {}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import type L from 'leaflet'
|
||||
import { getColorForCharacterId, type CharacterColors } from '~/lib/characterColors'
|
||||
import { HnHMaxZoom } from '~/lib/LeafletCustomTypes'
|
||||
|
||||
export type LeafletApi = typeof import('leaflet')
|
||||
export type LeafletApi = L
|
||||
|
||||
function buildCharacterIconUrl(colors: CharacterColors): string {
|
||||
const svg =
|
||||
|
||||
@@ -48,7 +48,7 @@ export interface MarkerIconOptions {
|
||||
fallbackIconUrl?: string
|
||||
}
|
||||
|
||||
export type LeafletApi = typeof import('leaflet')
|
||||
export type LeafletApi = L
|
||||
|
||||
export function createMarker(
|
||||
data: MarkerData,
|
||||
|
||||
@@ -39,7 +39,10 @@ export function uniqueListUpdate<T extends Identifiable>(
|
||||
if (addCallback) {
|
||||
elementsToAdd.forEach((it) => addCallback(it))
|
||||
}
|
||||
elementsToRemove.forEach((it) => delete list.elements[String(it.id)])
|
||||
const toRemove = new Set(elementsToRemove.map((it) => String(it.id)))
|
||||
list.elements = Object.fromEntries(
|
||||
Object.entries(list.elements).filter(([id]) => !toRemove.has(id))
|
||||
) as Record<string, T>
|
||||
elementsToAdd.forEach((it) => (list.elements[String(it.id)] = it))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
import type L from 'leaflet'
|
||||
import type { Map, LayerGroup } from 'leaflet'
|
||||
import { createCharacter, type CharacterData, type CharacterMapViewRef } from '../Character'
|
||||
|
||||
vi.mock('leaflet', () => {
|
||||
const markerMock = {
|
||||
on: vi.fn().mockReturnThis(),
|
||||
@@ -21,9 +25,6 @@ vi.mock('~/lib/LeafletCustomTypes', () => ({
|
||||
HnHMaxZoom: 6,
|
||||
}))
|
||||
|
||||
import type L from 'leaflet'
|
||||
import { createCharacter, type CharacterData, type CharacterMapViewRef } from '../Character'
|
||||
|
||||
function getL(): L {
|
||||
return require('leaflet').default
|
||||
}
|
||||
@@ -44,12 +45,12 @@ function makeMapViewRef(mapid = 1): CharacterMapViewRef {
|
||||
map: {
|
||||
unproject: vi.fn(() => ({ lat: 0, lng: 0 })),
|
||||
removeLayer: vi.fn(),
|
||||
} as unknown as import('leaflet').Map,
|
||||
} as unknown as Map,
|
||||
mapid,
|
||||
markerLayer: {
|
||||
removeLayer: vi.fn(),
|
||||
addLayer: vi.fn(),
|
||||
} as unknown as import('leaflet').LayerGroup,
|
||||
} as unknown as LayerGroup,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
import L from 'leaflet'
|
||||
import type { Map, LayerGroup } from 'leaflet'
|
||||
import { createMarker, type MarkerData, type MapViewRef } from '../Marker'
|
||||
|
||||
vi.mock('leaflet', () => {
|
||||
const markerMock = {
|
||||
on: vi.fn().mockReturnThis(),
|
||||
@@ -14,24 +18,19 @@ vi.mock('leaflet', () => {
|
||||
return {
|
||||
default: {
|
||||
marker: vi.fn(() => markerMock),
|
||||
Icon: class {},
|
||||
Icon: vi.fn(),
|
||||
},
|
||||
marker: vi.fn(() => markerMock),
|
||||
Icon: class {},
|
||||
Icon: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('~/lib/LeafletCustomTypes', () => ({
|
||||
HnHMaxZoom: 6,
|
||||
TileSize: 100,
|
||||
ImageIcon: class {
|
||||
constructor(_opts: Record<string, unknown>) {}
|
||||
},
|
||||
ImageIcon: vi.fn(),
|
||||
}))
|
||||
|
||||
import L from 'leaflet'
|
||||
import { createMarker, type MarkerData, type MapViewRef } from '../Marker'
|
||||
|
||||
function makeMarkerData(overrides: Partial<MarkerData> = {}): MarkerData {
|
||||
return {
|
||||
id: 1,
|
||||
@@ -48,12 +47,12 @@ function makeMapViewRef(): MapViewRef {
|
||||
return {
|
||||
map: {
|
||||
unproject: vi.fn(() => ({ lat: 0, lng: 0 })),
|
||||
} as unknown as import('leaflet').Map,
|
||||
} as unknown as Map,
|
||||
mapid: 1,
|
||||
markerLayer: {
|
||||
removeLayer: vi.fn(),
|
||||
addLayer: vi.fn(),
|
||||
} as unknown as import('leaflet').LayerGroup,
|
||||
} as unknown as LayerGroup,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
placeholder="Search users…"
|
||||
class="input input-sm input-bordered w-full min-h-11 touch-manipulation"
|
||||
aria-label="Search users"
|
||||
/>
|
||||
>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 max-h-[60vh] overflow-y-auto">
|
||||
<div
|
||||
@@ -101,7 +101,7 @@
|
||||
placeholder="Search maps…"
|
||||
class="input input-sm input-bordered w-full min-h-11 touch-manipulation"
|
||||
aria-label="Search maps"
|
||||
/>
|
||||
>
|
||||
</div>
|
||||
<div class="overflow-x-auto max-h-[60vh] overflow-y-auto">
|
||||
<table class="table table-sm table-zebra min-w-[32rem]">
|
||||
@@ -121,7 +121,7 @@
|
||||
</th>
|
||||
<th scope="col">Hidden</th>
|
||||
<th scope="col">Priority</th>
|
||||
<th scope="col" class="text-right"></th>
|
||||
<th scope="col" class="text-right"/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -159,7 +159,7 @@
|
||||
v-model="settings.prefix"
|
||||
type="text"
|
||||
class="input input-sm w-full min-h-11 touch-manipulation"
|
||||
/>
|
||||
>
|
||||
</fieldset>
|
||||
<fieldset class="fieldset w-full max-w-xs">
|
||||
<label class="label" for="admin-settings-title">Title</label>
|
||||
@@ -168,7 +168,7 @@
|
||||
v-model="settings.title"
|
||||
type="text"
|
||||
class="input input-sm w-full min-h-11 touch-manipulation"
|
||||
/>
|
||||
>
|
||||
</fieldset>
|
||||
<fieldset class="fieldset">
|
||||
<label class="label gap-2 cursor-pointer justify-start min-h-11 touch-manipulation" for="admin-settings-default-hide">
|
||||
@@ -177,7 +177,7 @@
|
||||
v-model="settings.defaultHide"
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-sm"
|
||||
/>
|
||||
>
|
||||
Default hide new maps
|
||||
</label>
|
||||
</fieldset>
|
||||
@@ -211,7 +211,7 @@
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<input ref="mergeFileRef" type="file" accept=".zip" class="hidden" @change="onMergeFile" />
|
||||
<input ref="mergeFileRef" type="file" accept=".zip" class="hidden" @change="onMergeFile" >
|
||||
<button type="button" class="btn btn-sm min-h-11 touch-manipulation" @click="mergeFileRef?.click()">
|
||||
Choose merge file
|
||||
</button>
|
||||
|
||||
@@ -2,20 +2,20 @@
|
||||
<div class="container mx-auto p-4 max-w-2xl min-w-0">
|
||||
<h1 class="text-2xl font-bold mb-6">Edit map {{ id }}</h1>
|
||||
|
||||
<form v-if="map" @submit.prevent="submit" class="flex flex-col gap-4">
|
||||
<form v-if="map" class="flex flex-col gap-4" @submit.prevent="submit">
|
||||
<fieldset class="fieldset">
|
||||
<label class="label" for="name">Name</label>
|
||||
<input id="name" v-model="form.name" type="text" class="input min-h-11 touch-manipulation" required />
|
||||
<input id="name" v-model="form.name" type="text" class="input min-h-11 touch-manipulation" required >
|
||||
</fieldset>
|
||||
<fieldset class="fieldset">
|
||||
<label class="label cursor-pointer gap-2">
|
||||
<input v-model="form.hidden" type="checkbox" class="checkbox" />
|
||||
<input v-model="form.hidden" type="checkbox" class="checkbox" >
|
||||
<span>Hidden</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset class="fieldset">
|
||||
<label class="label cursor-pointer gap-2">
|
||||
<input v-model="form.priority" type="checkbox" class="checkbox" />
|
||||
<input v-model="form.priority" type="checkbox" class="checkbox" >
|
||||
<span>Priority</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="container mx-auto p-4 max-w-2xl min-w-0">
|
||||
<h1 class="text-2xl font-bold mb-6">{{ isNew ? 'New user' : `Edit ${username}` }}</h1>
|
||||
|
||||
<form @submit.prevent="submit" class="flex flex-col gap-4">
|
||||
<form class="flex flex-col gap-4" @submit.prevent="submit">
|
||||
<fieldset class="fieldset">
|
||||
<label class="label" for="user">Username</label>
|
||||
<input
|
||||
@@ -12,7 +12,7 @@
|
||||
class="input min-h-11 touch-manipulation"
|
||||
required
|
||||
:readonly="!isNew"
|
||||
/>
|
||||
>
|
||||
</fieldset>
|
||||
<p id="admin-user-password-hint" class="text-sm text-base-content/60 mb-1">Leave blank to keep current password.</p>
|
||||
<PasswordInput
|
||||
@@ -25,7 +25,7 @@
|
||||
<label class="label">Auths</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<label v-for="a of authOptions" :key="a" class="label cursor-pointer gap-2" :for="`auth-${a}`">
|
||||
<input :id="`auth-${a}`" v-model="form.auths" type="checkbox" :value="a" class="checkbox checkbox-sm" />
|
||||
<input :id="`auth-${a}`" v-model="form.auths" type="checkbox" :value="a" class="checkbox checkbox-sm" >
|
||||
<span>{{ a }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -14,18 +14,18 @@
|
||||
</a>
|
||||
<div class="divider text-sm">or</div>
|
||||
</div>
|
||||
<form @submit.prevent="submit" class="flex flex-col gap-4">
|
||||
<form class="flex flex-col gap-4" @submit.prevent="submit">
|
||||
<fieldset class="fieldset">
|
||||
<label class="label" for="user">User</label>
|
||||
<input
|
||||
ref="userInputRef"
|
||||
id="user"
|
||||
ref="userInputRef"
|
||||
v-model="user"
|
||||
type="text"
|
||||
class="input min-h-11 touch-manipulation"
|
||||
required
|
||||
autocomplete="username"
|
||||
/>
|
||||
>
|
||||
</fieldset>
|
||||
<PasswordInput
|
||||
v-model="pass"
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
<icons-icon-settings />
|
||||
Change password
|
||||
</h2>
|
||||
<form @submit.prevent="changePass" class="flex flex-col gap-2">
|
||||
<form class="flex flex-col gap-2" @submit.prevent="changePass">
|
||||
<PasswordInput
|
||||
v-model="newPass"
|
||||
placeholder="New password"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
This is the first run. Create the administrator account using the bootstrap password
|
||||
from the server configuration (e.g. <code class="text-xs">HNHMAP_BOOTSTRAP_PASSWORD</code>).
|
||||
</p>
|
||||
<form @submit.prevent="submit" class="flex flex-col gap-4">
|
||||
<form class="flex flex-col gap-4" @submit.prevent="submit">
|
||||
<PasswordInput
|
||||
v-model="pass"
|
||||
label="Bootstrap password"
|
||||
|
||||
Reference in New Issue
Block a user