mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-07-31 14:30:13 +02:00
Add functionality to go next and back on questions
This commit is contained in:
@@ -13,28 +13,19 @@ type ProgressStatButtonProps = {
|
|||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
label: string;
|
label: string;
|
||||||
count: number;
|
count: number;
|
||||||
onClick: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function ProgressStatButton(props: ProgressStatButtonProps) {
|
function ProgressStatLabel(props: ProgressStatButtonProps) {
|
||||||
const { icon, label, count, onClick, isDisabled = false } = props;
|
const { icon, label, count } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<span className="group relative flex flex-1 items-center overflow-hidden rounded-md border border-gray-300 bg-white px-2 py-2 text-sm text-black transition-colors disabled:opacity-50 sm:rounded-xl sm:px-4 sm:py-3 sm:text-base">
|
||||||
disabled={isDisabled}
|
|
||||||
onClick={onClick}
|
|
||||||
className="group relative flex flex-1 items-center overflow-hidden rounded-md border border-gray-300 bg-white px-2 py-2 text-sm text-black transition-colors hover:border-black disabled:pointer-events-none disabled:opacity-50 sm:rounded-xl sm:px-4 sm:py-3 sm:text-base"
|
|
||||||
>
|
|
||||||
{icon}
|
{icon}
|
||||||
<span className="flex flex-grow justify-between">
|
<span className="flex flex-grow justify-between">
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
<span>{count}</span>
|
<span>{count}</span>
|
||||||
</span>
|
</span>
|
||||||
|
</span>
|
||||||
<span className="absolute left-0 right-0 top-full flex h-full items-center justify-center border border-black bg-black text-white transition-all duration-200 group-hover:top-0">
|
|
||||||
Restart Asking
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,12 +34,11 @@ type QuestionFinishedProps = {
|
|||||||
didNotKnowCount: number;
|
didNotKnowCount: number;
|
||||||
skippedCount: number;
|
skippedCount: number;
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
onReset: (type: QuestionProgressType | 'reset') => void;
|
onReset: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function QuestionFinished(props: QuestionFinishedProps) {
|
export function QuestionFinished(props: QuestionFinishedProps) {
|
||||||
const { knowCount, didNotKnowCount, skippedCount, totalCount, onReset } =
|
const { knowCount, didNotKnowCount, skippedCount, onReset } = props;
|
||||||
props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex flex-grow flex-col items-center justify-center px-4 sm:px-0">
|
<div className="relative flex flex-grow flex-col items-center justify-center px-4 sm:px-0">
|
||||||
@@ -63,31 +53,25 @@ export function QuestionFinished(props: QuestionFinishedProps) {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mb-5 mt-5 flex w-full flex-col gap-1.5 px-2 sm:flex-row sm:gap-3 sm:px-16">
|
<div className="mb-5 mt-5 flex w-full flex-col gap-1.5 px-2 sm:flex-row sm:gap-3 sm:px-16">
|
||||||
<ProgressStatButton
|
<ProgressStatLabel
|
||||||
icon={<ThumbsUp className="mr-1 h-4" />}
|
icon={<ThumbsUp className="mr-1 h-4" />}
|
||||||
label="Knew"
|
label="Knew"
|
||||||
count={knowCount}
|
count={knowCount}
|
||||||
isDisabled={knowCount === 0}
|
|
||||||
onClick={() => onReset('know')}
|
|
||||||
/>
|
/>
|
||||||
<ProgressStatButton
|
<ProgressStatLabel
|
||||||
icon={<Sparkles className="mr-1 h-4" />}
|
icon={<Sparkles className="mr-1 h-4" />}
|
||||||
label="Learned"
|
label="Learned"
|
||||||
count={didNotKnowCount}
|
count={didNotKnowCount}
|
||||||
isDisabled={didNotKnowCount === 0}
|
|
||||||
onClick={() => onReset('dontKnow')}
|
|
||||||
/>
|
/>
|
||||||
<ProgressStatButton
|
<ProgressStatLabel
|
||||||
icon={<SkipForward className="mr-1 h-4" />}
|
icon={<SkipForward className="mr-1 h-4" />}
|
||||||
label="Skipped"
|
label="Skipped"
|
||||||
count={skippedCount}
|
count={skippedCount}
|
||||||
isDisabled={skippedCount === 0}
|
|
||||||
onClick={() => onReset('skip')}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-4 mt-2 text-sm sm:mb-0">
|
<div className="mb-4 mt-2 text-sm sm:mb-0">
|
||||||
<button
|
<button
|
||||||
onClick={() => onReset('reset')}
|
onClick={() => onReset()}
|
||||||
className="flex items-center gap-0.5 text-sm text-red-700 hover:text-black sm:text-base"
|
className="flex items-center gap-0.5 text-sm text-red-700 hover:text-black sm:text-base"
|
||||||
>
|
>
|
||||||
<RefreshCcw className="mr-1 h-4" />
|
<RefreshCcw className="mr-1 h-4" />
|
||||||
|
@@ -24,14 +24,14 @@ type QuestionsListProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function QuestionsList(props: QuestionsListProps) {
|
export function QuestionsList(props: QuestionsListProps) {
|
||||||
const { questions: unshuffledQuestions, groupId } = props;
|
const { questions: defaultQuestions, groupId } = props;
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
|
const [questions, setQuestions] = useState(defaultQuestions);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [showConfetti, setShowConfetti] = useState(false);
|
const [showConfetti, setShowConfetti] = useState(false);
|
||||||
const [questions, setQuestions] = useState<QuestionType[]>();
|
const [currQuestionIndex, setCurrQuestionIndex] = useState(0);
|
||||||
const [pendingQuestions, setPendingQuestions] = useState<QuestionType[]>([]);
|
|
||||||
|
|
||||||
const [userProgress, setUserProgress] = useState<UserQuestionProgress>();
|
const [userProgress, setUserProgress] = useState<UserQuestionProgress>();
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -57,7 +57,7 @@ export function QuestionsList(props: QuestionsListProps) {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadQuestions() {
|
async function prepareProgress() {
|
||||||
const userProgress = await fetchUserProgress();
|
const userProgress = await fetchUserProgress();
|
||||||
setUserProgress(userProgress);
|
setUserProgress(userProgress);
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ export function QuestionsList(props: QuestionsListProps) {
|
|||||||
const didNotKnowQuestions = userProgress?.dontKnow || [];
|
const didNotKnowQuestions = userProgress?.dontKnow || [];
|
||||||
const skipQuestions = userProgress?.skip || [];
|
const skipQuestions = userProgress?.skip || [];
|
||||||
|
|
||||||
const pendingQuestions = unshuffledQuestions.filter((question) => {
|
const pendingQuestionIndex = questions.findIndex((question) => {
|
||||||
return (
|
return (
|
||||||
!knownQuestions.includes(question.id) &&
|
!knownQuestions.includes(question.id) &&
|
||||||
!didNotKnowQuestions.includes(question.id) &&
|
!didNotKnowQuestions.includes(question.id) &&
|
||||||
@@ -73,31 +73,21 @@ export function QuestionsList(props: QuestionsListProps) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Shuffle and set pending questions
|
setCurrQuestionIndex(pendingQuestionIndex);
|
||||||
// setPendingQuestions(pendingQuestions.sort(() => Math.random() - 0.5));
|
|
||||||
setPendingQuestions(pendingQuestions);
|
|
||||||
setQuestions(unshuffledQuestions);
|
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resetProgress(type: QuestionProgressType | 'reset' = 'reset') {
|
async function resetProgress() {
|
||||||
let knownQuestions = userProgress?.know || [];
|
let knownQuestions = userProgress?.know || [];
|
||||||
let didNotKnowQuestions = userProgress?.dontKnow || [];
|
let didNotKnowQuestions = userProgress?.dontKnow || [];
|
||||||
let skipQuestions = userProgress?.skip || [];
|
let skipQuestions = userProgress?.skip || [];
|
||||||
|
|
||||||
if (!isLoggedIn()) {
|
if (!isLoggedIn()) {
|
||||||
if (type === 'know') {
|
setQuestions(defaultQuestions);
|
||||||
knownQuestions = [];
|
|
||||||
} else if (type === 'dontKnow') {
|
knownQuestions = [];
|
||||||
didNotKnowQuestions = [];
|
didNotKnowQuestions = [];
|
||||||
} else if (type === 'skip') {
|
skipQuestions = [];
|
||||||
skipQuestions = [];
|
|
||||||
} else if (type === 'reset') {
|
|
||||||
knownQuestions = [];
|
|
||||||
didNotKnowQuestions = [];
|
|
||||||
skipQuestions = [];
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
@@ -106,7 +96,7 @@ export function QuestionsList(props: QuestionsListProps) {
|
|||||||
import.meta.env.PUBLIC_API_URL
|
import.meta.env.PUBLIC_API_URL
|
||||||
}/v1-reset-question-progress/${groupId}`,
|
}/v1-reset-question-progress/${groupId}`,
|
||||||
{
|
{
|
||||||
status: type,
|
status: 'reset',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -120,21 +110,13 @@ export function QuestionsList(props: QuestionsListProps) {
|
|||||||
skipQuestions = response?.skip || [];
|
skipQuestions = response?.skip || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const pendingQuestions = unshuffledQuestions.filter((question) => {
|
setCurrQuestionIndex(0);
|
||||||
return (
|
|
||||||
!knownQuestions.includes(question.id) &&
|
|
||||||
!didNotKnowQuestions.includes(question.id) &&
|
|
||||||
!skipQuestions.includes(question.id)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
setUserProgress({
|
setUserProgress({
|
||||||
know: knownQuestions,
|
know: knownQuestions,
|
||||||
dontKnow: didNotKnowQuestions,
|
dontKnow: didNotKnowQuestions,
|
||||||
skip: skipQuestions,
|
skip: skipQuestions,
|
||||||
});
|
});
|
||||||
|
|
||||||
setPendingQuestions(pendingQuestions.sort(() => Math.random() - 0.5));
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,30 +155,29 @@ export function QuestionsList(props: QuestionsListProps) {
|
|||||||
newProgress = response;
|
newProgress = response;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedQuestionList = pendingQuestions.filter(
|
const nextQuestionIndex = currQuestionIndex + 1;
|
||||||
(q) => q.id !== questionId,
|
|
||||||
);
|
|
||||||
|
|
||||||
setUserProgress(newProgress);
|
setUserProgress(newProgress);
|
||||||
setPendingQuestions(updatedQuestionList);
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|
||||||
if (updatedQuestionList.length === 0) {
|
if (!nextQuestionIndex || !questions[nextQuestionIndex]) {
|
||||||
setShowConfetti(true);
|
setShowConfetti(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCurrQuestionIndex(nextQuestionIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadQuestions().then(() => null);
|
prepareProgress().then(() => null);
|
||||||
}, [unshuffledQuestions]);
|
}, [questions]);
|
||||||
|
|
||||||
const knowCount = userProgress?.know.length || 0;
|
const knowCount = userProgress?.know.length || 0;
|
||||||
const dontKnowCount = userProgress?.dontKnow.length || 0;
|
const dontKnowCount = userProgress?.dontKnow.length || 0;
|
||||||
const skipCount = userProgress?.skip.length || 0;
|
const skipCount = userProgress?.skip.length || 0;
|
||||||
const hasProgress = knowCount > 0 || dontKnowCount > 0 || skipCount > 0;
|
const hasProgress = knowCount > 0 || dontKnowCount > 0 || skipCount > 0;
|
||||||
|
|
||||||
const currQuestion = pendingQuestions[0];
|
const currQuestion = questions[currQuestionIndex];
|
||||||
const hasFinished = !isLoading && hasProgress && !currQuestion;
|
const hasFinished = !isLoading && hasProgress && currQuestionIndex === -1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-0 gap-3 text-center sm:mb-40">
|
<div className="mb-0 gap-3 text-center sm:mb-40">
|
||||||
@@ -204,11 +185,37 @@ export function QuestionsList(props: QuestionsListProps) {
|
|||||||
knowCount={knowCount}
|
knowCount={knowCount}
|
||||||
didNotKnowCount={dontKnowCount}
|
didNotKnowCount={dontKnowCount}
|
||||||
skippedCount={skipCount}
|
skippedCount={skipCount}
|
||||||
totalCount={unshuffledQuestions?.length || questions?.length}
|
totalCount={questions?.length}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
showLoginAlert={!isLoggedIn() && hasProgress}
|
showLoginAlert={!isLoggedIn() && hasProgress}
|
||||||
onResetClick={() => {
|
onResetClick={() => {
|
||||||
resetProgress('reset').finally(() => null);
|
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);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -228,12 +235,12 @@ export function QuestionsList(props: QuestionsListProps) {
|
|||||||
>
|
>
|
||||||
{hasFinished && (
|
{hasFinished && (
|
||||||
<QuestionFinished
|
<QuestionFinished
|
||||||
totalCount={unshuffledQuestions?.length || questions?.length || 0}
|
totalCount={questions?.length || 0}
|
||||||
knowCount={knowCount}
|
knowCount={knowCount}
|
||||||
didNotKnowCount={dontKnowCount}
|
didNotKnowCount={dontKnowCount}
|
||||||
skippedCount={skipCount}
|
skippedCount={skipCount}
|
||||||
onReset={(type: QuestionProgressType | 'reset') => {
|
onReset={() => {
|
||||||
resetProgress(type).finally(() => null);
|
resetProgress().finally(() => null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@@ -1,4 +1,11 @@
|
|||||||
import { CheckCircle, RotateCcw, SkipForward, Sparkles } from 'lucide-react';
|
import {
|
||||||
|
CheckCircle,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
RotateCcw,
|
||||||
|
SkipForward,
|
||||||
|
Sparkles,
|
||||||
|
} from 'lucide-react';
|
||||||
import { showLoginPopup } from '../../lib/popup';
|
import { showLoginPopup } from '../../lib/popup';
|
||||||
|
|
||||||
type QuestionsProgressProps = {
|
type QuestionsProgressProps = {
|
||||||
@@ -9,6 +16,8 @@ type QuestionsProgressProps = {
|
|||||||
totalCount?: number;
|
totalCount?: number;
|
||||||
skippedCount?: number;
|
skippedCount?: number;
|
||||||
onResetClick?: () => void;
|
onResetClick?: () => void;
|
||||||
|
onPrevClick?: () => void;
|
||||||
|
onNextClick?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function QuestionsProgress(props: QuestionsProgressProps) {
|
export function QuestionsProgress(props: QuestionsProgressProps) {
|
||||||
@@ -20,6 +29,8 @@ export function QuestionsProgress(props: QuestionsProgressProps) {
|
|||||||
totalCount = 0,
|
totalCount = 0,
|
||||||
skippedCount = 0,
|
skippedCount = 0,
|
||||||
onResetClick = () => null,
|
onResetClick = () => null,
|
||||||
|
onPrevClick = () => null,
|
||||||
|
onNextClick = () => null,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const totalSolved = knowCount + didNotKnowCount + skippedCount;
|
const totalSolved = knowCount + didNotKnowCount + skippedCount;
|
||||||
@@ -36,8 +47,22 @@ export function QuestionsProgress(props: QuestionsProgressProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="ml-3 text-sm">
|
<span className="ml-3 flex items-center text-sm">
|
||||||
{totalSolved} / {totalCount}
|
<button
|
||||||
|
onClick={onPrevClick}
|
||||||
|
className="text-zinc-400 hover:text-black"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4" strokeWidth={3} />
|
||||||
|
</button>
|
||||||
|
<span className="block min-w-[41px] text-center">
|
||||||
|
<span className="tabular-nums">{totalSolved}</span> / {totalCount}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={onNextClick}
|
||||||
|
className="text-zinc-400 hover:text-black"
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-4" strokeWidth={3} />
|
||||||
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -46,8 +71,7 @@ export function QuestionsProgress(props: QuestionsProgressProps) {
|
|||||||
<CheckCircle className="mr-1 h-4" />
|
<CheckCircle className="mr-1 h-4" />
|
||||||
<span>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">
|
||||||
<span className="tabular-nums">{knowCount}</span>{' '}
|
<span className="tabular-nums">{knowCount}</span> Items
|
||||||
Items
|
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -55,8 +79,7 @@ export function QuestionsProgress(props: QuestionsProgressProps) {
|
|||||||
<Sparkles className="mr-1 h-4" />
|
<Sparkles className="mr-1 h-4" />
|
||||||
<span>Learnt</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">
|
||||||
<span className="tabular-nums">{didNotKnowCount}</span>{' '}
|
<span className="tabular-nums">{didNotKnowCount}</span> Items
|
||||||
Items
|
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -64,8 +87,7 @@ export function QuestionsProgress(props: QuestionsProgressProps) {
|
|||||||
<SkipForward className="mr-1 h-4" />
|
<SkipForward className="mr-1 h-4" />
|
||||||
<span>Skipped</span>
|
<span>Skipped</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">
|
||||||
<span className="tabular-nums">{skippedCount}</span>{' '}
|
<span className="tabular-nums">{skippedCount}</span> Items
|
||||||
Items
|
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user