Enhance frontend UI and functionality
- Added page transition effects in app.vue for smoother navigation. - Updated nuxt.config.ts to include custom font styles and page transitions. - Improved loading indicators in MapPageWrapper.vue and login.vue for better user experience. - Enhanced MapView.vue with a collapsible control panel and improved styling. - Introduced new icons for various components to enhance visual consistency. - Updated Tailwind CSS configuration to extend font families and improve theme management. - Refined layout styles in default.vue and admin pages for better responsiveness and aesthetics. - Implemented error handling and loading states across various forms for improved user feedback.
This commit is contained in:
@@ -4,6 +4,17 @@
|
|||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.page-enter-active,
|
||||||
|
.page-leave-active {
|
||||||
|
transition: opacity 0.15s ease-out;
|
||||||
|
}
|
||||||
|
.page-enter-from,
|
||||||
|
.page-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// Global error handling: on API auth failure, redirect to login
|
// Global error handling: on API auth failure, redirect to login
|
||||||
const { onApiError } = useMapApi()
|
const { onApiError } = useMapApi()
|
||||||
|
|||||||
@@ -7,3 +7,22 @@ body,
|
|||||||
#__nuxt {
|
#__nuxt {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', ui-sans-serif, system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes login-card-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(1rem);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
animation: login-card-in 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,15 @@
|
|||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
<template #fallback>
|
<template #fallback>
|
||||||
<div class="h-screen flex items-center justify-center bg-base-200">Loading map…</div>
|
<div class="h-screen flex flex-col items-center justify-center gap-4 bg-base-200">
|
||||||
|
<span class="loading loading-spinner loading-lg text-primary" />
|
||||||
|
<p class="text-base-content/80 font-medium">Loading map…</p>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<div class="w-24 h-3 rounded bg-base-300 animate-pulse" />
|
||||||
|
<div class="w-32 h-3 rounded bg-base-300 animate-pulse" />
|
||||||
|
<div class="w-20 h-3 rounded bg-base-300 animate-pulse" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,108 +17,138 @@
|
|||||||
<!-- Grid coords & zoom (bottom-right) -->
|
<!-- Grid coords & zoom (bottom-right) -->
|
||||||
<div
|
<div
|
||||||
v-if="displayCoords"
|
v-if="displayCoords"
|
||||||
class="absolute bottom-2 right-2 z-[501] rounded bg-base-100/90 px-2 py-1 font-mono text-xs shadow"
|
class="absolute bottom-2 right-2 z-[501] rounded-lg px-3 py-2 font-mono text-sm bg-base-100/95 backdrop-blur-sm border border-base-300/50 shadow"
|
||||||
aria-label="Current grid position and zoom"
|
aria-label="Current grid position and zoom"
|
||||||
|
title="mapId · x, y · zoom"
|
||||||
>
|
>
|
||||||
{{ mapid }} · {{ displayCoords.x }}, {{ displayCoords.y }} · z{{ displayCoords.z }}
|
{{ mapid }} · {{ displayCoords.x }}, {{ displayCoords.y }} · z{{ displayCoords.z }}
|
||||||
</div>
|
</div>
|
||||||
<!-- Control panel -->
|
<!-- Control panel -->
|
||||||
<div class="absolute left-3 top-[10%] z-[502] card card-compact bg-base-100 shadow-xl w-56">
|
<div
|
||||||
<div class="card-body p-3 gap-2">
|
class="absolute left-3 top-[10%] z-[502] flex transition-all duration-300 ease-out"
|
||||||
<div class="flex flex-col gap-2">
|
:class="panelCollapsed ? 'w-12' : 'w-64'"
|
||||||
<div class="flex items-center gap-1">
|
>
|
||||||
|
<div
|
||||||
|
class="rounded-xl bg-base-100/80 backdrop-blur-xl border border-base-300/50 shadow-xl overflow-hidden transition-all duration-300 flex flex-col"
|
||||||
|
:class="panelCollapsed ? 'w-12 items-center py-2' : 'w-56'"
|
||||||
|
>
|
||||||
|
<div v-show="!panelCollapsed" class="flex flex-col p-4 gap-4 flex-1 min-w-0">
|
||||||
|
<!-- Zoom -->
|
||||||
|
<section class="flex flex-col gap-2">
|
||||||
|
<h3 class="text-xs font-semibold uppercase tracking-wider text-base-content/70">Zoom</h3>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-neutral btn-sm btn-square"
|
class="btn btn-outline btn-sm btn-square transition-all duration-200 hover:scale-105"
|
||||||
title="Zoom in"
|
title="Zoom in"
|
||||||
aria-label="Zoom in"
|
aria-label="Zoom in"
|
||||||
@click="zoomIn"
|
@click="zoomIn"
|
||||||
>
|
>
|
||||||
+
|
<icons-icon-zoom-in />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-neutral btn-sm btn-square"
|
class="btn btn-outline btn-sm btn-square transition-all duration-200 hover:scale-105"
|
||||||
title="Zoom out"
|
title="Zoom out"
|
||||||
aria-label="Zoom out"
|
aria-label="Zoom out"
|
||||||
@click="zoomOutControl"
|
@click="zoomOutControl"
|
||||||
>
|
>
|
||||||
−
|
<icons-icon-zoom-out />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline btn-sm btn-square transition-all duration-200 hover:scale-105"
|
||||||
|
title="Reset view — center map and minimum zoom"
|
||||||
|
aria-label="Reset view"
|
||||||
|
@click="zoomOut"
|
||||||
|
>
|
||||||
|
<icons-icon-home />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<label class="label cursor-pointer justify-start gap-2 py-0">
|
</section>
|
||||||
|
<!-- Display -->
|
||||||
|
<section class="flex flex-col gap-2">
|
||||||
|
<h3 class="text-xs font-semibold uppercase tracking-wider text-base-content/70">Display</h3>
|
||||||
|
<label class="label cursor-pointer justify-start gap-2 py-0 hover:bg-base-200/50 rounded-lg px-2 -mx-2">
|
||||||
<input v-model="showGridCoordinates" type="checkbox" class="checkbox checkbox-sm" />
|
<input v-model="showGridCoordinates" type="checkbox" class="checkbox checkbox-sm" />
|
||||||
<span class="label-text">Show grid coordinates</span>
|
<span class="label-text">Show grid coordinates</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="label cursor-pointer justify-start gap-2 py-0">
|
<label class="label cursor-pointer justify-start gap-2 py-0 hover:bg-base-200/50 rounded-lg px-2 -mx-2">
|
||||||
<input v-model="hideMarkers" type="checkbox" class="checkbox checkbox-sm" />
|
<input v-model="hideMarkers" type="checkbox" class="checkbox checkbox-sm" />
|
||||||
<span class="label-text">Hide markers</span>
|
<span class="label-text">Hide markers</span>
|
||||||
</label>
|
</label>
|
||||||
<button
|
</section>
|
||||||
type="button"
|
<!-- Navigation -->
|
||||||
class="btn btn-neutral btn-sm"
|
<section class="flex flex-col gap-3">
|
||||||
title="Center map and minimum zoom"
|
<h3 class="text-xs font-semibold uppercase tracking-wider text-base-content/70">Navigation</h3>
|
||||||
@click="zoomOut"
|
|
||||||
>
|
|
||||||
Reset view
|
|
||||||
</button>
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label py-0"><span class="label-text">Jump to Map</span></label>
|
<label class="label py-0"><span class="label-text">Jump to Map</span></label>
|
||||||
<select v-model="selectedMapId" class="select select-bordered select-sm w-full">
|
<select v-model="selectedMapId" class="select select-bordered select-sm w-full focus:ring-2 focus:ring-primary">
|
||||||
<option :value="null">Select map</option>
|
<option :value="null">Select map</option>
|
||||||
<option v-for="m in maps" :key="m.ID" :value="m.ID">{{ m.Name }}</option>
|
<option v-for="m in maps" :key="m.ID" :value="m.ID">{{ m.Name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label py-0"><span class="label-text">Overlay Map</span></label>
|
<label class="label py-0"><span class="label-text">Overlay Map</span></label>
|
||||||
<select v-model="overlayMapId" class="select select-bordered select-sm w-full">
|
<select v-model="overlayMapId" class="select select-bordered select-sm w-full focus:ring-2 focus:ring-primary">
|
||||||
<option :value="-1">None</option>
|
<option :value="-1">None</option>
|
||||||
<option v-for="m in maps" :key="'overlay-' + m.ID" :value="m.ID">{{ m.Name }}</option>
|
<option v-for="m in maps" :key="'overlay-' + m.ID" :value="m.ID">{{ m.Name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label py-0"><span class="label-text">Jump to Quest Giver</span></label>
|
<label class="label py-0"><span class="label-text">Jump to Quest Giver</span></label>
|
||||||
<select v-model="selectedMarkerId" class="select select-bordered select-sm w-full">
|
<select v-model="selectedMarkerId" class="select select-bordered select-sm w-full focus:ring-2 focus:ring-primary">
|
||||||
<option :value="null">Select quest giver</option>
|
<option :value="null">Select quest giver</option>
|
||||||
<option v-for="q in questGivers" :key="q.id" :value="q.id">{{ q.name }}</option>
|
<option v-for="q in questGivers" :key="q.id" :value="q.id">{{ q.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label py-0"><span class="label-text">Jump to Player</span></label>
|
<label class="label py-0"><span class="label-text">Jump to Player</span></label>
|
||||||
<select v-model="selectedPlayerId" class="select select-bordered select-sm w-full">
|
<select v-model="selectedPlayerId" class="select select-bordered select-sm w-full focus:ring-2 focus:ring-primary">
|
||||||
<option :value="null">Select player</option>
|
<option :value="null">Select player</option>
|
||||||
<option v-for="p in players" :key="p.id" :value="p.id">{{ p.name }}</option>
|
<option v-for="p in players" :key="p.id" :value="p.id">{{ p.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost btn-sm btn-square shrink-0 m-1 transition-all duration-200 hover:scale-105"
|
||||||
|
:title="panelCollapsed ? 'Expand panel' : 'Collapse panel'"
|
||||||
|
:aria-label="panelCollapsed ? 'Expand panel' : 'Collapse panel'"
|
||||||
|
@click="panelCollapsed = !panelCollapsed"
|
||||||
|
>
|
||||||
|
<icons-icon-chevron-right v-if="panelCollapsed" />
|
||||||
|
<icons-icon-panel-left v-else />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Context menu (tile) -->
|
<!-- Context menu (tile) -->
|
||||||
<div
|
<div
|
||||||
v-show="contextMenu.tile.show"
|
v-show="contextMenu.tile.show"
|
||||||
class="fixed z-[1000] bg-base-100 shadow-lg rounded-lg border border-base-300 py-1 min-w-[180px]"
|
class="fixed z-[1000] bg-base-100/95 backdrop-blur-xl shadow-xl rounded-lg border border-base-300 py-1 min-w-[180px] transition-opacity duration-150"
|
||||||
:style="{ left: contextMenu.tile.x + 'px', top: contextMenu.tile.y + 'px' }"
|
:style="{ left: contextMenu.tile.x + 'px', top: contextMenu.tile.y + 'px' }"
|
||||||
>
|
>
|
||||||
<button type="button" class="btn btn-ghost btn-sm w-full justify-start" @click="contextMenu.tile.data && (wipeTile(contextMenu.tile.data), (contextMenu.tile.show = false))">
|
<button type="button" class="btn btn-ghost btn-sm w-full justify-start hover:bg-base-200 transition-colors" @click="contextMenu.tile.data && (wipeTile(contextMenu.tile.data), (contextMenu.tile.show = false))">
|
||||||
Wipe tile {{ contextMenu.tile.data?.coords?.x }}, {{ contextMenu.tile.data?.coords?.y }}
|
Wipe tile {{ contextMenu.tile.data?.coords?.x }}, {{ contextMenu.tile.data?.coords?.y }}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-ghost btn-sm w-full justify-start" @click="contextMenu.tile.data && (openCoordSet(contextMenu.tile.data), (contextMenu.tile.show = false))">
|
<button type="button" class="btn btn-ghost btn-sm w-full justify-start hover:bg-base-200 transition-colors" @click="contextMenu.tile.data && (openCoordSet(contextMenu.tile.data), (contextMenu.tile.show = false))">
|
||||||
Rewrite tile coords
|
Rewrite tile coords
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- Context menu (marker) -->
|
<!-- Context menu (marker) -->
|
||||||
<div
|
<div
|
||||||
v-show="contextMenu.marker.show"
|
v-show="contextMenu.marker.show"
|
||||||
class="fixed z-[1000] bg-base-100 shadow-lg rounded-lg border border-base-300 py-1 min-w-[180px]"
|
class="fixed z-[1000] bg-base-100/95 backdrop-blur-xl shadow-xl rounded-lg border border-base-300 py-1 min-w-[180px] transition-opacity duration-150"
|
||||||
:style="{ left: contextMenu.marker.x + 'px', top: contextMenu.marker.y + 'px' }"
|
:style="{ left: contextMenu.marker.x + 'px', top: contextMenu.marker.y + 'px' }"
|
||||||
>
|
>
|
||||||
<button type="button" class="btn btn-ghost btn-sm w-full justify-start" @click="contextMenu.marker.data?.id != null && (hideMarkerById(contextMenu.marker.data.id), (contextMenu.marker.show = false))">
|
<button type="button" class="btn btn-ghost btn-sm w-full justify-start hover:bg-base-200 transition-colors" @click="contextMenu.marker.data?.id != null && (hideMarkerById(contextMenu.marker.data.id), (contextMenu.marker.show = false))">
|
||||||
Hide marker {{ contextMenu.marker.data?.name }}
|
Hide marker {{ contextMenu.marker.data?.name }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- Coord-set modal: close via Cancel, backdrop click, or Escape -->
|
<!-- Coord-set modal: close via Cancel, backdrop click, or Escape -->
|
||||||
<dialog ref="coordSetModal" class="modal" @cancel="closeCoordSetModal">
|
<dialog ref="coordSetModal" class="modal" @cancel="closeCoordSetModal">
|
||||||
<div class="modal-box" @click.stop>
|
<div class="modal-box transition-all duration-200" @click.stop>
|
||||||
<h3 class="font-bold text-lg">Rewrite tile coords</h3>
|
<h3 class="font-bold text-lg">Rewrite tile coords</h3>
|
||||||
<p class="py-2">From ({{ coordSetFrom.x }}, {{ coordSetFrom.y }}) to:</p>
|
<p class="py-2">From ({{ coordSetFrom.x }}, {{ coordSetFrom.y }}) to:</p>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
@@ -162,6 +192,7 @@ const api = useMapApi()
|
|||||||
|
|
||||||
const showGridCoordinates = ref(false)
|
const showGridCoordinates = ref(false)
|
||||||
const hideMarkers = ref(false)
|
const hideMarkers = ref(false)
|
||||||
|
const panelCollapsed = ref(false)
|
||||||
const trackingCharacterId = ref(-1)
|
const trackingCharacterId = ref(-1)
|
||||||
const maps = ref<{ ID: number; Name: string; size?: number }[]>([])
|
const maps = ref<{ ID: number; Name: string; size?: number }[]>([])
|
||||||
const mapsLoaded = ref(false)
|
const mapsLoaded = ref(false)
|
||||||
@@ -361,6 +392,7 @@ onMounted(async () => {
|
|||||||
maxZoom: HnHMaxZoom,
|
maxZoom: HnHMaxZoom,
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
visible: false,
|
visible: false,
|
||||||
|
pane: 'tilePane',
|
||||||
} as any)
|
} as any)
|
||||||
coordLayer.addTo(map)
|
coordLayer.addTo(map)
|
||||||
coordLayer.setZIndex(500)
|
coordLayer.setZIndex(500)
|
||||||
|
|||||||
@@ -21,7 +21,8 @@
|
|||||||
:aria-label="showPass ? 'Hide password' : 'Show password'"
|
:aria-label="showPass ? 'Hide password' : 'Show password'"
|
||||||
@click="showPass = !showPass"
|
@click="showPass = !showPass"
|
||||||
>
|
>
|
||||||
{{ showPass ? '🙈' : '👁' }}
|
<icons-icon-eye-off v-if="showPass" />
|
||||||
|
<icons-icon-eye v-else />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
7
frontend-nuxt/components/icons/IconAlertTriangle.vue
Normal file
7
frontend-nuxt/components/icons/IconAlertTriangle.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
|
||||||
|
<line x1="12" y1="9" x2="12" y2="13" />
|
||||||
|
<line x1="12" y1="17" x2="12.01" y2="17" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
5
frontend-nuxt/components/icons/IconChevronRight.vue
Normal file
5
frontend-nuxt/components/icons/IconChevronRight.vue
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="m9 18 6-6-6-6" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
6
frontend-nuxt/components/icons/IconEye.vue
Normal file
6
frontend-nuxt/components/icons/IconEye.vue
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
6
frontend-nuxt/components/icons/IconEyeOff.vue
Normal file
6
frontend-nuxt/components/icons/IconEyeOff.vue
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" />
|
||||||
|
<line x1="1" y1="1" x2="23" y2="23" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
6
frontend-nuxt/components/icons/IconHome.vue
Normal file
6
frontend-nuxt/components/icons/IconHome.vue
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
||||||
|
<polyline points="9 22 9 12 15 12 15 22" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
7
frontend-nuxt/components/icons/IconKey.vue
Normal file
7
frontend-nuxt/components/icons/IconKey.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<!-- Simple key icon (Lucide-style): round head, shaft, teeth -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<circle cx="7.5" cy="15.5" r="5.5" />
|
||||||
|
<path d="M12 12l4-4 4 4-4 4" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
7
frontend-nuxt/components/icons/IconLogout.vue
Normal file
7
frontend-nuxt/components/icons/IconLogout.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
||||||
|
<polyline points="16 17 21 12 16 7" />
|
||||||
|
<line x1="21" y1="12" x2="9" y2="12" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
7
frontend-nuxt/components/icons/IconMap.vue
Normal file
7
frontend-nuxt/components/icons/IconMap.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6" />
|
||||||
|
<line x1="8" y1="2" x2="8" y2="18" />
|
||||||
|
<line x1="16" y1="6" x2="16" y2="22" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
6
frontend-nuxt/components/icons/IconMapPin.vue
Normal file
6
frontend-nuxt/components/icons/IconMapPin.vue
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
|
||||||
|
<circle cx="12" cy="10" r="3" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
5
frontend-nuxt/components/icons/IconMoon.vue
Normal file
5
frontend-nuxt/components/icons/IconMoon.vue
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
6
frontend-nuxt/components/icons/IconPanelLeft.vue
Normal file
6
frontend-nuxt/components/icons/IconPanelLeft.vue
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
||||||
|
<line x1="9" y1="3" x2="9" y2="21" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
6
frontend-nuxt/components/icons/IconPanelRight.vue
Normal file
6
frontend-nuxt/components/icons/IconPanelRight.vue
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
||||||
|
<line x1="15" y1="3" x2="15" y2="21" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
6
frontend-nuxt/components/icons/IconPencil.vue
Normal file
6
frontend-nuxt/components/icons/IconPencil.vue
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" />
|
||||||
|
<path d="m15 5 4 4" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
6
frontend-nuxt/components/icons/IconPlus.vue
Normal file
6
frontend-nuxt/components/icons/IconPlus.vue
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M5 12h14" />
|
||||||
|
<path d="M12 5v14" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
6
frontend-nuxt/components/icons/IconSettings.vue
Normal file
6
frontend-nuxt/components/icons/IconSettings.vue
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
5
frontend-nuxt/components/icons/IconShield.vue
Normal file
5
frontend-nuxt/components/icons/IconShield.vue
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
6
frontend-nuxt/components/icons/IconSun.vue
Normal file
6
frontend-nuxt/components/icons/IconSun.vue
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<circle cx="12" cy="12" r="4" />
|
||||||
|
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
6
frontend-nuxt/components/icons/IconUser.vue
Normal file
6
frontend-nuxt/components/icons/IconUser.vue
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
||||||
|
<circle cx="12" cy="7" r="4" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
7
frontend-nuxt/components/icons/IconUsers.vue
Normal file
7
frontend-nuxt/components/icons/IconUsers.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
||||||
|
<circle cx="9" cy="7" r="4" />
|
||||||
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
8
frontend-nuxt/components/icons/IconZoomIn.vue
Normal file
8
frontend-nuxt/components/icons/IconZoomIn.vue
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<circle cx="11" cy="11" r="8" />
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||||
|
<line x1="11" y1="8" x2="11" y2="14" />
|
||||||
|
<line x1="8" y1="11" x2="14" y2="11" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
7
frontend-nuxt/components/icons/IconZoomOut.vue
Normal file
7
frontend-nuxt/components/icons/IconZoomOut.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<circle cx="11" cy="11" r="8" />
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||||
|
<line x1="8" y1="11" x2="14" y2="11" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
@@ -1,27 +1,52 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-screen flex flex-col bg-base-100 overflow-hidden">
|
<div class="h-screen flex flex-col bg-base-100 overflow-hidden">
|
||||||
<header class="navbar bg-base-200/80 backdrop-blur px-4 gap-2 shrink-0">
|
<header class="navbar bg-base-100/80 backdrop-blur-xl border-b border-base-300/50 px-4 gap-2 shrink-0">
|
||||||
<NuxtLink to="/" class="text-lg font-semibold hover:opacity-80">{{ title }}</NuxtLink>
|
<NuxtLink to="/" class="text-lg font-semibold hover:opacity-80 transition-all duration-200">{{ title }}</NuxtLink>
|
||||||
<div class="flex-1" />
|
<div class="flex-1" />
|
||||||
<NuxtLink v-if="!isLogin" to="/" class="btn btn-ghost btn-sm">Map</NuxtLink>
|
<NuxtLink
|
||||||
<NuxtLink v-if="!isLogin" to="/profile" class="btn btn-ghost btn-sm">Profile</NuxtLink>
|
v-if="!isLogin"
|
||||||
<NuxtLink v-if="!isLogin && isAdmin" to="/admin" class="btn btn-ghost btn-sm">Admin</NuxtLink>
|
to="/"
|
||||||
|
class="btn btn-ghost btn-sm gap-1.5 transition-all duration-200 hover:scale-105"
|
||||||
|
:class="route.path === '/' ? 'btn-primary' : ''"
|
||||||
|
>
|
||||||
|
<icons-icon-map />
|
||||||
|
Map
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink
|
||||||
|
v-if="!isLogin"
|
||||||
|
to="/profile"
|
||||||
|
class="btn btn-ghost btn-sm gap-1.5 transition-all duration-200 hover:scale-105"
|
||||||
|
:class="route.path === '/profile' ? 'btn-primary' : ''"
|
||||||
|
>
|
||||||
|
<icons-icon-user />
|
||||||
|
Profile
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink
|
||||||
|
v-if="!isLogin && isAdmin"
|
||||||
|
to="/admin"
|
||||||
|
class="btn btn-ghost btn-sm gap-1.5 transition-all duration-200 hover:scale-105"
|
||||||
|
:class="route.path.startsWith('/admin') ? 'btn-primary' : ''"
|
||||||
|
>
|
||||||
|
<icons-icon-shield />
|
||||||
|
Admin
|
||||||
|
</NuxtLink>
|
||||||
<button
|
<button
|
||||||
v-if="!isLogin && me"
|
v-if="!isLogin && me"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-ghost btn-sm btn-outline"
|
class="btn btn-ghost btn-sm btn-error btn-outline gap-1.5 transition-all duration-200 hover:scale-105"
|
||||||
@click="doLogout"
|
@click="doLogout"
|
||||||
>
|
>
|
||||||
|
<icons-icon-logout />
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
<label class="swap swap-rotate btn btn-ghost btn-sm">
|
<label class="swap swap-rotate btn btn-ghost btn-sm transition-all duration-200 hover:scale-105">
|
||||||
<input type="checkbox" v-model="dark" @change="toggleTheme" />
|
<input type="checkbox" v-model="dark" @change="toggleTheme" />
|
||||||
<span class="swap-off">☀️</span>
|
<span class="swap-off"><icons-icon-sun /></span>
|
||||||
<span class="swap-on">🌙</span>
|
<span class="swap-on"><icons-icon-moon /></span>
|
||||||
</label>
|
</label>
|
||||||
<span v-if="live" class="badge badge-success badge-sm">Live</span>
|
<span v-if="live" class="badge badge-success badge-sm">Live</span>
|
||||||
</header>
|
</header>
|
||||||
<main class="flex-1 min-h-0 relative">
|
<main class="flex-1 min-h-0 overflow-y-auto relative">
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ export const GridCoordLayer = L.GridLayer.extend({
|
|||||||
const scaleFactor = Math.pow(2, HnHMaxZoom - coords.z)
|
const scaleFactor = Math.pow(2, HnHMaxZoom - coords.z)
|
||||||
const topLeft = { x: coords.x * scaleFactor, y: coords.y * scaleFactor }
|
const topLeft = { x: coords.x * scaleFactor, y: coords.y * scaleFactor }
|
||||||
const bottomRight = { x: topLeft.x + scaleFactor - 1, y: topLeft.y + scaleFactor - 1 }
|
const bottomRight = { x: topLeft.x + scaleFactor - 1, y: topLeft.y + scaleFactor - 1 }
|
||||||
|
const swPoint = { x: topLeft.x * TileSize, y: topLeft.y * TileSize }
|
||||||
|
const tileWidthPx = scaleFactor * TileSize
|
||||||
|
const tileHeightPx = scaleFactor * TileSize
|
||||||
|
|
||||||
if (scaleFactor > GRID_COORD_SCALE_FACTOR_THRESHOLD) {
|
if (scaleFactor > GRID_COORD_SCALE_FACTOR_THRESHOLD) {
|
||||||
// Low zoom: one label per tile to avoid hundreds of thousands of DOM nodes (Reset view freeze fix)
|
// Low zoom: one label per tile to avoid hundreds of thousands of DOM nodes (Reset view freeze fix)
|
||||||
@@ -45,8 +48,8 @@ export const GridCoordLayer = L.GridLayer.extend({
|
|||||||
|
|
||||||
for (let gx = topLeft.x; gx <= bottomRight.x; gx++) {
|
for (let gx = topLeft.x; gx <= bottomRight.x; gx++) {
|
||||||
for (let gy = topLeft.y; gy <= bottomRight.y; gy++) {
|
for (let gy = topLeft.y; gy <= bottomRight.y; gy++) {
|
||||||
const leftPx = ((gx - topLeft.x) / scaleFactor) * TileSize
|
const leftPx = tileWidthPx > 0 ? ((gx * TileSize - swPoint.x) / tileWidthPx) * TileSize : 0
|
||||||
const topPx = ((gy - topLeft.y) / scaleFactor) * TileSize
|
const topPx = tileHeightPx > 0 ? ((gy * TileSize - swPoint.y) / tileHeightPx) * TileSize : 0
|
||||||
const textElement = document.createElement('div')
|
const textElement = document.createElement('div')
|
||||||
textElement.classList.add('map-tile-text')
|
textElement.classList.add('map-tile-text')
|
||||||
textElement.textContent = `(${gx}, ${gy})`
|
textElement.textContent = `(${gx}, ${gy})`
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
import { viteUriGuard } from './plugins/vite-uri-guard'
|
import { viteUriGuard } from './vite/vite-uri-guard'
|
||||||
|
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: '2024-11-01',
|
compatibilityDate: '2024-11-01',
|
||||||
@@ -7,9 +7,15 @@ export default defineNuxtConfig({
|
|||||||
|
|
||||||
app: {
|
app: {
|
||||||
baseURL: '/map/',
|
baseURL: '/map/',
|
||||||
|
pageTransition: { name: 'page', mode: 'out-in' },
|
||||||
head: {
|
head: {
|
||||||
title: 'HnH Map',
|
title: 'HnH Map',
|
||||||
meta: [{ charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }],
|
meta: [{ charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }],
|
||||||
|
link: [
|
||||||
|
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
|
||||||
|
{ rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' },
|
||||||
|
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -4,49 +4,68 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="message.text"
|
v-if="message.text"
|
||||||
class="mb-4 rounded-lg px-4 py-2"
|
class="mb-4 rounded-lg px-4 py-2 transition-all duration-300"
|
||||||
:class="message.type === 'error' ? 'bg-error/20 text-error' : 'bg-success/20 text-success'"
|
:class="message.type === 'error' ? 'bg-error/20 text-error' : 'bg-success/20 text-success'"
|
||||||
role="alert"
|
role="alert"
|
||||||
>
|
>
|
||||||
{{ message.text }}
|
{{ message.text }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card bg-base-200 shadow-xl mb-6">
|
<div class="card bg-base-200 shadow-xl mb-6 transition-all duration-200">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title">Users</h2>
|
<h2 class="card-title gap-2">
|
||||||
<ul class="space-y-2">
|
<icons-icon-users />
|
||||||
<li
|
Users
|
||||||
|
</h2>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div
|
||||||
v-for="u in users"
|
v-for="u in users"
|
||||||
:key="u"
|
:key="u"
|
||||||
class="flex justify-between items-center gap-3 py-1 border-b border-base-300 last:border-0"
|
class="flex items-center justify-between gap-3 w-full p-3 rounded-lg bg-base-300/50 hover:bg-base-300/70 transition-colors"
|
||||||
>
|
>
|
||||||
<span>{{ u }}</span>
|
<div class="flex items-center gap-2 min-w-0">
|
||||||
<NuxtLink :to="`/admin/users/${u}`" class="btn btn-ghost btn-xs">Edit</NuxtLink>
|
<div class="avatar placeholder">
|
||||||
</li>
|
<div class="bg-neutral text-neutral-content rounded-full w-8">
|
||||||
<li v-if="!users.length" class="py-1 text-base-content/60">
|
<span class="text-xs">{{ u[0]?.toUpperCase() }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="font-medium">{{ u }}</span>
|
||||||
|
</div>
|
||||||
|
<NuxtLink :to="`/admin/users/${u}`" class="btn btn-outline btn-sm gap-1 shrink-0">
|
||||||
|
<icons-icon-pencil /> Edit
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
<div v-if="!users.length" class="py-6 text-center text-base-content/60 rounded-lg bg-base-300/30">
|
||||||
No users yet.
|
No users yet.
|
||||||
</li>
|
</div>
|
||||||
</ul>
|
<NuxtLink to="/admin/users/new" class="btn btn-primary btn-sm mt-2 gap-1">
|
||||||
<NuxtLink to="/admin/users/new" class="btn btn-primary btn-sm mt-2">Add user</NuxtLink>
|
<icons-icon-plus /> Add user
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card bg-base-200 shadow-xl mb-6">
|
<div class="card bg-base-200 shadow-xl mb-6 transition-all duration-200">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title">Maps</h2>
|
<h2 class="card-title gap-2">
|
||||||
|
<icons-icon-map-pin />
|
||||||
|
Maps
|
||||||
|
</h2>
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="table table-sm">
|
<table class="table table-sm table-zebra">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>ID</th><th>Name</th><th>Hidden</th><th>Priority</th><th></th></tr>
|
<tr><th>ID</th><th>Name</th><th>Hidden</th><th>Priority</th><th class="text-right"></th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="map in maps" :key="map.ID">
|
<tr v-for="map in maps" :key="map.ID" class="hover">
|
||||||
<td>{{ map.ID }}</td>
|
<td>{{ map.ID }}</td>
|
||||||
<td>{{ map.Name }}</td>
|
<td>{{ map.Name }}</td>
|
||||||
<td>{{ map.Hidden ? 'Yes' : 'No' }}</td>
|
<td>{{ map.Hidden ? 'Yes' : 'No' }}</td>
|
||||||
<td>{{ map.Priority ? 'Yes' : 'No' }}</td>
|
<td>{{ map.Priority ? 'Yes' : 'No' }}</td>
|
||||||
<td>
|
<td class="text-right">
|
||||||
<NuxtLink :to="`/admin/maps/${map.ID}`" class="btn btn-ghost btn-xs">Edit</NuxtLink>
|
<NuxtLink :to="`/admin/maps/${map.ID}`" class="btn btn-outline btn-sm gap-1">
|
||||||
|
<icons-icon-pencil /> Edit
|
||||||
|
</NuxtLink>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="!maps.length">
|
<tr v-if="!maps.length">
|
||||||
@@ -58,9 +77,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card bg-base-200 shadow-xl mb-6">
|
<div class="card bg-base-200 shadow-xl mb-6 transition-all duration-200">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title">Settings</h2>
|
<h2 class="card-title gap-2">
|
||||||
|
<icons-icon-settings />
|
||||||
|
Settings
|
||||||
|
</h2>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<div class="form-control w-full max-w-xs">
|
<div class="form-control w-full max-w-xs">
|
||||||
<label class="label" for="admin-settings-prefix">Prefix</label>
|
<label class="label" for="admin-settings-prefix">Prefix</label>
|
||||||
@@ -93,16 +115,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end mt-2">
|
<div class="flex justify-end mt-2">
|
||||||
<button class="btn btn-sm" :disabled="savingSettings" @click="saveSettings">
|
<button class="btn btn-primary btn-sm" :disabled="savingSettings" @click="saveSettings">
|
||||||
{{ savingSettings ? '…' : 'Save settings' }}
|
<span v-if="savingSettings" class="loading loading-spinner loading-sm" />
|
||||||
|
<span v-else>Save settings</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card bg-base-200 shadow-xl mb-6">
|
<div class="card bg-base-200 shadow-xl mb-6 transition-all duration-200">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title">Actions</h2>
|
<h2 class="card-title gap-2">
|
||||||
|
<icons-icon-alert-triangle />
|
||||||
|
Actions
|
||||||
|
</h2>
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<div>
|
<div>
|
||||||
<a :href="api.adminExportUrl()" target="_blank" rel="noopener" class="btn btn-sm">
|
<a :href="api.adminExportUrl()" target="_blank" rel="noopener" class="btn btn-sm">
|
||||||
@@ -111,7 +137,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button class="btn btn-sm" :disabled="rebuilding" @click="rebuildZooms">
|
<button class="btn btn-sm" :disabled="rebuilding" @click="rebuildZooms">
|
||||||
{{ rebuilding ? '…' : 'Rebuild zooms' }}
|
<span v-if="rebuilding" class="loading loading-spinner loading-sm" />
|
||||||
|
<span v-else>Rebuild zooms</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
@@ -126,14 +153,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<form @submit.prevent="doMerge">
|
<form @submit.prevent="doMerge">
|
||||||
<button type="submit" class="btn btn-sm btn-primary" :disabled="!mergeFile || merging">
|
<button type="submit" class="btn btn-sm btn-primary" :disabled="!mergeFile || merging">
|
||||||
{{ merging ? '…' : 'Merge' }}
|
<span v-if="merging" class="loading loading-spinner loading-sm" />
|
||||||
|
<span v-else>Merge</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="border-t border-base-300 pt-4 mt-1">
|
<div class="border-t border-red-500/30 pt-4 mt-1 bg-error/5 rounded-lg p-3 -mx-1">
|
||||||
<p class="text-sm font-medium text-error/90 mb-2">Danger zone</p>
|
<p class="text-sm font-medium text-error/90 mb-2">Danger zone</p>
|
||||||
<button class="btn btn-sm btn-error" :disabled="wiping" @click="confirmWipe">
|
<button class="btn btn-sm btn-error" :disabled="wiping" @click="confirmWipe">
|
||||||
{{ wiping ? '…' : 'Wipe all data' }}
|
<span v-if="wiping" class="loading loading-spinner loading-sm" />
|
||||||
|
<span v-else>Wipe all data</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,7 +21,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<p v-if="error" class="text-error text-sm">{{ error }}</p>
|
<p v-if="error" class="text-error text-sm">{{ error }}</p>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button type="submit" class="btn btn-primary" :disabled="loading">{{ loading ? '…' : 'Save' }}</button>
|
<button type="submit" class="btn btn-primary" :disabled="loading">
|
||||||
|
<span v-if="loading" class="loading loading-spinner loading-sm" />
|
||||||
|
<span v-else>Save</span>
|
||||||
|
</button>
|
||||||
<NuxtLink to="/admin" class="btn btn-ghost">Back</NuxtLink>
|
<NuxtLink to="/admin" class="btn btn-ghost">Back</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -30,7 +30,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<p v-if="error" class="text-error text-sm">{{ error }}</p>
|
<p v-if="error" class="text-error text-sm">{{ error }}</p>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button type="submit" class="btn btn-primary" :disabled="loading">{{ loading ? '…' : 'Save' }}</button>
|
<button type="submit" class="btn btn-primary" :disabled="loading">
|
||||||
|
<span v-if="loading" class="loading loading-spinner loading-sm" />
|
||||||
|
<span v-else>Save</span>
|
||||||
|
</button>
|
||||||
<NuxtLink to="/admin" class="btn btn-ghost">Back</NuxtLink>
|
<NuxtLink to="/admin" class="btn btn-ghost">Back</NuxtLink>
|
||||||
<button
|
<button
|
||||||
v-if="!isNew"
|
v-if="!isNew"
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen flex flex-col items-center justify-center bg-base-200 p-4 overflow-hidden">
|
<div class="min-h-screen flex flex-col items-center justify-center bg-gradient-to-br from-base-200 via-base-300 to-primary/10 p-4 overflow-hidden">
|
||||||
<div class="card w-full max-w-sm bg-base-100 shadow-xl">
|
<div class="card w-full max-w-sm bg-base-100 shadow-2xl ring-1 ring-base-300 login-card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h1 class="card-title justify-center">Log in</h1>
|
<h1 class="card-title justify-center text-2xl">HnH Map</h1>
|
||||||
|
<p class="text-center text-base-content/70 text-sm">Log in to continue</p>
|
||||||
<form @submit.prevent="submit" class="flex flex-col gap-4">
|
<form @submit.prevent="submit" class="flex flex-col gap-4">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="user">User</label>
|
<label class="label" for="user">User</label>
|
||||||
@@ -22,8 +23,9 @@
|
|||||||
autocomplete="current-password"
|
autocomplete="current-password"
|
||||||
/>
|
/>
|
||||||
<p v-if="error" class="text-error text-sm">{{ error }}</p>
|
<p v-if="error" class="text-error text-sm">{{ error }}</p>
|
||||||
<button type="submit" class="btn btn-primary" :disabled="loading">
|
<button type="submit" class="btn btn-primary transition-all duration-200 hover:scale-[1.02]" :disabled="loading">
|
||||||
{{ loading ? '…' : 'Log in' }}
|
<span v-if="loading" class="loading loading-spinner loading-sm" />
|
||||||
|
<span v-else>Log in</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,9 +2,12 @@
|
|||||||
<div class="container mx-auto p-4 max-w-2xl">
|
<div class="container mx-auto p-4 max-w-2xl">
|
||||||
<h1 class="text-2xl font-bold mb-6">Profile</h1>
|
<h1 class="text-2xl font-bold mb-6">Profile</h1>
|
||||||
|
|
||||||
<div class="card bg-base-200 shadow-xl mb-6">
|
<div class="card bg-base-200 shadow-xl mb-6 transition-all duration-200">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title">Upload tokens</h2>
|
<h2 class="card-title gap-2">
|
||||||
|
<icons-icon-key />
|
||||||
|
Upload tokens
|
||||||
|
</h2>
|
||||||
<p class="text-sm opacity-80">Tokens for upload API. Generate and copy as needed.</p>
|
<p class="text-sm opacity-80">Tokens for upload API. Generate and copy as needed.</p>
|
||||||
<ul v-if="tokens?.length" class="list-disc list-inside mt-2 space-y-1">
|
<ul v-if="tokens?.length" class="list-disc list-inside mt-2 space-y-1">
|
||||||
<li v-for="t in tokens" :key="t" class="font-mono text-sm flex items-center gap-2">
|
<li v-for="t in tokens" :key="t" class="font-mono text-sm flex items-center gap-2">
|
||||||
@@ -21,15 +24,18 @@
|
|||||||
</ul>
|
</ul>
|
||||||
<p v-else class="text-sm mt-2">No tokens yet.</p>
|
<p v-else class="text-sm mt-2">No tokens yet.</p>
|
||||||
<p v-if="tokenError" class="text-error text-sm mt-2">{{ tokenError }}</p>
|
<p v-if="tokenError" class="text-error text-sm mt-2">{{ tokenError }}</p>
|
||||||
<button class="btn btn-primary btn-sm mt-2" :disabled="loadingTokens" @click="generateToken">
|
<button class="btn btn-primary btn-sm mt-2 transition-all duration-200 hover:scale-[1.02]" :disabled="loadingTokens" @click="generateToken">
|
||||||
{{ loadingTokens ? '…' : 'Generate token' }}
|
{{ loadingTokens ? '…' : 'Generate token' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card bg-base-200 shadow-xl mb-6">
|
<div class="card bg-base-200 shadow-xl mb-6 transition-all duration-200">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title">Change password</h2>
|
<h2 class="card-title gap-2">
|
||||||
|
<icons-icon-settings />
|
||||||
|
Change password
|
||||||
|
</h2>
|
||||||
<form @submit.prevent="changePass" class="flex flex-col gap-2">
|
<form @submit.prevent="changePass" class="flex flex-col gap-2">
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
v-model="newPass"
|
v-model="newPass"
|
||||||
@@ -37,7 +43,7 @@
|
|||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
/>
|
/>
|
||||||
<p v-if="passMsg" class="text-sm" :class="passOk ? 'text-success' : 'text-error'">{{ passMsg }}</p>
|
<p v-if="passMsg" class="text-sm" :class="passOk ? 'text-success' : 'text-error'">{{ passMsg }}</p>
|
||||||
<button type="submit" class="btn btn-sm" :disabled="loadingPass">Save password</button>
|
<button type="submit" class="btn btn-sm transition-all duration-200 hover:scale-[1.02]" :disabled="loadingPass">Save password</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
/>
|
/>
|
||||||
<p v-if="error" class="text-error text-sm">{{ error }}</p>
|
<p v-if="error" class="text-error text-sm">{{ error }}</p>
|
||||||
<button type="submit" class="btn btn-primary" :disabled="loading">
|
<button type="submit" class="btn btn-primary" :disabled="loading">
|
||||||
{{ loading ? '…' : 'Create and log in' }}
|
<span v-if="loading" class="loading loading-spinner loading-sm" />
|
||||||
|
<span v-else>Create and log in</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,8 +8,36 @@ module.exports = {
|
|||||||
'./app.vue',
|
'./app.vue',
|
||||||
'./lib/**/*.js',
|
'./lib/**/*.js',
|
||||||
],
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Inter', 'system-ui', 'sans-serif'],
|
||||||
|
mono: ['JetBrains Mono', 'ui-monospace', 'monospace'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
plugins: [require('daisyui')],
|
plugins: [require('daisyui')],
|
||||||
daisyui: {
|
daisyui: {
|
||||||
themes: ['light', 'dark'],
|
themes: [
|
||||||
|
'light',
|
||||||
|
{
|
||||||
|
dark: {
|
||||||
|
'color-scheme': 'dark',
|
||||||
|
primary: '#6366f1',
|
||||||
|
'primary-content': '#ffffff',
|
||||||
|
secondary: '#8b5cf6',
|
||||||
|
'secondary-content': '#ffffff',
|
||||||
|
accent: '#06b6d4',
|
||||||
|
'accent-content': '#ffffff',
|
||||||
|
neutral: '#2a323c',
|
||||||
|
'neutral-focus': '#242b33',
|
||||||
|
'neutral-content': '#A6ADBB',
|
||||||
|
'base-100': '#1d232a',
|
||||||
|
'base-200': '#191e24',
|
||||||
|
'base-300': '#15191e',
|
||||||
|
'base-content': '#A6ADBB',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user