- AnalyticsView: avg_players, uptime_percentage - WipeAnalyticsView: success_rate, population curve, durations, CSV export - PlayerRetentionView: retention percentages, session duration, tooltip - MapAnalyticsView: rotation effectiveness, performance metrics, table All analytics views now use safe formatter utilities with optional chaining to prevent null/undefined runtime errors when displaying numeric data. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
696 lines
28 KiB
Vue
696 lines
28 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { useApi } from '@/composables/useApi'
|
|
import type { StoreCategory, StoreItem } from '@/types'
|
|
import { ShoppingBag, Plus, Trash2, RefreshCw, Edit2, DollarSign, X } from 'lucide-vue-next'
|
|
import { safeFixed } from '@/utils/formatters'
|
|
|
|
const api = useApi()
|
|
|
|
const tab = ref<'categories' | 'items'>('categories')
|
|
const isLoading = ref(false)
|
|
const categories = ref<StoreCategory[]>([])
|
|
const items = ref<StoreItem[]>([])
|
|
|
|
// Category modal state
|
|
const showCategoryModal = ref(false)
|
|
const editingCategory = ref<StoreCategory | null>(null)
|
|
const categoryForm = ref({
|
|
name: '',
|
|
slug: '',
|
|
description: '',
|
|
display_order: 0,
|
|
visible: true
|
|
})
|
|
|
|
// Item modal state
|
|
const showItemModal = ref(false)
|
|
const editingItem = ref<StoreItem | null>(null)
|
|
const itemForm = ref({
|
|
category_id: null as string | null,
|
|
name: '',
|
|
description: '',
|
|
price: 0,
|
|
image_url: '',
|
|
item_type: 'kit' as 'kit' | 'rank' | 'currency' | 'command',
|
|
delivery_commands: [''],
|
|
limit_per_player: null as number | null,
|
|
enabled: true
|
|
})
|
|
|
|
const itemTypes = [
|
|
{ value: 'kit', label: 'Kit', example: 'inventory.giveto {steam_id} rifle.ak 1' },
|
|
{ value: 'rank', label: 'Rank', example: 'oxide.usergroup add {steam_id} vip' },
|
|
{ value: 'currency', label: 'Currency', example: 'eco deposit {steam_id} 1000' },
|
|
{ value: 'command', label: 'Custom Command', example: 'yourplugin.givereward {steam_id}' }
|
|
]
|
|
|
|
function typeBadgeClass(type: string): string {
|
|
switch (type) {
|
|
case 'kit': return 'bg-blue-500/15 text-blue-400'
|
|
case 'rank': return 'bg-purple-500/15 text-purple-400'
|
|
case 'currency': return 'bg-yellow-500/15 text-yellow-400'
|
|
case 'command': return 'bg-oxide-500/15 text-oxide-400'
|
|
default: return 'bg-neutral-700/50 text-neutral-400'
|
|
}
|
|
}
|
|
|
|
function autoGenerateSlug(name: string): string {
|
|
return name
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9\s-]/g, '')
|
|
.replace(/\s+/g, '-')
|
|
.replace(/-+/g, '-')
|
|
.trim()
|
|
}
|
|
|
|
async function fetchCategories() {
|
|
isLoading.value = true
|
|
try {
|
|
const data = await api.get<StoreCategory[]>('/webstore/categories')
|
|
categories.value = data
|
|
} catch (error) {
|
|
console.error('Failed to fetch categories:', error)
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
async function fetchItems() {
|
|
isLoading.value = true
|
|
try {
|
|
const data = await api.get<StoreItem[]>('/webstore/items')
|
|
items.value = data
|
|
} catch (error) {
|
|
console.error('Failed to fetch items:', error)
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
function openCategoryModal(category?: StoreCategory) {
|
|
if (category) {
|
|
editingCategory.value = category
|
|
categoryForm.value = {
|
|
name: category.name,
|
|
slug: category.slug,
|
|
description: category.description || '',
|
|
display_order: category.display_order,
|
|
visible: category.visible
|
|
}
|
|
} else {
|
|
editingCategory.value = null
|
|
categoryForm.value = {
|
|
name: '',
|
|
slug: '',
|
|
description: '',
|
|
display_order: categories.value.length,
|
|
visible: true
|
|
}
|
|
}
|
|
showCategoryModal.value = true
|
|
}
|
|
|
|
function closeCategoryModal() {
|
|
showCategoryModal.value = false
|
|
editingCategory.value = null
|
|
}
|
|
|
|
async function saveCategory() {
|
|
try {
|
|
if (editingCategory.value) {
|
|
await api.put(`/webstore/categories/${editingCategory.value.id}`, categoryForm.value)
|
|
} else {
|
|
await api.post('/webstore/categories', categoryForm.value)
|
|
}
|
|
await fetchCategories()
|
|
closeCategoryModal()
|
|
} catch (error) {
|
|
console.error('Failed to save category:', error)
|
|
}
|
|
}
|
|
|
|
async function deleteCategory(category: StoreCategory) {
|
|
if (!confirm(`Delete category "${category.name}"? Items in this category will become uncategorized.`)) return
|
|
try {
|
|
await api.del(`/webstore/categories/${category.id}`)
|
|
await fetchCategories()
|
|
} catch (error) {
|
|
console.error('Failed to delete category:', error)
|
|
}
|
|
}
|
|
|
|
function openItemModal(item?: StoreItem) {
|
|
if (item) {
|
|
editingItem.value = item
|
|
itemForm.value = {
|
|
category_id: item.category_id,
|
|
name: item.name,
|
|
description: item.description || '',
|
|
price: item.price,
|
|
image_url: item.image_url || '',
|
|
item_type: item.item_type,
|
|
delivery_commands: item.delivery_commands.length > 0 ? item.delivery_commands : [''],
|
|
limit_per_player: item.limit_per_player,
|
|
enabled: item.enabled
|
|
}
|
|
} else {
|
|
editingItem.value = null
|
|
itemForm.value = {
|
|
category_id: null,
|
|
name: '',
|
|
description: '',
|
|
price: 0,
|
|
image_url: '',
|
|
item_type: 'kit',
|
|
delivery_commands: [''],
|
|
limit_per_player: null,
|
|
enabled: true
|
|
}
|
|
}
|
|
showItemModal.value = true
|
|
}
|
|
|
|
function closeItemModal() {
|
|
showItemModal.value = false
|
|
editingItem.value = null
|
|
}
|
|
|
|
function addCommand() {
|
|
itemForm.value.delivery_commands.push('')
|
|
}
|
|
|
|
function removeCommand(index: number) {
|
|
itemForm.value.delivery_commands.splice(index, 1)
|
|
if (itemForm.value.delivery_commands.length === 0) {
|
|
itemForm.value.delivery_commands.push('')
|
|
}
|
|
}
|
|
|
|
async function saveItem() {
|
|
// Validate
|
|
if (!itemForm.value.name.trim()) {
|
|
alert('Item name is required')
|
|
return
|
|
}
|
|
if (itemForm.value.price <= 0) {
|
|
alert('Price must be greater than 0')
|
|
return
|
|
}
|
|
const validCommands = itemForm.value.delivery_commands.filter(c => c.trim())
|
|
if (validCommands.length === 0) {
|
|
alert('At least one delivery command is required')
|
|
return
|
|
}
|
|
|
|
try {
|
|
const payload = {
|
|
...itemForm.value,
|
|
delivery_commands: validCommands
|
|
}
|
|
|
|
if (editingItem.value) {
|
|
await api.put(`/webstore/items/${editingItem.value.id}`, payload)
|
|
} else {
|
|
await api.post('/webstore/items', payload)
|
|
}
|
|
await fetchItems()
|
|
closeItemModal()
|
|
} catch (error) {
|
|
console.error('Failed to save item:', error)
|
|
}
|
|
}
|
|
|
|
async function deleteItem(item: StoreItem) {
|
|
if (!confirm(`Delete item "${item.name}"?`)) return
|
|
try {
|
|
await api.del(`/webstore/items/${item.id}`)
|
|
await fetchItems()
|
|
} catch (error) {
|
|
console.error('Failed to delete item:', error)
|
|
}
|
|
}
|
|
|
|
function getCategoryName(categoryId: string | null): string {
|
|
if (!categoryId) return 'Uncategorized'
|
|
const cat = categories.value.find(c => c.id === categoryId)
|
|
return cat?.name || 'Unknown'
|
|
}
|
|
|
|
const selectedTypeExample = computed(() => {
|
|
const type = itemTypes.find(t => t.value === itemForm.value.item_type)
|
|
return type?.example || ''
|
|
})
|
|
|
|
onMounted(() => {
|
|
fetchCategories()
|
|
fetchItems()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="p-6 space-y-6">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-3">
|
|
<ShoppingBag class="w-5 h-5 text-oxide-500" />
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-neutral-100">Store Items</h1>
|
|
<p class="text-sm text-neutral-500 mt-0.5">
|
|
{{ categories.length }} categories, {{ items.length }} items
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<button
|
|
@click="tab === 'categories' ? fetchCategories() : fetchItems()"
|
|
:disabled="isLoading"
|
|
class="flex items-center gap-2 px-3 py-2 text-sm text-neutral-400 hover:text-neutral-200 bg-neutral-800 hover:bg-neutral-700 rounded-lg transition-colors"
|
|
>
|
|
<RefreshCw class="w-4 h-4" :class="{ 'animate-spin': isLoading }" />
|
|
</button>
|
|
<button
|
|
v-if="tab === 'categories'"
|
|
@click="openCategoryModal()"
|
|
class="flex items-center gap-2 px-4 py-2 bg-oxide-600 hover:bg-oxide-700 text-white text-sm font-medium rounded-lg transition-colors"
|
|
>
|
|
<Plus class="w-4 h-4" />
|
|
Add Category
|
|
</button>
|
|
<button
|
|
v-else
|
|
@click="openItemModal()"
|
|
class="flex items-center gap-2 px-4 py-2 bg-oxide-600 hover:bg-oxide-700 text-white text-sm font-medium rounded-lg transition-colors"
|
|
>
|
|
<Plus class="w-4 h-4" />
|
|
Add Item
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
<div class="flex bg-neutral-800 rounded-lg border border-neutral-700 overflow-hidden w-fit">
|
|
<button
|
|
@click="tab = 'categories'"
|
|
class="px-4 py-2 text-sm font-medium transition-colors"
|
|
:class="tab === 'categories' ? 'bg-oxide-500/15 text-oxide-400' : 'text-neutral-400 hover:text-neutral-200'"
|
|
>
|
|
Categories
|
|
</button>
|
|
<button
|
|
@click="tab = 'items'"
|
|
class="px-4 py-2 text-sm font-medium transition-colors"
|
|
:class="tab === 'items' ? 'bg-oxide-500/15 text-oxide-400' : 'text-neutral-400 hover:text-neutral-200'"
|
|
>
|
|
Items
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Categories Tab -->
|
|
<div v-if="tab === 'categories'" class="bg-neutral-900 border border-neutral-800 rounded-lg overflow-hidden">
|
|
<table class="w-full">
|
|
<thead>
|
|
<tr class="border-b border-neutral-800 text-left">
|
|
<th class="px-4 py-3 text-xs font-medium text-neutral-500 uppercase tracking-wider">Name</th>
|
|
<th class="px-4 py-3 text-xs font-medium text-neutral-500 uppercase tracking-wider">Slug</th>
|
|
<th class="px-4 py-3 text-xs font-medium text-neutral-500 uppercase tracking-wider">Display Order</th>
|
|
<th class="px-4 py-3 text-xs font-medium text-neutral-500 uppercase tracking-wider">Visible</th>
|
|
<th class="px-4 py-3 text-xs font-medium text-neutral-500 uppercase tracking-wider text-right">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-neutral-800">
|
|
<tr v-if="categories.length === 0">
|
|
<td colspan="5" class="px-4 py-12 text-center text-neutral-500 text-sm">
|
|
<template v-if="isLoading">Loading categories...</template>
|
|
<template v-else>No categories yet. Add one to organize your store items.</template>
|
|
</td>
|
|
</tr>
|
|
<tr
|
|
v-for="category in categories"
|
|
:key="category.id"
|
|
class="hover:bg-neutral-800/50 transition-colors"
|
|
>
|
|
<td class="px-4 py-3">
|
|
<p class="text-sm font-medium text-neutral-100">{{ category.name }}</p>
|
|
<p v-if="category.description" class="text-xs text-neutral-500 truncate max-w-md">{{ category.description }}</p>
|
|
</td>
|
|
<td class="px-4 py-3 text-sm text-neutral-400 font-mono">{{ category.slug }}</td>
|
|
<td class="px-4 py-3 text-sm text-neutral-400">{{ category.display_order }}</td>
|
|
<td class="px-4 py-3">
|
|
<span
|
|
class="text-xs font-medium px-2 py-0.5 rounded-full"
|
|
:class="category.visible ? 'bg-green-500/10 text-green-400' : 'bg-neutral-700/50 text-neutral-400'"
|
|
>
|
|
{{ category.visible ? 'Visible' : 'Hidden' }}
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-3 text-right">
|
|
<div class="flex items-center justify-end gap-1">
|
|
<button
|
|
@click="openCategoryModal(category)"
|
|
class="p-1.5 text-neutral-500 hover:text-oxide-400 rounded transition-colors"
|
|
title="Edit"
|
|
>
|
|
<Edit2 class="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
@click="deleteCategory(category)"
|
|
class="p-1.5 text-neutral-500 hover:text-red-400 rounded transition-colors"
|
|
title="Delete"
|
|
>
|
|
<Trash2 class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Items Tab -->
|
|
<div v-if="tab === 'items'" class="bg-neutral-900 border border-neutral-800 rounded-lg overflow-hidden">
|
|
<table class="w-full">
|
|
<thead>
|
|
<tr class="border-b border-neutral-800 text-left">
|
|
<th class="px-4 py-3 text-xs font-medium text-neutral-500 uppercase tracking-wider">Item</th>
|
|
<th class="px-4 py-3 text-xs font-medium text-neutral-500 uppercase tracking-wider">Category</th>
|
|
<th class="px-4 py-3 text-xs font-medium text-neutral-500 uppercase tracking-wider">Type</th>
|
|
<th class="px-4 py-3 text-xs font-medium text-neutral-500 uppercase tracking-wider">Price</th>
|
|
<th class="px-4 py-3 text-xs font-medium text-neutral-500 uppercase tracking-wider">Commands</th>
|
|
<th class="px-4 py-3 text-xs font-medium text-neutral-500 uppercase tracking-wider">Status</th>
|
|
<th class="px-4 py-3 text-xs font-medium text-neutral-500 uppercase tracking-wider text-right">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-neutral-800">
|
|
<tr v-if="items.length === 0">
|
|
<td colspan="7" class="px-4 py-12 text-center text-neutral-500 text-sm">
|
|
<template v-if="isLoading">Loading items...</template>
|
|
<template v-else>No items yet. Add items to start selling.</template>
|
|
</td>
|
|
</tr>
|
|
<tr
|
|
v-for="item in items"
|
|
:key="item.id"
|
|
class="hover:bg-neutral-800/50 transition-colors"
|
|
>
|
|
<td class="px-4 py-3">
|
|
<p class="text-sm font-medium text-neutral-100">{{ item.name }}</p>
|
|
<p v-if="item.description" class="text-xs text-neutral-500 truncate max-w-xs">{{ item.description }}</p>
|
|
</td>
|
|
<td class="px-4 py-3 text-sm text-neutral-400">{{ getCategoryName(item.category_id) }}</td>
|
|
<td class="px-4 py-3">
|
|
<span class="text-xs font-medium px-2 py-0.5 rounded-full" :class="typeBadgeClass(item.item_type)">
|
|
{{ item.item_type }}
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-3">
|
|
<div class="flex items-center gap-1 text-sm text-neutral-200">
|
|
<DollarSign class="w-3.5 h-3.5 text-neutral-500" />
|
|
{{ safeFixed(item.price, 2) }}
|
|
</div>
|
|
</td>
|
|
<td class="px-4 py-3 text-sm text-neutral-400 font-mono">
|
|
{{ item.delivery_commands.length }} cmd{{ item.delivery_commands.length !== 1 ? 's' : '' }}
|
|
</td>
|
|
<td class="px-4 py-3">
|
|
<span
|
|
class="text-xs font-medium px-2 py-0.5 rounded-full"
|
|
:class="item.enabled ? 'bg-green-500/10 text-green-400' : 'bg-neutral-700/50 text-neutral-400'"
|
|
>
|
|
{{ item.enabled ? 'Enabled' : 'Disabled' }}
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-3 text-right">
|
|
<div class="flex items-center justify-end gap-1">
|
|
<button
|
|
@click="openItemModal(item)"
|
|
class="p-1.5 text-neutral-500 hover:text-oxide-400 rounded transition-colors"
|
|
title="Edit"
|
|
>
|
|
<Edit2 class="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
@click="deleteItem(item)"
|
|
class="p-1.5 text-neutral-500 hover:text-red-400 rounded transition-colors"
|
|
title="Delete"
|
|
>
|
|
<Trash2 class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Category Modal -->
|
|
<div
|
|
v-if="showCategoryModal"
|
|
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4"
|
|
@click.self="closeCategoryModal"
|
|
>
|
|
<div class="bg-neutral-900 border border-neutral-800 rounded-lg max-w-lg w-full">
|
|
<div class="border-b border-neutral-800 px-6 py-4 flex items-center justify-between">
|
|
<h2 class="text-xl font-bold text-neutral-100">{{ editingCategory ? 'Edit Category' : 'Add Category' }}</h2>
|
|
<button @click="closeCategoryModal" class="p-2 text-neutral-400 hover:text-neutral-200 rounded-lg transition-colors">
|
|
<X class="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
<div class="p-6 space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-neutral-300 mb-2">Name</label>
|
|
<input
|
|
v-model="categoryForm.name"
|
|
@input="categoryForm.slug = autoGenerateSlug(categoryForm.name)"
|
|
type="text"
|
|
placeholder="VIP Kits"
|
|
class="w-full px-3 py-2 bg-neutral-800 border border-neutral-700 rounded-lg text-sm text-neutral-100 placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-oxide-500/50 focus:border-oxide-500 transition-colors"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-neutral-300 mb-2">Slug (URL-safe)</label>
|
|
<input
|
|
v-model="categoryForm.slug"
|
|
type="text"
|
|
placeholder="vip-kits"
|
|
class="w-full px-3 py-2 bg-neutral-800 border border-neutral-700 rounded-lg text-sm text-neutral-100 font-mono placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-oxide-500/50 focus:border-oxide-500 transition-colors"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-neutral-300 mb-2">Description (optional)</label>
|
|
<textarea
|
|
v-model="categoryForm.description"
|
|
rows="2"
|
|
placeholder="Premium kits for VIP players"
|
|
class="w-full px-3 py-2 bg-neutral-800 border border-neutral-700 rounded-lg text-sm text-neutral-100 placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-oxide-500/50 focus:border-oxide-500 transition-colors resize-none"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-neutral-300 mb-2">Display Order</label>
|
|
<input
|
|
v-model.number="categoryForm.display_order"
|
|
type="number"
|
|
min="0"
|
|
class="w-full px-3 py-2 bg-neutral-800 border border-neutral-700 rounded-lg text-sm text-neutral-100 focus:outline-none focus:ring-2 focus:ring-oxide-500/50 focus:border-oxide-500 transition-colors"
|
|
/>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<input
|
|
v-model="categoryForm.visible"
|
|
type="checkbox"
|
|
id="category-visible"
|
|
class="w-4 h-4 rounded border-neutral-700 bg-neutral-800 text-oxide-600 focus:ring-2 focus:ring-oxide-500/50 transition-colors"
|
|
/>
|
|
<label for="category-visible" class="text-sm text-neutral-300">Visible to customers</label>
|
|
</div>
|
|
</div>
|
|
<div class="border-t border-neutral-800 px-6 py-4 flex items-center justify-end gap-3">
|
|
<button
|
|
@click="closeCategoryModal"
|
|
class="px-4 py-2 text-sm font-medium text-neutral-300 bg-neutral-800 hover:bg-neutral-700 rounded-lg transition-colors"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
@click="saveCategory"
|
|
class="px-6 py-2 text-sm font-medium text-white bg-oxide-600 hover:bg-oxide-700 rounded-lg transition-colors"
|
|
>
|
|
{{ editingCategory ? 'Save Changes' : 'Create Category' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Item Modal -->
|
|
<div
|
|
v-if="showItemModal"
|
|
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4"
|
|
@click.self="closeItemModal"
|
|
>
|
|
<div class="bg-neutral-900 border border-neutral-800 rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
|
<div class="sticky top-0 bg-neutral-900 border-b border-neutral-800 px-6 py-4 flex items-center justify-between">
|
|
<h2 class="text-xl font-bold text-neutral-100">{{ editingItem ? 'Edit Item' : 'Add Item' }}</h2>
|
|
<button @click="closeItemModal" class="p-2 text-neutral-400 hover:text-neutral-200 rounded-lg transition-colors">
|
|
<X class="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
<div class="p-6 space-y-6">
|
|
<!-- Basic Info -->
|
|
<div class="space-y-4">
|
|
<h3 class="text-sm font-medium text-neutral-400 uppercase tracking-wider">Basic Information</h3>
|
|
<div>
|
|
<label class="block text-sm font-medium text-neutral-300 mb-2">Item Name</label>
|
|
<input
|
|
v-model="itemForm.name"
|
|
type="text"
|
|
placeholder="VIP Starter Kit"
|
|
class="w-full px-3 py-2 bg-neutral-800 border border-neutral-700 rounded-lg text-sm text-neutral-100 placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-oxide-500/50 focus:border-oxide-500 transition-colors"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-neutral-300 mb-2">Description (optional)</label>
|
|
<textarea
|
|
v-model="itemForm.description"
|
|
rows="2"
|
|
placeholder="Get started with essential gear and resources"
|
|
class="w-full px-3 py-2 bg-neutral-800 border border-neutral-700 rounded-lg text-sm text-neutral-100 placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-oxide-500/50 focus:border-oxide-500 transition-colors resize-none"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-neutral-300 mb-2">Category</label>
|
|
<select
|
|
v-model="itemForm.category_id"
|
|
class="w-full px-3 py-2 bg-neutral-800 border border-neutral-700 rounded-lg text-sm text-neutral-100 focus:outline-none focus:ring-2 focus:ring-oxide-500/50 focus:border-oxide-500 transition-colors"
|
|
>
|
|
<option :value="null">Uncategorized</option>
|
|
<option v-for="cat in categories" :key="cat.id" :value="cat.id">{{ cat.name }}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pricing -->
|
|
<div class="space-y-4">
|
|
<h3 class="text-sm font-medium text-neutral-400 uppercase tracking-wider">Pricing</h3>
|
|
<div>
|
|
<label class="block text-sm font-medium text-neutral-300 mb-2">Price (USD)</label>
|
|
<div class="relative">
|
|
<DollarSign class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-neutral-500" />
|
|
<input
|
|
v-model.number="itemForm.price"
|
|
type="number"
|
|
step="0.01"
|
|
min="0.01"
|
|
placeholder="9.99"
|
|
class="w-full pl-10 pr-3 py-2 bg-neutral-800 border border-neutral-700 rounded-lg text-sm text-neutral-100 placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-oxide-500/50 focus:border-oxide-500 transition-colors"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Item Type -->
|
|
<div class="space-y-4">
|
|
<h3 class="text-sm font-medium text-neutral-400 uppercase tracking-wider">Item Type</h3>
|
|
<div class="grid grid-cols-2 gap-2">
|
|
<button
|
|
v-for="type in itemTypes"
|
|
:key="type.value"
|
|
@click="itemForm.item_type = type.value as any"
|
|
class="px-3 py-2 text-sm font-medium rounded-lg border transition-colors"
|
|
:class="itemForm.item_type === type.value
|
|
? 'bg-oxide-500/15 border-oxide-500 text-oxide-400'
|
|
: 'bg-neutral-800 border-neutral-700 text-neutral-400 hover:text-neutral-200'"
|
|
>
|
|
{{ type.label }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Delivery Commands -->
|
|
<div class="space-y-4">
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-sm font-medium text-neutral-400 uppercase tracking-wider">Delivery Commands</h3>
|
|
<button
|
|
@click="addCommand"
|
|
class="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-neutral-300 bg-neutral-800 hover:bg-neutral-700 border border-neutral-700 rounded-lg transition-colors"
|
|
>
|
|
<Plus class="w-3 h-3" />
|
|
Add Command
|
|
</button>
|
|
</div>
|
|
<div class="bg-neutral-800/50 border border-neutral-700 rounded-lg p-3 space-y-1.5 text-xs">
|
|
<p class="text-neutral-400">Use placeholders:</p>
|
|
<p class="text-neutral-300 font-mono">{'{steam_id}'} - Player's Steam ID</p>
|
|
<p class="text-neutral-300 font-mono">{'{player_name}'} - Player's name</p>
|
|
<p class="text-neutral-500 mt-2">Example: {{ selectedTypeExample }}</p>
|
|
</div>
|
|
<div class="space-y-2">
|
|
<div v-for="(_cmd, index) in itemForm.delivery_commands" :key="index" class="flex gap-2">
|
|
<input
|
|
v-model="itemForm.delivery_commands[index]"
|
|
type="text"
|
|
placeholder="inventory.giveto {steam_id} rifle.ak 1"
|
|
class="flex-1 px-3 py-2 bg-neutral-800 border border-neutral-700 rounded-lg text-sm text-neutral-100 font-mono placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-oxide-500/50 focus:border-oxide-500 transition-colors"
|
|
/>
|
|
<button
|
|
v-if="itemForm.delivery_commands.length > 1"
|
|
@click="removeCommand(index)"
|
|
class="p-2 text-neutral-500 hover:text-red-400 rounded-lg transition-colors"
|
|
>
|
|
<Trash2 class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Additional Settings -->
|
|
<div class="space-y-4">
|
|
<h3 class="text-sm font-medium text-neutral-400 uppercase tracking-wider">Additional Settings</h3>
|
|
<div>
|
|
<label class="block text-sm font-medium text-neutral-300 mb-2">Image URL (optional)</label>
|
|
<input
|
|
v-model="itemForm.image_url"
|
|
type="text"
|
|
placeholder="https://example.com/image.png"
|
|
class="w-full px-3 py-2 bg-neutral-800 border border-neutral-700 rounded-lg text-sm text-neutral-100 placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-oxide-500/50 focus:border-oxide-500 transition-colors"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-neutral-300 mb-2">Purchase Limit Per Player (optional)</label>
|
|
<input
|
|
v-model.number="itemForm.limit_per_player"
|
|
type="number"
|
|
min="0"
|
|
placeholder="Leave empty for unlimited"
|
|
class="w-full px-3 py-2 bg-neutral-800 border border-neutral-700 rounded-lg text-sm text-neutral-100 placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-oxide-500/50 focus:border-oxide-500 transition-colors"
|
|
/>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<input
|
|
v-model="itemForm.enabled"
|
|
type="checkbox"
|
|
id="item-enabled"
|
|
class="w-4 h-4 rounded border-neutral-700 bg-neutral-800 text-oxide-600 focus:ring-2 focus:ring-oxide-500/50 transition-colors"
|
|
/>
|
|
<label for="item-enabled" class="text-sm text-neutral-300">Enabled and available for purchase</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="border-t border-neutral-800 px-6 py-4 flex items-center justify-end gap-3">
|
|
<button
|
|
@click="closeItemModal"
|
|
class="px-4 py-2 text-sm font-medium text-neutral-300 bg-neutral-800 hover:bg-neutral-700 rounded-lg transition-colors"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
@click="saveItem"
|
|
class="px-6 py-2 text-sm font-medium text-white bg-oxide-600 hover:bg-oxide-700 rounded-lg transition-colors"
|
|
>
|
|
{{ editingItem ? 'Save Changes' : 'Create Item' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|