Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
84 changes: 50 additions & 34 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;
interface QuestionFormProps {
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) {
}: QuestionFormProps) {
const isAdvanced = Boolean(question.advanced);

const handleTextChange = (text: string) => {
onUpdate({ ...question, question: text });
};
Expand Down Expand Up @@ -95,16 +96,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 +131,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 @@ -145,25 +160,26 @@ export function QuestionForm({
}}
/>
</div>
<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 (można zaznaczyć więcej niż jedną odpowiedź)
</Label>
</div>
</div>
</div>
) : null}

<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>

<h6 className="text-sm font-semibold tracking-tight">
Odpowiedzi
<span className="text-muted-foreground ml-1 font-normal">
Expand All @@ -188,7 +204,7 @@ export function QuestionForm({
});
}}
/>
{advancedMode ? (
{isAdvanced ? (
<Input
placeholder="URL zdjęcia dla odpowiedzi"
value={answer.image ?? ""}
Expand Down Expand Up @@ -261,7 +277,7 @@ export function QuestionForm({
});
}}
/>
{advancedMode ? (
{isAdvanced ? (
<Input
placeholder="URL zdjęcia dla odpowiedzi"
value={answer.image ?? ""}
Expand Down
102 changes: 56 additions & 46 deletions src/components/quiz/quiz-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,22 @@ 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,
})),
}));
type QuestionWithAdvanced = Question & { advanced?: boolean };

const sanitizeQuestions = (questions: QuestionWithAdvanced[]) =>
questions.map((q) => {
const isAdvanced = Boolean(q.advanced);
const { advanced, ...rest } = q;
return {
...rest,
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 +66,28 @@ 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[]>(() => {

const [questions, setQuestions] = useState<QuestionWithAdvanced[]>(() => {
if (initialQuiz?.questions != null && initialQuiz.questions.length > 0) {
return initialQuiz.questions;
return initialQuiz.questions.map((q) => ({
...q,
advanced:
Boolean(q.image) ||
Boolean(q.explanation) ||
q.answers.some((a) => Boolean(a.image)),
}));
}
return [
{
Expand All @@ -78,23 +98,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 +145,7 @@ export function QuizEditor({
],
image: "",
explanation: "",
advanced: advancedMode,
},
]);
setPreviousQuestionId(newId);
Expand Down Expand Up @@ -158,7 +176,7 @@ export function QuizEditor({
});
};

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

draft.title = draft.title.trim();
Expand Down Expand Up @@ -223,7 +241,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 @@ -265,27 +283,20 @@ export function QuizEditor({
}}
/>
</div>
{advancedMode ? (
<div className="space-y-2">
<div className="flex items-center gap-3">
<Checkbox
id="all-multiple"
checked={allQuestionsMultiple === true}
className={
allQuestionsMultiple === null
? "bg-yellow-500/20 dark:bg-yellow-500/30"
: ""
}
onCheckedChange={(checked) => {
setAllQuestionsMultiple(Boolean(checked));
}}
/>
<Label htmlFor="all-multiple" className="cursor-pointer">
Wielokrotny wybór (dla wszystkich pytań)
</Label>
</div>
<div className="space-y-2">
<div className="flex items-center gap-3">
<Checkbox
id="all-multiple"
checked={allQuestionsMultiple ?? "indeterminate"}
onCheckedChange={(checked) => {
setAllQuestionsMultiple(Boolean(checked));
}}
/>
<Label htmlFor="all-multiple" className="cursor-pointer">
Wielokrotny wybór (dla wszystkich pytań)
</Label>
</div>
) : null}
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<h2 className="text-sm font-semibold">Pytania</h2>
Expand All @@ -300,7 +311,6 @@ export function QuizEditor({
question={q}
onUpdate={updateQuestion}
onRemove={removeQuestion}
advancedMode={advancedMode}
/>
))}
</div>
Expand Down