From a68951f1d3c5c60a4d442dd3d9c03e3e3f2c683d Mon Sep 17 00:00:00 2001 From: goddonebianu Date: Mon, 16 Mar 2026 07:58:43 +0800 Subject: [PATCH] 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 --- .../dictionary/DictionaryClient.tsx | 20 +- src/app/(features)/explore/ExploreClient.tsx | 29 +-- src/app/decks/DecksClient.tsx | 8 +- src/app/decks/[deck_id]/learn/Memorize.tsx | 223 +++++++----------- src/app/globals.css | 2 +- 5 files changed, 115 insertions(+), 167 deletions(-) diff --git a/src/app/(features)/dictionary/DictionaryClient.tsx b/src/app/(features)/dictionary/DictionaryClient.tsx index dd33364..f8ad026 100644 --- a/src/app/(features)/dictionary/DictionaryClient.tsx +++ b/src/app/(features)/dictionary/DictionaryClient.tsx @@ -7,6 +7,9 @@ import { useDictionaryStore } from "./stores/dictionaryStore"; import { PageLayout } from "@/components/ui/PageLayout"; import { LightButton } from "@/design-system/base/button"; 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 { DictionaryEntry } from "./DictionaryEntry"; import { LanguageSelector } from "./LanguageSelector"; @@ -231,10 +234,10 @@ export function DictionaryClient({ initialDecks }: DictionaryClientProps) {
{isSearching ? ( -
-
+ +

{t("searching")}

-
+ ) : query && !searchResult ? (

{t("noResults")}

@@ -248,18 +251,19 @@ export function DictionaryClient({ initialDecks }: DictionaryClientProps) { {searchResult.standardForm}
-
+ {session && decks.length > 0 && ( - + )} -
+
diff --git a/src/app/(features)/explore/ExploreClient.tsx b/src/app/(features)/explore/ExploreClient.tsx index 63734e5..94181f2 100644 --- a/src/app/(features)/explore/ExploreClient.tsx +++ b/src/app/(features)/explore/ExploreClient.tsx @@ -7,6 +7,9 @@ import { ArrowUpDown, } from "lucide-react"; 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 { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; @@ -148,18 +151,16 @@ export function ExploreClient({ initialPublicDecks }: ExploreClientProps) { -
-
- - setSearchQuery(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && handleSearch()} - placeholder={t("searchPlaceholder")} - 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" - /> -
+ + setSearchQuery(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleSearch()} + placeholder={t("searchPlaceholder")} + leftIcon={} + containerClassName="flex-1" + /> -
+ {loading ? (
-
+

{t("loading")}

) : sortedDecks.length === 0 ? ( diff --git a/src/app/decks/DecksClient.tsx b/src/app/decks/DecksClient.tsx index 5d2d4a6..d32d055 100644 --- a/src/app/decks/DecksClient.tsx +++ b/src/app/decks/DecksClient.tsx @@ -10,6 +10,8 @@ import { Trash2, } from "lucide-react"; 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 { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; @@ -202,10 +204,10 @@ export function DecksClient({ userId }: DecksClientProps) { {loading ? ( -
-
+ +

{t("loading")}

-
+ ) : decks.length === 0 ? (
diff --git a/src/app/decks/[deck_id]/learn/Memorize.tsx b/src/app/decks/[deck_id]/learn/Memorize.tsx index 9f5f65f..bca966b 100644 --- a/src/app/decks/[deck_id]/learn/Memorize.tsx +++ b/src/app/decks/[deck_id]/learn/Memorize.tsx @@ -8,7 +8,11 @@ import { Layers, Check, Clock, Sparkles, RotateCcw, Volume2, Headphones } from " import type { ActionOutputCardWithNote, ActionOutputScheduledCard } from "@/modules/card/card-action-dto"; import { actionGetCardsForReview, actionAnswerCard } from "@/modules/card/card-action"; 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 { calculatePreviewIntervals, formatPreviewInterval, type CardPreview } from "./interval-preview"; import { useAudioPlayer } from "@/hooks/useAudioPlayer"; @@ -38,8 +42,6 @@ const Memorize: React.FC = ({ deckId, deckName }) => { const [error, setError] = useState(null); const [isReversed, setIsReversed] = 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 audioUrlRef = useRef(null); const [isAudioLoading, setIsAudioLoading] = useState(false); @@ -60,8 +62,6 @@ const Memorize: React.FC = ({ deckId, deckName }) => { setLastScheduled(null); setIsReversed(false); setIsDictation(false); - setDictationInput(""); - setDictationResult(null); } else { setError(result.message); } @@ -87,18 +87,7 @@ const Memorize: React.FC = ({ deckId, deckName }) => { const handleShowAnswer = useCallback(() => { 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 card = getCurrentCard(); @@ -125,8 +114,6 @@ const Memorize: React.FC = ({ deckId, deckName }) => { setShowAnswer(false); setIsReversed(false); setIsDictation(false); - setDictationInput(""); - setDictationResult(null); if (audioUrlRef.current) { URL.revokeObjectURL(audioUrlRef.current); @@ -251,28 +238,28 @@ const Memorize: React.FC = ({ deckId, deckName }) => { } }; - const getCardTypeColor = (type: CardType): string => { + const getCardTypeVariant = (type: CardType): "info" | "warning" | "success" | "primary" => { switch (type) { case CardType.NEW: - return "bg-blue-100 text-blue-700"; + return "info"; case CardType.LEARNING: - return "bg-yellow-100 text-yellow-700"; + return "warning"; case CardType.REVIEW: - return "bg-green-100 text-green-700"; + return "success"; case CardType.RELEARNING: - return "bg-purple-100 text-purple-700"; + return "primary"; default: - return "bg-gray-100 text-gray-700"; + return "info"; } }; if (isLoading) { return ( -
-
+ +

{t("loading")}

-
+
); } @@ -280,12 +267,14 @@ const Memorize: React.FC = ({ deckId, deckName }) => { if (error) { return ( -
-

{error}

+ +
+ {error} +
router.push("/decks")} className="px-4 py-2"> {t("backToDecks")} -
+
); } @@ -293,7 +282,7 @@ const Memorize: React.FC = ({ deckId, deckName }) => { if (cards.length === 0) { return ( -
+
@@ -302,7 +291,7 @@ const Memorize: React.FC = ({ deckId, deckName }) => { router.push("/decks")} className="px-4 py-2"> {t("backToDecks")} -
+
); } @@ -325,162 +314,118 @@ const Memorize: React.FC = ({ deckId, deckName }) => { return ( -
-
+ + {deckName} -
-
- + + + {getCardTypeLabel(currentCard.type)} - + {t("progress", { current: currentIndex + 1, total: cards.length + currentIndex })} -
-
+ + -
-
-
+ {lastScheduled && (
-
+ {t("nextReview")}: {formatNextReview(lastScheduled)} -
+
)} -
- - -
+ +
{isDictation ? ( <> -
- +

{t("clickToPlay")}

-
- - {showAnswer ? ( - <> -
-
-
- - 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 - /> -
-
-

- {dictationResult === "correct" ? "✓ " + t("correct") : "✗ " + t("incorrect")} -

- {dictationResult === "incorrect" && ( -

{displayBack}

- )} -
-
- - ) : ( -
- 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 - /> -
- )} - - ) : ( - <> -
-
- {displayFront} -
-
+ {showAnswer && ( <>
-
+
{displayBack}
-
+ + + )} + + ) : ( + <> + +
+ {displayFront} +
+
+ + {showAnswer && ( + <> +
+ +
+ {displayBack} +
+
)} )}
-
+ {t("interval")}: {formatInterval(currentCard.ivl)} {t("ease")}: {currentCard.factor / 10}% {t("lapses")}: {currentCard.lapses} -
+ -
+ {!showAnswer ? ( = ({ deckId, deckName }) => { Space ) : ( -
+
{formatPreviewInterval(previewIntervals.good)} - 3/Space -
+ )} -
+ ); }; diff --git a/src/app/globals.css b/src/app/globals.css index e60a85e..c5c009a 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -177,7 +177,7 @@ body { height: 100%; margin: 0; padding: 0; - background: var(--background); + background: var(--primary-50); color: var(--foreground); font-family: var(--font-geist-sans), -apple-system, BlinkMacSystemFont, system-ui, sans-serif; font-size: 1rem;