diff --git a/messages/de-DE.json b/messages/de-DE.json index 2a2e2d6..b0cffe2 100644 --- a/messages/de-DE.json +++ b/messages/de-DE.json @@ -233,32 +233,11 @@ "showAnswer": "Antwort zeigen", "nextCard": "Weiter", "again": "Nochmal", - "hard": "Schwer", - "good": "Gut", - "easy": "Leicht", - "now": "Jetzt", - "lessThanMinute": "weniger als 1 Minute", - "inMinutes": "in {n} Minute{n, plural, one {} other {n}}", - "inHours": "in {n} Stunde{n, plural, one {} other {n}}", - "inDays": "in {n} Tag{en}", - "inMonths": "in {n} Monat{en}", - "minutes": "Minuten", - "days": "Tage", - "months": "Monate", - "minAbbr": "min", - "dayAbbr": "d", - "cardTypeNew": "Neu", - "cardTypeLearning": "Lernen", - "cardTypeReview": "Wiederholen", - "cardTypeRelearning": "Neu lernen", - "reverse": "Umkehren", - "dictation": "Diktat", - "clickToPlay": "Klicken zum Abspielen", - "yourAnswer": "Ihre Antwort", - "typeWhatYouHear": "Schreiben Sie was Sie hören", - "correct": "Richtig!", - "incorrect": "Falsch", - "nextCard": "Nächste" + "modeOrderLimited": "Reihenfolge", + "modeOrderInfinite": "Schleife", + "modeRandomLimited": "Zufällig", + "modeRandomInfinite": "Zufällig Schleife", + "restart": "Neustart" }, "page": { "unauthorized": "Nicht autorisiert" diff --git a/messages/en-US.json b/messages/en-US.json index 7651161..8f88180 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -296,6 +296,11 @@ "reverse": "Reverse", "dictation": "Dictation", "clickToPlay": "Click to play audio", + "modeOrderLimited": "Order", + "modeOrderInfinite": "Loop", + "modeRandomLimited": "Random", + "modeRandomInfinite": "Random Loop", + "restart": "Restart" "yourAnswer": "Your answer", "typeWhatYouHear": "Type what you hear...", "correct": "Correct", diff --git a/messages/fr-FR.json b/messages/fr-FR.json index 999f373..315f0c6 100644 --- a/messages/fr-FR.json +++ b/messages/fr-FR.json @@ -304,32 +304,11 @@ "showAnswer": "Montrer réponse", "nextCard": "Suivant", "again": "Encore", - "hard": "Difficile", - "good": "Bien", - "easy": "Facile", - "now": "Maintenant", - "lessThanMinute": "moins d'une minute", - "inMinutes": "dans {n} minute{s}", - "inHours": "dans {n} heure{s}", - "inDays": "dans {n} jour{s}", - "inMonths": "dans {n} mois", - "minutes": "minutes", - "days": "jours", - "months": "mois", - "minAbbr": "min", - "dayAbbr": "j", - "cardTypeNew": "Nouveau", - "cardTypeLearning": "Apprentissage", - "cardTypeReview": "Révision", - "cardTypeRelearning": "Réapprentissage", - "reverse": "Inverser", - "dictation": "Dictée", - "clickToPlay": "Cliquer pour jouer", - "yourAnswer": "Votre réponse", - "typeWhatYouHear": "Tapez ce que vous entendez", - "correct": "Correct!", - "incorrect": "Incorrect", - "nextCard": "Suivant" + "modeOrderLimited": "Ordre", + "modeOrderInfinite": "Boucle", + "modeRandomLimited": "Aléatoire", + "modeRandomInfinite": "Aléatoire Boucle", + "restart": "Recommencer" }, "page": { "unauthorized": "Non autorisé" diff --git a/messages/it-IT.json b/messages/it-IT.json index fb53892..a834e0b 100644 --- a/messages/it-IT.json +++ b/messages/it-IT.json @@ -304,6 +304,11 @@ "showAnswer": "Mostra risposta", "nextCard": "Prossima", "again": "Ancora", + "modeOrderLimited": "Ordine", + "modeOrderInfinite": "Ciclo", + "modeRandomLimited": "Casuale", + "modeRandomInfinite": "Casuale Ciclo", + "restart": "Ricomincia", "hard": "Difficile", "good": "Buono", "easy": "Facile", diff --git a/messages/ja-JP.json b/messages/ja-JP.json index 9ee99fb..018b681 100644 --- a/messages/ja-JP.json +++ b/messages/ja-JP.json @@ -297,7 +297,12 @@ "typeWhatYouHear": "聞こえた内容を入力", "correct": "正解", "incorrect": "不正解", - "nextCard": "次へ" + "nextCard": "次へ", + "modeOrderLimited": "順序", + "modeOrderInfinite": "ループ", + "modeRandomLimited": "ランダム", + "modeRandomInfinite": "ランダムループ", + "restart": "最初から" }, "page": { "unauthorized": "このデッキにアクセスする権限がありません" diff --git a/messages/ko-KR.json b/messages/ko-KR.json index 578912c..47a957a 100644 --- a/messages/ko-KR.json +++ b/messages/ko-KR.json @@ -304,32 +304,11 @@ "showAnswer": "정답 보기", "nextCard": "다음", "again": "다시", - "hard": "어려움", - "good": "좋음", - "easy": "쉬움", - "now": "지금", - "lessThanMinute": "1분 미만", - "inMinutes": "{n}분 후", - "inHours": "{n}시간 후", - "inDays": "{n}일 후", - "inMonths": "{n}개월 후", - "minutes": "분", - "days": "일", - "months": "개월", - "minAbbr": "분", - "dayAbbr": "일", - "cardTypeNew": "새 카드", - "cardTypeLearning": "학습 중", - "cardTypeReview": "복습", - "cardTypeRelearning": "재학습", - "reverse": "반대로", - "dictation": "받아쓰기", - "clickToPlay": "클릭하여 재생", - "yourAnswer": "당신의 답", - "typeWhatYouHear": "들은 내용을 입력하세요", - "correct": "정답!", - "incorrect": "오답", - "nextCard": "다음" + "modeOrderLimited": "순서", + "modeOrderInfinite": "반복", + "modeRandomLimited": "무작위", + "modeRandomInfinite": "무작위 반복", + "restart": "다시 시작" }, "page": { "unauthorized": "권한이 없습니다" diff --git a/messages/ug-CN.json b/messages/ug-CN.json index 7e55075..96acec5 100644 --- a/messages/ug-CN.json +++ b/messages/ug-CN.json @@ -329,7 +329,11 @@ "typeWhatYouHear": "ئاڭلىغىنىڭىزنى يېزىڭ", "correct": "توغرا!", "incorrect": "خاتا", - "nextCard": "كېيىنكى" + "modeOrderLimited": "تەرتىپ", + "modeOrderInfinite": "دەۋرىيە", + "modeRandomLimited": "ئىختىيارى", + "modeRandomInfinite": "ئىختىيارى دەۋرىيە", + "restart": "قايتا باشلا" }, "page": { "unauthorized": "ھوقۇقسىز" @@ -630,4 +634,4 @@ "noFollowers": "تېخى ئەگەشكۈچى يوق", "noFollowing": "تېخى ئەگىشىدىغان ئادەم يوق" } -} +} \ No newline at end of file diff --git a/messages/zh-CN.json b/messages/zh-CN.json index 706aba7..d81a9d6 100644 --- a/messages/zh-CN.json +++ b/messages/zh-CN.json @@ -297,7 +297,12 @@ "typeWhatYouHear": "输入你听到的内容", "correct": "正确", "incorrect": "错误", - "nextCard": "下一张" + "nextCard": "下一张", + "modeOrderLimited": "顺序", + "modeOrderInfinite": "循环", + "modeRandomLimited": "随机", + "modeRandomInfinite": "随机循环", + "restart": "重新开始" }, "page": { "unauthorized": "您无权访问该牌组" diff --git a/src/app/decks/[deck_id]/learn/Memorize.tsx b/src/app/decks/[deck_id]/learn/Memorize.tsx index 8cc64cc..45c2893 100644 --- a/src/app/decks/[deck_id]/learn/Memorize.tsx +++ b/src/app/decks/[deck_id]/learn/Memorize.tsx @@ -4,7 +4,7 @@ import { useState, useEffect, useTransition, useCallback, useRef } from "react"; import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; import localFont from "next/font/local"; -import { Layers, Check, RotateCcw, Volume2, Headphones, ChevronLeft, ChevronRight } from "lucide-react"; +import { Layers, Check, RotateCcw, Volume2, Headphones, ChevronLeft, ChevronRight, Shuffle, List, Repeat, Infinity } from "lucide-react"; import { actionGetCardsByDeckId } from "@/modules/card/card-action"; import type { ActionOutputCard } from "@/modules/card/card-action-dto"; import { PageLayout } from "@/components/ui/PageLayout"; @@ -19,6 +19,8 @@ const myFont = localFont({ src: "../../../../../public/fonts/NotoNaskhArabic-VariableFont_wght.ttf", }); +type StudyMode = "order-limited" | "order-infinite" | "random-limited" | "random-infinite"; + interface MemorizeProps { deckId: number; deckName: string; @@ -29,6 +31,7 @@ const Memorize: React.FC = ({ deckId, deckName }) => { const router = useRouter(); const [isPending, startTransition] = useTransition(); + const [originalCards, setOriginalCards] = useState([]); const [cards, setCards] = useState([]); const [currentIndex, setCurrentIndex] = useState(0); const [showAnswer, setShowAnswer] = useState(false); @@ -36,10 +39,20 @@ const Memorize: React.FC = ({ deckId, deckName }) => { const [error, setError] = useState(null); const [isReversed, setIsReversed] = useState(false); const [isDictation, setIsDictation] = useState(false); + const [studyMode, setStudyMode] = useState("order-limited"); const { play, stop, load } = useAudioPlayer(); const audioUrlRef = useRef(null); const [isAudioLoading, setIsAudioLoading] = useState(false); + const shuffleCards = useCallback((cardArray: ActionOutputCard[]): ActionOutputCard[] => { + const shuffled = [...cardArray]; + for (let i = shuffled.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; + } + return shuffled; + }, []); + useEffect(() => { let ignore = false; @@ -50,6 +63,7 @@ const Memorize: React.FC = ({ deckId, deckName }) => { const result = await actionGetCardsByDeckId({ deckId, limit: 100 }); if (!ignore) { if (result.success && result.data) { + setOriginalCards(result.data); setCards(result.data); setCurrentIndex(0); setShowAnswer(false); @@ -70,6 +84,16 @@ const Memorize: React.FC = ({ deckId, deckName }) => { }; }, [deckId]); + useEffect(() => { + if (studyMode.startsWith("random")) { + setCards(shuffleCards(originalCards)); + } else { + setCards(originalCards); + } + setCurrentIndex(0); + setShowAnswer(false); + }, [studyMode, originalCards, shuffleCards]); + const getCurrentCard = (): ActionOutputCard | null => { return cards[currentIndex] ?? null; }; @@ -108,25 +132,46 @@ const Memorize: React.FC = ({ deckId, deckName }) => { setShowAnswer(true); }, []); + const isInfinite = studyMode.endsWith("infinite"); + const handleNextCard = useCallback(() => { - if (currentIndex < cards.length - 1) { - setCurrentIndex(currentIndex + 1); - setShowAnswer(false); - setIsReversed(false); - setIsDictation(false); - cleanupAudio(); + if (isInfinite) { + if (currentIndex >= cards.length - 1) { + if (studyMode.startsWith("random")) { + setCards(shuffleCards(originalCards)); + } + setCurrentIndex(0); + } else { + setCurrentIndex(currentIndex + 1); + } + } else { + if (currentIndex < cards.length - 1) { + setCurrentIndex(currentIndex + 1); + } } - }, [currentIndex, cards.length]); + setShowAnswer(false); + setIsReversed(false); + setIsDictation(false); + cleanupAudio(); + }, [currentIndex, cards.length, isInfinite, studyMode, originalCards, shuffleCards]); const handlePrevCard = useCallback(() => { - if (currentIndex > 0) { - setCurrentIndex(currentIndex - 1); - setShowAnswer(false); - setIsReversed(false); - setIsDictation(false); - cleanupAudio(); + if (isInfinite) { + if (currentIndex <= 0) { + setCurrentIndex(cards.length - 1); + } else { + setCurrentIndex(currentIndex - 1); + } + } else { + if (currentIndex > 0) { + setCurrentIndex(currentIndex - 1); + } } - }, [currentIndex]); + setShowAnswer(false); + setIsReversed(false); + setIsDictation(false); + cleanupAudio(); + }, [currentIndex, cards.length, isInfinite]); const cleanupAudio = useCallback(() => { if (audioUrlRef.current) { @@ -249,6 +294,14 @@ const Memorize: React.FC = ({ deckId, deckName }) => { const currentCard = getCurrentCard()!; const displayFront = getFrontText(currentCard); + const isFinished = !isInfinite && currentIndex === cards.length - 1 && showAnswer; + + const studyModeOptions: { value: StudyMode; label: string; icon: React.ReactNode }[] = [ + { value: "order-limited", label: t("orderLimited"), icon: }, + { value: "order-infinite", label: t("orderInfinite"), icon: }, + { value: "random-limited", label: t("randomLimited"), icon: }, + { value: "random-infinite", label: t("randomInfinite"), icon: }, + ]; return ( @@ -257,54 +310,75 @@ const Memorize: React.FC = ({ deckId, deckName }) => { {deckName} - - {t("progress", { current: currentIndex + 1, total: cards.length })} - + {!isInfinite && ( + + {t("progress", { current: currentIndex + 1, total: cards.length })} + + )} - + {!isInfinite && ( + + )} - - { - setIsReversed(!isReversed); - setShowAnswer(false); - }} - selected={isReversed} - leftIcon={} - size="sm" - > - {t("reverse")} - - { - setIsDictation(!isDictation); - }} - selected={isDictation} - leftIcon={} - size="sm" - > - {t("dictation")} - - + + + {studyModeOptions.map((option) => ( + setStudyMode(option.value)} + selected={studyMode === option.value} + leftIcon={option.icon} + size="sm" + > + {option.label} + + ))} + + + + { + setIsReversed(!isReversed); + setShowAnswer(false); + }} + selected={isReversed} + leftIcon={} + size="sm" + > + {t("reverse")} + + { + setIsDictation(!isDictation); + }} + selected={isDictation} + leftIcon={} + size="sm" + > + {t("dictation")} + + +
{isDictation ? ( <> - - - -

{t("clickToPlay")}

+ {currentCard.ipa ? ( +
+ {currentCard.ipa} +
+ ) : ( +
+ {t("noIpa")} +
+ )}
{showAnswer && ( @@ -314,11 +388,6 @@ const Memorize: React.FC = ({ deckId, deckName }) => {
{displayFront}
- {currentCard.ipa && ( -
- {currentCard.ipa} -
- )} {getBackContent(currentCard)} @@ -354,11 +423,25 @@ const Memorize: React.FC = ({ deckId, deckName }) => { {t("showAnswer")} Space + ) : isFinished ? ( + +
+ +
+

{t("allDoneDesc")}

+ + router.push("/decks")} className="px-4 py-2"> + {t("backToDecks")} + + setCurrentIndex(0)} className="px-4 py-2"> + {t("restart")} + + +
) : ( @@ -369,7 +452,6 @@ const Memorize: React.FC = ({ deckId, deckName }) => {