382 lines
12 KiB
HTML
382 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>JSON Question Creator</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Open+Sans&display=swap" rel="stylesheet">
|
|
<style>
|
|
|
|
|
|
h1 {
|
|
color: #FF6900;
|
|
font-weight: lighter;
|
|
}
|
|
|
|
img {
|
|
width: 30%;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Open Sans', Arial, sans-serif;
|
|
background-color: #fff;
|
|
color: #434F4F;
|
|
margin: 0;
|
|
padding: 2rem;
|
|
}
|
|
|
|
.container {
|
|
max-width: 800px;
|
|
margin: auto;
|
|
}
|
|
|
|
.question-block {
|
|
border: 2px solid #FF6900;
|
|
padding: 1rem;
|
|
margin-bottom: 1rem;
|
|
border-radius: 8px;
|
|
position: relative;
|
|
}
|
|
|
|
.data-block {
|
|
padding: 1rem;
|
|
margin-bottom: 1rem;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
input[type="text"] {
|
|
width: 100%;
|
|
padding: 0.5rem;
|
|
margin: 0.25rem 0;
|
|
box-sizing: border-box;
|
|
border-radius:4px;
|
|
border:1px solid #D9DCDC;
|
|
}
|
|
|
|
button {
|
|
background-color: #FF6900;
|
|
color: white;
|
|
border: none;
|
|
padding: 0.75rem 1.5rem;
|
|
margin-top: 1rem;
|
|
cursor: pointer;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
pre {
|
|
background: #f4f4f4;
|
|
padding: 1rem;
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
.upload-btn {
|
|
background-color: #FF6900;
|
|
color: white;
|
|
margin-top: 1rem;
|
|
padding: 0.75rem 1.5rem;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 0.8rem;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
/* place near your other button styles */
|
|
.delete-question {
|
|
background-color: #FF6900;
|
|
color: #fff;
|
|
border: none;
|
|
padding: 0.5rem 0.9rem;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
position: absolute;
|
|
right: 0.75rem;
|
|
bottom: 0.75rem;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.smallLabel {
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div class="container">
|
|
<img src="iwm_logo.svg" alt="IWM Logo">
|
|
<h1>Question Creator</h1>
|
|
|
|
<div class="data-block">
|
|
<label for="gameTitle">Titel des Spiels:</label>
|
|
<input type="text" id="gameTitle" placeholder="Bitte den Titel des Fragespiels angeben">
|
|
<br><br>
|
|
<label for="targetAge">Alter:</label>
|
|
<input type="text" id="targetAge" placeholder="Bitte das Zielalter der Schüler:innen eingeben">
|
|
<br><br>
|
|
<label for="subject">Fach:</label>
|
|
<input type="text" id="subject" placeholder="Bitte das Fach eingeben">
|
|
</div>
|
|
|
|
<br>
|
|
<br>
|
|
<div id="questionList"></div>
|
|
<button id="addQuestion">Add Question</button>
|
|
<button id="generateJson">Generate JSON</button>
|
|
<label for="importJson" class="upload-btn" style="float: right;">Import JSON</label>
|
|
<input type="file" id="importJson" accept=".json" style="display: none;">
|
|
<pre id="output"></pre>
|
|
</div>
|
|
|
|
|
|
|
|
<script>
|
|
const questionList = document.getElementById('questionList');
|
|
const addQuestionButton = document.getElementById('addQuestion');
|
|
const generateJsonButton = document.getElementById('generateJson');
|
|
const output = document.getElementById('output');
|
|
|
|
questionList.addEventListener('click', (e) => {
|
|
if (e.target.classList.contains('delete-question')) {
|
|
const block = e.target.closest('.question-block');
|
|
if (block && confirm('Diese Frage wirklich löschen?')) {
|
|
block.remove();
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
//makes sure the browser remembers the name links for files and doesn't overwrite them with null
|
|
function bindFileInput(input, type, initialName = "") {
|
|
const span = input.nextElementSibling; // the “Linked:” label
|
|
// persist current filename on the input element
|
|
input.dataset.linkedName = initialName || "";
|
|
// set initial label
|
|
span.textContent = initialName ? `Linked: ${initialName}` : `No ${type} linked`;
|
|
|
|
// live updates when user picks/clears a file
|
|
input.addEventListener('change', function () {
|
|
const name = this.files && this.files[0] ? this.files[0].name : "";
|
|
this.dataset.linkedName = name;
|
|
span.textContent = name ? `Linked: ${name}` : `No ${type} linked`;
|
|
});
|
|
}
|
|
|
|
function createQuestionBlock() {
|
|
const div = document.createElement('div');
|
|
div.className = 'question-block';
|
|
|
|
div.innerHTML = `
|
|
<label>Frage:</label>
|
|
<input type="text" class="questionText" placeholder="Bitte die Frage eingeben">
|
|
<br><br>
|
|
|
|
<label class="smallLabel">Fragen-Bild (optional):</label>
|
|
<input type="file" class="questionImage" accept="image/*">
|
|
<span class="linked-filename" style="font-size:0.8rem; color:#666; margin-left:0.5rem;">Kein Bild eingebunden</span>
|
|
<br><br>
|
|
|
|
<label class="smallLabel">Fragen-Audio (optional):</label>
|
|
<input type="file" class="questionSound" accept="audio/*">
|
|
<span class="linked-filename" style="font-size:0.8rem; color:#666; margin-left:0.5rem;">Kein Audio eingebunden</span>
|
|
<br><br>
|
|
<br><br>
|
|
|
|
<label>Antworten:</label>
|
|
${[1, 2, 3, 4, 5].map(i => `
|
|
<div style="margin-bottom: 0.75rem;">
|
|
<input type="text" class="answerText" placeholder="Antwort ${i}${i === 1 ? ' (korrekte Antwort hier einfügen)' : ''}">
|
|
<br>
|
|
<input type="file" class="answerImage" accept="image/*">
|
|
<span class="linked-filename" style="font-size:0.8rem; color:#666; margin-left:0.5rem;">Kein Bild eingebunden</span>
|
|
</div>
|
|
`).join('')}
|
|
|
|
<button type="button" class="delete-question">Frage löschen</button>
|
|
`;
|
|
|
|
bindFileInput(div.querySelector('.questionImage'), 'image', "");
|
|
bindFileInput(div.querySelector('.questionSound'), 'sound', "");
|
|
div.querySelectorAll('.answerImage').forEach(el => bindFileInput(el, 'image', ""));
|
|
|
|
questionList.appendChild(div);
|
|
}
|
|
|
|
|
|
|
|
addQuestionButton.addEventListener('click', createQuestionBlock);
|
|
|
|
function download(filename, text) {
|
|
const element = document.createElement('a');
|
|
element.setAttribute('href', 'data:text/json;charset=utf-8,' + encodeURIComponent(text));
|
|
element.setAttribute('download', filename);
|
|
element.style.display = 'none';
|
|
document.body.appendChild(element);
|
|
element.click();
|
|
document.body.removeChild(element);
|
|
}
|
|
|
|
|
|
// export handler (generate file)
|
|
generateJsonButton.addEventListener('click', () => {
|
|
const questions = [];
|
|
const blocks = document.querySelectorAll('.question-block');
|
|
|
|
blocks.forEach(block => {
|
|
const questionText = (block.querySelector('.questionText')?.value || "").trim();
|
|
|
|
// collect question media (preserve linked filenames even after reload)
|
|
const qImgEl = block.querySelector('.questionImage');
|
|
const qSndEl = block.querySelector('.questionSound');
|
|
const questionImage = (qImgEl?.files?.[0]?.name || qImgEl?.dataset?.linkedName || "").trim();
|
|
const questionSound = (qSndEl?.files?.[0]?.name || qSndEl?.dataset?.linkedName || "").trim();
|
|
|
|
// collect answers (up to 5), then FILTER OUT empties
|
|
const answerTexts = block.querySelectorAll('.answerText');
|
|
const answerImages = block.querySelectorAll('.answerImage');
|
|
|
|
const collected = [];
|
|
for (let i = 0; i < 5; i++) {
|
|
const text = (answerTexts[i]?.value || "").trim();
|
|
const aImgEl = answerImages[i];
|
|
const image = (aImgEl?.files?.[0]?.name || aImgEl?.dataset?.linkedName || "").trim();
|
|
collected.push({ text, image });
|
|
}
|
|
|
|
// keep only answers that actually have text
|
|
const answers = collected.filter(a => a.text.length > 0);
|
|
|
|
// only include this question if it has a prompt AND at least one answer
|
|
if (questionText && answers.length > 0) {
|
|
questions.push({
|
|
questionText,
|
|
questionImage,
|
|
questionSound,
|
|
answers, // variable length (1..5)
|
|
correctAnswerIndex: 0 // first provided answer remains the correct one
|
|
});
|
|
}
|
|
// else: skip empty/unfinished question blocks entirely
|
|
});
|
|
|
|
const metadata = {
|
|
title: (document.getElementById('gameTitle').value || "").trim(),
|
|
age: (document.getElementById('targetAge').value || "").trim(),
|
|
subject: (document.getElementById('subject').value || "").trim()
|
|
};
|
|
|
|
const jsonOutput = JSON.stringify({
|
|
title: metadata.title,
|
|
age: metadata.age,
|
|
subject: metadata.subject,
|
|
questions
|
|
}, null, 2);
|
|
|
|
output.textContent = jsonOutput;
|
|
|
|
let filename = metadata.title || "questions";
|
|
filename = filename.replace(/[^a-z0-9_\-]/gi, "_");
|
|
download(filename + ".json", jsonOutput);
|
|
});
|
|
|
|
|
|
|
|
|
|
//import handler
|
|
document.getElementById('importJson').addEventListener('change', function(event) {
|
|
const file = event.target.files[0];
|
|
if (!file) return;
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = function(e) {
|
|
try {
|
|
const data = JSON.parse(e.target.result);
|
|
|
|
// metadata
|
|
if (data.title) document.getElementById('gameTitle').value = data.title;
|
|
if (data.age) document.getElementById('targetAge').value = data.age;
|
|
if (data.subject) document.getElementById('subject').value = data.subject;
|
|
|
|
// questions
|
|
questionList.innerHTML = '';
|
|
if (Array.isArray(data.questions)) {
|
|
data.questions.forEach(q => {
|
|
const div = document.createElement('div');
|
|
div.className = 'question-block';
|
|
|
|
// backward-compat answers (strings -> objects)
|
|
let importedAnswers = (q.answers || []).map(a =>
|
|
(typeof a === 'string') ? { text: a, image: "" } : {
|
|
text: a.text || "",
|
|
image: a.image || ""
|
|
}
|
|
);
|
|
while (importedAnswers.length < 5) {
|
|
importedAnswers.push({ text: "", image: "" });
|
|
}
|
|
importedAnswers = importedAnswers.slice(0, 5);
|
|
|
|
const answersHTML = importedAnswers.map(a => `
|
|
<div style="margin-bottom: 0.75rem;">
|
|
<input type="text" class="answerText" value="${a.text}">
|
|
<br>
|
|
<input type="file" class="answerImage" accept="image/*">
|
|
<span class="linked-filename" style="font-size:0.8rem; color:#666; margin-left:0.5rem;">
|
|
${a.image ? `Linked: ${a.image}` : 'Kein Bild eingebunden'}
|
|
</span>
|
|
</div>
|
|
`).join('');
|
|
|
|
|
|
div.innerHTML = `
|
|
<label>Question:</label>
|
|
<input type="text" class="questionText" value="${q.questionText || ''}">
|
|
<br><br>
|
|
|
|
<label>Fragen-Bild (optional):</label>
|
|
<input type="file" class="questionImage" accept="image/*">
|
|
<span class="linked-filename" style="font-size:0.8rem; color:#666; margin-left:0.5rem;">
|
|
${q.questionImage ? `Linked: ${q.questionImage}` : 'Kein Bild eingebunden'}
|
|
</span>
|
|
<br><br>
|
|
|
|
<label>Fragen-Audio (optional):</label>
|
|
<input type="file" class="questionSound" accept="audio/*">
|
|
<span class="linked-filename" style="font-size:0.8rem; color:#666; margin-left:0.5rem;">
|
|
${q.questionSound ? `Linked: ${q.questionSound}` : 'Kein Audio eingebunden'}
|
|
</span>
|
|
<br><br>
|
|
|
|
<label>Antworten (die erste Antwort ist die korrekte):</label>
|
|
${answersHTML}
|
|
|
|
<button type="button" class="delete-question">Frage löschen</button>
|
|
`;
|
|
|
|
questionList.appendChild(div);
|
|
|
|
// bind datasets + live labels using imported names
|
|
bindFileInput(div.querySelector('.questionImage'), 'image', q.questionImage || "");
|
|
bindFileInput(div.querySelector('.questionSound'), 'sound', q.questionSound || "");
|
|
|
|
const aImgEls = div.querySelectorAll('.answerImage');
|
|
importedAnswers.forEach((a, i) => {
|
|
bindFileInput(aImgEls[i], 'image', a.image || "");
|
|
});
|
|
});
|
|
}
|
|
} catch (err) {
|
|
alert('Invalid JSON file.');
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
});
|
|
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|