Skip to content

Commit

Permalink
Merge branch 'main' into qa
Browse files Browse the repository at this point in the history
  • Loading branch information
siinghd authored May 9, 2024
2 parents 111f310 + a653737 commit e45a8b7
Show file tree
Hide file tree
Showing 26 changed files with 5,094 additions and 64 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,6 @@ Read our [contribution guidelines](./CONTRIBUTING.md) for more details.
<a href="https://github.com/code100x/cms/graphs/contributors">
<img src="https://contrib.rocks/image?repo=code100x/cms&max=400&columns=20" />
</a>

## Issues on mac Silicon
brew install pkg-config cairo pango libpng jpeg giflib librsvg
Binary file added certificate.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"@uiw/react-md-editor": "^4.0.4",
"axios": "^1.6.2",
"bcrypt": "^5.1.1",
"canvas": "^2.11.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"dayjs": "^1.11.10",
Expand All @@ -57,9 +58,11 @@
"next-themes": "^0.2.1",
"node-fetch": "^3.3.2",
"notion-client": "^6.16.0",
"pdf-lib": "^1.17.1",
"react": "^18",
"react-dom": "^18",
"react-hook-form": "^7.50.1",
"react-icons": "^5.1.0",
"react-notion-x": "^6.16.0",
"react-resizable-panels": "^1.0.7",
"recoil": "^0.7.7",
Expand Down
17 changes: 17 additions & 0 deletions prisma/migrations/20240424002140_add_certificates/migration.sql
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;
2 changes: 2 additions & 0 deletions prisma/migrations/20240507073349_adds_certs/migration.sql
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;
13 changes: 13 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ model Course {
content CourseContent[]
purchasedBy UserPurchases[]
bookmarks Bookmark[]
certificate Certificate[]
certIssued Boolean @default(false)
}

model UserPurchases {
Expand Down Expand Up @@ -62,6 +64,16 @@ model CourseContent {
@@id([courseId, contentId])
}

model Certificate {
id String @id @default(cuid())
user User @relation(fields: [userId], references: [id])
userId String
course Course @relation(fields: [courseId], references: [id])
courseId Int
@@unique([userId, courseId])
}

model NotionMetadata {
id Int @id @default(autoincrement())
contentId Int
Expand Down Expand Up @@ -134,6 +146,7 @@ model User {
appxUsername String?
questions Question[]
answers Answer[]
certificate Certificate[]
}

model DiscordConnect {
Expand Down
33 changes: 33 additions & 0 deletions prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,38 @@ async function seedVideoMetadata() {
}
}

async function seedPurchases() {
try {
await db.userPurchases.create({
data: {
userId: '1',
courseId: 1,
},
});
await db.userPurchases.create({
data: {
userId: '2',
courseId: 1,
},
});
await db.userPurchases.create({
data: {
userId: '1',
courseId: 2,
},
});
await db.userPurchases.create({
data: {
userId: '2',
courseId: 2,
},
});
} catch (error) {
console.error('Error while seeding purchases');
throw error;
}
}

async function seedDatabase() {
try {
await seedUsers();
Expand All @@ -223,6 +255,7 @@ async function seedDatabase() {
await seedCourseContent();
await seedNotionMetadata();
await seedVideoMetadata();
await seedPurchases();
} catch (error) {
console.error('Error seeding database:', error);
throw error;
Expand Down
123 changes: 123 additions & 0 deletions src/actions/certificate/generateCertificate.ts
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;
};
Binary file added src/app/api/certificate/certiTemplate.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
104 changes: 104 additions & 0 deletions src/app/api/certificate/get/route.ts
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 });
}
}
Binary file added src/app/api/certificate/sign.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions src/app/certificate/page.tsx
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;
Loading

0 comments on commit e45a8b7

Please sign in to comment.