up
All checks were successful
Deploy to Production / deploy (push) Successful in 8s

This commit is contained in:
lubukhu
2026-01-24 13:35:11 +07:00
parent 6c3e93636e
commit 65fd0158a3
145 changed files with 10262 additions and 0 deletions

View File

@@ -0,0 +1,151 @@
/**
* SenaGame SDK Loader
*
* Ready-to-use interface for game developers.
* Handles SDK iframe creation, communication, and lifecycle.
*
* Usage:
* ```html
* <script src="sena-game-sdk.js"></script>
* <script>
* const game = new SenaGameSDK({
* iframePath: '/path/to/sdk-iframe/index.html',
* mode: 'live',
* gameCode: 'G001',
* onReady: (sdk) => {
* sdk.pushData({ items: [...] });
* },
* onAnswerResult: (result) => {
* console.log('Answer:', result);
* }
* });
* </script>
* ```
*
* @version 1.0.0
*/
export interface SenaGameConfig {
/** Path to SDK iframe HTML file (can be versioned) */
iframePath: string;
/** Game mode: 'live' | 'preview' */
mode: 'live' | 'preview';
/** Game code for identification */
gameCode: string;
/** Enable debug logging */
debug?: boolean;
/** Timeout for SDK initialization (ms) */
timeout?: number;
/** Custom iframe style */
iframeStyle?: string;
onReady?: (sdk: SenaGameSDK) => void;
onDataReady?: (payload: DataReadyPayload) => void;
onAnswerResult?: (result: AnswerResultPayload) => void;
onGameComplete?: (result: GameCompletePayload) => void;
onSessionStart?: (session: SessionStartPayload) => void;
onError?: (error: Error) => void;
}
export interface GameItem {
id: string;
question: string;
options?: string[];
answer?: string | number;
[key: string]: any;
}
export interface GameData {
items: GameItem[];
[key: string]: any;
}
export interface AnswerPayload {
questionId: string;
selectedAnswer: string | number | string[];
timeSpent?: number;
}
export interface DataReadyPayload {
items: GameItem[];
totalQuestions: number;
completedCount: number;
resumeData?: Array<{
id: string;
result: 0 | 1;
}>;
}
export interface AnswerResultPayload {
questionId: string;
isCorrect: boolean;
correctAnswer: string | number;
score: number;
currentScore: number;
totalAnswered: number;
}
export interface GameCompletePayload {
success: boolean;
finalScore: number;
correctCount: number;
totalQuestions: number;
wrongCount: number;
total: number;
}
export interface SessionStartPayload {
assignmentId: number;
userId: string;
gameId: string;
startedAt: string;
}
export declare class SenaGameSDK {
private config;
private iframe;
private isReady;
private isDataReady;
private pendingMessages;
private initPromise;
private initResolver;
private timeoutId;
/** SDK version */
static readonly VERSION = "1.0.0";
constructor(config: SenaGameConfig);
/**
* Push game data to SDK
*/
pushData(data: any): void;
/**
* Submit an answer
*/
submitAnswer(answer: AnswerPayload): void;
/**
* Complete the game
*/
completeGame(): void;
/**
* Get current status
*/
getStatus(): void;
/**
* Wait for SDK to be ready
*/
ready(): Promise<SenaGameSDK>;
/**
* Check if SDK is ready
*/
get sdkReady(): boolean;
/**
* Check if data is ready
*/
get dataReady(): boolean;
/**
* Destroy the SDK instance
*/
destroy(): void;
private _init;
private _createIframe;
private _send;
private _handleMessage;
private _onSDKReady;
private _onDataReady;
private _onAnswerResult;
private _onGameComplete;
private _onSessionStart;
private _error;
private _log;
}
export default SenaGameSDK;
//# sourceMappingURL=SenaGameSDK.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"SenaGameSDK.d.ts","sourceRoot":"","sources":["../../src/loader/SenaGameSDK.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,361 @@
"use strict";
/**
* SenaGame SDK Loader
*
* Ready-to-use interface for game developers.
* Handles SDK iframe creation, communication, and lifecycle.
*
* Usage:
* ```html
* <script src="sena-game-sdk.js"></script>
* <script>
* const game = new SenaGameSDK({
* iframePath: '/path/to/sdk-iframe/index.html',
* mode: 'live',
* gameCode: 'G001',
* onReady: (sdk) => {
* sdk.pushData({ items: [...] });
* },
* onAnswerResult: (result) => {
* console.log('Answer:', result);
* }
* });
* </script>
* ```
*
* @version 1.0.0
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SenaGameSDK = void 0;
// ========================================
// MESSAGE TYPES (matching SDK)
// ========================================
const MSG = {
// Outgoing (Game → SDK)
INIT: 'SDK_INIT',
PUSH_DATA: 'SDK_PUSH_DATA',
SUBMIT_ANSWER: 'SDK_CHECK_ANSWER',
COMPLETE_GAME: 'SDK_COMPLETE_GAME',
GET_STATUS: 'SDK_GET_STATUS',
// Incoming (SDK → Game)
READY: 'SDK_READY',
DATA_READY: 'SDK_DATA_READY',
ANSWER_RESULT: 'SDK_ANSWER_RESULT',
GAME_COMPLETE: 'SDK_GAME_COMPLETE',
SESSION_STARTED: 'SDK_SESSION_STARTED',
STATUS: 'SDK_STATUS',
ERROR: 'SDK_ERROR',
};
// ========================================
// DEFAULT CONFIG
// ========================================
const DEFAULT_CONFIG = {
iframePath: '../sdk-iframe/index.html',
mode: 'preview',
gameCode: '',
debug: false,
timeout: 10000,
iframeStyle: 'position:fixed;width:1px;height:1px;left:-9999px;border:none;',
};
// ========================================
// SENA GAME SDK CLASS
// ========================================
class SenaGameSDK {
constructor(config) {
this.iframe = null;
this.isReady = false;
this.isDataReady = false;
this.pendingMessages = [];
this.initResolver = null;
this.timeoutId = null;
this.config = { ...DEFAULT_CONFIG, ...config };
// --- CLEANUP OLD LISTENERS ---
// Tránh trường hợp init SDK nhiều lần bị trùng listener cũ
const oldSDK = window._sena_game_sdk_instance;
if (oldSDK && typeof oldSDK.destroy === 'function') {
oldSDK.destroy();
}
window._sena_game_sdk_instance = this;
// Create promise for ready state
this.initPromise = new Promise((resolve, reject) => {
this.initResolver = { resolve, reject };
});
// Bind methods
this._handleMessage = this._handleMessage.bind(this);
// Auto-initialize
this._init();
}
// ----------------------------------------
// PUBLIC API
// ----------------------------------------
/**
* Push game data to SDK
*/
pushData(data) {
// Safe extraction of the array
let itemsArray = [];
if (Array.isArray(data)) {
itemsArray = data;
}
else if (data && Array.isArray(data.items)) {
itemsArray = data.items;
}
else if (data && Array.isArray(data.data)) {
itemsArray = data.data;
}
else if (data && data.items && typeof data.items === 'object') {
itemsArray = [data.items];
}
else if (data && data.data && typeof data.data === 'object') {
itemsArray = [data.data];
}
// Transform to SDK iframe internal format
const payload = {
data: itemsArray,
completed_question_ids: (data && data.completed_question_ids) || []
};
this._send(MSG.PUSH_DATA, payload);
}
/**
* Submit an answer
*/
submitAnswer(answer) {
// Transform to SDK iframe format
const payload = {
question_id: answer.questionId,
choice: answer.selectedAnswer,
time_spent: answer.timeSpent ?? 0
};
this._send(MSG.SUBMIT_ANSWER, payload);
}
/**
* Complete the game
*/
completeGame() {
this._send(MSG.COMPLETE_GAME, {});
}
/**
* Get current status
*/
getStatus() {
this._send(MSG.GET_STATUS, {});
}
/**
* Wait for SDK to be ready
*/
async ready() {
if (this.isReady)
return this;
return this.initPromise;
}
/**
* Check if SDK is ready
*/
get sdkReady() {
return this.isReady;
}
/**
* Check if data is ready
*/
get dataReady() {
return this.isDataReady;
}
/**
* Destroy the SDK instance
*/
destroy() {
window.removeEventListener('message', this._handleMessage);
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
if (this.iframe && this.iframe.parentNode) {
this.iframe.parentNode.removeChild(this.iframe);
}
window.removeEventListener('message', this._handleMessage);
this.isReady = false;
this._log('SDK destroyed');
}
// ----------------------------------------
// PRIVATE METHODS
// ----------------------------------------
_init() {
this._log('Initializing SenaGameSDK...');
// Setup message listener
window.addEventListener('message', this._handleMessage);
// Create iframe
this._createIframe();
// Setup timeout
this.timeoutId = setTimeout(() => {
if (!this.isReady) {
const error = new Error('SDK initialization timeout');
this._error(error);
if (this.initResolver) {
this.initResolver.reject(error);
}
}
}, this.config.timeout);
}
_createIframe() {
this.iframe = document.createElement('iframe');
this.iframe.id = 'sena-game-sdk-iframe';
this.iframe.src = this.config.iframePath;
this.iframe.style.cssText = this.config.iframeStyle || '';
this.iframe.onload = () => {
this._log('Iframe loaded, sending INIT...');
setTimeout(() => {
this._send(MSG.INIT, {
mode: this.config.mode,
game_code: this.config.gameCode,
});
}, 100);
};
this.iframe.onerror = () => {
this._error(new Error('Failed to load SDK iframe'));
};
document.body.appendChild(this.iframe);
this._log(`Iframe created: ${this.config.iframePath}`);
}
_send(type, payload) {
if (!this.iframe || !this.iframe.contentWindow) {
this._log(`Queuing message: ${type}`, 'warn');
this.pendingMessages.push({ type, payload });
return;
}
const message = { type, payload, timestamp: Date.now() };
this._log(`${type}`, 'send');
this.iframe.contentWindow.postMessage(message, '*');
}
_handleMessage(event) {
const data = event.data;
if (!data || !data.type)
return;
// Only process SDK messages
if (!data.type.startsWith('SDK_'))
return;
this._log(`${data.type}`, 'recv');
switch (data.type) {
case MSG.READY:
this._onSDKReady();
break;
case MSG.DATA_READY:
this._onDataReady(data.payload);
break;
case MSG.ANSWER_RESULT:
this._onAnswerResult(data.payload);
break;
case MSG.GAME_COMPLETE:
this._onGameComplete(data.payload);
break;
case MSG.SESSION_STARTED:
this._onSessionStart(data.payload);
break;
case MSG.ERROR:
this._error(new Error(data.payload?.message || 'SDK Error'));
break;
}
}
_onSDKReady() {
this.isReady = true;
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
// Flush pending messages
while (this.pendingMessages.length > 0) {
const msg = this.pendingMessages.shift();
if (msg) {
this._send(msg.type, msg.payload);
}
}
// Callback
if (this.config.onReady) {
this.config.onReady(this);
}
// Resolve promise
if (this.initResolver) {
this.initResolver.resolve(this);
}
this._log('✅ SDK Ready!');
}
_onDataReady(payload) {
this.isDataReady = true;
// Normalize payload to camelCase
const normalized = {
items: payload.items || [],
totalQuestions: payload.total_questions || payload.totalQuestions || 0,
completedCount: payload.completed_count || payload.completedCount || 0,
resumeData: payload.resume_data || payload.resumeData || []
};
if (this.config.onDataReady) {
this.config.onDataReady(normalized);
}
this._log(`✅ Data Ready: ${normalized.items.length} items`);
}
_onAnswerResult(payload) {
const normalized = {
questionId: payload.question_id || payload.questionId,
isCorrect: payload.correct !== undefined ? payload.correct : payload.isCorrect,
correctAnswer: payload.correct_answer || payload.correctAnswer || '',
score: payload.score || 0,
currentScore: payload.current_score || payload.currentScore || 0,
totalAnswered: payload.total_answered || payload.totalAnswered || 0
};
if (this.config.onAnswerResult) {
this.config.onAnswerResult(normalized);
}
}
_onGameComplete(payload) {
const normalized = {
success: payload.success !== undefined ? payload.success : true,
finalScore: payload.score !== undefined ? payload.score : (payload.finalScore || 0),
correctCount: payload.correct !== undefined ? payload.correct : (payload.correctCount || 0),
totalQuestions: payload.total !== undefined ? payload.total : (payload.totalQuestions || 0),
wrongCount: payload.wrong !== undefined ? payload.wrong : (payload.wrongCount || 0),
total: payload.total || 0
};
if (this.config.onGameComplete) {
this.config.onGameComplete(normalized);
}
}
_onSessionStart(payload) {
const normalized = {
assignmentId: payload.assignment_id || payload.assignmentId,
userId: payload.student_id || payload.userId,
gameId: payload.game_code || payload.gameId,
startedAt: payload.started_at || payload.startedAt || new Date().toISOString()
};
if (this.config.onSessionStart) {
this.config.onSessionStart(normalized);
}
}
_error(error) {
this._log(`❌ Error: ${error.message}`, 'error');
if (this.config.onError) {
this.config.onError(error);
}
}
_log(message, type = 'info') {
if (!this.config.debug)
return;
const prefix = '[SenaGameSDK]';
const styles = {
info: 'color: #888',
send: 'color: #ff0',
recv: 'color: #0f0',
warn: 'color: #fa0',
error: 'color: #f00',
};
console.log(`%c${prefix} ${message}`, styles[type] || styles.info);
}
}
exports.SenaGameSDK = SenaGameSDK;
/** SDK version */
SenaGameSDK.VERSION = '1.0.0';
// ========================================
// EXPORT FOR BROWSER (UMD)
// ========================================
if (typeof window !== 'undefined') {
window.SenaGameSDK = SenaGameSDK;
}
exports.default = SenaGameSDK;
//# sourceMappingURL=SenaGameSDK.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,7 @@
/**
* SenaGame SDK Loader
* Export all types and classes for game developers
*/
export { SenaGameSDK, type SenaGameConfig, type GameItem, type GameData, type AnswerPayload, type DataReadyPayload, type AnswerResultPayload, type GameCompletePayload, type SessionStartPayload, } from './SenaGameSDK';
export { SenaGameSDK as default } from './SenaGameSDK';
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/loader/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACH,WAAW,EACX,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,GAC3B,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,WAAW,IAAI,OAAO,EAAE,MAAM,eAAe,CAAC"}

View File

@@ -0,0 +1,12 @@
"use strict";
/**
* SenaGame SDK Loader
* Export all types and classes for game developers
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = exports.SenaGameSDK = void 0;
var SenaGameSDK_1 = require("./SenaGameSDK");
Object.defineProperty(exports, "SenaGameSDK", { enumerable: true, get: function () { return SenaGameSDK_1.SenaGameSDK; } });
var SenaGameSDK_2 = require("./SenaGameSDK");
Object.defineProperty(exports, "default", { enumerable: true, get: function () { return SenaGameSDK_2.SenaGameSDK; } });
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/loader/index.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAEH,6CAUuB;AATnB,0GAAA,WAAW,OAAA;AAWf,6CAAuD;AAA9C,sGAAA,WAAW,OAAW"}