Skip to content

Commit

Permalink
feat: inline quiz component (#73)
Browse files Browse the repository at this point in the history
  • Loading branch information
ashucoder9 authored Aug 27, 2024
2 parents 965dd1d + 1ffa3d9 commit f41258e
Show file tree
Hide file tree
Showing 12 changed files with 6,623 additions and 268 deletions.
2 changes: 1 addition & 1 deletion app/course/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export default function Page({
enabled: page.data.toc,
footer: (
<a
href={`https://github.com/ava-labs/avalanche-academy/blob/main/${path}`}
href={`https://github.com/ava-labs/avalanche-academy/blob/dev/${path}`}
target="_blank"
rel="noreferrer noopener"
className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
Expand Down
71 changes: 70 additions & 1 deletion app/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,27 @@

.prose {
font-size: 18px;
}
}

.border-avax-red {
--tw-border-opacity: 1;
border-color: #e84142;
}

.bg-avax-red {
--tw-bg-opacity: 1;
background-color: #e84142;
}

.border-avax-green {
--tw-border-opacity: 1;
border-color: #0B7f54;
}

.bg-avax-green {
--tw-bg-opacity: 1;
background-color: #0B7f54;
}

/* List items in prose */
.prose :where(ul > li):not(:where([class~="not-prose"],[class~="not-prose"] *))::marker {
Expand Down Expand Up @@ -46,3 +66,52 @@ svg.lucide {
.my-6 > svg.lucide {
color: #fff;
}

.quiz-option-letter {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 50%;
background-color: #f3f4f6;
color: #4b5563;
font-weight: 600;
margin-right: 8px;
}

.quiz-feedback-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: #fef3c7;
color: #92400e;
margin-right: 8px;
}

.quiz-try-again-button {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 0.5rem 1rem;
border: 1px solid #e5e7eb;
border-radius: 0.375rem;
font-weight: 600;
color: #4b5563;
background-color: white;
transition: background-color 0.2s;
}

.quiz-try-again-button:hover {
background-color: #f9fafb;
}

.quiz-try-again-icon {
width: 20px;
height: 20px;
margin-right: 8px;
}
54 changes: 54 additions & 0 deletions components/QuizProgress.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"use client";
import React, { useState, useEffect } from 'react';
import { getQuizProgress, isEligibleForCertificate } from '../utils/quizProgress';

interface QuizProgressProps {
quizIds: string[];
}

const QuizProgress: React.FC<QuizProgressProps> = ({ quizIds }) => {
const [progress, setProgress] = useState<{ [quizId: string]: boolean }>({});
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
async function loadProgress() {
const quizProgress = await getQuizProgress(quizIds);
setProgress(quizProgress);
setIsLoading(false);
}
loadProgress();
}, [quizIds]);

if (isLoading) {
return <div>Loading progress...</div>;
}

const eligibleForCertificate = isEligibleForCertificate(progress);

return (
<div className="mt-8 p-6 bg-white shadow-md rounded-lg">
<h2 className="text-2xl font-bold mb-4">Quiz Progress</h2>
<ul className="mb-4">
{quizIds.map((quizId) => (
<li key={quizId} className="flex items-center mb-2">
<span className={`w-4 h-4 rounded-full mr-2 ${progress[quizId] ? 'bg-green-500' : 'bg-red-500'}`}></span>
Quiz {quizId}: {progress[quizId] ? 'Completed' : 'Not completed'}
</li>
))}
</ul>
{eligibleForCertificate ? (
<div className="bg-green-100 border-l-4 border-green-500 text-green-700 p-4" role="alert">
<p className="font-bold">Congratulations!</p>
<p>You're eligible for a certificate. Click here to claim it.</p>
</div>
) : (
<div className="bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4" role="alert">
<p className="font-bold">Keep going!</p>
<p>Complete more quizzes to earn your certificate.</p>
</div>
)}
</div>
);
};

export default QuizProgress;
225 changes: 225 additions & 0 deletions components/quiz.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
"use client";
import React, { useState, useEffect } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { saveQuizResponse, getQuizResponse, resetQuizResponse } from '@/utils/indexedDB';
import Image from 'next/image';
import { cn } from '@/utils/cn';
import { buttonVariants } from '@/components/ui/button'

interface QuizProps {
quizId: string;
question: string;
options: string[];
correctAnswers: string[];
hint: string;
explanation: string;
}

const Quiz: React.FC<QuizProps> = ({
quizId,
question,
options,
correctAnswers = [], // Provide a default empty array
hint,
explanation
}) => {
const [selectedAnswers, setSelectedAnswers] = useState<string[]>([]);
const [isAnswerChecked, setIsAnswerChecked] = useState<boolean>(false);
const [isCorrect, setIsCorrect] = useState<boolean>(false);
const [isClient, setIsClient] = useState(false);

const isSingleAnswer = correctAnswers.length === 1;

useEffect(() => {
setIsClient(true);
loadSavedResponse();
}, [quizId]);

const loadSavedResponse = async () => {
const savedResponse = await getQuizResponse(quizId);
if (savedResponse) {
setSelectedAnswers(savedResponse.selectedAnswers || []);
setIsAnswerChecked(savedResponse.isAnswerChecked || false);
setIsCorrect(savedResponse.isCorrect || false);
} else {
resetQuizState();
}
};

const resetQuizState = () => {
setSelectedAnswers([]);
setIsAnswerChecked(false);
setIsCorrect(false);
};

const handleAnswerSelect = (answer: string) => {
if (!isAnswerChecked) {
if (isSingleAnswer) {
setSelectedAnswers([answer]);
} else {
setSelectedAnswers(prev =>
prev.includes(answer)
? prev.filter(a => a !== answer)
: [...prev, answer]
);
}
}
};

const checkAnswer = async () => {
if (selectedAnswers.length > 0 && correctAnswers.length > 0) {
const correct = isSingleAnswer
? selectedAnswers[0] === correctAnswers[0]
: selectedAnswers.length === correctAnswers.length &&
selectedAnswers.every(answer => correctAnswers.includes(answer));
setIsCorrect(correct);
setIsAnswerChecked(true);

await saveQuizResponse(quizId, {
selectedAnswers,
isAnswerChecked: true,
isCorrect: correct,
});
}
};

const handleTryAgain = async () => {
await resetQuizResponse(quizId);
resetQuizState();
};

const renderAnswerFeedback = () => {
if (isAnswerChecked) {
if (isCorrect) {
return (
<div className="mt-4 p-4 bg-green-50 dark:bg-green-900/30 rounded-lg">
<div className="flex items-center text-green-800 dark:text-green-300 mb-2">
<svg className="mr-2" style={{width: '1rem', height: '1rem'}} fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
<span className="font-semibold text-sm">Correct</span>
</div>
<p className="text-sm text-gray-600 dark:text-gray-300 m-0">{explanation}</p>
</div>
);
} else {
return (
<div className="mt-4 p-3 bg-amber-50 dark:bg-amber-900/30 rounded-lg">
<div className="flex items-center text-amber-800 dark:text-amber-300 mb-2">
<svg className="mr-2" style={{width: '1rem', height: '1rem'}} fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
<span className="font-semibold text-sm">Not Quite</span>
</div>
<p className="text-sm text-gray-600 dark:text-gray-300 m-0"><b>Hint:</b> {hint}</p>
</div>
);
}
}
return null;
};

if (!isClient) {
return <div>Loading...</div>;
}

// If correctAnswers is undefined or empty, render an error message
if (!correctAnswers || correctAnswers.length === 0) {
return (
<div className="bg-red-50 dark:bg-red-900/30 p-4 rounded-lg">
<p className="text-red-800 dark:text-red-300">Error: No correct answers provided for this quiz.</p>
</div>
);
}

return (
<div className="bg-gray-50 dark:bg-black flex items-center justify-center p-4">
<div className="w-full max-w-2xl bg-white dark:bg-black shadow-lg rounded-lg overflow-hidden">
<div className="text-center p-4">
<div className="mx-auto flex items-center justify-center mb-4 overflow-hidden">
<Image
src="/wolfie-check.png"
alt="Quiz topic"
width={60}
height={60}
className="object-cover"
style={{margin: '0em'}}
/>
</div>
<h4 className="font-normal" style={{marginTop: '0'}}>Time for a Quiz!</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Wolfie wants to test your knowledge. {isSingleAnswer ? "Select the correct answer." : "Select all correct answers."}
</p>
</div>
<div className="px-6 py-4">
<div className="text-center mb-4">
<h2 className="text-lg font-medium text-gray-800 dark:text-white" style={{marginTop: '0'}}>{question}</h2>
</div>
<div className="space-y-3">
{options.map((option, index) => (
<div
key={uuidv4()}
className={`flex items-center p-3 rounded-lg border transition-colors cursor-pointer ${
isAnswerChecked
? selectedAnswers.includes(option)
? correctAnswers.includes(option)
? 'border-avax-green bg-green-50 dark:bg-green-900/30 dark:border-green-700'
: 'border-avax-red bg-red-50 dark:bg-red-900/30 dark:border-red-700'
: 'border-gray-200 bg-white dark:border-gray-700 dark:bg-black'
: selectedAnswers.includes(option)
? 'border-[#3752ac] bg-[#3752ac] bg-opacity-10 dark:bg-opacity-30'
: 'border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-900'
}`}
onClick={() => handleAnswerSelect(option)}
>
<span className={`w-6 h-6 flex items-center justify-center ${isSingleAnswer ? 'rounded-full' : 'rounded-md'} mr-3 text-sm ${
isAnswerChecked
? selectedAnswers.includes(option)
? correctAnswers.includes(option)
? 'bg-avax-green text-white'
: 'bg-avax-red text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300'
: selectedAnswers.includes(option)
? 'bg-[#3752ac] text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300'
}`}>
{isSingleAnswer
? String.fromCharCode(65 + index) // A, B, C, D for single answer
: (selectedAnswers.includes(option) ? '✓' : '')}
</span>
<span className="text-sm text-gray-600 dark:text-gray-300">{option}</span>
</div>
))}
</div>
{renderAnswerFeedback()}
</div>
<div className="px-6 py-4 flex justify-center">
{!isAnswerChecked ? (
<button
className={cn(
buttonVariants({ variant: 'default'}),
)}
onClick={checkAnswer}
disabled={selectedAnswers.length === 0}
>
Check Answer
</button>
) : (
!isCorrect && (
<button
className={cn(
buttonVariants({ variant: 'secondary' }),
)}
onClick={handleTryAgain}
>
Try Again!
</button>
)
)}
</div>
</div>
</div>
);
};

export default Quiz;
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,17 @@ With these parameters we can illustrate the consensus algorithm as pseudo code:
In the common case when a transaction has no conflicts, finalization happens very quickly. When conflicts exist, honest validators quickly cluster around conflicting transactions, entering a positive feedback loop until all correct validators prefer that transaction. This leads to the acceptance of non-conflicting transactions and the rejection of conflicting transactions.

Avalanche Consensus guarantees (with high probability based on system parameters) that if any honest validator accepts a transaction, all honest validators will come to the same conclusion.

<Quiz
quizId="avalanche-fundamentals-1"
question="In the Avalanche Consensus protocol, what determines whether a validator changes its preference?"
options={[
"A simple majority of sampled validators",
"An α-majority of sampled validators",
"A unanimous decision from sampled validators",
"The validator's initial random choice"
]}
correctAnswers={["An α-majority of sampled validators"]}
hint="Think about the concept of 'α-majority' mentioned in the chapter."
explanation="Avalanche consensus dictates that a validator changes its preference if an α-majority of the sampled validators agrees on another option. The α-majority is a key concept in the protocol, allowing for flexible decision-making based on the sampled subset of validators."
/>
Loading

0 comments on commit f41258e

Please sign in to comment.