Enhance frontend components and introduce new features

- Added a custom light theme in app.css to match the dark theme's palette.
- Introduced AdminBreadcrumbs component for improved navigation in admin pages.
- Implemented Skeleton component for loading states in various views.
- Added ToastContainer for displaying notifications and alerts.
- Enhanced MapView with loading indicators and improved marker handling.
- Updated MapCoordsDisplay to allow copying of shareable links.
- Refactored MapControls and MapContextMenu for better usability.
- Improved user experience in profile and admin pages with loading states and search functionality.
This commit is contained in:
2026-03-01 15:19:55 +03:00
parent 6529d7370e
commit 2bd2c8dbca
15 changed files with 817 additions and 212 deletions

View File

@@ -26,6 +26,10 @@ export interface MapLayersOptions {
getTrackingCharacterId: () => number
setTrackingCharacterId: (id: number) => void
onMarkerContextMenu: (clientX: number, clientY: number, id: number, name: string) => void
/** Resolves relative marker icon path to absolute URL. If omitted, relative paths are used. */
resolveIconUrl?: (path: string) => string
/** Fallback icon URL when a marker image fails to load. */
fallbackIconUrl?: string
}
export interface MapLayersManager {
@@ -55,6 +59,8 @@ export function createMapLayers(options: MapLayersOptions): MapLayersManager {
getTrackingCharacterId,
setTrackingCharacterId,
onMarkerContextMenu,
resolveIconUrl,
fallbackIconUrl,
} = options
const markers = createUniqueList<MapMarker>()
@@ -93,9 +99,13 @@ export function createMapLayers(options: MapLayersOptions): MapLayersManager {
function updateMarkers(markersData: ApiMarker[]) {
const list = Array.isArray(markersData) ? markersData : []
const ctx = markerCtx()
const iconOptions =
resolveIconUrl != null
? { resolveIconUrl, fallbackIconUrl }
: undefined
uniqueListUpdate(
markers,
list.map((it) => createMarker(it as MarkerData)),
list.map((it) => createMarker(it as MarkerData, iconOptions)),
(marker: MapMarker) => {
if (marker.map === getCurrentMapId() || marker.map === overlayLayer.map) marker.add(ctx)
marker.setClickCallback(() => {

View File

@@ -0,0 +1,36 @@
export type ToastType = 'success' | 'error' | 'info'
export interface Toast {
id: number
type: ToastType
text: string
}
const TOAST_STATE_KEY = 'hnh-map-toasts'
const DEFAULT_DURATION_MS = 4000
let nextId = 0
export function useToast() {
const toasts = useState<Toast[]>(TOAST_STATE_KEY, () => [])
function dismiss(id: number) {
toasts.value = toasts.value.filter((t) => t.id !== id)
}
function show(type: ToastType, text: string, durationMs = DEFAULT_DURATION_MS) {
const id = ++nextId
toasts.value = [...toasts.value, { id, type, text }]
if (durationMs > 0 && import.meta.client) {
setTimeout(() => dismiss(id), durationMs)
}
}
return {
toasts: readonly(toasts),
success: (text: string, durationMs?: number) => show('success', text, durationMs),
error: (text: string, durationMs?: number) => show('error', text, durationMs),
info: (text: string, durationMs?: number) => show('info', text, durationMs),
dismiss,
}
}