Roadmap to becoming a developer in 2022
 
 
 
 
 

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