diff options
author | polwex <polwex@sortug.com> | 2025-05-29 14:08:02 +0700 |
---|---|---|
committer | polwex <polwex@sortug.com> | 2025-05-29 14:08:02 +0700 |
commit | f243847216279cbd43879de8b5ef6dcceb3a2f1d (patch) | |
tree | 1e0be878f164d327762c7bc54f37077d9410dafe /src/components/Flashcard/StudyCard.tsx | |
parent | 4ed3994fb0f6a2a09eb6ac433a62daee2fc01686 (diff) |
lets see
Diffstat (limited to 'src/components/Flashcard/StudyCard.tsx')
-rw-r--r-- | src/components/Flashcard/StudyCard.tsx | 265 |
1 files changed, 265 insertions, 0 deletions
diff --git a/src/components/Flashcard/StudyCard.tsx b/src/components/Flashcard/StudyCard.tsx new file mode 100644 index 0000000..4e554b4 --- /dev/null +++ b/src/components/Flashcard/StudyCard.tsx @@ -0,0 +1,265 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { CardResponse } from "@/lib/types/cards"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Progress } from "@/components/ui/progress"; +import { processReview, gradeCard } from "@/actions/srs"; +import "./cards.css"; + +interface StudyCardProps { + card: CardResponse; + userId: number; + onComplete: (newCard: CardResponse) => void; + onSkip?: () => void; +} + +export default function StudyCard({ card, userId, onComplete, onSkip }: StudyCardProps) { + const [isFlipped, setIsFlipped] = useState(false); + const [startTime, setStartTime] = useState(0); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Reset the timer when a new card is shown + useEffect(() => { + setIsFlipped(false); + setStartTime(Date.now()); + }, [card.id]); + + // Toggle card flip + const flipCard = () => { + if (!isFlipped) { + setIsFlipped(true); + } + }; + + // Calculate time spent on card in milliseconds + const getReviewTime = () => { + return Date.now() - startTime; + }; + + // Handle card grading (Good/Again) + const handleGrade = async (isCorrect: boolean) => { + if (isSubmitting) return; + + setIsSubmitting(true); + + try { + const result = await gradeCard(userId, card.id, isCorrect); + + if ('error' in result) { + console.error("Error grading card:", result.error); + } else { + onComplete(result as CardResponse); + } + } catch (error) { + console.error("Error processing review:", error); + } finally { + setIsSubmitting(false); + } + }; + + // Handle detailed grading with accuracy level + const handleDetailedGrade = async (accuracy: number) => { + if (isSubmitting) return; + + setIsSubmitting(true); + + try { + const reviewTime = getReviewTime(); + const result = await processReview(userId, card.id, accuracy, reviewTime); + + if ('error' in result) { + console.error("Error processing review:", result.error); + } else { + onComplete(result as CardResponse); + } + } catch (error) { + console.error("Error processing review:", error); + } finally { + setIsSubmitting(false); + } + }; + + // Calculate progress percentage for the card + const getProgressPercentage = () => { + const { interval, easeFactor } = card.progress; + // Assuming max interval is 365 days and max ease factor is 4.0 + const intervalProgress = Math.min(interval / 365, 1) * 70; // 70% weight to interval + const easeProgress = Math.min((easeFactor - 1) / 3, 1) * 30; // 30% weight to ease factor + return intervalProgress + easeProgress; + }; + + // Format content based on card type + const formatCardContent = (content: string, isBack: boolean = false) => { + // You can add more sophisticated formatting here based on card type + return content; + }; + + // Render IPA pronunciation if available + const renderIPA = () => { + if (card.expression.ipa && card.expression.ipa.length > 0) { + return ( + <div className="text-gray-500 text-sm mt-2"> + /{card.expression.ipa[0].ipa}/ + </div> + ); + } + return null; + }; + + // Render senses/meanings if available + const renderSenses = () => { + if (card.expression.senses && card.expression.senses.length > 0) { + return ( + <div className="mt-4"> + {card.expression.senses.map((sense, index) => ( + <div key={index} className="mb-3"> + {sense.pos && <span className="text-xs font-medium text-blue-600 mr-2">{sense.pos}</span>} + {sense.senses && sense.senses.map((subsense, i) => ( + <div key={i} className="mt-1"> + {subsense.glosses && subsense.glosses.map((gloss, j) => ( + <div key={j} className="text-sm">{j+1}. {gloss}</div> + ))} + </div> + ))} + </div> + ))} + </div> + ); + } + return null; + }; + + // Show bookmarked status if applicable + const renderBookmarked = () => { + if (card.expression.isBookmarked) { + return <div className="absolute top-2 right-2 text-yellow-500">★</div>; + } + return null; + }; + + return ( + <div className="flex flex-col items-center"> + <div className={cn("flashcard-container", { flipped: isFlipped })} onClick={flipCard}> + <div className="flashcard"> + {/* Front of card */} + <div className="flashcard-front"> + <Card className="w-full h-full flex flex-col justify-center items-center p-6 relative"> + {renderBookmarked()} + <div className="text-2xl font-bold">{card.expression.spelling}</div> + {!isFlipped && renderIPA()} + <div className="mt-4 text-lg">{formatCardContent(card.text)}</div> + {card.note && <div className="mt-2 text-sm text-gray-500">{card.note}</div>} + {!isFlipped && ( + <div className="mt-6 text-sm text-gray-400"> + Click to flip + </div> + )} + </Card> + </div> + + {/* Back of card */} + <div className="flashcard-back"> + <Card className="w-full h-full flex flex-col justify-between p-6 relative"> + {renderBookmarked()} + <div> + <div className="text-2xl font-bold">{card.expression.spelling}</div> + {renderIPA()} + <div className="mt-4 text-lg">{formatCardContent(card.text, true)}</div> + {card.note && <div className="mt-2 text-sm text-gray-500">{card.note}</div>} + {renderSenses()} + </div> + + <div className="flex flex-col mt-6"> + <div className="text-sm text-gray-500 mb-2"> + How well did you remember this? + </div> + <div className="flex justify-between gap-2"> + <Button + variant="destructive" + onClick={() => handleGrade(false)} + disabled={isSubmitting} + className="flex-1" + > + Again + </Button> + <Button + variant="default" + onClick={() => handleGrade(true)} + disabled={isSubmitting} + className="flex-1" + > + Good + </Button> + </div> + + {/* Optional: Detailed grading */} + <div className="grid grid-cols-4 gap-2 mt-3"> + <Button + variant="outline" + size="sm" + onClick={() => handleDetailedGrade(0.2)} + disabled={isSubmitting} + className="text-red-500" + > + Forgot + </Button> + <Button + variant="outline" + size="sm" + onClick={() => handleDetailedGrade(0.6)} + disabled={isSubmitting} + className="text-orange-500" + > + Hard + </Button> + <Button + variant="outline" + size="sm" + onClick={() => handleDetailedGrade(0.8)} + disabled={isSubmitting} + className="text-green-500" + > + Good + </Button> + <Button + variant="outline" + size="sm" + onClick={() => handleDetailedGrade(1.0)} + disabled={isSubmitting} + className="text-blue-500" + > + Easy + </Button> + </div> + </div> + </Card> + </div> + </div> + </div> + + {/* Progress bar */} + <div className="w-full mt-4"> + <Progress value={getProgressPercentage()} className="h-2" /> + <div className="flex justify-between text-xs text-gray-500 mt-1"> + <span>Interval: {card.progress.interval} days</span> + <span>Ease: {card.progress.easeFactor.toFixed(1)}</span> + </div> + </div> + + {/* Skip button */} + {onSkip && ( + <Button + variant="ghost" + onClick={onSkip} + className="mt-4" + disabled={isSubmitting} + > + Skip + </Button> + )} + </div> + ); +}
\ No newline at end of file |