Skip to content

Commit 26991d7

Browse files
DangDang
authored andcommitted
2 parents 1cf0142 + a6e87dd commit 26991d7

File tree

85 files changed

+5090
-2176
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

85 files changed

+5090
-2176
lines changed

VehicleShowroom/src/api/ApiClient.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,6 @@ const createResponseInterceptor = (client) =>
9494
// 🌟 Gọi refresh token
9595
const newToken = await AuthService.refreshToken();
9696

97-
// ✅ Lưu token mới lại
98-
AuthService.setAccessToken(newToken);
99-
10097
// ✅ Update default header
10198
ApiClient.defaults.headers.common[
10299
'Authorization'

VehicleShowroom/src/components/categoryMenu/CategoryMenu.js

Lines changed: 220 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { useEffect, useState } from 'react';
22
import {
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';
1219
import { motion, AnimatePresence } from 'framer-motion';
1320
import { useNavigate } from 'react-router-dom';
1421
import { 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';
1625
import { useUser } from 'contexts/UserContext';
1726

1827
const 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

Comments
 (0)