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:
103
frontend/src/components/loot/LootContainerSidebar.vue
Normal file
103
frontend/src/components/loot/LootContainerSidebar.vue
Normal 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>
|
||||
Reference in New Issue
Block a user