1
+ /* eslint-disable linebreak-style */
1
2
import React , { useState , useEffect } from 'react' ;
2
3
import { useRouter } from 'next/router' ;
4
+ import {
5
+ Collapsible ,
6
+ CollapsibleContent ,
7
+ CollapsibleTrigger ,
8
+ } from '@/components/ui/collapsible' ;
9
+ import { cn } from '@/lib/utils' ;
3
10
4
11
interface AccordionItem {
5
12
question : string ;
@@ -12,84 +19,122 @@ interface AccordionProps {
12
19
}
13
20
14
21
const Accordion : React . FC < AccordionProps > = ( { items } ) => {
15
- const [ activeIndex , setActiveIndex ] = useState < number | null > ( null ) ;
22
+ const [ openItems , setOpenItems ] = useState < Set < number > > ( new Set ( ) ) ;
16
23
const router = useRouter ( ) ;
17
24
18
- const handleToggle = ( index : number ) => {
19
- setActiveIndex ( ( prevIndex ) => ( prevIndex === index ? null : index ) ) ;
25
+ const handleToggle = ( id : number ) => {
26
+ setOpenItems ( ( prev ) => {
27
+ const newSet = new Set ( prev ) ;
28
+ if ( newSet . has ( id ) ) {
29
+ newSet . delete ( id ) ;
30
+ } else {
31
+ newSet . add ( id ) ;
32
+ }
33
+ return newSet ;
34
+ } ) ;
20
35
} ;
21
36
22
37
useEffect ( ( ) => {
23
38
const hash = router . asPath . split ( '#' ) [ 1 ] ;
24
39
if ( hash ) {
25
40
const id = parseInt ( hash , 10 ) ;
26
- const index = items . findIndex ( ( item ) => item . id === id ) ;
27
- if ( index !== - 1 ) {
28
- setActiveIndex ( index ) ;
29
-
30
- setTimeout ( ( ) => {
31
- const element = document . getElementById ( hash ) ;
32
- if ( element ) {
33
- const navbarHeight = 150 ;
34
- const offset = element . offsetTop - navbarHeight ;
35
- window . scrollTo ( { top : offset , behavior : 'smooth' } ) ;
36
- }
37
- } , 0 ) ;
41
+ const item = items . find ( ( item ) => item . id === id ) ;
42
+ if ( item ) {
43
+ setOpenItems ( new Set ( [ id ] ) ) ;
38
44
}
39
45
}
40
46
} , [ items , router . asPath ] ) ;
41
47
42
- const handleLinkClick = ( id : number ) => {
43
- const index = items . findIndex ( ( item ) => item . id === id ) ;
44
- setActiveIndex ( index ) ;
45
-
46
- const newUrl = `#${ id } ` ;
47
- router . push ( newUrl , undefined , { shallow : true } ) ;
48
- } ;
49
-
50
48
return (
51
- < div >
52
- { items . map ( ( item , index ) => (
53
- < div
49
+ < div className = 'w-full space-y-1' >
50
+ { items . map ( ( item ) => (
51
+ < Collapsible
54
52
key = { item . id }
55
- className = { `overflow-hidden transition-max-height border-t-2 ${
56
- activeIndex === index ? 'max-h-96' : 'max-h-20'
57
- } ${ index === items . length - 1 ? 'border-b-2' : '' } ` }
53
+ open = { openItems . has ( item . id ) }
54
+ onOpenChange = { ( ) => handleToggle ( item . id ) }
55
+ className = 'w-full'
58
56
data-test = { `accordion-item-${ item . id } ` }
59
57
>
60
- < div className = 'flex justify-between p-4 pl-2 cursor-pointer' >
61
- < div className = 'text-[20px]' >
62
- < a
63
- href = { `#${ item . id } ` }
64
- onClick = { ( e ) => {
65
- e . preventDefault ( ) ;
66
- handleLinkClick ( item . id ) ;
67
- } }
68
- data-test = { `accordion-question-${ item . id } ` }
69
- >
70
- { item . question }
71
- </ a >
72
- </ div >
73
- < div
74
- className = { `transform transition-transform duration-200 max-h-7 text-[20px] ${
75
- activeIndex === index ? 'rotate-45' : ''
76
- } `}
77
- onClick = { ( ) => handleToggle ( index ) }
58
+ < div
59
+ className = { cn (
60
+ 'border border-border dark:border-[#bfdbfe] rounded-lg transition-colors' ,
61
+ openItems . has ( item . id ) &&
62
+ 'border-primary/50 bg-[#e2e8f0] dark:bg-[#0f172a]' ,
63
+ ) }
64
+ >
65
+ < CollapsibleTrigger
66
+ asChild
67
+ className = 'w-full'
78
68
data-test = { `accordion-toggle-${ item . id } ` }
79
69
>
80
- +
81
- </ div >
82
- </ div >
83
- { activeIndex === index && (
84
- < div
85
- id = { `${ item . id } ` }
86
- className = 'p-2 text-gray-500 dark:text-slate-200 pb-4'
70
+ < div className = 'flex items-center justify-between w-full p-4 text-left hover:bg-muted/50 transition-colors rounded-lg' >
71
+ < div className = 'flex-1' >
72
+ < a
73
+ href = { `#${ item . id } ` }
74
+ onClick = { ( e ) => {
75
+ const isCurrentlyOpen = openItems . has ( item . id ) ;
76
+ if ( isCurrentlyOpen ) {
77
+ // If open, just close it without navigation
78
+ e . preventDefault ( ) ;
79
+ handleToggle ( item . id ) ;
80
+ } else {
81
+ // If closed, open it and scroll to a position a few pixels higher
82
+ e . preventDefault ( ) ;
83
+ handleToggle ( item . id ) ;
84
+ setTimeout ( ( ) => {
85
+ const element = document . getElementById ( `${ item . id } ` ) ;
86
+ if ( element ) {
87
+ const navbarHeight = 150 ;
88
+ const offset =
89
+ element . offsetTop - navbarHeight - 20 ; // 20px higher
90
+ window . scrollTo ( {
91
+ top : offset ,
92
+ behavior : 'smooth' ,
93
+ } ) ;
94
+ }
95
+ } , 100 ) ;
96
+ }
97
+ } }
98
+ className = { cn (
99
+ 'text-lg font-medium text-foreground transition-all duration-200 dark:hover:text-[#bfdbfe] hover:text-lg hover:text-blue-600' ,
100
+ openItems . has ( item . id ) &&
101
+ 'text-primary dark:text-[#bfdbfe]' ,
102
+ ) }
103
+ data-test = { `accordion-question-${ item . id } ` }
104
+ >
105
+ { item . question }
106
+ </ a >
107
+ </ div >
108
+ < div className = 'ml-4 flex-shrink-0' >
109
+ < div
110
+ className = { cn (
111
+ 'w-6 h-6 rounded-full border-2 border-border flex items-center justify-center transition-all duration-200 cursor-pointer' ,
112
+ openItems . has ( item . id )
113
+ ? 'border-primary bg-primary text-white rotate-45 dark:bg-[#bfdbfe] dark:text-black dark:border-[#bfdbfe]'
114
+ : 'hover:border-primary/50' ,
115
+ ) }
116
+ >
117
+ < span className = 'text-sm font-bold leading-none' >
118
+ { openItems . has ( item . id ) ? '×' : '+' }
119
+ </ span >
120
+ </ div >
121
+ </ div >
122
+ </ div >
123
+ </ CollapsibleTrigger >
124
+
125
+ < CollapsibleContent
126
+ className = 'overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down'
87
127
data-test = { `accordion-answer-${ item . id } ` }
88
128
>
89
- { item . answer }
90
- </ div >
91
- ) }
92
- </ div >
129
+ < div
130
+ id = { `${ item . id } ` }
131
+ className = 'px-4 pb-4 text-muted-foreground leading-relaxed'
132
+ >
133
+ { item . answer }
134
+ </ div >
135
+ </ CollapsibleContent >
136
+ </ div >
137
+ </ Collapsible >
93
138
) ) }
94
139
</ div >
95
140
) ;
0 commit comments