Skip to content

Commit

Permalink
initial commit for certificate feature
Browse files Browse the repository at this point in the history
  • Loading branch information
ashucoder9 committed Sep 9, 2024
1 parent 5db2159 commit 25b3a92
Show file tree
Hide file tree
Showing 13 changed files with 360 additions and 34 deletions.
51 changes: 51 additions & 0 deletions app/api/generate-certificate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { NextRequest, NextResponse } from 'next/server';
import { PDFDocument } from 'pdf-lib';

const courseMapping: Record<string, string> = {
'avalanche-fundamentals': 'Avalanche Fundamentals',
};

function getCourseName(courseId: string): string {
return courseMapping[courseId] || courseId;
}

export async function POST(req: NextRequest) {
try {
const { courseId, userName } = await req.json();
if (!courseId || !userName) { return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); }
const courseName = getCourseName(courseId);
const templateUrl = 'http://localhost:3000/certificates/AvalancheAcademy_Certificate.pdf';
const templateResponse = await fetch(templateUrl);

if (!templateResponse.ok) { throw new Error(`Failed to fetch template`); }

const templateArrayBuffer = await templateResponse.arrayBuffer();
const pdfDoc = await PDFDocument.load(templateArrayBuffer);
const form = pdfDoc.getForm();

try {
// fills the form fields in our certificate template
form.getTextField('FullName').setText(userName);
form.getTextField('Class').setText(courseName);
form.getTextField('Awarded').setText(new Date().toLocaleDateString('en-US', { day: 'numeric', month: 'short', year: 'numeric' }));
form.getTextField('Id').setText(Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15));
} catch (error) {
throw new Error('Failed to fill form fields');
}

