| '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> |
| ) |
| } |
|
|