11"use client" ;
22
33import { motion } from "framer-motion" ;
4+ import { useState } from "react" ;
45import {
56 categoryStaggerVariants ,
67 categorySlideInVariants ,
@@ -10,13 +11,19 @@ import {
1011 sectionVariants
1112} from "@/lib" ;
1213import { type SkillCategory } from "@/content" ;
13- import { Icon , OthersIconList } from "@/components/micro" ;
14+ import { Icon , OthersIconList , Button } from "@/components/micro" ;
1415
1516export interface TechnicalExpertiseProps {
1617 categories ?: SkillCategory [ ] ;
1718}
1819
1920export function TechnicalExpertise ( { categories } : TechnicalExpertiseProps ) {
21+ const [ showAllCategories , setShowAllCategories ] = useState ( false ) ;
22+ const categoryCount = categories ?. length || 0 ;
23+
24+ const initialCategories = categories ?. slice ( 0 , 4 ) || [ ] ;
25+ const shouldShowMoreButton = categoryCount > 4 ;
26+
2027 return (
2128 < motion . section
2229 aria-label = "Technical Expertise"
@@ -34,84 +41,186 @@ export function TechnicalExpertise({ categories }: TechnicalExpertiseProps) {
3441 < h2 className = "font-mono text-[2rem] md:text-[2.5rem] font-semibold text-[#24292f] dark:text-[#f0f6fc]" >
3542 Technical Expertise
3643 </ h2 >
37- < motion . div
38- className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-8 mt-12"
39- data-testid = "categories-grid"
40- variants = { categoryStaggerVariants }
41- >
42- { categories ?. map ( ( category , index ) => (
43- < motion . div
44- key = { index }
45- data-testid = "category-card"
46- className = "bg-[#f6f8fa] dark:bg-[#21262d] border border-muted/20 rounded-lg p-6 hover:shadow-lg transition-all duration-300"
47- variants = { categorySlideInVariants }
48- whileHover = "hover"
49- >
50- < h3 className = "font-mono text-xl font-semibold text-[#24292f] dark:text-[#f0f6fc]" >
51- { category . title }
52- </ h3 >
53- { category . skills && category . skills . length > 0 && (
54- < motion . div
55- className = "mt-4 space-y-3"
56- data-testid = "skills-list"
57- variants = { staggerVariants }
58- >
59- { category . skills . map ( ( skill , skillIndex ) => (
60- < motion . div
61- key = { skillIndex }
62- variants = { fadeVariants }
63- className = "group relative flex items-start gap-3"
64- data-testid = "skill-item"
65- >
66- { skill . icon && (
67- < Icon
68- name = { skill . icon }
69- alt = { `${ skill . name } icon` }
70- size = { 32 }
71- className = "flex-shrink-0 mt-0.5"
72- />
73- ) }
74- < div className = "flex-1 min-w-0" >
75- < div
76- className = "font-sans text-sm font-medium text-[#24292f] dark:text-[#f0f6fc] cursor-pointer"
77- data-testid = "skill-name"
78- title = { skill . years ? `${ skill . years } years of experience` : undefined }
79- >
80- { skill . name }
81- { skill . years && (
82- < span className = "ml-2 text-xs text-[#656d76] dark:text-[#8b949e] opacity-0 group-hover:opacity-100 transition-opacity duration-200" >
83- ({ skill . years } years)
84- </ span >
85- ) }
44+
45+ { ! showAllCategories ? (
46+ < motion . div
47+ className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-8 mt-12"
48+ data-testid = "categories-grid"
49+ variants = { categoryStaggerVariants }
50+ >
51+ { initialCategories ?. map ( ( category , index ) => (
52+ < motion . div
53+ key = { index }
54+ data-testid = "category-card"
55+ className = "bg-[#f6f8fa] dark:bg-[#21262d] border border-muted/20 rounded-lg p-6 hover:shadow-lg transition-all duration-300"
56+ variants = { categorySlideInVariants }
57+ whileHover = "hover"
58+ >
59+ < h3 className = "font-mono text-xl font-semibold text-[#24292f] dark:text-[#f0f6fc]" >
60+ { category . title }
61+ </ h3 >
62+ { category . skills && category . skills . length > 0 && (
63+ < motion . div
64+ className = "mt-4 space-y-3"
65+ data-testid = "skills-list"
66+ variants = { staggerVariants }
67+ >
68+ { category . skills . map ( ( skill , skillIndex ) => (
69+ < motion . div
70+ key = { skillIndex }
71+ variants = { fadeVariants }
72+ className = "group relative flex items-start gap-3"
73+ data-testid = "skill-item"
74+ >
75+ { skill . icon && (
76+ < Icon
77+ name = { skill . icon }
78+ alt = { `${ skill . name } icon` }
79+ size = { 32 }
80+ className = "flex-shrink-0 mt-0.5"
81+ />
82+ ) }
83+ < div className = "flex-1 min-w-0" >
84+ < div
85+ className = "font-sans text-sm font-medium text-[#24292f] dark:text-[#f0f6fc] cursor-pointer"
86+ data-testid = "skill-name"
87+ title = { skill . years ? `${ skill . years } years of experience` : undefined }
88+ >
89+ { skill . name }
90+ { skill . years && (
91+ < span className = "ml-2 text-xs text-[#656d76] dark:text-[#8b949e] opacity-0 group-hover:opacity-100 transition-opacity duration-200" >
92+ ({ skill . years } years)
93+ </ span >
94+ ) }
95+ </ div >
96+ < motion . div
97+ className = "mt-1 relative h-2 bg-[#e1e4e8] dark:bg-[#30363d] rounded-full"
98+ data-testid = "proficiency-bar"
99+ variants = { scaleVariants }
100+ >
101+ < motion . div
102+ className = "absolute top-0 left-0 h-full bg-[#0969da] dark:bg-[#58a6ff] rounded-full"
103+ data-testid = "proficiency-fill"
104+ data-skill-name = { skill . name }
105+ initial = { { width : "0%" } }
106+ whileInView = { { width : `${ skill . proficiency } %` } }
107+ viewport = { { once : true } }
108+ transition = { {
109+ duration : 1.2 ,
110+ ease : "easeOut" ,
111+ delay : 0.8
112+ } }
113+ />
114+ </ motion . div >
115+ </ div >
116+ </ motion . div >
117+ ) ) }
118+ </ motion . div >
119+ ) }
120+ < OthersIconList others = { category . others } />
121+ </ motion . div >
122+ ) ) }
123+ </ motion . div >
124+ ) : (
125+ < motion . div
126+ className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-8 mt-12"
127+ data-testid = "categories-grid"
128+ variants = { categoryStaggerVariants }
129+ initial = "hidden"
130+ animate = "visible"
131+ >
132+ { categories ?. map ( ( category , index ) => (
133+ < motion . div
134+ key = { index }
135+ data-testid = "category-card"
136+ className = "bg-[#f6f8fa] dark:bg-[#21262d] border border-muted/20 rounded-lg p-6 hover:shadow-lg transition-all duration-300"
137+ variants = { categorySlideInVariants }
138+ whileHover = "hover"
139+ >
140+ < h3 className = "font-mono text-xl font-semibold text-[#24292f] dark:text-[#f0f6fc]" >
141+ { category . title }
142+ </ h3 >
143+ { category . skills && category . skills . length > 0 && (
144+ < motion . div
145+ className = "mt-4 space-y-3"
146+ data-testid = "skills-list"
147+ variants = { staggerVariants }
148+ >
149+ { category . skills . map ( ( skill , skillIndex ) => (
150+ < motion . div
151+ key = { skillIndex }
152+ variants = { fadeVariants }
153+ className = "group relative flex items-start gap-3"
154+ data-testid = "skill-item"
155+ >
156+ { skill . icon && (
157+ < Icon
158+ name = { skill . icon }
159+ alt = { `${ skill . name } icon` }
160+ size = { 32 }
161+ className = "flex-shrink-0 mt-0.5"
162+ />
163+ ) }
164+ < div className = "flex-1 min-w-0" >
165+ < div
166+ className = "font-sans text-sm font-medium text-[#24292f] dark:text-[#f0f6fc] cursor-pointer"
167+ data-testid = "skill-name"
168+ title = { skill . years ? `${ skill . years } years of experience` : undefined }
169+ >
170+ { skill . name }
171+ { skill . years && (
172+ < span className = "ml-2 text-xs text-[#656d76] dark:text-[#8b949e] opacity-0 group-hover:opacity-100 transition-opacity duration-200" >
173+ ({ skill . years } years)
174+ </ span >
175+ ) }
176+ </ div >
177+ < motion . div
178+ className = "mt-1 relative h-2 bg-[#e1e4e8] dark:bg-[#30363d] rounded-full"
179+ data-testid = "proficiency-bar"
180+ variants = { scaleVariants }
181+ >
182+ < motion . div
183+ className = "absolute top-0 left-0 h-full bg-[#0969da] dark:bg-[#58a6ff] rounded-full"
184+ data-testid = "proficiency-fill"
185+ data-skill-name = { skill . name }
186+ initial = { { width : "0%" } }
187+ whileInView = { { width : `${ skill . proficiency } %` } }
188+ viewport = { { once : true } }
189+ transition = { {
190+ duration : 1.2 ,
191+ ease : "easeOut" ,
192+ delay : 0.8
193+ } }
194+ />
195+ </ motion . div >
86196 </ div >
87- < motion . div
88- className = "mt-1 relative h-2 bg-[#e1e4e8] dark:bg-[#30363d] rounded-full"
89- data-testid = "proficiency-bar"
90- variants = { scaleVariants }
91- >
92- < motion . div
93- className = "absolute top-0 left-0 h-full bg-[#0969da] dark:bg-[#58a6ff] rounded-full"
94- data-testid = "proficiency-fill"
95- data-skill-name = { skill . name }
96- initial = { { width : "0%" } }
97- whileInView = { { width : `${ skill . proficiency } %` } }
98- viewport = { { once : true } }
99- transition = { {
100- duration : 1.2 ,
101- ease : "easeOut" ,
102- delay : 0.8
103- } }
104- />
105- </ motion . div >
106- </ div >
107- </ motion . div >
108- ) ) }
109- </ motion . div >
110- ) }
111- < OthersIconList others = { category . others } />
112- </ motion . div >
113- ) ) }
114- </ motion . div >
197+ </ motion . div >
198+ ) ) }
199+ </ motion . div >
200+ ) }
201+ < OthersIconList others = { category . others } />
202+ </ motion . div >
203+ ) ) }
204+ </ motion . div >
205+ ) }
206+
207+ { shouldShowMoreButton && ! showAllCategories && (
208+ < motion . div
209+ className = "flex justify-end mt-8"
210+ data-testid = "more-button-container"
211+ initial = { { opacity : 0 , y : 20 } }
212+ animate = { { opacity : 1 , y : 0 } }
213+ transition = { { duration : 0.5 , delay : 0.5 } }
214+ >
215+ < Button
216+ variant = "link"
217+ onClick = { ( ) => setShowAllCategories ( true ) }
218+ data-testid = "more-categories-button"
219+ >
220+ more...
221+ </ Button >
222+ </ motion . div >
223+ ) }
115224 </ div >
116225 </ motion . section >
117226 ) ;
0 commit comments