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:
79
frontend-nuxt/composables/__tests__/useMapBookmarks.test.ts
Normal file
79
frontend-nuxt/composables/__tests__/useMapBookmarks.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
49
frontend-nuxt/composables/__tests__/useToast.test.ts
Normal file
49
frontend-nuxt/composables/__tests__/useToast.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
24
frontend-nuxt/composables/useFormSubmit.ts
Normal file
24
frontend-nuxt/composables/useFormSubmit.ts
Normal 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 }
|
||||
}
|
||||
Reference in New Issue
Block a user