Release 0.15.0-alpha #43

Merged
uz1mani merged 61 commits from staging into main 2026-03-12 23:13:42 +00:00
14 changed files with 1739 additions and 12 deletions
Showing only changes of commit 0889079372 - Show all commits

View File

@@ -1,7 +1,22 @@
'use client' 'use client'
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell } from 'recharts'
import { TrendUp } from '@phosphor-icons/react' import { TrendUp } from '@phosphor-icons/react'
import { Pie, PieChart } from 'recharts'
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from '@/components/charts'
import type { FrustrationSummary } from '@/lib/api/stats' import type { FrustrationSummary } from '@/lib/api/stats'
interface FrustrationTrendProps { interface FrustrationTrendProps {
@@ -16,40 +31,55 @@ function SkeletonCard() {
<div className="h-5 w-36 bg-neutral-200 dark:bg-neutral-700 rounded" /> <div className="h-5 w-36 bg-neutral-200 dark:bg-neutral-700 rounded" />
<div className="h-4 w-48 bg-neutral-200 dark:bg-neutral-700 rounded" /> <div className="h-4 w-48 bg-neutral-200 dark:bg-neutral-700 rounded" />
</div> </div>
<div className="flex-1 min-h-[270px] animate-pulse flex items-end gap-6 justify-center pb-8"> <div className="flex-1 min-h-[270px] animate-pulse flex items-center justify-center">
{[120, 80, 140, 100].map((h, i) => ( <div className="w-[200px] h-[200px] rounded-full bg-neutral-200 dark:bg-neutral-700" />
<div key={i} className="w-12 bg-neutral-200 dark:bg-neutral-700 rounded-t" style={{ height: h }} />
))}
</div> </div>
</div> </div>
) )
} }
const chartConfig = {
count: {
label: 'Count',
},
rage_clicks: {
label: 'Rage Clicks',
color: '#FD5E0F',
},
dead_clicks: {
label: 'Dead Clicks',
color: '#F59E0B',
},
prev_rage_clicks: {
label: 'Prev Rage Clicks',
color: '#78350F',
},
prev_dead_clicks: {
label: 'Prev Dead Clicks',
color: '#92400E',
},
} satisfies ChartConfig
export default function FrustrationTrend({ summary, loading }: FrustrationTrendProps) { export default function FrustrationTrend({ summary, loading }: FrustrationTrendProps) {
if (loading || !summary) return <SkeletonCard /> if (loading || !summary) return <SkeletonCard />
const hasData = summary.rage_clicks > 0 || summary.dead_clicks > 0 || const hasData = summary.rage_clicks > 0 || summary.dead_clicks > 0 ||
summary.prev_rage_clicks > 0 || summary.prev_dead_clicks > 0 summary.prev_rage_clicks > 0 || summary.prev_dead_clicks > 0
const chartData = [
{
label: 'Rage',
current: summary.rage_clicks,
previous: summary.prev_rage_clicks,
},
{
label: 'Dead',
current: summary.dead_clicks,
previous: summary.prev_dead_clicks,
},
]
const totalCurrent = summary.rage_clicks + summary.dead_clicks const totalCurrent = summary.rage_clicks + summary.dead_clicks
const totalPrevious = summary.prev_rage_clicks + summary.prev_dead_clicks const totalPrevious = summary.prev_rage_clicks + summary.prev_dead_clicks
const totalChange = totalPrevious > 0 const totalChange = totalPrevious > 0
? Math.round(((totalCurrent - totalPrevious) / totalPrevious) * 100) ? Math.round(((totalCurrent - totalPrevious) / totalPrevious) * 100)
: null : null
const chartData = [
{ type: 'rage_clicks', count: summary.rage_clicks, fill: 'var(--color-rage_clicks)' },
{ type: 'dead_clicks', count: summary.dead_clicks, fill: 'var(--color-dead_clicks)' },
{ type: 'prev_rage_clicks', count: summary.prev_rage_clicks, fill: 'var(--color-prev_rage_clicks)' },
{ type: 'prev_dead_clicks', count: summary.prev_dead_clicks, fill: 'var(--color-prev_dead_clicks)' },
]
if (!hasData) {
return ( return (
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 h-full flex flex-col"> <div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 h-full flex flex-col">
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
@@ -60,87 +90,6 @@ export default function FrustrationTrend({ summary, loading }: FrustrationTrendP
<p className="text-sm text-neutral-500 dark:text-neutral-400 mb-4"> <p className="text-sm text-neutral-500 dark:text-neutral-400 mb-4">
Current vs. previous period comparison Current vs. previous period comparison
</p> </p>
{hasData ? (
<div className="flex-1 min-h-[270px] flex flex-col">
{/* Summary line */}
<div className="flex items-baseline gap-2 mb-6">
<span className="text-2xl font-bold text-neutral-900 dark:text-white tabular-nums">
{totalCurrent.toLocaleString()}
</span>
<span className="text-sm text-neutral-400 dark:text-neutral-500">
total signals
</span>
{totalChange !== null && (
<span className={`text-xs font-medium ${
totalChange > 0
? 'text-red-600 dark:text-red-400'
: totalChange < 0
? 'text-green-600 dark:text-green-400'
: 'text-neutral-500 dark:text-neutral-400'
}`}>
{totalChange > 0 ? '+' : ''}{totalChange}%
</span>
)}
{totalChange === null && totalCurrent > 0 && (
<span className="text-xs font-medium bg-brand-orange/10 text-brand-orange px-1.5 py-0.5 rounded">
New
</span>
)}
</div>
{/* Chart */}
<div className="flex-1">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={chartData} barGap={4} barCategoryGap="30%">
<XAxis
dataKey="label"
axisLine={false}
tickLine={false}
tick={{ fill: '#a3a3a3', fontSize: 12 }}
/>
<YAxis hide />
<Tooltip
cursor={{ fill: 'transparent' }}
contentStyle={{
backgroundColor: '#171717',
border: '1px solid #404040',
borderRadius: 8,
fontSize: 12,
color: '#fff',
}}
formatter={(value: number, name: string) => [
value.toLocaleString(),
name === 'current' ? 'Current' : 'Previous',
]}
/>
<Bar dataKey="previous" radius={[4, 4, 0, 0]} maxBarSize={40}>
{chartData.map((_, i) => (
<Cell key={i} fill="#404040" />
))}
</Bar>
<Bar dataKey="current" radius={[4, 4, 0, 0]} maxBarSize={40}>
{chartData.map((_, i) => (
<Cell key={i} fill="#FD5E0F" />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
{/* Legend */}
<div className="flex items-center justify-center gap-6 mt-4 text-xs text-neutral-400 dark:text-neutral-500">
<div className="flex items-center gap-1.5">
<div className="w-2.5 h-2.5 rounded-sm bg-[#FD5E0F]" />
<span>Current period</span>
</div>
<div className="flex items-center gap-1.5">
<div className="w-2.5 h-2.5 rounded-sm bg-[#404040]" />
<span>Previous period</span>
</div>
</div>
</div>
) : (
<div className="flex-1 min-h-[270px] flex flex-col items-center justify-center text-center px-6 py-8 gap-4"> <div className="flex-1 min-h-[270px] flex flex-col items-center justify-center text-center px-6 py-8 gap-4">
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4"> <div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
<TrendUp className="w-8 h-8 text-neutral-500 dark:text-neutral-400" /> <TrendUp className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
@@ -152,7 +101,53 @@ export default function FrustrationTrend({ summary, loading }: FrustrationTrendP
Frustration trend data will appear here once rage clicks or dead clicks are detected across periods. Frustration trend data will appear here once rage clicks or dead clicks are detected across periods.
</p> </p>
</div> </div>
)}
</div> </div>
) )
}
return (
<Card className="flex flex-col h-full">
<CardHeader className="items-center pb-0">
<CardTitle>Frustration Trend</CardTitle>
<CardDescription>Current vs. previous period</CardDescription>
</CardHeader>
<CardContent className="flex-1 pb-0">
<ChartContainer
config={chartConfig}
className="mx-auto aspect-square max-h-[250px]"
>
<PieChart>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent hideLabel />}
/>
<Pie
data={chartData}
dataKey="count"
nameKey="type"
stroke="0"
/>
</PieChart>
</ChartContainer>
</CardContent>
<CardFooter className="flex-col gap-2 text-sm">
<div className="flex items-center gap-2 leading-none font-medium">
{totalChange !== null ? (
<>
{totalChange > 0 ? 'Up' : totalChange < 0 ? 'Down' : 'No change'} by {Math.abs(totalChange)}% vs previous period <TrendUp className="h-4 w-4" />
</>
) : totalCurrent > 0 ? (
<>
{totalCurrent.toLocaleString()} new signals this period <TrendUp className="h-4 w-4" />
</>
) : (
'No frustration signals detected'
)}
</div>
<div className="leading-none text-muted-foreground">
{totalCurrent.toLocaleString()} total signals in current period
</div>
</CardFooter>
</Card>
)
} }