summaryrefslogtreecommitdiff
path: root/src/pages/study.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/pages/study.tsx')
-rw-r--r--src/pages/study.tsx480
1 files changed, 480 insertions, 0 deletions
diff --git a/src/pages/study.tsx b/src/pages/study.tsx
new file mode 100644
index 0000000..f9450a7
--- /dev/null
+++ b/src/pages/study.tsx
@@ -0,0 +1,480 @@
+import { getContextData } from "waku/middleware/context";
+import { Link } from "waku";
+import { getUserLessons, getUserStudyStats } from "@/actions/srs";
+import {
+ BookOpen,
+ GraduationCap,
+ Clock,
+ Star,
+ ChevronRight,
+ BrainCircuit,
+ Flame,
+ Layers,
+ CalendarDays
+} from "lucide-react";
+
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
+import { Progress } from "@/components/ui/progress";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Badge } from "@/components/ui/badge";
+import Navbar from "@/components/Navbar";
+
+// This is a server component that gets the initial data
+export default async function StudyPage() {
+ const { user } = getContextData() as any;
+
+ // Redirect to login if not authenticated
+ if (!user) {
+ return (
+ <div className="min-h-screen bg-gray-50">
+ <Navbar user={null} />
+ <div className="container mx-auto py-16 px-4">
+ <Card className="max-w-md mx-auto p-6 text-center">
+ <h1 className="text-2xl font-bold mb-4">Login Required</h1>
+ <p className="text-gray-600 mb-6">
+ You need to be logged in to access your study dashboard.
+ </p>
+ <div className="flex flex-col space-y-4">
+ <Button asChild>
+ <a href="/login">Log in</a>
+ </Button>
+ <Button variant="outline" asChild>
+ <a href="/">Return to home page</a>
+ </Button>
+ </div>
+ </Card>
+ </div>
+ </div>
+ );
+ }
+
+ // Fetch user study data
+ let userStats;
+ let userLessons;
+
+ try {
+ // Get user stats and lessons in parallel
+ [userStats, userLessons] = await Promise.all([
+ getUserStudyStats(user.id),
+ getUserLessons(user.id)
+ ]);
+ } catch (error) {
+ console.error("Error fetching study data:", error);
+ userStats = {
+ totalCards: 0,
+ masteredCards: 0,
+ dueCards: 0,
+ averageEaseFactor: 2.5,
+ successRate: 0,
+ streakDays: 0
+ };
+ userLessons = [];
+ }
+
+ // Calculate overall progress
+ const overallProgress = userStats.totalCards > 0
+ ? Math.round((userStats.masteredCards / userStats.totalCards) * 100)
+ : 0;
+
+ // Sort lessons by different criteria
+ const dueLessons = [...userLessons].sort((a, b) => b.dueCards - a.dueCards).filter(l => l.dueCards > 0);
+ const inProgressLessons = userLessons.filter(lesson => lesson.progress > 0 && lesson.progress < 100);
+ const recentLessons = [...userLessons].sort((a, b) => b.id - a.id).slice(0, 4);
+
+ return (
+ <div className="min-h-screen bg-gray-50">
+ <Navbar user={user} />
+
+ <div className="container mx-auto py-8 px-4">
+ {/* Dashboard Header */}
+ <div className="mb-8">
+ <h1 className="text-3xl font-bold text-gray-900">Study Dashboard</h1>
+ <p className="text-gray-600 mt-2">
+ Track your progress, review due cards, and continue your language learning journey
+ </p>
+ </div>
+
+ {/* Stats Overview */}
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
+ <Card>
+ <CardContent className="p-6">
+ <div className="flex items-center justify-between mb-4">
+ <div className="bg-indigo-100 p-3 rounded-full">
+ <BrainCircuit className="h-6 w-6 text-indigo-600" />
+ </div>
+ <Badge variant="outline" className="bg-gray-100">
+ Total
+ </Badge>
+ </div>
+ <h3 className="text-2xl font-bold">{userStats.totalCards}</h3>
+ <p className="text-gray-500 text-sm">Total Cards</p>
+ </CardContent>
+ </Card>
+
+ <Card>
+ <CardContent className="p-6">
+ <div className="flex items-center justify-between mb-4">
+ <div className="bg-green-100 p-3 rounded-full">
+ <Star className="h-6 w-6 text-green-600" />
+ </div>
+ <Badge variant="outline" className="bg-gray-100">
+ {Math.round(userStats.masteredCards / Math.max(userStats.totalCards, 1) * 100)}%
+ </Badge>
+ </div>
+ <h3 className="text-2xl font-bold">{userStats.masteredCards}</h3>
+ <p className="text-gray-500 text-sm">Mastered Cards</p>
+ </CardContent>
+ </Card>
+
+ <Card>
+ <CardContent className="p-6">
+ <div className="flex items-center justify-between mb-4">
+ <div className="bg-amber-100 p-3 rounded-full">
+ <Clock className="h-6 w-6 text-amber-600" />
+ </div>
+ <Badge variant="outline" className={userStats.dueCards > 0 ? "bg-red-100 text-red-800" : "bg-gray-100"}>
+ {userStats.dueCards > 0 ? "Due Today" : "All Caught Up"}
+ </Badge>
+ </div>
+ <h3 className="text-2xl font-bold">{userStats.dueCards}</h3>
+ <p className="text-gray-500 text-sm">Cards Due for Review</p>
+ </CardContent>
+ </Card>
+
+ <Card>
+ <CardContent className="p-6">
+ <div className="flex items-center justify-between mb-4">
+ <div className="bg-blue-100 p-3 rounded-full">
+ <Flame className="h-6 w-6 text-blue-600" />
+ </div>
+ <Badge variant="outline" className="bg-gray-100">
+ Streak
+ </Badge>
+ </div>
+ <h3 className="text-2xl font-bold">{userStats.streakDays}</h3>
+ <p className="text-gray-500 text-sm">Days in a Row</p>
+ </CardContent>
+ </Card>
+ </div>
+
+ {/* Main Content Tabs */}
+ <Tabs defaultValue="all" className="mb-8">
+ <TabsList className="mb-6">
+ <TabsTrigger value="all">All Lessons</TabsTrigger>
+ <TabsTrigger value="due">
+ Due for Review {dueLessons.length > 0 && `(${dueLessons.length})`}
+ </TabsTrigger>
+ <TabsTrigger value="in-progress">In Progress</TabsTrigger>
+ <TabsTrigger value="stats">Study Stats</TabsTrigger>
+ </TabsList>
+
+ {/* All Lessons Tab */}
+ <TabsContent value="all">
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
+ {userLessons.length > 0 ? (
+ userLessons.map((lesson) => (
+ <LessonCard
+ key={lesson.id}
+ lesson={lesson}
+ />
+ ))
+ ) : (
+ <div className="col-span-full text-center py-12">
+ <h3 className="text-lg font-medium text-gray-700 mb-2">No lessons available yet</h3>
+ <p className="text-gray-500 mb-6">
+ Start your language learning journey by adding lessons
+ </p>
+ <Button asChild>
+ <a href="/parse">Parse New Text</a>
+ </Button>
+ </div>
+ )}
+ </div>
+ </TabsContent>
+
+ {/* Due for Review Tab */}
+ <TabsContent value="due">
+ {dueLessons.length > 0 ? (
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
+ {dueLessons.map((lesson) => (
+ <LessonCard
+ key={lesson.id}
+ lesson={lesson}
+ showDueCardsBadge={true}
+ />
+ ))}
+ </div>
+ ) : (
+ <Card>
+ <CardContent className="p-8 text-center">
+ <div className="bg-green-100 h-16 w-16 rounded-full flex items-center justify-center mx-auto mb-4">
+ <GraduationCap className="h-8 w-8 text-green-600" />
+ </div>
+ <h3 className="text-xl font-medium text-gray-800 mb-2">All caught up!</h3>
+ <p className="text-gray-600 mb-6">
+ You don't have any cards due for review right now. Check back later or start a new lesson.
+ </p>
+ <Button asChild>
+ <a href="/parse">Parse New Text</a>
+ </Button>
+ </CardContent>
+ </Card>
+ )}
+ </TabsContent>
+
+ {/* In Progress Tab */}
+ <TabsContent value="in-progress">
+ {inProgressLessons.length > 0 ? (
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
+ {inProgressLessons.map((lesson) => (
+ <LessonCard
+ key={lesson.id}
+ lesson={lesson}
+ showProgress={true}
+ />
+ ))}
+ </div>
+ ) : (
+ <Card>
+ <CardContent className="p-8 text-center">
+ <div className="bg-indigo-100 h-16 w-16 rounded-full flex items-center justify-center mx-auto mb-4">
+ <BookOpen className="h-8 w-8 text-indigo-600" />
+ </div>
+ <h3 className="text-xl font-medium text-gray-800 mb-2">No lessons in progress</h3>
+ <p className="text-gray-600 mb-6">
+ Start learning by selecting a lesson from the All Lessons tab.
+ </p>
+ <Button asChild>
+ <Link to="/">Browse Languages</Link>
+ </Button>
+ </CardContent>
+ </Card>
+ )}
+ </TabsContent>
+
+ {/* Stats Tab */}
+ <TabsContent value="stats">
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
+ <Card className="md:col-span-2">
+ <CardHeader>
+ <CardTitle>Overall Progress</CardTitle>
+ <CardDescription>Your language learning journey progress</CardDescription>
+ </CardHeader>
+ <CardContent className="p-6">
+ <div className="mb-4">
+ <div className="flex justify-between text-sm text-gray-600 mb-2">
+ <span>Progress</span>
+ <span>{overallProgress}%</span>
+ </div>
+ <Progress value={overallProgress} className="h-3" />
+ </div>
+
+ <div className="grid grid-cols-2 gap-4 mt-8">
+ <div className="flex flex-col">
+ <span className="text-sm text-gray-500">Success Rate</span>
+ <span className="text-2xl font-bold">{Math.round(userStats.successRate * 100)}%</span>
+ </div>
+ <div className="flex flex-col">
+ <span className="text-sm text-gray-500">Average Ease</span>
+ <span className="text-2xl font-bold">{userStats.averageEaseFactor.toFixed(1)}</span>
+ </div>
+ <div className="flex flex-col">
+ <span className="text-sm text-gray-500">Total Cards</span>
+ <span className="text-2xl font-bold">{userStats.totalCards}</span>
+ </div>
+ <div className="flex flex-col">
+ <span className="text-sm text-gray-500">Mastered</span>
+ <span className="text-2xl font-bold">{userStats.masteredCards}</span>
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+
+ <Card>
+ <CardHeader>
+ <CardTitle>Study Streak</CardTitle>
+ <CardDescription>Keep the momentum going!</CardDescription>
+ </CardHeader>
+ <CardContent className="p-6">
+ <div className="flex flex-col items-center">
+ <div className="bg-indigo-100 w-20 h-20 rounded-full flex items-center justify-center mb-4">
+ <Flame className="h-10 w-10 text-indigo-600" />
+ </div>
+ <div className="text-4xl font-bold mb-1">{userStats.streakDays}</div>
+ <div className="text-gray-500 mb-4">Days in a row</div>
+ <Button variant="outline" className="w-full" asChild>
+ <Link to={dueLessons.length > 0 ? `/study/${dueLessons[0].id}` : "/study"}>
+ Keep the streak alive
+ </Link>
+ </Button>
+ </div>
+ </CardContent>
+ </Card>
+
+ <Card className="md:col-span-3">
+ <CardHeader>
+ <CardTitle>Recent Activity</CardTitle>
+ <CardDescription>Your latest learning progress</CardDescription>
+ </CardHeader>
+ <CardContent className="p-0">
+ <div className="divide-y">
+ {recentLessons.map(lesson => (
+ <div key={lesson.id} className="flex items-center justify-between py-4 px-6">
+ <div className="flex items-center">
+ <div className="bg-gray-100 p-2 rounded-lg mr-4">
+ <Layers className="h-5 w-5 text-gray-600" />
+ </div>
+ <div>
+ <div className="font-medium">{lesson.name}</div>
+ <div className="text-sm text-gray-500">
+ {lesson.description || `Lesson ${lesson.id}`}
+ </div>
+ </div>
+ </div>
+ <div className="flex items-center">
+ <div className="text-right mr-4">
+ <div className="font-medium">{Math.round(lesson.progress)}%</div>
+ <div className="text-xs text-gray-500">
+ {lesson.masteredCards} / {lesson.totalCards} cards
+ </div>
+ </div>
+ <Button variant="ghost" size="icon" asChild>
+ <Link to={`/study/${lesson.id}`}>
+ <ChevronRight className="h-5 w-5" />
+ </Link>
+ </Button>
+ </div>
+ </div>
+ ))}
+ </div>
+ </CardContent>
+ </Card>
+ </div>
+ </TabsContent>
+ </Tabs>
+
+ {/* Quick Access Section */}
+ <div className="mb-8">
+ <h2 className="text-xl font-bold text-gray-900 mb-4">Quick Access</h2>
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
+ <Card className="hover:shadow-md transition-shadow duration-200">
+ <CardContent className="p-6 flex items-center">
+ <div className="bg-indigo-100 p-3 rounded-full mr-4">
+ <BookOpen className="h-6 w-6 text-indigo-600" />
+ </div>
+ <div className="flex-1">
+ <h3 className="font-medium">Start New Lesson</h3>
+ <p className="text-sm text-gray-500">Add text to study</p>
+ </div>
+ <Button variant="ghost" size="icon" asChild>
+ <Link to="/parse">
+ <ChevronRight className="h-5 w-5" />
+ </Link>
+ </Button>
+ </CardContent>
+ </Card>
+
+ <Card className="hover:shadow-md transition-shadow duration-200">
+ <CardContent className="p-6 flex items-center">
+ <div className="bg-amber-100 p-3 rounded-full mr-4">
+ <CalendarDays className="h-6 w-6 text-amber-600" />
+ </div>
+ <div className="flex-1">
+ <h3 className="font-medium">Review Due Cards</h3>
+ <p className="text-sm text-gray-500">{userStats.dueCards} cards waiting</p>
+ </div>
+ <Button variant="ghost" size="icon" asChild>
+ <Link to={dueLessons.length > 0 ? `/study/${dueLessons[0].id}` : "/study"}>
+ <ChevronRight className="h-5 w-5" />
+ </Link>
+ </Button>
+ </CardContent>
+ </Card>
+
+ <Card className="hover:shadow-md transition-shadow duration-200">
+ <CardContent className="p-6 flex items-center">
+ <div className="bg-green-100 p-3 rounded-full mr-4">
+ <GraduationCap className="h-6 w-6 text-green-600" />
+ </div>
+ <div className="flex-1">
+ <h3 className="font-medium">Track Progress</h3>
+ <p className="text-sm text-gray-500">View detailed statistics</p>
+ </div>
+ <Button variant="ghost" size="icon" asChild onClick={() => document.querySelector('[data-value="stats"]')?.click()}>
+ <span>
+ <ChevronRight className="h-5 w-5" />
+ </span>
+ </Button>
+ </CardContent>
+ </Card>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+
+// Lesson Card Component
+function LessonCard({
+ lesson,
+ showDueCardsBadge = false,
+ showProgress = false
+}: {
+ lesson: any;
+ showDueCardsBadge?: boolean;
+ showProgress?: boolean;
+}) {
+ return (
+ <Card className="hover:shadow-md transition-shadow duration-300 h-full">
+ <CardHeader className="pb-2">
+ <div className="flex justify-between items-start">
+ <div>
+ <CardTitle className="text-lg">{lesson.name}</CardTitle>
+ <CardDescription className="line-clamp-1">
+ {lesson.description || `Lesson ${lesson.id}`}
+ </CardDescription>
+ </div>
+ {showDueCardsBadge && lesson.dueCards > 0 && (
+ <Badge variant="destructive">
+ {lesson.dueCards} due
+ </Badge>
+ )}
+ </div>
+ </CardHeader>
+ <CardContent className="pb-4">
+ <div className="text-sm text-gray-500 mb-2 flex justify-between">
+ <span>{lesson.masteredCards} of {lesson.totalCards} mastered</span>
+ <span>{Math.round(lesson.progress)}% complete</span>
+ </div>
+ <Progress value={lesson.progress} className="h-2" />
+
+ {showProgress && (
+ <div className="grid grid-cols-2 gap-4 mt-4 text-sm">
+ <div className="flex flex-col">
+ <span className="text-gray-500">Due Cards</span>
+ <span className="font-medium">{lesson.dueCards}</span>
+ </div>
+ <div className="flex flex-col">
+ <span className="text-gray-500">Mastered</span>
+ <span className="font-medium">{lesson.masteredCards}</span>
+ </div>
+ </div>
+ )}
+ </CardContent>
+ <CardFooter className="pt-0">
+ <Button className="w-full" asChild>
+ <Link to={`/study/${lesson.id}`}>
+ {lesson.dueCards > 0 ? "Review Due Cards" : "Continue Learning"}
+ </Link>
+ </Button>
+ </CardFooter>
+ </Card>
+ );
+}
+
+export const getConfig = async () => {
+ return {
+ render: "dynamic",
+ } as const;
+};