feat: add study modes to Memorize page
- Add 4 study modes: order-limited, order-infinite, random-limited, random-infinite - Add mode selector buttons with icons - Update progress display for infinite modes - Add translations for all 8 locales
This commit is contained in:
@@ -233,32 +233,11 @@
|
|||||||
"showAnswer": "Antwort zeigen",
|
"showAnswer": "Antwort zeigen",
|
||||||
"nextCard": "Weiter",
|
"nextCard": "Weiter",
|
||||||
"again": "Nochmal",
|
"again": "Nochmal",
|
||||||
"hard": "Schwer",
|
"modeOrderLimited": "Reihenfolge",
|
||||||
"good": "Gut",
|
"modeOrderInfinite": "Schleife",
|
||||||
"easy": "Leicht",
|
"modeRandomLimited": "Zufällig",
|
||||||
"now": "Jetzt",
|
"modeRandomInfinite": "Zufällig Schleife",
|
||||||
"lessThanMinute": "weniger als 1 Minute",
|
"restart": "Neustart"
|
||||||
"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"
|
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
"unauthorized": "Nicht autorisiert"
|
"unauthorized": "Nicht autorisiert"
|
||||||
|
|||||||
@@ -296,6 +296,11 @@
|
|||||||
"reverse": "Reverse",
|
"reverse": "Reverse",
|
||||||
"dictation": "Dictation",
|
"dictation": "Dictation",
|
||||||
"clickToPlay": "Click to play audio",
|
"clickToPlay": "Click to play audio",
|
||||||
|
"modeOrderLimited": "Order",
|
||||||
|
"modeOrderInfinite": "Loop",
|
||||||
|
"modeRandomLimited": "Random",
|
||||||
|
"modeRandomInfinite": "Random Loop",
|
||||||
|
"restart": "Restart"
|
||||||
"yourAnswer": "Your answer",
|
"yourAnswer": "Your answer",
|
||||||
"typeWhatYouHear": "Type what you hear...",
|
"typeWhatYouHear": "Type what you hear...",
|
||||||
"correct": "Correct",
|
"correct": "Correct",
|
||||||
|
|||||||
@@ -304,32 +304,11 @@
|
|||||||
"showAnswer": "Montrer réponse",
|
"showAnswer": "Montrer réponse",
|
||||||
"nextCard": "Suivant",
|
"nextCard": "Suivant",
|
||||||
"again": "Encore",
|
"again": "Encore",
|
||||||
"hard": "Difficile",
|
"modeOrderLimited": "Ordre",
|
||||||
"good": "Bien",
|
"modeOrderInfinite": "Boucle",
|
||||||
"easy": "Facile",
|
"modeRandomLimited": "Aléatoire",
|
||||||
"now": "Maintenant",
|
"modeRandomInfinite": "Aléatoire Boucle",
|
||||||
"lessThanMinute": "moins d'une minute",
|
"restart": "Recommencer"
|
||||||
"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"
|
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
"unauthorized": "Non autorisé"
|
"unauthorized": "Non autorisé"
|
||||||
|
|||||||
@@ -304,6 +304,11 @@
|
|||||||
"showAnswer": "Mostra risposta",
|
"showAnswer": "Mostra risposta",
|
||||||
"nextCard": "Prossima",
|
"nextCard": "Prossima",
|
||||||
"again": "Ancora",
|
"again": "Ancora",
|
||||||
|
"modeOrderLimited": "Ordine",
|
||||||
|
"modeOrderInfinite": "Ciclo",
|
||||||
|
"modeRandomLimited": "Casuale",
|
||||||
|
"modeRandomInfinite": "Casuale Ciclo",
|
||||||
|
"restart": "Ricomincia",
|
||||||
"hard": "Difficile",
|
"hard": "Difficile",
|
||||||
"good": "Buono",
|
"good": "Buono",
|
||||||
"easy": "Facile",
|
"easy": "Facile",
|
||||||
|
|||||||
@@ -297,7 +297,12 @@
|
|||||||
"typeWhatYouHear": "聞こえた内容を入力",
|
"typeWhatYouHear": "聞こえた内容を入力",
|
||||||
"correct": "正解",
|
"correct": "正解",
|
||||||
"incorrect": "不正解",
|
"incorrect": "不正解",
|
||||||
"nextCard": "次へ"
|
"nextCard": "次へ",
|
||||||
|
"modeOrderLimited": "順序",
|
||||||
|
"modeOrderInfinite": "ループ",
|
||||||
|
"modeRandomLimited": "ランダム",
|
||||||
|
"modeRandomInfinite": "ランダムループ",
|
||||||
|
"restart": "最初から"
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
"unauthorized": "このデッキにアクセスする権限がありません"
|
"unauthorized": "このデッキにアクセスする権限がありません"
|
||||||
|
|||||||
@@ -304,32 +304,11 @@
|
|||||||
"showAnswer": "정답 보기",
|
"showAnswer": "정답 보기",
|
||||||
"nextCard": "다음",
|
"nextCard": "다음",
|
||||||
"again": "다시",
|
"again": "다시",
|
||||||
"hard": "어려움",
|
"modeOrderLimited": "순서",
|
||||||
"good": "좋음",
|
"modeOrderInfinite": "반복",
|
||||||
"easy": "쉬움",
|
"modeRandomLimited": "무작위",
|
||||||
"now": "지금",
|
"modeRandomInfinite": "무작위 반복",
|
||||||
"lessThanMinute": "1분 미만",
|
"restart": "다시 시작"
|
||||||
"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": "다음"
|
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
"unauthorized": "권한이 없습니다"
|
"unauthorized": "권한이 없습니다"
|
||||||
|
|||||||
@@ -329,7 +329,11 @@
|
|||||||
"typeWhatYouHear": "ئاڭلىغىنىڭىزنى يېزىڭ",
|
"typeWhatYouHear": "ئاڭلىغىنىڭىزنى يېزىڭ",
|
||||||
"correct": "توغرا!",
|
"correct": "توغرا!",
|
||||||
"incorrect": "خاتا",
|
"incorrect": "خاتا",
|
||||||
"nextCard": "كېيىنكى"
|
"modeOrderLimited": "تەرتىپ",
|
||||||
|
"modeOrderInfinite": "دەۋرىيە",
|
||||||
|
"modeRandomLimited": "ئىختىيارى",
|
||||||
|
"modeRandomInfinite": "ئىختىيارى دەۋرىيە",
|
||||||
|
"restart": "قايتا باشلا"
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
"unauthorized": "ھوقۇقسىز"
|
"unauthorized": "ھوقۇقسىز"
|
||||||
|
|||||||
@@ -297,7 +297,12 @@
|
|||||||
"typeWhatYouHear": "输入你听到的内容",
|
"typeWhatYouHear": "输入你听到的内容",
|
||||||
"correct": "正确",
|
"correct": "正确",
|
||||||
"incorrect": "错误",
|
"incorrect": "错误",
|
||||||
"nextCard": "下一张"
|
"nextCard": "下一张",
|
||||||
|
"modeOrderLimited": "顺序",
|
||||||
|
"modeOrderInfinite": "循环",
|
||||||
|
"modeRandomLimited": "随机",
|
||||||
|
"modeRandomInfinite": "随机循环",
|
||||||
|
"restart": "重新开始"
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
"unauthorized": "您无权访问该牌组"
|
"unauthorized": "您无权访问该牌组"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useState, useEffect, useTransition, useCallback, useRef } from "react";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import localFont from "next/font/local";
|
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 { actionGetCardsByDeckId } from "@/modules/card/card-action";
|
||||||
import type { ActionOutputCard } from "@/modules/card/card-action-dto";
|
import type { ActionOutputCard } from "@/modules/card/card-action-dto";
|
||||||
import { PageLayout } from "@/components/ui/PageLayout";
|
import { PageLayout } from "@/components/ui/PageLayout";
|
||||||
@@ -19,6 +19,8 @@ const myFont = localFont({
|
|||||||
src: "../../../../../public/fonts/NotoNaskhArabic-VariableFont_wght.ttf",
|
src: "../../../../../public/fonts/NotoNaskhArabic-VariableFont_wght.ttf",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type StudyMode = "order-limited" | "order-infinite" | "random-limited" | "random-infinite";
|
||||||
|
|
||||||
interface MemorizeProps {
|
interface MemorizeProps {
|
||||||
deckId: number;
|
deckId: number;
|
||||||
deckName: string;
|
deckName: string;
|
||||||
@@ -29,6 +31,7 @@ const Memorize: React.FC<MemorizeProps> = ({ deckId, deckName }) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
|
const [originalCards, setOriginalCards] = useState<ActionOutputCard[]>([]);
|
||||||
const [cards, setCards] = useState<ActionOutputCard[]>([]);
|
const [cards, setCards] = useState<ActionOutputCard[]>([]);
|
||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
const [showAnswer, setShowAnswer] = useState(false);
|
const [showAnswer, setShowAnswer] = useState(false);
|
||||||
@@ -36,10 +39,20 @@ const Memorize: React.FC<MemorizeProps> = ({ deckId, deckName }) => {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isReversed, setIsReversed] = useState(false);
|
const [isReversed, setIsReversed] = useState(false);
|
||||||
const [isDictation, setIsDictation] = useState(false);
|
const [isDictation, setIsDictation] = useState(false);
|
||||||
|
const [studyMode, setStudyMode] = useState<StudyMode>("order-limited");
|
||||||
const { play, stop, load } = useAudioPlayer();
|
const { play, stop, load } = useAudioPlayer();
|
||||||
const audioUrlRef = useRef<string | null>(null);
|
const audioUrlRef = useRef<string | null>(null);
|
||||||
const [isAudioLoading, setIsAudioLoading] = useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
let ignore = false;
|
let ignore = false;
|
||||||
|
|
||||||
@@ -50,6 +63,7 @@ const Memorize: React.FC<MemorizeProps> = ({ deckId, deckName }) => {
|
|||||||
const result = await actionGetCardsByDeckId({ deckId, limit: 100 });
|
const result = await actionGetCardsByDeckId({ deckId, limit: 100 });
|
||||||
if (!ignore) {
|
if (!ignore) {
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
|
setOriginalCards(result.data);
|
||||||
setCards(result.data);
|
setCards(result.data);
|
||||||
setCurrentIndex(0);
|
setCurrentIndex(0);
|
||||||
setShowAnswer(false);
|
setShowAnswer(false);
|
||||||
@@ -70,6 +84,16 @@ const Memorize: React.FC<MemorizeProps> = ({ deckId, deckName }) => {
|
|||||||
};
|
};
|
||||||
}, [deckId]);
|
}, [deckId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (studyMode.startsWith("random")) {
|
||||||
|
setCards(shuffleCards(originalCards));
|
||||||
|
} else {
|
||||||
|
setCards(originalCards);
|
||||||
|
}
|
||||||
|
setCurrentIndex(0);
|
||||||
|
setShowAnswer(false);
|
||||||
|
}, [studyMode, originalCards, shuffleCards]);
|
||||||
|
|
||||||
const getCurrentCard = (): ActionOutputCard | null => {
|
const getCurrentCard = (): ActionOutputCard | null => {
|
||||||
return cards[currentIndex] ?? null;
|
return cards[currentIndex] ?? null;
|
||||||
};
|
};
|
||||||
@@ -108,25 +132,46 @@ const Memorize: React.FC<MemorizeProps> = ({ deckId, deckName }) => {
|
|||||||
setShowAnswer(true);
|
setShowAnswer(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const isInfinite = studyMode.endsWith("infinite");
|
||||||
|
|
||||||
const handleNextCard = useCallback(() => {
|
const handleNextCard = useCallback(() => {
|
||||||
|
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) {
|
if (currentIndex < cards.length - 1) {
|
||||||
setCurrentIndex(currentIndex + 1);
|
setCurrentIndex(currentIndex + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
setShowAnswer(false);
|
setShowAnswer(false);
|
||||||
setIsReversed(false);
|
setIsReversed(false);
|
||||||
setIsDictation(false);
|
setIsDictation(false);
|
||||||
cleanupAudio();
|
cleanupAudio();
|
||||||
}
|
}, [currentIndex, cards.length, isInfinite, studyMode, originalCards, shuffleCards]);
|
||||||
}, [currentIndex, cards.length]);
|
|
||||||
|
|
||||||
const handlePrevCard = useCallback(() => {
|
const handlePrevCard = useCallback(() => {
|
||||||
|
if (isInfinite) {
|
||||||
|
if (currentIndex <= 0) {
|
||||||
|
setCurrentIndex(cards.length - 1);
|
||||||
|
} else {
|
||||||
|
setCurrentIndex(currentIndex - 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (currentIndex > 0) {
|
if (currentIndex > 0) {
|
||||||
setCurrentIndex(currentIndex - 1);
|
setCurrentIndex(currentIndex - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
setShowAnswer(false);
|
setShowAnswer(false);
|
||||||
setIsReversed(false);
|
setIsReversed(false);
|
||||||
setIsDictation(false);
|
setIsDictation(false);
|
||||||
cleanupAudio();
|
cleanupAudio();
|
||||||
}
|
}, [currentIndex, cards.length, isInfinite]);
|
||||||
}, [currentIndex]);
|
|
||||||
|
|
||||||
const cleanupAudio = useCallback(() => {
|
const cleanupAudio = useCallback(() => {
|
||||||
if (audioUrlRef.current) {
|
if (audioUrlRef.current) {
|
||||||
@@ -249,6 +294,14 @@ const Memorize: React.FC<MemorizeProps> = ({ deckId, deckName }) => {
|
|||||||
|
|
||||||
const currentCard = getCurrentCard()!;
|
const currentCard = getCurrentCard()!;
|
||||||
const displayFront = getFrontText(currentCard);
|
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: <List className="w-4 h-4" /> },
|
||||||
|
{ value: "order-infinite", label: t("orderInfinite"), icon: <Repeat className="w-4 h-4" /> },
|
||||||
|
{ value: "random-limited", label: t("randomLimited"), icon: <Shuffle className="w-4 h-4" /> },
|
||||||
|
{ value: "random-infinite", label: t("randomInfinite"), icon: <Infinity className="w-4 h-4" /> },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
@@ -257,19 +310,38 @@ const Memorize: React.FC<MemorizeProps> = ({ deckId, deckName }) => {
|
|||||||
<Layers className="w-5 h-5" />
|
<Layers className="w-5 h-5" />
|
||||||
<span className="font-medium">{deckName}</span>
|
<span className="font-medium">{deckName}</span>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
{!isInfinite && (
|
||||||
<span className="text-sm text-gray-500">
|
<span className="text-sm text-gray-500">
|
||||||
{t("progress", { current: currentIndex + 1, total: cards.length })}
|
{t("progress", { current: currentIndex + 1, total: cards.length })}
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
|
{!isInfinite && (
|
||||||
<Progress
|
<Progress
|
||||||
value={((currentIndex + 1) / cards.length) * 100}
|
value={((currentIndex + 1) / cards.length) * 100}
|
||||||
showLabel={false}
|
showLabel={false}
|
||||||
animated={false}
|
animated={false}
|
||||||
className="mb-6"
|
className="mb-6"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<HStack justify="center" gap={2} className="mb-4">
|
<VStack gap={2} className="mb-4">
|
||||||
|
<HStack justify="center" gap={1} className="flex-wrap">
|
||||||
|
{studyModeOptions.map((option) => (
|
||||||
|
<LightButton
|
||||||
|
key={option.value}
|
||||||
|
onClick={() => setStudyMode(option.value)}
|
||||||
|
selected={studyMode === option.value}
|
||||||
|
leftIcon={option.icon}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</LightButton>
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<HStack justify="center" gap={2}>
|
||||||
<LightButton
|
<LightButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsReversed(!isReversed);
|
setIsReversed(!isReversed);
|
||||||
@@ -292,19 +364,21 @@ const Memorize: React.FC<MemorizeProps> = ({ deckId, deckName }) => {
|
|||||||
{t("dictation")}
|
{t("dictation")}
|
||||||
</LightButton>
|
</LightButton>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
<div className={`bg-white border border-gray-200 rounded-xl shadow-sm mb-6 ${myFont.className}`}>
|
<div className={`bg-white border border-gray-200 rounded-xl shadow-sm mb-6 ${myFont.className}`}>
|
||||||
{isDictation ? (
|
{isDictation ? (
|
||||||
<>
|
<>
|
||||||
<VStack align="center" justify="center" gap={4} className="p-8 min-h-[20dvh]">
|
<VStack align="center" justify="center" gap={4} className="p-8 min-h-[20dvh]">
|
||||||
<CircleButton
|
{currentCard.ipa ? (
|
||||||
onClick={playCurrentCard}
|
<div className="text-gray-700 text-2xl text-center font-mono">
|
||||||
disabled={isAudioLoading}
|
{currentCard.ipa}
|
||||||
className="p-4 bg-blue-100 hover:bg-blue-200 text-blue-700 transition-colors disabled:opacity-50"
|
</div>
|
||||||
>
|
) : (
|
||||||
<Volume2 className="w-8 h-8" />
|
<div className="text-gray-400 text-lg">
|
||||||
</CircleButton>
|
{t("noIpa")}
|
||||||
<p className="text-gray-500 text-sm">{t("clickToPlay")}</p>
|
</div>
|
||||||
|
)}
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
||||||
{showAnswer && (
|
{showAnswer && (
|
||||||
@@ -314,11 +388,6 @@ const Memorize: React.FC<MemorizeProps> = ({ deckId, deckName }) => {
|
|||||||
<div className="text-gray-900 text-xl md:text-2xl text-center whitespace-pre-line">
|
<div className="text-gray-900 text-xl md:text-2xl text-center whitespace-pre-line">
|
||||||
{displayFront}
|
{displayFront}
|
||||||
</div>
|
</div>
|
||||||
{currentCard.ipa && (
|
|
||||||
<div className="text-gray-500 text-sm mt-2">
|
|
||||||
{currentCard.ipa}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{getBackContent(currentCard)}
|
{getBackContent(currentCard)}
|
||||||
</VStack>
|
</VStack>
|
||||||
</>
|
</>
|
||||||
@@ -354,11 +423,25 @@ const Memorize: React.FC<MemorizeProps> = ({ deckId, deckName }) => {
|
|||||||
{t("showAnswer")}
|
{t("showAnswer")}
|
||||||
<span className="ml-2 text-xs opacity-60">Space</span>
|
<span className="ml-2 text-xs opacity-60">Space</span>
|
||||||
</LightButton>
|
</LightButton>
|
||||||
|
) : isFinished ? (
|
||||||
|
<VStack align="center" gap={4}>
|
||||||
|
<div className="text-green-500">
|
||||||
|
<Check className="w-12 h-12" />
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-600">{t("allDoneDesc")}</p>
|
||||||
|
<HStack gap={2}>
|
||||||
|
<LightButton onClick={() => router.push("/decks")} className="px-4 py-2">
|
||||||
|
{t("backToDecks")}
|
||||||
|
</LightButton>
|
||||||
|
<LightButton onClick={() => setCurrentIndex(0)} className="px-4 py-2">
|
||||||
|
{t("restart")}
|
||||||
|
</LightButton>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
) : (
|
) : (
|
||||||
<HStack gap={4}>
|
<HStack gap={4}>
|
||||||
<LightButton
|
<LightButton
|
||||||
onClick={handlePrevCard}
|
onClick={handlePrevCard}
|
||||||
disabled={currentIndex === 0}
|
|
||||||
className="px-4 py-2"
|
className="px-4 py-2"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="w-5 h-5" />
|
<ChevronLeft className="w-5 h-5" />
|
||||||
@@ -369,7 +452,6 @@ const Memorize: React.FC<MemorizeProps> = ({ deckId, deckName }) => {
|
|||||||
</span>
|
</span>
|
||||||
<LightButton
|
<LightButton
|
||||||
onClick={handleNextCard}
|
onClick={handleNextCard}
|
||||||
disabled={currentIndex === cards.length - 1}
|
|
||||||
className="px-4 py-2"
|
className="px-4 py-2"
|
||||||
>
|
>
|
||||||
<ChevronRight className="w-5 h-5" />
|
<ChevronRight className="w-5 h-5" />
|
||||||
|
|||||||
Reference in New Issue
Block a user