diff --git a/frontend/src/components/ds/core/Icon.vue b/frontend/src/components/ds/core/Icon.vue index 94419d6..1349f66 100644 --- a/frontend/src/components/ds/core/Icon.vue +++ b/frontend/src/components/ds/core/Icon.vue @@ -20,6 +20,9 @@ import { Info, OctagonAlert, CircleCheck, Sparkles, Inbox, LayoutDashboard, CalendarClock, Drama, ChevronsUpDown, ServerCog, LayoutGrid, SquareDashed, MemoryStick, CornerDownLeft, + Ban, Flag, + CircleAlert, ArrowDown, Award, DollarSign, FlaskConical, Mail, Package, + Pencil, Save, ShoppingBag, Target, User, } from 'lucide-vue-next' const props = withDefaults( @@ -50,6 +53,11 @@ const registry: Record = { 'layout-dashboard': LayoutDashboard, 'calendar-clock': CalendarClock, drama: Drama, 'chevrons-up-down': ChevronsUpDown, 'server-cog': ServerCog, 'layout-grid': LayoutGrid, 'square-dashed': SquareDashed, 'memory-stick': MemoryStick, 'corner-down-left': CornerDownLeft, + ban: Ban, flag: Flag, + 'alert-circle': CircleAlert, 'arrow-down': ArrowDown, award: Award, + 'dollar-sign': DollarSign, 'flask-conical': FlaskConical, mail: Mail, + package: Package, pencil: Pencil, save: Save, 'shopping-bag': ShoppingBag, + target: Target, user: User, } const cmp = computed(() => registry[props.name] ?? null) diff --git a/frontend/src/views/admin/AlertsView.vue b/frontend/src/views/admin/AlertsView.vue index ee84e2a..54b1669 100644 --- a/frontend/src/views/admin/AlertsView.vue +++ b/frontend/src/views/admin/AlertsView.vue @@ -1,8 +1,14 @@ + + diff --git a/frontend/src/views/admin/AnalyticsView.vue b/frontend/src/views/admin/AnalyticsView.vue index 5033608..2285874 100644 --- a/frontend/src/views/admin/AnalyticsView.vue +++ b/frontend/src/views/admin/AnalyticsView.vue @@ -1,6 +1,5 @@ + + diff --git a/frontend/src/views/admin/ChatLogView.vue b/frontend/src/views/admin/ChatLogView.vue index d2914fd..c358254 100644 --- a/frontend/src/views/admin/ChatLogView.vue +++ b/frontend/src/views/admin/ChatLogView.vue @@ -3,7 +3,14 @@ import { ref, computed, onMounted } from 'vue' import { useApi } from '@/composables/useApi' import { useToastStore } from '@/stores/toast' import type { ChatMessage } from '@/types' -import { MessageSquare, Search, Flag, RefreshCw } from 'lucide-vue-next' +import Panel from '@/components/ds/data/Panel.vue' +import Button from '@/components/ds/core/Button.vue' +import Badge from '@/components/ds/core/Badge.vue' +import Icon from '@/components/ds/core/Icon.vue' +import IconButton from '@/components/ds/core/IconButton.vue' +import Input from '@/components/ds/forms/Input.vue' +import EmptyState from '@/components/ds/feedback/EmptyState.vue' +import Tabs from '@/components/ds/navigation/Tabs.vue' const api = useApi() const toast = useToastStore() @@ -32,12 +39,18 @@ const filteredMessages = computed(() => { return result }) -function channelBadgeClass(channel: string): string { +const channelTabItems = computed(() => [ + { value: 'all', label: 'All', count: messages.value.length }, + { value: 'global', label: 'Global' }, + { value: 'team', label: 'Team' }, + { value: 'server', label: 'Server' }, +]) + +function channelTone(channel: string): 'accent' | 'info' | 'neutral' { switch (channel) { - case 'global': return 'bg-oxide-500/15 text-oxide-400' - case 'team': return 'bg-blue-500/15 text-blue-400' - case 'server': return 'bg-neutral-700/50 text-neutral-400' - default: return 'bg-neutral-700/50 text-neutral-400' + case 'global': return 'accent' + case 'team': return 'info' + default: return 'neutral' } } @@ -76,89 +89,163 @@ onMounted(() => { + + diff --git a/frontend/src/views/admin/MapAnalyticsView.vue b/frontend/src/views/admin/MapAnalyticsView.vue index 6483633..3bba563 100644 --- a/frontend/src/views/admin/MapAnalyticsView.vue +++ b/frontend/src/views/admin/MapAnalyticsView.vue @@ -1,11 +1,17 @@ + + diff --git a/frontend/src/views/admin/MapsView.vue b/frontend/src/views/admin/MapsView.vue index 1335920..8161c97 100644 --- a/frontend/src/views/admin/MapsView.vue +++ b/frontend/src/views/admin/MapsView.vue @@ -4,8 +4,12 @@ import { useApi } from '@/composables/useApi' import { useAuthStore } from '@/stores/auth' import { useToastStore } from '@/stores/toast' import type { MapEntry } from '@/types' -import { Map, Upload, Trash2, RefreshCw, Loader2 } from 'lucide-vue-next' import { safeFileSize } from '@/utils/formatters' +import Panel from '@/components/ds/data/Panel.vue' +import Button from '@/components/ds/core/Button.vue' +import Badge from '@/components/ds/core/Badge.vue' +import IconButton from '@/components/ds/core/IconButton.vue' +import EmptyState from '@/components/ds/feedback/EmptyState.vue' const api = useApi() const auth = useAuthStore() @@ -20,8 +24,8 @@ function formatSize(bytes: number): string { return safeFileSize(bytes) } -function typeBadgeClass(type: string): string { - return type === 'custom' ? 'bg-oxide-500/15 text-oxide-400' : 'bg-blue-500/15 text-blue-400' +function mapTypeTone(type: string): 'accent' | 'info' { + return type === 'custom' ? 'accent' : 'info' } async function fetchMaps() { @@ -92,84 +96,211 @@ onMounted(() => { + + diff --git a/frontend/src/views/admin/ModuleStoreView.vue b/frontend/src/views/admin/ModuleStoreView.vue index befaeec..28755af 100644 --- a/frontend/src/views/admin/ModuleStoreView.vue +++ b/frontend/src/views/admin/ModuleStoreView.vue @@ -3,8 +3,17 @@ import { ref, computed, onMounted } from 'vue' import { useApi } from '@/composables/useApi' import { useAuthStore } from '@/stores/auth' import type { Module } from '@/types' -import { ShoppingCart, Package, Search, Filter, X, Check, Download, AlertCircle } from 'lucide-vue-next' import { safeFixed } from '@/utils/formatters' +import Panel from '@/components/ds/data/Panel.vue' +import Button from '@/components/ds/core/Button.vue' +import Badge from '@/components/ds/core/Badge.vue' +import Icon from '@/components/ds/core/Icon.vue' +import IconButton from '@/components/ds/core/IconButton.vue' +import Tabs from '@/components/ds/navigation/Tabs.vue' +import Alert from '@/components/ds/feedback/Alert.vue' +import EmptyState from '@/components/ds/feedback/EmptyState.vue' +import Input from '@/components/ds/forms/Input.vue' +import Select from '@/components/ds/forms/Select.vue' const api = useApi() const auth = useAuthStore() @@ -22,17 +31,22 @@ const isPurchasing = ref(false) const purchaseError = ref('') const categories = [ - { value: 'all', label: 'All Modules' }, + { value: 'all', label: 'All modules' }, { value: 'loot', label: 'Loot' }, { value: 'events', label: 'Events' }, { value: 'economy', label: 'Economy' }, { value: 'kits', label: 'Kits' }, - { value: 'admin', label: 'Admin Tools' }, + { value: 'admin', label: 'Admin tools' }, { value: 'pvp', label: 'PVP' }, { value: 'pve', label: 'PVE' }, { value: 'building', label: 'Building' }, ] +const tabItems = computed(() => [ + { value: 'catalog', label: 'Catalog', icon: 'package' }, + { value: 'my-modules', label: `My modules (${myModules.value.length})`, icon: 'download' }, +]) + const filteredModules = computed(() => { let result = activeTab.value === 'catalog' ? modules.value : myModules.value @@ -51,6 +65,21 @@ const filteredModules = computed(() => { return result }) +type BadgeTone = 'info' | 'accent' | 'warn' | 'online' | 'neutral' | 'offline' +function categoryTone(category: string): BadgeTone { + const map: Record = { + loot: 'warn', + events: 'accent', + economy: 'online', + kits: 'info', + admin: 'accent', + pvp: 'offline', + pve: 'info', + building: 'warn', + } + return map[category] ?? 'neutral' +} + async function loadCatalog() { isLoading.value = true try { @@ -94,10 +123,8 @@ async function confirmPurchase() { }) if (response.payment_url) { - // Redirect to external payment provider window.location.href = response.payment_url } else if (response.success) { - // Instant purchase confirmed showPurchaseModal.value = false selectedModule.value = null await loadCatalog() @@ -131,20 +158,6 @@ function closeModals() { purchaseError.value = '' } -function categoryBadgeClass(category: string): string { - const colors: Record = { - loot: 'bg-yellow-500/15 text-yellow-400', - events: 'bg-purple-500/15 text-purple-400', - economy: 'bg-green-500/15 text-green-400', - kits: 'bg-blue-500/15 text-blue-400', - admin: 'bg-oxide-500/15 text-oxide-400', - pvp: 'bg-red-500/15 text-red-400', - pve: 'bg-indigo-500/15 text-indigo-400', - building: 'bg-orange-500/15 text-orange-400', - } - return colors[category] || 'bg-neutral-700/50 text-neutral-400' -} - onMounted(() => { loadCatalog() loadMyModules() @@ -152,323 +165,371 @@ onMounted(() => { + + diff --git a/frontend/src/views/admin/PlayerRetentionView.vue b/frontend/src/views/admin/PlayerRetentionView.vue index 0536a96..75f4310 100644 --- a/frontend/src/views/admin/PlayerRetentionView.vue +++ b/frontend/src/views/admin/PlayerRetentionView.vue @@ -1,11 +1,15 @@ + + diff --git a/frontend/src/views/admin/PlayersView.vue b/frontend/src/views/admin/PlayersView.vue index fb75a01..d3fc7f2 100644 --- a/frontend/src/views/admin/PlayersView.vue +++ b/frontend/src/views/admin/PlayersView.vue @@ -3,7 +3,14 @@ import { ref, computed, onMounted } from 'vue' import { useServerStore } from '@/stores/server' import { useApi } from '@/composables/useApi' import { useToastStore } from '@/stores/toast' -import { Users, Search, Ban, LogOut, Shield, RefreshCw } from 'lucide-vue-next' +import Panel from '@/components/ds/data/Panel.vue' +import Button from '@/components/ds/core/Button.vue' +import Badge from '@/components/ds/core/Badge.vue' +import Icon from '@/components/ds/core/Icon.vue' +import IconButton from '@/components/ds/core/IconButton.vue' +import Input from '@/components/ds/forms/Input.vue' +import EmptyState from '@/components/ds/feedback/EmptyState.vue' +import Tabs from '@/components/ds/navigation/Tabs.vue' const server = useServerStore() const api = useApi() @@ -50,6 +57,12 @@ const filteredPlayers = computed(() => { const onlineCount = computed(() => players.value.filter(p => p.is_online).length) +const statusTabItems = computed(() => [ + { value: 'all', label: 'All', count: players.value.length }, + { value: 'online', label: 'Online', count: players.value.filter(p => p.is_online).length }, + { value: 'offline', label: 'Offline', count: players.value.filter(p => !p.is_online).length }, +]) + function formatPlaytime(seconds: number): string { const h = Math.floor(seconds / 3600) const m = Math.floor((seconds % 3600) / 60) @@ -58,7 +71,7 @@ function formatPlaytime(seconds: number): string { } function formatConnectedTime(iso: string | null): string { - if (!iso) return '\u2014' + if (!iso) return '—' const diff = Date.now() - new Date(iso).getTime() const m = Math.floor(diff / 60000) const h = Math.floor(m / 60) @@ -66,6 +79,18 @@ function formatConnectedTime(iso: string | null): string { return `${m}m` } +function playerStatusTone(player: Player): 'online' | 'offline' | 'warn' { + if (player.is_banned) return 'warn' + if (player.is_online) return 'online' + return 'offline' +} + +function playerStatusLabel(player: Player): string { + if (player.is_banned) return 'Banned' + if (player.is_online) return 'Online' + return 'Offline' +} + async function fetchPlayers() { isLoading.value = true try { @@ -106,132 +131,197 @@ onMounted(() => { + + diff --git a/frontend/src/views/admin/PluginsView.vue b/frontend/src/views/admin/PluginsView.vue index d0deb43..573d7f3 100644 --- a/frontend/src/views/admin/PluginsView.vue +++ b/frontend/src/views/admin/PluginsView.vue @@ -4,7 +4,15 @@ import { usePluginStore } from '@/stores/plugins' import type { UmodPlugin } from '@/stores/plugins' import { useToastStore } from '@/stores/toast' import type { PluginEntry } from '@/types' -import { Puzzle, Search, Download, RefreshCw, Power, PowerOff, Trash2, Loader2, Upload, X } from 'lucide-vue-next' +import Panel from '@/components/ds/data/Panel.vue' +import Button from '@/components/ds/core/Button.vue' +import Badge from '@/components/ds/core/Badge.vue' +import Icon from '@/components/ds/core/Icon.vue' +import IconButton from '@/components/ds/core/IconButton.vue' +import Input from '@/components/ds/forms/Input.vue' +import EmptyState from '@/components/ds/feedback/EmptyState.vue' +import Alert from '@/components/ds/feedback/Alert.vue' +import Tabs from '@/components/ds/navigation/Tabs.vue' const pluginStore = usePluginStore() const toast = useToastStore() @@ -35,6 +43,12 @@ const browsePlugins = computed(() => pluginStore.browseResults?.data ?? []) const loadedCount = computed(() => pluginStore.plugins.filter((p: PluginEntry) => p.is_loaded).length) +const tabItems = [ + { value: 'installed', label: 'Installed' }, + { value: 'browse', label: 'Browse uMod' }, + { value: 'upload', label: 'Upload custom' }, +] + function sourceLabel(source: string): string { switch (source) { case 'umod': return 'uMod' @@ -44,11 +58,11 @@ function sourceLabel(source: string): string { } } -function sourceBadgeClass(source: string): string { +function sourceTone(source: string): 'online' | 'accent' | 'neutral' { switch (source) { - case 'umod': return 'bg-green-500/10 text-green-400' - case 'corrosion_module': return 'bg-oxide-500/15 text-oxide-400' - default: return 'bg-neutral-700/50 text-neutral-400' + case 'umod': return 'online' + case 'corrosion_module': return 'accent' + default: return 'neutral' } } @@ -168,274 +182,249 @@ onMounted(() => { + + diff --git a/frontend/src/views/admin/SchedulesView.vue b/frontend/src/views/admin/SchedulesView.vue index b813d46..7a4430c 100644 --- a/frontend/src/views/admin/SchedulesView.vue +++ b/frontend/src/views/admin/SchedulesView.vue @@ -1,8 +1,14 @@