Merge pull request #6 from Danieli4/codex/add-python-programming-mission-module-ygunqb
Fix coding mission panel state handling
This commit is contained in:
commit
989a413162
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
|
||||||
import { apiFetch } from '../lib/api';
|
import { apiFetch } from '../lib/api';
|
||||||
|
|
||||||
interface CodingChallengeState {
|
interface CodingChallengeState {
|
||||||
|
|
@ -46,56 +46,59 @@ interface RunResult {
|
||||||
export function CodingMissionPanel({ missionId, token, initialState, initialCompleted = false }: CodingMissionPanelProps) {
|
export function CodingMissionPanel({ missionId, token, initialState, initialCompleted = false }: CodingMissionPanelProps) {
|
||||||
const [state, setState] = useState<CodingMissionState | null>(initialState);
|
const [state, setState] = useState<CodingMissionState | null>(initialState);
|
||||||
const [missionCompleted, setMissionCompleted] = useState(initialState?.is_mission_completed || initialCompleted);
|
const [missionCompleted, setMissionCompleted] = useState(initialState?.is_mission_completed || initialCompleted);
|
||||||
const [activeChallengeId, setActiveChallengeId] = useState<number | null>(
|
const [editorCode, setEditorCode] = useState<string>('');
|
||||||
initialState?.current_challenge_id ?? initialState?.challenges?.[0]?.id ?? null
|
|
||||||
);
|
|
||||||
const [editorCode, setEditorCode] = useState<string>(() => {
|
|
||||||
const active = initialState?.challenges.find((challenge) => challenge.id === activeChallengeId);
|
|
||||||
if (!active) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return active.last_submitted_code ?? active.starter_code ?? '';
|
|
||||||
});
|
|
||||||
const [status, setStatus] = useState<string | null>(null);
|
const [status, setStatus] = useState<string | null>(null);
|
||||||
const [runResult, setRunResult] = useState<RunResult | null>(null);
|
const [runResult, setRunResult] = useState<RunResult | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const previousChallengeIdRef = useRef<number | null>(null);
|
||||||
|
|
||||||
|
|
||||||
const activeChallenge = useMemo(() => {
|
const activeChallenge = useMemo(() => {
|
||||||
if (!state || !state.challenges.length) {
|
if (!state || !state.challenges.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const targetId = state.current_challenge_id ?? activeChallengeId;
|
if (state.current_challenge_id) {
|
||||||
if (targetId == null) {
|
const current = state.challenges.find((challenge) => challenge.id === state.current_challenge_id);
|
||||||
return null;
|
if (current) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return state.challenges.find((challenge) => challenge.id === targetId) ?? null;
|
|
||||||
}, [state, activeChallengeId]);
|
const nextUnlocked = state.challenges.find((challenge) => challenge.is_unlocked && !challenge.is_passed);
|
||||||
|
if (nextUnlocked) {
|
||||||
|
return nextUnlocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstIncomplete = state.challenges.find((challenge) => !challenge.is_passed);
|
||||||
|
if (firstIncomplete) {
|
||||||
|
return firstIncomplete;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.challenges[state.challenges.length - 1];
|
||||||
|
}, [state]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!state || !state.challenges.length) {
|
if (!state) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const nextActiveId = state.current_challenge_id ?? state.challenges[state.challenges.length - 1].id;
|
|
||||||
if (nextActiveId !== activeChallengeId) {
|
|
||||||
setActiveChallengeId(nextActiveId);
|
|
||||||
const nextChallenge = state.challenges.find((challenge) => challenge.id === nextActiveId);
|
|
||||||
const nextCode = nextChallenge?.last_submitted_code ?? nextChallenge?.starter_code ?? '';
|
|
||||||
setEditorCode(nextCode);
|
|
||||||
setRunResult(null);
|
|
||||||
setStatus(null);
|
|
||||||
}
|
|
||||||
setMissionCompleted(state.is_mission_completed);
|
setMissionCompleted(state.is_mission_completed);
|
||||||
}, [state, activeChallengeId]);
|
}, [state]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeChallenge) {
|
if (!activeChallenge) {
|
||||||
|
previousChallengeIdRef.current = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (editorCode) {
|
|
||||||
|
if (previousChallengeIdRef.current === activeChallenge.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
previousChallengeIdRef.current = activeChallenge.id;
|
||||||
const baseCode = activeChallenge.last_submitted_code ?? activeChallenge.starter_code ?? '';
|
const baseCode = activeChallenge.last_submitted_code ?? activeChallenge.starter_code ?? '';
|
||||||
setEditorCode(baseCode);
|
setEditorCode(baseCode);
|
||||||
|
setRunResult(null);
|
||||||
|
setStatus(null);
|
||||||
}, [activeChallenge]);
|
}, [activeChallenge]);
|
||||||
|
|
||||||
if (!state) {
|
if (!state) {
|
||||||
|
|
@ -113,10 +116,16 @@ export function CodingMissionPanel({ missionId, token, initialState, initialComp
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const updated = await apiFetch<CodingMissionState>(`/api/missions/${missionId}/coding/challenges`, {
|
try {
|
||||||
authToken: token
|
const updated = await apiFetch<CodingMissionState>(`/api/missions/${missionId}/coding/challenges`, {
|
||||||
});
|
authToken: token
|
||||||
setState(updated);
|
});
|
||||||
|
setState(updated);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
setStatus(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRun = async () => {
|
const handleRun = async () => {
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,8 @@ export function OnboardingCarousel({ token, slides, initialCompletedOrder, nextO
|
||||||
border: '1px solid rgba(162, 155, 254, 0.25)'
|
border: '1px solid rgba(162, 155, 254, 0.25)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* В медиаконтенте могут встречаться внешние изображения, которые Next.js не умеет оптимизировать автоматически. */}
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
<img src={currentSlide.media_url} alt={currentSlide.title} style={{ width: '100%', height: 'auto' }} />
|
<img src={currentSlide.media_url} alt={currentSlide.title} style={{ width: '100%', height: 'auto' }} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user