feat: question page with progress tracking

pull/4446/head
Kamran Ahmed 1 year ago
parent 5bbcd85e6c
commit 2e18d5a563
  1. 3
      package.json
  2. 17
      pnpm-lock.yaml
  3. 65
      src/components/Confetti.tsx
  4. 2
      src/components/CreateTeam/NextButton.tsx
  5. 13
      src/components/GridItem.astro
  6. 142
      src/components/Questions/PrismAtom.css
  7. 77
      src/components/Questions/QuestionCard.tsx
  8. 7
      src/components/Questions/QuestionLoader.tsx
  9. 193
      src/components/Questions/QuestionsList.tsx
  10. 3
      src/components/Questions/QuestionsProgress.tsx
  11. 12
      src/data/question-groups/backend/backend.json
  12. 1
      src/data/question-groups/backend/what-is-server.md
  13. 3
      src/data/question-groups/react/content/controlled-vs-uncontrolled.md
  14. 28
      src/data/question-groups/react/content/react-performance.md
  15. 65
      src/data/question-groups/react/react.md
  16. 8
      src/lib/markdown.ts
  17. 118
      src/lib/question-group.ts
  18. 47
      src/pages/questions/[questionGroupId].astro
  19. 37
      src/pages/questions/index.astro

@ -30,12 +30,14 @@
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.6",
"astro": "^3.0.5", "astro": "^3.0.5",
"astro-compress": "^2.0.8", "astro-compress": "^2.0.8",
"dracula-prism": "^2.1.13",
"jose": "^4.14.4", "jose": "^4.14.4",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"lucide-react": "^0.274.0", "lucide-react": "^0.274.0",
"nanostores": "^0.9.2", "nanostores": "^0.9.2",
"node-html-parser": "^6.1.5", "node-html-parser": "^6.1.5",
"npm-check-updates": "^16.10.12", "npm-check-updates": "^16.10.12",
"prismjs": "^1.29.0",
"react": "^18.0.0", "react": "^18.0.0",
"react-confetti": "^6.1.0", "react-confetti": "^6.1.0",
"react-dom": "^18.0.0", "react-dom": "^18.0.0",
@ -48,6 +50,7 @@
"@playwright/test": "^1.35.1", "@playwright/test": "^1.35.1",
"@tailwindcss/typography": "^0.5.9", "@tailwindcss/typography": "^0.5.9",
"@types/js-cookie": "^3.0.3", "@types/js-cookie": "^3.0.3",
"@types/prismjs": "^1.26.0",
"csv-parser": "^3.0.0", "csv-parser": "^3.0.0",
"gh-pages": "^5.0.0", "gh-pages": "^5.0.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",

