Roadmap to becoming a developer in 2022
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

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>
);
}