-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
initial commit for certificate feature
- Loading branch information
1 parent
5db2159
commit 25b3a92
Showing
13 changed files
with
360 additions
and
34 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file not shown.
Oops, something went wrong.