milestone 9

This commit is contained in:
Stefan Hardegger
2025-11-25 14:16:25 +01:00
parent de4e7c4c6e
commit 9a30d7c4e5
10 changed files with 1225 additions and 32 deletions

View File

@@ -1,6 +1,7 @@
import { auth } from '@/lib/auth'
import { redirect } from 'next/navigation'
import { logout } from '@/actions/auth'
import { getStatistics, getLearningSessions } from '@/actions/progress'
import Link from 'next/link'
async function logoutAction() {
@@ -18,6 +19,14 @@ export default async function DashboardPage() {
const user = session.user as any
// Get dashboard statistics
const statsResult = await getStatistics()
const stats = statsResult.success ? statsResult.data : null
// Get recent learning sessions
const sessionsResult = await getLearningSessions(5)
const recentSessions = sessionsResult.success ? sessionsResult.data : []
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<nav className="bg-white dark:bg-gray-800 shadow">
@@ -39,6 +48,12 @@ export default async function DashboardPage() {
>
Search Hanzi
</Link>
<Link
href="/progress"
className="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
>
Progress
</Link>
<Link
href="/settings"
className="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
@@ -95,10 +110,10 @@ export default async function DashboardPage() {
Due Cards
</h3>
<p className="text-3xl font-bold text-blue-600 dark:text-blue-400 mb-2">
0
{stats?.dueNow || 0}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
No cards due right now
{stats?.dueNow === 0 ? "No cards due right now" : `${stats?.dueToday || 0} due today`}
</p>
</div>
@@ -107,10 +122,10 @@ export default async function DashboardPage() {
Total Learned
</h3>
<p className="text-3xl font-bold text-green-600 dark:text-green-400 mb-2">
0
{stats?.totalLearned || 0}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
Characters mastered
{stats?.streak ? `${stats.streak} day streak!` : "Characters in progress"}
</p>
</div>
@@ -119,7 +134,7 @@ export default async function DashboardPage() {
Daily Goal
</h3>
<p className="text-3xl font-bold text-purple-600 dark:text-purple-400 mb-2">
0/50
{stats?.reviewedToday || 0}/{stats?.dailyGoal || 50}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
Cards reviewed today
@@ -167,6 +182,52 @@ export default async function DashboardPage() {
</Link>
</div>
</div>
{/* Recent Activity */}
{recentSessions && recentSessions.length > 0 && (
<div className="mt-8">
<div className="flex justify-between items-center mb-4">
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
Recent Activity
</h3>
<Link
href="/progress"
className="text-sm text-blue-600 dark:text-blue-400 hover:underline"
>
View All
</Link>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
<div className="divide-y divide-gray-200 dark:divide-gray-700">
{recentSessions.map((session) => (
<div key={session.id} className="p-4 hover:bg-gray-50 dark:hover:bg-gray-700/50">
<div className="flex justify-between items-start">
<div>
<p className="font-medium text-gray-900 dark:text-white">
{session.collectionName}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
{session.cardsReviewed} cards {session.accuracyPercent}% accuracy
</p>
</div>
<div className="text-right">
<p className="text-sm text-gray-500 dark:text-gray-400">
{new Date(session.startedAt).toLocaleDateString()}
</p>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
{new Date(session.startedAt).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
})}
</p>
</div>
</div>
</div>
))}
</div>
</div>
</div>
)}
</main>
</div>
)

View File

@@ -0,0 +1,316 @@
"use client"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import Link from "next/link"
import {
LineChart,
Line,
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from "recharts"
import { getUserProgress, getLearningSessions } from "@/actions/progress"
export default function ProgressPage() {
const router = useRouter()
const [loading, setLoading] = useState(true)
const [progressData, setProgressData] = useState<any>(null)
const [sessions, setSessions] = useState<any[]>([])
const [dateRange, setDateRange] = useState("30") // days
useEffect(() => {
loadProgress()
}, [dateRange])
const loadProgress = async () => {
setLoading(true)
const days = parseInt(dateRange)
const endDate = new Date()
const startDate = new Date()
startDate.setDate(startDate.getDate() - days)
const [progressResult, sessionsResult] = await Promise.all([
getUserProgress(startDate, endDate),
getLearningSessions(20),
])
if (progressResult.success) {
setProgressData(progressResult.data)
}
if (sessionsResult.success) {
setSessions(sessionsResult.data || [])
}
setLoading(false)
}
if (loading) {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center">
<div className="text-gray-600 dark:text-gray-400">Loading progress...</div>
</div>
)
}
const chartData = progressData?.dailyActivity || []
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<nav className="bg-white dark:bg-gray-800 shadow">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16 items-center">
<Link href="/dashboard">
<h1 className="text-xl font-bold text-gray-900 dark:text-white cursor-pointer">
MemoHanzi <span className="text-sm font-normal text-gray-500"></span>
</h1>
</Link>
<div className="flex items-center space-x-4">
<Link
href="/dashboard"
className="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
>
Dashboard
</Link>
<Link
href="/collections"
className="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
>
Collections
</Link>
<Link
href="/hanzi"
className="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
>
Search Hanzi
</Link>
<Link
href="/settings"
className="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
>
Settings
</Link>
</div>
</div>
</div>
</nav>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="mb-8">
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
Learning Progress
</h2>
<p className="text-gray-600 dark:text-gray-400">
Track your learning journey and review statistics
</p>
</div>
{/* Date Range Selector */}
<div className="mb-6">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300 mr-3">
Time Period:
</label>
<select
value={dateRange}
onChange={(e) => setDateRange(e.target.value)}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
>
<option value="7">Last 7 days</option>
<option value="30">Last 30 days</option>
<option value="90">Last 90 days</option>
<option value="365">Last year</option>
</select>
</div>
{/* Summary Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">
Cards Reviewed
</h3>
<p className="text-3xl font-bold text-blue-600 dark:text-blue-400">
{progressData?.cardsReviewed || 0}
</p>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">
Accuracy
</h3>
<p className="text-3xl font-bold text-green-600 dark:text-green-400">
{progressData?.accuracyPercent || 0}%
</p>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">
Total Cards
</h3>
<p className="text-3xl font-bold text-purple-600 dark:text-purple-400">
{progressData?.totalCards || 0}
</p>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">
Avg Session
</h3>
<p className="text-3xl font-bold text-orange-600 dark:text-orange-400">
{progressData?.averageSessionLength || 0}m
</p>
</div>
</div>
{/* Daily Activity Chart */}
{chartData.length > 0 && (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Daily Activity
</h3>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
<XAxis
dataKey="date"
stroke="#9CA3AF"
tick={{ fill: '#9CA3AF' }}
/>
<YAxis stroke="#9CA3AF" tick={{ fill: '#9CA3AF' }} />
<Tooltip
contentStyle={{
backgroundColor: '#1F2937',
border: '1px solid #374151',
borderRadius: '0.5rem',
color: '#F3F4F6'
}}
/>
<Legend wrapperStyle={{ color: '#9CA3AF' }} />
<Bar dataKey="correct" fill="#10B981" name="Correct" stackId="a" />
<Bar dataKey="incorrect" fill="#EF4444" name="Incorrect" stackId="a" />
</BarChart>
</ResponsiveContainer>
</div>
)}
{/* Accuracy Trend Chart */}
{chartData.length > 0 && (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Accuracy Trend
</h3>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
<XAxis
dataKey="date"
stroke="#9CA3AF"
tick={{ fill: '#9CA3AF' }}
/>
<YAxis
stroke="#9CA3AF"
tick={{ fill: '#9CA3AF' }}
domain={[0, 100]}
/>
<Tooltip
contentStyle={{
backgroundColor: '#1F2937',
border: '1px solid #374151',
borderRadius: '0.5rem',
color: '#F3F4F6'
}}
formatter={(value: any, name: any) => {
if (name === "Accuracy") return [`${value.toFixed(1)}%`, name]
return [value, name]
}}
/>
<Legend wrapperStyle={{ color: '#9CA3AF' }} />
<Line
type="monotone"
dataKey={(data: any) => {
const total = data.correct + data.incorrect
return total > 0 ? (data.correct / total) * 100 : 0
}}
stroke="#3B82F6"
strokeWidth={2}
name="Accuracy"
dot={{ fill: '#3B82F6' }}
/>
</LineChart>
</ResponsiveContainer>
</div>
)}
{/* Session History */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
Recent Sessions
</h3>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-900">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Date
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Collection
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Cards
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Correct
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Incorrect
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Accuracy
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{sessions.length === 0 ? (
<tr>
<td colSpan={6} className="px-6 py-8 text-center text-gray-500 dark:text-gray-400">
No learning sessions yet. Start learning to see your progress!
</td>
</tr>
) : (
sessions.map((session) => (
<tr key={session.id} className="hover:bg-gray-50 dark:hover:bg-gray-700/50">
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
{new Date(session.startedAt).toLocaleString()}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
{session.collectionName}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
{session.cardsReviewed}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-green-600 dark:text-green-400">
{session.correctAnswers}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-red-600 dark:text-red-400">
{session.incorrectAnswers}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
{session.accuracyPercent}%
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</main>
</div>
)
}