refactor(ui): use design-system components across pages
- Replace custom spinners with Skeleton - Replace native inputs/select with design-system components - Simplify dictation mode (user self-judges instead of input) - Set body background to primary-50 - Clean up answer button shortcuts
This commit is contained in:
@@ -7,6 +7,9 @@ import { useDictionaryStore } from "./stores/dictionaryStore";
|
|||||||
import { PageLayout } from "@/components/ui/PageLayout";
|
import { PageLayout } from "@/components/ui/PageLayout";
|
||||||
import { LightButton } from "@/design-system/base/button";
|
import { LightButton } from "@/design-system/base/button";
|
||||||
import { Input } from "@/design-system/base/input";
|
import { Input } from "@/design-system/base/input";
|
||||||
|
import { Select } from "@/design-system/base/select";
|
||||||
|
import { Skeleton } from "@/design-system/feedback/skeleton";
|
||||||
|
import { HStack, VStack } from "@/design-system/layout/stack";
|
||||||
import { Plus, RefreshCw } from "lucide-react";
|
import { Plus, RefreshCw } from "lucide-react";
|
||||||
import { DictionaryEntry } from "./DictionaryEntry";
|
import { DictionaryEntry } from "./DictionaryEntry";
|
||||||
import { LanguageSelector } from "./LanguageSelector";
|
import { LanguageSelector } from "./LanguageSelector";
|
||||||
@@ -231,10 +234,10 @@ export function DictionaryClient({ initialDecks }: DictionaryClientProps) {
|
|||||||
|
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
{isSearching ? (
|
{isSearching ? (
|
||||||
<div className="text-center py-12">
|
<VStack align="center" className="py-12">
|
||||||
<div className="w-8 h-8 border-2 border-gray-200 border-t-primary-500 rounded-full animate-spin mx-auto mb-3"></div>
|
<Skeleton variant="circular" className="w-8 h-8 mb-3" />
|
||||||
<p className="text-gray-600">{t("searching")}</p>
|
<p className="text-gray-600">{t("searching")}</p>
|
||||||
</div>
|
</VStack>
|
||||||
) : query && !searchResult ? (
|
) : query && !searchResult ? (
|
||||||
<div className="text-center py-12 bg-white/20 rounded-lg">
|
<div className="text-center py-12 bg-white/20 rounded-lg">
|
||||||
<p className="text-gray-800 text-xl">{t("noResults")}</p>
|
<p className="text-gray-800 text-xl">{t("noResults")}</p>
|
||||||
@@ -248,18 +251,19 @@ export function DictionaryClient({ initialDecks }: DictionaryClientProps) {
|
|||||||
{searchResult.standardForm}
|
{searchResult.standardForm}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 ml-4">
|
<HStack align="center" gap={2} className="ml-4">
|
||||||
{session && decks.length > 0 && (
|
{session && decks.length > 0 && (
|
||||||
<select
|
<Select
|
||||||
id="deck-select"
|
id="deck-select"
|
||||||
className="px-3 py-2 text-sm border border-gray-300 rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-[#35786f]"
|
variant="bordered"
|
||||||
|
size="sm"
|
||||||
>
|
>
|
||||||
{decks.map((deck) => (
|
{decks.map((deck) => (
|
||||||
<option key={deck.id} value={deck.id}>
|
<option key={deck.id} value={deck.id}>
|
||||||
{deck.name}
|
{deck.name}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
<LightButton
|
<LightButton
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
@@ -270,7 +274,7 @@ export function DictionaryClient({ initialDecks }: DictionaryClientProps) {
|
|||||||
>
|
>
|
||||||
<Plus />
|
<Plus />
|
||||||
</LightButton>
|
</LightButton>
|
||||||
</div>
|
</HStack>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import {
|
|||||||
ArrowUpDown,
|
ArrowUpDown,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { CircleButton } from "@/design-system/base/button";
|
import { CircleButton } from "@/design-system/base/button";
|
||||||
|
import { Input } from "@/design-system/base/input";
|
||||||
|
import { Skeleton } from "@/design-system/feedback/skeleton";
|
||||||
|
import { HStack } from "@/design-system/layout/stack";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
@@ -148,18 +151,16 @@ export function ExploreClient({ initialPublicDecks }: ExploreClientProps) {
|
|||||||
<PageLayout>
|
<PageLayout>
|
||||||
<PageHeader title={t("title")} subtitle={t("subtitle")} />
|
<PageHeader title={t("title")} subtitle={t("subtitle")} />
|
||||||
|
|
||||||
<div className="flex items-center gap-2 mb-6">
|
<HStack align="center" gap={2} className="mb-6">
|
||||||
<div className="relative flex-1">
|
<Input
|
||||||
<Search size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
variant="bordered"
|
||||||
<input
|
value={searchQuery}
|
||||||
type="text"
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
value={searchQuery}
|
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
placeholder={t("searchPlaceholder")}
|
||||||
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
|
leftIcon={<Search size={18} />}
|
||||||
placeholder={t("searchPlaceholder")}
|
containerClassName="flex-1"
|
||||||
className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<CircleButton
|
<CircleButton
|
||||||
onClick={handleToggleSort}
|
onClick={handleToggleSort}
|
||||||
title={sortByFavorites ? t("sortByFavoritesActive") : t("sortByFavorites")}
|
title={sortByFavorites ? t("sortByFavoritesActive") : t("sortByFavorites")}
|
||||||
@@ -170,11 +171,11 @@ export function ExploreClient({ initialPublicDecks }: ExploreClientProps) {
|
|||||||
<CircleButton onClick={handleSearch}>
|
<CircleButton onClick={handleSearch}>
|
||||||
<Search size={18} />
|
<Search size={18} />
|
||||||
</CircleButton>
|
</CircleButton>
|
||||||
</div>
|
</HStack>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="p-8 text-center">
|
<div className="p-8 text-center">
|
||||||
<div className="w-8 h-8 border-2 border-gray-200 border-t-gray-400 rounded-full animate-spin mx-auto mb-3"></div>
|
<Skeleton variant="circular" className="w-8 h-8 mx-auto mb-3" />
|
||||||
<p className="text-sm text-gray-500">{t("loading")}</p>
|
<p className="text-sm text-gray-500">{t("loading")}</p>
|
||||||
</div>
|
</div>
|
||||||
) : sortedDecks.length === 0 ? (
|
) : sortedDecks.length === 0 ? (
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { CircleButton, LightButton } from "@/design-system/base/button";
|
import { CircleButton, LightButton } from "@/design-system/base/button";
|
||||||
|
import { Skeleton } from "@/design-system/feedback/skeleton";
|
||||||
|
import { VStack } from "@/design-system/layout/stack";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
@@ -202,10 +204,10 @@ export function DecksClient({ userId }: DecksClientProps) {
|
|||||||
|
|
||||||
<CardList>
|
<CardList>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="p-8 text-center">
|
<VStack align="center" className="p-8">
|
||||||
<div className="w-8 h-8 border-2 border-gray-200 border-t-gray-400 rounded-full animate-spin mx-auto mb-3"></div>
|
<Skeleton variant="circular" className="w-8 h-8 mb-3" />
|
||||||
<p className="text-sm text-gray-500">{t("loading")}</p>
|
<p className="text-sm text-gray-500">{t("loading")}</p>
|
||||||
</div>
|
</VStack>
|
||||||
) : decks.length === 0 ? (
|
) : decks.length === 0 ? (
|
||||||
<div className="text-center py-12 text-gray-400">
|
<div className="text-center py-12 text-gray-400">
|
||||||
<div className="w-16 h-16 mx-auto mb-3 rounded-full bg-gray-100 flex items-center justify-center">
|
<div className="w-16 h-16 mx-auto mb-3 rounded-full bg-gray-100 flex items-center justify-center">
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ import { Layers, Check, Clock, Sparkles, RotateCcw, Volume2, Headphones } from "
|
|||||||
import type { ActionOutputCardWithNote, ActionOutputScheduledCard } from "@/modules/card/card-action-dto";
|
import type { ActionOutputCardWithNote, ActionOutputScheduledCard } from "@/modules/card/card-action-dto";
|
||||||
import { actionGetCardsForReview, actionAnswerCard } from "@/modules/card/card-action";
|
import { actionGetCardsForReview, actionAnswerCard } from "@/modules/card/card-action";
|
||||||
import { PageLayout } from "@/components/ui/PageLayout";
|
import { PageLayout } from "@/components/ui/PageLayout";
|
||||||
import { LightButton } from "@/design-system/base/button";
|
import { LightButton, CircleButton } from "@/design-system/base/button";
|
||||||
|
import { Badge } from "@/design-system/data-display/badge";
|
||||||
|
import { Progress } from "@/design-system/feedback/progress";
|
||||||
|
import { Skeleton } from "@/design-system/feedback/skeleton";
|
||||||
|
import { HStack, VStack } from "@/design-system/layout/stack";
|
||||||
import { CardType } from "../../../../../generated/prisma/enums";
|
import { CardType } from "../../../../../generated/prisma/enums";
|
||||||
import { calculatePreviewIntervals, formatPreviewInterval, type CardPreview } from "./interval-preview";
|
import { calculatePreviewIntervals, formatPreviewInterval, type CardPreview } from "./interval-preview";
|
||||||
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
||||||
@@ -38,8 +42,6 @@ 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 [dictationInput, setDictationInput] = useState("");
|
|
||||||
const [dictationResult, setDictationResult] = useState<"correct" | "incorrect" | null>(null);
|
|
||||||
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);
|
||||||
@@ -60,8 +62,6 @@ const Memorize: React.FC<MemorizeProps> = ({ deckId, deckName }) => {
|
|||||||
setLastScheduled(null);
|
setLastScheduled(null);
|
||||||
setIsReversed(false);
|
setIsReversed(false);
|
||||||
setIsDictation(false);
|
setIsDictation(false);
|
||||||
setDictationInput("");
|
|
||||||
setDictationResult(null);
|
|
||||||
} else {
|
} else {
|
||||||
setError(result.message);
|
setError(result.message);
|
||||||
}
|
}
|
||||||
@@ -87,18 +87,7 @@ const Memorize: React.FC<MemorizeProps> = ({ deckId, deckName }) => {
|
|||||||
|
|
||||||
const handleShowAnswer = useCallback(() => {
|
const handleShowAnswer = useCallback(() => {
|
||||||
setShowAnswer(true);
|
setShowAnswer(true);
|
||||||
|
}, []);
|
||||||
if (isDictation) {
|
|
||||||
const currentCard = getCurrentCard();
|
|
||||||
if (currentCard) {
|
|
||||||
const fields = getNoteFields(currentCard);
|
|
||||||
const answer = isReversed ? (fields[0] ?? "") : (fields[1] ?? "");
|
|
||||||
const normalizedInput = dictationInput.trim().toLowerCase();
|
|
||||||
const normalizedAnswer = answer.trim().toLowerCase();
|
|
||||||
setDictationResult(normalizedInput === normalizedAnswer ? "correct" : "incorrect");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [isDictation, dictationInput, isReversed]);
|
|
||||||
|
|
||||||
const handleAnswer = useCallback((ease: ReviewEase) => {
|
const handleAnswer = useCallback((ease: ReviewEase) => {
|
||||||
const card = getCurrentCard();
|
const card = getCurrentCard();
|
||||||
@@ -125,8 +114,6 @@ const Memorize: React.FC<MemorizeProps> = ({ deckId, deckName }) => {
|
|||||||
setShowAnswer(false);
|
setShowAnswer(false);
|
||||||
setIsReversed(false);
|
setIsReversed(false);
|
||||||
setIsDictation(false);
|
setIsDictation(false);
|
||||||
setDictationInput("");
|
|
||||||
setDictationResult(null);
|
|
||||||
|
|
||||||
if (audioUrlRef.current) {
|
if (audioUrlRef.current) {
|
||||||
URL.revokeObjectURL(audioUrlRef.current);
|
URL.revokeObjectURL(audioUrlRef.current);
|
||||||
@@ -251,28 +238,28 @@ const Memorize: React.FC<MemorizeProps> = ({ deckId, deckName }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCardTypeColor = (type: CardType): string => {
|
const getCardTypeVariant = (type: CardType): "info" | "warning" | "success" | "primary" => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case CardType.NEW:
|
case CardType.NEW:
|
||||||
return "bg-blue-100 text-blue-700";
|
return "info";
|
||||||
case CardType.LEARNING:
|
case CardType.LEARNING:
|
||||||
return "bg-yellow-100 text-yellow-700";
|
return "warning";
|
||||||
case CardType.REVIEW:
|
case CardType.REVIEW:
|
||||||
return "bg-green-100 text-green-700";
|
return "success";
|
||||||
case CardType.RELEARNING:
|
case CardType.RELEARNING:
|
||||||
return "bg-purple-100 text-purple-700";
|
return "primary";
|
||||||
default:
|
default:
|
||||||
return "bg-gray-100 text-gray-700";
|
return "info";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div className="text-center py-12">
|
<VStack align="center" className="py-12">
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900 mx-auto mb-4"></div>
|
<Skeleton variant="circular" className="h-12 w-12 mb-4" />
|
||||||
<p className="text-gray-600">{t("loading")}</p>
|
<p className="text-gray-600">{t("loading")}</p>
|
||||||
</div>
|
</VStack>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -280,12 +267,14 @@ const Memorize: React.FC<MemorizeProps> = ({ deckId, deckName }) => {
|
|||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div className="text-center py-12">
|
<VStack align="center" className="py-12">
|
||||||
<p className="text-red-600 mb-4">{error}</p>
|
<div className="text-red-600 mb-4 px-4 py-3 bg-red-50 border border-red-200 rounded-lg max-w-md">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
<LightButton onClick={() => router.push("/decks")} className="px-4 py-2">
|
<LightButton onClick={() => router.push("/decks")} className="px-4 py-2">
|
||||||
{t("backToDecks")}
|
{t("backToDecks")}
|
||||||
</LightButton>
|
</LightButton>
|
||||||
</div>
|
</VStack>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -293,7 +282,7 @@ const Memorize: React.FC<MemorizeProps> = ({ deckId, deckName }) => {
|
|||||||
if (cards.length === 0) {
|
if (cards.length === 0) {
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div className="text-center py-12">
|
<VStack align="center" className="py-12">
|
||||||
<div className="text-green-500 mb-4">
|
<div className="text-green-500 mb-4">
|
||||||
<Check className="w-16 h-16 mx-auto" />
|
<Check className="w-16 h-16 mx-auto" />
|
||||||
</div>
|
</div>
|
||||||
@@ -302,7 +291,7 @@ const Memorize: React.FC<MemorizeProps> = ({ deckId, deckName }) => {
|
|||||||
<LightButton onClick={() => router.push("/decks")} className="px-4 py-2">
|
<LightButton onClick={() => router.push("/decks")} className="px-4 py-2">
|
||||||
{t("backToDecks")}
|
{t("backToDecks")}
|
||||||
</LightButton>
|
</LightButton>
|
||||||
</div>
|
</VStack>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -325,162 +314,118 @@ const Memorize: React.FC<MemorizeProps> = ({ deckId, deckName }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div className="flex items-center justify-between mb-4">
|
<HStack justify="between" className="mb-4">
|
||||||
<div className="flex items-center gap-2 text-gray-600">
|
<HStack gap={2} className="text-gray-600">
|
||||||
<Layers className="w-5 h-5" />
|
<Layers className="w-5 h-5" />
|
||||||
<span className="font-medium">{deckName}</span>
|
<span className="font-medium">{deckName}</span>
|
||||||
</div>
|
</HStack>
|
||||||
<div className="flex items-center gap-3">
|
<HStack gap={3}>
|
||||||
<span className={`text-xs px-2 py-0.5 rounded-full ${getCardTypeColor(currentCard.type)}`}>
|
<Badge variant={getCardTypeVariant(currentCard.type)} size="sm">
|
||||||
{getCardTypeLabel(currentCard.type)}
|
{getCardTypeLabel(currentCard.type)}
|
||||||
</span>
|
</Badge>
|
||||||
<span className="text-sm text-gray-500">
|
<span className="text-sm text-gray-500">
|
||||||
{t("progress", { current: currentIndex + 1, total: cards.length + currentIndex })}
|
{t("progress", { current: currentIndex + 1, total: cards.length + currentIndex })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</HStack>
|
||||||
</div>
|
</HStack>
|
||||||
|
|
||||||
<div className="w-full bg-gray-200 rounded-full h-2 mb-6">
|
<Progress
|
||||||
<div
|
value={Math.max(0, ((currentIndex) / (cards.length + currentIndex)) * 100)}
|
||||||
className="bg-blue-500 h-2 rounded-full transition-all duration-300"
|
showLabel={false}
|
||||||
style={{ width: `${Math.max(0, ((currentIndex) / (cards.length + currentIndex)) * 100)}%` }}
|
animated={false}
|
||||||
/>
|
className="mb-6"
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
{lastScheduled && (
|
{lastScheduled && (
|
||||||
<div className="mb-4 p-3 bg-gray-50 rounded-lg text-sm text-gray-600">
|
<div className="mb-4 p-3 bg-gray-50 rounded-lg text-sm text-gray-600">
|
||||||
<div className="flex items-center gap-2">
|
<HStack gap={2}>
|
||||||
<Clock className="w-4 h-4" />
|
<Clock className="w-4 h-4" />
|
||||||
<span>
|
<span>
|
||||||
{t("nextReview")}: {formatNextReview(lastScheduled)}
|
{t("nextReview")}: {formatNextReview(lastScheduled)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</HStack>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex justify-center gap-2 mb-4">
|
<HStack justify="center" gap={2} className="mb-4">
|
||||||
<button
|
<LightButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsReversed(!isReversed);
|
setIsReversed(!isReversed);
|
||||||
setDictationInput("");
|
|
||||||
setDictationResult(null);
|
|
||||||
}}
|
}}
|
||||||
className={`flex items-center gap-1 px-3 py-1.5 rounded-lg text-sm transition-colors ${
|
selected={isReversed}
|
||||||
isReversed
|
leftIcon={<RotateCcw className="w-4 h-4" />}
|
||||||
? "bg-indigo-100 text-indigo-700"
|
size="sm"
|
||||||
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<RotateCcw className="w-4 h-4" />
|
|
||||||
{t("reverse")}
|
{t("reverse")}
|
||||||
</button>
|
</LightButton>
|
||||||
<button
|
<LightButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsDictation(!isDictation);
|
setIsDictation(!isDictation);
|
||||||
setDictationInput("");
|
|
||||||
setDictationResult(null);
|
|
||||||
}}
|
}}
|
||||||
className={`flex items-center gap-1 px-3 py-1.5 rounded-lg text-sm transition-colors ${
|
selected={isDictation}
|
||||||
isDictation
|
leftIcon={<Headphones className="w-4 h-4" />}
|
||||||
? "bg-purple-100 text-purple-700"
|
size="sm"
|
||||||
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<Headphones className="w-4 h-4" />
|
|
||||||
{t("dictation")}
|
{t("dictation")}
|
||||||
</button>
|
</LightButton>
|
||||||
</div>
|
</HStack>
|
||||||
|
|
||||||
<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 ? (
|
||||||
<>
|
<>
|
||||||
<div className="p-8 min-h-[20dvh] flex flex-col items-center justify-center gap-4">
|
<VStack align="center" justify="center" gap={4} className="p-8 min-h-[20dvh]">
|
||||||
<button
|
<CircleButton
|
||||||
onClick={playCurrentCard}
|
onClick={playCurrentCard}
|
||||||
disabled={isAudioLoading}
|
disabled={isAudioLoading}
|
||||||
className="p-4 rounded-full bg-blue-100 hover:bg-blue-200 text-blue-700 transition-colors disabled:opacity-50"
|
className="p-4 bg-blue-100 hover:bg-blue-200 text-blue-700 transition-colors disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<Volume2 className="w-8 h-8" />
|
<Volume2 className="w-8 h-8" />
|
||||||
</button>
|
</CircleButton>
|
||||||
<p className="text-gray-500 text-sm">{t("clickToPlay")}</p>
|
<p className="text-gray-500 text-sm">{t("clickToPlay")}</p>
|
||||||
</div>
|
</VStack>
|
||||||
|
|
||||||
{showAnswer ? (
|
|
||||||
<>
|
|
||||||
<div className="border-t border-gray-200" />
|
|
||||||
<div className="p-8 min-h-[20dvh] flex flex-col items-center justify-center bg-gray-50 rounded-b-xl gap-4">
|
|
||||||
<div className="w-full max-w-md">
|
|
||||||
<label className="block text-sm text-gray-600 mb-2">{t("yourAnswer")}</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={dictationInput}
|
|
||||||
onChange={(e) => setDictationInput(e.target.value)}
|
|
||||||
className={`w-full p-3 border rounded-lg text-lg ${
|
|
||||||
dictationResult === "correct"
|
|
||||||
? "border-green-500 bg-green-50"
|
|
||||||
: dictationResult === "incorrect"
|
|
||||||
? "border-red-500 bg-red-50"
|
|
||||||
: "border-gray-300"
|
|
||||||
}`}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<p className={`text-lg font-medium ${
|
|
||||||
dictationResult === "correct" ? "text-green-600" : "text-red-600"
|
|
||||||
}`}>
|
|
||||||
{dictationResult === "correct" ? "✓ " + t("correct") : "✗ " + t("incorrect")}
|
|
||||||
</p>
|
|
||||||
{dictationResult === "incorrect" && (
|
|
||||||
<p className="text-gray-900 text-xl mt-2">{displayBack}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="border-t border-gray-200 p-6 bg-gray-50 rounded-b-xl">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={dictationInput}
|
|
||||||
onChange={(e) => setDictationInput(e.target.value)}
|
|
||||||
placeholder={t("typeWhatYouHear")}
|
|
||||||
className="w-full p-3 border border-gray-300 rounded-lg text-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="p-8 min-h-[20dvh] flex items-center justify-center">
|
|
||||||
<div className="text-gray-900 text-xl md:text-2xl text-center">
|
|
||||||
{displayFront}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showAnswer && (
|
{showAnswer && (
|
||||||
<>
|
<>
|
||||||
<div className="border-t border-gray-200" />
|
<div className="border-t border-gray-200" />
|
||||||
<div className="p-8 min-h-[20dvh] flex items-center justify-center bg-gray-50 rounded-b-xl">
|
<VStack align="center" justify="center" className="p-8 min-h-[20dvh] bg-gray-50 rounded-b-xl">
|
||||||
<div className="text-gray-900 text-xl md:text-2xl text-center">
|
<div className="text-gray-900 text-xl md:text-2xl text-center">
|
||||||
{displayBack}
|
{displayBack}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</VStack>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<HStack align="center" justify="center" className="p-8 min-h-[20dvh]">
|
||||||
|
<div className="text-gray-900 text-xl md:text-2xl text-center">
|
||||||
|
{displayFront}
|
||||||
|
</div>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{showAnswer && (
|
||||||
|
<>
|
||||||
|
<div className="border-t border-gray-200" />
|
||||||
|
<HStack align="center" justify="center" className="p-8 min-h-[20dvh] bg-gray-50 rounded-b-xl">
|
||||||
|
<div className="text-gray-900 text-xl md:text-2xl text-center">
|
||||||
|
{displayBack}
|
||||||
|
</div>
|
||||||
|
</HStack>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-center gap-4 mb-6 text-sm text-gray-500">
|
<HStack justify="center" gap={4} className="mb-6 text-sm text-gray-500">
|
||||||
<span>{t("interval")}: {formatInterval(currentCard.ivl)}</span>
|
<span>{t("interval")}: {formatInterval(currentCard.ivl)}</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span>{t("ease")}: {currentCard.factor / 10}%</span>
|
<span>{t("ease")}: {currentCard.factor / 10}%</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span>{t("lapses")}: {currentCard.lapses}</span>
|
<span>{t("lapses")}: {currentCard.lapses}</span>
|
||||||
</div>
|
</HStack>
|
||||||
|
|
||||||
<div className="flex justify-center">
|
<HStack justify="center">
|
||||||
{!showAnswer ? (
|
{!showAnswer ? (
|
||||||
<LightButton
|
<LightButton
|
||||||
onClick={handleShowAnswer}
|
onClick={handleShowAnswer}
|
||||||
@@ -491,7 +436,7 @@ const Memorize: React.FC<MemorizeProps> = ({ deckId, deckName }) => {
|
|||||||
<span className="ml-2 text-xs opacity-60">Space</span>
|
<span className="ml-2 text-xs opacity-60">Space</span>
|
||||||
</LightButton>
|
</LightButton>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-wrap justify-center gap-3">
|
<HStack wrap justify="center" gap={3}>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleAnswer(1)}
|
onClick={() => handleAnswer(1)}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
@@ -499,7 +444,6 @@ const Memorize: React.FC<MemorizeProps> = ({ deckId, deckName }) => {
|
|||||||
>
|
>
|
||||||
<span className="font-medium">{t("again")}</span>
|
<span className="font-medium">{t("again")}</span>
|
||||||
<span className="text-xs opacity-75">{formatPreviewInterval(previewIntervals.again)}</span>
|
<span className="text-xs opacity-75">{formatPreviewInterval(previewIntervals.again)}</span>
|
||||||
<span className="text-xs opacity-50 mt-1">1</span>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -509,7 +453,6 @@ const Memorize: React.FC<MemorizeProps> = ({ deckId, deckName }) => {
|
|||||||
>
|
>
|
||||||
<span className="font-medium">{t("hard")}</span>
|
<span className="font-medium">{t("hard")}</span>
|
||||||
<span className="text-xs opacity-75">{formatPreviewInterval(previewIntervals.hard)}</span>
|
<span className="text-xs opacity-75">{formatPreviewInterval(previewIntervals.hard)}</span>
|
||||||
<span className="text-xs opacity-50 mt-1">2</span>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -522,7 +465,6 @@ const Memorize: React.FC<MemorizeProps> = ({ deckId, deckName }) => {
|
|||||||
<Sparkles className="w-3 h-3 opacity-60" />
|
<Sparkles className="w-3 h-3 opacity-60" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs opacity-75">{formatPreviewInterval(previewIntervals.good)}</span>
|
<span className="text-xs opacity-75">{formatPreviewInterval(previewIntervals.good)}</span>
|
||||||
<span className="text-xs opacity-50 mt-1">3/Space</span>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -532,11 +474,10 @@ const Memorize: React.FC<MemorizeProps> = ({ deckId, deckName }) => {
|
|||||||
>
|
>
|
||||||
<span className="font-medium">{t("easy")}</span>
|
<span className="font-medium">{t("easy")}</span>
|
||||||
<span className="text-xs opacity-75">{formatPreviewInterval(previewIntervals.easy)}</span>
|
<span className="text-xs opacity-75">{formatPreviewInterval(previewIntervals.easy)}</span>
|
||||||
<span className="text-xs opacity-50 mt-1">4</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</HStack>
|
||||||
)}
|
)}
|
||||||
</div>
|
</HStack>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ body {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: var(--background);
|
background: var(--primary-50);
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
font-family: var(--font-geist-sans), -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
|
font-family: var(--font-geist-sans), -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user