Browse Source

feat: implement interactive test question flow component with local storage persistence and progress tracking

front-test-2
ghorbani 2 weeks ago
parent
commit
99c0856c7b
  1. 84
      src/components/Componentes/test-questions-flow.tsx

84
src/components/Componentes/test-questions-flow.tsx

@ -2,7 +2,8 @@
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { GoArrowLeft } from "react-icons/go";
import { GoArrowLeft, GoArrowRight } from "react-icons/go";
import { useI18n } from "@/translations/provider";
import Button from "./button";
import { ExplanationUiFont } from "./explanation-ui-font";
@ -73,24 +74,24 @@ function formatOptionLabel(str: string): string {
// Remove trailing numbers in parentheses, e.g., (1) or (۱)
result = result.replace(/\s*\([\d۱۲۳۴۵۶۷۸۹۰]+\)\s*$/, "");
if (result.startsWith(')')) {
if (result.startsWith(")")) {
result = result.slice(1).trim();
}
if (result.endsWith('(')) {
if (result.endsWith("(")) {
result = result.slice(0, -1).trim();
}
let openCount = 0;
let closeCount = 0;
for (let i = 0; i < result.length; i++) {
if (result[i] === '(') openCount++;
else if (result[i] === ')') closeCount++;
if (result[i] === "(") openCount++;
else if (result[i] === ")") closeCount++;
}
if (openCount > closeCount) {
result = result + ')'.repeat(openCount - closeCount);
result = result + ")".repeat(openCount - closeCount);
} else if (closeCount > openCount) {
result = '('.repeat(closeCount - openCount) + result;
result = "(".repeat(closeCount - openCount) + result;
}
if (result.length > 0) {
@ -113,6 +114,7 @@ export default function TestQuestionsFlow({
draftStorageKey,
}: TestQuestionsFlowProps) {
const router = useRouter();
const { locale } = useI18n();
const [currentIndex, setCurrentIndex] = useState(
() => getStoredDraft(draftStorageKey, questions.length).currentIndex,
);
@ -121,6 +123,41 @@ export default function TestQuestionsFlow({
);
const [isSubmitting, setIsSubmitting] = useState(false);
const isTargetTest =
!!draftStorageKey &&
(draftStorageKey.includes("personality_test") ||
draftStorageKey.includes("glasser_5_needs_test"));
const [maxVisitedIndex, setMaxVisitedIndex] = useState(() => {
const initialIndex = getStoredDraft(
draftStorageKey,
questions.length,
).currentIndex;
const initialAnswers = getStoredDraft(
draftStorageKey,
questions.length,
).answers;
let highestAnswered = -1;
for (let i = 0; i < questions.length; i++) {
if (initialAnswers[questions[i].id] !== undefined) {
highestAnswered = i;
}
}
const furthestReached =
highestAnswered !== -1
? Math.min(highestAnswered + 1, questions.length - 1)
: 0;
return Math.max(initialIndex, furthestReached);
});
useEffect(() => {
if (Object.keys(answers).length === 0) {
setMaxVisitedIndex(currentIndex);
} else if (currentIndex > maxVisitedIndex) {
setMaxVisitedIndex(currentIndex);
}
}, [currentIndex, answers, maxVisitedIndex]);
const currentQuestion = questions[currentIndex] ?? questions[0];
const totalQuestions = questions.length;
const isLastQuestion = currentIndex === totalQuestions - 1;
@ -157,6 +194,10 @@ export default function TestQuestionsFlow({
setCurrentIndex((prev) => Math.max(prev - 1, 0));
};
const handleNext = () => {
setCurrentIndex((prev) => Math.min(prev + 1, totalQuestions - 1));
};
const handleFinishSubmit = async () => {
if (isSubmitting) return;
setIsSubmitting(true);
@ -187,7 +228,9 @@ export default function TestQuestionsFlow({
return null;
}
const progressPercent = ((currentIndex + 1) / totalQuestions) * 100;
const progressPercent =
(((isTargetTest ? maxVisitedIndex : currentIndex) + 1) / totalQuestions) *
100;
const areAllQuestionsAnswered = questions.every(
(question) => answers[question.id] !== undefined,
);
@ -286,7 +329,9 @@ export default function TestQuestionsFlow({
(() => {
const parts = option.label.split(" - ");
const title = formatOptionLabel(parts[0]);
const description = formatOptionLabel(parts.slice(1).join(" - "));
const description = formatOptionLabel(
parts.slice(1).join(" - "),
);
return (
<span className="flex flex-col gap-1 text-start flex-1">
<span className="font-bold">{title}</span>
@ -303,7 +348,9 @@ export default function TestQuestionsFlow({
);
})()
) : (
<span className="flex-1">{formatOptionLabel(option.label)}</span>
<span className="flex-1">
{formatOptionLabel(option.label)}
</span>
)}
</button>
);
@ -323,8 +370,23 @@ export default function TestQuestionsFlow({
className="flex items-center gap-1.5 group-14 font-semibold text-[#4A4A4A] disabled:opacity-30 disabled:pointer-events-none transition-opacity py-2 px-1"
>
<GoArrowLeft className="size-5" />
<span>{previousLabel}</span>
<span>{locale === "fa" ? "قبلی" : previousLabel}</span>
</button>
{/* Next Button */}
{isTargetTest &&
!isLastQuestion &&
selectedValue !== undefined &&
currentIndex < maxVisitedIndex && (
<button
type="button"
onClick={handleNext}
className="flex items-center gap-1.5 group-14 font-semibold text-[#4A4A4A] transition-opacity py-2 px-1"
>
<span>{locale === "fa" ? "بعدی" : "Next"}</span>
<GoArrowRight className="size-5" />
</button>
)}
{/* Finish Button on Last Question */}
{isLastQuestion ? (

Loading…
Cancel
Save