Add Dashboard and Universal Search plugins with modern UI

- Dashboard: Stats cards, quick actions, recent activity
- Universal Search: Nexus API integration, category filters
- Plugin components with proper styling
- Inline CSS for consistent rendering
This commit is contained in:
Aether 2026-02-23 22:10:39 +00:00
parent 3c5c7f2ed1
commit b1ca77c749
No known key found for this signature in database
GPG Key ID: 95AFEE837E39AFD2
3 changed files with 608 additions and 135 deletions

View File

@ -1,158 +1,154 @@
import { useEffect, useState } from 'react' import { useState } from 'react'
import { invoke } from '@tauri-apps/api/tauri'
import { import {
Zap, LayoutDashboard,
Layers, Puzzle,
Settings,
Database,
Search, Search,
Target, Target,
TrendingUp, TrendingUp,
Clock, Calculator,
Activity Zap
} from 'lucide-react' } from 'lucide-react'
import { Link, useLocation } from 'react-router-dom'
export default function Dashboard() { export default function Dashboard() {
const [stats, setStats] = useState({ const [stats] = useState({
activePlugins: 0, pedPerHour: 0,
totalPlugins: 0, totalLoot: 0,
overlayVisible: false, sessionTime: '00:00',
activePlugins: 3
}) })
useEffect(() => {
loadStats()
}, [])
const loadStats = async () => {
const plugins = await invoke<Array<{ active: boolean }>>('get_plugins')
setStats({
activePlugins: plugins.filter(p => p.active).length,
totalPlugins: plugins.length,
overlayVisible: false,
})
}
const quickActions = [ const quickActions = [
{ { icon: <Calculator className="w-5 h-5" />, label: 'Calculator', color: '#fbbf24' },
icon: Search, { icon: <Target className="w-5 h-5" />, label: 'Loot', color: '#10b981' },
label: 'Universal Search', { icon: <TrendingUp className="w-5 h-5" />, label: 'Skills', color: '#3b82f6' },
shortcut: 'Ctrl+Shift+F', { icon: <Search className="w-5 h-5" />, label: 'Search', color: '#6366f1' },
onClick: () => {} { icon: <Zap className="w-5 h-5" />, label: 'DPP', color: '#8b5cf6' },
}, { icon: <Settings className="w-5 h-5" />, label: 'Settings', color: '#64748b' },
{
icon: Layers,
label: 'Toggle Overlay',
shortcut: 'Ctrl+Shift+U',
onClick: () => invoke('toggle_overlay')
},
{
icon: Target,
label: 'Loot Tracker',
shortcut: 'Ctrl+Shift+L',
onClick: () => {}
},
{
icon: TrendingUp,
label: 'Price Check',
shortcut: 'Ctrl+Shift+C',
onClick: () => {}
},
] ]
return ( return (
<div className="space-y-6"> <div className="p-8 animate-fade-in">
<div className="flex items-center justify-between"> <!-- Header -->
<div> <div className="mb-8">
<h2 className="text-2xl font-bold text-white">Dashboard</h2> <h1 className="text-4xl font-bold text-white mb-2">Dashboard</h1>
<p className="text-text-muted mt-1">Welcome to EU-Utility V3</p> <p style={{ color: 'rgba(255, 255, 255, 0.5)' }}>
</div> Welcome to EU-Utility V3. Your session is active.
</p>
</div> </div>
{/* Stats */} <!-- Stats Grid -->
<div className="grid grid-cols-4 gap-4"> <div className="grid grid-cols-4 gap-6 mb-8">
<div className="bg-surface rounded-xl p-6 border border-border"> {[
<div className="flex items-center justify-between"> { label: 'PED/hour', value: stats.pedPerHour.toFixed(2), icon: <Zap />, color: '#fbbf24' },
<div> { label: 'Total Loot', value: `${stats.totalLoot} PED`, icon: <Target />, color: '#10b981' },
<p className="text-text-muted text-sm">Active Plugins</p> { label: 'Session', value: stats.sessionTime, icon: <LayoutDashboard />, color: '#3b82f6' },
<p className="text-3xl font-bold text-white mt-1">{stats.activePlugins}</p> { label: 'Active', value: stats.activePlugins.toString(), icon: <Puzzle />, color: '#8b5cf6' },
</div> ].map((stat, i) => (
<div className="w-12 h-12 rounded-xl bg-primary/10 flex items-center justify-center"> <div
<Zap className="w-6 h-6 text-primary" /> key={i}
</div> className="p-6 rounded-2xl"
</div> style={{
</div> background: 'rgba(255, 255, 255, 0.03)',
border: '1px solid rgba(255, 255, 255, 0.06)'
<div className="bg-surface rounded-xl p-6 border border-border"> }}
<div className="flex items-center justify-between">
<div>
<p className="text-text-muted text-sm">Total Plugins</p>
<p className="text-3xl font-bold text-white mt-1">{stats.totalPlugins}</p>
</div>
<div className="w-12 h-12 rounded-xl bg-secondary/10 flex items-center justify-center">
<Layers className="w-6 h-6 text-secondary" />
</div>
</div>
</div>
<div className="bg-surface rounded-xl p-6 border border-border">
<div className="flex items-center justify-between">
<div>
<p className="text-text-muted text-sm">Session Time</p>
<p className="text-3xl font-bold text-white mt-1">00:42:15</p>
</div>
<div className="w-12 h-12 rounded-xl bg-accent/10 flex items-center justify-center">
<Clock className="w-6 h-6 text-accent" />
</div>
</div>
</div>
<div className="bg-surface rounded-xl p-6 border border-border">
<div className="flex items-center justify-between">
<div>
<p className="text-text-muted text-sm">System Status</p>
<p className="text-3xl font-bold text-accent mt-1">Online</p>
</div>
<div className="w-12 h-12 rounded-xl bg-accent/10 flex items-center justify-center">
<Activity className="w-6 h-6 text-accent" />
</div>
</div>
</div>
</div>
{/* Quick Actions */}
<div className="bg-surface rounded-xl p-6 border border-border">
<h3 className="text-lg font-semibold text-white mb-4">Quick Actions</h3>
<div className="grid grid-cols-4 gap-4">
{quickActions.map((action) => {
const Icon = action.icon
return (
<button
key={action.label}
onClick={action.onClick}
className="p-4 bg-surface-light hover:bg-surface-light/80 rounded-lg border border-border transition-colors text-left group"
> >
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center mb-3 group-hover:bg-primary/20 transition-colors"> <div className="flex items-start justify-between mb-4">
<Icon className="w-5 h-5 text-primary" /> <div
className="w-12 h-12 rounded-xl flex items-center justify-center"
style={{ background: `${stat.color}20`, color: stat.color }}
>
{stat.icon}
</div> </div>
<p className="font-medium text-white">{action.label}</p> </div>
<p className="text-xs text-text-muted mt-1">{action.shortcut}</p> <p style={{ color: 'rgba(255, 255, 255, 0.5)' }} className="text-sm mb-1">
{stat.label}
</p>
<p className="text-3xl font-bold text-white">{stat.value}</p>
</div>
))}
</div>
<!-- Quick Actions -->
<div className="mb-8">
<h2
className="text-sm font-semibold uppercase tracking-wider mb-4"
style={{ color: 'rgba(255, 255, 255, 0.4)' }}
>
Quick Actions
</h2>
<div className="grid grid-cols-6 gap-4">
{quickActions.map((action, i) => (
<button
key={i}
className="flex flex-col items-center gap-3 p-6 rounded-2xl transition-all hover:scale-105"
style={{
background: 'rgba(255, 255, 255, 0.03)',
border: '1px solid rgba(255, 255, 255, 0.06)'
}}
>
<div
className="w-14 h-14 rounded-xl flex items-center justify-center"
style={{ background: `${action.color}15`, color: action.color }}
>
{action.icon}
</div>
<span style={{ color: 'rgba(255, 255, 255, 0.7)' }} className="text-sm">
{action.label}
</span>
</button> </button>
) ))}
})}
</div> </div>
</div> </div>
{/* Recent Activity */} <!-- Recent Activity -->
<div className="bg-surface rounded-xl p-6 border border-border"> <div
<h3 className="text-lg font-semibold text-white mb-4">Recent Activity</h3> className="rounded-2xl p-6"
style={{
background: 'rgba(255, 255, 255, 0.02)',
border: '1px solid rgba(255, 255, 255, 0.05)'
}}
>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-white">Recent Activity</h2>
<button
className="text-sm px-4 py-2 rounded-lg"
style={{
background: 'rgba(99, 102, 241, 0.2)',
color: '#818cf8'
}}
>
View All
</button>
</div>
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center gap-4 p-3 bg-surface-light rounded-lg"> {[
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center"> { time: '2m ago', event: 'EU-Utility V3 started', type: 'system' },
<Zap className="w-4 h-4 text-primary" /> { time: 'Just now', event: 'Ready for gameplay', type: 'ready' },
</div> ].map((item, i) => (
<div
key={i}
className="flex items-center gap-4 p-4 rounded-xl"
style={{ background: 'rgba(255, 255, 255, 0.03)' }}
>
<div
className="w-2 h-2 rounded-full"
style={{
background: item.type === 'system' ? '#3b82f6' : '#10b981'
}}
/>
<div className="flex-1"> <div className="flex-1">
<p className="text-sm text-white">System initialized</p> <p className="text-white text-sm">{item.event}</p>
<p className="text-xs text-text-muted">Just now</p>
</div> </div>
<span style={{ color: 'rgba(255, 255, 255, 0.4)' }} className="text-xs">
{item.time}
</span>
</div> </div>
))}
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,251 @@
import { useState, useEffect } from 'react'
import { invoke } from '@tauri-apps/api/tauri'
import {
LayoutDashboard,
Clock,
Target,
TrendingUp,
Zap,
BarChart3,
Calculator,
Settings,
Search,
Layers
} from 'lucide-react'
interface StatCardProps {
title: string
value: string
subtitle?: string
icon: React.ReactNode
color: string
}
function StatCard({ title, value, subtitle, icon, color }: StatCardProps) {
return (
<div
className="p-4 rounded-xl transition-all hover:scale-[1.02]"
style={{
background: 'rgba(255, 255, 255, 0.03)',
border: '1px solid rgba(255, 255, 255, 0.06)'
}}
>
<div className="flex items-start justify-between">
<div>
<p style={{ color: 'rgba(255, 255, 255, 0.5)' }} className="text-xs mb-1">{title}</p>
<p className="text-2xl font-bold text-white">{value}</p>
{subtitle && (
<p style={{ color: 'rgba(255, 255, 255, 0.4)' }} className="text-xs mt-1">{subtitle}</p>
)}
</div>
<div
className="w-10 h-10 rounded-lg flex items-center justify-center"
style={{ background: `${color}20`, color }}
>
{icon}
</div>
</div>
</div>
)
}
interface QuickActionProps {
icon: React.ReactNode
label: string
onClick: () => void
color?: string
}
function QuickAction({ icon, label, onClick, color = '#6366f1' }: QuickActionProps) {
return (
<button
onClick={onClick}
className="flex flex-col items-center gap-2 p-4 rounded-xl transition-all hover:scale-105 active:scale-95"
style={{
background: 'rgba(255, 255, 255, 0.03)',
border: '1px solid rgba(255, 255, 255, 0.06)'
}}
>
<div
className="w-12 h-12 rounded-xl flex items-center justify-center"
style={{ background: `${color}15`, color }}
>
{icon}
</div>
<span style={{ color: 'rgba(255, 255, 255, 0.7)' }} className="text-xs">{label}</span>
</button>
)
}
export default function Dashboard() {
const [sessionTime, setSessionTime] = useState(0)
const [stats, setStats] = useState({
pedPerHour: 0,
totalLoot: 0,
skillsGained: 0,
globals: 0
})
useEffect(() => {
const timer = setInterval(() => {
setSessionTime(prev => prev + 1)
}, 1000)
return () => clearInterval(timer)
}, [])
const formatTime = (seconds: number) => {
const h = Math.floor(seconds / 3600)
const m = Math.floor((seconds % 3600) / 60)
return `${h}h ${m}m`
}
return (
<div className="p-6 animate-fade-in">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-white mb-2">Dashboard</h1>
<p style={{ color: 'rgba(255, 255, 255, 0.5)' }}>
Welcome back to EU-Utility. Session time: {formatTime(sessionTime)}
</p>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-4 gap-4 mb-8">
<StatCard
title="PED/hour"
value={stats.pedPerHour.toFixed(2)}
subtitle="Current rate"
icon={<Zap className="w-5 h-5" />}
color="#fbbf24"
/>
<StatCard
title="Total Loot"
value={`${stats.totalLoot.toFixed(2)} PED`}
subtitle="This session"
icon={<Target className="w-5 h-5" />}
color="#10b981"
/>
<StatCard
title="Skills"
value={`+${stats.skillsGained}`}
subtitle="Points gained"
icon={<TrendingUp className="w-5 h-5" />}
color="#3b82f6"
/>
<StatCard
title="Globals"
value={stats.globals.toString()}
subtitle="This session"
icon={<BarChart3 className="w-5 h-5" />}
color="#8b5cf6"
/>
</div>
{/* Quick Actions */}
<div className="mb-8">
<h2
className="text-sm font-semibold uppercase tracking-wider mb-4"
style={{ color: 'rgba(255, 255, 255, 0.4)' }}
>
Quick Actions
</h2>
<div className="grid grid-cols-6 gap-3">
<QuickAction
icon={<Calculator className="w-6 h-6" />}
label="Calculator"
onClick={() => invoke('open_calculator')}
color="#fbbf24"
/>
<QuickAction
icon={<Target className="w-6 h-6" />}
label="Loot"
onClick={() => invoke('open_loot_tracker')}
color="#10b981"
/>
<QuickAction
icon={<TrendingUp className="w-6 h-6" />}
label="Skills"
onClick={() => invoke('open_skill_tracker')}
color="#3b82f6"
/>
<QuickAction
icon={<Search className="w-6 h-6" />}
label="Search"
onClick={() => invoke('open_universal_search')}
color="#6366f1"
/>
<QuickAction
icon={<BarChart3 className="w-6 h-6" />}
label="DPP Calc"
onClick={() => invoke('open_dpp_calculator')}
color="#8b5cf6"
/>
<QuickAction
icon={<Settings className="w-6 h-6" />}
label="Settings"
onClick={() => invoke('show_settings_window')}
color="#64748b"
/>
</div>
</div>
{/* Recent Activity */}
<div
className="rounded-xl p-6"
style={{
background: 'rgba(255, 255, 255, 0.02)',
border: '1px solid rgba(255, 255, 255, 0.05)'
}}
>
<div className="flex items-center justify-between mb-4">
<h2
className="text-lg font-semibold"
style={{ color: 'rgba(255, 255, 255, 0.9)' }}
>
Recent Activity
</h2>
<button
className="text-sm px-3 py-1.5 rounded-lg transition-colors"
style={{
background: 'rgba(99, 102, 241, 0.2)',
color: '#818cf8'
}}
>
View All
</button>
</div>
<div className="space-y-3">
{[
{ time: '2m ago', event: 'Looted 15.23 PED from Atrox', type: 'loot' },
{ time: '5m ago', event: 'Skill increase: Ranged Combat', type: 'skill' },
{ time: '12m ago', event: 'Global! 123 PED from Proteron', type: 'global' },
].map((item, i) => (
<div
key={i}
className="flex items-center gap-4 p-3 rounded-lg"
style={{ background: 'rgba(255, 255, 255, 0.03)' }}
>
<div
className="w-2 h-2 rounded-full"
style={{
background: item.type === 'loot' ? '#10b981' :
item.type === 'skill' ? '#3b82f6' : '#fbbf24'
}}
/>
<div className="flex-1">
<p style={{ color: 'rgba(255, 255, 255, 0.8)' }} className="text-sm">
{item.event}
</p>
</div>
<span style={{ color: 'rgba(255, 255, 255, 0.4)' }} className="text-xs">
{item.time}
</span>
</div>
))}
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,226 @@
import { useState, useEffect, useCallback } from 'react'
import { invoke } from '@tauri-apps/api/tauri'
import { Search, Database, TrendingUp, Package, ExternalLink } from 'lucide-react'
interface SearchResult {
id: string
name: string
type: string
category: string
value?: number
markup?: number
icon?: string
}
export default function UniversalSearch() {
const [query, setQuery] = useState('')
const [results, setResults] = useState<SearchResult[]>([])
const [loading, setLoading] = useState(false)
const [selectedCategory, setSelectedCategory] = useState('all')
const categories = [
{ id: 'all', label: 'All', icon: <Database className="w-4 h-4" /> },
{ id: 'items', label: 'Items', icon: <Package className="w-4 h-4" /> },
{ id: 'mobs', label: 'Creatures', icon: <TrendingUp className="w-4 h-4" /> },
{ id: 'locations', label: 'Locations', icon: <ExternalLink className="w-4 h-4" /> },
]
const performSearch = useCallback(async (searchQuery: string) => {
if (!searchQuery.trim()) {
setResults([])
return
}
setLoading(true)
try {
const searchResults = await invoke<SearchResult[]>('search_nexus', {
query: searchQuery,
entityType: selectedCategory === 'all' ? undefined : selectedCategory,
limit: 20
})
setResults(searchResults)
} catch (error) {
console.error('Search failed:', error)
setResults([])
} finally {
setLoading(false)
}
}, [selectedCategory])
useEffect(() => {
const timeoutId = setTimeout(() => {
performSearch(query)
}, 300)
return () => clearTimeout(timeoutId)
}, [query, performSearch])
const getTypeColor = (type: string) => {
const colors: Record<string, string> = {
weapon: '#ef4444',
armor: '#3b82f6',
tool: '#10b981',
material: '#f59e0b',
creature: '#8b5cf6',
location: '#06b6d4'
}
return colors[type] || '#64748b'
}
return (
<div className="p-6 animate-fade-in h-full flex flex-col">
{/* Header */}
<div className="mb-6">
<h1 className="text-3xl font-bold text-white mb-2">Universal Search</h1>
<p style={{ color: 'rgba(255, 255, 255, 0.5)' }}>
Search items, creatures, locations and more
</p>
</div>
{/* Search Bar */}
<div className="mb-6">
<div
className="relative"
style={{
background: 'rgba(255, 255, 255, 0.05)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px'
}}
>
<Search
className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5"
style={{ color: 'rgba(255, 255, 255, 0.4)' }}
/>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search for weapons, armor, creatures..."
className="w-full pl-12 pr-4 py-4 bg-transparent border-none text-white text-lg focus:outline-none"
style={{ fontSize: '16px' }}
/>
{loading && (
<div className="absolute right-4 top-1/2 -translate-y-1/2">
<div
className="w-5 h-5 border-2 rounded-full animate-spin"
style={{
borderColor: 'rgba(99, 102, 241, 0.3)',
borderTopColor: '#6366f1'
}}
/>
</div>
)}
</div>
</div>
{/* Category Filters */}
<div className="flex gap-2 mb-6">
{categories.map(cat => (
<button
key={cat.id}
onClick={() => setSelectedCategory(cat.id)}
className="flex items-center gap-2 px-4 py-2 rounded-lg transition-all"
style={{
background: selectedCategory === cat.id
? 'rgba(99, 102, 241, 0.2)'
: 'rgba(255, 255, 255, 0.05)',
border: `1px solid ${selectedCategory === cat.id ? 'rgba(99, 102, 241, 0.3)' : 'rgba(255, 255, 255, 0.1)'}`,
color: selectedCategory === cat.id ? '#818cf8' : 'rgba(255, 255, 255, 0.6)'
}}
>
{cat.icon}
<span className="text-sm font-medium">{cat.label}</span>
</button>
))}
</div>
{/* Results */}
<div className="flex-1 overflow-y-auto">
{results.length > 0 ? (
<div className="space-y-2">
{results.map((result) => (
<div
key={result.id}
className="flex items-center gap-4 p-4 rounded-xl transition-all hover:bg-white/5 cursor-pointer group"
style={{ background: 'rgba(255, 255, 255, 0.03)' }}
>
{/* Icon/Type Badge */}
<div
className="w-12 h-12 rounded-lg flex items-center justify-center text-lg font-bold"
style={{
background: `${getTypeColor(result.type)}20`,
color: getTypeColor(result.type)
}}
>
{result.name.charAt(0).toUpperCase()}
</div>
{/* Content */}
<div className="flex-1">
<h3 className="text-white font-medium mb-1">{result.name}</h3>
<div className="flex items-center gap-2">
<span
className="text-xs px-2 py-0.5 rounded"
style={{
background: `${getTypeColor(result.type)}20`,
color: getTypeColor(result.type)
}}
>
{result.type}
</span>
<span style={{ color: 'rgba(255, 255, 255, 0.4)' }} className="text-xs">
{result.category}
</span>
</div>
</div>
{/* Value/Markup */}
{(result.value || result.markup) && (
<div className="text-right">
{result.value && (
<p className="text-white font-medium">
{result.value.toFixed(2)} PED
</p>
)}
{result.markup && (
<p
className="text-sm"
style={{ color: result.markup > 100 ? '#10b981' : '#ef4444' }}
>
{result.markup.toFixed(1)}% MU
</p>
)}
</div>
)}
{/* Arrow */}
<ExternalLink
className="w-5 h-5 opacity-0 group-hover:opacity-100 transition-opacity"
style={{ color: 'rgba(255, 255, 255, 0.4)' }}
/>
</div>
))}
</div>
) : query ? (
<div className="text-center py-12">
<Search className="w-16 h-16 mx-auto mb-4" style={{ color: 'rgba(255, 255, 255, 0.1)' }} />
<p style={{ color: 'rgba(255, 255, 255, 0.4)' }} className="text-lg mb-2">
No results found
</p>
<p style={{ color: 'rgba(255, 255, 255, 0.3)' }} className="text-sm">
Try searching for "weapons", "armor", or "creatures"
</p>
</div>
) : (
<div className="text-center py-12">
<Database className="w-16 h-16 mx-auto mb-4" style={{ color: 'rgba(255, 255, 255, 0.1)' }} />
<p style={{ color: 'rgba(255, 255, 255, 0.4)' }} className="text-lg">
Start typing to search the Nexus database
</p>
</div>
)}
</div>
</div>
)
}