computer-scienceangular-roadmapbackend-roadmapblockchain-roadmapdba-roadmapdeveloper-roadmapdevops-roadmapfrontend-roadmapgo-roadmaphactoberfestjava-roadmapjavascript-roadmapnodejs-roadmappython-roadmapqa-roadmapreact-roadmaproadmapstudy-planvue-roadmapweb3-roadmap
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
294 lines
9.2 KiB
294 lines
9.2 KiB
import { useEffect, useRef, useState } from 'react'; |
|
import { QuestionsProgress } from './QuestionsProgress'; |
|
import { CheckCircle, SkipForward, Sparkles } from 'lucide-react'; |
|
import { QuestionCard } from './QuestionCard'; |
|
import { QuestionLoader } from './QuestionLoader'; |
|
import { isLoggedIn } from '../../lib/jwt'; |
|
import type { QuestionType } from '../../lib/question-group'; |
|
import { httpGet, httpPut } from '../../lib/http'; |
|
import { useToast } from '../../hooks/use-toast'; |
|
import { QuestionFinished } from './QuestionFinished'; |
|
import { Confetti } from '../Confetti'; |
|
|
|
type UserQuestionProgress = { |
|
know: string[]; |
|
dontKnow: string[]; |
|
skip: string[]; |
|
}; |
|
|
|
export type QuestionProgressType = keyof UserQuestionProgress; |
|
|
|
type QuestionsListProps = { |
|
groupId: string; |
|
questions: QuestionType[]; |
|
}; |
|
|
|
export function QuestionsList(props: QuestionsListProps) { |
|
const { questions: defaultQuestions, groupId } = props; |
|
|
|
const toast = useToast(); |
|
|
|
const [questions, setQuestions] = useState(defaultQuestions); |
|
const [isLoading, setIsLoading] = useState(true); |
|
const [showConfetti, setShowConfetti] = useState(false); |
|
const [currQuestionIndex, setCurrQuestionIndex] = useState(0); |
|
|
|
const [userProgress, setUserProgress] = useState<UserQuestionProgress>(); |
|
const containerRef = useRef<HTMLDivElement>(null); |
|
|
|
async function fetchUserProgress(): Promise< |
|
UserQuestionProgress | undefined |
|
> { |
|
if (!isLoggedIn()) { |
|
return; |
|
} |
|
|
|
const { response, error } = await httpGet<UserQuestionProgress>( |
|
`${ |
|
import.meta.env.PUBLIC_API_URL |
|
}/v1-get-user-question-progress/${groupId}`, |
|
); |
|
|
|
if (error) { |
|
toast.error(error.message || 'Error fetching user progress'); |
|
return; |
|
} |
|
|
|
return response; |
|
} |
|
|
|
async function prepareProgress() { |
|
const userProgress = await fetchUserProgress(); |
|
setUserProgress(userProgress); |
|
|
|
const knownQuestions = userProgress?.know || []; |
|
const didNotKnowQuestions = userProgress?.dontKnow || []; |
|
const skipQuestions = userProgress?.skip || []; |
|
|
|
const pendingQuestionIndex = questions.findIndex((question) => { |
|
return ( |
|
!knownQuestions.includes(question.id) && |
|
!didNotKnowQuestions.includes(question.id) && |
|
!skipQuestions.includes(question.id) |
|
); |
|
}); |
|
|
|
setCurrQuestionIndex(pendingQuestionIndex); |
|
setIsLoading(false); |
|
} |
|
|
|
async function resetProgress() { |
|
let knownQuestions = userProgress?.know || []; |
|
let didNotKnowQuestions = userProgress?.dontKnow || []; |
|
let skipQuestions = userProgress?.skip || []; |
|
|
|
if (!isLoggedIn()) { |
|
setQuestions(defaultQuestions); |
|
|
|
knownQuestions = []; |
|
didNotKnowQuestions = []; |
|
skipQuestions = []; |
|
} else { |
|
setIsLoading(true); |
|
|
|
const { response, error } = await httpPut<UserQuestionProgress>( |
|
`${ |
|
import.meta.env.PUBLIC_API_URL |
|
}/v1-reset-question-progress/${groupId}`, |
|
{ |
|
status: 'reset', |
|
}, |
|
); |
|
|
|
if (error) { |
|
toast.error(error.message || 'Error resetting progress'); |
|
return; |
|
} |
|
|
|
knownQuestions = response?.know || []; |
|
didNotKnowQuestions = response?.dontKnow || []; |
|
skipQuestions = response?.skip || []; |
|
} |
|
|
|
setCurrQuestionIndex(0); |
|
setUserProgress({ |
|
know: knownQuestions, |
|
dontKnow: didNotKnowQuestions, |
|
skip: skipQuestions, |
|
}); |
|
|
|
setIsLoading(false); |
|
} |
|
|
|
async function updateQuestionStatus( |
|
status: QuestionProgressType, |
|
questionId: string, |
|
) { |
|
setIsLoading(true); |
|
let newProgress = userProgress || { know: [], dontKnow: [], skip: [] }; |
|
|
|
if (!isLoggedIn()) { |
|
if (status === 'know') { |
|
newProgress.know.push(questionId); |
|
} else if (status == 'dontKnow') { |
|
newProgress.dontKnow.push(questionId); |
|
} else if (status == 'skip') { |
|
newProgress.skip.push(questionId); |
|
} |
|
} else { |
|
const { response, error } = await httpPut<UserQuestionProgress>( |
|
`${ |
|
import.meta.env.PUBLIC_API_URL |
|
}/v1-update-question-status/${groupId}`, |
|
{ |
|
status, |
|
questionId, |
|
questionGroupId: groupId, |
|
}, |
|
); |
|
|
|
if (error || !response) { |
|
toast.error(error?.message || 'Error marking question status'); |
|
return; |
|
} |
|
|
|
newProgress = response; |
|
} |
|
|
|
const nextQuestionIndex = currQuestionIndex + 1; |
|
|
|
setUserProgress(newProgress); |
|
setIsLoading(false); |
|
|
|
if (!nextQuestionIndex || !questions[nextQuestionIndex]) { |
|
setShowConfetti(true); |
|
} |
|
|
|
setCurrQuestionIndex(nextQuestionIndex); |
|
} |
|
|
|
useEffect(() => { |
|
prepareProgress().then(() => null); |
|
}, [questions]); |
|
|
|
const knowCount = userProgress?.know.length || 0; |
|
const dontKnowCount = userProgress?.dontKnow.length || 0; |
|
const skipCount = userProgress?.skip.length || 0; |
|
const hasProgress = knowCount > 0 || dontKnowCount > 0 || skipCount > 0; |
|
|
|
const currQuestion = questions[currQuestionIndex]; |
|
const hasFinished = !isLoading && hasProgress && currQuestionIndex === -1; |
|
|
|
return ( |
|
<div className="mb-0 gap-3 text-center sm:mb-40"> |
|
<QuestionsProgress |
|
knowCount={knowCount} |
|
didNotKnowCount={dontKnowCount} |
|
skippedCount={skipCount} |
|
totalCount={questions?.length} |
|
isLoading={isLoading} |
|
showLoginAlert={!isLoggedIn() && hasProgress} |
|
onResetClick={() => { |
|
resetProgress().finally(() => null); |
|
}} |
|
onNextClick={() => { |
|
if ( |
|
currQuestionIndex !== -1 && |
|
currQuestionIndex < questions.length - 1 |
|
) { |
|
updateQuestionStatus('skip', currQuestion.id).finally(() => null); |
|
} |
|
}} |
|
onPrevClick={() => { |
|
if (currQuestionIndex > 0) { |
|
const prevQuestion = questions[currQuestionIndex - 1]; |
|
// remove last question from the progress of the user |
|
const tempUserProgress = { |
|
know: |
|
userProgress?.know.filter((id) => id !== prevQuestion.id) || [], |
|
dontKnow: |
|
userProgress?.dontKnow.filter((id) => id !== prevQuestion.id) || |
|
[], |
|
skip: |
|
userProgress?.skip.filter((id) => id !== prevQuestion.id) || [], |
|
}; |
|
|
|
setUserProgress(tempUserProgress); |
|
setCurrQuestionIndex(currQuestionIndex - 1); |
|
} |
|
}} |
|
/> |
|
|
|
{showConfetti && containerRef.current && ( |
|
<Confetti |
|
pieces={100} |
|
element={containerRef.current} |
|
onDone={() => { |
|
setShowConfetti(false); |
|
}} |
|
/> |
|
)} |
|
|
|
<div |
|
ref={containerRef} |
|
className="relative mb-4 flex min-h-[250px] w-full overflow-hidden rounded-lg border border-gray-300 bg-white sm:min-h-[400px]" |
|
> |
|
{hasFinished && ( |
|
<QuestionFinished |
|
totalCount={questions?.length || 0} |
|
knowCount={knowCount} |
|
didNotKnowCount={dontKnowCount} |
|
skippedCount={skipCount} |
|
onReset={() => { |
|
resetProgress().finally(() => null); |
|
}} |
|
/> |
|
)} |
|
{!isLoading && currQuestion && <QuestionCard question={currQuestion} />} |
|
{isLoading && <QuestionLoader />} |
|
</div> |
|
|
|
<div |
|
className={`flex flex-col gap-1 transition-opacity duration-300 sm:flex-row sm:gap-3 ${ |
|
hasFinished ? 'opacity-0' : 'opacity-100' |
|
}`} |
|
> |
|
<button |
|
disabled={isLoading || !currQuestion} |
|
onClick={(e) => { |
|
e.stopPropagation(); |
|
e.preventDefault(); |
|
updateQuestionStatus('know', currQuestion.id).finally(() => null); |
|
}} |
|
className="flex flex-1 items-center rounded-md border border-gray-300 bg-white px-2 py-2 text-sm text-black transition-colors hover:border-black hover:bg-black hover:text-white disabled:pointer-events-none disabled:opacity-50 sm:rounded-lg sm:px-4 sm:py-3 sm:text-base" |
|
> |
|
<CheckCircle className="mr-1 h-4 text-current" /> |
|
Already Know that |
|
</button> |
|
<button |
|
onClick={() => { |
|
updateQuestionStatus('dontKnow', currQuestion.id).finally( |
|
() => null, |
|
); |
|
}} |
|
disabled={isLoading || !currQuestion} |
|
className="flex flex-1 items-center rounded-md border border-gray-300 bg-white px-2 py-2 text-sm text-black transition-colors hover:border-black hover:bg-black hover:text-white disabled:pointer-events-none disabled:opacity-50 sm:rounded-lg sm:px-4 sm:py-3 sm:text-base" |
|
> |
|
<Sparkles className="mr-1 h-4 text-current" /> |
|
Didn't Know that |
|
</button> |
|
<button |
|
onClick={() => { |
|
updateQuestionStatus('skip', currQuestion.id).finally(() => null); |
|
}} |
|
disabled={isLoading || !currQuestion} |
|
data-next-question="skip" |
|
className="flex flex-1 items-center rounded-md border border-red-600 px-2 py-2 text-sm text-red-600 hover:bg-red-600 hover:text-white disabled:pointer-events-none disabled:opacity-50 sm:rounded-lg sm:px-4 sm:py-3 sm:text-base" |
|
> |
|
<SkipForward className="mr-1 h-4" /> |
|
Skip Question |
|
</button> |
|
</div> |
|
</div> |
|
); |
|
}
|
|
|