@@ -6,9 +6,98 @@ import { IconTypes, LeftPanelOptions } from "@/components/Types/enum";
66import { LeftPanelProps } from "@/components/Types" ;
77import { useState , useEffect , useRef } from "react" ;
88import { motion , AnimatePresence , PanInfo } from "framer-motion" ;
9- import { FiMenu , FiX , FiChevronLeft } from "react-icons/fi" ;
9+ import { FiMenu , FiX , FiChevronLeft , FiChevronRight } from "react-icons/fi" ;
1010import { siteConfig } from "@/config/site-config" ;
1111
12+ const GoalCardStack = ( {
13+ controls = false ,
14+ swipe = true ,
15+ swipeAxis = "x" ,
16+ } : {
17+ controls ?: boolean ;
18+ swipe ?: boolean ;
19+ swipeAxis ?: "x" | "y" ;
20+ } ) => {
21+ const [ activeIndex , setActiveIndex ] = useState ( 0 ) ;
22+ const goals = siteConfig . currentGoalItems ;
23+ const total = goals . length ;
24+ const nextGoal = ( ) => setActiveIndex ( ( index ) => ( index + 1 ) % total ) ;
25+ const prevGoal = ( ) => setActiveIndex ( ( index ) => ( index - 1 + total ) % total ) ;
26+
27+ const handleSwipe = (
28+ _ : MouseEvent | TouchEvent | PointerEvent ,
29+ info : PanInfo ,
30+ ) => {
31+ const offset = swipeAxis === "y" ? info . offset . y : info . offset . x ;
32+ if ( offset < - 50 ) nextGoal ( ) ;
33+ if ( offset > 50 ) prevGoal ( ) ;
34+ } ;
35+
36+ return (
37+ < div
38+ className = "relative mt-6 h-40"
39+ onPointerDown = { ( event ) => event . stopPropagation ( ) }
40+ >
41+ { goals . map ( ( goal , index ) => {
42+ const stackIndex = ( index - activeIndex + total ) % total ;
43+ if ( stackIndex > 2 ) return null ;
44+
45+ return (
46+ < motion . div
47+ key = { goal . title }
48+ className = "absolute inset-x-0 top-0 h-32 rounded-2xl border border-white/10 p-5 text-white shadow-md"
49+ style = { {
50+ background : goal . color ,
51+ zIndex : 30 - stackIndex ,
52+ } }
53+ animate = { {
54+ y : stackIndex * 12 ,
55+ scale : 1 - stackIndex * 0.04 ,
56+ opacity : 1 - stackIndex * 0.18 ,
57+ } }
58+ drag = { swipe && stackIndex === 0 ? swipeAxis : false }
59+ dragConstraints = { { left : 0 , right : 0 , top : 0 , bottom : 0 } }
60+ dragElastic = { 0.25 }
61+ onDragEnd = { handleSwipe }
62+ whileTap = {
63+ swipe && stackIndex === 0 ? { cursor : "grabbing" } : undefined
64+ }
65+ >
66+ < p className = "text-sm font-medium text-white/75" > { goal . label } </ p >
67+ < p className = "mt-2 text-lg font-semibold leading-snug" >
68+ { goal . title }
69+ </ p >
70+ < p className = "mt-2 text-xs leading-relaxed text-white/75" >
71+ { goal . description }
72+ </ p >
73+ </ motion . div >
74+ ) ;
75+ } ) }
76+
77+ { controls && (
78+ < div className = "absolute bottom-0 right-0 z-40 flex gap-2" >
79+ < button
80+ type = "button"
81+ onClick = { prevGoal }
82+ className = "grid h-8 w-8 place-items-center rounded-lg border border-neutral-200 bg-white text-neutral-700 shadow-sm dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100"
83+ aria-label = "Previous goal"
84+ >
85+ < FiChevronLeft size = { 16 } />
86+ </ button >
87+ < button
88+ type = "button"
89+ onClick = { nextGoal }
90+ className = "grid h-8 w-8 place-items-center rounded-lg border border-neutral-200 bg-white text-neutral-700 shadow-sm dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100"
91+ aria-label = "Next goal"
92+ >
93+ < FiChevronRight size = { 16 } />
94+ </ button >
95+ </ div >
96+ ) }
97+ </ div >
98+ ) ;
99+ } ;
100+
12101const LeftPanel = ( { clickedCategory, handleInteraction } : LeftPanelProps ) => {
13102 const [ clicked , setClicked ] = useState ( LeftPanelOptions . ABOUT ) ;
14103 const [ isOpen , setIsOpen ] = useState ( true ) ;
@@ -37,27 +126,33 @@ const LeftPanel = ({ clickedCategory, handleInteraction }: LeftPanelProps) => {
37126
38127 useEffect ( ( ) => {
39128 const handleEscape = ( e : KeyboardEvent ) => {
40- if ( e . key === ' Escape' ) closeMenu ( ) ;
129+ if ( e . key === " Escape" ) closeMenu ( ) ;
41130 } ;
42-
131+
43132 if ( isOpen ) {
44- document . addEventListener ( ' keydown' , handleEscape ) ;
45- document . body . style . overflow = ' hidden' ;
133+ document . addEventListener ( " keydown" , handleEscape ) ;
134+ document . body . style . overflow = " hidden" ;
46135 }
47-
136+
48137 return ( ) => {
49- document . removeEventListener ( ' keydown' , handleEscape ) ;
50- document . body . style . overflow = ' unset' ;
138+ document . removeEventListener ( " keydown" , handleEscape ) ;
139+ document . body . style . overflow = " unset" ;
51140 } ;
52141 } , [ isOpen ] ) ;
53142
54143 // Handle drag to close
55- const handleDrag = ( event : MouseEvent | TouchEvent | PointerEvent , info : PanInfo ) => {
144+ const handleDrag = (
145+ event : MouseEvent | TouchEvent | PointerEvent ,
146+ info : PanInfo ,
147+ ) => {
56148 const progress = Math . max ( 0 , Math . min ( 1 , - info . offset . x / 250 ) ) ;
57149 setDragProgress ( progress ) ;
58150 } ;
59151
60- const handleDragEnd = ( event : MouseEvent | TouchEvent | PointerEvent , info : PanInfo ) => {
152+ const handleDragEnd = (
153+ event : MouseEvent | TouchEvent | PointerEvent ,
154+ info : PanInfo ,
155+ ) => {
61156 if ( info . offset . x < - 100 || info . velocity . x < - 500 ) {
62157 closeMenu ( ) ;
63158 }
@@ -67,7 +162,7 @@ const LeftPanel = ({ clickedCategory, handleInteraction }: LeftPanelProps) => {
67162 return (
68163 < >
69164 { /* Mobile Header with Hamburger */ }
70- < motion . div
165+ < motion . div
71166 className = "xl:hidden flex items-center justify-between p-4 bg-white/80 dark:bg-neutral-900/80 backdrop-blur-md border-b border-neutral-200 dark:border-neutral-700 sticky top-0 z-40"
72167 initial = { { y : - 100 } }
73168 animate = { { y : 0 } }
@@ -83,18 +178,18 @@ const LeftPanel = ({ clickedCategory, handleInteraction }: LeftPanelProps) => {
83178 < FiMenu size = { 20 } />
84179 < span className = "text-sm font-medium" > Menu</ span >
85180 </ motion . button >
86-
87- < motion . h1
181+
182+ < motion . h1
88183 className = "text-lg font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent"
89184 initial = { { opacity : 0 , x : 20 } }
90185 animate = { { opacity : 1 , x : 0 } }
91186 transition = { { delay : 0.1 } }
92187 >
93- { clicked === LeftPanelOptions . ABOUT ? "Portfolio" : clicked }
188+ { clicked === LeftPanelOptions . ABOUT ? "Portfolio" : clicked }
94189 </ motion . h1 >
95190 </ motion . div >
96191
97- < motion . div
192+ < motion . div
98193 className = "hidden xl:flex flex-col justify-between py-6 xl:max-w-sm xl:py-8 xl:h-full"
99194 initial = { { x : - 50 , opacity : 0 } }
100195 animate = { { x : 0 , opacity : 1 } }
@@ -121,6 +216,13 @@ const LeftPanel = ({ clickedCategory, handleInteraction }: LeftPanelProps) => {
121216 </ motion . div >
122217 ) ) }
123218 </ div >
219+ < motion . div
220+ initial = { { y : 20 , opacity : 0 } }
221+ animate = { { y : 0 , opacity : 1 } }
222+ transition = { { delay : 0.75 } }
223+ >
224+ < GoalCardStack controls swipe = { false } />
225+ </ motion . div >
124226 < motion . div
125227 className = "hidden mt-6 xl:flex cursor-pointer relative group"
126228 onClick = { handleInteraction }
@@ -150,27 +252,27 @@ const LeftPanel = ({ clickedCategory, handleInteraction }: LeftPanelProps) => {
150252 onClick = { closeMenu }
151253 transition = { { duration : 0.2 } }
152254 />
153-
255+
154256 { /* Slide-out Panel */ }
155257 < motion . div
156258 ref = { constraintsRef }
157259 className = "fixed inset-0 z-50 w-screen xl:hidden"
158260 initial = { { x : "-100%" } }
159261 animate = { { x : 0 } }
160262 exit = { { x : "-100%" } }
161- transition = { {
162- type : "spring" ,
163- stiffness : 400 ,
263+ transition = { {
264+ type : "spring" ,
265+ stiffness : 400 ,
164266 damping : 30 ,
165- mass : 0.8
267+ mass : 0.8 ,
166268 } }
167269 drag = "x"
168270 dragConstraints = { { left : - 250 , right : 0 } }
169271 dragElastic = { 0.1 }
170272 onDrag = { handleDrag }
171273 onDragEnd = { handleDragEnd }
172274 style = { {
173- x : dragProgress ? - dragProgress * 250 : 0
275+ x : dragProgress ? - dragProgress * 250 : 0 ,
174276 } }
175277 >
176278 < div className = "h-full bg-white dark:bg-neutral-900 shadow-2xl flex flex-col" >
@@ -199,9 +301,9 @@ const LeftPanel = ({ clickedCategory, handleInteraction }: LeftPanelProps) => {
199301 >
200302 < Bio compact />
201303 </ motion . div >
202-
304+
203305 < div className = "border-b my-6 border-neutral-200 dark:border-neutral-700" />
204-
306+
205307 < div className = "flex flex-col gap-2" >
206308 { menuItems . map ( ( item , index ) => (
207309 < motion . div
@@ -215,13 +317,25 @@ const LeftPanel = ({ clickedCategory, handleInteraction }: LeftPanelProps) => {
215317 category = { item . icon }
216318 href = { `#` }
217319 clickedCategory = { clickedCategory }
218- setClicked = { ( ) => handleMenuClick ( item . text as LeftPanelOptions ) }
320+ setClicked = { ( ) =>
321+ handleMenuClick ( item . text as LeftPanelOptions )
322+ }
219323 clicked = { clicked }
220324 />
221325 </ motion . div >
222326 ) ) }
223327 </ div >
224328
329+ < div className = "flex-1 overflow-y-auto pt-6" >
330+ < motion . div
331+ initial = { { y : 20 , opacity : 0 } }
332+ animate = { { y : 0 , opacity : 1 } }
333+ transition = { { delay : 0.35 } }
334+ >
335+ < GoalCardStack swipeAxis = "y" />
336+ </ motion . div >
337+ </ div >
338+
225339 < motion . div
226340 className = "mt-8 flex cursor-pointer relative group"
227341 onClick = { handleHiddenClick }
@@ -235,26 +349,26 @@ const LeftPanel = ({ clickedCategory, handleInteraction }: LeftPanelProps) => {
235349 </ div >
236350
237351 { /* Drag Hint */ }
238- < motion . div
352+ < motion . div
239353 className = "absolute right-4 top-1/2 transform -translate-y-1/2 opacity-30 pointer-events-none"
240- animate = { {
354+ animate = { {
241355 x : [ 0 , - 10 , 0 ] ,
242- opacity : [ 0.3 , 0.6 , 0.3 ]
356+ opacity : [ 0.3 , 0.6 , 0.3 ] ,
243357 } }
244- transition = { {
245- duration : 2 ,
358+ transition = { {
359+ duration : 2 ,
246360 repeat : Infinity ,
247- ease : "easeInOut"
361+ ease : "easeInOut" ,
248362 } }
249363 >
250364 < FiChevronLeft size = { 24 } />
251365 </ motion . div >
252366
253- < div
367+ < div
254368 className = "absolute right-0 top-0 bottom-0 w-1 bg-gradient-to-b from-blue-500 to-purple-500 rounded-l-full transition-all duration-200"
255- style = { {
369+ style = { {
256370 opacity : dragProgress ,
257- transform : `scaleY(${ 0.3 + dragProgress * 0.7 } )`
371+ transform : `scaleY(${ 0.3 + dragProgress * 0.7 } )` ,
258372 } }
259373 />
260374 </ div >
0 commit comments