refactor: migrate to monorepo structure with Docker, PocketBase, and e2e tests
- 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
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
'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;
|
||||
}
|
||||
Reference in New Issue
Block a user