computer-scienceangular-roadmapbackend-roadmapblockchain-roadmapdba-roadmapdeveloper-roadmapdevops-roadmapfrontend-roadmapgo-roadmaphactoberfestjava-roadmapjavascript-roadmapnodejs-roadmappython-roadmapqa-roadmapreact-roadmaproadmapstudy-planvue-roadmapweb3-roadmap
193 lines
6.0 KiB
193 lines
6.0 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 { Confetti } from '../Confetti'; |
|
import { httpGet, httpPut } from '../../lib/http'; |
|
import { useToast } from '../../hooks/use-toast'; |
|
|
|
type UserQuestionProgress = { |
|
know: string[]; |
|
didNotKnow: string[]; |
|
}; |
|
|
|
type QuestionsListProps = { |
|
groupId: string; |
|
questions: QuestionType[]; |
|
}; |
|
|
|
export function QuestionsList(props: QuestionsListProps) { |
|
const { questions: defaultQuestions, groupId } = props; |
|
|
|
const toast = useToast(); |
|
|
|
const [isLoading, setIsLoading] = useState(true); |
|
const [isUpdatingStatus, setIsUpdatingStatus] = useState(false); |
|
|
|
const [confettiEl, setConfettiEl] = useState<HTMLElement | null>(null); |
|
|
|
const [questions, setQuestions] = useState<QuestionType[]>(); |
|
const [pendingQuestions, setPendingQuestions] = useState<QuestionType[]>([]); |
|
|
|
const [userProgress, setUserProgress] = useState<UserQuestionProgress>(); |
|
const alreadyKnowRef = useRef<HTMLButtonElement>(null); |
|
const didNotKnowRef = useRef<HTMLButtonElement>(null); |
|
|
|
function showConfetti(el: HTMLElement | null) { |
|
// If confetti is already showing, remove that first |
|
if (confettiEl) { |
|
setConfettiEl(null); |
|
} |
|
|
|
window.setTimeout(() => { |
|
setConfettiEl(el); |
|
}, 0); |
|
} |
|
|
|
async function fetchUserProgress(): Promise< |
|
UserQuestionProgress | undefined |
|
> { |
|
if (!isLoggedIn()) { |
|
return; |
|
} |
|
|
|
const { response, error } = await httpGet<UserQuestionProgress>( |
|
`/v1-get-user-question-progress/${groupId}` |
|
); |
|
|
|
if (error) { |
|
toast.error(error.message || 'Error fetching user progress'); |
|
return; |
|
} |
|
|
|
return response; |
|
} |
|
|
|
async function loadQuestions() { |
|
const userProgress = await fetchUserProgress(); |
|
setUserProgress(userProgress); |
|
|
|
const knownQuestions = userProgress?.know || []; |
|
const didNotKnowQuestions = userProgress?.didNotKnow || []; |
|
|
|
const pendingQuestions = defaultQuestions.filter((question) => { |
|
return ( |
|
!knownQuestions.includes(question.id) && |
|
!didNotKnowQuestions.includes(question.id) |
|
); |
|
}); |
|
|
|
// Shuffle and set pending questions |
|
setPendingQuestions(pendingQuestions.sort(() => Math.random() - 0.5)); |
|
setQuestions(defaultQuestions); |
|
|
|
setIsLoading(false); |
|
} |
|
|
|
async function updateQuestionStatus( |
|
status: 'know' | 'dontKnow', |
|
questionId: string |
|
) { |
|
setIsUpdatingStatus(true); |
|
let newProgress = userProgress || { know: [], didNotKnow: [] }; |
|
|
|
if (!isLoggedIn()) { |
|
if (status === 'know') { |
|
newProgress.know.push(questionId); |
|
} else { |
|
newProgress.didNotKnow.push(questionId); |
|
} |
|
} else { |
|
const { response, error } = await httpPut<UserQuestionProgress>( |
|
`/v1-update-question-status/${groupId}`, |
|
{ |
|
status, |
|
questionId, |
|
questionGroupId: groupId, |
|
} |
|
); |
|
|
|
if (error || !response) { |
|
toast.error(error?.message || 'Error marking question status'); |
|
return; |
|
} |
|
|
|
newProgress = response; |
|
} |
|
|
|
setUserProgress(newProgress); |
|
setPendingQuestions(pendingQuestions.filter((q) => q.id !== questionId)); |
|
setIsUpdatingStatus(false); |
|
} |
|
|
|
useEffect(() => { |
|
loadQuestions().then(() => null); |
|
}, [defaultQuestions]); |
|
|
|
const knownCount = userProgress?.know.length || 0; |
|
const didNotKnowCount = userProgress?.didNotKnow.length || 0; |
|
const hasProgress = knownCount > 0 || didNotKnowCount > 0; |
|
|
|
const currQuestion = pendingQuestions[0]; |
|
|
|
return ( |
|
<div className="mb-40 gap-3 text-center"> |
|
<Confetti |
|
element={confettiEl} |
|
onDone={() => { |
|
setConfettiEl(null); |
|
}} |
|
/> |
|
|
|
<QuestionsProgress |
|
isLoading={isLoading} |
|
showLoginAlert={!isLoggedIn() && hasProgress} |
|
/> |
|
|
|
<div className="relative mb-4 flex min-h-[400px] w-full overflow-hidden rounded-lg border border-gray-300 bg-white"> |
|
{!isLoading && <QuestionCard question={currQuestion} />} |
|
{isLoading && <QuestionLoader />} |
|
</div> |
|
|
|
<div className="flex flex-col gap-3 sm:flex-row"> |
|
<button |
|
disabled={isLoading || isUpdatingStatus} |
|
ref={alreadyKnowRef} |
|
onClick={(e) => { |
|
showConfetti(alreadyKnowRef.current); |
|
updateQuestionStatus('know', currQuestion.id).finally(() => null); |
|
}} |
|
className="flex flex-1 items-center rounded-xl border border-gray-300 bg-white py-3 px-4 text-black transition-colors hover:border-black hover:bg-black hover:text-white disabled:pointer-events-none disabled:opacity-50" |
|
> |
|
<CheckCircle className="mr-1 h-4 text-current" /> |
|
Already Know that |
|
</button> |
|
<button |
|
ref={didNotKnowRef} |
|
onClick={() => { |
|
showConfetti(didNotKnowRef.current); |
|
updateQuestionStatus('dontKnow', currQuestion.id).finally( |
|
() => null |
|
); |
|
}} |
|
disabled={isLoading || isUpdatingStatus} |
|
className="flex flex-1 items-center rounded-xl border border-gray-300 bg-white py-3 px-4 text-black transition-colors hover:border-black hover:bg-black hover:text-white disabled:pointer-events-none disabled:opacity-50" |
|
> |
|
<Sparkles className="mr-1 h-4 text-current" /> |
|
Didn't Know that |
|
</button> |
|
<button |
|
disabled={isLoading || isUpdatingStatus} |
|
data-next-question="skip" |
|
className="flex flex-1 items-center rounded-xl border border-red-600 p-3 text-red-600 hover:bg-red-600 hover:text-white disabled:pointer-events-none disabled:opacity-50" |
|
> |
|
<SkipForward className="mr-1 h-4" /> |
|
Skip Question |
|
</button> |
|
</div> |
|
</div> |
|
); |
|
}
|
|
|