From f243847216279cbd43879de8b5ef6dcceb3a2f1d Mon Sep 17 00:00:00 2001
From: polwex <polwex@sortug.com>
Date: Thu, 29 May 2025 14:08:02 +0700
Subject: lets see

---
 src/components/Flashcard/StudyCard.tsx | 265 +++++++++++++++++++++++++++++++++
 1 file changed, 265 insertions(+)
 create mode 100644 src/components/Flashcard/StudyCard.tsx

(limited to 'src/components/Flashcard/StudyCard.tsx')

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
-- 
cgit v1.2.3

