diff options
Diffstat (limited to 'src/pages')
-rw-r--r-- | src/pages/logintest/Form.tsx | 53 | ||||
-rw-r--r-- | src/pages/logintest/ServerForm.tsx | 67 | ||||
-rw-r--r-- | src/pages/logintest/funcs.ts | 24 | ||||
-rw-r--r-- | src/pages/logintest/index.tsx | 6 | ||||
-rw-r--r-- | src/pages/study.tsx | 480 |
5 files changed, 483 insertions, 147 deletions
diff --git a/src/pages/logintest/Form.tsx b/src/pages/logintest/Form.tsx deleted file mode 100644 index a593acb..0000000 --- a/src/pages/logintest/Form.tsx +++ /dev/null @@ -1,53 +0,0 @@ -"use client"; - -import { useFormStatus } from "react-dom"; - -const SubmitButton = () => { - const { pending } = useFormStatus(); - return ( - <> - <button - disabled={pending} - type="submit" - className="hover:bg-slate-50 w-fit rounded-lg bg-white p-2" - > - {pending ? "Pending..." : "Submit"} - </button> - </> - ); -}; - -export const Form = ({ - message, - greet, -}: { - message: Promise<string>; - greet: (formData: FormData) => Promise<void>; -}) => ( - <div style={{ border: "3px blue dashed", margin: "1em", padding: "1em" }}> - <p>{message}</p> - <form action={greet}> - <div className="flex flex-col gap-1 text-left"> - <div> - Name:{" "} - <input - name="name" - required - className="invalid:border-red-500 rounded-sm border px-2 py-1" - /> - </div> - <div> - Email:{" "} - <input - type="email" - name="email" - required - className="invalid:border-red-500 rounded-sm border px-2 py-1" - /> - </div> - <SubmitButton /> - </div> - </form> - <h3>This is a client component.</h3> - </div> -); diff --git a/src/pages/logintest/ServerForm.tsx b/src/pages/logintest/ServerForm.tsx deleted file mode 100644 index 8e629b8..0000000 --- a/src/pages/logintest/ServerForm.tsx +++ /dev/null @@ -1,67 +0,0 @@ -async function submitUserProfile(formData: FormData) { - "use server"; - const name = formData.get("name"); - const age = formData.get("age"); - const favoriteColor = formData.get("favoriteColor"); - const hobby = formData.get("hobby"); - const isSubscribed = formData.get("newsletter") === "on"; - - console.log({ - name, - age, - favoriteColor, - hobby, - isSubscribed, - }); -} - -export const ServerForm = () => { - return ( - <form action={submitUserProfile} className="space-y-4"> - <div style={{ display: "flex", gap: 4, marginBottom: 4 }}> - <label htmlFor="name">Full Name</label> - <input type="text" name="name" id="name" required /> - </div> - - <div style={{ display: "flex", gap: 4, marginBottom: 4 }}> - <label htmlFor="age">Age</label> - <input type="number" name="age" id="age" min="13" max="120" /> - </div> - - <div style={{ display: "flex", gap: 4, marginBottom: 4 }}> - <label htmlFor="favoriteColor">Favorite Color</label> - <select name="favoriteColor" id="favoriteColor"> - <option value="red">Red</option> - <option value="blue">Blue</option> - <option value="green">Green</option> - <option value="purple">Purple</option> - <option value="yellow">Yellow</option> - </select> - </div> - - <div style={{ display: "flex", gap: 4, marginBottom: 4 }}> - <label htmlFor="hobby">Favorite Hobby</label> - <input - type="text" - name="hobby" - id="hobby" - placeholder="e.g. Reading, Gaming, Cooking" - /> - </div> - - <div style={{ display: "flex", gap: 4, marginBottom: 4 }}> - <label> - <input type="checkbox" name="newsletter" /> - Subscribe to newsletter - </label> - </div> - - <button - type="submit" - className="hover:bg-slate-50 w-fit rounded-lg bg-white p-2" - > - Save Profile - </button> - </form> - ); -}; diff --git a/src/pages/logintest/funcs.ts b/src/pages/logintest/funcs.ts deleted file mode 100644 index 4ffd5ef..0000000 --- a/src/pages/logintest/funcs.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { readFile, writeFile } from "node:fs/promises"; -import { unstable_rerenderRoute } from "waku/router/server"; - -export const getMessage = async () => { - const data = await readFile("./message.txt", "utf8"); - return data; -}; - -export const greet = async (formData: FormData) => { - "use server"; - // simulate a slow server response - await new Promise((resolve) => setTimeout(resolve, 1000)); - const currentData = await getMessage(); - await writeFile( - "./message.txt", - currentData + "\n" + formData.get("name") + " from server!", - ); - unstable_rerenderRoute("/"); -}; - -export const increment = async (count: number) => { - "use server"; - return count + 1; -}; diff --git a/src/pages/logintest/index.tsx b/src/pages/logintest/index.tsx index df8bc08..5707c69 100644 --- a/src/pages/logintest/index.tsx +++ b/src/pages/logintest/index.tsx @@ -1,6 +1,6 @@ -import { Form } from "./Form"; -import { getMessage, greet } from "./funcs"; -import { ServerForm } from "./ServerForm"; +import { Form } from "../../components/logintest/Form"; +import { getMessage, greet } from "../../actions/logintest/funcs"; +import { ServerForm } from "../../components/logintest/ServerForm"; export default function HomePage() { return ( 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; +}; |