-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
26 changed files
with
5,094 additions
and
64 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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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
17 changes: 17 additions & 0 deletions
17
prisma/migrations/20240424002140_add_certificates/migration.sql
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,17 @@ | ||
-- CreateTable | ||
CREATE TABLE "Certificate" ( | ||
"id" TEXT NOT NULL, | ||
"userId" TEXT NOT NULL, | ||
"courseId" INTEGER NOT NULL, | ||
|
||
CONSTRAINT "Certificate_pkey" PRIMARY KEY ("id") | ||
); | ||
|
||
-- CreateIndex | ||
CREATE UNIQUE INDEX "Certificate_userId_courseId_key" ON "Certificate"("userId", "courseId"); | ||
|
||
-- AddForeignKey | ||
ALTER TABLE "Certificate" ADD CONSTRAINT "Certificate_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; | ||
|
||
-- AddForeignKey | ||
ALTER TABLE "Certificate" ADD CONSTRAINT "Certificate_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE RESTRICT ON UPDATE CASCADE; |
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,2 @@ | ||
-- AlterTable | ||
ALTER TABLE "Course" ADD COLUMN "certIssued" BOOLEAN NOT NULL DEFAULT false; |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
import { createCanvas, loadImage } from 'canvas'; | ||
import { PDFDocument, rgb } from 'pdf-lib'; | ||
import fs from 'fs'; | ||
|
||
const getTextProperties = ( | ||
certificateId: string, | ||
userName: string, | ||
hostname: string, | ||
) => { | ||
return [ | ||
{ text: 'CERTIFICATE', fontSize: 80, offsetY: 450 }, | ||
{ text: 'OF COMPLETION', fontSize: 35, offsetY: 400 }, | ||
{ | ||
text: 'This certificate is proudly presented to', | ||
fontSize: 30, | ||
offsetY: 100, | ||
}, | ||
{ text: userName, fontSize: 65, offsetY: 0 }, | ||
{ | ||
text: "For Successfully Completing the '0-1' cohort offered by 100xdevs", | ||
fontSize: 30, | ||
offsetY: -100, | ||
}, | ||
{ text: 'HARKIRAT SINGH', fontSize: 30, offsetY: -400 }, | ||
{ text: 'Instructor', fontSize: 25, offsetY: -450 }, | ||
{ text: `Certificate id: ${certificateId}`, fontSize: 20, offsetY: -500 }, | ||
{ | ||
text: `Verify at: http://${hostname}/certificate/verify/${certificateId}`, | ||
fontSize: 20, | ||
offsetY: -530, | ||
}, | ||
]; | ||
}; | ||
export const generatePng = async ( | ||
certificateId: string, | ||
userName: string, | ||
hostname: string, | ||
) => { | ||
const textProperties = getTextProperties(certificateId, userName, hostname); | ||
|
||
const certiTemplate = await loadImage( | ||
'src/app/api/certificate/certitemplate.png', | ||
); | ||
const canvas = createCanvas(certiTemplate.width, certiTemplate.height); | ||
const ctx = canvas.getContext('2d'); | ||
|
||
ctx.drawImage(certiTemplate, 0, 0, canvas.width, canvas.height); | ||
|
||
ctx.fillStyle = 'black'; | ||
ctx.textAlign = 'center'; | ||
|
||
for (const { text, fontSize, offsetY } of textProperties) { | ||
ctx.font = `${fontSize}px "Times New Roman"`; | ||
ctx.fillText(text, canvas.width / 2, canvas.height / 2 - offsetY); | ||
} | ||
|
||
const sign = await loadImage('src/app/api/certificate/sign.png'); | ||
|
||
ctx.drawImage( | ||
sign, | ||
canvas.width / 2 - sign.width / 2, | ||
canvas.height / 2 + 350 - sign.height, | ||
); | ||
|
||
const buffer = canvas.toBuffer('image/png'); | ||
return buffer; | ||
}; | ||
|
||
export const generatePdf = async ( | ||
certificateId: string, | ||
userName: string, | ||
hostname: string, | ||
) => { | ||
const textProperties = getTextProperties(certificateId, userName, hostname); | ||
|
||
const [imageData, signData] = await Promise.all([ | ||
fs.promises.readFile('src/app/api/certificate/certitemplate.png'), | ||
fs.promises.readFile('src/app/api/certificate/sign.png'), | ||
]); | ||
|
||
const pdfDoc = await PDFDocument.create(); | ||
const page = pdfDoc.addPage([2000, 1545]); | ||
|
||
const image = await pdfDoc.embedPng(imageData); | ||
const { width: imgWidth, height: imgHeight } = image.scaleToFit( | ||
page.getWidth(), | ||
page.getHeight(), | ||
); | ||
page.drawImage(image, { | ||
x: page.getWidth() / 2 - imgWidth / 2, | ||
y: page.getHeight() / 2 - imgHeight / 2, | ||
width: imgWidth, | ||
height: imgHeight, | ||
}); | ||
const font = await pdfDoc.embedFont('Times-Roman'); | ||
for (const { text, fontSize, offsetY } of textProperties) { | ||
const textWidth = font.widthOfTextAtSize(text, fontSize); | ||
const textX = (page.getWidth() - textWidth) / 2; | ||
const textY = page.getHeight() / 2 + offsetY; | ||
page.drawText(text, { | ||
x: textX, | ||
y: textY, | ||
size: fontSize, | ||
color: rgb(0, 0, 0), | ||
font, | ||
}); | ||
} | ||
|
||
const sign = await pdfDoc.embedPng(signData); | ||
const { width: signWidth, height: signHeight } = sign.scaleToFit( | ||
page.getWidth() * 0.5, | ||
page.getHeight() * 0.1, | ||
); | ||
page.drawImage(sign, { | ||
x: page.getWidth() / 2 - signWidth / 2, | ||
y: page.getHeight() / 2 - 350, | ||
width: signWidth, | ||
height: signHeight, | ||
}); | ||
|
||
const pdfBytes = await pdfDoc.save(); | ||
return pdfBytes; | ||
}; |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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,104 @@ | ||
import { NextRequest, NextResponse } from 'next/server'; | ||
import { getServerSession } from 'next-auth'; | ||
import { authOptions } from '@/lib/auth'; | ||
import db from '@/db'; | ||
import { | ||
generatePng, | ||
generatePdf, | ||
} from '@/actions/certificate/generateCertificate'; | ||
import { headers } from 'next/headers'; | ||
|
||
export async function GET(req: NextRequest) { | ||
const headersList = headers(); | ||
const hostname = headersList.get('x-forwarded-host'); | ||
if (!hostname) return new NextResponse('No host found', { status: 400 }); | ||
|
||
const url = new URL(req.url); | ||
const searchParams = new URLSearchParams(url.search); | ||
|
||
const certId = searchParams.get('certificateId'); | ||
if (certId) { | ||
const certificate = await db.certificate.findFirst({ | ||
where: { | ||
id: certId, | ||
}, | ||
include: { | ||
user: true, | ||
}, | ||
}); | ||
if (!certificate) | ||
return new NextResponse('Cannot find certificate', { status: 400 }); | ||
const data = await generatePng( | ||
certId, | ||
certificate.user.name || '', | ||
hostname, | ||
); | ||
return new NextResponse(data, { | ||
headers: { | ||
'Content-Type': 'image/png', | ||
'Content-Disposition': 'attachment; filename="certificate.png"', | ||
}, | ||
}); | ||
} | ||
|
||
const session = await getServerSession(authOptions); | ||
const user = session.user; | ||
|
||
const courseId = searchParams.get('courseId'); | ||
const type = searchParams.get('type'); | ||
|
||
if (!user) return new NextResponse('Login required', { status: 400 }); | ||
if (!type) return new NextResponse('Type not specified', { status: 400 }); | ||
if (!courseId) return new NextResponse('No course id found', { status: 400 }); | ||
|
||
const purchase = await db.userPurchases.findFirst({ | ||
where: { | ||
userId: user.id, | ||
courseId: parseInt(courseId, 10), | ||
}, | ||
}); | ||
|
||
if (!purchase) return new NextResponse('No Purchase found', { status: 400 }); | ||
let certificate = await db.certificate.findFirst({ | ||
where: { | ||
userId: user.id, | ||
courseId: parseInt(courseId, 10), | ||
}, | ||
}); | ||
if (!certificate) { | ||
certificate = await db.certificate.create({ | ||
data: { | ||
userId: user.id, | ||
courseId: parseInt(courseId, 10), | ||
}, | ||
}); | ||
} | ||
|
||
const certificateId = certificate.id; | ||
const userName = session.user.name; | ||
|
||
try { | ||
if (type === 'pdf') { | ||
const data = await generatePdf(certificateId, userName, hostname); | ||
return new NextResponse(data, { | ||
headers: { | ||
'Content-Type': 'application/pdf', | ||
'Content-Disposition': 'attachment; filename="certificate.pdf"', | ||
}, | ||
}); | ||
} | ||
|
||
if (type === 'png') { | ||
const data = await generatePng(certificateId, userName, hostname); | ||
return new NextResponse(data, { | ||
headers: { | ||
'Content-Type': 'image/png', | ||
'Content-Disposition': 'attachment; filename="certificate.png"', | ||
}, | ||
}); | ||
} | ||
} catch (error) { | ||
console.error('Error generating certificate:', error); | ||
return new NextResponse(null, { status: 500 }); | ||
} | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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,20 @@ | ||
import { CertificateComponent } from '@/components/Certificate'; | ||
import { getCertificates } from '@/db/cert'; | ||
|
||
const CertificatePage = async () => { | ||
const certificates = await getCertificates(); | ||
|
||
return ( | ||
<section className="flex flex-wrap justify-center items-center w-full"> | ||
{certificates?.map(({ cert, course }) => ( | ||
<CertificateComponent | ||
certificateId={cert.id} | ||
course={course} | ||
key={course.id} | ||
/> | ||
))} | ||
</section> | ||
); | ||
}; | ||
|
||
export default CertificatePage; |
Oops, something went wrong.