/* 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 (
{isWizardMode ? (
Curation mode
{module.icon} {module.name}
{wizardContext.eventName || 'Event'}
) : (
{module.icon} {module.name}
)}
{['en', 'es', 'pt'].map(function(lang) { return ( ); })}
{translationsLoading && (
Loading translations…
)}
{enabledCount} {' / ' + totalCount} enabled
{isWizardMode && ( )}
); } // ── ContentTabs ─────────────────────────────────────────────────── function ContentTabs(props) { var activeTab = props.activeTab; var counts = props.counts || {}; var onTabChange = props.onTabChange; return (
{CONTENT_TABS.map(function(tab) { return ( ); })}
); } // ── FilterRail ──────────────────────────────────────────────────── function FilterRail(props) { var filters = props.filters || {}; var onFilterChange = props.onFilterChange; var categories = props.categories || []; var showExcluded = props.showExcluded; var onToggleExcluded = props.onToggleExcluded; var onSelectAll = props.onSelectAll; var onDeselectAll = props.onDeselectAll; var activeTab = props.activeTab; function handleChange(field) { return function(e) { onFilterChange(field, e.target.value); }; } return (
{activeTab === 'quiz' && ( )}
); } // --- END PART 1 --- (Part 2 continues with QuestionRow, ContentList, ModuleContentExplorer, and main page wrapper) // --- PART 2: QuestionRow, ContentList, ModuleContentExplorer, OrgContentLibraryPage --- // Safe render helper — prevents React error #31 when rendering object fields function safeRender(val) { if (val === null || val === undefined) return '—'; if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') return String(val); if (Array.isArray(val)) return val.map(function(v) { return safeRender(v); }).join(', '); if (typeof val === 'object') return JSON.stringify(val, null, 2); return String(val); } function compactInlineText(val, maxLen) { var text = safeRender(val); var limit = maxLen || 140; text = String(text).replace(/\s+/g, ' ').trim(); if (!text) return '—'; if (text.length > limit) return text.slice(0, limit - 1) + '…'; return text; } // --------------------------------------------------------------------------- // TranslationBadge // --------------------------------------------------------------------------- function TranslationBadge(props) { var itemId = props.itemId; var i18nCache = props.i18nCache || {}; var moduleId = props.moduleId; var esLoaded = !!(i18nCache['es'] && i18nCache['es'][moduleId]); var ptLoaded = !!(i18nCache['pt'] && i18nCache['pt'][moduleId]); return (
); } // --------------------------------------------------------------------------- // QtypePreview — interactive QType renderer preview (proper component for hooks) // --------------------------------------------------------------------------- function QtypePreview(props) { var item = props.item; var tr = props.tr || function(t) { return t; }; var renderer = window.RENDERERS && window.RENDERERS[item.type]; if (!renderer || !renderer.Component) return null; var Comp = renderer.Component; var defaultVal = renderer.getDefaultValue ? renderer.getDefaultValue(item.spec) : null; var _v = React.useState(defaultVal); var previewVal = _v[0]; var setPreviewVal = _v[1]; var _l = React.useState(false); var locked = _l[0]; var setLocked = _l[1]; var _g = React.useState(false); var graded = _g[0]; var setGraded = _g[1]; function handleLockIn() { setLocked(true); setGraded(true); } function handleReset() { setPreviewVal(defaultVal); setLocked(false); setGraded(false); } return (
Player Preview
{tr(item.prompt || '')}
{!locked && } {locked && }
); } // --------------------------------------------------------------------------- // renderAnswerSection — QType-specific answer rendering // --------------------------------------------------------------------------- function renderAnswerSection(item, tr) { var spec = item.spec || {}; if (!tr) { tr = function(t) { return t; }; } // fallback passthrough switch (item.type) { case 'single_select': case 'multi_select': { var correctSet = Array.isArray(spec.correct) ? spec.correct : [spec.correct]; return (
Options
{(spec.options || []).map(function(opt) { var isCorrect = correctSet.indexOf(opt.id) >= 0; return (
{opt.id} {tr(opt.label)} {isCorrect && ✓ CORRECT}
); })}
); } case 'true_false': return (
Answer
TTrue {(spec.correct === true || spec.correct === 'true') && }
FFalse {(spec.correct === false || spec.correct === 'false') && }
); case 'cascade': return (
Cascade Stages
{(spec.stages || []).map(function(stage, i) { return (
Stage {i+1}: {tr(stage.prompt || '')}
{(stage.options || []).map(function(opt) { var isCorrect = stage.correct === opt.id; return (
{opt.id} {tr(opt.label)} {isCorrect && }
); })}
); })}
); case 'drag_match': return (
Matching Pairs
{(spec.pairs || []).map(function(pair, i) { return (
{tr(pair.left)} {tr(pair.right)}
); })}
); case 'group_sort': return (
Groups
{(spec.groups || []).map(function(group, i) { return (
{tr(group.name)}
{(group.items || []).map(function(groupItem, j) { return
{tr(groupItem)}
; })}
); })}
); case 'rank_order': return (
Correct Order
{(spec.correct_order || []).map(function(rankItem, i) { return (
{i+1} {tr(typeof rankItem === 'string' ? rankItem : rankItem.label || rankItem.id)}
); })}
); case 'hotspot': return (
Hotspot
{spec.imageUrl &&
Image: {spec.imageUrl}
} {(spec.regions || spec.hotspots || []).map(function(r, i) { return
{tr(r.label || r.description || JSON.stringify(r))}
; })}
); case 'numeric_slider': return (
Slider
Range: {spec.min} – {spec.max} {spec.unit || ''}
Correct: {spec.correct} {spec.unit || ''} (±{spec.tolerance || 0})
); case 'free_text': return (
Expected Answer
{tr(spec.sampleAnswer || spec.sample_answer || (spec.keywords || []).join(', ') || '(open-ended)')}
); case 'structured_form': return (
Form Fields
{(spec.fields || []).map(function(f, i) { return (
{tr(f.label || f.name)} {f.type || 'text'} {f.correct && {tr(f.correct)}}
); })}
); case 'buzzer': return (
Buzzer
Question: {tr(spec.question || item.prompt)}
{spec.hints && spec.hints.length > 0 && (
HINTS
{spec.hints.map(function(h, i) { return
Hint {i+1}: {tr(typeof h === 'string' ? h : h.text)}
; })}
)}
ANSWER
{tr(typeof spec.answer === 'string' ? spec.answer : (spec.answer || []).join(', '))}
{spec.judgingNotes && (
JUDGING NOTES
{tr(spec.judgingNotes)}
)}
); default: // text type (tickets, briefs, curveballs) return (
{spec.scenario && (
Scenario
{tr(spec.scenario)}
)} {spec.tasks && spec.tasks.length > 0 && (
Tasks
{spec.tasks.map(function(t, i) { return
{i+1}. {tr(typeof t === 'string' ? t : t.description || JSON.stringify(t))}
; })}
)} {spec.rubric && (
Rubric
{tr(typeof spec.rubric === 'string' ? spec.rubric : JSON.stringify(spec.rubric, null, 2))}
)}
); } } // --------------------------------------------------------------------------- // QuestionRowDetail — expanded detail panel // --------------------------------------------------------------------------- function QuestionRowDetail(props) { var item = props.item; var catColor = props.catColor; var moduleId = props.moduleId; var contentType = props.contentType || 'quiz'; var spec = item.spec || {}; var tCtx = React.useContext(CurationContext); var tLang = tCtx ? tCtx.state.language : 'en'; var tCache = tCtx ? tCtx.state.i18nCache : {}; function tr(text) { return clibTranslate(text, moduleId, tLang, tCache); } var fullPrompt = tr(item.prompt || item.title || item.text || (item.client ? item.client + ' — ' + (item.industry || '') : '') || (spec.scenario) || (spec.question) || '—'); return (
{fullPrompt}
{contentType === 'quiz' && renderAnswerSection(item, tr)} {contentType === 'quiz' && } {contentType === 'tickets' && (
{item.scenario &&
Scenario
{typeof item.scenario === 'string' ? tr(item.scenario) : safeRender(item.scenario)}
} {item.cliOutput &&
CLI Output
{safeRender(item.cliOutput)}
} {item.symptoms && item.symptoms.length > 0 &&
Symptoms
{item.symptoms.map(function(s,i){return
{typeof s === 'string' ? tr(s) : safeRender(s)}
;})}
} {item.solution &&
Solution
{typeof item.solution === 'string' ? tr(item.solution) : safeRender(item.solution)}
} {item.licensingAngle &&
Licensing Angle
{typeof item.licensingAngle === 'string' ? tr(item.licensingAngle) : safeRender(item.licensingAngle)}
}
)} {contentType === 'briefs' && (
{item.employees && 👥 {safeRender(item.employees)} employees} {item.sites && 🏢 {safeRender(item.sites)} sites} {item.endpoints && 💻 {safeRender(item.endpoints)} endpoints} {item.budget && 💰 {safeRender(item.budget)}}
{item.context &&
Context
{typeof item.context === 'string' ? tr(item.context) : safeRender(item.context)}
} {item.painPoints && item.painPoints.length > 0 &&
Pain Points
{item.painPoints.map(function(p,i){return
{typeof p === 'string' ? tr(p) : safeRender(p)}
;})}
} {item.constraints && item.constraints.length > 0 &&
Constraints
{item.constraints.map(function(c,i){return
{typeof c === 'string' ? tr(c) : safeRender(c)}
;})}
} {item.trickRequirement &&
Trick Requirement
{typeof item.trickRequirement === 'string' ? tr(item.trickRequirement) : safeRender(item.trickRequirement)}
}
)} {contentType === 'curveballs' && (
{item.context &&
Context
{typeof item.context === 'string' ? tr(item.context) : safeRender(item.context)}
} {item.strongResponse &&
Strong Response
{typeof item.strongResponse === 'string' ? tr(item.strongResponse) : safeRender(item.strongResponse)}
} {item.weakResponse &&
Weak Response
{typeof item.weakResponse === 'string' ? tr(item.weakResponse) : safeRender(item.weakResponse)}
} {item.proctorGuide &&
Proctor Guide
{safeRender(item.proctorGuide)}
}
)} {contentType === 'buzzers' && (
{item.answer &&
Answer
{typeof item.answer === 'string' ? tr(item.answer) : JSON.stringify(item.answer)}
} {item.points && typeof item.points === 'object' &&
Points
🥇 {item.points.first || 0} · 🥈 {item.points.second || 0} · 🥉 {item.points.third || 0}
} {item.proctorGuide &&
Proctor Guide
{typeof item.proctorGuide === 'string' ? tr(item.proctorGuide) : (item.proctorGuide.expectedAnswer ? tr(item.proctorGuide.expectedAnswer) : JSON.stringify(item.proctorGuide))}
}
)} {contentType === 'buzzers' && window.RENDERERS && window.RENDERERS.buzzer && ( )} {contentType === 'quiz' && spec.explanation && (
Explanation
{tr(spec.explanation)}
)}
ID: {item.id} Source: {item.source || '—'} Tag: {item.tag || '—'} Phase: {item.phase || '—'}
); } // --------------------------------------------------------------------------- // QuestionRow — summary + expandable detail // --------------------------------------------------------------------------- function QuestionRow(props) { var item = props.item; var isIncluded = props.isIncluded; var onToggle = props.onToggle; var i18nCache = props.i18nCache; var moduleId = props.moduleId; var moduleColor = props.moduleColor; var categories = props.categories; var expandRef = React.useState(false); var expanded = expandRef[0]; var setExpanded = expandRef[1]; var tCtx = React.useContext(CurationContext); var tLang = tCtx ? tCtx.state.language : 'en'; var tCache = tCtx ? tCtx.state.i18nCache : {}; function tr(text) { return clibTranslate(text, moduleId, tLang, tCache); } var spec = item.spec || {}; var contentType = props.contentType || 'quiz'; var summary = getItemSummary(item, contentType); var qtype = summary.qtype; var qtypeLabel = summary.qtypeLabel; var difficulty = summary.difficulty; var category = summary.category; var prompt = summary.label; var audiences = summary.audiences; var points = summary.points; var timer = summary.timer; var phase = item.phase || '—'; var tag = item.tag || '—'; var rowClass = 'quiz-row'; var catColor = 'var(--i4)'; if (categories && categories[category]) { catColor = categories[category]; } if (contentType === 'tickets') rowClass = 'ticket-row'; else if (contentType === 'briefs') rowClass = 'brief-row'; else if (contentType === 'curveballs') rowClass = 'curveball-row'; else if (contentType === 'buzzers') rowClass = 'buzzer-row'; function handleToggleClick(e) { e.stopPropagation(); onToggle(item.id); } function handleExpandClick(e) { e.stopPropagation(); setExpanded(!expanded); } function renderSummaryColumns() { if (contentType === 'tickets') { var ticketText = item.title || item.scenario || prompt; return ( 🎫 Ticket {compactInlineText(typeof ticketText === 'string' ? tr(ticketText) : safeRender(ticketText), 140)}
{audiences.map(function(a) { return {a}; })}
{tr(safeRender(phase))} {tr(safeRender(tag))}
); } if (contentType === 'briefs') { var briefText = (item.client || '?') + ' — ' + (item.industry || '?'); return ( 📋 Brief {compactInlineText(tr(briefText), 140)} {tr(safeRender(item.budget || '—'))} {tr(safeRender(phase))} {tr(safeRender(tag))} ); } if (contentType === 'curveballs') { var curveballText = item.text || prompt; return ( 🎱 Curveball {compactInlineText(typeof curveballText === 'string' ? tr(curveballText) : safeRender(curveballText), 180)} {tr(safeRender(phase))} {tr(safeRender(tag))} ); } if (contentType === 'buzzers') { var buzzerText = item.text || prompt; return ( 🔔 Buzzer {compactInlineText(typeof buzzerText === 'string' ? tr(buzzerText) : safeRender(buzzerText), 180)} {points} {tr(safeRender(item.type || 'buzzer'))} {tr(safeRender(tag))} ); } return ( {qtypeLabel} {difficulty} {tr(category)} {tr(prompt)}
{audiences.map(function(a) { return {a}; })}
{points} {timer}
); } return (
{renderSummaryColumns()}
{expanded && ( )}
); } // --------------------------------------------------------------------------- // 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 (
{expanded ? '▾' : '▸'} {label} {items.length + ' items'} {hasEvent && ( {enabledCount + ' enabled'} )} {hasEvent && ( )}
{expanded && (
Toggle QType Difficulty Category Prompt Audiences Points Timer
{items.map(function(item) { var isIncluded = includedIds.indexOf(item.id) >= 0; 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 (
{groupOrder.map(function(qtype, idx) { return ( ); })}
); } // Non-quiz content: flat list as before return (
{items.map(function(item) { var isIncluded = includedIds.indexOf(item.id) >= 0; return ( ); })}
); } // --------------------------------------------------------------------------- // ModuleContentExplorer — View 2: drill-down into a module's content // --------------------------------------------------------------------------- function ModuleContentExplorer(props) { var module = props.module; var events = props.events; var state = props.state; var dispatch = props.dispatch; var tabRef = React.useState('quiz'); var activeTab = tabRef[0]; var setActiveTab = tabRef[1]; var filterRef = React.useState({ search: '', category: '', qtype: '', difficulty: '', audience: '' }); var filters = filterRef[0]; var setFilters = filterRef[1]; var excludedRef = React.useState(true); var showExcluded = excludedRef[0]; var setShowExcluded = excludedRef[1]; var moduleData = module.data; var counts = getModuleContentCounts(moduleData); var allItems = getContentTypeItems(moduleData, activeTab); var includedIds = getIncludedIds(state.configs, module.id, activeTab, allItems); var categories = module.categories || {}; var categoryNames = Object.keys(categories); var filteredItems = filterContentItems(allItems, { search: filters.search, category: filters.category, qtype: filters.qtype, difficulty: filters.difficulty, audience: filters.audience, showExcluded: showExcluded, includedIds: includedIds }); var enabledCount = includedIds.length; var totalCount = allItems.length; function handleToggle(itemId) { var ids = allItems.map(function(it) { return it.id; }); dispatch({ type: 'TOGGLE_ITEM', moduleId: module.id, contentType: activeTab, itemId: itemId, allItemIds: ids }); } function handleFilterChange(key, value) { var next = Object.assign({}, filters); next[key] = value; setFilters(next); } function handleSelectAll() { var ids = allItems.map(function(it) { return it.id; }); dispatch({ type: 'SELECT_ALL', moduleId: module.id, contentType: activeTab, itemIds: ids }); } function handleDeselectAll() { dispatch({ type: 'DESELECT_ALL', moduleId: module.id, contentType: activeTab }); } function handleBulkSelect(groupItems) { var currentIds = getIncludedIds(state.configs, module.id, activeTab, allItems); var groupIds = groupItems.map(function(it) { return it.id; }); var merged = currentIds.slice(); groupIds.forEach(function(id) { if (merged.indexOf(id) === -1) merged.push(id); }); dispatch({ type: 'SELECT_ALL', moduleId: module.id, contentType: activeTab, itemIds: merged }); } function handleBulkDeselect(groupItems) { var currentIds = getIncludedIds(state.configs, module.id, activeTab, allItems); var groupIds = groupItems.map(function(it) { return it.id; }); var filtered = currentIds.filter(function(id) { return groupIds.indexOf(id) === -1; }); dispatch({ type: 'SELECT_ALL', moduleId: module.id, contentType: activeTab, itemIds: filtered }); } return (
); } // --------------------------------------------------------------------------- // ContentLibraryInner — reads context, routes between View 1 and View 2 // --------------------------------------------------------------------------- function ContentLibraryInner(props) { var ctx = React.useContext(CurationContext); var state = ctx.state; var dispatch = ctx.dispatch; var saveTimerRef = React.useRef(null); var lastSavedRef = React.useRef(null); var selectedModule = state.selectedModuleId ? ((window.GAME_MODULES || {})[state.selectedModuleId] || null) : null; // Wizard context state var _wc = React.useState(null); var wizardContext = _wc[0]; var setWizardContext = _wc[1]; // Check for preselected module or wizard context on mount React.useEffect(function() { if (window.__clibWizardContext) { var ctx = window.__clibWizardContext; window.__clibWizardContext = null; setWizardContext(ctx); if (ctx.moduleId) dispatch({ type: 'SELECT_MODULE', moduleId: ctx.moduleId }); if (ctx.eventId) dispatch({ type: 'SELECT_EVENT', eventId: String(ctx.eventId) }); } else if (window.__clibPreselect) { var moduleId = window.__clibPreselect; window.__clibPreselect = null; dispatch({ type: 'SELECT_MODULE', moduleId: moduleId }); } }, []); // Load module_configs when event selection changes React.useEffect(function() { if (!state.selectedEventId) { dispatch({ type: 'INIT_CONFIGS', configs: {} }); return; } if (typeof AdminApi === 'undefined') return; AdminApi.getModuleConfigs(state.selectedEventId).then(function(res) { var mc = (res && res.module_configs) ? res.module_configs : {}; dispatch({ type: 'INIT_CONFIGS', configs: mc }); lastSavedRef.current = JSON.stringify(mc); }).catch(function(err) { console.error('[ContentLibrary] Failed to load module configs:', err); dispatch({ type: 'INIT_CONFIGS', configs: {} }); }); }, [state.selectedEventId]); // Auto-save configs when they change (debounced 1.5s) React.useEffect(function() { if (!state.selectedEventId) return; var serialized = JSON.stringify(state.configs); if (serialized === lastSavedRef.current) return; // no change if (saveTimerRef.current) clearTimeout(saveTimerRef.current); saveTimerRef.current = setTimeout(function() { if (typeof AdminApi === 'undefined') return; AdminApi.saveModuleConfigs(state.selectedEventId, state.configs).then(function() { lastSavedRef.current = serialized; if (props.showToast) props.showToast('Curation saved', 'ok'); }).catch(function(err) { console.error('[ContentLibrary] Failed to save module configs:', err); if (props.showToast) props.showToast('Save failed', 'error'); }); }, 1500); return function() { if (saveTimerRef.current) clearTimeout(saveTimerRef.current); }; }, [state.configs, state.selectedEventId]); // Lazy-load i18n JSON when language or module changes React.useEffect(function() { if (!state.selectedModuleId || state.language === 'en') return; var langSuffix = state.language === 'es' ? 'es' : 'pt'; var cacheKey = state.selectedModuleId + '-' + langSuffix; if (state.i18nCache[cacheKey]) return; // Already cached if (state.i18nLoading[cacheKey]) return; // Already loading dispatch({ type: 'SET_I18N_LOADING', key: cacheKey, loading: true }); var url = '/modules/i18n/' + state.selectedModuleId + '-' + langSuffix + '.json'; fetch(url).then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); }).then(function(data) { dispatch({ type: 'CACHE_I18N', key: cacheKey, data: data }); dispatch({ type: 'SET_I18N_LOADING', key: cacheKey, loading: false }); }).catch(function(err) { console.warn('[ContentLibrary] Failed to load i18n:', url, err); dispatch({ type: 'SET_I18N_LOADING', key: cacheKey, loading: false }); }); }, [state.selectedModuleId, state.language]); function handleSelectModule(moduleId) { dispatch({ type: 'SELECT_MODULE', moduleId: moduleId }); } function handleDone() { if (wizardContext && wizardContext.returnPage && props.onNavigate) { setWizardContext(null); props.onNavigate(wizardContext.returnPage); } } return (
{selectedModule ? ( ) : ( )}
); } // --------------------------------------------------------------------------- // OrgContentLibraryPage — public entry point registered on window // --------------------------------------------------------------------------- window.OrgContentLibraryPage = function OrgContentLibraryPage(props) { return ( ); }; // --- END organizer-content-library.jsx ---