Upload complete FlowState project source code
Browse files- src/app/(dashboard)/agent/page.tsx +79 -0
- src/app/(dashboard)/analytics/page.tsx +97 -0
- src/app/(dashboard)/campaigns/page.tsx +59 -0
- src/app/(dashboard)/layout.tsx +14 -0
- src/app/(dashboard)/leaderboard/page.tsx +69 -0
- src/app/(dashboard)/page.tsx +58 -0
- src/app/(dashboard)/wallets/page.tsx +67 -0
- src/app/api/agent/scan/route.ts +3 -0
- src/app/api/torque/campaigns/route.ts +3 -0
- src/app/api/torque/events/route.ts +3 -0
- src/components/layout/Sidebar.tsx +48 -0
- src/components/layout/Topbar.tsx +23 -0
- src/components/ui/StatCard.tsx +25 -0
src/app/(dashboard)/agent/page.tsx
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
import { cn } from '@/lib/utils'
|
| 3 |
+
import { Brain, Zap, Shield, Eye, Play, Pause, Bot, CheckCircle, ArrowRight, Cpu, RefreshCw, GitBranch, Target, Activity, AlertTriangle } from 'lucide-react'
|
| 4 |
+
import { useState, useEffect } from 'react'
|
| 5 |
+
|
| 6 |
+
const msgs = ['🔍 Scanning 312,847 active wallets...', '⚠️ Critical: 7xKXtg...AsU inactive 10d', '🎁 Gift sent: 0.5 SOL via Torque MCP', '📊 Leaderboard: 12,847 scored', '🎟️ Raffle: 9WzDXw...WWM 3x tickets', '🔥 Comeback: HN7cAB...WrH after 12d', '💰 Rebate 2x activated for HN7cAB...WrH', '🤖 Auto-creating Weekend Streak Challenge', '✅ Campaign created — Budget: 5,000 SOL', '📈 Retention up 0.5%', '🛡️ Sybil check: 99.7% legit', '🎯 Targeting 1,234 wallets']
|
| 7 |
+
|
| 8 |
+
export default function AgentPage() {
|
| 9 |
+
const [running, setRunning] = useState(true)
|
| 10 |
+
const [tab, setTab] = useState<'feed' | 'config'>('feed')
|
| 11 |
+
const [feed, setFeed] = useState<{text:string;time:string}[]>([])
|
| 12 |
+
|
| 13 |
+
useEffect(() => {
|
| 14 |
+
const init = msgs.slice(0,6).map((t,i) => ({text:t, time: new Date(Date.now()-i*120000).toLocaleTimeString('en-US',{hour12:false})}))
|
| 15 |
+
setFeed(init)
|
| 16 |
+
if (!running) return
|
| 17 |
+
let c = 6
|
| 18 |
+
const iv = setInterval(() => { const t = msgs[c++ % msgs.length]; setFeed(p => [{text:t, time: new Date().toLocaleTimeString('en-US',{hour12:false})}, ...p].slice(0,25)) }, 3000)
|
| 19 |
+
return () => clearInterval(iv)
|
| 20 |
+
}, [running])
|
| 21 |
+
|
| 22 |
+
return (
|
| 23 |
+
<div className="p-6 space-y-6">
|
| 24 |
+
<div className="flex items-center justify-between">
|
| 25 |
+
<div className="flex items-center gap-4">
|
| 26 |
+
<div className={cn('w-12 h-12 rounded-xl flex items-center justify-center', running ? 'bg-trading-up/10 animate-pulse-glow' : 'bg-surface-elevated')}><Brain className={cn('w-6 h-6', running ? 'text-trading-up' : 'text-muted')} /></div>
|
| 27 |
+
<div><h1 className="text-display-sm text-[#eaecef]">AI Agent</h1><p className="text-body-md text-muted mt-0.5">Autonomous churn detection & retention engine</p></div>
|
| 28 |
+
</div>
|
| 29 |
+
<div className="flex items-center gap-3">
|
| 30 |
+
<div className={cn('px-4 py-2 rounded-lg border', running ? 'bg-trading-up/10 border-trading-up/30 text-trading-up' : 'bg-surface-card border-hairline-dark text-muted')}><div className="flex items-center gap-2"><div className={cn('w-2.5 h-2.5 rounded-full', running ? 'bg-trading-up animate-pulse' : 'bg-muted')} /><span className="text-button font-semibold">{running ? 'RUNNING' : 'PAUSED'}</span></div></div>
|
| 31 |
+
<button onClick={() => setRunning(!running)} className={cn('flex items-center gap-2 px-5 py-2.5 rounded-md text-button font-semibold transition', running ? 'bg-trading-down/10 text-trading-down border border-trading-down/30' : 'bg-brand-yellow text-ink')}>{running ? <><Pause className="w-4 h-4" />Pause</> : <><Play className="w-4 h-4" />Start</>}</button>
|
| 32 |
+
</div>
|
| 33 |
+
</div>
|
| 34 |
+
<div className="grid grid-cols-4 gap-4">
|
| 35 |
+
{[{i:Activity,l:'Actions Today',v:'3,847',c:'text-brand-yellow'},{i:Eye,l:'Wallets Scanned',v:'312.8K',c:'text-info'},{i:Shield,l:'Wallets Saved',v:'15.2K',c:'text-trading-up'},{i:Target,l:'Churn Prevented',v:'23.9K',c:'text-brand-yellow'}].map(s => { const I = s.i; return (
|
| 36 |
+
<div key={s.l} className="rounded-xl bg-surface-card border border-hairline-dark p-4"><div className="flex items-center gap-2 mb-2"><I className={cn('w-4 h-4', s.c)} /><span className="text-caption text-muted">{s.l}</span></div><p className={cn('font-mono text-title-lg tabular-nums', s.c === 'text-info' ? 'text-[#eaecef]' : s.c)}>{s.v}</p></div>
|
| 37 |
+
)})}
|
| 38 |
+
</div>
|
| 39 |
+
<div className="flex items-center gap-1 p-1 rounded-lg bg-surface-card border border-hairline-dark w-fit">
|
| 40 |
+
{(['feed','config'] as const).map(t => <button key={t} onClick={() => setTab(t)} className={cn('px-5 py-2 rounded-md text-button transition capitalize', tab === t ? 'bg-brand-yellow text-ink font-semibold' : 'text-muted hover:text-[#eaecef]')}>{t === 'feed' ? 'Live Feed' : 'Configuration'}</button>)}
|
| 41 |
+
</div>
|
| 42 |
+
{tab === 'feed' && (
|
| 43 |
+
<div className="rounded-xl bg-surface-card border border-hairline-dark overflow-hidden">
|
| 44 |
+
<div className="p-4 border-b border-hairline-dark flex items-center justify-between"><div className="flex items-center gap-2"><Bot className="w-4 h-4 text-brand-yellow" /><span className="text-title-sm">Real-Time Agent Feed</span>{running && <div className="w-2 h-2 rounded-full bg-trading-up animate-pulse" />}</div><span className="text-caption text-muted">{feed.length} messages</span></div>
|
| 45 |
+
<div className="max-h-[500px] overflow-y-auto divide-y divide-hairline-dark/50">{feed.map((m,i) => (
|
| 46 |
+
<div key={`${m.time}-${i}`} className={cn('flex items-start gap-4 px-5 py-3', i===0 && running && 'animate-slide-up bg-brand-yellow/5')}><span className="font-mono text-caption text-muted tabular-nums whitespace-nowrap mt-0.5">{m.time}</span><span className="text-body-sm text-[#eaecef]">{m.text}</span></div>
|
| 47 |
+
))}</div>
|
| 48 |
+
</div>
|
| 49 |
+
)}
|
| 50 |
+
{tab === 'config' && (
|
| 51 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
| 52 |
+
<div className="rounded-xl bg-surface-card border border-hairline-dark p-5">
|
| 53 |
+
<h3 className="text-title-sm mb-4 flex items-center gap-2"><AlertTriangle className="w-4 h-4 text-brand-yellow" />Detection Thresholds</h3>
|
| 54 |
+
{[{l:'critical',d:10,v:90},{l:'high',d:7,v:60},{l:'medium',d:5,v:30}].map(t => (
|
| 55 |
+
<div key={t.l} className="p-3 rounded-lg bg-surface-elevated border border-hairline-dark/50 mb-3">
|
| 56 |
+
<span className={cn('text-caption font-semibold uppercase', t.l==='critical'?'text-trading-down':t.l==='high'?'text-[#ff9500]':'text-brand-yellow')}>{t.l}</span>
|
| 57 |
+
<div className="grid grid-cols-2 gap-3 mt-2"><div><span className="text-caption text-muted">Inactive Days</span><p className="font-mono text-num-md text-[#eaecef]">≥ {t.d}</p></div><div><span className="text-caption text-muted">Volume Drop</span><p className="font-mono text-num-md text-[#eaecef]">≥ {t.v}%</p></div></div>
|
| 58 |
+
</div>
|
| 59 |
+
))}
|
| 60 |
+
</div>
|
| 61 |
+
<div className="rounded-xl bg-surface-card border border-hairline-dark p-5">
|
| 62 |
+
<h3 className="text-title-sm mb-4 flex items-center gap-2"><Cpu className="w-4 h-4 text-brand-yellow" />Agent Config</h3>
|
| 63 |
+
{[['Scan Interval','30s'],['Protocols','6'],['Torque MCP','Connected'],['Helius','Active'],['Sybil Filter','Enabled']].map(([k,v]) => (
|
| 64 |
+
<div key={k} className="flex items-center justify-between py-2 border-b border-hairline-dark/50"><span className="text-body-sm text-muted">{k}</span><span className={cn('font-mono text-num-sm', v==='Connected'||v==='Active'||v==='Enabled' ? 'text-trading-up font-semibold' : 'text-[#eaecef]')}>{v}</span></div>
|
| 65 |
+
))}
|
| 66 |
+
</div>
|
| 67 |
+
</div>
|
| 68 |
+
)}
|
| 69 |
+
<div className="rounded-xl bg-brand-yellow/5 border border-brand-yellow/20 p-6">
|
| 70 |
+
<h3 className="text-title-sm text-brand-yellow mb-3">How FlowState AI Agent Works</h3>
|
| 71 |
+
<div className="grid grid-cols-5 gap-3 items-center">
|
| 72 |
+
{[{i:Eye,l:'Monitor',d:'Helius webhooks scan txns'},{i:Brain,l:'Detect',d:'AI scores churn risk'},{i:Zap,l:'Decide',d:'Select incentive type'},{i:Bot,l:'Execute',d:'Fire via Torque MCP'},{i:RefreshCw,l:'Learn',d:'Track outcomes'}].map((s,i) => (
|
| 73 |
+
<div key={s.l} className="text-center"><div className="w-10 h-10 rounded-lg bg-surface-card border border-brand-yellow/30 flex items-center justify-center mx-auto mb-2"><s.i className="w-5 h-5 text-brand-yellow" /></div><p className="text-caption text-brand-yellow font-semibold">{s.l}</p><p className="text-[10px] text-muted mt-0.5">{s.d}</p></div>
|
| 74 |
+
))}
|
| 75 |
+
</div>
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
)
|
| 79 |
+
}
|
src/app/(dashboard)/analytics/page.tsx
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
import { TrendingUp, TrendingDown, Target, Zap, Award, Clock } from 'lucide-react'
|
| 3 |
+
import { cn } from '@/lib/utils'
|
| 4 |
+
|
| 5 |
+
const cohorts = [
|
| 6 |
+
{ week: 'Mar 3', d1: 100, d7: 72, d14: 58, d30: 41, d60: 28 },
|
| 7 |
+
{ week: 'Mar 10', d1: 100, d7: 74, d14: 61, d30: 44, d60: 31 },
|
| 8 |
+
{ week: 'Mar 17', d1: 100, d7: 76, d14: 63, d30: 47, d60: 33 },
|
| 9 |
+
{ week: 'Mar 24', d1: 100, d7: 78, d14: 65, d30: 49, d60: 35 },
|
| 10 |
+
{ week: 'Mar 31', d1: 100, d7: 79, d14: 67, d30: 52, d60: 0 },
|
| 11 |
+
{ week: 'Apr 7', d1: 100, d7: 81, d14: 69, d30: 0, d60: 0 },
|
| 12 |
+
{ week: 'Apr 14', d1: 100, d7: 83, d14: 0, d30: 0, d60: 0 },
|
| 13 |
+
{ week: 'Apr 21', d1: 100, d7: 0, d14: 0, d30: 0, d60: 0 },
|
| 14 |
+
]
|
| 15 |
+
|
| 16 |
+
const events = [
|
| 17 |
+
{ event: 'churn_risk_high', count: 2341, color: '#f6465d', max: 34567 },
|
| 18 |
+
{ event: 'churn_risk_medium', count: 5678, color: '#ff9500', max: 34567 },
|
| 19 |
+
{ event: 'comeback_detected', count: 8923, color: '#0ecb81', max: 34567 },
|
| 20 |
+
{ event: 'streak_maintained', count: 34567, color: '#FCD535', max: 34567 },
|
| 21 |
+
{ event: 'volume_milestone', count: 12345, color: '#2dbdb6', max: 34567 },
|
| 22 |
+
{ event: 'referral_from_saved', count: 4567, color: '#a78bfa', max: 34567 },
|
| 23 |
+
{ event: 'inactivity_detected', count: 21011, color: '#707a8a', max: 34567 },
|
| 24 |
+
]
|
| 25 |
+
|
| 26 |
+
function fmt(n: number) { return n >= 1000 ? `${(n/1000).toFixed(1)}K` : String(n) }
|
| 27 |
+
|
| 28 |
+
function getColor(v: number) {
|
| 29 |
+
if (v === 0) return 'bg-surface-elevated text-muted'
|
| 30 |
+
if (v >= 75) return 'bg-trading-up/20 text-trading-up'
|
| 31 |
+
if (v >= 50) return 'bg-brand-yellow/15 text-brand-yellow'
|
| 32 |
+
if (v >= 30) return 'bg-[#ff9500]/15 text-[#ff9500]'
|
| 33 |
+
return 'bg-trading-down/15 text-trading-down'
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
export default function AnalyticsPage() {
|
| 37 |
+
return (
|
| 38 |
+
<div className="p-6 space-y-6">
|
| 39 |
+
<div><h1 className="text-display-sm text-[#eaecef]">Analytics</h1><p className="text-body-md text-muted mt-1">Deep retention insights, cohort analysis & ROI tracking</p></div>
|
| 40 |
+
<div className="grid grid-cols-2 lg:grid-cols-5 gap-4">
|
| 41 |
+
{[
|
| 42 |
+
{ label: 'Avg Retention', value: '67.3%', change: '+9.6%', up: true, icon: Target },
|
| 43 |
+
{ label: 'Churn Rate', value: '4.2%', change: '-4.3%', up: false, icon: TrendingDown },
|
| 44 |
+
{ label: 'Campaign ROI', value: '847%', change: '+15.2%', up: true, icon: TrendingUp },
|
| 45 |
+
{ label: 'Saved Wallets', value: '15.2K', change: '+23.4%', up: true, icon: Award },
|
| 46 |
+
{ label: 'Agent Actions', value: '3,847', change: '+12.1%', up: true, icon: Zap },
|
| 47 |
+
].map(k => { const I = k.icon; return (
|
| 48 |
+
<div key={k.label} className="rounded-xl bg-surface-card border border-hairline-dark p-4">
|
| 49 |
+
<div className="flex items-center justify-between mb-2"><span className="text-caption text-muted">{k.label}</span><I className="w-4 h-4 text-muted" /></div>
|
| 50 |
+
<div className="font-mono text-title-lg text-[#eaecef] tabular-nums">{k.value}</div>
|
| 51 |
+
<span className={cn('font-mono text-num-sm', k.up ? 'text-trading-up' : 'text-trading-down')}>{k.change}</span>
|
| 52 |
+
</div>
|
| 53 |
+
)})}
|
| 54 |
+
</div>
|
| 55 |
+
<div className="rounded-xl bg-surface-card border border-hairline-dark p-5">
|
| 56 |
+
<div className="flex items-center justify-between mb-4">
|
| 57 |
+
<div><h3 className="text-title-sm">Retention Cohorts</h3><p className="text-caption text-muted mt-0.5">FlowState launched Mar 31</p></div>
|
| 58 |
+
<div className="flex items-center gap-2">
|
| 59 |
+
<div className="flex items-center gap-1"><div className="w-3 h-3 rounded-sm bg-trading-down/30" /><span className="text-[10px] text-muted">Low</span></div>
|
| 60 |
+
<div className="flex items-center gap-1"><div className="w-3 h-3 rounded-sm bg-brand-yellow/50" /><span className="text-[10px] text-muted">Mid</span></div>
|
| 61 |
+
<div className="flex items-center gap-1"><div className="w-3 h-3 rounded-sm bg-trading-up/80" /><span className="text-[10px] text-muted">High</span></div>
|
| 62 |
+
</div>
|
| 63 |
+
</div>
|
| 64 |
+
<table className="w-full">
|
| 65 |
+
<thead><tr className="border-b border-hairline-dark">
|
| 66 |
+
<th className="text-left text-caption text-muted uppercase px-4 py-2">Cohort</th>
|
| 67 |
+
<th className="text-center text-caption text-muted uppercase px-4 py-2">Day 1</th>
|
| 68 |
+
<th className="text-center text-caption text-muted uppercase px-4 py-2">Day 7</th>
|
| 69 |
+
<th className="text-center text-caption text-muted uppercase px-4 py-2">Day 14</th>
|
| 70 |
+
<th className="text-center text-caption text-muted uppercase px-4 py-2">Day 30</th>
|
| 71 |
+
<th className="text-center text-caption text-muted uppercase px-4 py-2">Day 60</th>
|
| 72 |
+
</tr></thead>
|
| 73 |
+
<tbody>{cohorts.map(c => (
|
| 74 |
+
<tr key={c.week} className="border-b border-hairline-dark/50">
|
| 75 |
+
<td className="px-4 py-2"><div className="flex items-center gap-2"><Clock className="w-3.5 h-3.5 text-muted" /><span className="text-body-sm text-muted font-mono">{c.week}</span></div></td>
|
| 76 |
+
{[c.d1, c.d7, c.d14, c.d30, c.d60].map((v, i) => (
|
| 77 |
+
<td key={i} className="px-4 py-2 text-center"><span className={cn('inline-block min-w-[48px] px-2 py-1 rounded font-mono text-num-sm tabular-nums', getColor(v))}>{v === 0 ? '\u2014' : `${v}%`}</span></td>
|
| 78 |
+
))}
|
| 79 |
+
</tr>
|
| 80 |
+
))}</tbody>
|
| 81 |
+
</table>
|
| 82 |
+
<div className="mt-4 p-3 rounded-lg bg-trading-up/5 border border-trading-up/20">
|
| 83 |
+
<p className="text-body-sm text-trading-up">📈 <strong>FlowState Impact:</strong> Cohorts after Mar 31 show +7pp higher day-7 retention and +11pp higher day-14 retention.</p>
|
| 84 |
+
</div>
|
| 85 |
+
</div>
|
| 86 |
+
<div className="rounded-xl bg-surface-card border border-hairline-dark p-5">
|
| 87 |
+
<h3 className="text-title-sm mb-4">Torque Custom Events Breakdown</h3>
|
| 88 |
+
<div className="space-y-4">{events.map(e => (
|
| 89 |
+
<div key={e.event} className="space-y-1.5">
|
| 90 |
+
<div className="flex items-center justify-between"><code className="font-mono text-caption text-muted">{e.event}</code><span className="font-mono text-num-sm text-[#eaecef] tabular-nums">{fmt(e.count)}</span></div>
|
| 91 |
+
<div className="h-2 rounded-full bg-surface-elevated overflow-hidden"><div className="h-full rounded-full transition-all duration-700" style={{ width: `${(e.count / e.max) * 100}%`, backgroundColor: e.color }} /></div>
|
| 92 |
+
</div>
|
| 93 |
+
))}</div>
|
| 94 |
+
</div>
|
| 95 |
+
</div>
|
| 96 |
+
)
|
| 97 |
+
}
|
src/app/(dashboard)/campaigns/page.tsx
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
import { Plus, Bot, Users, Activity, DollarSign, Trophy, Gift, Ticket, Percent } from 'lucide-react'
|
| 3 |
+
import { cn } from '@/lib/utils'
|
| 4 |
+
|
| 5 |
+
const campaigns = [
|
| 6 |
+
{ id: '1', name: 'Weekly Volume Champions', type: 'leaderboard' as const, status: 'active', desc: 'Top 50 traders by weekly swap volume earn SOL rewards', budget: 50000, participants: 12847, events: 89432, ai: true, formula: 'SUM(swap_volume)' },
|
| 7 |
+
{ id: '2', name: 'Comeback Raffle', type: 'raffle' as const, status: 'active', desc: 'Returning users after 7+ days get raffle tickets', budget: 25000, participants: 3456, events: 8923, ai: true },
|
| 8 |
+
{ id: '3', name: 'Anti-Churn Gift Drop', type: 'gift' as const, status: 'active', desc: 'High churn risk wallets receive 0.5 SOL gift', budget: 15000, participants: 1234, events: 4567, ai: true },
|
| 9 |
+
{ id: '4', name: 'Streak Multiplier Rebate', type: 'rebate' as const, status: 'active', desc: '7+ day streak unlocks 2x fee rebate for 48h', budget: 75000, participants: 8932, events: 45678, ai: true, formula: 'streak_days >= 7' },
|
| 10 |
+
{ id: '5', name: 'DeFi Explorer Rewards', type: 'leaderboard' as const, status: 'active', desc: 'Multi-protocol users rank higher', budget: 30000, participants: 6789, events: 23456, ai: false, formula: 'COUNT(protocols) * SUM(volume)' },
|
| 11 |
+
{ id: '6', name: 'New User Welcome Gift', type: 'gift' as const, status: 'ended', desc: 'First-time users who complete 3 swaps receive SOL', budget: 10000, participants: 4567, events: 12345, ai: false },
|
| 12 |
+
]
|
| 13 |
+
|
| 14 |
+
const typeConfig = { leaderboard: { icon: Trophy, color: 'text-brand-yellow', bg: 'bg-brand-yellow/10', label: 'Leaderboard' }, gift: { icon: Gift, color: 'text-brand-turquoise', bg: 'bg-brand-turquoise/10', label: 'Gift' }, raffle: { icon: Ticket, color: 'text-[#a78bfa]', bg: 'bg-[#a78bfa]/10', label: 'Raffle' }, rebate: { icon: Percent, color: 'text-trading-up', bg: 'bg-trading-up/10', label: 'Rebate' } }
|
| 15 |
+
const statusColors = { active: 'bg-trading-up/10 text-trading-up', ended: 'bg-brand-yellow/10 text-brand-yellow', draft: 'bg-muted/10 text-muted', distributed: 'bg-brand-turquoise/10 text-brand-turquoise' }
|
| 16 |
+
|
| 17 |
+
function fmt(n: number) { return n >= 1000 ? `${(n/1000).toFixed(1)}K` : String(n) }
|
| 18 |
+
function fmtC(n: number) { return `$${n.toLocaleString()}` }
|
| 19 |
+
|
| 20 |
+
export default function CampaignsPage() {
|
| 21 |
+
return (
|
| 22 |
+
<div className="p-6 space-y-6">
|
| 23 |
+
<div className="flex items-center justify-between">
|
| 24 |
+
<div><h1 className="text-display-sm text-[#eaecef]">Campaigns</h1><p className="text-body-md text-muted mt-1">Manage Torque campaigns — leaderboards, rebates, raffles & gifts</p></div>
|
| 25 |
+
<button className="flex items-center gap-2 px-5 py-2.5 rounded-md bg-brand-yellow text-ink text-button font-semibold hover:bg-brand-yellow-active transition"><Plus className="w-4 h-4" />Create Campaign</button>
|
| 26 |
+
</div>
|
| 27 |
+
<div className="grid grid-cols-4 gap-4">
|
| 28 |
+
<div className="rounded-xl bg-surface-card border border-hairline-dark p-4"><span className="text-caption text-muted">Total Budget</span><p className="font-mono text-title-lg text-[#eaecef] mt-1">{fmtC(205000)}</p></div>
|
| 29 |
+
<div className="rounded-xl bg-surface-card border border-hairline-dark p-4"><span className="text-caption text-muted">Distributed</span><p className="font-mono text-title-lg text-trading-up mt-1">{fmtC(42801)}</p></div>
|
| 30 |
+
<div className="rounded-xl bg-surface-card border border-hairline-dark p-4"><span className="text-caption text-muted">Total Participants</span><p className="font-mono text-title-lg text-[#eaecef] mt-1">{fmt(47825)}</p></div>
|
| 31 |
+
<div className="rounded-xl bg-surface-card border border-hairline-dark p-4"><div className="flex items-center gap-1.5"><Bot className="w-3.5 h-3.5 text-brand-yellow" /><span className="text-caption text-muted">AI Created</span></div><p className="font-mono text-title-lg text-brand-yellow mt-1">4/6</p></div>
|
| 32 |
+
</div>
|
| 33 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
| 34 |
+
{campaigns.map(c => {
|
| 35 |
+
const tc = typeConfig[c.type]; const Icon = tc.icon; const sc = statusColors[c.status as keyof typeof statusColors] || statusColors.active
|
| 36 |
+
return (
|
| 37 |
+
<div key={c.id} className="rounded-xl bg-surface-card border border-hairline-dark p-5 hover:border-brand-yellow/30 transition-all group cursor-pointer">
|
| 38 |
+
<div className="flex items-start justify-between mb-3">
|
| 39 |
+
<div className="flex items-center gap-2">
|
| 40 |
+
<div className={cn('inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md', tc.bg)}><Icon className={cn('w-3.5 h-3.5', tc.color)} /><span className={cn('text-caption font-semibold', tc.color)}>{tc.label}</span></div>
|
| 41 |
+
<span className={cn('text-[10px] font-semibold uppercase px-2 py-0.5 rounded-pill', sc)}>{c.status}</span>
|
| 42 |
+
</div>
|
| 43 |
+
{c.ai && <div className="flex items-center gap-1 px-2 py-0.5 rounded-pill bg-brand-yellow/10"><Bot className="w-3 h-3 text-brand-yellow" /><span className="text-[10px] text-brand-yellow font-semibold">AI</span></div>}
|
| 44 |
+
</div>
|
| 45 |
+
<h3 className="text-title-sm text-[#eaecef] group-hover:text-brand-yellow transition-colors">{c.name}</h3>
|
| 46 |
+
<p className="text-body-sm text-muted mt-1 line-clamp-2">{c.desc}</p>
|
| 47 |
+
{c.formula && <code className="inline-block mt-2 text-[11px] font-mono text-brand-yellow/70 bg-brand-yellow/5 px-2 py-0.5 rounded">{c.formula}</code>}
|
| 48 |
+
<div className="grid grid-cols-3 gap-3 mt-4 pt-4 border-t border-hairline-dark/50">
|
| 49 |
+
<div><div className="flex items-center gap-1 text-caption text-muted"><Users className="w-3 h-3" /><span>Users</span></div><span className="font-mono text-num-sm text-[#eaecef]">{fmt(c.participants)}</span></div>
|
| 50 |
+
<div><div className="flex items-center gap-1 text-caption text-muted"><Activity className="w-3 h-3" /><span>Events</span></div><span className="font-mono text-num-sm text-[#eaecef]">{fmt(c.events)}</span></div>
|
| 51 |
+
<div><div className="flex items-center gap-1 text-caption text-muted"><DollarSign className="w-3 h-3" /><span>Budget</span></div><span className="font-mono text-num-sm text-brand-yellow">{fmtC(c.budget)}</span></div>
|
| 52 |
+
</div>
|
| 53 |
+
</div>
|
| 54 |
+
)
|
| 55 |
+
})}
|
| 56 |
+
</div>
|
| 57 |
+
</div>
|
| 58 |
+
)
|
| 59 |
+
}
|
src/app/(dashboard)/layout.tsx
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Sidebar } from '@/components/layout/Sidebar'
|
| 2 |
+
import { Topbar } from '@/components/layout/Topbar'
|
| 3 |
+
|
| 4 |
+
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
| 5 |
+
return (
|
| 6 |
+
<div className="flex h-screen overflow-hidden bg-canvas-dark">
|
| 7 |
+
<Sidebar />
|
| 8 |
+
<div className="flex flex-col flex-1 min-w-0">
|
| 9 |
+
<Topbar />
|
| 10 |
+
<main className="flex-1 overflow-y-auto">{children}</main>
|
| 11 |
+
</div>
|
| 12 |
+
</div>
|
| 13 |
+
)
|
| 14 |
+
}
|
src/app/(dashboard)/leaderboard/page.tsx
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
import { Trophy, Crown, Medal, Award, Flame, ArrowUp, ArrowDown } from 'lucide-react'
|
| 3 |
+
import { cn } from '@/lib/utils'
|
| 4 |
+
|
| 5 |
+
const entries = [
|
| 6 |
+
{ rank: 1, wallet: 'BKiKp1...mS2', score: 98750, change: 12.5, volume: '$28.5M', streak: 62, protocols: ['Jupiter','Raydium','Drift','Marginfi','Kamino','Tensor'], rewards: '$5,000' },
|
| 7 |
+
{ rank: 2, wallet: '5Q544f...4j1', score: 87234, change: 8.3, volume: '$15.2M', streak: 47, protocols: ['Jupiter','Raydium','Drift','Marginfi','Kamino'], rewards: '$3,500' },
|
| 8 |
+
{ rank: 3, wallet: 'Fq8xSc...BHu', score: 76543, change: -2.1, volume: '$12.3M', streak: 31, protocols: ['Jupiter','Raydium','Drift'], rewards: '$2,500' },
|
| 9 |
+
{ rank: 4, wallet: 'HN7cAB...WrH', score: 65432, change: 15.7, volume: '$8.7M', streak: 14, protocols: ['Jupiter','Raydium','Drift','Kamino'], rewards: '$1,800' },
|
| 10 |
+
{ rank: 5, wallet: '4zMMC9...cDU', score: 54321, change: 3.4, volume: '$3.5M', streak: 5, protocols: ['Kamino','Jupiter'], rewards: '$1,200' },
|
| 11 |
+
]
|
| 12 |
+
|
| 13 |
+
export default function LeaderboardPage() {
|
| 14 |
+
return (
|
| 15 |
+
<div className="p-6 space-y-6">
|
| 16 |
+
<div>
|
| 17 |
+
<h1 className="text-display-sm text-[#eaecef]">Leaderboard</h1>
|
| 18 |
+
<p className="text-body-md text-muted mt-1">Cross-protocol DeFi Power Rankings powered by Torque</p>
|
| 19 |
+
</div>
|
| 20 |
+
<div className="grid grid-cols-3 gap-4">
|
| 21 |
+
{entries.slice(0,3).map((e,i) => {
|
| 22 |
+
const icons = [Crown, Medal, Award]
|
| 23 |
+
const colors = ['text-brand-yellow','text-[#c0c0c0]','text-[#cd7f32]']
|
| 24 |
+
const Icon = icons[i]
|
| 25 |
+
return (
|
| 26 |
+
<div key={e.rank} className={cn('rounded-xl bg-surface-card border p-6 text-center', i===0 ? 'border-brand-yellow/40 bg-brand-yellow/5 glow-yellow' : 'border-hairline-dark')}>
|
| 27 |
+
<Icon className={cn('w-8 h-8 mx-auto mb-3', colors[i])} />
|
| 28 |
+
<div className="font-mono text-num-display text-[#eaecef]">#{e.rank}</div>
|
| 29 |
+
<p className="text-body-md text-muted font-mono mt-1">{e.wallet}</p>
|
| 30 |
+
<div className="mt-4 font-mono text-title-lg text-brand-yellow">{e.score.toLocaleString()}</div>
|
| 31 |
+
<p className="text-caption text-muted mt-1">Score</p>
|
| 32 |
+
</div>
|
| 33 |
+
)
|
| 34 |
+
})}
|
| 35 |
+
</div>
|
| 36 |
+
<div className="rounded-xl bg-surface-card border border-hairline-dark overflow-hidden">
|
| 37 |
+
<table className="w-full">
|
| 38 |
+
<thead><tr className="border-b border-hairline-dark bg-surface-elevated/50">
|
| 39 |
+
<th className="text-left text-caption text-muted uppercase px-5 py-3 w-16">Rank</th>
|
| 40 |
+
<th className="text-left text-caption text-muted uppercase px-5 py-3">Wallet</th>
|
| 41 |
+
<th className="text-right text-caption text-muted uppercase px-5 py-3">Score</th>
|
| 42 |
+
<th className="text-right text-caption text-muted uppercase px-5 py-3">24h</th>
|
| 43 |
+
<th className="text-right text-caption text-muted uppercase px-5 py-3">Volume</th>
|
| 44 |
+
<th className="text-right text-caption text-muted uppercase px-5 py-3">Streak</th>
|
| 45 |
+
<th className="text-right text-caption text-muted uppercase px-5 py-3">Rewards</th>
|
| 46 |
+
</tr></thead>
|
| 47 |
+
<tbody>{entries.map(e => (
|
| 48 |
+
<tr key={e.rank} className="border-b border-hairline-dark/50 hover:bg-surface-hover transition-colors">
|
| 49 |
+
<td className="px-5 py-4"><span className={cn('font-mono text-num-md font-bold', e.rank<=3 ? 'text-brand-yellow' : 'text-muted')}>{e.rank}</span></td>
|
| 50 |
+
<td className="px-5 py-4 font-mono text-body-md">{e.wallet}</td>
|
| 51 |
+
<td className="px-5 py-4 text-right font-mono text-num-md tabular-nums">{e.score.toLocaleString()}</td>
|
| 52 |
+
<td className="px-5 py-4 text-right"><div className="flex items-center justify-end gap-1">{e.change>=0 ? <ArrowUp className="w-3.5 h-3.5 text-trading-up"/> : <ArrowDown className="w-3.5 h-3.5 text-trading-down"/>}<span className={cn('font-mono text-num-sm', e.change>=0 ? 'text-trading-up' : 'text-trading-down')}>{e.change>=0?'+':''}{e.change}%</span></div></td>
|
| 53 |
+
<td className="px-5 py-4 text-right font-mono text-num-sm text-muted">{e.volume}</td>
|
| 54 |
+
<td className="px-5 py-4 text-right"><div className="flex items-center justify-end gap-1"><Flame className={cn('w-3.5 h-3.5', e.streak>=30?'text-brand-yellow':'text-trading-up')}/><span className="font-mono text-num-sm">{e.streak}d</span></div></td>
|
| 55 |
+
<td className="px-5 py-4 text-right font-mono text-num-sm text-brand-yellow">{e.rewards}</td>
|
| 56 |
+
</tr>
|
| 57 |
+
))}</tbody>
|
| 58 |
+
</table>
|
| 59 |
+
</div>
|
| 60 |
+
<div className="rounded-xl bg-brand-yellow/5 border border-brand-yellow/20 p-5 flex items-center gap-4">
|
| 61 |
+
<Trophy className="w-6 h-6 text-brand-yellow" />
|
| 62 |
+
<div>
|
| 63 |
+
<h3 className="text-title-sm text-brand-yellow">Scoring Formula</h3>
|
| 64 |
+
<code className="font-mono text-brand-yellow/80 bg-surface-card px-2 py-0.5 rounded text-body-sm">SCORE = COUNT(protocols) x SUM(swap_volume) x STREAK_MULTIPLIER(days)</code>
|
| 65 |
+
</div>
|
| 66 |
+
</div>
|
| 67 |
+
</div>
|
| 68 |
+
)
|
| 69 |
+
}
|
src/app/(dashboard)/page.tsx
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
import { Users, ShieldAlert, HeartPulse, TrendingUp, Bot, Flame } from 'lucide-react'
|
| 3 |
+
import { StatCard } from '@/components/ui/StatCard'
|
| 4 |
+
|
| 5 |
+
export default function DashboardPage() {
|
| 6 |
+
return (
|
| 7 |
+
<div className="p-6 space-y-6">
|
| 8 |
+
<div>
|
| 9 |
+
<h1 className="text-display-sm text-[#eaecef]">Dashboard</h1>
|
| 10 |
+
<p className="text-body-md text-muted mt-1">Real-time churn detection and autonomous retention</p>
|
| 11 |
+
</div>
|
| 12 |
+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
| 13 |
+
<StatCard title="Active Wallets" value="312.8K" change={12.3} changeLabel="vs last week" icon={Users} variant="yellow" />
|
| 14 |
+
<StatCard title="Wallets At Risk" value="23.9K" change={-8.7} changeLabel="vs last week" icon={ShieldAlert} variant="red" />
|
| 15 |
+
<StatCard title="Wallets Saved" value="15.2K" change={23.4} changeLabel="vs last week" icon={HeartPulse} variant="green" />
|
| 16 |
+
<StatCard title="ROI" value="847%" change={15.2} changeLabel="vs last week" icon={TrendingUp} variant="yellow" />
|
| 17 |
+
</div>
|
| 18 |
+
<div className="rounded-xl bg-surface-card border border-hairline-dark p-5">
|
| 19 |
+
<div className="flex items-center gap-2 mb-4">
|
| 20 |
+
<Bot className="w-4 h-4 text-brand-yellow" />
|
| 21 |
+
<span className="text-title-sm">AI Agent Feed</span>
|
| 22 |
+
<div className="w-2 h-2 rounded-full bg-trading-up animate-pulse" />
|
| 23 |
+
</div>
|
| 24 |
+
<div className="space-y-2">
|
| 25 |
+
{['Scanning 312,847 active wallets for churn signals...', 'Critical: Wallet 7xKXtg...AsU inactive 10 days', 'Gift sent: 0.5 SOL via Torque MCP', 'Leaderboard updated: 12,847 participants scored', 'Comeback detected: HN7cAB...WrH returned after 12 days'].map((msg, i) => (
|
| 26 |
+
<div key={i} className="flex items-start gap-3 px-4 py-2 border-b border-hairline-dark/50">
|
| 27 |
+
<span className="font-mono text-caption text-muted tabular-nums whitespace-nowrap">14:2{i}:00</span>
|
| 28 |
+
<span className="text-body-sm text-[#eaecef]">{['\u{1F50D}','\u26A0\uFE0F','\u{1F381}','\u{1F4CA}','\u{1F525}'][i]} {msg}</span>
|
| 29 |
+
</div>
|
| 30 |
+
))}
|
| 31 |
+
</div>
|
| 32 |
+
</div>
|
| 33 |
+
<div className="rounded-xl bg-surface-card border border-hairline-dark p-5">
|
| 34 |
+
<h3 className="text-title-sm mb-4">Protocol Performance</h3>
|
| 35 |
+
<table className="w-full">
|
| 36 |
+
<thead>
|
| 37 |
+
<tr className="border-b border-hairline-dark">
|
| 38 |
+
<th className="text-left text-caption text-muted uppercase px-4 py-3">Protocol</th>
|
| 39 |
+
<th className="text-right text-caption text-muted uppercase px-4 py-3">Users</th>
|
| 40 |
+
<th className="text-right text-caption text-muted uppercase px-4 py-3">Retention</th>
|
| 41 |
+
<th className="text-right text-caption text-muted uppercase px-4 py-3">Streak</th>
|
| 42 |
+
</tr>
|
| 43 |
+
</thead>
|
| 44 |
+
<tbody>
|
| 45 |
+
{[{p:'Jupiter',u:'234.6K',r:72,s:12,c:'#22d3ee'},{p:'Raydium',u:'189.2K',r:64,s:8,c:'#a78bfa'},{p:'Drift',u:'87.7K',r:68,s:10,c:'#f472b6'},{p:'Marginfi',u:'67.9K',r:74,s:15,c:'#34d399'},{p:'Kamino',u:'45.7K',r:76,s:18,c:'#fbbf24'}].map((row) => (
|
| 46 |
+
<tr key={row.p} className="border-b border-hairline-dark/50 hover:bg-surface-hover transition-colors">
|
| 47 |
+
<td className="px-4 py-3"><div className="flex items-center gap-2"><div className="w-3 h-3 rounded-full" style={{backgroundColor:row.c}} /><span className="text-body-md">{row.p}</span></div></td>
|
| 48 |
+
<td className="text-right px-4 py-3 font-mono text-num-md tabular-nums">{row.u}</td>
|
| 49 |
+
<td className="text-right px-4 py-3"><span className={row.r >= 70 ? 'font-mono text-num-sm text-trading-up' : 'font-mono text-num-sm text-brand-yellow'}>{row.r}%</span></td>
|
| 50 |
+
<td className="text-right px-4 py-3"><div className="flex items-center justify-end gap-1"><Flame className="w-3.5 h-3.5 text-brand-yellow" /><span className="font-mono text-num-sm">{row.s}d</span></div></td>
|
| 51 |
+
</tr>
|
| 52 |
+
))}
|
| 53 |
+
</tbody>
|
| 54 |
+
</table>
|
| 55 |
+
</div>
|
| 56 |
+
</div>
|
| 57 |
+
)
|
| 58 |
+
}
|
src/app/(dashboard)/wallets/page.tsx
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
import { cn } from '@/lib/utils'
|
| 3 |
+
import { Search, Flame, ExternalLink, ChevronRight } from 'lucide-react'
|
| 4 |
+
import { useState } from 'react'
|
| 5 |
+
|
| 6 |
+
type Risk = 'critical' | 'high' | 'medium' | 'low' | 'safe'
|
| 7 |
+
const wallets = [
|
| 8 |
+
{ addr: '7xKXtg...AsU', risk: 'critical' as Risk, score: 94, vol: '$847K', streak: 0, protocols: ['Jupiter','Raydium'], last: '10d ago', saved: 0 },
|
| 9 |
+
{ addr: '9WzDXw...WWM', risk: 'high' as Risk, score: 78, vol: '$1.2M', streak: 2, protocols: ['Drift','Marginfi','Jupiter'], last: '5d ago', saved: 1 },
|
| 10 |
+
{ addr: '4zMMC9...cDU', risk: 'medium' as Risk, score: 52, vol: '$3.5M', streak: 5, protocols: ['Kamino','Jupiter'], last: '2d ago', saved: 0 },
|
| 11 |
+
{ addr: 'HN7cAB...WrH', risk: 'low' as Risk, score: 23, vol: '$8.7M', streak: 14, protocols: ['Jupiter','Raydium','Drift','Kamino'], last: '1h ago', saved: 2 },
|
| 12 |
+
{ addr: '5Q544f...4j1', risk: 'safe' as Risk, score: 8, vol: '$15.2M', streak: 47, protocols: ['Jupiter','Raydium','Drift','Marginfi','Kamino'], last: '30m ago', saved: 3 },
|
| 13 |
+
{ addr: 'DRpbCB...1hy', risk: 'critical' as Risk, score: 91, vol: '$235K', streak: 0, protocols: ['Raydium'], last: '12d ago', saved: 0 },
|
| 14 |
+
{ addr: 'BKiKp1...mS2', risk: 'safe' as Risk, score: 5, vol: '$28.5M', streak: 62, protocols: ['Jupiter','Raydium','Drift','Marginfi','Kamino','Tensor'], last: '15m ago', saved: 5 },
|
| 15 |
+
]
|
| 16 |
+
|
| 17 |
+
const riskCfg: Record<Risk, { label: string; color: string; bg: string; dot: string }> = {
|
| 18 |
+
critical: { label: 'CRITICAL', color: 'text-trading-down', bg: 'bg-trading-down/10', dot: 'bg-trading-down' },
|
| 19 |
+
high: { label: 'HIGH', color: 'text-[#ff9500]', bg: 'bg-[#ff9500]/10', dot: 'bg-[#ff9500]' },
|
| 20 |
+
medium: { label: 'MEDIUM', color: 'text-brand-yellow', bg: 'bg-brand-yellow/10', dot: 'bg-brand-yellow' },
|
| 21 |
+
low: { label: 'LOW', color: 'text-trading-up', bg: 'bg-trading-up/10', dot: 'bg-trading-up' },
|
| 22 |
+
safe: { label: 'SAFE', color: 'text-brand-turquoise', bg: 'bg-brand-turquoise/10', dot: 'bg-brand-turquoise' },
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
export default function WalletsPage() {
|
| 26 |
+
const [filter, setFilter] = useState<Risk | 'all'>('all')
|
| 27 |
+
const filtered = wallets.filter(w => filter === 'all' || w.risk === filter)
|
| 28 |
+
return (
|
| 29 |
+
<div className="p-6 space-y-6">
|
| 30 |
+
<div><h1 className="text-display-sm text-[#eaecef]">Wallets</h1><p className="text-body-md text-muted mt-1">Monitor wallet health, churn risk & activity patterns</p></div>
|
| 31 |
+
<div className="grid grid-cols-2 lg:grid-cols-6 gap-3">
|
| 32 |
+
{(['all','critical','high','medium','low','safe'] as const).map(r => (
|
| 33 |
+
<button key={r} onClick={() => setFilter(r)} className={cn('rounded-xl border p-3 text-center transition-all', filter === r ? 'border-brand-yellow/40 bg-brand-yellow/5' : 'border-hairline-dark bg-surface-card hover:border-brand-yellow/20')}>
|
| 34 |
+
<span className="text-caption text-muted capitalize">{r}</span>
|
| 35 |
+
<p className="font-mono text-title-md text-[#eaecef] tabular-nums mt-0.5">{r === 'all' ? wallets.length : wallets.filter(w => w.risk === r).length}</p>
|
| 36 |
+
</button>
|
| 37 |
+
))}
|
| 38 |
+
</div>
|
| 39 |
+
<div className="rounded-xl bg-surface-card border border-hairline-dark overflow-hidden">
|
| 40 |
+
<table className="w-full">
|
| 41 |
+
<thead><tr className="border-b border-hairline-dark bg-surface-elevated/50">
|
| 42 |
+
<th className="text-left text-caption text-muted uppercase px-5 py-3">Wallet</th>
|
| 43 |
+
<th className="text-center text-caption text-muted uppercase px-5 py-3">Risk</th>
|
| 44 |
+
<th className="text-right text-caption text-muted uppercase px-5 py-3">Score</th>
|
| 45 |
+
<th className="text-right text-caption text-muted uppercase px-5 py-3">Volume</th>
|
| 46 |
+
<th className="text-right text-caption text-muted uppercase px-5 py-3">Streak</th>
|
| 47 |
+
<th className="text-center text-caption text-muted uppercase px-5 py-3">Protocols</th>
|
| 48 |
+
<th className="text-right text-caption text-muted uppercase px-5 py-3">Last Active</th>
|
| 49 |
+
<th className="w-10"></th>
|
| 50 |
+
</tr></thead>
|
| 51 |
+
<tbody>{filtered.map(w => { const rc = riskCfg[w.risk]; return (
|
| 52 |
+
<tr key={w.addr} className="border-b border-hairline-dark/50 hover:bg-surface-hover transition-colors group">
|
| 53 |
+
<td className="px-5 py-4"><div className="flex items-center gap-2"><span className="font-mono text-body-md">{w.addr}</span><ExternalLink className="w-3 h-3 text-muted opacity-0 group-hover:opacity-100 transition" /></div></td>
|
| 54 |
+
<td className="px-5 py-4 text-center"><div className={cn('inline-flex items-center gap-1.5 px-3 py-1 rounded-pill', rc.bg)}><div className={cn('w-2 h-2 rounded-full', rc.dot)} /><span className={cn('font-mono text-caption font-semibold', rc.color)}>{rc.label}</span></div></td>
|
| 55 |
+
<td className="px-5 py-4 text-right"><div className="flex items-center justify-end gap-2"><div className="w-16 h-1.5 rounded-full bg-surface-elevated overflow-hidden"><div className={cn('h-full rounded-full', w.score >= 80 ? 'bg-trading-down' : w.score >= 60 ? 'bg-[#ff9500]' : w.score >= 40 ? 'bg-brand-yellow' : w.score >= 20 ? 'bg-trading-up' : 'bg-brand-turquoise')} style={{ width: `${w.score}%` }} /></div><span className="font-mono text-num-sm text-muted tabular-nums w-8 text-right">{w.score}</span></div></td>
|
| 56 |
+
<td className="px-5 py-4 text-right font-mono text-num-sm tabular-nums">{w.vol}</td>
|
| 57 |
+
<td className="px-5 py-4 text-right"><div className="flex items-center justify-end gap-1"><Flame className={cn('w-3.5 h-3.5', w.streak >= 30 ? 'text-brand-yellow' : w.streak >= 7 ? 'text-trading-up' : w.streak === 0 ? 'text-trading-down' : 'text-muted')} /><span className="font-mono text-num-sm">{w.streak}d</span></div></td>
|
| 58 |
+
<td className="px-5 py-4"><div className="flex flex-wrap justify-center gap-1">{w.protocols.slice(0,3).map(p => <span key={p} className="text-[10px] px-1.5 py-0.5 rounded bg-surface-elevated text-muted">{p}</span>)}{w.protocols.length > 3 && <span className="text-[10px] px-1.5 py-0.5 rounded bg-surface-elevated text-muted">+{w.protocols.length-3}</span>}</div></td>
|
| 59 |
+
<td className="px-5 py-4 text-right text-body-sm text-muted">{w.last}</td>
|
| 60 |
+
<td className="px-2 py-4"><ChevronRight className="w-4 h-4 text-muted opacity-0 group-hover:opacity-100 transition" /></td>
|
| 61 |
+
</tr>
|
| 62 |
+
)})}</tbody>
|
| 63 |
+
</table>
|
| 64 |
+
</div>
|
| 65 |
+
</div>
|
| 66 |
+
)
|
| 67 |
+
}
|
src/app/api/agent/scan/route.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse } from 'next/server'
|
| 2 |
+
export async function POST() { return NextResponse.json({ detections: [], count: 0 }) }
|
| 3 |
+
export async function GET() { return NextResponse.json({ status: 'active', capabilities: ['churn_detection','auto_campaign_creation','comeback_detection','streak_tracking'] }) }
|
src/app/api/torque/campaigns/route.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse } from 'next/server'
|
| 2 |
+
export async function POST() { return NextResponse.json({ success: true, campaignId: `demo-camp-${Date.now()}` }) }
|
| 3 |
+
export async function GET() { return NextResponse.json({ campaigns: [] }) }
|
src/app/api/torque/events/route.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse } from 'next/server'
|
| 2 |
+
export async function POST() { return NextResponse.json({ success: true, eventId: `demo-${Date.now()}` }) }
|
| 3 |
+
export async function GET() { return NextResponse.json({ status: 'ok', events: ['churn_risk_high','churn_risk_medium','comeback_detected','streak_maintained','volume_milestone','referral_from_saved','inactivity_detected'] }) }
|
src/components/layout/Sidebar.tsx
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
import Link from 'next/link'
|
| 3 |
+
import { usePathname } from 'next/navigation'
|
| 4 |
+
import { cn } from '@/lib/utils'
|
| 5 |
+
import { LayoutDashboard, Trophy, Megaphone, BarChart3, Wallet, Bot, Zap, Shield, ChevronLeft, ChevronRight } from 'lucide-react'
|
| 6 |
+
import { useState } from 'react'
|
| 7 |
+
|
| 8 |
+
const navItems = [
|
| 9 |
+
{ label: 'Dashboard', href: '/', icon: LayoutDashboard },
|
| 10 |
+
{ label: 'Leaderboard', href: '/leaderboard', icon: Trophy },
|
| 11 |
+
{ label: 'Campaigns', href: '/campaigns', icon: Megaphone },
|
| 12 |
+
{ label: 'Analytics', href: '/analytics', icon: BarChart3 },
|
| 13 |
+
{ label: 'Wallets', href: '/wallets', icon: Wallet },
|
| 14 |
+
{ label: 'AI Agent', href: '/agent', icon: Bot },
|
| 15 |
+
]
|
| 16 |
+
|
| 17 |
+
export function Sidebar() {
|
| 18 |
+
const pathname = usePathname()
|
| 19 |
+
const [collapsed, setCollapsed] = useState(false)
|
| 20 |
+
return (
|
| 21 |
+
<aside className={cn('hidden md:flex flex-col border-r border-hairline-dark bg-surface-card transition-all duration-300', collapsed ? 'w-[68px]' : 'w-[240px]')}>
|
| 22 |
+
<div className="flex items-center h-16 px-4 border-b border-hairline-dark">
|
| 23 |
+
<div className="flex items-center gap-2.5 overflow-hidden">
|
| 24 |
+
<div className="w-8 h-8 rounded-lg bg-brand-yellow flex items-center justify-center flex-shrink-0"><Zap className="w-5 h-5 text-ink" /></div>
|
| 25 |
+
{!collapsed && <span className="text-title-sm text-brand-yellow font-bold tracking-tight animate-slide-in-right">FlowState</span>}
|
| 26 |
+
</div>
|
| 27 |
+
</div>
|
| 28 |
+
<nav className="flex-1 py-4 px-2 space-y-1">
|
| 29 |
+
{navItems.map(item => {
|
| 30 |
+
const isActive = pathname === item.href || (item.href !== '/' && pathname.startsWith(item.href))
|
| 31 |
+
return (
|
| 32 |
+
<Link key={item.href} href={item.href} className={cn('flex items-center gap-3 px-3 py-2.5 rounded-lg text-nav transition-all duration-200', isActive ? 'bg-brand-yellow/10 text-brand-yellow' : 'text-muted hover:text-[#eaecef] hover:bg-surface-elevated', collapsed && 'justify-center px-2')}>
|
| 33 |
+
<item.icon className={cn('w-5 h-5 flex-shrink-0', isActive && 'text-brand-yellow')} />
|
| 34 |
+
{!collapsed && <span>{item.label}</span>}
|
| 35 |
+
</Link>
|
| 36 |
+
)
|
| 37 |
+
})}
|
| 38 |
+
</nav>
|
| 39 |
+
<div className="p-2 border-t border-hairline-dark">
|
| 40 |
+
{!collapsed && <div className="mx-3 mb-3 p-3 rounded-xl bg-brand-yellow/5 border border-brand-yellow/20"><div className="flex items-center gap-2 mb-1"><Shield className="w-4 h-4 text-trading-up" /><span className="text-caption text-trading-up font-semibold">AI Agent Active</span></div><p className="text-caption text-muted">3,847 actions today</p></div>}
|
| 41 |
+
<button onClick={() => setCollapsed(!collapsed)} className="flex items-center gap-3 px-3 py-2.5 rounded-lg text-nav text-muted hover:text-[#eaecef] hover:bg-surface-elevated w-full transition-colors">
|
| 42 |
+
{collapsed ? <ChevronRight className="w-5 h-5" /> : <ChevronLeft className="w-5 h-5" />}
|
| 43 |
+
{!collapsed && <span>Collapse</span>}
|
| 44 |
+
</button>
|
| 45 |
+
</div>
|
| 46 |
+
</aside>
|
| 47 |
+
)
|
| 48 |
+
}
|
src/components/layout/Topbar.tsx
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
import { Bell, Search, Globe, Wifi } from 'lucide-react'
|
| 3 |
+
import { useState, useEffect } from 'react'
|
| 4 |
+
|
| 5 |
+
export function Topbar() {
|
| 6 |
+
const [time, setTime] = useState('')
|
| 7 |
+
useEffect(() => { const u = () => setTime(new Date().toLocaleTimeString('en-US', { hour12: false })); u(); const i = setInterval(u, 1000); return () => clearInterval(i) }, [])
|
| 8 |
+
return (
|
| 9 |
+
<header className="h-14 border-b border-hairline-dark bg-surface-card/80 backdrop-blur-sm flex items-center justify-between px-6">
|
| 10 |
+
<div className="relative">
|
| 11 |
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted" />
|
| 12 |
+
<input type="text" placeholder="Search wallets, campaigns..." className="w-64 h-9 pl-9 pr-4 rounded-lg bg-surface-elevated border border-hairline-dark text-body-sm text-[#eaecef] placeholder:text-muted focus:outline-none focus:ring-1 focus:ring-brand-yellow/50" />
|
| 13 |
+
</div>
|
| 14 |
+
<div className="flex items-center gap-4">
|
| 15 |
+
<div className="flex items-center gap-2 px-3 py-1.5 rounded-pill bg-trading-up/10 border border-trading-up/20"><div className="w-2 h-2 rounded-full bg-trading-up animate-pulse" /><span className="text-caption text-trading-up font-semibold">LIVE</span></div>
|
| 16 |
+
<span className="font-mono text-num-sm text-muted tabular-nums">{time}</span>
|
| 17 |
+
<button className="flex items-center gap-1.5 text-muted hover:text-[#eaecef] transition"><Wifi className="w-4 h-4 text-trading-up" /><span className="text-caption">Solana</span></button>
|
| 18 |
+
<button className="relative p-2 rounded-lg hover:bg-surface-elevated transition"><Bell className="w-4 h-4 text-muted" /><span className="absolute top-1 right-1 w-2 h-2 bg-brand-yellow rounded-full" /></button>
|
| 19 |
+
<button className="flex items-center gap-2 px-4 py-2 rounded-md bg-brand-yellow text-ink text-button font-semibold hover:bg-brand-yellow-active transition"><Globe className="w-4 h-4" />Connect</button>
|
| 20 |
+
</div>
|
| 21 |
+
</header>
|
| 22 |
+
)
|
| 23 |
+
}
|
src/components/ui/StatCard.tsx
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
import { cn } from '@/lib/utils'
|
| 3 |
+
import { LucideIcon } from 'lucide-react'
|
| 4 |
+
|
| 5 |
+
interface StatCardProps { title: string; value: string; change?: number; changeLabel?: string; icon: LucideIcon; variant?: 'default' | 'yellow' | 'green' | 'red' }
|
| 6 |
+
|
| 7 |
+
export function StatCard({ title, value, change, changeLabel, icon: Icon, variant = 'default' }: StatCardProps) {
|
| 8 |
+
const vs: Record<string, string> = { default: 'border-hairline-dark', yellow: 'border-brand-yellow/20 bg-brand-yellow/5', green: 'border-trading-up/20 bg-trading-up/5', red: 'border-trading-down/20 bg-trading-down/5' }
|
| 9 |
+
const is: Record<string, string> = { default: 'bg-surface-elevated text-muted', yellow: 'bg-brand-yellow/10 text-brand-yellow', green: 'bg-trading-up/10 text-trading-up', red: 'bg-trading-down/10 text-trading-down' }
|
| 10 |
+
return (
|
| 11 |
+
<div className={cn('p-5 rounded-xl bg-surface-card border transition-all hover:border-brand-yellow/30', vs[variant])}>
|
| 12 |
+
<div className="flex items-start justify-between mb-3">
|
| 13 |
+
<span className="text-caption text-muted uppercase tracking-wider">{title}</span>
|
| 14 |
+
<div className={cn('p-2 rounded-lg', is[variant])}><Icon className="w-4 h-4" /></div>
|
| 15 |
+
</div>
|
| 16 |
+
<div className="font-mono text-num-display text-[#eaecef] tabular-nums leading-none">{value}</div>
|
| 17 |
+
{(change !== undefined || changeLabel) && (
|
| 18 |
+
<div className="mt-2 flex items-center gap-1.5">
|
| 19 |
+
{change !== undefined && <span className={cn('text-num-sm font-mono tabular-nums', change >= 0 ? 'text-trading-up' : 'text-trading-down')}>{change >= 0 ? '+' : ''}{change.toFixed(1)}%</span>}
|
| 20 |
+
{changeLabel && <span className="text-caption text-muted">{changeLabel}</span>}
|
| 21 |
+
</div>
|
| 22 |
+
)}
|
| 23 |
+
</div>
|
| 24 |
+
)
|
| 25 |
+
}
|