Skip to content

Commit 545b0f0

Browse files
certifications
1 parent 8664a70 commit 545b0f0

14 files changed

Lines changed: 649 additions & 130 deletions

package-lock.json

Lines changed: 23 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
"react-markdown": "^10.1.0",
2828
"react-router-dom": "^7.6.2",
2929
"socket.io-client": "^4.8.1",
30-
"tailwindcss": "^4.1.10"
30+
"tailwindcss": "^4.1.10",
31+
"usehooks-ts": "^3.1.1"
3132
},
3233
"devDependencies": {
3334
"@eslint/js": "^9.25.0",
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { useState } from 'react';
2+
import { useQuery } from '@tanstack/react-query';
3+
import { motion } from 'framer-motion';
4+
import { Link } from 'react-router-dom';
5+
import { FiAward } from 'react-icons/fi';
6+
import { getCertificates } from '../services/certificateService';
7+
import SectionHeader from './ui/SectionHeader';
8+
import CertificateCard from './certificates/CertificateCard';
9+
import Loader from './ui/Loader';
10+
11+
export default function CertificatesSection() {
12+
const [inView, setInView] = useState(false);
13+
14+
const { data: certResponse, isLoading } = useQuery({
15+
queryKey: ['publicCertificates', 'featured'],
16+
queryFn: () => getCertificates({ limit: 3 }),
17+
enabled: inView,
18+
staleTime: 1000 * 60 * 10,
19+
});
20+
21+
const certificates = certResponse?.data || [];
22+
23+
return (
24+
<motion.section
25+
id="certificates"
26+
className="py-20 md:py-28 bg-white"
27+
onViewportEnter={() => setInView(true)}
28+
viewport={{ once: true, amount: 0.1 }}
29+
>
30+
<div className="container mx-auto px-6">
31+
<motion.div
32+
initial={{ opacity: 0, y: 20 }}
33+
whileInView={{ opacity: 1, y: 0 }}
34+
viewport={{ once: true }}
35+
transition={{ duration: 0.5 }}
36+
>
37+
<SectionHeader
38+
title="Certifications"
39+
description="Lessons I learn, certificates I earn."
40+
/>
41+
</motion.div>
42+
43+
<div className="mt-12 min-h-[10rem]">
44+
{inView && isLoading && <Loader />}
45+
46+
{inView && !isLoading && certificates.length > 0 && (
47+
<motion.div
48+
variants={{ visible: { transition: { staggerChildren: 0.1 } } }}
49+
initial="hidden"
50+
animate="visible"
51+
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
52+
>
53+
{certificates.map((cert: any) => (
54+
<CertificateCard key={cert.id} certificate={cert} />
55+
))}
56+
</motion.div>
57+
)}
58+
59+
{inView && !isLoading && certificates.length === 0 && (
60+
<div className="text-center text-slate-400 py-10">
61+
<FiAward size={40} className="mx-auto mb-2 opacity-50"/>
62+
<p>Certifications coming soon.</p>
63+
</div>
64+
)}
65+
</div>
66+
67+
{certificates.length > 0 && (
68+
<div className="mt-12 text-center">
69+
<Link to="/certificates" className="font-semibold text-blue-600 hover:text-blue-800 transition-colors inline-flex items-center gap-1">
70+
View All Certifications &rarr;
71+
</Link>
72+
</div>
73+
)}
74+
</div>
75+
</motion.section>
76+
);
77+
}

src/components/FeaturedProjectsSection.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { useQuery } from '@tanstack/react-query';
33
import { motion } from 'framer-motion';
44
import { getRecentProjects } from '../services/projectService';
55
import ProjectCard from './projects/ProjectCard';
6-
import ProjectDetailModal from './projects/ProjectDetailModal';
76
import { FiAlertTriangle } from 'react-icons/fi';
87
import { Link } from 'react-router-dom';
98
import SectionHeader from './ui/SectionHeader';
@@ -57,7 +56,6 @@ export default function FeaturedProjectsSection() {
5756
</Link>
5857
</div>
5958
</div>
60-
<ProjectDetailModal isOpen={!!selectedProject} onClose={() => setSelectedProject(null)} project={selectedProject} />
6159
</motion.section>
6260
);
6361
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { motion } from 'framer-motion';
2+
import { Link } from 'react-router-dom';
3+
import { FiAward, FiArrowUpRight } from 'react-icons/fi';
4+
5+
interface Certificate {
6+
id: string;
7+
name: string;
8+
certificateFile: string;
9+
// category: { name: string };
10+
issuer?: string;
11+
}
12+
13+
export default function CertificateCard({ certificate }: { certificate: Certificate }) {
14+
return (
15+
<motion.div
16+
layout
17+
initial={{ opacity: 0, y: 20 }}
18+
animate={{ opacity: 1, y: 0 }}
19+
whileHover="hover"
20+
className="group relative h-[260px] w-full rounded-2xl overflow-hidden cursor-pointer"
21+
>
22+
<Link to={`/certificates/${certificate.id}`} className="block h-full w-full">
23+
24+
<div className="absolute inset-0 bg-slate-50">
25+
<div className="absolute -top-10 -right-10 w-40 h-40 bg-blue-200/40 rounded-full blur-3xl group-hover:bg-blue-300/50 transition-colors duration-500" />
26+
<div className="absolute top-20 -left-10 w-40 h-40 bg-purple-200/40 rounded-full blur-3xl group-hover:bg-purple-300/50 transition-colors duration-500" />
27+
<div className="absolute inset-0 opacity-[0.4] bg-[radial-gradient(#94a3b8_1px,transparent_1px)] [background-size:12px_12px]" />
28+
</div>
29+
30+
<div className="absolute inset-0 pb-16 flex items-center justify-center p-6">
31+
<motion.div
32+
variants={{
33+
hover: { y: -5, scale: 1.05, filter: "drop-shadow(0 20px 30px rgba(0,0,0,0.15))" },
34+
rest: { y: 0, scale: 1, filter: "drop-shadow(0 10px 15px rgba(0,0,0,0.1))" }
35+
}}
36+
initial="rest"
37+
transition={{ duration: 0.3 }}
38+
className="relative z-10 w-full h-full flex items-center justify-center"
39+
>
40+
{certificate.certificateFile ? (
41+
<img
42+
src={certificate.certificateFile}
43+
alt={certificate.name}
44+
className="max-w-full max-h-full object-contain rounded-lg border border-white/50"
45+
/>
46+
) : (
47+
<FiAward className="text-slate-400/50 text-6xl" />
48+
)}
49+
</motion.div>
50+
</div>
51+
52+
<div className="absolute bottom-3 left-3 right-3 h-[72px] bg-white/70 backdrop-blur-md rounded-xl border border-white/60 shadow-sm p-3 flex flex-col justify-center transition-colors duration-300 group-hover:bg-white/90">
53+
54+
<div className='pt-7 p-1'>
55+
<h3 className="text-xs sm:text-sm text-center font-bold text-slate-800 leading-tight line-clamp-2" title={certificate.name}>
56+
{certificate.name}
57+
</h3>
58+
59+
<div className="text-[9px] sm:text-[10px] text-center font-bold uppercase tracking-widest text-blue-600 mb-1">
60+
{certificate.issuer}
61+
</div>
62+
</div>
63+
</div>
64+
<div>
65+
<motion.div
66+
className="absolute top-3 right-3 bg-white/70 backdrop-blur-md rounded-full p-2 border border-white/60 shadow-sm transition-opacity duration-300"
67+
>
68+
<FiArrowUpRight className="text-slate-600" />
69+
</motion.div>
70+
</div>
71+
72+
<div className="absolute inset-0 bg-gradient-to-tr from-transparent via-white/20 to-transparent translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700 pointer-events-none z-20" />
73+
74+
</Link>
75+
</motion.div>
76+
);
77+
}

src/components/projects/ProjectDetailModal.tsx

Lines changed: 0 additions & 75 deletions
This file was deleted.

src/components/ui/MobileMenu.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ interface MobileMenuProps {
1919
export default function MobileMenu({ isOpen, onClose, scrollToSection, links }: MobileMenuProps) {
2020
const navLinks = [
2121
{ id: 'about', label: 'About' },
22-
{ id: 'projects', label: 'Projects' },
2322
{ id: 'skills', label: 'Skills' },
23+
{ id: 'projects', label: 'Projects' },
2424
{ id: 'journey', label: 'Journey' },
25+
{ id: 'certificates', label: 'Certifications' },
2526
];
2627

2728
const menuVariants = {

src/layouts/Header.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export default function Header() {
2929
{ id: 'skills', label: 'Skills' },
3030
{ id: 'projects', label: 'Projects' },
3131
{ id: 'journey', label: 'Journey' },
32+
{ id: 'certificates', label: 'Certifications' },
3233
{ id: 'contact', label: 'Contact' },
3334
];
3435

0 commit comments

Comments
 (0)