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 <20>Linked:<3A> 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>
|