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:
2026-03-04 14:06:27 +03:00
parent 761fbaed55
commit fd624c2357
30 changed files with 109 additions and 97 deletions

View File

@@ -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>

View File

@@ -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)"

View File

@@ -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] }>()

View File

@@ -6,5 +6,5 @@
</template>
<script setup lang="ts">
defineOptions({ inheritAttrs: false })
defineOptions({ name: 'AppSkeleton', inheritAttrs: false })
</script>

View File

@@ -30,7 +30,7 @@ const props = withDefaults(
email?: string
size?: number
}>(),
{ size: 32 }
{ size: 32, email: undefined }
)
const gravatarError = ref(false)

View File

@@ -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">

View File

@@ -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

View File

@@ -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>

View File

@@ -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">

View File

@@ -22,7 +22,7 @@
@keydown.enter="onEnter"
@keydown.down.prevent="moveHighlight(1)"
@keydown.up.prevent="moveHighlight(-1)"
/>
>
<button
v-if="query"
type="button"

View File

@@ -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: '/' } })

View File

@@ -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,

View File

@@ -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'] = '[]'

View File

@@ -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 })
})

View File

@@ -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([])

View File

@@ -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() {

View File

@@ -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,

View File

@@ -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() {

View File

@@ -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(() => {

View File

@@ -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 =

View File

@@ -48,7 +48,7 @@ export interface MarkerIconOptions {
fallbackIconUrl?: string
}
export type LeafletApi = typeof import('leaflet')
export type LeafletApi = L
export function createMarker(
data: MarkerData,

View File

@@ -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))
}

View File

@@ -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,
}
}

View File

@@ -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,
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"