- 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
414 lines
13 KiB
Vue
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>
|