diff options
Diffstat (limited to 'src/pages/study.tsx')
-rw-r--r-- | src/pages/study.tsx | 480 |
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; +}; |