11"use client" ;
22import { useState , useEffect } from "react" ;
33import { motion , AnimatePresence } from "framer-motion" ;
4- import { FiX , FiExternalLink , FiEye , FiCheckCircle } from "react-icons/fi" ;
4+ import { FiX , FiExternalLink , FiEye } from "react-icons/fi" ;
55import { certifications , education } from "@/data/portfolio" ;
66import { getIcon } from "./icons" ;
77
8+ /* compact card — smaller padding, smaller icon, single-line title, no hover-expand */
89function Card ( { item, i, onOpen } ) {
910 const Icon = getIcon ( item . icon ) ;
1011 const hasProof = Boolean ( item . image || item . verifyUrl ) ;
1112 return (
12- < motion . div
13+ < motion . button
14+ type = "button"
1315 data-hover
14- initial = { { opacity : 0 , y : 20 } }
16+ initial = { { opacity : 0 , y : 12 } }
1517 whileInView = { { opacity : 1 , y : 0 } }
1618 viewport = { { once : true } }
17- transition = { { duration : 0.45 , delay : ( i % 4 ) * 0.06 } }
18- whileHover = { { y : - 6 } }
19+ transition = { { duration : 0.35 , delay : ( i % 6 ) * 0.04 } }
20+ whileHover = { { y : - 3 } }
1921 onClick = { ( ) => hasProof && onOpen ( item ) }
20- className = { `group relative glass rounded-2xl p-5 overflow-hidden transition-colors ${
21- hasProof ? "cursor-pointer hover:border-accent/60" : "hover:border-accent/40"
22- } `}
22+ className = { `glass rounded-xl p-3 flex items-center gap-3 text-left transition-colors ${ hasProof ? "cursor-pointer hover:border-accent/60" : "" } ` }
2323 >
24- < div className = "flex items-center gap-3" >
25- < span className = "w-11 h-11 shrink-0 rounded-xl bg-gradient-to-br from-accent to-accent2 flex items-center justify-center text-bg" >
26- < Icon size = { 20 } />
24+ < span className = "w-9 h-9 shrink-0 rounded-lg bg-gradient-to-br from-accent to-accent2 flex items-center justify-center text-bg" >
25+ < Icon size = { 16 } />
26+ </ span >
27+ < span className = "min-w-0 flex-1" >
28+ < span className = "block text-[13px] font-semibold leading-tight truncate" > { item . title } </ span >
29+ < span className = "block text-[11px] text-muted truncate" >
30+ { item . issuer } { item . year ? ` · ${ item . year } ` : "" }
2731 </ span >
28- < div className = "min-w-0" >
29- < h4 className = "font-semibold leading-tight" > { item . title } </ h4 >
30- < p className = "text-xs text-muted truncate" >
31- { item . issuer } { item . year ? ` · ${ item . year } ` : "" }
32- </ p >
33- </ div >
34- </ div >
35- < div className = "grid grid-rows-[0fr] group-hover:grid-rows-[1fr] transition-all duration-300 ease-out" >
36- < div className = "overflow-hidden" >
37- < p className = "text-muted text-sm mt-3 leading-relaxed" > { item . detail } </ p >
38- </ div >
39- </ div >
40- { hasProof && (
41- < span className = "mt-3 inline-flex items-center gap-1.5 text-xs font-medium text-accent" >
42- < FiEye size = { 13 } /> View certificate
43- </ span >
44- ) }
45- </ motion . div >
32+ </ span >
33+ { hasProof && < FiEye size = { 13 } className = "text-accent shrink-0" /> }
34+ </ motion . button >
4635 ) ;
4736}
4837
49- function CertModal ( { item, onClose } ) {
38+ function Modal ( { item, onClose } ) {
5039 const isPdf = item ?. image ?. toLowerCase ( ) . endsWith ( ".pdf" ) ;
5140 useEffect ( ( ) => {
5241 const onKey = ( e ) => e . key === "Escape" && onClose ( ) ;
53- document . addEventListener ( "keydown" , onKey ) ;
54- document . body . style . overflow = "hidden" ;
55- return ( ) => {
56- document . removeEventListener ( "keydown" , onKey ) ;
57- document . body . style . overflow = "" ;
58- } ;
42+ document . addEventListener ( "keydown" , onKey ) ; document . body . style . overflow = "hidden" ;
43+ return ( ) => { document . removeEventListener ( "keydown" , onKey ) ; document . body . style . overflow = "" ; } ;
5944 } , [ onClose ] ) ;
60-
6145 return (
62- < motion . div
63- initial = { { opacity : 0 } }
64- animate = { { opacity : 1 } }
65- exit = { { opacity : 0 } }
66- onClick = { onClose }
67- className = "fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm"
68- >
69- < motion . div
70- initial = { { scale : 0.94 , y : 12 , opacity : 0 } }
71- animate = { { scale : 1 , y : 0 , opacity : 1 } }
72- exit = { { scale : 0.94 , y : 12 , opacity : 0 } }
73- transition = { { type : "spring" , stiffness : 260 , damping : 24 } }
74- onClick = { ( e ) => e . stopPropagation ( ) }
75- className = "relative w-full max-w-2xl glass rounded-2xl border border-line overflow-hidden"
76- >
46+ < motion . div initial = { { opacity : 0 } } animate = { { opacity : 1 } } exit = { { opacity : 0 } } onClick = { onClose }
47+ className = "fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm" >
48+ < motion . div initial = { { scale : 0.95 , y : 10 , opacity : 0 } } animate = { { scale : 1 , y : 0 , opacity : 1 } } exit = { { scale : 0.95 , y : 10 , opacity : 0 } }
49+ onClick = { ( e ) => e . stopPropagation ( ) } className = "relative w-full max-w-2xl glass rounded-2xl border border-line overflow-hidden" >
7750 < div className = "flex items-start justify-between gap-4 p-5 border-b border-line" >
78- < div className = "min-w-0" >
79- < h3 className = "font-bold truncate" > { item . title } </ h3 >
80- < p className = "text-xs text-muted truncate" > { item . issuer } { item . year ? ` · ${ item . year } ` : "" } </ p >
81- </ div >
82- < button data-hover onClick = { onClose } aria-label = "Close"
83- className = "w-9 h-9 shrink-0 rounded-full border border-line flex items-center justify-center hover:border-accent hover:text-accent transition-colors" >
84- < FiX size = { 18 } />
85- </ button >
51+ < div className = "min-w-0" > < h3 className = "font-bold truncate" > { item . title } </ h3 >
52+ < p className = "text-xs text-muted truncate" > { item . issuer } { item . year ? ` · ${ item . year } ` : "" } </ p > </ div >
53+ < button data-hover onClick = { onClose } className = "w-9 h-9 shrink-0 rounded-full border border-line flex items-center justify-center hover:border-accent hover:text-accent transition-colors" > < FiX size = { 18 } /> </ button >
8654 </ div >
8755 < div className = "p-5" >
88- { item . image ? (
89- isPdf ? (
90- < object data = { item . image } type = "application/pdf" className = "w-full h-[60vh] rounded-lg bg-white" >
91- < p className = "text-muted text-sm" > Preview unavailable.{ " " }
92- < a href = { item . image } target = "_blank" rel = "noopener noreferrer" className = "text-accent hover:underline" > Open PDF ↗</ a >
93- </ p >
94- </ object >
95- ) : (
96- /* eslint-disable-next-line @next/next/no-img-element */
97- < img src = { item . image } alt = { item . title } className = "w-full max-h-[60vh] object-contain rounded-lg bg-white" />
98- )
99- ) : (
100- < div className = "text-center py-10" >
101- < FiCheckCircle className = "mx-auto text-accent mb-3" size = { 40 } />
102- < p className = "text-muted text-sm" > { item . detail } </ p >
103- </ div >
104- ) }
56+ { item . image
57+ ? ( isPdf
58+ ? < object data = { item . image } type = "application/pdf" className = "w-full h-[60vh] rounded-lg bg-white" > < p className = "text-muted text-sm" > Preview unavailable. < a href = { item . image } target = "_blank" rel = "noopener noreferrer" className = "text-accent hover:underline" > Open ↗</ a > </ p > </ object >
59+ : /* eslint-disable-next-line @next/next/no-img-element */ < img src = { item . image } alt = { item . title } className = "w-full max-h-[60vh] object-contain rounded-lg bg-white" /> )
60+ : < p className = "text-muted text-sm text-center py-8" > { item . detail } </ p > }
10561 </ div >
106- < div className = "flex flex-wrap items-center justify-end gap-3 p-5 border-t border-line" >
107- { item . image && (
108- < a href = { item . image } target = "_blank" rel = "noopener noreferrer" data-hover
109- className = "text-sm px-4 py-2 rounded-full border border-line hover:border-accent hover:text-accent transition-colors flex items-center gap-2" >
110- < FiEye size = { 15 } /> Open full
111- </ a >
112- ) }
113- { item . verifyUrl && (
114- < a href = { item . verifyUrl } target = "_blank" rel = "noopener noreferrer" data-hover
115- className = "text-sm px-4 py-2 rounded-full bg-accent text-bg font-semibold hover:opacity-90 transition flex items-center gap-2" >
116- < FiExternalLink size = { 15 } /> Verify credential
117- </ a >
118- ) }
62+ < div className = "flex items-center justify-end gap-3 p-5 border-t border-line" >
63+ { item . image && < a href = { item . image } target = "_blank" rel = "noopener noreferrer" data-hover className = "text-sm px-4 py-2 rounded-full border border-line hover:border-accent hover:text-accent transition-colors flex items-center gap-2" > < FiEye size = { 15 } /> Open full</ a > }
64+ { item . verifyUrl && < a href = { item . verifyUrl } target = "_blank" rel = "noopener noreferrer" data-hover className = "text-sm px-4 py-2 rounded-full bg-accent text-bg font-semibold hover:opacity-90 transition flex items-center gap-2" > < FiExternalLink size = { 15 } /> Verify</ a > }
11965 </ div >
12066 </ motion . div >
12167 </ motion . div >
@@ -124,46 +70,43 @@ function CertModal({ item, onClose }) {
12470
12571export default function Credentials ( ) {
12672 const [ active , setActive ] = useState ( null ) ;
73+
74+ // group by category
12775 const groups = [ ] ;
12876 const seen = { } ;
12977 ( certifications || [ ] ) . forEach ( ( c ) => {
130- const key = c . category || "Certifications" ;
131- if ( ! ( key in seen ) ) { seen [ key ] = groups . length ; groups . push ( { category : key , items : [ ] } ) ; }
132- groups [ seen [ key ] ] . items . push ( c ) ;
78+ const k = c . category || "Certifications" ;
79+ if ( ! ( k in seen ) ) { seen [ k ] = groups . length ; groups . push ( { category : k , items : [ ] } ) ; }
80+ groups [ seen [ k ] ] . items . push ( c ) ;
13381 } ) ;
13482
13583 return (
136- < section id = "credentials" className = "py-24 border-t border-line" >
84+ < section id = "credentials" className = "py-20 border-t border-line" >
13785 < div className = "container" >
138- < h2 className = "text-3xl sm:text-4xl font-bold mb-3 " > Certifications & Education</ h2 >
139- < p className = "text-muted mb-12 " > Click a certificate to preview or verify it.</ p >
86+ < h2 className = "text-3xl sm:text-4xl font-bold mb-1 " > Certifications & Education</ h2 >
87+ < p className = "text-muted mb-8 text-sm " > Click any certificate to preview or verify it.</ p >
14088
89+ { /* compact: 2 → 3 columns, small gaps, tight category spacing */ }
14190 { groups . map ( ( g ) => (
142- < div key = { g . category } className = "mb-12" >
143- < h3 className = "text-sm font-mono text-accent mb-4" > ★ { g . category . toUpperCase ( ) } </ h3 >
144- < div className = "grid sm:grid-cols-2 lg:grid-cols-4 gap-5" >
145- { g . items . map ( ( c , i ) => (
146- < Card key = { c . title } item = { c } i = { i } onOpen = { setActive } />
147- ) ) }
91+ < div key = { g . category } className = "mb-7" >
92+ < h3 className = "text-xs font-mono text-accent mb-3" > ★ { g . category . toUpperCase ( ) } </ h3 >
93+ < div className = "grid sm:grid-cols-2 lg:grid-cols-3 gap-3" >
94+ { g . items . map ( ( c , i ) => < Card key = { c . title } item = { c } i = { i } onOpen = { setActive } /> ) }
14895 </ div >
14996 </ div >
15097 ) ) }
15198
15299 { education ?. length > 0 && (
153100 < >
154- < h3 className = "text-sm font-mono text-accent mb-4" > 🎓 EDUCATION</ h3 >
155- < div className = "grid sm:grid-cols-2 lg:grid-cols-4 gap-5" >
156- { education . map ( ( e , i ) => (
157- < Card key = { e . title } item = { e } i = { i } onOpen = { setActive } />
158- ) ) }
101+ < h3 className = "text-xs font-mono text-accent mb-3" > 🎓 EDUCATION</ h3 >
102+ < div className = "grid sm:grid-cols-2 lg:grid-cols-3 gap-3" >
103+ { education . map ( ( e , i ) => < Card key = { e . title } item = { e } i = { i } onOpen = { setActive } /> ) }
159104 </ div >
160105 </ >
161106 ) }
162107 </ div >
163108
164- < AnimatePresence >
165- { active && < CertModal item = { active } onClose = { ( ) => setActive ( null ) } /> }
166- </ AnimatePresence >
109+ < AnimatePresence > { active && < Modal item = { active } onClose = { ( ) => setActive ( null ) } /> } </ AnimatePresence >
167110 </ section >
168111 ) ;
169112}
0 commit comments