Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 52 additions & 23 deletions src/components/quiz/question-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,23 @@ import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import type { Answer, Question } from "@/types/quiz.ts";

interface questionFormProps {
question: Question;
onUpdate: (updatedQuestion: Question) => void;
question: Question & { advanced?: boolean };
onUpdate: (updatedQuestion: Question & { advanced?: boolean }) => void;
onRemove: (id: number) => void;
advancedMode?: boolean;
}

export function QuestionForm({
question,
onUpdate,
onRemove,
advancedMode = false,
}: questionFormProps) {
const isAdvanced = Boolean(question.advanced);

const handleTextChange = (text: string) => {
onUpdate({ ...question, question: text });
};
Expand All @@ -34,8 +35,6 @@ export function QuestionForm({
};

const handleMultipleChange = (multiple: boolean) => {
// If switching from multiple choice to single choice and there are multiple correct answers,
// keep only the first correct answer
if (
!multiple &&
question.multiple &&
Expand All @@ -53,13 +52,11 @@ export function QuestionForm({
};

const addAnswer = () => {
const newAnswer = { answer: "", correct: false, image: "" };
const newAnswer = { answer: "", correct: false, image: "" } as Answer;
onUpdate({ ...question, answers: [...question.answers, newAnswer] });
};

const updateAnswer = (index: number, updatedAnswer: Answer) => {
// If this is a single-choice question and we're marking an answer as correct,
// unmark all other answers as correct
if (!question.multiple && updatedAnswer.correct) {
const updatedAnswers = question.answers.map((a, index_) => ({
...a,
Expand Down Expand Up @@ -95,16 +92,30 @@ export function QuestionForm({
>
Pytanie {question.id}
</Label>
<Button
variant="ghost"
size="icon"
className="text-muted-foreground hover:bg-destructive/10 hover:text-destructive h-7 w-7 shrink-0 rounded-full transition"
onClick={() => {
onRemove(question.id);
}}
>
<Trash2 className="size-4" />
</Button>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
<Switch
id={`advanced-question-${question.id.toString()}`}
checked={isAdvanced}
onCheckedChange={(checked) => {
onUpdate({ ...question, advanced: checked });
}}
/>
<span className="text-muted-foreground text-xs">
Zaawansowane
</span>
</div>
<Button
variant="ghost"
size="icon"
className="text-muted-foreground hover:bg-destructive/10 hover:text-destructive h-7 w-7 shrink-0 rounded-full transition"
onClick={() => {
onRemove(question.id);
}}
>
<Trash2 className="size-4" />
</Button>
</div>
</div>
<Textarea
id={`question-text-${question.id.toString()}`}
Expand All @@ -116,7 +127,7 @@ export function QuestionForm({
/>
</div>

{advancedMode ? (
{isAdvanced ? (
<div className="space-y-4">
<div className="flex flex-col gap-4">
<div className="space-y-2">
Expand Down Expand Up @@ -162,7 +173,25 @@ export function QuestionForm({
</div>
</div>
</div>
) : null}
) : (
<div className="space-y-2">
<div className="flex items-center gap-2">
<Checkbox
id={`multiple-choice-${question.id.toString()}`}
checked={question.multiple}
onCheckedChange={(checked) => {
handleMultipleChange(Boolean(checked));
}}
/>
<Label
htmlFor={`multiple-choice-${question.id.toString()}`}
className="cursor-pointer"
>
Wielokrotny wybór
</Label>
</div>
</div>
)}

<h6 className="text-sm font-semibold tracking-tight">
Odpowiedzi
Expand All @@ -188,7 +217,7 @@ export function QuestionForm({
});
}}
/>
{advancedMode ? (
{isAdvanced ? (
<Input
placeholder="URL zdjęcia dla odpowiedzi"
value={answer.image ?? ""}
Expand Down Expand Up @@ -261,7 +290,7 @@ export function QuestionForm({
});
}}
/>
{advancedMode ? (
{isAdvanced ? (
<Input
placeholder="URL zdjęcia dla odpowiedzi"
value={answer.image ?? ""}
Expand Down
78 changes: 46 additions & 32 deletions src/components/quiz/quiz-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,19 @@ interface QuizEditorProps {
saving?: boolean;
}

// Utility to strip advanced fields when advancedMode is disabled
const sanitizeQuestions = (questions: Question[], advancedMode: boolean) =>
questions.map((q) => ({
...q,
image: advancedMode ? q.image : undefined,
explanation: advancedMode ? q.explanation : undefined,
answers: q.answers.map((a) => ({
...a,
image: advancedMode ? a.image : undefined,
})),
}));
const sanitizeQuestions = (questions: (Question & { advanced?: boolean })[]) =>
questions.map((q) => {
const isAdvanced = Boolean(q.advanced);
return {
...q,
image: isAdvanced ? q.image : undefined,
explanation: isAdvanced ? q.explanation : undefined,
answers: q.answers.map((a) => ({
...a,
image: isAdvanced ? a.image : undefined,
})),
};
});

const scrollToBottom = () => {
window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" });
Expand All @@ -61,13 +63,33 @@ export function QuizEditor({
onSaveAndClose,
saving = false,
}: QuizEditorProps) {
const initialAdvancedDefault =
initialQuiz?.questions?.some(
(q) =>
Boolean(q.image) ||
Boolean(q.explanation) ||
q.answers.some((a) => Boolean(a.image)),
) ?? false;

const [title, setTitle] = useState(initialQuiz?.title ?? "");
const [description, setDescription] = useState(
initialQuiz?.description ?? "",
);
const [questions, setQuestions] = useState<Question[]>(() => {

type Q = Question & { advanced?: boolean };

const [questions, setQuestions] = useState<Q[]>(() => {
if (initialQuiz?.questions != null && initialQuiz.questions.length > 0) {
return initialQuiz.questions;
return initialQuiz.questions.map((q) => ({
...q,

advanced:
Boolean((q as unknown as Q).advanced) ||
Boolean(q.image) ||
Boolean(q.explanation) ||
q.answers.some((a) => Boolean(a.image)) ||
initialAdvancedDefault,
}));
}
return [
{
Expand All @@ -78,23 +100,20 @@ export function QuizEditor({
{ answer: "", correct: false },
{ answer: "", correct: false },
],
image: "",
explanation: "",
advanced: initialAdvancedDefault,
},
];
});

const [error, setError] = useState<string | null>(null);
const [advancedMode, setAdvancedMode] = useState(
initialQuiz?.questions?.some(
(q) =>
Boolean(q.image) ||
Boolean(q.explanation) ||
q.answers.some((a) => Boolean(a.image)),
) ?? false,
);
const [advancedMode, setAdvancedMode] = useState(initialAdvancedDefault);

const [previousQuestionId, setPreviousQuestionId] = useState<number>(() =>
questions.reduce((max, q) => Math.max(q.id, max), 0),
);

// all questions multiple toggle state (true / false / mixed null)
const allQuestionsMultiple: boolean | null = useMemo(() => {
if (questions.length === 0) {
return null;
Expand Down Expand Up @@ -128,6 +147,7 @@ export function QuizEditor({
],
image: "",
explanation: "",
advanced: advancedMode,
},
]);
setPreviousQuestionId(newId);
Expand Down Expand Up @@ -158,7 +178,7 @@ export function QuizEditor({
});
};

const updateQuestion = (updated: Question) => {
const updateQuestion = (updated: Q) => {
setQuestions((previous) =>
previous.map((q) => (q.id === updated.id ? updated : q)),
);
Expand All @@ -171,7 +191,7 @@ export function QuizEditor({
const draft = {
title,
description,
questions: sanitizeQuestions(questions, advancedMode),
questions: sanitizeQuestions(questions),
};

draft.title = draft.title.trimEnd();
Expand Down Expand Up @@ -223,7 +243,7 @@ export function QuizEditor({
</div>
<div className="bg-muted/40 flex items-center justify-between gap-3 rounded-md border px-3 py-2">
<Label htmlFor="advanced-mode" className="cursor-pointer">
Tryb zaawansowany
Tryb zaawansowany (domyślny dla nowych pytań)
</Label>
<Switch
id="advanced-mode"
Expand Down Expand Up @@ -271,13 +291,8 @@ export function QuizEditor({
<Checkbox
id="all-multiple"
checked={allQuestionsMultiple === true}
className={
allQuestionsMultiple === null
? "bg-yellow-500/20 dark:bg-yellow-500/30"
: ""
}
onCheckedChange={(checked) => {
setAllQuestionsMultiple(Boolean(checked));
setAllQuestionsMultiple(checked as boolean);
}}
/>
<Label htmlFor="all-multiple" className="cursor-pointer">
Expand All @@ -300,7 +315,6 @@ export function QuizEditor({
question={q}
onUpdate={updateQuestion}
onRemove={removeQuestion}
advancedMode={advancedMode}
/>
))}
</div>
Expand Down
Loading