/* organizer-content-library.jsx — Content Library for Arena Organizer
* React 18 · in-browser Babel · no build step
* Deps: window.GAME_MODULES, window.AdminApi, tc(), React, ReactDOM
*
* Part 1: CurationContext, helpers, View 1 (ModuleOverviewGrid), toolbar components
*/
// ── Curation Context ──────────────────────────────────────────────
var CurationContext = React.createContext(null);
function curationReducer(state, action) {
switch (action.type) {
case 'SELECT_EVENT':
return Object.assign({}, state, { selectedEventId: action.eventId });
case 'SELECT_MODULE':
return Object.assign({}, state, { selectedModuleId: action.moduleId });
case 'SET_LANGUAGE':
return Object.assign({}, state, { language: action.language });
case 'INIT_CONFIGS': {
return Object.assign({}, state, { configs: action.configs || {} });
}
case 'TOGGLE_ITEM': {
var moduleId = action.moduleId;
var contentType = action.contentType;
var itemId = action.itemId;
var existingModuleConfig = state.configs[moduleId] || {};
var existingTypeConfig = existingModuleConfig[contentType] || { included_ids: null, overrides: {} };
var currentIds = existingTypeConfig.included_ids;
var newIds;
if (currentIds === null || currentIds === undefined) {
// null means "all included" — initialize from allItemIds, then remove the toggled item
var all = action.allItemIds || [];
newIds = all.filter(function(id) { return id !== itemId; });
} else if (currentIds.indexOf(itemId) !== -1) {
newIds = currentIds.filter(function(id) { return id !== itemId; });
} else {
newIds = currentIds.concat([itemId]);
}
var newTypeConfig = Object.assign({}, existingTypeConfig, { included_ids: newIds });
var newModuleConfig = Object.assign({}, existingModuleConfig);
newModuleConfig[contentType] = newTypeConfig;
var newConfigs = Object.assign({}, state.configs);
newConfigs[moduleId] = newModuleConfig;
return Object.assign({}, state, { configs: newConfigs });
}
case 'SELECT_ALL': {
var moduleId = action.moduleId;
var contentType = action.contentType;
var itemIds = action.itemIds || [];
var existingModuleConfig = state.configs[moduleId] || {};
var existingTypeConfig = existingModuleConfig[contentType] || { overrides: {} };
var newTypeConfig = Object.assign({}, existingTypeConfig, { included_ids: itemIds.slice() });
var newModuleConfig = Object.assign({}, existingModuleConfig);
newModuleConfig[contentType] = newTypeConfig;
var newConfigs = Object.assign({}, state.configs);
newConfigs[moduleId] = newModuleConfig;
return Object.assign({}, state, { configs: newConfigs });
}
case 'DESELECT_ALL': {
var moduleId = action.moduleId;
var contentType = action.contentType;
var existingModuleConfig = state.configs[moduleId] || {};
var existingTypeConfig = existingModuleConfig[contentType] || { overrides: {} };
var newTypeConfig = Object.assign({}, existingTypeConfig, { included_ids: [] });
var newModuleConfig = Object.assign({}, existingModuleConfig);
newModuleConfig[contentType] = newTypeConfig;
var newConfigs = Object.assign({}, state.configs);
newConfigs[moduleId] = newModuleConfig;
return Object.assign({}, state, { configs: newConfigs });
}
case 'SET_OVERRIDE': {
var moduleId = action.moduleId;
var itemId = action.itemId;
var field = action.field;
var value = action.value;
var existingModuleConfig = state.configs[moduleId] || {};
var existingOverrides = existingModuleConfig.overrides || {};
var existingItemOverrides = existingOverrides[itemId] || {};
var newItemOverrides = Object.assign({}, existingItemOverrides);
newItemOverrides[field] = value;
var newOverrides = Object.assign({}, existingOverrides);
newOverrides[itemId] = newItemOverrides;
var newModuleConfig = Object.assign({}, existingModuleConfig, { overrides: newOverrides });
var newConfigs = Object.assign({}, state.configs);
newConfigs[moduleId] = newModuleConfig;
return Object.assign({}, state, { configs: newConfigs });
}
case 'CACHE_I18N': {
var newI18nCache = Object.assign({}, state.i18nCache);
newI18nCache[action.key] = action.data;
return Object.assign({}, state, { i18nCache: newI18nCache });
}
case 'SET_I18N_LOADING': {
var newI18nLoading = Object.assign({}, state.i18nLoading);
newI18nLoading[action.key] = action.loading;
return Object.assign({}, state, { i18nLoading: newI18nLoading });
}
case 'BACK_TO_GRID':
return Object.assign({}, state, { selectedModuleId: null });
default:
return state;
}
}
function CurationProvider(props) {
var ref = React.useReducer(curationReducer, {
selectedEventId: null,
selectedModuleId: null,
language: 'en',
configs: {},
i18nCache: {},
i18nLoading: {}
});
var state = ref[0], dispatch = ref[1];
return React.createElement(CurationContext.Provider, { value: { state: state, dispatch: dispatch } }, props.children);
}
// ── Helper Functions ──────────────────────────────────────────────
function getModuleContentCounts(moduleData) {
if (!moduleData) return { quiz: 0, tickets: 0, briefs: 0, curveballs: 0, buzzers: 0, total: 0 };
var quizCount = 0, ticketsCount = 0, briefsCount = 0, curveballsCount = 0, buzzersCount = 0;
try { var q = moduleData.quiz; quizCount = (q && q.length) ? q.length : 0; } catch(e) {}
try { var t = moduleData.tickets; ticketsCount = (t && t.length) ? t.length : 0; } catch(e) {}
try { var b = moduleData.briefs; briefsCount = (b && b.length) ? b.length : 0; } catch(e) {}
try { var cb = moduleData.curveballs; curveballsCount = (cb && cb.length) ? cb.length : 0; } catch(e) {}
try { var bz = moduleData.buzzers; buzzersCount = (bz && bz.length) ? bz.length : 0; } catch(e) {}
var total = quizCount + ticketsCount + briefsCount + curveballsCount + buzzersCount;
return { quiz: quizCount, tickets: ticketsCount, briefs: briefsCount, curveballs: curveballsCount, buzzers: buzzersCount, total: total };
}
function getContentTypeItems(moduleData, contentType) {
if (!moduleData) return [];
try {
var items = moduleData[contentType];
return Array.isArray(items) ? items : [];
} catch(e) {
return [];
}
}
function getIncludedIds(configs, moduleId, contentType, allItems) {
var moduleConfig = configs && configs[moduleId];
if (!moduleConfig) {
// No config means all items are included by default
return allItems ? allItems.map(function(item) { return item.id; }) : [];
}
var typeConfig = moduleConfig[contentType];
if (!typeConfig || typeConfig.included_ids === null || typeConfig.included_ids === undefined) {
// null / undefined means all included
return allItems ? allItems.map(function(item) { return item.id; }) : [];
}
return typeConfig.included_ids;
}
function getModuleEnabledCount(moduleData, configs, moduleId) {
var enabledCount = 0;
CONTENT_TABS.forEach(function(tab) {
var typeItems = getContentTypeItems(moduleData, tab.key);
enabledCount += getIncludedIds(configs, moduleId, tab.key, typeItems).length;
});
return enabledCount;
}
function hasAnyI18nLoading(i18nLoading) {
var key;
if (!i18nLoading) return false;
for (key in i18nLoading) {
if (Object.prototype.hasOwnProperty.call(i18nLoading, key) && i18nLoading[key]) {
return true;
}
}
return false;
}
function clibTranslate(text, moduleId, lang, i18nCache) {
if (!text || typeof text !== 'string') return text;
if (lang === 'en' || !lang) return text; // English is the source language
var langSuffix = lang === 'es' ? 'es' : 'pt';
var cacheKey = moduleId + '-' + langSuffix;
var dict = i18nCache[cacheKey];
if (!dict) return text; // Translation not loaded yet
return dict[text] || text; // Translate or fallback to English
}
// ── Constants ─────────────────────────────────────────────────────
var QTYPE_LABELS = {
single_select: 'Single Select',
multi_select: 'Multi Select',
true_false: 'True/False',
cascade: 'Cascade',
drag_match: 'Drag & Match',
group_sort: 'Group Sort',
rank_order: 'Rank Order',
hotspot: 'Hotspot',
numeric_slider: 'Slider',
free_text: 'Free Text',
structured_form: 'Form',
text: 'Text',
buzzer: 'Buzzer'
};
var CONTENT_TABS = [
{ key: 'quiz', label: 'Questions', icon: '❓' },
{ key: 'tickets', label: 'Tickets', icon: '🎫' },
{ key: 'briefs', label: 'Briefs', icon: '📋' },
{ key: 'curveballs', label: 'Curveballs', icon: '🎱' },
{ key: 'buzzers', label: 'Buzzers', icon: '🔔' }
];
var DIFFICULTY_ORDER = { easy: 0, medium: 1, hard: 2 };
function getItemSummary(item, contentType) {
switch (contentType) {
case 'tickets':
return {
label: item.title || '(untitled ticket)',
category: '—',
difficulty: '—',
points: '—',
timer: '—',
qtype: 'ticket',
qtypeLabel: 'Ticket',
audiences: item.audiences || []
};
case 'briefs':
return {
label: (item.client || '?') + ' — ' + (item.industry || '?'),
category: item.industry || '—',
difficulty: '—',
points: '—',
timer: '—',
qtype: 'brief',
qtypeLabel: 'Brief',
audiences: []
};
case 'curveballs':
return {
label: item.text || '(no text)',
category: '—',
difficulty: '—',
points: '—',
timer: '—',
qtype: 'curveball',
qtypeLabel: 'Curveball',
audiences: []
};
case 'buzzers':
var bPts = item.points;
var bPtsStr = '—';
if (bPts && typeof bPts === 'object') { bPtsStr = (bPts.first || 0) + '/' + (bPts.second || 0) + '/' + (bPts.third || 0); }
else if (typeof bPts === 'number') { bPtsStr = bPts; }
return {
label: item.text || '(no text)',
category: '—',
difficulty: '—',
points: bPtsStr,
timer: '—',
qtype: item.type || 'buzzer',
qtypeLabel: 'Buzzer',
audiences: []
};
default: // quiz
return {
label: item.prompt || '—',
category: item.category || '—',
difficulty: item.difficulty || '—',
points: item.points || 0,
timer: (item.timeSec || 0) + 's',
qtype: item.type || 'text',
qtypeLabel: QTYPE_LABELS[item.type] || item.type || 'Text',
audiences: item.audiences || []
};
}
}
function filterContentItems(items, filters) {
if (!items || !items.length) return [];
var search = filters.search ? filters.search.toLowerCase().trim() : '';
var category = filters.category || '';
var qtype = filters.qtype || '';
var difficulty = filters.difficulty || '';
var audience = filters.audience || '';
var showExcluded = filters.showExcluded !== false; // default true
var includedIds = filters.includedIds || null;
return items.filter(function(item) {
// showExcluded filter
if (!showExcluded && includedIds !== null) {
if (includedIds.indexOf(item.id) === -1) return false;
}
// search filter
if (search) {
var prompt = (item.prompt || item.title || item.text || item.client || '').toLowerCase();
var id = (item.id || '').toLowerCase();
if (prompt.indexOf(search) === -1 && id.indexOf(search) === -1) return false;
}
// category filter — skip if item has no category field
if (category) {
if (item.category === undefined) { /* pass through */ }
else if (item.category !== category) return false;
}
// qtype filter — skip if item has no type field
if (qtype) {
if (item.type === undefined) { /* pass through */ }
else if (item.type !== qtype) return false;
}
// difficulty filter — skip if item has no difficulty field
if (difficulty) {
if (item.difficulty === undefined) { /* pass through */ }
else if (item.difficulty !== difficulty) return false;
}
// audience filter
if (audience) {
var audiences = item.audiences || [];
if (audiences.indexOf(audience) === -1) return false;
}
return true;
});
}
// ── ModuleCard (View 1) ───────────────────────────────────────────
function ModuleCard(props) {
var module = props.module;
var counts = props.counts;
var enabledCount = props.enabledCount;
var onClick = props.onClick;
return (
{module.icon}
{module.name}
{module.fullName}
{enabledCount} / {counts.total} enabled
{counts.quiz}Questions
{counts.tickets}Tickets
{counts.briefs}Briefs
{counts.curveballs}Curveballs
{counts.buzzers}Buzzers
{counts.total}Total
);
}
// ── ModuleOverviewGrid (View 1) ───────────────────────────────────
function ModuleOverviewGrid(props) {
var onSelectModule = props.onSelectModule;
var configs = props.configs;
var moduleKeys = (typeof GAME_MODULES !== 'undefined' && GAME_MODULES) ? Object.keys(GAME_MODULES) : [];
if (!moduleKeys.length) {
return (
Arena
Content Library
Browse and curate game content across all modules. Select a module to explore its questions, tickets, briefs, curveballs, and buzzers.
No modules loaded. Ensure GAME_MODULES is available.
);
}
return (
Arena
Content Library
Browse and curate game content across all modules. Select a module to explore its questions, tickets, briefs, curveballs, and buzzers.
{moduleKeys.map(function(key) {
var mod = GAME_MODULES[key];
if (!mod) return null;
var counts = getModuleContentCounts(mod.data);
var enabledCount = getModuleEnabledCount(mod.data, configs, mod.id);
return (
);
})}
);
}
// ── EventContextBar (View 2 toolbar) ─────────────────────────────
function EventContextBar(props) {
var module = props.module;
var events = props.events || [];
var state = props.state;
var dispatch = props.dispatch;
var enabledCount = props.enabledCount;
var totalCount = props.totalCount;
var wizardContext = props.wizardContext;
var onDone = props.onDone;
var translationsLoading = hasAnyI18nLoading(state && state.i18nLoading);
var isWizardMode = !!wizardContext;
return (
);
}
// ---------------------------------------------------------------------------
// QTypeGroup — collapsible group of quiz items by QType
// ---------------------------------------------------------------------------
function QTypeGroup(props) {
var qtype = props.qtype;
var items = props.items;
var includedIds = props.includedIds;
var onToggle = props.onToggle;
var onBulkSelect = props.onBulkSelect;
var onBulkDeselect = props.onBulkDeselect;
var i18nCache = props.i18nCache;
var moduleId = props.moduleId;
var moduleColor = props.moduleColor;
var categories = props.categories;
var contentType = props.contentType;
var hasEvent = props.hasEvent;
var defaultOpen = props.defaultOpen;
var _ex = React.useState(defaultOpen !== false);
var expanded = _ex[0];
var setExpanded = _ex[1];
var enabledCount = 0;
items.forEach(function(item) {
if (includedIds.indexOf(item.id) >= 0) enabledCount++;
});
var label = QTYPE_LABELS[qtype] || qtype || 'Unknown';
var qtBadgeClass = 'qt-badge ' + qtype;
return (
);
}
// ---------------------------------------------------------------------------
// ContentList — renders list of QuestionRow items
// ---------------------------------------------------------------------------
function ContentList(props) {
var items = props.items;
var includedIds = props.includedIds;
var onToggle = props.onToggle;
var onBulkSelect = props.onBulkSelect;
var onBulkDeselect = props.onBulkDeselect;
var i18nCache = props.i18nCache;
var moduleId = props.moduleId;
var moduleColor = props.moduleColor;
var categories = props.categories;
var contentType = props.contentType;
var hasEvent = props.hasEvent;
if (!items || items.length === 0) {
return
No items match your filters.
;
}
// For quiz content, group by QType with collapsible sections
if (contentType === 'quiz') {
var groups = {};
var groupOrder = [];
items.forEach(function(item) {
var t = item.type || 'text';
if (!groups[t]) {
groups[t] = [];
groupOrder.push(t);
}
groups[t].push(item);
});
return (