@ -32,6 +32,9 @@ dependencies:
astro-compress: astro-compress:
specifier: ^2.0.8 specifier: ^2.0.8
version: 2.0.8 version: 2.0.8
dracula-prism:
specifier: ^2.1.13
version: 2.1.13
jose: jose:
specifier: ^4.14.4 specifier: ^4.14.4
version: 4.14.4 version: 4.14.4
@ -50,6 +53,9 @@ dependencies:
npm-check-updates: npm-check-updates:
specifier: ^16.10.12 specifier: ^16.10.12
version: 16.10.12 version: 16.10.12
prismjs:
specifier: ^1.29.0
version: 1.29.0
react: react:
specifier: ^18.0.0 specifier: ^18.0.0
version: 18.0.0 version: 18.0.0
@ -82,6 +88,9 @@ devDependencies:
'@types/js-cookie': '@types/js-cookie':
specifier: ^3.0.3 specifier: ^3.0.3
version: 3.0.3 version: 3.0.3
'@types/prismjs':
specifier: ^1.26.0
version: 1.26.0
csv-parser: csv-parser:
specifier: ^3.0.0 specifier: ^3.0.0
version: 3.0.0 version: 3.0.0
@ -1230,6 +1239,10 @@ packages:
resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==} resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==}
dev: false dev: false
/@types/prismjs@1.26.0:
resolution: {integrity: sha512-ZTaqn/qSqUuAq1YwvOFQfVW1AR/oQJlLSZVustdjwI+GZ8kr0MSHBj0tsXPW1EqHubx50gtBEjbPGsdZwQwCjQ==}
dev: true
/@types/prop-types@15.7.5: /@types/prop-types@15.7.5:
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
dev: false dev: false
@ -2100,6 +2113,10 @@ packages:
is-obj: 2.0.0 is-obj: 2.0.0
dev: false dev: false
/dracula-prism@2.1.13:
resolution: {integrity: sha512-mgm8Nr/X0RGUz/wpha88dKN/xw0QIGK8BQmWXrzgtOp9be+qcKiFEa2J5SQ3+/WNvL5sPPtNQXPL+Vy//Q8+dg==}
dev: false
/dset@3.1.2: /dset@3.1.2:
resolution: {integrity: sha512-g/M9sqy3oHe477Ar4voQxWtaPIFw1jTdKZuomOjhCcBx9nHUNn0pu6NopuFFrTh/TRZIKEj+76vLWFu9BNKk+Q==} resolution: {integrity: sha512-g/M9sqy3oHe477Ar4voQxWtaPIFw1jTdKZuomOjhCcBx9nHUNn0pu6NopuFFrTh/TRZIKEj+76vLWFu9BNKk+Q==}
engines: {node: '>=4'} engines: {node: '>=4'}

@ -0,0 +1,65 @@
import { useEffect, useState } from 'react';
import ReactConfetti from 'react-confetti';
type ConfettiPosition = {
x: number;
y: number;
w: number;
h: number;
};
type ConfettiProps = {
element: HTMLElement | null;
onDone: () => void;
};
export function Confetti(props: ConfettiProps) {
const { element, onDone } = props;
const [confettiPos, setConfettiPos] = useState<
undefined | ConfettiPosition
>();
useEffect(() => {
if (!element) {
setConfettiPos(undefined);
return;
}
const elRect = element.getBoundingClientRect();
// set confetti position, keeping in mind the scroll values
setConfettiPos({
x: elRect?.x || 0,
y: (elRect?.y || 0) + window.scrollY,
w: elRect?.width || 0,
h: elRect?.height || 0,
});
}, [element]);
if (!confettiPos) {
return null;
}
return (
<ReactConfetti
height={document.body.scrollHeight}
numberOfPieces={40}
recycle={false}
onConfettiComplete={(confettiInstance) => {
confettiInstance?.reset();
setConfettiPos(undefined);
onDone();
}}
initialVelocityX={4}
initialVelocityY={8}
tweenDuration={25}
confettiSource={{
x: confettiPos.x,
y: confettiPos.y,
w: confettiPos.w,
h: confettiPos.h,
}}
/>
);
}

@ -21,7 +21,7 @@ export function NextButton(props: NextButtonProps) {
return ( return (
<button <button
type={type} type={type as any}
onClick={onClick} onClick={onClick}
disabled={isLoading} disabled={isLoading}
className={ className={

@ -16,15 +16,22 @@ const { url, title, description, isNew } = Astro.props;
class='relative flex h-full flex-col rounded-md border border-gray-200 bg-white p-2.5 hover:border-gray-400 sm:rounded-lg sm:p-5' class='relative flex h-full flex-col rounded-md border border-gray-200 bg-white p-2.5 hover:border-gray-400 sm:rounded-lg sm:p-5'
> >
<span <span
class='font-semibold text-md mb-0 text-gray-900 hover:text-black sm:mb-1.5 sm:text-xl' class='text-md mb-0 font-semibold text-gray-900 hover:text-black sm:mb-1.5 sm:text-xl'
> >
{title} {title}
</span> </span>
<span class='hidden text-sm leading-normal text-gray-400 sm:block' set:html={description} /> <span
class='hidden text-sm leading-normal text-gray-400 sm:block'
set:html={description}
/>
{ {
isNew && ( isNew && (
<span class='absolute bottom-1 right-1 rounded-sm bg-yellow-300 px-1 py-0.5 text-xs font-medium uppercase text-yellow-900 sm:px-1.5'> <span class='flex items-center gap-1.5 absolute bottom-1.5 right-1 rounded-sm text-xs font-semibold uppercase text-purple-500 sm:px-1.5'>
<span class='relative flex h-2 w-2'>
<span class='absolute inline-flex h-full w-full animate-ping rounded-full bg-purple-400 opacity-75' />
<span class='relative inline-flex h-2 w-2 rounded-full bg-purple-500' />
</span>
New New
</span> </span>
) )

@ -0,0 +1,142 @@
/**
* atom-dark theme for `prism.js`
* Based on Atom's `atom-dark` theme: https://github.com/atom/atom-dark-syntax
* @author Joe Gibson (@gibsjose)
*/
code[class*="language-"],
pre[class*="language-"] {
color: #c5c8c6;
text-shadow: 0 1px rgba(0, 0, 0, 0.3);
font-family: Inconsolata, Monaco, Consolas, 'Courier New', Courier, monospace;
direction: ltr;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
border-radius: 0.3em;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: #1d1f21;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: .1em;
border-radius: .3em;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #7C7C7C;
}
.token.punctuation {
color: #c5c8c6;
}
.namespace {
opacity: .7;
}
.token.property,
.token.keyword,
.token.tag {
color: #96CBFE;
}
.token.class-name {
color: #FFFFB6;
text-decoration: underline;
}
.token.boolean,
.token.constant {
color: #99CC99;
}
.token.symbol,
.token.deleted {
color: #f92672;
}
.token.number {
color: #FF73FD;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #A8FF60;
}
.token.variable {
color: #C6C5FE;
}
.token.operator {
color: #EDEDED;
}
.token.entity {
color: #FFFFB6;
cursor: help;
}
.token.url {
color: #96CBFE;
}
.language-css .token.string,
.style .token.string {
color: #87C38A;
}
.token.atrule,
.token.attr-value {
color: #F9EE98;
}
.token.function {
color: #DAD085;
}
.token.regex {
color: #E9C062;
}
.token.important {
color: #fd971f;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}

@ -1,6 +1,16 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import type { QuestionType } from '../../lib/question-group';
import { markdownToHtml } from '../../lib/markdown';
import Prism from 'prismjs';
import './PrismAtom.css';
type QuestionCardProps = {
question: QuestionType;
};
export function QuestionCard(props: QuestionCardProps) {
const { question } = props;
export function QuestionCard() {
const [isAnswerVisible, setIsAnswerVisible] = useState<boolean>(false); const [isAnswerVisible, setIsAnswerVisible] = useState<boolean>(false);
const answerRef = useRef<HTMLDivElement>(null); const answerRef = useRef<HTMLDivElement>(null);
const questionRef = useRef<HTMLDivElement>(null); const questionRef = useRef<HTMLDivElement>(null);
@ -10,6 +20,8 @@ export function QuestionCard() {
// width if the answer is visible and the question height is less than // width if the answer is visible and the question height is less than
// the answer height // the answer height
if (isAnswerVisible) { if (isAnswerVisible) {
Prism.highlightAll();
const answerHeight = answerRef.current?.clientHeight || 0; const answerHeight = answerRef.current?.clientHeight || 0;
const questionHeight = questionRef.current?.clientHeight || 0; const questionHeight = questionRef.current?.clientHeight || 0;
@ -22,7 +34,8 @@ export function QuestionCard() {
// if the user has scrolled down and the top of the answer is not // if the user has scrolled down and the top of the answer is not
// visible, scroll to the top of the answer // visible, scroll to the top of the answer
const questionTop = (questionRef.current?.getBoundingClientRect().top || 0) - 147; const questionTop =
(questionRef.current?.getBoundingClientRect().top || 0) - 147;
if (questionTop < 0) { if (questionTop < 0) {
window.scrollTo({ window.scrollTo({
top: window.scrollY + questionTop - 10, top: window.scrollY + questionTop - 10,
@ -30,6 +43,10 @@ export function QuestionCard() {
} }
}, [isAnswerVisible]); }, [isAnswerVisible]);
useEffect(() => {
setIsAnswerVisible(false);
}, [question]);
return ( return (
<> <>
<div <div
@ -37,14 +54,23 @@ export function QuestionCard() {
className={`flex flex-grow flex-col items-center justify-center py-8`} className={`flex flex-grow flex-col items-center justify-center py-8`}
> >
<div className="text-gray-400"> <div className="text-gray-400">
<span>Frontend</span> {question.topics?.map((topic, counter) => {
<span className="mx-3">&middot;</span> const totalTopics = question.topics?.length || 0;
<span className="capitalize">Easy Question</span>
return (
<>
<span className="capitalize">{topic}</span>
{counter !== totalTopics - 1 && (
<span className="mx-2">&middot;</span>
)}
</>
);
})}
</div> </div>
<div className="mx-auto flex max-w-[550px] flex-1 items-center justify-center py-8"> <div className="mx-auto flex max-w-[550px] flex-1 items-center justify-center py-8">
<p className="text-3xl leading-normal text-black font-semibold"> <p className="text-3xl font-semibold leading-normal text-black">
What do you think is the output of the following code? {question.question}
</p> </p>
</div> </div>
@ -66,28 +92,23 @@ export function QuestionCard() {
isAnswerVisible ? 'top-0 min-h-[398px]' : 'top-full' isAnswerVisible ? 'top-0 min-h-[398px]' : 'top-full'
}`} }`}
> >
<div className="mx-auto flex max-w-[600px] flex-grow items-center py-0 px-5 text-xl leading-normal"> {!question.isLongAnswer && (
<p> <div
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam className={`mx-auto flex max-w-[600px] flex-grow flex-col items-center justify-center py-0 px-5 text-center text-xl leading-normal`}
voluptatum, quod, quas, quia, voluptates voluptate quibusdam dangerouslySetInnerHTML={{
voluptatibus quos quae quidem. Quisqu __html: markdownToHtml(question.answer, false),
}}
/>
)}
<br /> {question.isLongAnswer && (
<br /> <div
Quisquam voluptatum, quod, quas, quia, voluptates voluptate className={`qa-answer prose prose-sm prose-quoteless mx-auto flex w-full max-w-[600px] flex-grow flex-col items-start justify-center py-0 px-5 text-left text-lg prose-h1:mb-2.5 prose-h1:mt-7 prose-h2:mb-3 prose-h2:mt-0 prose-h3:mb-[5px] prose-h3:mt-[10px] prose-p:mb-4 prose-p:mt-0 prose-blockquote:font-normal prose-blockquote:not-italic prose-blockquote:text-gray-700 prose-pre:w-full prose-pre:!mb-6 prose-li:m-0 prose-li:mb-0.5`}
quibusdam voluptatibus quos quae quidem. Quisquam voluptatum, quod, dangerouslySetInnerHTML={{
quas, quia, voluptates voluptate quibusdam voluptatibus quos quae __html: markdownToHtml(question.answer, false),
quidem. Quisquam voluptatum, quod, quas, quia, voluptates voluptate }}
quibusdam voluptatibus quos quae quidem. />
<br /> )}
<br />
Quisquam voluptatum, quod, quas, quia, voluptates voluptate
quibusdam voluptatibus quos quae quidem. Quisquam voluptatum, quod,
quas, quia, voluptates voluptate quibusdam voluptatibus quos quae
quidem. Quisquam voluptatum, quod, quas, quia, voluptates voluptate
quibusdam voluptatibus quos quae quidem.
</p>
</div>
<div className="mt-7 text-center"> <div className="mt-7 text-center">
<button <button
onClick={() => { onClick={() => {

@ -1,7 +1,10 @@
import {Spinner} from "../ReactIcons/Spinner";
export function QuestionLoader() { export function QuestionLoader() {
return ( return (
<div className="flex h-full w-full items-center justify-center"> <div className="flex flex-grow flex-col items-center justify-center">
<p className="animate-pulse text-2xl text-black duration-100"> <p className="text-xl font-medium text-gray-500 flex items-center gap-3.5">
<Spinner isDualRing={false} innerFill='#6b7280' className="h-5 w-5" />
Please wait .. Please wait ..
</p> </p>
</div> </div>

@ -1,90 +1,177 @@
import { useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import ReactConfetti from 'react-confetti';
import { QuestionsProgress } from './QuestionsProgress'; import { QuestionsProgress } from './QuestionsProgress';
import { CheckCircle, SkipForward, Sparkles } from 'lucide-react'; import { CheckCircle, SkipForward, Sparkles } from 'lucide-react';
import { QuestionCard } from './QuestionCard'; import { QuestionCard } from './QuestionCard';
import { QuestionLoader } from './QuestionLoader'; import { QuestionLoader } from './QuestionLoader';
import { isLoggedIn } from '../../lib/jwt'; 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 ConfettiPosition = { type UserQuestionProgress = {
x: number; know: string[];
y: number; didNotKnow: string[];
w: number;
h: number;
}; };
export function QuestionsList() { type QuestionsListProps = {
const [isLoading, setIsLoading] = useState<boolean>(false); groupId: string;
const [givenAnswers, setGivenAnswers] = useState<string[]>([]); questions: QuestionType[];
const [confettiPos, setConfettiPos] = useState<undefined | ConfettiPosition>( };
undefined
); 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 alreadyKnowRef = useRef<HTMLButtonElement>(null);
const didNotKnowRef = useRef<HTMLButtonElement>(null);
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 ( return (
<div className="mb-40 gap-3 text-center"> <div className="mb-40 gap-3 text-center">
<QuestionsProgress <Confetti
showLoginAlert={!isLoggedIn() && givenAnswers.length !== 0} element={confettiEl}
onDone={() => {
setConfettiEl(null);
}}
/> />
{confettiPos && ( <QuestionsProgress
<ReactConfetti isLoading={isLoading}
height={document.body.scrollHeight} showLoginAlert={!isLoggedIn() && hasProgress}
numberOfPieces={40} />
recycle={false}
onConfettiComplete={(confettiInstance) => {
confettiInstance?.reset();
setConfettiPos(undefined);
}}
initialVelocityX={4}
initialVelocityY={8}
tweenDuration={25}
confettiSource={{
x: confettiPos.x,
y: confettiPos.y,
w: confettiPos.w,
h: confettiPos.h,
}}
/>
)}
<div className="relative mb-4 flex min-h-[400px] w-full overflow-hidden rounded-lg border border-gray-300 bg-white"> <div className="relative mb-4 flex min-h-[400px] w-full overflow-hidden rounded-lg border border-gray-300 bg-white">
<QuestionCard /> {!isLoading && <QuestionCard question={currQuestion} />}
{isLoading && <QuestionLoader />} {isLoading && <QuestionLoader />}
</div> </div>
<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}
ref={alreadyKnowRef} ref={alreadyKnowRef}
onClick={(e) => { onClick={(e) => {
const alreadyKnowRect = setConfettiEl(alreadyKnowRef.current);
alreadyKnowRef.current?.getBoundingClientRect(); updateQuestionStatus('know', currQuestion.id).finally(() => null);
const buttonX = alreadyKnowRect?.x || 0;
const buttonY = alreadyKnowRect?.y || 0;
// set confetti position, keeping in mind the scroll values
setConfettiPos({
x: buttonX,
y: buttonY + window.scrollY,
w: alreadyKnowRect?.width || 0,
h: alreadyKnowRect?.height || 0,
});
setGivenAnswers((prev) => [...prev, 'alreadyKnow']);
}} }}
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" 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" /> <CheckCircle className="mr-1 h-4 text-current" />
Already Know that Already Know that
</button> </button>
<button 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"> <button
ref={didNotKnowRef}
onClick={() => {
setConfettiEl(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" /> <Sparkles className="mr-1 h-4 text-current" />
Didn't Know that Didn't Know that
</button> </button>
<button <button
disabled={isLoading || isUpdatingStatus}
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" 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" /> <SkipForward className="mr-1 h-4" />
Skip Question Skip Question

@ -2,11 +2,12 @@ import { CheckCircle, RotateCcw, Sparkles } from 'lucide-react';
import { showLoginPopup } from '../../lib/popup'; import { showLoginPopup } from '../../lib/popup';
type QuestionsProgressProps = { type QuestionsProgressProps = {
isLoading?: boolean;
showLoginAlert?: boolean; showLoginAlert?: boolean;
}; };
export function QuestionsProgress(props: QuestionsProgressProps) { export function QuestionsProgress(props: QuestionsProgressProps) {
const { showLoginAlert } = props; const { showLoginAlert, isLoading = false } = props;
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">

@ -1,12 +0,0 @@
[
{
"id": "what-is-server",
"question": "What is a Server",
"answer": {
"heading": "",
"answer": "",
"list": [],
"file": "what-is-server.md"
}
}
]

@ -0,0 +1,3 @@
JSX stands for JavaScript XML and it is an extension to the JavaScript language syntax. It is used with React to describe what the user interface should look like. By using JSX, we can write HTML structures in the same file that contains JavaScript code. This makes the code easier to understand and debug.
It is basically a syntactic sugar around `React.createElement()` function.

@ -0,0 +1,28 @@
To improve the performance of React app, we can use the following techniques which are not limited to. I would recommend to read [React docs](https://reactjs.org/docs/optimizing-performance.html) for more details.
- Use production build
- Use suspense with lazy loading
- Avoid unnecessary re-renders
- Memoize expensive computations
- Monitor performance with React Profiler
- ...
And more. Here's a test code snippet to demonstrate the performance of React app:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body></body>
</html>
```
In the above code snippet, we have a simple HTML page with no CSS and JS. Let's add a React app to it.

@ -0,0 +1,65 @@
---
order: 1
briefTitle: 'React'
briefDescription: 'Test, rate and improve your React knowledge with these questions.'
title: 'React Questions'
description: 'Test, rate and improve your React knowledge with these questions.'
isNew: true
seo:
title: 'React Questions'
description: 'Curated list of React questions to test, rate and improve your knowledge. Questions are based on real world experience and knowledge.'
keywords:
- 'react quiz'
- 'react questions'
- 'react interview questions'
- 'react interview'
- 'react test'
sitemap:
priority: 1
changefreq: 'monthly'
questions:
- question: What is a React?
answer: React, is an open-source JavaScript library for building user interfaces (UIs). It was developed and is maintained by Meta, and is widely used by developers to create interactive and dynamic web applications.
topics:
- 'Basics'
- 'Beginner'
- question: What are the features of React?
answer: Use of Virtual DOM instead of Real DOM, JSX, Server-side rendering, Unidirectional data flow or data binding, Reusable components, etc.
topics:
- 'Basics'
- 'Beginner'
- question: What is JSX?
answer: JSX is a syntax extension to JavaScript and comes with the full power of JavaScript. JSX produces React “elements”. You can embed any JavaScript expression in JSX by wrapping it in curly braces. After compilation, JSX expressions become regular JavaScript objects.
topics:
- 'Basics'
- 'Beginner'
- question: What is the difference between Real DOM and Virtual DOM?
answer: |
Virtual DOM is the representation of a UI in the form of a plain javascript object. It is a node tree that lists the elements, their attributes and content as Objects and their properties. Real DOM is the real representation of a UI which can be seen and inspected in the browser.
Manipulating the virtual DOM is much faster than real DOM, because nothing gets drawn on the screen. React uses this virtual DOM to figure out the most efficient way to update the browser DOM.
topics:
- 'Basics'
- 'Beginner'
- question: What is the difference between state and props?
answer: |
Props are used to pass data from parent to child. They are like function arguments. They are immutable.
State is managed within the component and is mutable.
topics:
- 'Basics'
- 'Beginner'
- question: What is the difference between controlled and uncontrolled components?
answer: controlled-vs-uncontrolled.md
topics:
- 'Basics'
- 'Beginner'
- question: What are different options to style a React component?
answer: CSS Stylesheets, Inline styles, CSS Modules, Styled Components, CSS in JS libraries, etc.
topics:
- 'Styling'
- 'Beginner'
- question: What are different ways to keep React performant?
answer: 'react-performance.md'
topics:
- 'Performance'
- 'Intermediate'
---

@ -1,8 +1,12 @@
// @ts-ignore // @ts-ignore
import MarkdownIt from 'markdown-it'; import MarkdownIt from 'markdown-it';
export function markdownToHtml(markdown: string): string { export function markdownToHtml(markdown: string, isInline = true): string {
const md = new MarkdownIt(); const md = new MarkdownIt();
return md.renderInline(markdown); if (isInline) {
return md.renderInline(markdown);
} else {
return md.render(markdown);
}
} }

@ -0,0 +1,118 @@
import type { MarkdownFileType } from './file';
import slugify from 'slugify';
interface RawQuestionGroupFrontmatter {
order: number;
briefTitle: string;
briefDescription: string;
title: string;
description: string;
isNew: boolean;
seo: {
title: string;
description: string;
ogImageUrl?: string;
keywords: string[];
};
sitemap: {
priority: number;
changefreq: string;
};
questions: {
question: string;
answer: string;
topics: string[];
}[];
}
type RawQuestionGroupFileType =
MarkdownFileType<RawQuestionGroupFrontmatter> & {
id: string;
};
export type QuestionType = {
id: string;
question: string;
answer: string;
isLongAnswer: boolean;
topics?: string[];
};
export type QuestionGroupType = RawQuestionGroupFileType & {
questions: QuestionType[];
allTopics: string[];
};
/**
* Gets all the best practice files
*
* @returns Promisified BestPracticeFileType[]
*/
export async function getAllQuestionGroups(): Promise<QuestionGroupType[]> {
const questionGroupFilesMap =
await import.meta.glob<RawQuestionGroupFileType>(
`/src/data/question-groups/*/*.md`,
{
eager: true,
}
);
const answerFilesMap = await import.meta.glob<string>(
// get the files inside /src/data/question-groups/[ignore]/content/*.md
`/src/data/question-groups/*/content/*.md`,
{
eager: true,
as: 'raw',
}
);
return Object.values(questionGroupFilesMap)
.map((questionGroupFile) => {
const fileParts = questionGroupFile?.file?.split('/');
const [questionGroupDir, questionGroupFileName] = fileParts?.slice(-2);
const questionGroupFileId = questionGroupFileName?.replace('.md', '');
const formattedAnswers: QuestionType[] =
questionGroupFile.frontmatter.questions.map((qa) => {
const questionText = qa.question;
let answerText = qa.answer;
let isLongAnswer = false;
if (answerText.endsWith('.md')) {
const answerFilePath = `/src/data/question-groups/${questionGroupDir}/content/${answerText}`;
answerText =
answerFilesMap[answerFilePath] ||
`File missing: ${answerFilePath}`;
isLongAnswer = true;
}
return {
id: slugify(questionText, { lower: true }),
question: questionText,
answer: answerText,
topics: qa.topics,
isLongAnswer,
};
});
const uniqueTopics = formattedAnswers
.flatMap((answer) => answer.topics || [])
.filter((topic) => topic)
.reduce((acc, topic) => {
if (!acc.includes(topic)) {
acc.push(topic);
}
return acc;
}, [] as string[]);
return {
...questionGroupFile,
id: questionGroupFileId,
questions: formattedAnswers,
allTopics: uniqueTopics,
};
})
.sort((a, b) => a.frontmatter.order - b.frontmatter.order);
}

@ -3,39 +3,52 @@ import GridItem from '../../components/GridItem.astro';
import SimplePageHeader from '../../components/SimplePageHeader.astro'; import SimplePageHeader from '../../components/SimplePageHeader.astro';
import BaseLayout from '../../layouts/BaseLayout.astro'; import BaseLayout from '../../layouts/BaseLayout.astro';
import Footer from '../../components/Footer.astro'; import Footer from '../../components/Footer.astro';
import AstroIcon from "../../components/AstroIcon.astro"; import AstroIcon from '../../components/AstroIcon.astro';
import { QuestionsList } from '../../components/Questions/QuestionsList'; import { QuestionsList } from '../../components/Questions/QuestionsList';
import {
getAllQuestionGroups,
type QuestionGroupType,
} from '../../lib/question-group';
export interface Props {
questionGroup: QuestionGroupType;
}
export async function getStaticPaths() { export async function getStaticPaths() {
return [ const questionGroups = await getAllQuestionGroups();
{
params: { questionGroupId: 'frontend' }, return questionGroups.map((questionGroup) => {
props: {}, return {
}, params: { questionGroupId: questionGroup.id },
]; props: { questionGroup },
};
});
} }
const { questionGroup } = Astro.props;
const { frontmatter } = questionGroup;
--- ---
<BaseLayout <BaseLayout
title='Questions' title={frontmatter.seo.title}
description={'Step by step guides and paths to learn different tools or technologies'} briefTitle={frontmatter.briefTitle}
permalink={'/roadmaps'} description={frontmatter.seo.description}
keywords={frontmatter.seo.keywords}
permalink={`/questions/${questionGroup.id}`}
noIndex={true} noIndex={true}
> >
<div class='flex bg-gray-50 pb-14 pt-4 sm:pb-16 sm:pt-8'> <div class='flex bg-gray-50 pb-14 pt-4 sm:pb-16 sm:pt-8'>
<div class='container !max-w-[700px]'> <div class='container !max-w-[700px]'>
<div class='mb-5 mt-2 text-center sm:mb-10 sm:mt-8'> <div class='mb-5 mt-2 text-center sm:mb-10 sm:mt-8'>
<h1 <h1 class='my-2 text-3xl font-bold sm:my-5 sm:text-5xl'>
class='my-2 text-3xl font-bold sm:my-5 sm:text-5xl' {frontmatter.title}
>
Frontend Questions
</h1> </h1>
<p class='text-gray-500 text-xl'> <p class='text-xl text-gray-500'>
Test, rate and improve your frontend knowledge with these questions. {frontmatter.description}
</p> </p>
</div> </div>
<QuestionsList client:load /> <QuestionsList questions={questionGroup.questions} client:load />
</div> </div>
</div> </div>

@ -2,6 +2,9 @@
import GridItem from '../../components/GridItem.astro'; import GridItem from '../../components/GridItem.astro';
import SimplePageHeader from '../../components/SimplePageHeader.astro'; import SimplePageHeader from '../../components/SimplePageHeader.astro';
import BaseLayout from '../../layouts/BaseLayout.astro'; import BaseLayout from '../../layouts/BaseLayout.astro';
import { getAllQuestionGroups } from '../../lib/question-group';
const questionGroups = await getAllQuestionGroups();
--- ---
<BaseLayout <BaseLayout
@ -19,30 +22,16 @@ import BaseLayout from '../../layouts/BaseLayout.astro';
<div class='flex bg-gray-100 pb-14 pt-4 sm:pb-16 sm:pt-8'> <div class='flex bg-gray-100 pb-14 pt-4 sm:pb-16 sm:pt-8'>
<div class='container'> <div class='container'>
<div class='grid grid-cols-1 gap-1 sm:grid-cols-2 sm:gap-3'> <div class='grid grid-cols-1 gap-1 sm:grid-cols-2 sm:gap-3'>
<GridItem {
url={`/questions/frontend`} questionGroups.map((questionGroup) => (
isNew={false} <GridItem
title={'Frontend Development'} url={`/questions/${questionGroup.id}`}
description={'25 Questions &middot; 5 topics'} isNew={questionGroup.frontmatter.isNew}
/> title={questionGroup.frontmatter.title}
<GridItem description={`${questionGroup.questions.length} Questions &middot; ${questionGroup.allTopics.length} topics`}
url={`/questions/backend`} />
isNew={false} ))
title={'Backend Development'} }
description={'25 Questions &middot; 5 topics'}
/>
<GridItem
url={`/questions/devops`}
isNew={false}
title={'DevOps Engineering'}
description={'25 Questions &middot; 5 topics'}
/>
<GridItem
url={`/questions/javascript`}
isNew={false}
title={'JavaScript'}
description={'25 Questions &middot; 5 topics'}
/>
</div> </div>
</div> </div>
</div> </div>

Loading…
Cancel
Save