Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

push changes to main from dev #77

Merged
merged 20 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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