summaryrefslogtreecommitdiff
path: root/src/components/Flashcard/StudyCard.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/Flashcard/StudyCard.tsx')
-rw-r--r--src/components/Flashcard/StudyCard.tsx265
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