1- import React from 'react' ;
1+ import React , { useEffect , useState } from 'react' ;
22import {
33 Box ,
44 Flex ,
@@ -7,38 +7,225 @@ import {
77 useColorModeValue ,
88 HStack ,
99 Text ,
10+ Grid ,
11+ GridItem ,
12+ Image ,
13+ Heading ,
14+ VStack ,
15+ Tag ,
16+ Spinner ,
1017} from '@chakra-ui/react' ;
11- import { CloseIcon } from '@chakra-ui/icons' ;
18+ import { CloseIcon , ArrowBackIcon } from '@chakra-ui/icons' ;
1219import { motion , AnimatePresence } from 'framer-motion' ;
1320import { useNavigate } from 'react-router-dom' ;
1421import { MdLogin , MdPerson , MdLogout } from 'react-icons/md' ;
15- import IndividualCars from 'components/categoryMenu/components/IndividualCars' ;
22+ import VehicleModelService from 'services/VehicleModelService' ;
23+ import VehiclePhotoService from 'services/VehiclePhotoService' ;
24+ import VehicleSpecService from 'services/VehicleSpecService' ;
1625import { useUser } from 'contexts/UserContext' ;
1726
1827const MotionBox = motion ( Box ) ;
1928
20- function CategoryMenu ( { isVisible, closeHandler } ) {
29+ export default function CategoryMenu ( { isVisible, closeHandler } ) {
2130 const bgColor = useColorModeValue ( 'white' , 'gray.900' ) ;
2231 const textColor = useColorModeValue ( 'gray.700' , 'white' ) ;
2332 const borderColor = useColorModeValue ( 'gray.200' , 'gray.700' ) ;
2433 const navigate = useNavigate ( ) ;
2534 const { isAuthenticated, user, logout } = useUser ( ) ;
2635
27- // 🔹 Handle sign out
36+ const [ loading , setLoading ] = useState ( true ) ;
37+ const [ allModels , setAllModels ] = useState ( [ ] ) ; // cấp 1
38+ const [ displayedModels , setDisplayedModels ] = useState ( [ ] ) ;
39+ const [ parentModel , setParentModel ] = useState ( null ) ;
40+
41+ // 🟢 Fetch cấp 1
42+ useEffect ( ( ) => {
43+ const fetchLevel1Models = async ( ) => {
44+ setLoading ( true ) ;
45+ try {
46+ const res = await VehicleModelService . get ( { pageNumber : 1 , pageSize : 100 } ) ;
47+ const models = res ?. items ?. filter ( ( m ) => m . level === 1 ) || [ ] ;
48+
49+ const enriched = await Promise . all (
50+ models . map ( async ( m ) => {
51+ let photoUrl = m . photo || 'https://placehold.co/600x400?text=No+Image' ;
52+ try {
53+ const photos = await VehiclePhotoService . getByModelNumber ( m . modelNumber ) ;
54+ const displayPhoto =
55+ photos . items ?. find ( ( p ) => p . displayOrder === 0 ) ?. photoUrl ||
56+ photos . items ?. [ 0 ] ?. photoUrl ||
57+ photos . items ?. [ 0 ] ?. url ;
58+ if ( displayPhoto ) photoUrl = displayPhoto ;
59+ } catch { }
60+ return { ...m , photo : photoUrl } ;
61+ } )
62+ ) ;
63+
64+ setAllModels ( enriched ) ;
65+ setDisplayedModels ( enriched ) ;
66+ } catch ( err ) {
67+ console . error ( '❌ Error fetching level 1 models:' , err ) ;
68+ } finally {
69+ setLoading ( false ) ;
70+ }
71+ } ;
72+
73+ fetchLevel1Models ( ) ;
74+ } , [ ] ) ;
75+
76+ // 🟣 Fetch cấp 2 (variants)
77+ const handleOpenLevel2 = async ( parentModelNumber , name ) => {
78+ console . log ( '➡ Fetching level 2 for' , parentModelNumber ) ;
79+ setLoading ( true ) ;
80+ try {
81+ const res = await VehicleModelService . get ( { parentModelNumber } ) ;
82+ const variants = res ?. items || res ; // fallback nếu API trả mảng thẳng
83+
84+ if ( ! variants . length ) {
85+ console . warn ( '⚠️ No variants found for' , parentModelNumber ) ;
86+ }
87+
88+ const enrichedVariants = await Promise . all (
89+ variants . map ( async ( m ) => {
90+ let photoUrl = m . photo || 'https://placehold.co/600x400?text=No+Image' ;
91+ let fuelType = 'N/A' ;
92+
93+ try {
94+ const photos = await VehiclePhotoService . getByModelNumber ( m . modelNumber ) ;
95+ const displayPhoto =
96+ photos . items ?. find ( ( p ) => p . displayOrder === 0 ) ?. photoUrl ||
97+ photos . items ?. [ 0 ] ?. photoUrl ||
98+ photos . items ?. [ 0 ] ?. url ;
99+ if ( displayPhoto ) photoUrl = displayPhoto ;
100+ } catch { }
101+
102+ try {
103+ const specs = await VehicleSpecService . getByModelNumber ( m . modelNumber ) ;
104+ const fuelSpec = specs . items . find ( ( s ) => s . specName === 'Fuel Type' ) ;
105+ if ( fuelSpec ) fuelType = fuelSpec . specValue ;
106+ } catch { }
107+
108+ return { ...m , photo : photoUrl , fuelType } ;
109+ } )
110+ ) ;
111+
112+ setDisplayedModels ( enrichedVariants ) ;
113+ setParentModel ( { modelNumber : parentModelNumber , name } ) ;
114+ } catch ( err ) {
115+ console . error ( '❌ Error fetching submodels:' , err ) ;
116+ } finally {
117+ setLoading ( false ) ;
118+ }
119+ } ;
120+
121+ const handleBack = ( ) => {
122+ setDisplayedModels ( allModels ) ;
123+ setParentModel ( null ) ;
124+ } ;
125+
28126 const handleSignOut = async ( ) => {
29127 await logout ( ) ;
30128 closeHandler ( ) ;
31129 } ;
32130
33- // 🔹 Handle navigation safely
34131 const handleNavigate = ( path ) => {
35132 closeHandler ( ) ;
36133 navigate ( path ) ;
37134 } ;
38135
136+ // 🌀 Render nội dung
137+ const renderContent = ( ) => {
138+ if ( loading )
139+ return (
140+ < Flex justify = "center" align = "center" h = "100%" >
141+ < Spinner size = "lg" />
142+ </ Flex >
143+ ) ;
144+
145+ if ( ! displayedModels . length )
146+ return (
147+ < Text color = "gray.500" fontStyle = "italic" textAlign = "center" py = { 4 } >
148+ No models found
149+ </ Text >
150+ ) ;
151+
152+ return (
153+ < Grid templateColumns = "1fr" gap = { 6 } placeItems = "center" pb = { 8 } >
154+ { displayedModels . map ( ( el ) => (
155+ < GridItem
156+ key = { el . modelNumber }
157+ w = "full"
158+ maxW = "28rem"
159+ borderRadius = "md"
160+ transition = "0.25s ease"
161+ cursor = "pointer"
162+ p = { 4 }
163+ role = "group"
164+ _hover = { { bg : '#eeeff2' } }
165+ >
166+ < VStack align = "start" spacing = { 3 } >
167+ { /* 🔹 Tên model */ }
168+ < Heading
169+ size = "md"
170+ fontWeight = "semibold"
171+ role = "button"
172+ onClick = { ( ) => {
173+ if ( el . level === 1 ) {
174+ handleOpenLevel2 ( el . modelNumber , el . name ) ;
175+ } else if ( el . level === 2 && el . slug ) {
176+ closeHandler ( ) ;
177+ navigate ( `/user/detail/${ el . slug } ` ) ;
178+ }
179+ } }
180+ _hover = { { color : 'brand.500' } }
181+ >
182+ { el . name }
183+ </ Heading >
184+
185+ { /* 🔹 Ảnh */ }
186+ < Box
187+ w = "full"
188+ overflow = "hidden"
189+ borderRadius = "md"
190+ role = "button"
191+ onClick = { ( ) => {
192+ if ( el . level === 1 ) {
193+ handleOpenLevel2 ( el . modelNumber , el . name ) ;
194+ } else if ( el . level === 2 && el . slug ) {
195+ closeHandler ( ) ;
196+ navigate ( `/user/detail/${ el . slug } ` ) ;
197+ }
198+ } }
199+ >
200+ < Image
201+ src = { el . photo || 'https://placehold.co/600x400?text=No+Image' }
202+ alt = { el . name }
203+ w = "full"
204+ h = "auto"
205+ objectFit = "cover"
206+ borderRadius = "md"
207+ transition = "transform 0.3s ease"
208+ _groupHover = { { transform : 'translateX(10px)' } }
209+ />
210+ </ Box >
211+
212+ { /* 🔹 Fuel type */ }
213+ { el . level === 2 && (
214+ < Flex gap = { 2 } wrap = "wrap" >
215+ < Tag bg = "gray.200" color = "black" fontWeight = "medium" >
216+ { el . fuelType || 'N/A' }
217+ </ Tag >
218+ </ Flex >
219+ ) }
220+ </ VStack >
221+ </ GridItem >
222+ ) ) }
223+ </ Grid >
224+ ) ;
225+ } ;
226+
39227 return (
40228 < >
41- { /* 🔹 Drawer animation */ }
42229 < AnimatePresence >
43230 { isVisible && (
44231 < MotionBox
@@ -59,16 +246,27 @@ function CategoryMenu({ isVisible, closeHandler }) {
59246 justifyContent = "space-between"
60247 zIndex = "1500"
61248 >
62- { /* Header */ }
63- < Flex
64- justify = "space-between"
65- align = "center"
66- p = { 6 }
67- borderColor = { borderColor }
68- >
69- < Text fontSize = "2xl" fontWeight = "600" >
70- Models
71- </ Text >
249+ { /* 🔹 Header */ }
250+ < Flex justify = "space-between" align = "center" p = { 6 } borderColor = { borderColor } >
251+ { parentModel ? (
252+ < Flex align = "center" gap = { 3 } >
253+ < IconButton
254+ icon = { < ArrowBackIcon /> }
255+ aria-label = "Back"
256+ onClick = { handleBack }
257+ variant = "ghost"
258+ size = "sm"
259+ />
260+ < Text fontSize = "xl" fontWeight = "600" >
261+ { parentModel . name }
262+ </ Text >
263+ </ Flex >
264+ ) : (
265+ < Text fontSize = "2xl" fontWeight = "600" >
266+ Models
267+ </ Text >
268+ ) }
269+
72270 < IconButton
73271 icon = { < CloseIcon /> }
74272 aria-label = "Close menu"
@@ -78,12 +276,12 @@ function CategoryMenu({ isVisible, closeHandler }) {
78276 />
79277 </ Flex >
80278
81- { /* Content: Danh sách xe */ }
82- < Box flex = "1" overflowY = "auto" px = "1rem" py = "1rem" >
83- < IndividualCars />
279+ { /* 🔹 Content */ }
280+ < Box flex = "1" overflowY = "auto" >
281+ { renderContent ( ) }
84282 </ Box >
85283
86- { /* Footer */ }
284+ { /* 🔹 Footer */ }
87285 < Flex
88286 px = "1rem"
89287 py = "1rem"
@@ -105,15 +303,12 @@ function CategoryMenu({ isVisible, closeHandler }) {
105303 ) : (
106304 < HStack w = "full" justify = "space-between" >
107305 < Flex gap = { 3 } align = "center" >
108- { /* 👤 Profile Button */ }
109306 < Button
110307 leftIcon = { < MdPerson size = { 20 } /> }
111308 onClick = { ( ) => handleNavigate ( '/user/profile' ) }
112309 >
113310 Profile
114311 </ Button >
115-
116- { /* 🛠️ Admin Button (only for admins) */ }
117312 { user ?. role === 'Admin' && (
118313 < Button
119314 leftIcon = { < MdPerson size = { 20 } /> }
@@ -138,7 +333,7 @@ function CategoryMenu({ isVisible, closeHandler }) {
138333 ) }
139334 </ AnimatePresence >
140335
141- { /* 🔹 Overlay */ }
336+ { /* Overlay */ }
142337 < AnimatePresence >
143338 { isVisible && (
144339 < MotionBox
@@ -160,5 +355,3 @@ function CategoryMenu({ isVisible, closeHandler }) {
160355 </ >
161356 ) ;
162357}
163-
164- export default CategoryMenu ;
0 commit comments