File size: 11,051 Bytes
7200823 f667d47 de40b1a f667d47 7200823 de40b1a f667d47 de40b1a 7200823 f667d47 7200823 de40b1a f667d47 7200823 de40b1a 7200823 de40b1a 7200823 de40b1a f667d47 de40b1a 7200823 de40b1a 7200823 de40b1a f667d47 de40b1a 7200823 f667d47 7200823 de40b1a 7200823 de40b1a 7200823 de40b1a 7200823 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | 'use client'
import { TrendingUp, TrendingDown, Target, Zap, Award, Clock, CheckCircle2, XCircle, BarChart2 } from 'lucide-react'
import { cn, fmtNum } from '@/lib/utils'
import { cohorts, eventBreakdown, dailyEvents, roiData } from '@/lib/mock-data'
import { BarChart, Bar, LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'
import { useState, useEffect } from 'react'
const Tip = ({ active, payload, label }: any) => {
if (!active || !payload?.length) return null
return (<div className="bg-surface-elevated border border-hairline-dark rounded-lg p-3 shadow-lg">
<p className="text-caption text-muted mb-1">{label}</p>
{payload.map((p: any, i: number) => <p key={i} className="font-mono text-num-sm" style={{color:p.color}}>{p.name}: {typeof p.value==='number' && p.value>1000 ? fmtNum(p.value) : p.value}{p.unit||''}</p>)}
</div>)
}
function getColor(v: number) {
if (v === 0) return 'bg-surface-elevated text-muted'
if (v >= 75) return 'bg-trading-up/20 text-trading-up'
if (v >= 50) return 'bg-brand-yellow/15 text-brand-yellow'
if (v >= 30) return 'bg-[#ff9500]/15 text-[#ff9500]'
return 'bg-trading-down/15 text-trading-down'
}
interface AttributionStats {
total: number; recovered: number; pending: number; successRate: number
avgDaysToRecover: number
weeklyTrend: { week: string; rate: number }[]
interventions: { wallet: string; eventFired: string; score: number; recovered: boolean; daysToRecover?: number }[]
}
export default function AnalyticsPage() {
const maxEvt = Math.max(...eventBreakdown.map(e => e.count))
const [attr, setAttr] = useState<AttributionStats | null>(null)
useEffect(() => {
fetch('/api/torque/attribution')
.then(r => r.ok ? r.json() : null)
.then(d => d && setAttr(d))
.catch(() => {})
}, [])
return (
<div className="p-6 space-y-6">
<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>
<div className="grid grid-cols-2 lg:grid-cols-5 gap-4">
{[
{ l:'Avg Retention', v:'67.3%', c:'+9.6%', up:true, i:Target },
{ l:'Churn Rate', v:'4.2%', c:'-4.3%', up:false, i:TrendingDown },
{ l:'Campaign ROI', v:'847%', c:'+15.2%', up:true, i:TrendingUp },
{ l:'Saved Wallets', v:'15.2K', c:'+23.4%', up:true, i:Award },
{ l:'Agent Actions', v:'3,847', c:'+12.1%', up:true, i:Zap },
].map(k => { const I = k.i; return (
<div key={k.l} className="rounded-xl bg-surface-card border border-hairline-dark p-4">
<div className="flex items-center justify-between mb-2"><span className="text-caption text-muted">{k.l}</span><I className="w-4 h-4 text-muted"/></div>
<div className="font-mono text-title-lg text-[#eaecef] tabular-nums">{k.v}</div>
<span className={cn('font-mono text-num-sm tabular-nums', k.up?'text-trading-up':'text-trading-down')}>{k.c}</span>
</div>
)})}
</div>
{/* Recovery Attribution Dashboard */}
{attr && (
<div className="rounded-xl bg-surface-card border border-hairline-dark overflow-hidden">
<div className="px-5 py-4 border-b border-hairline-dark flex items-center justify-between">
<div className="flex items-center gap-2">
<BarChart2 className="w-4 h-4 text-trading-up" />
<h3 className="text-title-sm">Recovery Attribution</h3>
<span className="text-[10px] px-2 py-0.5 rounded-pill bg-trading-up/10 text-trading-up font-semibold border border-trading-up/20">LIVE</span>
</div>
<p className="text-caption text-muted">Did interventions actually work?</p>
</div>
<div className="p-5 space-y-5">
<div className="grid grid-cols-4 gap-4">
<div className="rounded-xl bg-trading-up/5 border border-trading-up/20 p-4 text-center">
<p className="text-caption text-trading-up/70 mb-1">Success Rate</p>
<p className="font-mono text-display-sm text-trading-up tabular-nums">{attr.successRate}%</p>
<p className="text-caption text-muted mt-0.5">{attr.recovered}/{attr.total} rescued</p>
</div>
<div className="rounded-xl bg-surface-elevated border border-hairline-dark p-4 text-center">
<p className="text-caption text-muted mb-1">Avg Recovery</p>
<p className="font-mono text-title-lg text-[#eaecef] tabular-nums">{attr.avgDaysToRecover}d</p>
<p className="text-caption text-muted mt-0.5">days to return</p>
</div>
<div className="rounded-xl bg-brand-yellow/5 border border-brand-yellow/20 p-4 text-center">
<p className="text-caption text-brand-yellow/70 mb-1">Pending</p>
<p className="font-mono text-title-lg text-brand-yellow tabular-nums">{attr.pending}</p>
<p className="text-caption text-muted mt-0.5">awaiting return</p>
</div>
<div className="rounded-xl bg-surface-elevated border border-hairline-dark p-4">
<p className="text-caption text-muted mb-2">Weekly trend</p>
<ResponsiveContainer width="100%" height={52}>
<LineChart data={attr.weeklyTrend}>
<Line type="monotone" dataKey="rate" stroke="#0ecb81" strokeWidth={2} dot={false} />
</LineChart>
</ResponsiveContainer>
</div>
</div>
<div>
<p className="text-caption text-muted uppercase tracking-wider mb-3">Recent interventions</p>
<div className="space-y-1.5">
{attr.interventions.map((item, i) => (
<div key={i} className="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-surface-elevated/50 transition">
{item.recovered
? <CheckCircle2 className="w-4 h-4 text-trading-up flex-shrink-0" />
: <XCircle className="w-4 h-4 text-muted flex-shrink-0" />}
<span className="font-mono text-body-sm text-[#eaecef] w-32">{item.wallet}</span>
<code className="text-[10px] text-muted bg-surface-elevated px-1.5 py-0.5 rounded">{item.eventFired}</code>
<span className="font-mono text-num-sm text-muted tabular-nums">score={item.score}</span>
<span className={cn('ml-auto text-caption font-semibold', item.recovered ? 'text-trading-up' : 'text-muted')}>
{item.recovered ? `✓ returned in ${item.daysToRecover}d` : 'pending'}
</span>
</div>
))}
</div>
</div>
</div>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="rounded-xl bg-surface-card border border-hairline-dark p-5">
<h3 className="text-title-sm mb-4">Daily Custom Events</h3>
<ResponsiveContainer width="100%" height={250}>
<BarChart data={dailyEvents}><CartesianGrid strokeDasharray="3 3" stroke="#2b3139"/><XAxis dataKey="date" tick={{fill:'#707a8a',fontSize:11}} axisLine={{stroke:'#2b3139'}}/><YAxis tick={{fill:'#707a8a',fontSize:11}} axisLine={{stroke:'#2b3139'}}/><Tooltip content={<Tip/>}/><Bar dataKey="value" fill="#FCD535" radius={[4,4,0,0]} name="Events"/></BarChart>
</ResponsiveContainer>
</div>
<div className="rounded-xl bg-surface-card border border-hairline-dark p-5">
<h3 className="text-title-sm mb-4">Campaign ROI Over Time</h3>
<ResponsiveContainer width="100%" height={250}>
<LineChart data={roiData}><CartesianGrid strokeDasharray="3 3" stroke="#2b3139"/><XAxis dataKey="date" tick={{fill:'#707a8a',fontSize:11}} axisLine={{stroke:'#2b3139'}}/><YAxis tick={{fill:'#707a8a',fontSize:11}} axisLine={{stroke:'#2b3139'}} unit="%"/><Tooltip content={<Tip/>}/><Line type="monotone" dataKey="value" stroke="#0ecb81" strokeWidth={2.5} dot={{fill:'#0ecb81',r:4}} name="ROI %"/></LineChart>
</ResponsiveContainer>
</div>
</div>
<div className="rounded-xl bg-surface-card border border-hairline-dark p-5">
<div className="flex items-center justify-between mb-4">
<div><h3 className="text-title-sm">Retention Cohorts</h3><p className="text-caption text-muted mt-0.5">Weekly cohort heatmap — FlowState launched Mar 31</p></div>
</div>
<table className="w-full"><thead><tr className="border-b border-hairline-dark">
<th className="text-left text-caption text-muted uppercase px-4 py-2">Cohort</th>
<th className="text-center text-caption text-muted uppercase px-4 py-2">Day 1</th>
<th className="text-center text-caption text-muted uppercase px-4 py-2">Day 7</th>
<th className="text-center text-caption text-muted uppercase px-4 py-2">Day 14</th>
<th className="text-center text-caption text-muted uppercase px-4 py-2">Day 30</th>
<th className="text-center text-caption text-muted uppercase px-4 py-2">Day 60</th>
</tr></thead><tbody>{cohorts.map(c => (
<tr key={c.week} className="border-b border-hairline-dark/50">
<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>
{[c.d1,c.d7,c.d14,c.d30,c.d60].map((v,i) => (
<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?'—':v+'%'}</span></td>
))}
</tr>
))}</tbody></table>
<div className="mt-4 p-3 rounded-lg bg-trading-up/5 border border-trading-up/20">
<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 vs pre-launch.</p>
</div>
</div>
<div className="rounded-xl bg-surface-card border border-hairline-dark p-5">
<h3 className="text-title-sm mb-4">Torque Custom Events Breakdown</h3>
<div className="space-y-4">{eventBreakdown.map(e => (
<div key={e.event} className="space-y-1.5">
<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">{fmtNum(e.count)}</span></div>
<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/maxEvt*100)+'%',backgroundColor:e.color}}/></div>
</div>
))}</div>
</div>
</div>
)
}
|