mirror of
https://github.com/OHTU-German-learning-website/OHTU-German-learning-website.git
synced 2026-10-12 19:13:51 +03:00
continuing from the draft for the functionality to create multiple choice exercises with preview window
This commit is contained in:
@@ -1,5 +1,242 @@
|
||||
.textField {
|
||||
display: inline;
|
||||
field-sizing: content;
|
||||
min-width: 0;
|
||||
/* --- Global Container --- */
|
||||
.admin-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
color: #e2e8f0; /* Default text color for the page (Light Grey) */
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #ff8080; /* Lighter red for visibility on dark */
|
||||
background: #451a1a; /* Dark red background */
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ff8080;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Label styling to ensure visibility */
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
padding: 10px;
|
||||
background-color: #1e293b; /* Dark Slate Blue */
|
||||
color: #ffffff; /* White Text */
|
||||
border: 1px solid #475569; /* Grey Border */
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: #94a3b8; /* Light grey placeholder text */
|
||||
}
|
||||
|
||||
.divider {
|
||||
border: 0;
|
||||
border-top: 1px solid #334155;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
/* --- Sentence Builder Area (Top Box) --- */
|
||||
.sentence-builder-container {
|
||||
background: #1e293b; /* Dark background */
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #334155;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.sentence-flow {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
min-height: 50px;
|
||||
padding: 10px;
|
||||
background: #0f172a; /* Slightly darker inner box for the sentence area */
|
||||
border-radius: 6px;
|
||||
border: 1px solid #334155;
|
||||
}
|
||||
|
||||
.inline-block-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* The text input in the sentence flow (The "..." lines) */
|
||||
.text-block-input {
|
||||
border: none;
|
||||
border-bottom: 2px solid #64748b;
|
||||
background: transparent;
|
||||
padding: 5px;
|
||||
font-size: 1.1rem;
|
||||
color: #ffffff; /* FORCE WHITE TEXT HERE */
|
||||
width: auto;
|
||||
min-width: 60px;
|
||||
max-width: 150px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.text-block-input:focus {
|
||||
outline: none;
|
||||
border-bottom-color: var(--blue, #3b82f6);
|
||||
}
|
||||
|
||||
/* The Dropdown Placeholder (Clickable Chips) */
|
||||
.dropdown-placeholder {
|
||||
background: #334155; /* Dark grey background */
|
||||
border: 2px solid var(--blue, #3b82f6);
|
||||
color: #ffffff; /* White text */
|
||||
padding: 5px 12px;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.dropdown-placeholder:hover {
|
||||
background: #475569;
|
||||
}
|
||||
|
||||
.dropdown-placeholder.active {
|
||||
background: var(--blue, #3b82f6); /* Bright blue when selected */
|
||||
color: white;
|
||||
box-shadow: 0 0 10px rgba(59, 130, 246, 0.5);
|
||||
}
|
||||
|
||||
.delete-x {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
margin-left: -5px;
|
||||
margin-top: -15px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.delete-x:hover {
|
||||
color: #ef4444; /* Red on hover */
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* --- Editor Panel (Bottom Box - "Edit Options") --- */
|
||||
.dropdown-editor-panel {
|
||||
border: 1px solid var(--blue, #3b82f6);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
background: #1e293b; /* Match top container */
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* Headings inside the editor panel */
|
||||
.dropdown-editor-panel h3,
|
||||
.dropdown-editor-panel h4 {
|
||||
margin-top: 0;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.option-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* Inputs for the answers (Option 1, Option 2...) */
|
||||
.option-input {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
background: #0f172a; /* Very dark background */
|
||||
color: #ffffff; /* White text */
|
||||
border: 1px solid #475569;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: #94a3b8;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 0.9rem;
|
||||
color: #94a3b8; /* Light grey for hints */
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
/* --- Main Buttons --- */
|
||||
.main-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding-bottom: 50px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* --- Preview Modal --- */
|
||||
.preview-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.8); /* Darker overlay */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
background: #1e293b; /* Dark modal background */
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
border: 1px solid #334155;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.preview-sentence {
|
||||
font-size: 1.2rem;
|
||||
line-height: 2;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.preview-select {
|
||||
padding: 5px;
|
||||
font-size: 1rem;
|
||||
border: 1px solid #475569;
|
||||
border-radius: 4px;
|
||||
margin: 0 5px;
|
||||
background: #0f172a; /* Dark dropdown in preview */
|
||||
color: white;
|
||||
}
|
||||
|
||||
.preview-close {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
@@ -1,121 +1,303 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Row } from "@/components/ui/layout/container";
|
||||
import "./multichoice.css";
|
||||
|
||||
const testData = [
|
||||
{ id: "sec-1", type: "text", text: "Ich" },
|
||||
{ id: "sec-2", type: "text", text: "bin" },
|
||||
{ id: "sec-3", type: "multichoice", options: ["ein", "einer", "eine"] },
|
||||
{ id: "sec-4", type: "text", text: "Berliner" },
|
||||
{ id: "sec-5", type: "multichoice", options: ["!", "?", "."] },
|
||||
];
|
||||
export default function CreateMultichoicePage() {
|
||||
const router = useRouter();
|
||||
const [title, setTitle] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
|
||||
export default function CreateExercise() {
|
||||
const [sections, setSections] = useState(testData);
|
||||
const [selectedMultiChoice, setSelectedMultiChoice] = useState();
|
||||
const [sections, setSections] = useState([
|
||||
{ id: "1", type: "text", value: "Ich" },
|
||||
{ id: "2", type: "text", value: "bin" },
|
||||
{
|
||||
id: "3",
|
||||
type: "multichoice",
|
||||
options: ["ein", "einer", "eine"],
|
||||
correct: 0,
|
||||
},
|
||||
{ id: "4", type: "text", value: "Berliner" },
|
||||
]);
|
||||
|
||||
// Which dropdown is currently being edited? (Index of the section)
|
||||
const [activeDropdownIndex, setActiveDropdownIndex] = useState(null);
|
||||
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
// --- Actions ---
|
||||
|
||||
const addTextSection = () => {
|
||||
setSections([
|
||||
...sections,
|
||||
{ id: crypto.randomUUID(), type: "text", value: "" },
|
||||
]);
|
||||
};
|
||||
|
||||
const addMultiChoiceSection = () => {
|
||||
const newIndex = sections.length;
|
||||
setSections([
|
||||
...sections,
|
||||
{
|
||||
id: crypto.randomUUID(),
|
||||
type: "multichoice",
|
||||
options: ["Option 1", "Option 2"],
|
||||
correct: 0, // Index of the correct answer
|
||||
},
|
||||
]);
|
||||
// Automatically select the new dropdown for editing
|
||||
setActiveDropdownIndex(newIndex);
|
||||
};
|
||||
|
||||
const handleTextChange = (index, newValue) => {
|
||||
const newSections = [...sections];
|
||||
newSections[index].value = newValue;
|
||||
setSections(newSections);
|
||||
};
|
||||
|
||||
const removeSection = (index) => {
|
||||
const newSections = sections.filter((_, i) => i !== index);
|
||||
setSections(newSections);
|
||||
if (activeDropdownIndex === index) setActiveDropdownIndex(null);
|
||||
};
|
||||
|
||||
// --- Backend Submission ---
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
setError("");
|
||||
|
||||
const res = await fetch("/api/admin/exercises/multichoice", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
description,
|
||||
content: sections, // Sends the array exactly as backend expects
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
throw new Error(data.error || "Fehler beim Speichern");
|
||||
}
|
||||
|
||||
router.push("/admin/create-exercise");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>Multichoice</h1>
|
||||
{selectedMultiChoice !== undefined && (
|
||||
<DropDownEditor data={sections[selectedMultiChoice]} />
|
||||
)}
|
||||
<Row pb="xl">
|
||||
<Button
|
||||
onClick={() =>
|
||||
setSections(
|
||||
sections.concat([
|
||||
{ id: crypto.randomUUID(), type: "text", text: "" },
|
||||
])
|
||||
)
|
||||
}
|
||||
>
|
||||
New text section
|
||||
</Button>
|
||||
<div className="admin-container">
|
||||
<h1>Multiple Choice Übung erstellen</h1>
|
||||
|
||||
<Button
|
||||
onClick={() =>
|
||||
setSections(
|
||||
sections.concat([
|
||||
{
|
||||
id: crypto.randomUUID(),
|
||||
type: "multichoice",
|
||||
options: ["1", "2"],
|
||||
},
|
||||
])
|
||||
)
|
||||
}
|
||||
>
|
||||
New multichoice
|
||||
</Button>
|
||||
</Row>
|
||||
<div>
|
||||
{sections.map((s, i) => {
|
||||
if (s.type === "text") {
|
||||
return (
|
||||
<TextBox
|
||||
key={s.id}
|
||||
value={s.text}
|
||||
onChange={(e) => {
|
||||
const newString = e.target.value;
|
||||
const newList = sections.slice();
|
||||
newList[i] = { ...newList[i], text: newString };
|
||||
setSections(newList);
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
|
||||
<div className="form-group">
|
||||
<label>Titel</label>
|
||||
<input
|
||||
className="form-input"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Titel der Übung"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Beschreibung</label>
|
||||
<input
|
||||
className="form-input"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Anleitung für den Schüler"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<hr className="divider" />
|
||||
|
||||
{/* --- The Sentence Builder (Inline Flow) --- */}
|
||||
<div className="sentence-builder-container">
|
||||
<h3>Satz bauen:</h3>
|
||||
<div className="sentence-flow">
|
||||
{sections.map((section, index) => {
|
||||
if (section.type === "text") {
|
||||
return (
|
||||
<div key={section.id} className="inline-block-wrapper">
|
||||
<input
|
||||
type="text"
|
||||
value={section.value}
|
||||
onChange={(e) => handleTextChange(index, e.target.value)}
|
||||
className="text-block-input"
|
||||
placeholder="..."
|
||||
/>
|
||||
<button
|
||||
onClick={() => removeSection(index)}
|
||||
className="delete-x"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
// Multichoice Block
|
||||
const isActive = activeDropdownIndex === index;
|
||||
return (
|
||||
<div key={section.id} className="inline-block-wrapper">
|
||||
<button
|
||||
onClick={() => setActiveDropdownIndex(index)}
|
||||
className={`dropdown-placeholder ${isActive ? "active" : ""}`}
|
||||
>
|
||||
{/* Show the Correct Answer or "Dropdown" if empty */}
|
||||
{section.options[section.correct] || "Dropdown"}
|
||||
<span className="icon">▼</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => removeSection(index)}
|
||||
className="delete-x"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* --- Buttons to add new parts --- */}
|
||||
<div className="action-row">
|
||||
<Button size="sm" onClick={addTextSection}>
|
||||
+ Text
|
||||
</Button>
|
||||
<Button size="sm" onClick={addMultiChoiceSection}>
|
||||
+ Dropdown
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* --- Dropdown Editor (Only shows if a dropdown is clicked) --- */}
|
||||
{activeDropdownIndex !== null && sections[activeDropdownIndex] && (
|
||||
<div className="dropdown-editor-panel">
|
||||
<h3>Optionen bearbeiten</h3>
|
||||
<p className="hint">
|
||||
Wähle den Radio-Button für die richtige Antwort.
|
||||
</p>
|
||||
|
||||
{sections[activeDropdownIndex].options.map((opt, optIndex) => (
|
||||
<div key={optIndex} className="option-row">
|
||||
<input
|
||||
type="radio"
|
||||
name="correct-answer"
|
||||
checked={sections[activeDropdownIndex].correct === optIndex}
|
||||
onChange={() => {
|
||||
const newSections = [...sections];
|
||||
newSections[activeDropdownIndex].correct = optIndex;
|
||||
setSections(newSections);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (s.type === "multichoice") {
|
||||
return (
|
||||
<DropDownIndicator
|
||||
key={s.id}
|
||||
value={i}
|
||||
onChange={(v) => setSelectedMultiChoice(Number(v))}
|
||||
<input
|
||||
type="text"
|
||||
value={opt}
|
||||
onChange={(e) => {
|
||||
const newSections = [...sections];
|
||||
newSections[activeDropdownIndex].options[optIndex] =
|
||||
e.target.value;
|
||||
setSections(newSections);
|
||||
}}
|
||||
className="form-input option-input"
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
<button
|
||||
className="delete-btn"
|
||||
onClick={() => {
|
||||
const newSections = [...sections];
|
||||
newSections[activeDropdownIndex].options = newSections[
|
||||
activeDropdownIndex
|
||||
].options.filter((_, i) => i !== optIndex);
|
||||
// Reset correct index if needed
|
||||
if (newSections[activeDropdownIndex].correct >= optIndex) {
|
||||
newSections[activeDropdownIndex].correct = 0;
|
||||
}
|
||||
setSections(newSections);
|
||||
}}
|
||||
>
|
||||
🗑
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
const newSections = [...sections];
|
||||
newSections[activeDropdownIndex].options.push(
|
||||
`Option ${newSections[activeDropdownIndex].options.length + 1}`
|
||||
);
|
||||
setSections(newSections);
|
||||
}}
|
||||
>
|
||||
+ Weitere Option
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<hr className="divider" />
|
||||
|
||||
{/* --- Main Action Buttons --- */}
|
||||
<div className="main-actions">
|
||||
<Button onClick={handleSubmit} disabled={isSubmitting || !title}>
|
||||
{isSubmitting ? "Speichern..." : "Erstellen"}
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={() => setShowPreview(true)}>
|
||||
Vorschau
|
||||
</Button>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={() => router.push("/admin/create-exercise")}
|
||||
>
|
||||
Abbrechen
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function DropDownIndicator({ value, onChange }) {
|
||||
// An indicator to indicate that there is a dropdown here. The currently selected
|
||||
// dropdown will be edited in the DropDownEditor section.
|
||||
return (
|
||||
<input
|
||||
type="radio"
|
||||
name="dropdown"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
{/* --- Preview Modal --- */}
|
||||
{showPreview && (
|
||||
<div className="preview-overlay">
|
||||
<div className="preview-content">
|
||||
<h2>Vorschau: {title}</h2>
|
||||
<p className="preview-desc">{description}</p>
|
||||
|
||||
function DropDownEditor({ data }) {
|
||||
// This section will have an editor to edit and view the currently active dropdown.
|
||||
return (
|
||||
<div>
|
||||
<h2>Dropdown section</h2>
|
||||
<p>Options:</p>
|
||||
<ul>
|
||||
{data.options.map((v) => (
|
||||
<li key={`dropdown-${v}`}>{v}</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="preview-sentence">
|
||||
{sections.map((s, i) =>
|
||||
s.type === "text" ? (
|
||||
<span key={i} className="preview-text">
|
||||
{s.value}
|
||||
</span>
|
||||
) : (
|
||||
<select key={i} className="preview-select">
|
||||
<option value="">???</option>
|
||||
{s.options.map((opt, k) => (
|
||||
<option key={k} value={opt}>
|
||||
{opt}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="preview-close">
|
||||
<Button onClick={() => setShowPreview(false)}>Schließen</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TextBox({ value, onChange }) {
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className="textField"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export default function CreateExercise() {
|
||||
Drag und drop
|
||||
</LinkButton>
|
||||
<LinkButton href="/admin/create-exercise/multichoice">
|
||||
Multichoice
|
||||
Multiple choice
|
||||
</LinkButton>
|
||||
</Column>
|
||||
);
|
||||
|
||||
@@ -8,19 +8,14 @@ export const POST = withAuth(
|
||||
const { title, description, content } = json;
|
||||
|
||||
// Validation
|
||||
if (
|
||||
!title ||
|
||||
!content ||
|
||||
!Array.isArray(content) ||
|
||||
content.length === 0
|
||||
) {
|
||||
if (!title || !Array.isArray(content) || content.length === 0) {
|
||||
return Response.json(
|
||||
{ error: "Titel und Inhalt erforderlich." },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Prevent duplicates
|
||||
// Check for duplicates (Title)
|
||||
const exists = await DB.pool(
|
||||
"SELECT id FROM multichoice_exercises WHERE title = $1",
|
||||
[title]
|
||||
@@ -32,56 +27,94 @@ export const POST = withAuth(
|
||||
);
|
||||
}
|
||||
|
||||
// Insert into exercises table
|
||||
const created_by = 1;
|
||||
|
||||
// 1. INSERT with a temporary slug first
|
||||
//use a timestamp to ensure the temporary slug is unique for a millisecond
|
||||
const tempSlug = `temp-${Date.now()}`;
|
||||
|
||||
const exResult = await DB.pool(
|
||||
`INSERT INTO exercises (category) VALUES ('multichoice') RETURNING id`
|
||||
`INSERT INTO exercises (created_by, category, slug)
|
||||
VALUES ($1, 'multichoice', $2)
|
||||
RETURNING id`,
|
||||
[created_by, tempSlug]
|
||||
);
|
||||
const exercise_id = exResult.rows[0].id;
|
||||
|
||||
// Insert into multichoice_exercises metadata table
|
||||
// 2. UPDATE the slug to match the ID
|
||||
// This makes the URL ".../multichoice/123"
|
||||
await DB.pool(`UPDATE exercises SET slug = $1 WHERE id = $2`, [
|
||||
exercise_id.toString(),
|
||||
exercise_id,
|
||||
]);
|
||||
|
||||
// 3. Insert multichoice_exercises metadata
|
||||
const mcRes = await DB.pool(
|
||||
`INSERT INTO multichoice_exercises (exercise_id, title, exercise_description)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id`,
|
||||
`INSERT INTO multichoice_exercises (exercise_id, title, description)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id`,
|
||||
[exercise_id, title, description ?? null]
|
||||
);
|
||||
const multichoice_id = mcRes.rows[0].id;
|
||||
|
||||
// Insert content rows
|
||||
let contentOrder = 1;
|
||||
// Insert content blocks
|
||||
let order = 1;
|
||||
for (const item of content) {
|
||||
if (!item.type || !item.value) {
|
||||
return Response.json(
|
||||
{ error: "Jedes Content-Item benötigt Typ und Wert." },
|
||||
{ status: 422 }
|
||||
);
|
||||
}
|
||||
|
||||
const contentRes = await DB.pool(
|
||||
`INSERT INTO multichoice_content
|
||||
(multichoice_exercise_id, content_type, content_value, content_order, correct_answer)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id`,
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id`,
|
||||
[
|
||||
multichoice_id,
|
||||
item.type,
|
||||
item.value ?? "",
|
||||
contentOrder,
|
||||
item.value,
|
||||
order,
|
||||
item.type === "multichoice" ? item.correct : null,
|
||||
]
|
||||
);
|
||||
|
||||
const content_id = contentRes.rows[0].id;
|
||||
|
||||
// Insert options if it is a selection field
|
||||
if (item.type === "multichoice" && item.options?.length > 0) {
|
||||
for (const option of item.options) {
|
||||
// Insert options
|
||||
if (item.type === "multichoice") {
|
||||
if (!Array.isArray(item.options) || item.options.length < 2) {
|
||||
return Response.json(
|
||||
{
|
||||
error:
|
||||
"Multiple-Choice Felder benötigen mindestens zwei Optionen.",
|
||||
},
|
||||
{ status: 422 }
|
||||
);
|
||||
}
|
||||
|
||||
for (const opt of item.options) {
|
||||
await DB.pool(
|
||||
`INSERT INTO multichoice_options (multichoice_content_id, option_value)
|
||||
VALUES ($1, $2)`,
|
||||
[content_id, option]
|
||||
`INSERT INTO multichoice_options
|
||||
(multichoice_content_id, option_value)
|
||||
VALUES ($1, $2)`,
|
||||
[content_id, opt]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
contentOrder++;
|
||||
order++;
|
||||
}
|
||||
|
||||
// Return the ID as the slug so the frontend redirects correctly
|
||||
return Response.json(
|
||||
{ success: true, exerciseId: exercise_id },
|
||||
{
|
||||
success: true,
|
||||
exerciseId: exercise_id,
|
||||
slug: exercise_id.toString(),
|
||||
},
|
||||
{ status: 201 }
|
||||
);
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user