Ziya/app/pages/hunting-ground.vue
rizary 6efcf43691
feat: complete ESLint configuration overhaul and theme system improvements
- Migrate from legacy .eslintrc.json to modern flat config system
- Remove conflicting ESLint configuration files
- Fix auto-generation of eslint.config.mjs by Nuxt
- Update ESLint rules to use single quotes and proper formatting
- Add comprehensive theme switching system with 24 palettes
- Implement proper daisyUI theme integration
- Add theme store with persistence and dark/light mode support
- Create ThemeSwitcher component with enhanced UI
- Fix package.json scripts to work with new ESLint flat config
- Update VS Code settings for proper ESLint integration
- Add changelogen scripts for proper changelog management

BREAKING CHANGE: ESLint configuration migrated to flat config system
2025-06-22 00:53:24 +07:00

414 lines
13 KiB
Vue

<template>
<div class="desktop-container">
<!-- Top bar with user info -->
<div class="navbar bg-base-300 px-4">
<div class="navbar-start">
<div class="text-xl font-bold">Hunting Ground</div>
</div>
<div class="navbar-end">
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar">
<div class="w-10 rounded-full bg-primary flex items-center justify-center">
<span class="text-primary-content font-bold text-sm">
{{ appStore.userInitials }}
</span>
</div>
</div>
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52">
<li><a @click="navigateToProfile">Profile</a></li>
<li><a>Settings</a></li>
<li><a @click="handleLogout">Logout</a></li>
</ul>
</div>
</div>
</div>
<!-- Main content area -->
<div class="flex-1 flex overflow-hidden">
<!-- Sidebar -->
<div class="w-64 bg-base-200 p-4">
<ul class="menu">
<li><a @click="navigateToDashboard">Dashboard</a></li>
<li><a @click="navigateToProfile">Profile</a></li>
<li><a>Trading</a></li>
<li><a>Portfolio</a></li>
<li><a>Markets</a></li>
<li><a class="active">Hunting Ground</a></li>
<li><a>Analytics</a></li>
</ul>
</div>
<!-- Main content - Three columns -->
<div class="flex-1 p-4 overflow-hidden">
<div class="grid grid-cols-3 gap-4 h-full">
<!-- New Tokens Column -->
<div class="bg-base-100 rounded-lg shadow-lg flex flex-col">
<div class="p-4 border-b border-base-200">
<h3 class="text-lg font-bold text-primary">New Tokens</h3>
<p class="text-sm text-base-content/70">{{ newTokens.length }} tokens found</p>
</div>
<div class="flex-1 overflow-y-auto p-2 scrollbar-thin">
<div class="space-y-3">
<div
v-for="token in newTokens"
:key="token.mint"
class="card bg-base-200 shadow cursor-pointer hover:shadow-lg transition-all duration-200 hover:bg-base-300"
@click="openToken(token.bonding_curve)"
>
<div class="card-body p-4">
<div class="flex items-center justify-between mb-2">
<h4 class="card-title text-sm">{{ token.name }}</h4>
<div class="badge badge-primary badge-sm">{{ token.symbol }}</div>
</div>
<div class="text-xs text-base-content/70 space-y-1">
<div class="flex justify-between">
<span>Mint:</span>
<span class="font-mono">{{ truncateAddress(token.mint) }}</span>
</div>
<div class="flex justify-between">
<span>Creator:</span>
<span class="font-mono">{{ truncateAddress(token.creator) }}</span>
</div>
<div class="flex justify-between">
<span>Time:</span>
<span>{{ formatTime(token.created_at) }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- CEX Found Column -->
<div class="bg-base-100 rounded-lg shadow-lg flex flex-col">
<div class="p-4 border-b border-base-200">
<h3 class="text-lg font-bold text-secondary">CEX Found</h3>
<p class="text-sm text-base-content/70">{{ cexTokens.length }} CEX connections</p>
</div>
<div class="flex-1 overflow-y-auto p-2 scrollbar-thin">
<div class="space-y-3">
<div
v-for="token in cexTokens"
:key="token.mint"
:class="getCexCardClass(token.data.cex_name)"
class="card shadow cursor-pointer hover:shadow-lg transition-all duration-200"
@click="openToken(token.data.bonding_curve)"
>
<div class="card-body p-4">
<div class="flex items-center justify-between mb-2">
<h4 class="card-title text-sm text-white">{{ token.data.name }}</h4>
<div class="badge badge-sm text-white border-white/20">{{ getCexDisplayName(token.data.cex_name) }}</div>
</div>
<div class="text-xs text-white/80 space-y-1">
<div class="flex justify-between">
<span>Mint:</span>
<span class="font-mono">{{ truncateAddress(token.data.mint) }}</span>
</div>
<div class="flex justify-between">
<span>CEX Address:</span>
<span class="font-mono">{{ truncateAddress(token.data.cex_address) }}</span>
</div>
<div class="flex justify-between">
<span>Dev:</span>
<span>{{ token.data.dev_name || 'Unknown' }}</span>
</div>
<div class="flex justify-between">
<span>Nodes:</span>
<span>{{ token.data.node_count || 0 }}</span>
</div>
<div class="flex justify-between">
<span>Time:</span>
<span>{{ formatTime(token.data.cex_updated_at) }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Dev Tracker Column -->
<div class="bg-base-100 rounded-lg shadow-lg flex flex-col">
<div class="p-4 border-b border-base-200">
<h3 class="text-lg font-bold text-accent">Dev Tracker</h3>
<p class="text-sm text-base-content/70">{{ maxDepthTokens.length }} analysis complete</p>
</div>
<div class="flex-1 overflow-y-auto p-2 scrollbar-thin">
<div class="space-y-3">
<div
v-for="token in maxDepthTokens"
:key="token.mint"
class="card bg-base-200 shadow cursor-pointer hover:shadow-lg transition-all duration-200 hover:bg-base-300"
@click="openToken(token.data.bonding_curve)"
>
<div class="card-body p-4">
<div class="flex items-center justify-between mb-2">
<h4 class="card-title text-sm">{{ token.data.name }}</h4>
<div class="badge badge-accent badge-sm">Complete</div>
</div>
<div class="text-xs text-base-content/70 space-y-1">
<div class="flex justify-between">
<span>Mint:</span>
<span class="font-mono">{{ truncateAddress(token.data.mint) }}</span>
</div>
<div class="flex justify-between">
<span>Nodes:</span>
<span>{{ token.data.node_count || 0 }}</span>
</div>
<div class="flex justify-between">
<span>Edges:</span>
<span>{{ token.data.edge_count || 0 }}</span>
</div>
<div class="flex justify-between">
<span>Time:</span>
<span>{{ formatTime(token.data.updated_at) }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface RedisMessage {
channel: string;
data: any;
timestamp: number;
}
interface NewTokenData {
mint: string;
bonding_curve?: string;
name: string;
symbol: string;
uri: string;
creator: string;
created_at: number;
}
interface CexTokenData {
mint: string;
name: string;
uri: string;
dev_name: string;
cex_name: string;
cex_address: string;
cex_updated_at: number;
node_count: number;
edge_count: number;
bonding_curve?: string;
}
interface MaxDepthTokenData {
mint: string;
name: string;
uri: string;
bonding_curve: string;
updated_at: string;
node_count: number;
edge_count: number;
}
const appStore = useAppStore();
const router = useRouter();
// Reactive data for three columns
const newTokens = ref<NewTokenData[]>([]);
const cexTokens = ref<Array<{ mint: string, data: CexTokenData }>>([]);
const maxDepthTokens = ref<Array<{ mint: string, data: MaxDepthTokenData }>>([]);
// Redirect if not authenticated
onMounted(() => {
if (!appStore.isAuthenticated) {
router.push('/login');
}
// Set up Redis pubsub listener
if (window.electronAPI) {
window.electronAPI.onRedisData(handleRedisMessage);
}
});
onUnmounted(() => {
// Clean up Redis listener
if (window.electronAPI) {
window.electronAPI.removeRedisDataListener();
}
});
const handleRedisMessage = (message: RedisMessage) => {
console.log('Received Redis message:', message);
switch (message.channel) {
case 'new_token_created':
addNewToken(message.data);
break;
case 'token_cex_updated':
addCexToken(message.data);
break;
case 'max_depth_reached':
addMaxDepthToken(message.data);
break;
}
};
const addNewToken = (data: NewTokenData) => {
// Add to front of array (latest first)
newTokens.value.unshift(data);
// Keep only latest 50 items for performance
if (newTokens.value.length > 50) {
newTokens.value = newTokens.value.slice(0, 50);
}
};
const addCexToken = (data: CexTokenData) => {
// Add to front of array (latest first)
cexTokens.value.unshift({
mint: data.mint,
data,
});
// Keep only latest 50 items for performance
if (cexTokens.value.length > 50) {
cexTokens.value = cexTokens.value.slice(0, 50);
}
};
const addMaxDepthToken = (data: MaxDepthTokenData) => {
// Add to front of array (latest first)
maxDepthTokens.value.unshift({
mint: data.mint,
data,
});
// Keep only latest 50 items for performance
if (maxDepthTokens.value.length > 50) {
maxDepthTokens.value = maxDepthTokens.value.slice(0, 50);
}
};
const getCexCardClass = (cexName: string): string => {
const name = cexName?.toLowerCase() || '';
if (name.includes('coinbase')) {
return 'bg-blue-600 hover:bg-blue-700';
}
else if (name.includes('mexc')) {
return 'bg-green-600 hover:bg-green-700';
}
else if (name.includes('binance')) {
return 'bg-yellow-600 hover:bg-yellow-700';
}
else if (name.includes('okx') || name.includes('okex')) {
return 'bg-gray-600 hover:bg-gray-700';
}
else if (name.includes('bybit')) {
return 'bg-purple-600 hover:bg-purple-700';
}
else if (name.includes('kucoin')) {
return 'bg-emerald-600 hover:bg-emerald-700';
}
else if (name.includes('gate')) {
return 'bg-red-600 hover:bg-red-700';
}
else {
return 'bg-slate-600 hover:bg-slate-700';
}
};
const getCexDisplayName = (cexName: string): string => {
const name = cexName?.toLowerCase() || '';
if (name.includes('coinbase')) {
return 'Coinbase';
}
else if (name.includes('mexc')) {
return 'MEXC';
}
else if (name.includes('binance')) {
return 'Binance';
}
else if (name.includes('okx') || name.includes('okex')) {
return 'OKX';
}
else if (name.includes('bybit')) {
return 'Bybit';
}
else if (name.includes('kucoin')) {
return 'KuCoin';
}
else if (name.includes('gate')) {
return 'Gate.io';
}
else {
return cexName || 'Unknown';
}
};
const truncateAddress = (address: string): string => {
if (!address) return '';
return `${address.slice(0, 4)}...${address.slice(-4)}`;
};
const formatTime = (timestamp: number | string): string => {
let time: number;
if (typeof timestamp === 'string') {
time = parseInt(timestamp);
}
else {
time = timestamp;
}
const date = new Date(time * 1000);
const now = new Date();
const diff = now.getTime() - date.getTime();
if (diff < 60000) { // Less than 1 minute
return 'Just now';
}
else if (diff < 3600000) { // Less than 1 hour
const minutes = Math.floor(diff / 60000);
return `${minutes}m ago`;
}
else if (diff < 86400000) { // Less than 1 day
const hours = Math.floor(diff / 3600000);
return `${hours}h ago`;
}
else {
return date.toLocaleDateString();
}
};
const openToken = (bondingCurve: string | undefined) => {
if (bondingCurve && window.electronAPI) {
const url = `https://axiom.trade/meme/${bondingCurve}`;
window.electronAPI.openExternal(url);
}
};
const handleLogout = async () => {
await appStore.logout();
router.push('/login');
};
const navigateToDashboard = () => {
router.push('/dashboard');
};
const navigateToProfile = () => {
router.push('/profile');
};
</script>
<style scoped>
/* Page-specific styles if needed */
</style>