feat: Add loot builder frontend — Pinia store, views, 4 components, router + nav
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:
Vantz Stockwell
2026-02-22 00:27:46 -05:00
parent eb57c51a24
commit 9d28fdfb65
8 changed files with 1186 additions and 0 deletions

View File

@@ -0,0 +1,103 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { rustContainers, containerCategories } from '@/data/rust-containers'
import { Search, Box, Cylinder, Shield, Users, HelpCircle } from 'lucide-vue-next'
const props = defineProps<{
lootTable: Record<string, any>
selected: string | null
}>()
const emit = defineEmits<{
select: [prefab: string]
}>()
const searchQuery = ref('')
const categoryIcons: Record<string, any> = {
crates: Box,
barrels: Cylinder,
military: Shield,
npcs: Users,
other: HelpCircle,
}
const categoryLabels: Record<string, string> = {
crates: 'CRATES',
barrels: 'BARRELS',
military: 'MILITARY',
npcs: 'NPCs',
other: 'OTHER',
}
const filteredContainers = computed(() => {
const q = searchQuery.value.toLowerCase()
if (!q) return rustContainers
return rustContainers.filter(c => c.name.toLowerCase().includes(q) || c.prefab.toLowerCase().includes(q))
})
const groupedContainers = computed(() => {
const groups: Record<string, typeof rustContainers> = {}
for (const cat of containerCategories) {
const items = filteredContainers.value.filter(c => c.category === cat)
if (items.length > 0) groups[cat] = items
}
return groups
})
function isConfigured(prefab: string): boolean {
const entry = props.lootTable[prefab]
if (!entry) return false
const hasItems = entry.UngroupedItems && Object.keys(entry.UngroupedItems).length > 0
const hasGuaranteed = entry.GuaranteedItems && Object.keys(entry.GuaranteedItems).length > 0
const hasProfiles = entry.LootProfiles && entry.LootProfiles.length > 0
return hasItems || hasGuaranteed || hasProfiles
}
</script>
<template>
<div class="w-64 shrink-0 bg-neutral-900 border border-neutral-800 rounded-xl overflow-hidden flex flex-col">
<!-- Search -->
<div class="p-3 border-b border-neutral-800">
<div class="relative">
<Search class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-neutral-500" />
<input
v-model="searchQuery"
placeholder="Search containers..."
class="w-full bg-neutral-800 border border-neutral-700 rounded-lg pl-9 pr-3 py-2 text-sm text-neutral-200 placeholder-neutral-500"
/>
</div>
</div>
<!-- Container List -->
<div class="flex-1 overflow-y-auto py-2">
<template v-for="(containers, category) in groupedContainers" :key="category">
<div class="px-3 pt-3 pb-1">
<div class="flex items-center gap-2 text-[10px] font-semibold uppercase tracking-widest text-neutral-500">
<component :is="categoryIcons[category]" class="w-3 h-3" />
{{ categoryLabels[category] || category }}
</div>
</div>
<button
v-for="c in containers"
:key="c.prefab"
@click="emit('select', c.prefab)"
class="w-full text-left px-3 py-1.5 text-sm flex items-center gap-2 transition-colors"
:class="selected === c.prefab
? 'bg-oxide-500/10 text-oxide-400'
: 'text-neutral-400 hover:bg-neutral-800 hover:text-neutral-200'"
>
<span class="truncate flex-1">{{ c.name }}</span>
<span
v-if="isConfigured(c.prefab)"
class="w-1.5 h-1.5 rounded-full bg-oxide-500 shrink-0"
/>
</button>
</template>
<div v-if="Object.keys(groupedContainers).length === 0" class="px-3 py-6 text-center text-neutral-500 text-sm">
No containers match
</div>
</div>
</div>
</template>