feat: Implement 6 views + updated hero — Server, Chat, Team, Notifications, Settings, Setup Wizard
Server: Connection status, start/stop/restart controls, config editor with edit mode, automation toggles (crash recovery, force wipe, auto-update). Chat Log: Message feed with channel filter (global/team/server), search, flag/unflag per message, timestamped entries with channel badges. Team: Member table with role badges, invite form with role select, pending/active status, remove action. Notifications: Discord webhook, Pushbullet, email toggle cards. 6 event triggers (wipe start/complete/fail, crash, offline, purchase). Settings: 3-tab layout (Account, License, Domain). Account editing, license info display, subdomain + custom domain config with CNAME hint. Setup Wizard: 3-step flow (Configure → Install Agent → Done). Connection type radio cards, RCON/game port config, companion agent install instructions with license key pre-filled. Also swaps hero graphic to corrected version (two-column Control vs Infrastructure layout per brand brief). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,162 @@
|
||||
<script setup lang="ts">
|
||||
// TODO: Implement real-time chat feed from the game server
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useApi } from '@/composables/useApi'
|
||||
import type { ChatMessage } from '@/types'
|
||||
import { MessageSquare, Search, Flag, RefreshCw } from 'lucide-vue-next'
|
||||
|
||||
const api = useApi()
|
||||
|
||||
const messages = ref<ChatMessage[]>([])
|
||||
const isLoading = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const channelFilter = ref<'all' | 'global' | 'team' | 'server'>('all')
|
||||
|
||||
const filteredMessages = computed(() => {
|
||||
let result = messages.value
|
||||
|
||||
if (channelFilter.value !== 'all') {
|
||||
result = result.filter(m => m.channel === channelFilter.value)
|
||||
}
|
||||
|
||||
if (searchQuery.value.trim()) {
|
||||
const q = searchQuery.value.toLowerCase()
|
||||
result = result.filter(m =>
|
||||
m.player_name.toLowerCase().includes(q) ||
|
||||
m.message.toLowerCase().includes(q) ||
|
||||
m.steam_id.includes(q)
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
function channelBadgeClass(channel: string): string {
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(iso: string): string {
|
||||
return new Date(iso).toLocaleTimeString('en-US', { hour12: false })
|
||||
}
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
|
||||
}
|
||||
|
||||
async function fetchMessages() {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const data = await api.get<{ messages: ChatMessage[] }>('/chat')
|
||||
messages.value = data.messages
|
||||
} catch {
|
||||
// API not wired yet
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleFlag(msg: ChatMessage) {
|
||||
try {
|
||||
await api.put(`/chat/${msg.id}/flag`, { flagged: !msg.flagged })
|
||||
msg.flagged = !msg.flagged
|
||||
} catch {
|
||||
// Handle error
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchMessages()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<h1 class="text-2xl font-bold text-neutral-100 mb-4">Chat Log</h1>
|
||||
<p class="text-neutral-400">Real-time chat feed from your Rust server with search and filtering.</p>
|
||||
<div class="p-6 space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<MessageSquare class="w-5 h-5 text-oxide-500" />
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-neutral-100">Chat Log</h1>
|
||||
<p class="text-sm text-neutral-500 mt-0.5">{{ messages.length }} messages</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="fetchMessages"
|
||||
:disabled="isLoading"
|
||||
class="flex items-center gap-2 px-4 py-2 text-sm font-medium text-neutral-300 bg-neutral-800 hover:bg-neutral-700 disabled:opacity-50 rounded-lg transition-colors"
|
||||
>
|
||||
<RefreshCw class="w-4 h-4" :class="{ 'animate-spin': isLoading }" />
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="relative flex-1 max-w-sm">
|
||||
<Search class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-neutral-500" />
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Search messages, players, or Steam IDs..."
|
||||
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 class="flex bg-neutral-800 rounded-lg border border-neutral-700 overflow-hidden">
|
||||
<button
|
||||
v-for="opt in (['all', 'global', 'team', 'server'] as const)"
|
||||
:key="opt"
|
||||
@click="channelFilter = opt"
|
||||
class="px-3 py-2 text-sm font-medium transition-colors capitalize"
|
||||
:class="channelFilter === opt
|
||||
? 'bg-oxide-500/15 text-oxide-400'
|
||||
: 'text-neutral-400 hover:text-neutral-200'"
|
||||
>
|
||||
{{ opt }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
<div class="bg-neutral-900 border border-neutral-800 rounded-lg divide-y divide-neutral-800">
|
||||
<div v-if="filteredMessages.length === 0" class="px-4 py-12 text-center text-neutral-500 text-sm">
|
||||
<template v-if="isLoading">Loading chat messages...</template>
|
||||
<template v-else-if="searchQuery">No messages matching "{{ searchQuery }}"</template>
|
||||
<template v-else>No chat messages yet. Messages will appear when the server is active.</template>
|
||||
</div>
|
||||
<div
|
||||
v-for="msg in filteredMessages"
|
||||
:key="msg.id"
|
||||
class="flex items-start gap-4 px-4 py-3 hover:bg-neutral-800/50 transition-colors"
|
||||
:class="{ 'bg-red-500/5 border-l-2 border-l-red-500/30': msg.flagged }"
|
||||
>
|
||||
<div class="shrink-0 text-right w-20">
|
||||
<p class="text-xs text-neutral-500">{{ formatDate(msg.created_at) }}</p>
|
||||
<p class="text-xs text-neutral-600">{{ formatTime(msg.created_at) }}</p>
|
||||
</div>
|
||||
<span
|
||||
class="shrink-0 text-xs font-medium px-2 py-0.5 rounded-full mt-0.5"
|
||||
:class="channelBadgeClass(msg.channel)"
|
||||
>
|
||||
{{ msg.channel }}
|
||||
</span>
|
||||
<div class="flex-1 min-w-0">
|
||||
<span class="text-sm font-medium text-oxide-400">{{ msg.player_name }}</span>
|
||||
<span class="text-sm text-neutral-500 ml-2 font-mono">{{ msg.steam_id }}</span>
|
||||
<p class="text-sm text-neutral-300 mt-0.5 break-words">{{ msg.message }}</p>
|
||||
</div>
|
||||
<button
|
||||
@click="toggleFlag(msg)"
|
||||
class="shrink-0 p-1 rounded transition-colors"
|
||||
:class="msg.flagged ? 'text-red-400 hover:text-red-300' : 'text-neutral-600 hover:text-neutral-400'"
|
||||
:title="msg.flagged ? 'Unflag message' : 'Flag message'"
|
||||
>
|
||||
<Flag class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user