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

Added Automated Contribution Certificate #318

Merged
merged 4 commits into from
Feb 12, 2025
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
7 changes: 7 additions & 0 deletions front-end/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions front-end/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"dotenv": "^16.4.7",
"framer-motion": "^11.18.1",
"gsap": "^3.12.7",
"html-to-image": "^1.11.11",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^0.475.0",
"react": "^18.2.0",
Expand Down
110 changes: 110 additions & 0 deletions front-end/src/pages/CertificateGenerator.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import React, { useRef, useEffect, useState } from "react";
import { toPng } from "html-to-image";
import backgroundImage from "../assets/logo.png";

const CertificateGenerator = ({ username }) => {
const certificateRef = useRef(null);
const [profile, setProfile] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

useEffect(() => {
const fetchGitHubProfile = async () => {
if (!username) {
setError("Username is missing.");
return;
}

setLoading(true);
setError(null);

try {
const response = await fetch(`https://api.github.com/users/${username}`);
if (!response.ok) throw new Error("GitHub profile not found");

const data = await response.json();
setProfile(data);
} catch (err) {
setError(err.message);
setProfile(null);
} finally {
setLoading(false);
}
};

fetchGitHubProfile();
}, [username]);

const handleDownloadCertificate = async () => {
if (certificateRef.current) {
const dataUrl = await toPng(certificateRef.current);
const link = document.createElement("a");
link.href = dataUrl;
link.download = `${username}_certificate.png`;
link.click();
}
};

return (
<div className="flex flex-col items-center">
<div
ref={certificateRef}
className="w-[650px] h-[450px] border-4 border-blue-600 bg-white p-8 shadow-2xl text-gray-900 relative rounded-xl flex flex-col items-center overflow-hidden"
>
<div
className="absolute inset-0 bg-cover bg-center opacity-20"
style={{ backgroundImage: `url(${backgroundImage})` }}
></div>

<h1 className="text-3xl font-bold text-blue-700 mb-2 relative">
Certificate of Appreciation
</h1>
<p className="text-lg text-gray-700 relative">This is proudly presented to</p>

{loading && <p className="text-gray-500 mt-4 relative">Fetching GitHub Profile...</p>}
{error && <p className="text-red-500 mt-4 relative">{error}</p>}

{profile && !loading && (
<div className="flex flex-col items-center mt-4 relative">
<a href={profile.html_url} target="_blank" rel="noopener noreferrer">
<img
src={profile.avatar_url}
alt={`${username}'s GitHub`}
className="w-20 h-20 rounded-full border-4 border-blue-500 shadow-md"
crossOrigin="anonymous"
/>
</a>
<h2 className="mt-2 text-2xl font-semibold text-gray-900">
{profile.name || username}
</h2>
<p className="text-gray-600">@{username}</p>
</div>
)}

<p className="mt-4 text-lg text-gray-700 text-center px-4 relative">
For valuable contributions to react-blog Project in Social Winter of Code (SWoC) from January 1, 2025 to March 1, 2025.
</p>

<div className="mt-8 flex justify-between w-full px-6 text-gray-700 relative">
<div className="text-left">
<p className="font-semibold text-sm">Project Mentor</p>
<p className="text-sm">OkenHaha</p>
</div>
<div className="text-right">
<p className="font-semibold text-sm">Date</p>
<p className="text-sm">{new Date().toLocaleDateString()}</p>
</div>
</div>
</div>

<button
onClick={handleDownloadCertificate}
className="mt-6 bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-5 rounded-lg shadow-md transition duration-200"
>
Download Certificate
</button>
</div>
);
};

export default CertificateGenerator;
101 changes: 63 additions & 38 deletions front-end/src/pages/Contributors.jsx
Original file line number Diff line number Diff line change
@@ -1,84 +1,109 @@
import React, { useEffect, useState } from 'react';
import { getContributors } from '../components/contributors/contribution.js';
import { useWindowSize } from 'react-use';
import Confetti from 'react-confetti';
import React, { useEffect, useState } from "react";
import CertificateGenerator from "./CertificateGenerator";
import { getContributors } from "../components/contributors/contribution.js";
import { useWindowSize } from "react-use";
import Confetti from "react-confetti";
import { XCircle } from "lucide-react"; // Import icon for close button

