- Reorganize into apps/, packages/, docs/, e2e/, pocketbase/ directories - Add Dockerfiles for web, worker, and PocketBase services - Add docker-compose.yml for local orchestration - Add turbo.json for monorepo task management - Add Playwright e2e test infrastructure - Add PocketBase backend with migrations - Remove Vite/Next.js/ESLint/PostCSS config files - Update package.json with workspace dependencies - Add .env.example and .dockerignore
346 lines
9.9 KiB
TypeScript
346 lines
9.9 KiB
TypeScript
'use client';
|
|
|
|
import {
|
|
TrendingUp,
|
|
Clock,
|
|
Flame,
|
|
BarChart3,
|
|
PieChart as PieChartIcon,
|
|
} from 'lucide-react';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import {
|
|
LineChart,
|
|
Line,
|
|
BarChart,
|
|
Bar,
|
|
PieChart,
|
|
Pie,
|
|
Cell,
|
|
AreaChart,
|
|
Area,
|
|
XAxis,
|
|
YAxis,
|
|
CartesianGrid,
|
|
Tooltip,
|
|
Legend,
|
|
ResponsiveContainer,
|
|
} from 'recharts';
|
|
|
|
interface TimeData {
|
|
date: string;
|
|
tasks: number;
|
|
habits: number;
|
|
time: number;
|
|
}
|
|
|
|
interface DomainData {
|
|
name: string;
|
|
value: number;
|
|
color: string;
|
|
}
|
|
|
|
interface HabitData {
|
|
name: string;
|
|
streak: number;
|
|
score: number;
|
|
consistency: number;
|
|
}
|
|
|
|
const COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#8b5cf6', '#ef4444', '#06b6d4'];
|
|
|
|
const chartTooltipStyle = {
|
|
backgroundColor: 'hsl(var(--card))',
|
|
border: '1px solid hsl(var(--border))',
|
|
borderRadius: '0.5rem',
|
|
};
|
|
|
|
interface AnalyticsChartsProps {
|
|
timeData: TimeData[];
|
|
domainData: DomainData[];
|
|
habitData: HabitData[];
|
|
activeTab: string;
|
|
}
|
|
|
|
export function AnalyticsCharts({
|
|
timeData,
|
|
domainData,
|
|
habitData,
|
|
activeTab,
|
|
}: AnalyticsChartsProps) {
|
|
if (activeTab === 'trends') {
|
|
return (
|
|
<div className="grid grid-cols-1 gap-6">
|
|
{/* Productivity trend */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<TrendingUp className="h-5 w-5" aria-hidden="true" />
|
|
Productivity Trend
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<AreaChart data={timeData}>
|
|
<CartesianGrid
|
|
strokeDasharray="3 3"
|
|
stroke="hsl(var(--border))"
|
|
/>
|
|
<XAxis
|
|
dataKey="date"
|
|
stroke="hsl(var(--muted-foreground))"
|
|
fontSize={12}
|
|
/>
|
|
<YAxis
|
|
stroke="hsl(var(--muted-foreground))"
|
|
fontSize={12}
|
|
/>
|
|
<Tooltip contentStyle={chartTooltipStyle} />
|
|
<Legend />
|
|
<Area
|
|
type="monotone"
|
|
dataKey="tasks"
|
|
stackId="1"
|
|
stroke="#3b82f6"
|
|
fill="#3b82f6"
|
|
fillOpacity={0.6}
|
|
name="Tasks Completed"
|
|
/>
|
|
<Area
|
|
type="monotone"
|
|
dataKey="habits"
|
|
stackId="1"
|
|
stroke="#10b981"
|
|
fill="#10b981"
|
|
fillOpacity={0.6}
|
|
name="Habits Logged"
|
|
/>
|
|
</AreaChart>
|
|
</ResponsiveContainer>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Time tracked trend */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Clock className="h-5 w-5" aria-hidden="true" />
|
|
Time Tracked
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<LineChart data={timeData}>
|
|
<CartesianGrid
|
|
strokeDasharray="3 3"
|
|
stroke="hsl(var(--border))"
|
|
/>
|
|
<XAxis
|
|
dataKey="date"
|
|
stroke="hsl(var(--muted-foreground))"
|
|
fontSize={12}
|
|
/>
|
|
<YAxis
|
|
stroke="hsl(var(--muted-foreground))"
|
|
fontSize={12}
|
|
label={{
|
|
value: 'Minutes',
|
|
angle: -90,
|
|
position: 'insideLeft',
|
|
}}
|
|
/>
|
|
<Tooltip contentStyle={chartTooltipStyle} />
|
|
<Line
|
|
type="monotone"
|
|
dataKey="time"
|
|
stroke="#8b5cf6"
|
|
strokeWidth={2}
|
|
dot={{ fill: '#8b5cf6', r: 3 }}
|
|
name="Time (minutes)"
|
|
/>
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (activeTab === 'habits') {
|
|
return (
|
|
<div className="grid grid-cols-1 gap-6">
|
|
{/* Habit streaks */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Flame className="h-5 w-5" aria-hidden="true" />
|
|
Habit Streaks
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{habitData.length === 0 ? (
|
|
<p className="py-8 text-center text-muted-foreground">
|
|
No habits tracked
|
|
</p>
|
|
) : (
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<BarChart data={habitData} layout="vertical">
|
|
<CartesianGrid
|
|
strokeDasharray="3 3"
|
|
stroke="hsl(var(--border))"
|
|
/>
|
|
<XAxis
|
|
type="number"
|
|
stroke="hsl(var(--muted-foreground))"
|
|
fontSize={12}
|
|
/>
|
|
<YAxis
|
|
dataKey="name"
|
|
type="category"
|
|
stroke="hsl(var(--muted-foreground))"
|
|
fontSize={12}
|
|
width={120}
|
|
/>
|
|
<Tooltip contentStyle={chartTooltipStyle} />
|
|
<Bar
|
|
dataKey="streak"
|
|
fill="#f59e0b"
|
|
radius={[0, 4, 4, 0]}
|
|
name="Current Streak (days)"
|
|
/>
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Habit scores */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Habit Scores</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{habitData.length === 0 ? (
|
|
<p className="py-8 text-center text-muted-foreground">
|
|
No habits tracked
|
|
</p>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{habitData.map((habit) => (
|
|
<div key={habit.name}>
|
|
<div className="mb-1 flex items-center justify-between">
|
|
<span className="text-sm font-medium">
|
|
{habit.name}
|
|
</span>
|
|
<span className="text-sm text-muted-foreground">
|
|
{habit.score}/100
|
|
</span>
|
|
</div>
|
|
<div className="h-2 rounded-full bg-muted">
|
|
<div
|
|
className="h-full rounded-full bg-primary transition-all"
|
|
style={{ width: `${habit.score}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (activeTab === 'time') {
|
|
return (
|
|
<div className="grid grid-cols-1 gap-6">
|
|
{/* Time by domain */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<PieChartIcon className="h-5 w-5" aria-hidden="true" />
|
|
Time by Domain
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{domainData.length === 0 ? (
|
|
<p className="py-8 text-center text-muted-foreground">
|
|
No time tracked
|
|
</p>
|
|
) : (
|
|
<div className="flex items-center gap-8">
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<PieChart>
|
|
<Pie
|
|
data={domainData}
|
|
cx="50%"
|
|
cy="50%"
|
|
labelLine={false}
|
|
label={({ name, percent }) =>
|
|
`${name}: ${((percent || 0) * 100).toFixed(0)}%`
|
|
}
|
|
outerRadius={100}
|
|
fill="#8884d8"
|
|
dataKey="value"
|
|
>
|
|
{domainData.map((entry, index) => (
|
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
|
))}
|
|
</Pie>
|
|
<Tooltip contentStyle={chartTooltipStyle} />
|
|
</PieChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Daily breakdown */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<BarChart3 className="h-5 w-5" aria-hidden="true" />
|
|
Daily Activity
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<BarChart data={timeData.slice(-7)}>
|
|
<CartesianGrid
|
|
strokeDasharray="3 3"
|
|
stroke="hsl(var(--border))"
|
|
/>
|
|
<XAxis
|
|
dataKey="date"
|
|
stroke="hsl(var(--muted-foreground))"
|
|
fontSize={12}
|
|
/>
|
|
<YAxis
|
|
stroke="hsl(var(--muted-foreground))"
|
|
fontSize={12}
|
|
/>
|
|
<Tooltip contentStyle={chartTooltipStyle} />
|
|
<Legend />
|
|
<Bar
|
|
dataKey="tasks"
|
|
fill="#3b82f6"
|
|
name="Tasks"
|
|
radius={[4, 4, 0, 0]}
|
|
/>
|
|
<Bar
|
|
dataKey="habits"
|
|
fill="#10b981"
|
|
name="Habits"
|
|
radius={[4, 4, 0, 0]}
|
|
/>
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|