Refactor frontend components and enhance API integration

- Updated frontend-nuxt.mdc to specify usage of composables for API calls.
- Added new AuthCard and ConfirmModal components for improved UI consistency.
- Introduced UserAvatar component for user profile display, replacing previous Gravatar implementation.
- Implemented useFormSubmit composable for handling form submissions with loading and error states.
- Enhanced vitest.config.ts to include coverage reporting for composables and components.
- Removed deprecated useAdminApi and useAuth composables to streamline API interactions.
- Updated login and setup pages to utilize new components and composables for better user experience.
This commit is contained in:
2026-03-04 00:14:05 +03:00
parent f6375e7d0f
commit 8f769543f4
34 changed files with 878 additions and 379 deletions

View File

@@ -0,0 +1,79 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { ref } from 'vue'
const stateByKey: Record<string, ReturnType<typeof ref>> = {}
const useStateMock = vi.fn((key: string, init: () => unknown) => {
if (!stateByKey[key]) {
stateByKey[key] = ref(init())
}
return stateByKey[key]
})
vi.stubGlobal('useState', useStateMock)
vi.stubGlobal('onMounted', vi.fn())
const storage: Record<string, string> = {}
const localStorageMock = {
getItem: vi.fn((key: string) => storage[key] ?? null),
setItem: vi.fn((key: string, value: string) => {
storage[key] = value
}),
clear: vi.fn(() => {
for (const k of Object.keys(storage)) delete storage[k]
}),
}
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'] = '[]'
stateByKey['hnh-map-bookmarks-state'] = ref([])
localStorageMock.getItem.mockImplementation((key: string) => storage[key] ?? null)
localStorageMock.setItem.mockImplementation((key: string, value: string) => {
storage[key] = value
})
})
it('add adds a bookmark and refresh updates state', () => {
const { bookmarks, add, refresh } = useMapBookmarks()
refresh()
expect(bookmarks.value).toEqual([])
const id = add({ name: 'Home', mapId: 1, x: 0, y: 0 })
expect(id).toBeDefined()
expect(bookmarks.value).toHaveLength(1)
expect(bookmarks.value[0].name).toBe('Home')
expect(bookmarks.value[0].mapId).toBe(1)
expect(bookmarks.value[0].id).toBe(id)
})
it('update changes name', () => {
const { bookmarks, add, update, refresh } = useMapBookmarks()
refresh()
const id = add({ name: 'Old', mapId: 1, x: 0, y: 0 })
const ok = update(id, { name: 'New' })
expect(ok).toBe(true)
expect(bookmarks.value[0].name).toBe('New')
})
it('remove deletes bookmark', () => {
const { bookmarks, add, remove, refresh } = useMapBookmarks()
refresh()
const id = add({ name: 'X', mapId: 1, x: 0, y: 0 })
expect(bookmarks.value).toHaveLength(1)
remove(id)
expect(bookmarks.value).toHaveLength(0)
})
it('clear empties bookmarks', () => {
const { bookmarks, add, clear, refresh } = useMapBookmarks()
refresh()
add({ name: 'A', mapId: 1, x: 0, y: 0 })
expect(bookmarks.value).toHaveLength(1)
clear()
expect(bookmarks.value).toHaveLength(0)
})
})

View File

@@ -0,0 +1,49 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { ref } from 'vue'
const stateByKey: Record<string, ReturnType<typeof ref>> = {}
const useStateMock = vi.fn((key: string, init: () => unknown) => {
if (!stateByKey[key]) {
stateByKey[key] = ref(init())
}
return stateByKey[key]
})
vi.stubGlobal('useState', useStateMock)
import { useToast } from '../useToast'
describe('useToast', () => {
beforeEach(() => {
stateByKey['hnh-map-toasts'] = ref([])
})
it('exposes toasts and show/dismiss', () => {
const { toasts, success, dismiss } = useToast()
expect(toasts.value).toEqual([])
success('Done!')
expect(toasts.value).toHaveLength(1)
expect(toasts.value[0].type).toBe('success')
expect(toasts.value[0].text).toBe('Done!')
const id = toasts.value[0].id
dismiss(id)
expect(toasts.value).toHaveLength(0)
})
it('error and info set type', () => {
const { toasts, error, info } = useToast()
error('Failed')
expect(toasts.value[0].type).toBe('error')
info('Note')
expect(toasts.value[1].type).toBe('info')
})
it('each toast has unique id', () => {
const { toasts, success } = useToast()
success('A')
success('B')
const ids = toasts.value.map((t) => t.id)
expect(new Set(ids).size).toBe(2)
})
})

View File

@@ -1,23 +0,0 @@
/** Admin API composable. Uses useMapApi internally. */
export function useAdminApi() {
const api = useMapApi()
return {
adminUsers: api.adminUsers,
adminUserByName: api.adminUserByName,
adminUserPost: api.adminUserPost,
adminUserDelete: api.adminUserDelete,
adminSettings: api.adminSettings,
adminSettingsPost: api.adminSettingsPost,
adminMaps: api.adminMaps,
adminMapPost: api.adminMapPost,
adminMapToggleHidden: api.adminMapToggleHidden,
adminWipe: api.adminWipe,
adminRebuildZooms: api.adminRebuildZooms,
adminExportUrl: api.adminExportUrl,
adminMerge: api.adminMerge,
adminWipeTile: api.adminWipeTile,
adminSetCoords: api.adminSetCoords,
adminHideMarker: api.adminHideMarker,
}
}

View File

@@ -1,15 +0,0 @@
import type { MeResponse } from '~/types/api'
/** Auth composable: login, logout, me, OAuth, setup. Uses useMapApi internally. */
export function useAuth() {
const api = useMapApi()
return {
login: api.login,
logout: api.logout,
me: api.me,
oauthLoginUrl: api.oauthLoginUrl,
oauthProviders: api.oauthProviders,
setupRequired: api.setupRequired,
}
}

View File

@@ -0,0 +1,24 @@
/**
* Composable for form submit with loading and error state.
* Use run(fn) to execute an async action; loading and error are updated automatically.
*/
export function useFormSubmit(defaultError = 'Something went wrong') {
const loading = ref(false)
const error = ref('')
async function run<T>(fn: () => Promise<T>): Promise<T | undefined> {
error.value = ''
loading.value = true
try {
const result = await fn()
return result
} catch (e: unknown) {
error.value = e instanceof Error ? e.message : defaultError
return undefined
} finally {
loading.value = false
}
}
return { loading, error, run }
}