form.flatten();
const pdfBytes = await pdfDoc.save();
return new NextResponse(pdfBytes, {
status: 200,
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename=${courseId}_certificate.pdf`,
},
});
} catch (error) {
return NextResponse.json(
{ error: 'Failed to generate certificate, contact the Avalanche team.', details: (error as Error).message },
{ status: 500 }
);
}
}
144 changes: 144 additions & 0 deletions components/certificates.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// components/CertificatePage.tsx
"use client";
import React, { useState, useEffect } from 'react';
import { getQuizResponse } from '@/utils/indexedDB';
import { buttonVariants } from '@/components/ui/button';
import { cn } from '@/utils/cn';
import { getChapterUrlForQuiz, getAllQuizIds, getChaptersForCourse, getQuizzesForChapter } from '@/components/quiz/quizMap';
import Link from 'next/link';

interface CertificatePageProps {
courseId: string;
}

const CertificatePage: React.FC<CertificatePageProps> = ({ courseId }) => {
const [completedQuizzes, setCompletedQuizzes] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [userName, setUserName] = useState('');
const [isGenerating, setIsGenerating] = useState(false);

const allQuizIds = getAllQuizIds(courseId);
const chapters = getChaptersForCourse(courseId);

useEffect(() => {
const checkQuizCompletion = async () => {
const completed = await Promise.all(
allQuizIds.map(async (quizId) => {
const response = await getQuizResponse(quizId);
return response && response.isCorrect ? quizId : null;
})
);
setCompletedQuizzes(completed.filter((id): id is string => id !== null));
setIsLoading(false);
};

checkQuizCompletion();
}, [allQuizIds]);

const allQuizzesCompleted = completedQuizzes.length === allQuizIds.length;

const generateCertificate = async () => {
if (!userName.trim()) {
alert('Please enter your name');
return;
}

setIsGenerating(true);
try {
const response = await fetch('/api/generate-certificate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
courseId,
userName,
}),
});

if (!response.ok) {
throw new Error('Failed to generate certificate');
}

const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = `${courseId}_certificate.pdf`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('Error generating certificate:', error);
alert('Failed to generate certificate. Please try again.');
} finally {
setIsGenerating(false);
}
};

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

return (
<div className="max-w-2xl mx-auto mt-8 p-6 bg-white dark:bg-gray-800 rounded-lg shadow-md">
<h1 className="text-2xl font-bold mb-4 text-center">Course Completion Certificate</h1>
<div className="mb-6">
<h2 className="text-xl font-semibold mb-2">Progress:</h2>
{chapters.map((chapter) => (
<div key={chapter} className="mb-4">
<h3 className="text-lg font-medium mb-2">{chapter}</h3>
<ul>
{getQuizzesForChapter(courseId, chapter).map((quizId) => (
<li key={quizId} className="flex items-center mb-2">
<span className={`w-4 h-4 mr-2 rounded-full ${completedQuizzes.includes(quizId) ? 'bg-green-500' : 'bg-red-500'}`}></span>
{completedQuizzes.includes(quizId) ? (
<span>Quiz {quizId}: Completed</span>
) : (
<Link href={getChapterUrlForQuiz(quizId)} className="text-blue-500 hover:underline">
Quiz {quizId}: Not completed - Click to go to quiz
</Link>
)}
</li>
))}
</ul>
</div>
))}
</div>
{allQuizzesCompleted && (
<div className="mb-4">
<label htmlFor="userName" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Enter your full name:
</label>
<input
type="text"
id="userName"
value={userName}
onChange={(e) => setUserName(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
placeholder="John Doe"
/>
</div>
)}
<div className="text-center">
<button
className={cn(
buttonVariants({ variant: allQuizzesCompleted ? 'default' : 'secondary' }),
'w-full'
)}
onClick={generateCertificate}
disabled={!allQuizzesCompleted || isGenerating}
>
{isGenerating
? 'Generating Certificate...'
: allQuizzesCompleted
? 'Generate Certificate'
: 'Complete All Quizzes to Unlock Certificate'}
</button>
</div>
</div>
);
};

export default CertificatePage;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";
import React, { useState, useEffect } from 'react';
import { getQuizProgress, isEligibleForCertificate } from '../utils/quizProgress';
import { getQuizProgress, isEligibleForCertificate } from '@/utils/quizProgress';

interface QuizProgressProps {
quizIds: string[];
Expand Down
File renamed without changes.
110 changes: 110 additions & 0 deletions components/quiz/quizMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
interface QuizInfo {
url: string;
chapter: string;
}

interface ChapterQuizzes {
[chapterId: string]: {
[quizId: string]: QuizInfo;
};
}

export const quizMapping: ChapterQuizzes = {
'avalanche-fundamentals': {
'avalanche-consensus-intro-01': {
url: '/course/avalanche-fundamentals/02-avalanche-consensus-intro/01-avalanche-consensus-intro',
chapter: 'Avalanche Consensus Intro'
},
'avalanche-fundamentals-02-1': {
url: '/course/avalanche-fundamentals/02-avalanche-consensus-intro/02-consensus-mechanisms#ordering-through-consensus',
chapter: 'Avalanche Consensus Intro'
},
'avalanche-fundamentals-02-2': {
url: '/course/avalanche-fundamentals/02-avalanche-consensus-intro/02-consensus-mechanisms#double-spending-attack',
chapter: 'Avalanche Consensus Intro'
},
'snowman-consensus-03-1': {
url: '/course/avalanche-fundamentals/02-avalanche-consensus-intro/03-snowman-consensus#changing-preference',
chapter: 'Avalanche Consensus Intro'
},
'snowman-consensus-03-2': {
url: '/course/avalanche-fundamentals/02-avalanche-consensus-intro/03-snowman-consensus#finalization',
chapter: 'Avalanche Consensus Intro'
},
'multi-chain-architecture-02': {
url: '/course/avalanche-fundamentals/03-multi-chain-architecture-intro/02-subnet',
chapter: 'Multi-Chain Architecture'
},
'multi-chain-architecture-benefits-03-1': {
url: '/course/avalanche-fundamentals/03-multi-chain-architecture-intro/03-benefits#independence-from-other-avalanche-l1s',
chapter: 'Multi-Chain Architecture'
},
'multi-chain-architecture-benefits-03-2': {
url: '/course/avalanche-fundamentals/03-multi-chain-architecture-intro/03-benefits#other-multi-chain-systems',
chapter: 'Multi-Chain Architecture'
},
'vms-and-blockchains-state-machine-02-1': {
url: '/course/avalanche-fundamentals/05-vms-and-blockchains/02-state-machine#soda-dispenser-a-simple-machine',
chapter: 'VMs and Blockchains'
},
'vms-and-blockchains-state-machine-02-2': {
url: '/course/avalanche-fundamentals/05-vms-and-blockchains/02-state-machine#soda-dispenser-a-simple-machine',
chapter: 'VMs and Blockchains'
},
'vms-and-blockchains-state-machine-02-3': {
url: '/course/avalanche-fundamentals/05-vms-and-blockchains/02-state-machine#advantages-of-vms',
chapter: 'VMs and Blockchains'
},
'vms-and-blockchains-03-1': {
url: '/course/avalanche-fundamentals/05-vms-and-blockchains/03-blockchains',
chapter: 'VMs and Blockchains'
},
'vms-and-blockchains-03-2': {
url: '/course/avalanche-fundamentals/05-vms-and-blockchains/03-blockchains',
chapter: 'VMs and Blockchains'
},
'vms-and-blockchains-04': {
url: '/course/avalanche-fundamentals/05-vms-and-blockchains/04-variety-of-vm',
chapter: 'VMs and Blockchains'
},
'vm-customization-01': {
url: '/course/avalanche-fundamentals/06-vm-customization/00-vm-customization',
chapter: 'VM Customization'
},
'vm-customization-configuration-01': {
url: '/course/avalanche-fundamentals/06-vm-customization/01-configuration',
chapter: 'VM Customization'
},
'vm-customization-modification-01': {
url: '/course/avalanche-fundamentals/06-vm-customization/02-modification',
chapter: 'VM Customization'
},
},
};

export function getChapterUrlForQuiz(quizId: string): string {
for (const course of Object.values(quizMapping)) {
if (quizId in course) {
return course[quizId].url;
}
}
return '/';
}

export function getQuizzesForChapter(courseId: string, chapter: string): string[] {
return Object.entries(quizMapping[courseId] || {})
.filter(([_, quizInfo]) => quizInfo.chapter === chapter)
.map(([quizId, _]) => quizId);
}

export function getAllQuizIds(courseId: string): string[] {
return Object.keys(quizMapping[courseId] || {});
}

export function getChaptersForCourse(courseId: string): string[] {
const chapters = new Set<string>();
Object.values(quizMapping[courseId] || {}).forEach(quizInfo => {
chapters.add(quizInfo.chapter);
});
return Array.from(chapters);
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,6 @@ hint="Think about how a VM can be customized to create different blockchains."
explanation="You can think of a Virtual Machine (VM) as a blueprint for a blockchain, where the same VM can be used to create multiple blockchains. Each of these blockchains adheres to the same rules but remains logically independent from the others."
/>

<Quiz
quizId="vm-customization-configuration-01"
question="Can a Virtual Machine (VM) be used to create multiple blockchains?"
options={[
"Yes, the same VM can be used to create multiple blockchains",
"No, each blockchain requires a unique VM",
"Yes, but only if the blockchains are part of different networks"
]}
correctAnswers={[0]}
hint="Think about how a VM can be customized to create different blockchains."
explanation="You can think of a Virtual Machine (VM) as a blueprint for a blockchain, where the same VM can be used to create multiple blockchains. Each of these blockchains adheres to the same rules but remains logically independent from the others."
/>

Examples of the configurable parameters of the subnetEVM include:

**trxAllowList:** Define a whitelist of accounts that restrict which account's transactions are accepted by the VM.
Expand Down
13 changes: 13 additions & 0 deletions content/course/avalanche-fundamentals/get-certificate.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
title: Course Completion Certificate
updated: 2024-09-09
authors: [ashucoder9]
---

import CertificatePage from '@/components/certificates';

You've made it to the end of the course. Let's check your progress and get your certificate.

<CertificatePage courseId="avalanche-fundamentals"/>

Thank you for participating in this course. We hope you found it informative and enjoyable!
4 changes: 3 additions & 1 deletion content/course/avalanche-fundamentals/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"---Virtual Machines & Blockchains---",
"...05-vms-and-blockchains",
"---Virtual Machine Customization---",
"...06-vm-customization"
"...06-vm-customization",
"---Course Completion---",
"get-certificate"
]
}
2 changes: 1 addition & 1 deletion mdx-components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
Pre,
} from 'fumadocs-ui/components/codeblock';
import type { ReactNode } from 'react';
import Quiz from '@/components/quiz'
import Quiz from '@/components/quiz/quiz'
import { Popup, PopupContent, PopupTrigger } from 'fumadocs-ui/twoslash/popup';
import YouTube from '@/components/youtube';
import Gallery from '@/components/gallery';
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"idb": "^8.0.0",
"lucide-react": "^0.408.0",
"next": "^14.2.4",
"pdf-lib": "^1.17.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwindcss-animate": "^1.0.7",
Expand Down
Binary file not shown.
Loading

0 comments on commit 25b3a92

Please sign in to comment.