feat: add word import and class type display controls
Build Docker Package / docker-package (push) Successful in 6m34s
Build Docker Package / docker-package (push) Successful in 6m34s
This commit is contained in:
@@ -10,6 +10,7 @@ import { WordImage } from "./WordImage";
|
||||
type GameScreenProps = {
|
||||
selectedGrade: Grade;
|
||||
selectedClassTypeName?: string;
|
||||
showCurrentWord: boolean;
|
||||
roundWords: WordItem[];
|
||||
currentWord: WordItem;
|
||||
options: WordItem[];
|
||||
@@ -30,6 +31,7 @@ type GameScreenProps = {
|
||||
export function GameScreen({
|
||||
selectedGrade,
|
||||
selectedClassTypeName,
|
||||
showCurrentWord,
|
||||
roundWords,
|
||||
currentWord,
|
||||
options,
|
||||
@@ -83,8 +85,13 @@ export function GameScreen({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="pk-game-grid grid gap-[clamp(18px,3vw,34px)] lg:grid-cols-[minmax(300px,0.9fr)_minmax(420px,1.25fr)]">
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<div
|
||||
className={cn(
|
||||
"pk-game-grid grid items-center gap-[clamp(18px,3vw,34px)] lg:grid-cols-[minmax(300px,0.9fr)_minmax(420px,1.25fr)]",
|
||||
!showCurrentWord && "pk-game-grid-word-hidden",
|
||||
)}
|
||||
>
|
||||
<div className="flex min-w-0 flex-col justify-center">
|
||||
<div className="flex items-center gap-[clamp(10px,1.8vw,18px)]">
|
||||
<TeamCard team="red" active={currentTeam === "red"} score={scoreRed} />
|
||||
<div className="text-[clamp(20px,2.8vw,30px)] font-black text-amber-500">VS</div>
|
||||
@@ -104,7 +111,14 @@ export function GameScreen({
|
||||
<div className="h-full rounded-full bg-emerald-500 transition-all" style={{ width: `${progress}%` }} />
|
||||
</div>
|
||||
|
||||
<div className="pk-listen-area flex flex-1 flex-col items-center justify-center py-[clamp(24px,5vw,70px)] text-center">
|
||||
<div
|
||||
className={cn(
|
||||
"pk-listen-area flex flex-col items-center justify-center text-center",
|
||||
showCurrentWord
|
||||
? "py-[clamp(24px,5vw,70px)]"
|
||||
: "min-h-[clamp(150px,24vh,230px)] py-[clamp(16px,3vw,34px)]",
|
||||
)}
|
||||
>
|
||||
<p className="text-[clamp(16px,1.7vw,20px)] text-slate-600">听一听,这是哪个单词?</p>
|
||||
<button
|
||||
onClick={() => onSpeakWord(currentWord)}
|
||||
@@ -113,8 +127,12 @@ export function GameScreen({
|
||||
>
|
||||
<Volume2 className="size-[clamp(28px,3vw,36px)]" />
|
||||
</button>
|
||||
<AdaptiveWordText text={formatWordDisplay(currentWord.english, settings.wordDisplayMode)} variant="hero" />
|
||||
<p className="mt-2 min-h-6 text-[clamp(13px,1.4vw,16px)] text-slate-500">{speechTip}</p>
|
||||
{showCurrentWord && (
|
||||
<>
|
||||
<AdaptiveWordText text={formatWordDisplay(currentWord.english, settings.wordDisplayMode)} variant="hero" />
|
||||
<p className="mt-2 min-h-6 text-[clamp(13px,1.4vw,16px)] text-slate-500">{speechTip}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -17,20 +17,41 @@ export function usePkSetup() {
|
||||
useEffect(() => {
|
||||
async function loadInitialSetup() {
|
||||
setLoadingSetup(true);
|
||||
const [classTypesResponse, nextSettings] = await Promise.all([
|
||||
fetch("/api/class-types?includeDisabled=false", { cache: "no-store" }),
|
||||
loadSettings(),
|
||||
]);
|
||||
const classTypesData = (await classTypesResponse.json()) as { classTypes: ClassType[] };
|
||||
setClassTypes(classTypesData.classTypes);
|
||||
const nextSettings = await loadSettings();
|
||||
setSettings(mergeSettings(nextSettings));
|
||||
setSelectedClassTypeId((current) => current || classTypesData.classTypes[0]?.id || 0);
|
||||
setLoadingSetup(false);
|
||||
}
|
||||
|
||||
void loadInitialSetup();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
|
||||
async function loadClassTypes() {
|
||||
setLoadingSetup(true);
|
||||
setSelectedClassTypeId(0);
|
||||
const params = new URLSearchParams({
|
||||
includeDisabled: "false",
|
||||
grade: selectedGrade,
|
||||
});
|
||||
const response = await fetch(`/api/class-types?${params.toString()}`, { cache: "no-store" });
|
||||
const data = (await response.json()) as { classTypes: ClassType[] };
|
||||
|
||||
if (!ignore) {
|
||||
setClassTypes(data.classTypes);
|
||||
setSelectedClassTypeId(data.classTypes[0]?.id || 0);
|
||||
setLoadingSetup(false);
|
||||
}
|
||||
}
|
||||
|
||||
void loadClassTypes();
|
||||
|
||||
return () => {
|
||||
ignore = true;
|
||||
};
|
||||
}, [selectedGrade]);
|
||||
|
||||
useEffect(() => {
|
||||
function refreshSettingsOnFocus() {
|
||||
void loadSettings().then((nextSettings) => setSettings(mergeSettings(nextSettings)));
|
||||
@@ -41,6 +62,8 @@ export function usePkSetup() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
|
||||
async function loadWords() {
|
||||
if (!selectedClassTypeId) {
|
||||
setAvailableWords([]);
|
||||
@@ -54,10 +77,16 @@ export function usePkSetup() {
|
||||
});
|
||||
const response = await fetch(`/api/words?${params.toString()}`, { cache: "no-store" });
|
||||
const data = (await response.json()) as { words: WordItem[] };
|
||||
setAvailableWords(data.words);
|
||||
if (!ignore) {
|
||||
setAvailableWords(data.words);
|
||||
}
|
||||
}
|
||||
|
||||
void loadWords();
|
||||
|
||||
return () => {
|
||||
ignore = true;
|
||||
};
|
||||
}, [selectedGrade, selectedClassTypeId]);
|
||||
|
||||
return {
|
||||
|
||||
@@ -128,6 +128,7 @@ export default function PkPage() {
|
||||
<GameScreen
|
||||
selectedGrade={selectedGrade}
|
||||
selectedClassTypeName={selectedClassType?.name}
|
||||
showCurrentWord={selectedClassType?.showCurrentWord ?? true}
|
||||
roundWords={roundWords}
|
||||
currentWord={currentWord}
|
||||
options={options}
|
||||
|
||||
@@ -4,8 +4,10 @@ export type Grade = "小班" | "中班" | "大班";
|
||||
|
||||
export type ClassType = {
|
||||
id: number;
|
||||
grade: Grade;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
showCurrentWord: boolean;
|
||||
};
|
||||
|
||||
export type WordItem = {
|
||||
|
||||
Reference in New Issue
Block a user