mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-28 11:39:52 +02:00
Show user progress
This commit is contained in:
@@ -12,6 +12,7 @@ import { useToast } from '../../hooks/use-toast';
|
|||||||
type UserQuestionProgress = {
|
type UserQuestionProgress = {
|
||||||
know: string[];
|
know: string[];
|
||||||
didNotKnow: string[];
|
didNotKnow: string[];
|
||||||
|
skipped: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type QuestionsListProps = {
|
type QuestionsListProps = {
|
||||||
@@ -20,13 +21,11 @@ type QuestionsListProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function QuestionsList(props: QuestionsListProps) {
|
export function QuestionsList(props: QuestionsListProps) {
|
||||||
const { questions: defaultQuestions, groupId } = props;
|
const { questions: unshuffledQuestions, groupId } = props;
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isUpdatingStatus, setIsUpdatingStatus] = useState(false);
|
|
||||||
|
|
||||||
const [confettiEl, setConfettiEl] = useState<HTMLElement | null>(null);
|
const [confettiEl, setConfettiEl] = useState<HTMLElement | null>(null);
|
||||||
|
|
||||||
const [questions, setQuestions] = useState<QuestionType[]>();
|
const [questions, setQuestions] = useState<QuestionType[]>();
|
||||||
@@ -72,33 +71,37 @@ export function QuestionsList(props: QuestionsListProps) {
|
|||||||
|
|
||||||
const knownQuestions = userProgress?.know || [];
|
const knownQuestions = userProgress?.know || [];
|
||||||
const didNotKnowQuestions = userProgress?.didNotKnow || [];
|
const didNotKnowQuestions = userProgress?.didNotKnow || [];
|
||||||
|
const skippedQuestions = userProgress?.skipped || [];
|
||||||
|
|
||||||
const pendingQuestions = defaultQuestions.filter((question) => {
|
const pendingQuestions = unshuffledQuestions.filter((question) => {
|
||||||
return (
|
return (
|
||||||
!knownQuestions.includes(question.id) &&
|
!knownQuestions.includes(question.id) &&
|
||||||
!didNotKnowQuestions.includes(question.id)
|
!didNotKnowQuestions.includes(question.id) &&
|
||||||
|
!skippedQuestions.includes(question.id)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Shuffle and set pending questions
|
// Shuffle and set pending questions
|
||||||
setPendingQuestions(pendingQuestions.sort(() => Math.random() - 0.5));
|
setPendingQuestions(pendingQuestions.sort(() => Math.random() - 0.5));
|
||||||
setQuestions(defaultQuestions);
|
setQuestions(unshuffledQuestions);
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateQuestionStatus(
|
async function updateQuestionStatus(
|
||||||
status: 'know' | 'dontKnow',
|
status: 'know' | 'dontKnow' | 'skip',
|
||||||
questionId: string
|
questionId: string
|
||||||
) {
|
) {
|
||||||
setIsUpdatingStatus(true);
|
setIsLoading(true);
|
||||||
let newProgress = userProgress || { know: [], didNotKnow: [] };
|
let newProgress = userProgress || { know: [], didNotKnow: [], skipped: [] };
|
||||||
|
|
||||||
if (!isLoggedIn()) {
|
if (!isLoggedIn()) {
|
||||||
if (status === 'know') {
|
if (status === 'know') {
|
||||||
newProgress.know.push(questionId);
|
newProgress.know.push(questionId);
|
||||||
} else {
|
} else if (status == 'dontKnow') {
|
||||||
newProgress.didNotKnow.push(questionId);
|
newProgress.didNotKnow.push(questionId);
|
||||||
|
} else if (status == 'skip') {
|
||||||
|
newProgress.skipped.push(questionId);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const { response, error } = await httpPut<UserQuestionProgress>(
|
const { response, error } = await httpPut<UserQuestionProgress>(
|
||||||
@@ -120,16 +123,17 @@ export function QuestionsList(props: QuestionsListProps) {
|
|||||||
|
|
||||||
setUserProgress(newProgress);
|
setUserProgress(newProgress);
|
||||||
setPendingQuestions(pendingQuestions.filter((q) => q.id !== questionId));
|
setPendingQuestions(pendingQuestions.filter((q) => q.id !== questionId));
|
||||||
setIsUpdatingStatus(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadQuestions().then(() => null);
|
loadQuestions().then(() => null);
|
||||||
}, [defaultQuestions]);
|
}, [unshuffledQuestions]);
|
||||||
|
|
||||||
const knownCount = userProgress?.know.length || 0;
|
const knownCount = userProgress?.know.length || 0;
|
||||||
const didNotKnowCount = userProgress?.didNotKnow.length || 0;
|
const didNotKnowCount = userProgress?.didNotKnow.length || 0;
|
||||||
const hasProgress = knownCount > 0 || didNotKnowCount > 0;
|
const skippedCount = userProgress?.skipped.length || 0;
|
||||||
|
const hasProgress = knownCount > 0 || didNotKnowCount > 0 || skippedCount > 0;
|
||||||
|
|
||||||
const currQuestion = pendingQuestions[0];
|
const currQuestion = pendingQuestions[0];
|
||||||
|
|
||||||
@@ -143,6 +147,10 @@ export function QuestionsList(props: QuestionsListProps) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<QuestionsProgress
|
<QuestionsProgress
|
||||||
|
knowCount={knownCount}
|
||||||
|
didNotKnowCount={didNotKnowCount}
|
||||||
|
skippedCount={skippedCount}
|
||||||
|
totalCount={unshuffledQuestions?.length || questions?.length}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
showLoginAlert={!isLoggedIn() && hasProgress}
|
showLoginAlert={!isLoggedIn() && hasProgress}
|
||||||
/>
|
/>
|
||||||
@@ -154,7 +162,7 @@ export function QuestionsList(props: QuestionsListProps) {
|
|||||||
|
|
||||||
<div className="flex flex-col gap-3 sm:flex-row">
|
<div className="flex flex-col gap-3 sm:flex-row">
|
||||||
<button
|
<button
|
||||||
disabled={isLoading || isUpdatingStatus}
|
disabled={isLoading}
|
||||||
ref={alreadyKnowRef}
|
ref={alreadyKnowRef}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
showConfetti(alreadyKnowRef.current);
|
showConfetti(alreadyKnowRef.current);
|
||||||
@@ -173,14 +181,17 @@ export function QuestionsList(props: QuestionsListProps) {
|
|||||||
() => null
|
() => null
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
disabled={isLoading || isUpdatingStatus}
|
disabled={isLoading}
|
||||||
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"
|
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" />
|
<Sparkles className="mr-1 h-4 text-current" />
|
||||||
Didn't Know that
|
Didn't Know that
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
disabled={isLoading || isUpdatingStatus}
|
onClick={() => {
|
||||||
|
updateQuestionStatus('skip', currQuestion.id).finally(() => null);
|
||||||
|
}}
|
||||||
|
disabled={isLoading}
|
||||||
data-next-question="skip"
|
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"
|
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"
|
||||||
>
|
>
|
||||||
|
@@ -1,43 +1,72 @@
|
|||||||
import { CheckCircle, RotateCcw, Sparkles } from 'lucide-react';
|
import { CheckCircle, RotateCcw, SkipForward, Sparkles } from 'lucide-react';
|
||||||
import { showLoginPopup } from '../../lib/popup';
|
import { showLoginPopup } from '../../lib/popup';
|
||||||
|
|
||||||
type QuestionsProgressProps = {
|
type QuestionsProgressProps = {
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
showLoginAlert?: boolean;
|
showLoginAlert?: boolean;
|
||||||
|
knowCount?: number;
|
||||||
|
didNotKnowCount?: number;
|
||||||
|
totalCount?: number;
|
||||||
|
skippedCount?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function QuestionsProgress(props: QuestionsProgressProps) {
|
export function QuestionsProgress(props: QuestionsProgressProps) {
|
||||||
const { showLoginAlert, isLoading = false } = props;
|
const {
|
||||||
|
showLoginAlert,
|
||||||
|
isLoading = false,
|
||||||
|
knowCount = 0,
|
||||||
|
didNotKnowCount = 0,
|
||||||
|
totalCount = 0,
|
||||||
|
skippedCount = 0,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const totalSolved = knowCount + didNotKnowCount + skippedCount;
|
||||||
|
const donePercentage = (totalSolved / totalCount) * 100;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-5 overflow-hidden rounded-lg border border-gray-300 bg-white p-6">
|
<div className="mb-5 overflow-hidden rounded-lg border border-gray-300 bg-white p-6">
|
||||||
<div className="mb-3 flex items-center text-gray-600">
|
<div className="mb-3 flex items-center text-gray-600">
|
||||||
<div className="relative w-full flex-1 rounded-xl bg-gray-200 p-1">
|
<div className="relative w-full flex-1 rounded-xl bg-gray-200 p-1">
|
||||||
<div className="absolute bottom-0 left-0 top-0 w-[30%] rounded-xl bg-slate-800"></div>
|
<div
|
||||||
|
className="absolute bottom-0 left-0 top-0 rounded-xl bg-slate-800"
|
||||||
|
style={{
|
||||||
|
width: `${donePercentage}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="ml-3 text-sm">5 / 100</span>
|
<span className="ml-3 text-sm">
|
||||||
|
{totalSolved} / {totalCount}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative -left-1 flex flex-col gap-2 text-sm text-black sm:flex-row sm:gap-3">
|
<div className="relative -left-1 flex flex-col gap-2 text-sm text-black sm:flex-row sm:gap-3">
|
||||||
<span className="flex items-center">
|
<span className="flex items-center">
|
||||||
<CheckCircle className="mr-1 h-4" />
|
<CheckCircle className="mr-1 h-4" />
|
||||||
<span>Already knew</span>
|
<span>Knew</span>
|
||||||
<span className="ml-2 rounded-md bg-gray-200/80 px-1.5 font-medium text-black">
|
<span className="ml-2 rounded-md bg-gray-200/80 px-1.5 font-medium text-black">
|
||||||
44 Questions
|
{knowCount} Questions
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span className="flex items-center">
|
<span className="flex items-center">
|
||||||
<Sparkles className="mr-1 h-4" />
|
<Sparkles className="mr-1 h-4" />
|
||||||
<span>Didn't Know</span>
|
<span>Learnt</span>
|
||||||
<span className="ml-2 rounded-md bg-gray-200/80 px-1.5 font-medium text-black">
|
<span className="ml-2 rounded-md bg-gray-200/80 px-1.5 font-medium text-black">
|
||||||
20 Questions
|
{didNotKnowCount} Questions
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className="flex items-center">
|
||||||
|
<SkipForward className="mr-1 h-4" />
|
||||||
|
<span>Skipped</span>
|
||||||
|
<span className="ml-2 rounded-md bg-gray-200/80 px-1.5 font-medium text-black">
|
||||||
|
{skippedCount} Questions
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<button className="flex items-center text-red-600 hover:text-red-900">
|
<button className="flex items-center text-red-600 hover:text-red-900">
|
||||||
<RotateCcw className="mr-1 h-4" />
|
<RotateCcw className="mr-1 h-4" />
|
||||||
Reset Progress
|
Reset
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user