This commit is contained in:
583
G102-sequence/sdk/package/dist/kit/react/GamePlayer.js
vendored
Normal file
583
G102-sequence/sdk/package/dist/kit/react/GamePlayer.js
vendored
Normal file
@@ -0,0 +1,583 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.GamePlayer = exports.SessionErrorCodes = void 0;
|
||||
const jsx_runtime_1 = require("react/jsx-runtime");
|
||||
const react_1 = require("react");
|
||||
const useGameIframeSDK_1 = require("../../useGameIframeSDK");
|
||||
const api_1 = require("../api");
|
||||
const mappers_1 = require("../mappers");
|
||||
// Session Error Codes (synced with backend)
|
||||
exports.SessionErrorCodes = {
|
||||
SESSION_NOT_STARTED: -60,
|
||||
SESSION_ENDED: -61,
|
||||
SESSION_COMPLETED: -62,
|
||||
SESSION_TIMEOUT: -63,
|
||||
SESSION_NOT_FOUND: -64,
|
||||
SESSION_ALREADY_ANSWERED: -65,
|
||||
};
|
||||
const GamePlayer = ({ apiConfig, assignmentId, studentId, className, style, debug = false, onComplete, onError, onGameLoaded, extraGameData, onLog, onSessionStatus, renderLoading, renderError, loadingTimeout = 30000 }) => {
|
||||
const iframeRef = (0, react_1.useRef)(null);
|
||||
const [gameUrl, setGameUrl] = (0, react_1.useState)('');
|
||||
const [gameState, setGameState] = (0, react_1.useState)(null);
|
||||
const [loading, setLoading] = (0, react_1.useState)(true);
|
||||
const [error, setError] = (0, react_1.useState)(null);
|
||||
const [hasLoadedGame, setHasLoadedGame] = (0, react_1.useState)(false);
|
||||
const [apiClient] = (0, react_1.useState)(() => new api_1.GameApiClient(apiConfig));
|
||||
// Refs to prevent infinite loops
|
||||
const sendLeaderboardRef = (0, react_1.useRef)(null);
|
||||
const hasInitRef = (0, react_1.useRef)(false); // Track if init has been called
|
||||
const callbacksRef = (0, react_1.useRef)({ onGameLoaded, onLog, onSessionStatus, onError }); // Stable callback refs
|
||||
// Update callback refs on each render
|
||||
callbacksRef.current = { onGameLoaded, onLog, onSessionStatus, onError };
|
||||
// Helper: Create structured error
|
||||
const createError = (0, react_1.useCallback)((err) => {
|
||||
const errorMsg = err?.message || err?.error || String(err);
|
||||
const errorCode = err?.code; // Error code from API response
|
||||
// Check error code first (more reliable than message parsing)
|
||||
if (errorCode !== undefined) {
|
||||
// Session not started
|
||||
if (errorCode === exports.SessionErrorCodes.SESSION_NOT_STARTED) {
|
||||
return {
|
||||
type: 'not_started',
|
||||
code: errorCode,
|
||||
message: errorMsg || 'Bài tập chưa bắt đầu',
|
||||
details: err,
|
||||
canRetry: false
|
||||
};
|
||||
}
|
||||
// Session ended
|
||||
if (errorCode === exports.SessionErrorCodes.SESSION_ENDED) {
|
||||
return {
|
||||
type: 'session',
|
||||
code: errorCode,
|
||||
message: errorMsg || 'Bài tập đã kết thúc',
|
||||
details: err,
|
||||
canRetry: false
|
||||
};
|
||||
}
|
||||
// Session completed
|
||||
if (errorCode === exports.SessionErrorCodes.SESSION_COMPLETED) {
|
||||
return {
|
||||
type: 'session',
|
||||
code: errorCode,
|
||||
message: errorMsg || 'Bạn đã hoàn thành bài tập này rồi',
|
||||
details: err,
|
||||
canRetry: false
|
||||
};
|
||||
}
|
||||
// Session timeout
|
||||
if (errorCode === exports.SessionErrorCodes.SESSION_TIMEOUT) {
|
||||
return {
|
||||
type: 'timeout',
|
||||
code: errorCode,
|
||||
message: errorMsg || 'Đã hết thời gian làm bài',
|
||||
details: err,
|
||||
canRetry: false
|
||||
};
|
||||
}
|
||||
}
|
||||
// Fallback: Parse error message
|
||||
// API errors
|
||||
if (errorMsg.includes('API Error') || errorMsg.includes('Failed to fetch')) {
|
||||
// Check if it contains session-related message
|
||||
if (errorMsg.includes('chưa bắt đầu')) {
|
||||
return {
|
||||
type: 'not_started',
|
||||
message: 'Bài tập chưa bắt đầu',
|
||||
details: err,
|
||||
canRetry: false
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: 'api',
|
||||
message: errorMsg,
|
||||
details: err,
|
||||
canRetry: true
|
||||
};
|
||||
}
|
||||
// Network errors
|
||||
if (errorMsg.includes('network') || errorMsg.includes('ECONNREFUSED')) {
|
||||
return {
|
||||
type: 'network',
|
||||
message: 'Lỗi kết nối mạng. Vui lòng kiểm tra internet.',
|
||||
details: err,
|
||||
canRetry: true
|
||||
};
|
||||
}
|
||||
// Session errors (message-based fallback)
|
||||
if (errorMsg.includes('chưa bắt đầu')) {
|
||||
return {
|
||||
type: 'not_started',
|
||||
message: errorMsg || 'Bài tập chưa bắt đầu',
|
||||
details: err,
|
||||
canRetry: false
|
||||
};
|
||||
}
|
||||
if (errorMsg.includes('hết thời gian') || errorMsg.includes('đã kết thúc') ||
|
||||
errorMsg.includes('đã hoàn thành') || errorMsg.includes('session')) {
|
||||
return {
|
||||
type: 'session',
|
||||
message: errorMsg,
|
||||
details: err,
|
||||
canRetry: false
|
||||
};
|
||||
}
|
||||
// Timeout errors
|
||||
if (errorMsg.includes('timeout') || errorMsg.includes('timed out')) {
|
||||
return {
|
||||
type: 'timeout',
|
||||
message: 'Kết nối quá lâu. Vui lòng thử lại.',
|
||||
details: err,
|
||||
canRetry: true
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: 'unknown',
|
||||
message: errorMsg || 'Đã xảy ra lỗi không xác định',
|
||||
details: err,
|
||||
canRetry: true
|
||||
};
|
||||
}, []);
|
||||
// Helper: Handle error with callback
|
||||
const handleError = (0, react_1.useCallback)((err) => {
|
||||
const gameError = createError(err);
|
||||
setError(gameError);
|
||||
setLoading(false);
|
||||
if (onError) {
|
||||
onError(gameError);
|
||||
}
|
||||
if (onLog) {
|
||||
onLog(`[SDK ERR] ${gameError.type}: ${gameError.message}`, 'error');
|
||||
}
|
||||
}, [createError, onError, onLog]);
|
||||
// Retry function
|
||||
const retryInit = (0, react_1.useCallback)(() => {
|
||||
if (callbacksRef.current.onLog)
|
||||
callbacksRef.current.onLog('[SDK] Retrying initialization...', 'info');
|
||||
hasInitRef.current = false; // Allow re-init
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
setGameUrl('');
|
||||
setGameState(null);
|
||||
}, []);
|
||||
// Define Handlers
|
||||
const handleAnswer = (0, react_1.useCallback)(async (data) => {
|
||||
try {
|
||||
if (onLog)
|
||||
onLog(`[SDK RECV] Answer Raw: ${JSON.stringify(data)}`, 'info');
|
||||
const report = (0, mappers_1.normalizeAnswerReport)(data);
|
||||
if (onLog)
|
||||
onLog(`[SDK] Processed Answer: ID ${report.question_id} | Choice: ${report.choice} | ${report.is_correct ? 'Correct' : 'Wrong'}`, 'info');
|
||||
if (onLog)
|
||||
onLog(`[API REQ] Submitting Answer ID ${report.question_id}...`, 'info');
|
||||
const res = await apiClient.submitAnswer(assignmentId, studentId, report.question_id, report.choice, report.time_spent, report.is_timeout);
|
||||
if (onLog)
|
||||
onLog(`[API RES] Submit Result: ${JSON.stringify(res)}`, 'success');
|
||||
}
|
||||
catch (err) {
|
||||
console.error('[GamePlayer] Submit error:', err);
|
||||
if (onLog)
|
||||
onLog(`[API ERR] Submit Failed: ${err.message}`, 'error');
|
||||
}
|
||||
}, [apiClient, assignmentId, studentId, onLog]);
|
||||
const handleFinalResult = (0, react_1.useCallback)(async (data) => {
|
||||
try {
|
||||
if (onLog)
|
||||
onLog(`[SDK RECV] Final Result Raw: ${JSON.stringify(data)}`, 'info');
|
||||
if (onLog)
|
||||
onLog('[SDK] Game Complete. Calling API...', 'info');
|
||||
const res = await apiClient.completeSession(assignmentId, studentId);
|
||||
if (onLog)
|
||||
onLog(`[API RES] Session Completed. Score: ${res.data?.finalScore}`, 'success');
|
||||
// Auto-refresh leaderboard after completion to ensure user rank is included
|
||||
if (onLog)
|
||||
onLog('[SDK] Auto-refreshing leaderboard after completion...', 'info');
|
||||
await new Promise(resolve => setTimeout(resolve, 500)); // Small delay for backend to process
|
||||
try {
|
||||
const lbRes = await apiClient.getLeaderboard(assignmentId, studentId);
|
||||
if (onLog)
|
||||
onLog(`[API RES] Post-completion Leaderboard: ${JSON.stringify(lbRes)}`, 'success');
|
||||
if (lbRes.success && lbRes.data && sendLeaderboardRef.current) {
|
||||
const leaderboardData = {
|
||||
top_players: (lbRes.data.topPlayers || []).map((p) => ({
|
||||
rank: p.rank,
|
||||
name: p.name || p.studentName || p.userId,
|
||||
score: p.score ?? p.finalScore ?? 0,
|
||||
student_id: p.studentId || p.userId,
|
||||
time_spent: p.timeSpent ?? p.time_spent ?? 0,
|
||||
completed_at: p.completedAt
|
||||
})),
|
||||
user_rank: lbRes.data.userRank ? {
|
||||
rank: lbRes.data.userRank.rank,
|
||||
name: lbRes.data.userRank.name || lbRes.data.userRank.studentName,
|
||||
score: lbRes.data.userRank.score ?? lbRes.data.userRank.finalScore ?? 0,
|
||||
student_id: lbRes.data.userRank.studentId || lbRes.data.userRank.userId,
|
||||
time_spent: lbRes.data.userRank.timeSpent ?? lbRes.data.userRank.time_spent ?? 0,
|
||||
completed_at: lbRes.data.userRank.completedAt
|
||||
} : null
|
||||
};
|
||||
if (onLog)
|
||||
onLog(`[SDK SEND] Updated Leaderboard: ${JSON.stringify(leaderboardData)}`, 'info');
|
||||
sendLeaderboardRef.current(leaderboardData);
|
||||
}
|
||||
}
|
||||
catch (lbErr) {
|
||||
if (onLog)
|
||||
onLog(`[API ERR] Leaderboard refresh failed: ${lbErr.message}`, 'warning');
|
||||
}
|
||||
if (onComplete)
|
||||
onComplete(res);
|
||||
}
|
||||
catch (err) {
|
||||
console.error('[GamePlayer] Complete error:', err);
|
||||
if (onLog)
|
||||
onLog(`[API ERR] Complete Failed: ${err.message}`, 'error');
|
||||
if (onError)
|
||||
onError(err);
|
||||
}
|
||||
}, [apiClient, assignmentId, studentId, onComplete, onError, onLog]);
|
||||
// SDK Hook
|
||||
const { isReady, sendGameData, sendLeaderboard } = (0, useGameIframeSDK_1.useGameIframeSDK)({
|
||||
iframeRef,
|
||||
iframeOrigin: '*',
|
||||
debug,
|
||||
onGameReady: () => {
|
||||
if (onLog)
|
||||
onLog('[SDK RECV] Iframe Ready', 'success');
|
||||
},
|
||||
onAnswerReport: handleAnswer,
|
||||
onFinalResult: handleFinalResult,
|
||||
onLeaderboardRequest: async (top) => {
|
||||
try {
|
||||
if (onLog)
|
||||
onLog(`[SDK RECV] Leaderboard Request Raw: top=${top}`, 'info');
|
||||
if (onLog)
|
||||
onLog(`[API REQ] Fetching Leaderboard (top ${top})...`, 'info');
|
||||
const res = await apiClient.getLeaderboard(assignmentId, studentId);
|
||||
if (onLog)
|
||||
onLog(`[API RES] Leaderboard Raw: ${JSON.stringify(res)}`, 'success');
|
||||
if (res.success && res.data) {
|
||||
const leaderboardData = {
|
||||
top_players: (res.data.topPlayers || []).map((p) => ({
|
||||
rank: p.rank,
|
||||
name: p.name || p.studentName || p.userId,
|
||||
score: p.score ?? p.finalScore ?? 0,
|
||||
student_id: p.studentId || p.userId,
|
||||
time_spent: p.timeSpent ?? p.time_spent ?? 0,
|
||||
completed_at: p.completedAt
|
||||
})),
|
||||
user_rank: res.data.userRank ? {
|
||||
rank: res.data.userRank.rank,
|
||||
name: res.data.userRank.name || res.data.userRank.studentName,
|
||||
score: res.data.userRank.score ?? res.data.userRank.finalScore ?? 0,
|
||||
student_id: res.data.userRank.studentId || res.data.userRank.userId,
|
||||
time_spent: res.data.userRank.timeSpent ?? res.data.userRank.time_spent ?? 0,
|
||||
completed_at: res.data.userRank.completedAt
|
||||
} : null
|
||||
};
|
||||
if (onLog)
|
||||
onLog(`[SDK SEND] Leaderboard Payload: ${JSON.stringify(leaderboardData)}`, 'info');
|
||||
const sent = sendLeaderboard(leaderboardData);
|
||||
if (onLog)
|
||||
onLog(sent ? '[SDK] Leaderboard sent successfully' : '[SDK ERR] Failed to send leaderboard', sent ? 'success' : 'error');
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error('[GamePlayer] Leaderboard error:', err);
|
||||
if (onLog)
|
||||
onLog(`[API ERR] Leaderboard Failed: ${err.message}`, 'error');
|
||||
}
|
||||
}
|
||||
});
|
||||
// Update ref after hook
|
||||
sendLeaderboardRef.current = sendLeaderboard;
|
||||
// 1. Fetch Game Data & Start Session
|
||||
(0, react_1.useEffect)(() => {
|
||||
let mounted = true;
|
||||
let loadingTimeoutId;
|
||||
const initGame = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
if (onLog)
|
||||
onLog('[SDK] Initializing Game...', 'info');
|
||||
// Set loading timeout
|
||||
loadingTimeoutId = setTimeout(() => {
|
||||
if (mounted && loading) {
|
||||
if (onLog)
|
||||
onLog('[SDK WARN] Loading taking longer than expected...', 'warning');
|
||||
}
|
||||
}, loadingTimeout);
|
||||
// getGameWithProgress đã tự động tạo/restore session trong backend
|
||||
// thông qua getOrCreateSubmissionCache - không cần gọi startLiveSession riêng
|
||||
const res = await apiClient.getGameWithProgress(assignmentId, studentId);
|
||||
if (!res.success || !res.data) {
|
||||
throw new Error(res.message || 'Failed to load game');
|
||||
}
|
||||
if (mounted) {
|
||||
// Check session status TRƯỚC để quyết định có load game hay không
|
||||
const session = res.data.session;
|
||||
const progress = res.data.progress;
|
||||
// Case 1: Already completed
|
||||
if (res.data.isFinished || session?.completed) {
|
||||
// CHỈ set error nếu CHƯA load game (lần đầu vào)
|
||||
if (!hasLoadedGame) {
|
||||
const errorObj = {
|
||||
type: 'session',
|
||||
message: 'Bạn đã hoàn thành bài tập này rồi',
|
||||
details: { score: progress?.score, answered: progress?.answered, total: progress?.total },
|
||||
canRetry: false
|
||||
};
|
||||
setError(errorObj);
|
||||
setLoading(false);
|
||||
clearTimeout(loadingTimeoutId);
|
||||
if (onError)
|
||||
onError(errorObj);
|
||||
if (onLog)
|
||||
onLog(`[SDK] ${errorObj.message}`, 'warning');
|
||||
}
|
||||
// Luôn gọi callback (cho cả 2 trường hợp)
|
||||
if (onSessionStatus) {
|
||||
onSessionStatus({
|
||||
type: 'completed',
|
||||
message: 'Bạn đã hoàn thành bài tập này',
|
||||
data: {
|
||||
answered: progress?.answered || 0,
|
||||
total: progress?.total || 0,
|
||||
score: progress?.score || 0
|
||||
}
|
||||
});
|
||||
}
|
||||
// Nếu CHƯA load game → STOP
|
||||
// Nếu ĐÃ load game → tiếp tục (cho game hiển thị leaderboard)
|
||||
if (!hasLoadedGame) {
|
||||
return; // ❌ STOP - không load game
|
||||
}
|
||||
}
|
||||
// Case 2: Session timeout
|
||||
if (session && session.remainingTime !== null && session.remainingTime <= 0) {
|
||||
// CHỈ set error nếu CHƯA load game
|
||||
if (!hasLoadedGame) {
|
||||
const errorObj = {
|
||||
type: 'timeout',
|
||||
message: 'Thời gian làm bài đã hết',
|
||||
details: { answered: progress?.answered, total: progress?.total },
|
||||
canRetry: false
|
||||
};
|
||||
setError(errorObj);
|
||||
setLoading(false);
|
||||
clearTimeout(loadingTimeoutId);
|
||||
if (onError)
|
||||
onError(errorObj);
|
||||
if (onLog)
|
||||
onLog(`[SDK] ${errorObj.message}`, 'warning');
|
||||
}
|
||||
// Luôn gọi callback
|
||||
if (onSessionStatus) {
|
||||
onSessionStatus({
|
||||
type: 'timeout',
|
||||
message: 'Thời gian làm bài đã hết',
|
||||
data: {
|
||||
answered: progress?.answered || 0,
|
||||
total: progress?.total || 0,
|
||||
score: progress?.score || 0,
|
||||
remainingTime: 0
|
||||
}
|
||||
});
|
||||
}
|
||||
// Nếu CHƯA load game → STOP
|
||||
if (!hasLoadedGame) {
|
||||
return; // ❌ STOP - không load game
|
||||
}
|
||||
}
|
||||
// Case 3: Valid session - LOAD game
|
||||
setGameState(res.data);
|
||||
setGameUrl(res.data.gameUrl);
|
||||
setLoading(false);
|
||||
setHasLoadedGame(true); // ✅ Đánh dấu đã load game
|
||||
clearTimeout(loadingTimeoutId);
|
||||
if (onGameLoaded)
|
||||
onGameLoaded(res.data);
|
||||
if (onLog)
|
||||
onLog(`[API RES] Game Loaded: ${res.data.title || res.data.gameType || assignmentId}`, 'success');
|
||||
// Notify session status for valid sessions
|
||||
if (session && onSessionStatus) {
|
||||
// Resumed session (có câu đã trả lời)
|
||||
if (progress && progress.answered > 0) {
|
||||
onSessionStatus({
|
||||
type: 'resumed',
|
||||
message: `Tiếp tục làm bài (Đã làm ${progress.answered}/${progress.total} câu)`,
|
||||
data: {
|
||||
answered: progress.answered,
|
||||
total: progress.total,
|
||||
score: progress.score,
|
||||
remainingTime: session.remainingTime,
|
||||
startedAt: session.startedAt
|
||||
}
|
||||
});
|
||||
}
|
||||
// New session
|
||||
else {
|
||||
onSessionStatus({
|
||||
type: 'new',
|
||||
message: 'Bắt đầu làm bài mới',
|
||||
data: {
|
||||
answered: 0,
|
||||
total: progress?.total || 0,
|
||||
score: 0,
|
||||
remainingTime: session.remainingTime,
|
||||
startedAt: session.startedAt
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// Log session info
|
||||
if (session && onLog) {
|
||||
const sessionInfo = `Session started at ${session.startedAt}, remaining: ${session.remainingTime}s`;
|
||||
onLog(`[SDK] ${sessionInfo}`, 'info');
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error('[GamePlayer] Init error:', err);
|
||||
if (mounted) {
|
||||
handleError(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (assignmentId && studentId && !hasInitRef.current) {
|
||||
hasInitRef.current = true; // Prevent re-init
|
||||
initGame();
|
||||
}
|
||||
return () => {
|
||||
mounted = false;
|
||||
hasInitRef.current = false; // ✅ Reset for StrictMode remount
|
||||
if (loadingTimeoutId)
|
||||
clearTimeout(loadingTimeoutId);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [assignmentId, studentId]); // ✅ Chỉ re-init khi assignmentId hoặc studentId thay đổi
|
||||
// 2. Auto Send Data when Ready
|
||||
(0, react_1.useEffect)(() => {
|
||||
if (isReady && gameState) {
|
||||
// Get expiresAt from session (absolute timestamp for accurate sync)
|
||||
const getEndTimeIso = () => {
|
||||
const session = gameState.session;
|
||||
if (!session)
|
||||
return undefined;
|
||||
// Ưu tiên dùng expiresAt
|
||||
if (session.expiresAt) {
|
||||
return session.expiresAt;
|
||||
}
|
||||
// Fallback: tính từ startedAt + duration
|
||||
if (session.startedAt && session.duration) {
|
||||
const startTimeMs = new Date(session.startedAt).getTime();
|
||||
const expiresAtMs = startTimeMs + (session.duration * 1000);
|
||||
return new Date(expiresAtMs).toISOString();
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
const payload = (0, mappers_1.createGamePayload)({
|
||||
gameId: gameState.id,
|
||||
userId: studentId,
|
||||
gameData: gameState.jsonData,
|
||||
answeredQuestions: gameState.completed_question_ids,
|
||||
extraData: extraGameData,
|
||||
endTimeIso: getEndTimeIso() // ✅ Absolute timestamp
|
||||
});
|
||||
if (onLog)
|
||||
onLog(`[SDK SEND] Game Payload: ${JSON.stringify(payload)}`, 'info');
|
||||
sendGameData(payload);
|
||||
}
|
||||
}, [isReady, gameState, studentId, sendGameData, extraGameData, onLog]);
|
||||
// ===== RENDER =====
|
||||
// 1. Error State
|
||||
if (error) {
|
||||
if (renderError) {
|
||||
return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: renderError(error, retryInit) });
|
||||
}
|
||||
// Default error UI
|
||||
return ((0, jsx_runtime_1.jsxs)("div", { className: "game-player-error", style: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
padding: '2rem',
|
||||
textAlign: 'center',
|
||||
backgroundColor: '#f8f9fa',
|
||||
...style
|
||||
}, children: [(0, jsx_runtime_1.jsx)("div", { style: {
|
||||
fontSize: '3rem',
|
||||
marginBottom: '1rem'
|
||||
}, children: error.type === 'network' ? '🌐' :
|
||||
error.type === 'timeout' ? '⏱️' :
|
||||
error.type === 'session' ? '🔒' :
|
||||
error.type === 'not_started' ? '📅' : '⚠️' }), (0, jsx_runtime_1.jsx)("h2", { style: {
|
||||
fontSize: '1.5rem',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '0.5rem',
|
||||
color: error.type === 'not_started' ? '#fd7e14' : '#dc3545'
|
||||
}, children: error.type === 'network' ? 'Lỗi Kết Nối' :
|
||||
error.type === 'timeout' ? 'Hết Giờ Làm Bài' :
|
||||
error.type === 'session' ? 'Phiên Làm Bài Đã Kết Thúc' :
|
||||
error.type === 'not_started' ? 'Bài Tập Chưa Bắt Đầu' :
|
||||
'Đã Xảy Ra Lỗi' }), (0, jsx_runtime_1.jsx)("p", { style: {
|
||||
fontSize: '1rem',
|
||||
color: '#6c757d',
|
||||
marginBottom: '1.5rem',
|
||||
maxWidth: '500px'
|
||||
}, children: error.message }), error.canRetry && ((0, jsx_runtime_1.jsx)("button", { onClick: retryInit, style: {
|
||||
padding: '0.75rem 2rem',
|
||||
fontSize: '1rem',
|
||||
fontWeight: 'bold',
|
||||
color: '#fff',
|
||||
backgroundColor: '#007bff',
|
||||
border: 'none',
|
||||
borderRadius: '0.5rem',
|
||||
cursor: 'pointer',
|
||||
transition: 'background-color 0.2s'
|
||||
}, onMouseEnter: (e) => e.currentTarget.style.backgroundColor = '#0056b3', onMouseLeave: (e) => e.currentTarget.style.backgroundColor = '#007bff', children: "\uD83D\uDD04 Th\u1EED L\u1EA1i" })), !error.canRetry && ((0, jsx_runtime_1.jsx)("p", { style: {
|
||||
fontSize: '0.875rem',
|
||||
color: '#6c757d',
|
||||
marginTop: '1rem'
|
||||
}, children: "Vui l\u00F2ng li\u00EAn h\u1EC7 gi\u00E1o vi\u00EAn ho\u1EB7c quay l\u1EA1i trang ch\u1EE7" }))] }));
|
||||
}
|
||||
// 2. Loading State
|
||||
if (loading || !gameUrl) {
|
||||
if (renderLoading) {
|
||||
return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: renderLoading() });
|
||||
}
|
||||
// Default loading UI
|
||||
return ((0, jsx_runtime_1.jsxs)("div", { className: "game-player-loading", style: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
backgroundColor: '#f8f9fa',
|
||||
...style
|
||||
}, children: [(0, jsx_runtime_1.jsx)("div", { style: {
|
||||
width: '50px',
|
||||
height: '50px',
|
||||
border: '5px solid #e9ecef',
|
||||
borderTop: '5px solid #007bff',
|
||||
borderRadius: '50%',
|
||||
animation: 'spin 1s linear infinite',
|
||||
marginBottom: '1rem'
|
||||
} }), (0, jsx_runtime_1.jsx)("p", { style: {
|
||||
fontSize: '1.125rem',
|
||||
color: '#6c757d',
|
||||
fontWeight: '500'
|
||||
}, children: "\u0110ang t\u1EA3i tr\u00F2 ch\u01A1i..." }), (0, jsx_runtime_1.jsx)("style", { children: `
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
` })] }));
|
||||
}
|
||||
// 3. Game Iframe
|
||||
return ((0, jsx_runtime_1.jsx)("iframe", { ref: iframeRef, src: gameUrl, className: className, style: { width: '100%', height: '100%', border: 'none', ...style }, allowFullScreen: true }));
|
||||
};
|
||||
exports.GamePlayer = GamePlayer;
|
||||
//# sourceMappingURL=GamePlayer.js.map
|
||||
Reference in New Issue
Block a user