feat: Add loot builder frontend — Pinia store, views, 4 components, router + nav
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
Implements the complete frontend for BetterLoot profile management: - Pinia store (loot.ts) with CRUD, import/export, apply-to-server actions - LootBuilderView orchestrator with profile bar, modals, two-column layout - LootContainerSidebar with categorized container list, search, config indicators - LootItemEditor for per-container item settings and ungrouped item table - LootItemPicker modal with searchable/filterable Rust item grid - LootGroupEditor for reusable loot group management - Router integration at /loot-builder - Sidebar nav item with Crosshair icon and loot.view permission Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
179
frontend/src/stores/loot.ts
Normal file
179
frontend/src/stores/loot.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useApi } from '@/composables/useApi'
|
||||
import { useToastStore } from '@/stores/toast'
|
||||
import type { LootProfileSummary, LootProfileFull, LootApplyResult } from '@/types'
|
||||
|
||||
export const useLootStore = defineStore('loot', () => {
|
||||
const profiles = ref<LootProfileSummary[]>([])
|
||||
const currentProfile = ref<LootProfileFull | null>(null)
|
||||
const selectedContainer = ref<string | null>(null)
|
||||
const isLoading = ref(false)
|
||||
const isSaving = ref(false)
|
||||
const isApplying = ref(false)
|
||||
const isDirty = ref(false)
|
||||
const api = useApi()
|
||||
const toast = useToastStore()
|
||||
|
||||
const activeProfile = computed(() => profiles.value.find(p => p.is_active) || null)
|
||||
|
||||
async function fetchProfiles() {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const res = await api.get<{ profiles: LootProfileSummary[] }>('/loot/profiles')
|
||||
profiles.value = res.profiles
|
||||
} catch (err) {
|
||||
toast.error((err as Error).message)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProfile(id: string) {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const res = await api.get<{ profile: LootProfileFull }>(`/loot/profiles/${id}`)
|
||||
currentProfile.value = res.profile
|
||||
isDirty.value = false
|
||||
// Select first container if none selected
|
||||
if (!selectedContainer.value && currentProfile.value.loot_table) {
|
||||
const keys = Object.keys(currentProfile.value.loot_table)
|
||||
if (keys.length > 0) selectedContainer.value = keys[0]
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error((err as Error).message)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createProfile(name: string, description?: string) {
|
||||
try {
|
||||
const res = await api.post<{ profile: LootProfileFull }>('/loot/profiles', {
|
||||
profile_name: name,
|
||||
description,
|
||||
})
|
||||
await fetchProfiles()
|
||||
currentProfile.value = res.profile
|
||||
isDirty.value = false
|
||||
toast.success(`Profile "${name}" created`)
|
||||
return res.profile
|
||||
} catch (err) {
|
||||
toast.error((err as Error).message)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function saveCurrentProfile() {
|
||||
if (!currentProfile.value) return
|
||||
isSaving.value = true
|
||||
try {
|
||||
await api.put(`/loot/profiles/${currentProfile.value.id}`, {
|
||||
profile_name: currentProfile.value.profile_name,
|
||||
description: currentProfile.value.description,
|
||||
loot_table: currentProfile.value.loot_table,
|
||||
loot_groups: currentProfile.value.loot_groups,
|
||||
})
|
||||
isDirty.value = false
|
||||
await fetchProfiles()
|
||||
toast.success('Profile saved')
|
||||
} catch (err) {
|
||||
toast.error((err as Error).message)
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteProfile(id: string) {
|
||||
try {
|
||||
await api.del(`/loot/profiles/${id}`)
|
||||
if (currentProfile.value?.id === id) {
|
||||
currentProfile.value = null
|
||||
selectedContainer.value = null
|
||||
}
|
||||
await fetchProfiles()
|
||||
toast.success('Profile deleted')
|
||||
} catch (err) {
|
||||
toast.error((err as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
async function duplicateProfile(id: string) {
|
||||
try {
|
||||
const res = await api.post<{ profile: LootProfileFull }>(`/loot/profiles/${id}/duplicate`)
|
||||
await fetchProfiles()
|
||||
toast.success(`Profile duplicated as "${res.profile.profile_name}"`)
|
||||
return res.profile
|
||||
} catch (err) {
|
||||
toast.error((err as Error).message)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function applyToServer(id: string, multiplier: number) {
|
||||
isApplying.value = true
|
||||
try {
|
||||
const res = await api.post<LootApplyResult>(`/loot/profiles/${id}/apply`, { multiplier })
|
||||
await fetchProfiles()
|
||||
toast.success(res.message)
|
||||
return res
|
||||
} catch (err) {
|
||||
toast.error((err as Error).message)
|
||||
return null
|
||||
} finally {
|
||||
isApplying.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function importProfile(name: string, lootTable: Record<string, any>, lootGroups?: Record<string, any>) {
|
||||
try {
|
||||
const res = await api.post<{ profile: LootProfileFull }>('/loot/import', {
|
||||
profile_name: name,
|
||||
loot_table: lootTable,
|
||||
loot_groups: lootGroups || {},
|
||||
})
|
||||
await fetchProfiles()
|
||||
toast.success(`Profile "${name}" imported`)
|
||||
return res.profile
|
||||
} catch (err) {
|
||||
toast.error((err as Error).message)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function exportProfile(id: string, multiplier: number) {
|
||||
try {
|
||||
return await api.get<{ profile_name: string; multiplier: number; loot_table: any; loot_groups: any }>(
|
||||
`/loot/export/${id}?multiplier=${multiplier}`,
|
||||
)
|
||||
} catch (err) {
|
||||
toast.error((err as Error).message)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function markDirty() {
|
||||
isDirty.value = true
|
||||
}
|
||||
|
||||
return {
|
||||
profiles,
|
||||
currentProfile,
|
||||
selectedContainer,
|
||||
isLoading,
|
||||
isSaving,
|
||||
isApplying,
|
||||
isDirty,
|
||||
activeProfile,
|
||||
fetchProfiles,
|
||||
loadProfile,
|
||||
createProfile,
|
||||
saveCurrentProfile,
|
||||
deleteProfile,
|
||||
duplicateProfile,
|
||||
applyToServer,
|
||||
importProfile,
|
||||
exportProfile,
|
||||
markDirty,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user