const Contributors = () => {
const [data, setData] = useState([]);
const [showConfetti, setShowConfetti] = useState(true);
const [selectedContributor, setSelectedContributor] = useState(null);
const { width, height } = useWindowSize();

const getData = async () => {
const res = await getContributors({});
if (res) {
setData(res);
}
};

useEffect(() => {
const getData = async () => {
const res = await getContributors({});
if (res) {
setData(res);
}
};

getData();
const timer = setTimeout(() => {
setShowConfetti(false);
}, 5000);
return () => clearTimeout(timer);
}, []);

const handleAddCertificate = (contributor) => {
setSelectedContributor(contributor);
};

return (
<div className="min-h-screen bg-gradient-to-b from-white to-gray-100 dark:from-gray-900 dark:to-gray-800 relative pt-14">
<div className="min-h-screen bg-gradient-to-br from-gray-100 to-gray-300 dark:from-gray-900 dark:to-gray-800 relative pt-14">
{showConfetti && (
<Confetti
width={width}
height={height}
recycle={false}
numberOfPieces={200}
colors={['#3b82f6', '#1d4ed8', '#FFB800', '#2563eb']}
/>
<Confetti width={width} height={height} recycle={false} numberOfPieces={200} colors={["#3b82f6", "#1d4ed8", "#FFB800", "#2563eb"]} />
)}
<div className="container mx-auto px-4 py-12">

<div className="container mx-auto px-6 py-12">
<div className="max-w-4xl mx-auto text-center mb-12">
<h1 className="text-5xl font-extrabold mb-6 text-blue-700 dark:text-blue-400">
<h1 className="text-5xl font-extrabold mb-4 bg-clip-text text-transparent bg-gradient-to-r from-blue-600 to-indigo-400 dark:from-yellow-400 dark:to-orange-500">
Contributors
</h1>
<p className="text-lg text-gray-700 dark:text-gray-300">
Meet the brilliant minds who brought this project to life!
</p>
</div>

{/* Contributor Cards */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-8">
{data.map((item) => (
<a
key={item.id}
href={item.html_url}
target="_blank"
rel="noopener noreferrer"
className="group"
>
<div className="bg-white dark:bg-gray-800 p-6 border-2 border-gray-200 dark:border-gray-700 rounded-lg
transform transition-all duration-200 hover:scale-105 hover:border-blue-500 dark:hover:border-yellow-300">
<div key={item.id} className="group">
<div className="bg-white/80 dark:bg-gray-800/80 backdrop-blur-md p-6 border-2 border-gray-200 dark:border-gray-700 rounded-xl
shadow-lg transform transition-all duration-300 hover:scale-105 hover:border-blue-500 dark:hover:border-yellow-300">
<div className="relative flex flex-col items-center">
{/* Contributor Avatar */}
<img
src={item.avatar_url}
className="w-24 h-24 rounded-full border-4 border-gray-200 dark:border-gray-700
group-hover:border-blue-500 dark:group-hover:border-yellow-300 transition-colors duration-200"
group-hover:border-blue-500 dark:group-hover:border-yellow-300 transition-all duration-200 shadow-lg"
alt={`${item.login}'s avatar`}
/>
<span className="absolute -top-2 -right-2 bg-blue-600 dark:bg-yellow-500 text-white
dark:text-gray-900 text-sm font-bold px-3 py-1 rounded-full min-w-[2rem]">

{/* Contribution Badge */}
<span className="absolute -top-2 -right-2 bg-gradient-to-r from-blue-500 to-indigo-600 dark:from-yellow-500 dark:to-orange-400 text-white text-sm font-bold px-3 py-1 rounded-full shadow-md">
{item.contributions}
</span>
<span className="mt-4 text-lg font-medium text-gray-800 dark:text-gray-200
group-hover:text-blue-600 dark:group-hover:text-yellow-300 transition-colors duration-200">

{/* Contributor Name */}
<span className="mt-4 text-lg font-semibold text-gray-800 dark:text-gray-200 group-hover:text-blue-600 dark:group-hover:text-yellow-300 transition-all">
{item.login}
</span>

{/* Certificate Button */}
<button
className="mt-4 bg-gradient-to-r from-blue-500 to-indigo-600 hover:from-blue-600 hover:to-indigo-700 text-white font-semibold py-2 px-4 rounded-lg shadow-md
transition-all duration-200 dark:from-yellow-500 dark:to-orange-500 dark:hover:from-yellow-600 dark:hover:to-orange-600"
onClick={() => handleAddCertificate(item)}
>
Certificate
</button>
</div>
</div>
</a>
</div>
))}
</div>
</div>

{/* Certificate Generator Modal */}
{selectedContributor && (
<div className="fixed inset-0 flex items-center justify-center bg-black/60 backdrop-blur-md z-50 p-4">
<div className="bg-white dark:bg-gray-900 p-6 rounded-xl shadow-2xl max-w-lg w-full text-center relative">
{/* Close Button */}
<button className="absolute top-3 right-3 text-red-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200 transition-all" onClick={() => setSelectedContributor(null)}>
<XCircle className="w-6 h-6" />
</button>

<h2 className="text-2xl font-bold mb-4 text-blue-700 dark:text-yellow-400">
Certificate for {selectedContributor.login}
</h2>
<CertificateGenerator username={selectedContributor.login} />
</div>
</div>
)}
</div>
);
};

export default Contributors;
export default Contributors;
12 changes: 11 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.