tosanoob commited on
Commit
fdbdf19
1 Parent(s): a6d8fe5

Update: add api menu&feed

Browse files
frontend/public/desserts.jpg ADDED
frontend/public/dishes.jpg ADDED
frontend/public/drinks.jpg ADDED
frontend/src/index.js CHANGED
@@ -20,6 +20,8 @@ import AdminMenuPage from './pages/AdminMenuPage';
20
  import AdminStaffPage from './pages/AdminStaffPage';
21
  import AdminOrderPage from './pages/AdminOrderPage';
22
  import AdminSchedulePage from './pages/AdminSchedulePage';
 
 
23
 
24
  const router = createBrowserRouter([
25
  {
@@ -56,7 +58,7 @@ const router = createBrowserRouter([
56
  element: <UserInfoPage />,
57
  errorElement: <ErrorPage />
58
  },
59
-
60
  {
61
  path: "/admin",
62
  element: <AdminSummaryPage />,
@@ -91,6 +93,16 @@ const router = createBrowserRouter([
91
  path: "/admin-orders",
92
  element: <AdminOrderPage />,
93
  errorElement: <ErrorPage />
 
 
 
 
 
 
 
 
 
 
94
  }
95
  ]);
96
 
 
20
  import AdminStaffPage from './pages/AdminStaffPage';
21
  import AdminOrderPage from './pages/AdminOrderPage';
22
  import AdminSchedulePage from './pages/AdminSchedulePage';
23
+ import AdminLoginPage from './pages/AdminLoginPage';
24
+ import AdminUserInfoPage from './pages/AdminUserInfoPage';
25
 
26
  const router = createBrowserRouter([
27
  {
 
58
  element: <UserInfoPage />,
59
  errorElement: <ErrorPage />
60
  },
61
+ // admin section
62
  {
63
  path: "/admin",
64
  element: <AdminSummaryPage />,
 
93
  path: "/admin-orders",
94
  element: <AdminOrderPage />,
95
  errorElement: <ErrorPage />
96
+ },
97
+ {
98
+ path: "/admin-login",
99
+ element: <AdminLoginPage />,
100
+ errorElement: <ErrorPage />
101
+ },
102
+ {
103
+ path:"/admin-info",
104
+ element: <AdminUserInfoPage/>,
105
+ errorElement: <ErrorPage/>
106
  }
107
  ]);
108
 
frontend/src/molecules/AdminNavBar.js CHANGED
@@ -22,7 +22,7 @@ export default function AdminNavbar() {
22
  if (isLoggedIn === 'true') {
23
  userContent = <>
24
  <Stack direction='horizontal' gap={2}>
25
- <Button href="/userinfo" variant='primary'>
26
  Xin chào, ADMIN {username}
27
  </Button>
28
  <Button onClick={handleLogout} variant='outline-primary'>
@@ -31,7 +31,13 @@ export default function AdminNavbar() {
31
  </Stack>
32
  </>
33
  } else {
34
- userContent = <></>
 
 
 
 
 
 
35
  }
36
  return (
37
  <Navbar id="home" expand="lg" className="bg-body-tertiary" sticky='top' >
@@ -50,12 +56,12 @@ export default function AdminNavbar() {
50
  <Navbar.Collapse id="basic-navbar-nav">
51
  <Nav className="me-auto text-center">
52
  {/* These are the navigators */}
53
- <Nav.Link href="/admin-summary">Dashboard</Nav.Link>
54
- <Nav.Link href="/admin-feed">Quản lý bài đăng</Nav.Link>
55
- <Nav.Link href="/admin-menu">Quản lý thực đơn</Nav.Link>
56
- <Nav.Link href="/admin-staff">Quản lý nhân viên</Nav.Link>
57
- <Nav.Link href="/admin-schedule">Lịch làm việc</Nav.Link>
58
- <Nav.Link href="/admin-orders">Danh sách đơn hàng</Nav.Link>
59
  </Nav>
60
  {userContent}
61
  </Navbar.Collapse>
 
22
  if (isLoggedIn === 'true') {
23
  userContent = <>
24
  <Stack direction='horizontal' gap={2}>
25
+ <Button href="/admin-info" variant='primary'>
26
  Xin chào, ADMIN {username}
27
  </Button>
28
  <Button onClick={handleLogout} variant='outline-primary'>
 
31
  </Stack>
32
  </>
33
  } else {
34
+ userContent = <>
35
+ <Stack direction='horizontal' gap={2}>
36
+ <Button href="/admin-login" variant='primary'>
37
+ Đăng nhập
38
+ </Button>
39
+ </Stack>
40
+ </>
41
  }
42
  return (
43
  <Navbar id="home" expand="lg" className="bg-body-tertiary" sticky='top' >
 
56
  <Navbar.Collapse id="basic-navbar-nav">
57
  <Nav className="me-auto text-center">
58
  {/* These are the navigators */}
59
+ <Nav.Link disabled={!isLoggedIn} href="/admin-summary">Dashboard</Nav.Link>
60
+ <Nav.Link disabled={!isLoggedIn} href="/admin-feed">Bài đăng</Nav.Link>
61
+ <Nav.Link disabled={!isLoggedIn} href="/admin-menu">Thực đơn</Nav.Link>
62
+ <Nav.Link disabled={!isLoggedIn} href="/admin-staff">Nhân viên</Nav.Link>
63
+ <Nav.Link disabled={!isLoggedIn} href="/admin-schedule">Lịch làm việc</Nav.Link>
64
+ <Nav.Link disabled={!isLoggedIn} href="/admin-orders">Đơn hàng</Nav.Link>
65
  </Nav>
66
  {userContent}
67
  </Navbar.Collapse>
frontend/src/molecules/Navbar.js CHANGED
@@ -1,4 +1,4 @@
1
- import {Container, Nav, Navbar, Button, Stack} from 'react-bootstrap'
2
  import { useNavigate } from 'react-router-dom';
3
  import DataStorage from '../organisms/DataStorage';
4
 
@@ -7,7 +7,7 @@ export default function ANavbar() {
7
  const navigate = useNavigate();
8
 
9
  function handleLogout() {
10
- DataStorage.set('isLoggedIn','false');
11
  DataStorage.remove('accessToken');
12
  DataStorage.remove('role');
13
  DataStorage.remove('username');
@@ -49,17 +49,21 @@ export default function ANavbar() {
49
  }
50
 
51
  return (
52
- <Navbar id="home" expand="lg" className="bg-body-tertiary" sticky='top' style={{'min-height':'6vh'}}>
53
- <Container fluid style={{maxWidth:"90%"}}>
54
  <Navbar.Brand href="/">
55
- <img
56
- alt=""
57
- src="/cats-logo.png"
58
- width="30"
59
- height="30"
60
- className="d-inline-block align-top"
61
- />{' '}
62
- CATS-Shop
 
 
 
 
63
  </Navbar.Brand>
64
  <Navbar.Toggle aria-controls="basic-navbar-nav" />
65
  <Navbar.Collapse id="basic-navbar-nav">
 
1
+ import { Container, Nav, Navbar, Button, Stack } from 'react-bootstrap'
2
  import { useNavigate } from 'react-router-dom';
3
  import DataStorage from '../organisms/DataStorage';
4
 
 
7
  const navigate = useNavigate();
8
 
9
  function handleLogout() {
10
+ DataStorage.set('isLoggedIn', 'false');
11
  DataStorage.remove('accessToken');
12
  DataStorage.remove('role');
13
  DataStorage.remove('username');
 
49
  }
50
 
51
  return (
52
+ <Navbar id="home" expand="lg" className="bg-body-tertiary" sticky='top' style={{ minHeight: '6vh' }}>
53
+ <Container fluid style={{ maxWidth: "90%" }}>
54
  <Navbar.Brand href="/">
55
+ <div style={{ fontWeight: 'bold' }}>
56
+ <span className='mr-1'>
57
+ <img
58
+ alt=""
59
+ src="/cats-logo.png"
60
+ width="30"
61
+ height="30"
62
+ className="d-inline-block align-top"
63
+ />
64
+ </span>
65
+ {" "}
66
+ CATS - Shop</div>
67
  </Navbar.Brand>
68
  <Navbar.Toggle aria-controls="basic-navbar-nav" />
69
  <Navbar.Collapse id="basic-navbar-nav">
frontend/src/organisms/CacheStorage.js ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Cookies from 'js-cookie';
2
+
3
+ export default class CacheStorage {
4
+ static storageMethod = process.env.REACT_APP_CACHE_METHOD;
5
+
6
+ // Get data
7
+ static get(key) {
8
+ if (this.storageMethod === 'session') {
9
+ return sessionStorage.getItem(key);
10
+ } else if (this.storageMethod === 'cookie') {
11
+ return Cookies.get(key);
12
+ }
13
+ return null;
14
+ }
15
+
16
+ // Set data
17
+ static set(key, value, param = { expiryDate: null }) {
18
+ if (this.storageMethod === 'session') {
19
+ sessionStorage.setItem(key, value);
20
+ } else if (this.storageMethod === 'cookie') {
21
+ if (param.expiryDate) {
22
+ const expiryDate = new Date(param.expiryDate * 1000);
23
+ Cookies.set(key, value, { expires: expiryDate })
24
+ } else if (Cookies.get('expiryDate')) {
25
+ const expiryDate = new Date(Cookies.get('expiryDate') * 1000);
26
+ Cookies.set(key, value, { expires: expiryDate });
27
+ } else Cookies.set(key, value, { expires: 7 })// Expires in 7 by default
28
+ }
29
+ }
30
+
31
+ // Remove data
32
+ static remove(key) {
33
+ if (this.storageMethod === 'session') {
34
+ sessionStorage.removeItem(key);
35
+ } else if (this.storageMethod === 'cookie') {
36
+ Cookies.remove(key);
37
+ }
38
+ }
39
+
40
+ // Get all data
41
+ static getAll() {
42
+ const data = {};
43
+ if (this.storageMethod === 'session') {
44
+ for (let i = 0; i < sessionStorage.length; i++) {
45
+ const key = sessionStorage.key(i);
46
+ data[key] = sessionStorage.getItem(key);
47
+ }
48
+ } else if (this.storageMethod === 'cookie') {
49
+ const cookies = Cookies.get();
50
+ Object.keys(cookies).forEach(key => {
51
+ data[key] = cookies[key];
52
+ });
53
+ }
54
+ return data;
55
+ }
56
+
57
+ // Clear all data
58
+ static clearAll() {
59
+ if (this.storageMethod === 'session') {
60
+ sessionStorage.clear();
61
+ } else if (this.storageMethod === 'cookie') {
62
+ const cookies = Cookies.get();
63
+ Object.keys(cookies).forEach(key => {
64
+ Cookies.remove(key);
65
+ });
66
+ }
67
+ }
68
+
69
+ // Check if a key exists
70
+ static hasKey(key) {
71
+ if (this.storageMethod === 'session') {
72
+ return sessionStorage.getItem(key) !== null;
73
+ } else if (this.storageMethod === 'cookie') {
74
+ return Cookies.get(key) !== undefined;
75
+ }
76
+ return false;
77
+ }
78
+
79
+ // Get all keys
80
+ static getKeys() {
81
+ const keys = [];
82
+ if (this.storageMethod === 'session') {
83
+ for (let i = 0; i < sessionStorage.length; i++) {
84
+ keys.push(sessionStorage.key(i));
85
+ }
86
+ } else if (this.storageMethod === 'cookie') {
87
+ keys.push(...Object.keys(Cookies.get()));
88
+ }
89
+ return keys;
90
+ }
91
+ }
frontend/src/organisms/MenuSection.js CHANGED
@@ -1,58 +1,151 @@
1
- import { useState } from 'react';
2
  import { Container, Carousel, Row, Col, Button } from 'react-bootstrap';
3
  import MenuItem from '../molecules/MenuItem';
 
 
 
 
 
 
 
 
4
 
5
  function MenuSection() {
6
- const [index, setIndex] = useState(0);
7
-
8
- const handleSelect = (selectedIndex) => {
9
- setIndex(selectedIndex);
10
- };
11
-
12
- const menuItems = [
13
- { name: 'Món 1', description: 'Mô tả món 1', imageSrc: '/placeholder3.jpg' },
14
- { name: 'Món 2', description: 'Mô tả món 2', imageSrc: '/placeholder3.jpg' },
15
- { name: 'Món 3', description: 'Mô tả món 3', imageSrc: '/placeholder3.jpg' }
16
- ];
17
-
18
- const countCarouselSlides = 4;
19
-
20
- return (
21
- <Container fluid id="menu" className="text-center justify-content-center align-items-center my-5" style={{maxWidth:"90%"}}>
22
- <h1 className='mb-5'>Menu</h1>
23
- <Carousel activeIndex={index} onSelect={handleSelect} data-bs-theme="dark">
24
- {Array.from({ length: countCarouselSlides }).map((_, slideIndex) => (
25
- <Carousel.Item key={slideIndex}>
26
- <div className="d-flex justify-content-center align-items-center" style={{ width: '100%' }}>
27
- <img
28
- className="img-fluid"
29
- src="/placeholder4.jpg"
30
- alt="a placeholder"
31
- style={{ maxHeight: '100%', maxWidth: '100%', objectFit: 'contain' }}
32
- />
33
- </div>
34
-
35
- <Carousel.Caption>
36
- <div className='my-5'>
37
- <Row md={3} className="g-4">
38
- {menuItems.map((item, idx) => (
39
- <Col key={idx}>
40
- <MenuItem
41
- dishName={item.name}
42
- description={item.description}
43
- imageSrc={item.imageSrc}
44
- />
45
- </Col>
46
- ))}
47
- </Row>
48
- <Button as='a' href='/menu' className='mt-5'> Xem thêm các món </Button>
49
- </div>
50
- </Carousel.Caption>
51
- </Carousel.Item>
52
- ))}
53
- </Carousel>
54
- </Container>
55
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  }
57
 
58
  export default MenuSection;
 
1
+ import { useState, useEffect } from 'react';
2
  import { Container, Carousel, Row, Col, Button } from 'react-bootstrap';
3
  import MenuItem from '../molecules/MenuItem';
4
+ import CacheStorage from './CacheStorage';
5
+ import axios from 'axios';
6
+
7
+ const carouselImages = [
8
+ { itemType: 1, imageUrl: '/dishes.jpg' },
9
+ { itemType: 2, imageUrl: '/drinks.jpg' },
10
+ { itemType: 3, imageUrl: '/desserts.jpg' },
11
+ ];
12
 
13
  function MenuSection() {
14
+
15
+ const [carouselMaxHeight, setHeight] = useState('700px');
16
+ const [index, setIndex] = useState(0);
17
+ const [menuItems, setMenuItems] = useState({}); // Đối tượng lưu danh sách món theo từng itemType
18
+ const [loading, setLoading] = useState(true);
19
+
20
+ const handleSelect = (selectedIndex) => {
21
+ setIndex(selectedIndex);
22
+ };
23
+
24
+ // const menuItems = [
25
+ // { name: 'Món 1', description: 'Mô tả món 1', imageSrc: '/placeholder3.jpg' },
26
+ // { name: 'Món 2', description: 'Mô tả món 2', imageSrc: '/placeholder3.jpg' },
27
+ // { name: 'Món 3', description: 'Mô tả món 3', imageSrc: '/placeholder3.jpg' }
28
+ // ];
29
+
30
+
31
+ const categoryMapper = {
32
+ 1: 'Món chính',
33
+ 2: 'Đồ uống',
34
+ 3: 'Tráng miệng',
35
+ };
36
+
37
+ useEffect(() => {
38
+ const fetchMenuItems = async () => {
39
+ try {
40
+ const menuItemsByType = {};
41
+
42
+ for (const carousel of carouselImages) {
43
+ // Gọi API để lấy món theo itemType
44
+ const response = await axios.get(process.env.REACT_APP_API_URL + `/menu-items?limit=3&filter.item_type=${carousel.itemType}`);
45
+ // Lưu danh sách món theo itemType
46
+ menuItemsByType[carousel.itemType] = response.data.data;
47
+ }
48
+ console.log(menuItemsByType);
49
+ setMenuItems(menuItemsByType);
50
+ setLoading(false);
51
+ CacheStorage.set('homeMenuItems',JSON.stringify(menuItemsByType));
52
+ console.log('fetched from API');
53
+ } catch (error) {
54
+ console.error('Error fetching menu items:', error);
55
+ setLoading(false);
56
+ }
57
+ };
58
+ if (CacheStorage.get('homeMenuItems')) {
59
+ setMenuItems(JSON.parse(CacheStorage.get('homeMenuItems')));
60
+ setLoading(false);
61
+ console.log('fetched from cache');
62
+ } else {
63
+ fetchMenuItems();
64
+ }
65
+ }, []);
66
+
67
+ useEffect(() => {
68
+ const handleResize = () => {
69
+ if (window.innerWidth < 992) { // md
70
+ setHeight('1500px'); // Không giới hạn chiều cao
71
+ } else {
72
+ setHeight('700px'); // Giới hạn chiều cao cho md trở lên
73
+ }
74
+ };
75
+
76
+ // Đăng ký event resize
77
+ window.addEventListener('resize', handleResize);
78
+
79
+ // Gọi lần đầu để xác định chiều cao ban đầu
80
+ handleResize();
81
+
82
+ // Cleanup
83
+ return () => {
84
+ window.removeEventListener('resize', handleResize);
85
+ };
86
+ }, []);
87
+
88
+
89
+ let menuContent;
90
+
91
+ if (loading) {
92
+ menuContent = (<p>Đang tải dữ liệu</p>);
93
+ } else {
94
+ menuContent = (<Carousel activeIndex={index} onSelect={handleSelect} data-bs-theme="dark">
95
+ {carouselImages.map((carousel, index) => (
96
+ <Carousel.Item key={index}>
97
+ <div className="d-flex justify-content-center align-items-center" style={{ width: '100%' }}>
98
+ <img
99
+ className="img-fluid"
100
+ src={carousel.imageUrl}
101
+ alt={`Carousel ${carousel.itemType}`}
102
+ style={{
103
+ height: carouselMaxHeight,
104
+ width: '100vw',
105
+ objectPosition: 'center',
106
+ objectFit: 'cover',
107
+ }}
108
+ />
109
+ <div
110
+ style={{
111
+ position: 'absolute',
112
+ top: 0,
113
+ left: 0,
114
+ width: '100%',
115
+ height: '100%',
116
+ backgroundColor: 'rgba(0, 0, 0, 0.5)', // Màu tối với độ trong suốt
117
+ }}
118
+ ></div>
119
+ </div>
120
+
121
+ <Carousel.Caption>
122
+ <div className='my-5'>
123
+ <h1 style={{ color: "var(--text-color)" }} className='mb-5'>{categoryMapper[carousel.itemType]}</h1>
124
+ <Row className="g-4">
125
+ {Array.from(menuItems[carousel.itemType]).map((item, idx) => (
126
+ <Col sm={12} lg={4} key={idx}>
127
+ <MenuItem
128
+ dishName={item.item_name}
129
+ description={item.description}
130
+ imageSrc={item.image_url}
131
+ />
132
+ </Col>
133
+ ))}
134
+ </Row>
135
+ <Button as='a' href='/menu' className='mt-5'> Xem thêm các món </Button>
136
+ </div>
137
+ </Carousel.Caption>
138
+ </Carousel.Item>
139
+ ))}
140
+ </Carousel>);
141
+ }
142
+
143
+ return (
144
+ <Container id="menu" className="text-center justify-content-center align-items-center my-5" style={{ maxWidth: "100%" }}>
145
+ <h1 className='mb-5'>Menu</h1>
146
+ {menuContent}
147
+ </Container>
148
+ );
149
  }
150
 
151
  export default MenuSection;
frontend/src/organisms/NewsSection.js CHANGED
@@ -1,30 +1,62 @@
1
  import { Container, Col, Row } from 'react-bootstrap';
2
  import NewsItem from '../molecules/NewsItem';
 
 
 
3
 
4
  function NewsSection() {
5
 
6
- const newsFeeds = [
7
- { title: 'Feed 1', text: 'This is the first feed', imageSrc: '/placeholder1.jpg', feedHref: '' },
8
- { title: 'Feed 2', text: 'This is the second feed', imageSrc: '/placeholder1.jpg', feedHref: '' },
9
- { title: 'Feed 3', text: 'This is the third feed', imageSrc: '/placeholder1.jpg', feedHref: ''}
10
- ];
11
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  return (
13
  <Container fluid id="news" className="text-center justify-content-center align-items-center my-5" style={{maxWidth:"90%"}}>
14
  <h1 className='mb-5'>Tin tức</h1>
15
- <Row xs={1} md={2} xl={3} className="g-4">
16
- {Array.from(newsFeeds).map((feed, idx) => (
17
- <Col key={idx}>
18
- <NewsItem title={feed.title}
19
- text={feed.text}
20
- imageSrc={feed.imageSrc}
21
- // feedHref={feed.feedHref}
22
- feedHref="/news" //demo
23
- >
24
- </NewsItem>
25
- </Col>
26
- ))}
27
- </Row>
28
  </Container>
29
  );
30
  }
 
1
  import { Container, Col, Row } from 'react-bootstrap';
2
  import NewsItem from '../molecules/NewsItem';
3
+ import CacheStorage from './CacheStorage';
4
+ import axios from 'axios';
5
+ import { useState, useEffect } from 'react';
6
 
7
  function NewsSection() {
8
 
9
+ const [feeds, setFeeds] = useState([]); // Lưu danh sách bài đăng
10
+ const [loading, setLoading] = useState(true); // Trạng thái tải dữ liệu
11
+
12
+ useEffect(() => {
13
+ const fetchFeeds = async () => {
14
+ try {
15
+ const response = await axios.get(process.env.REACT_APP_API_URL + '/feeds?limit=3');
16
+ setFeeds(response.data.data);
17
+ setLoading(false);
18
+ CacheStorage.set('feeds',JSON.stringify(response.data.data));
19
+ console.log(response.data);
20
+ } catch (error) {
21
+ console.error('Error fetching branches:', error);
22
+ setLoading(false);
23
+ }
24
+ }
25
+
26
+ if (CacheStorage.get('feeds')) {
27
+ setFeeds(JSON.parse(CacheStorage.get('feeds')));
28
+ setLoading(false);
29
+ console.log(JSON.parse(CacheStorage.get('feeds')));
30
+ console.log('fetched from cache');
31
+ } else {
32
+ fetchFeeds();
33
+ }
34
+ }, []);
35
+
36
+ let feedContent;
37
+
38
+ if (loading) {
39
+ feedContent = (<p>Đang tải các bài đăng...</p>);
40
+ } else {
41
+ feedContent = (<Row xs={1} md={2} xl={3} className="g-4">
42
+ {Array.from(feeds).map((feed, idx) => (
43
+ <Col key={idx}>
44
+ <NewsItem title={feed.title}
45
+ text={feed.description}
46
+ imageSrc={feed.image_url}
47
+ // feedHref={feed.feedHref}
48
+ feedHref={'/news?id='+feed.id} //demo
49
+ >
50
+ </NewsItem>
51
+ </Col>
52
+ ))}
53
+ </Row>);
54
+ }
55
+
56
  return (
57
  <Container fluid id="news" className="text-center justify-content-center align-items-center my-5" style={{maxWidth:"90%"}}>
58
  <h1 className='mb-5'>Tin tức</h1>
59
+ {feedContent}
 
 
 
 
 
 
 
 
 
 
 
 
60
  </Container>
61
  );
62
  }
frontend/src/organisms/StoreSection.js CHANGED
@@ -1,36 +1,71 @@
1
  import { Container, Col, Row } from 'react-bootstrap';
2
  import StoreItem from '../molecules/StoreItem';
 
 
 
3
 
4
  function StoreSection() {
5
-
6
- const stores = [
7
- { storeName: 'Store 1', address: 'Address 1', imageSrc: '/placeholder2.jpg' },
8
- { storeName: 'Store 2', address: 'Address 2', imageSrc: '/placeholder2.jpg' },
9
- { storeName: 'Store 3', address: 'Address 3', imageSrc: '/placeholder2.jpg' },
10
- ];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  return (
13
- <Container fluid id="store" className="text-center justify-content-center align-items-center my-5" style={{maxWidth:"90%"}}>
14
  <h1 className='mb-5'>Các chi nhánh</h1>
15
  <Row className="align-items-center">
16
- <Col xs={12} md={4} className="d-flex justify-content-center align-items-center">
17
- Đây các chi nhánh đang mở, chưa mở, sắp mở thể sẽ không mở
18
  </Col>
19
 
20
- <Col xs={12} md={8}>
21
- <Container fluid>
22
- <Row xs={1} md={2} xl={3} className="g-4">
23
- {Array.from(stores).map((store, idx) => (
24
- <Col key={idx}>
25
- <StoreItem storeName={store.storeName}
26
- address={store.address}
27
- imageSrc={store.imageSrc}>
28
- </StoreItem>
29
- </Col>
30
- ))}
31
- </Row>
32
- </Container>
33
- </Col>
34
  </Row>
35
  </Container>
36
  );
 
1
  import { Container, Col, Row } from 'react-bootstrap';
2
  import StoreItem from '../molecules/StoreItem';
3
+ import CacheStorage from './CacheStorage';
4
+ import axios from 'axios';
5
+ import { useState, useEffect } from 'react';
6
 
7
  function StoreSection() {
8
+
9
+ // const stores = [
10
+ // { storeName: 'Store 1', address: 'Address 1', imageSrc: '/placeholder2.jpg' },
11
+ // { storeName: 'Store 2', address: 'Address 2', imageSrc: '/placeholder2.jpg' },
12
+ // { storeName: 'Store 3', address: 'Address 3', imageSrc: '/placeholder2.jpg' },
13
+ // ];
14
+
15
+ const [stores, setStores] = useState([]); // Lưu danh sách chi nhánh
16
+ const [loading, setLoading] = useState(true); // Trạng thái tải dữ liệu
17
+
18
+ useEffect(() => {
19
+ // Gọi API lấy danh sách chi nhánh
20
+ const fetchBranches = async () => {
21
+ try {
22
+ const response = await axios.get(process.env.REACT_APP_API_URL + '/branchs'); // Thay 'API_ENDPOINT' bằng URL của API
23
+ setStores(response.data); // Lưu dữ liệu vào state
24
+ setLoading(false); // Đặt loading thành false khi hoàn tất
25
+ CacheStorage.set('stores',JSON.stringify(Object(response.data)));
26
+ } catch (error) {
27
+ console.error('Error fetching branches:', error);
28
+ setLoading(false); // Đặt loading thành false nếu lỗi
29
+ }
30
+ };
31
+ if (CacheStorage.get('stores')) {
32
+ setStores(JSON.parse(CacheStorage.get('stores')));
33
+ setLoading(false);
34
+ } else {
35
+ fetchBranches();
36
+ }
37
+ }, []); // Chỉ chạy một lần khi component được mount
38
+
39
+ let storesContent;
40
+
41
+ if (loading) {
42
+ storesContent = (<p>Đang tải danh sách chi nhánh...</p>); // Hiển thị thông báo khi đang tải dữ liệu
43
+ } else {
44
+ storesContent = (<Col xs={12} md={9}>
45
+ <Container fluid>
46
+ <Row xs={1} md={2} xl={3} className="g-4">
47
+ {Array.from(stores).map((store, idx) => (
48
+ <Col key={idx}>
49
+ <StoreItem storeName={store.name}
50
+ address={store.location}
51
+ imageSrc={store.image_url}>
52
+ </StoreItem>
53
+ </Col>
54
+ ))}
55
+ </Row>
56
+ </Container>
57
+ </Col>)
58
+ }
59
 
60
  return (
61
+ <Container fluid id="store" className="text-center justify-content-center align-items-center my-5" style={{ maxWidth: "90%" }}>
62
  <h1 className='mb-5'>Các chi nhánh</h1>
63
  <Row className="align-items-center">
64
+ <Col xs={12} md={3} className="d-flex justify-content-center align-items-center">
65
+ hệ thống chuỗi nhà hàng nổi tiếng toàn quốc, chúng tôi đang hoạt động các cơ sở sau
66
  </Col>
67
 
68
+ {storesContent}
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  </Row>
70
  </Container>
71
  );
frontend/src/pages/AdminFeedPage.js CHANGED
@@ -2,10 +2,11 @@ import { Container, Row, Col } from "react-bootstrap";
2
  import AdminTemplate from "../templates/AdminTemplate";
3
 
4
  export default function AdminFeedPage() {
 
5
  return (
6
  <AdminTemplate content={
7
  (
8
- <Container className='d-flex text-center align-items-center justify-content-center' style={{ 'min-height': '80vh' }}>
9
  <Row>
10
  <Col xs={12}>
11
  <h1>This is a demo feed page</h1>
 
2
  import AdminTemplate from "../templates/AdminTemplate";
3
 
4
  export default function AdminFeedPage() {
5
+
6
  return (
7
  <AdminTemplate content={
8
  (
9
+ <Container className='d-flex text-center align-items-center justify-content-center' style={{ minHeight: '80vh' }}>
10
  <Row>
11
  <Col xs={12}>
12
  <h1>This is a demo feed page</h1>
frontend/src/pages/AdminLoginPage.js CHANGED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import { Alert, Container, Form, Row, Col, Button, Card } from "react-bootstrap";
3
+ import AdminTemplate from "../templates/AdminTemplate";
4
+ import { useNavigate } from "react-router-dom";
5
+ import axios from 'axios';
6
+ import jwtDecoder from "../organisms/jwtDecoder";
7
+ import DataStorage from "../organisms/DataStorage";
8
+
9
+ export default function AdminLoginPage() {
10
+
11
+ const domain = process.env.REACT_APP_API_URL;
12
+
13
+ const [username, setUsername] = useState('');
14
+ const [password, setPassword] = useState('');
15
+ const [error, setError] = useState('');
16
+
17
+ const navigate = useNavigate();
18
+
19
+ const handleSubmit = (e) => {
20
+ e.preventDefault();
21
+ // Validate password and confirm password match
22
+ if (password.length === 0) {
23
+ setError('Hãy nhập mật khẩu');
24
+ } else if (username.trim().length === 0) {
25
+ setError('Tên đăng nhập không thể trống')
26
+ } else {
27
+ setError('');
28
+
29
+ let data = {
30
+ 'username': username,
31
+ 'password': password
32
+ }
33
+
34
+ axios.post(domain + '/authentication/login', data)
35
+ .then((response) => {
36
+ if (response.status === 200) {
37
+ // setError(JSON.stringify(jwtDecoder(response.data.access_token)));
38
+ const decodedToken = jwtDecoder(response.data.access_token);
39
+ const role = decodedToken.payload.roles;
40
+
41
+ if (role !== 'ADMIN' && role !== 'AREA_MANAGER' && role !== 'BRANCH_MANAGER') {
42
+ setError('TÀI KHOẢN KHÔNG HỢP LỆ');
43
+ } else {
44
+ const full_name = decodedToken.payload.username;
45
+ const expiryTime = decodedToken.payload.exp;
46
+ DataStorage.set('expiryDateAdmin', expiryTime, { expiryDate: expiryTime });
47
+ DataStorage.set('usernameAdmin', full_name);
48
+ DataStorage.set('roleAdmin', role);
49
+ DataStorage.set('isLoggedInAdmin', 'true');
50
+ DataStorage.set('accessTokenAdmin', response.data.access_token);
51
+ navigate('/admin');
52
+ }
53
+ } else {
54
+ setError(JSON.stringify(response));
55
+ }
56
+ })
57
+ .catch((error) => {
58
+ // if (error.status === 400) {
59
+ // setError('Lỗi đăng nhập, vui lòng kiểm tra lại tài khoản và mật khẩu');
60
+ // } else if (error.status === 500) {
61
+ // setError('Server đang tạm gặp vấn đề');
62
+ // }
63
+ setError(JSON.stringify(error));
64
+ })
65
+ }
66
+ };
67
+
68
+ return (
69
+ <AdminTemplate content={
70
+ (<Container fluid className="d-flex justify-content-center align-items-center mt-5"
71
+ style={{
72
+ maxWidth: "100%",
73
+ minHeight: "70vh",
74
+ }}>
75
+ <Row style={{ maxWidth: "90vw" }}>
76
+ {/* <Col xs={1} md={2}></Col> */}
77
+ <Col xs={10} md={8}>
78
+ <Card style={{ width: '35vw' }} className='justify-content-center card-nospan'>
79
+ <Card.Header>
80
+ <Card.Title className='mt-1 text-center'>Đăng nhập quyền quản lý</Card.Title>
81
+ </Card.Header>
82
+ <Card.Body>
83
+ <Form onSubmit={handleSubmit} >
84
+ {error && <Alert variant="danger">{error}</Alert>}
85
+
86
+ <Form.Group controlId="username" className='mb-3'>
87
+ <Form.Label>Administration credential</Form.Label>
88
+ <Form.Control
89
+ type="text"
90
+ placeholder="Administrator email"
91
+ onChange={(e) => setUsername(e.target.value)}
92
+ />
93
+ </Form.Group>
94
+
95
+ <Form.Group controlId="password" className='mb-4'>
96
+ <Form.Label>Mật khẩu</Form.Label>
97
+ <Form.Control
98
+ type="password"
99
+ placeholder="Mật khẩu"
100
+ onChange={(e) => setPassword(e.target.value)}
101
+ />
102
+ </Form.Group>
103
+ <div className='d-flex justify-content-center align-items-center'>
104
+ {/* <Button as='a' variant='outline-primary' href="/register" className='m-2'>Đăng ký</Button> */}
105
+ <Button type="submit" className='m-2'>Đăng nhập</Button>
106
+ {/* <Button as='a' variant='outline-primary' href="/forgot-password" className='m-2'>Quên mật khẩu</Button> */}
107
+ </div>
108
+
109
+ </Form>
110
+ </Card.Body>
111
+ </Card>
112
+
113
+ </Col>
114
+ {/* <Col xs={1} md={2}></Col> */}
115
+ </Row>
116
+ </Container>)
117
+ }>
118
+ </AdminTemplate>
119
+ );
120
+ }
frontend/src/pages/AdminMenuPage.js CHANGED
@@ -1,11 +1,11 @@
1
- import { Container, Row, Col } from "react-bootstrap";
2
  import AdminTemplate from "../templates/AdminTemplate";
3
 
4
  export default function AdminMenuPage() {
5
  return (
6
  <AdminTemplate content={
7
  (
8
- <Container className='d-flex text-center align-items-center justify-content-center' style={{ 'min-height': '80vh' }}>
9
  <Row>
10
  <Col xs={12}>
11
  <h1>This is a demo menu page</h1>
 
1
+ import { Container, Row, Col } from 'react-bootstrap';
2
  import AdminTemplate from "../templates/AdminTemplate";
3
 
4
  export default function AdminMenuPage() {
5
  return (
6
  <AdminTemplate content={
7
  (
8
+ <Container className='d-flex text-center align-items-center justify-content-center' style={{ minHeight: '80vh' }}>
9
  <Row>
10
  <Col xs={12}>
11
  <h1>This is a demo menu page</h1>
frontend/src/pages/AdminOrderPage.js CHANGED
@@ -5,7 +5,7 @@ export default function AdminOrderPage() {
5
  return (
6
  <AdminTemplate content={
7
  (
8
- <Container className='d-flex text-center align-items-center justify-content-center' style={{ 'min-height': '80vh' }}>
9
  <Row>
10
  <Col xs={12}>
11
  <h1>This is a demo orders page</h1>
 
5
  return (
6
  <AdminTemplate content={
7
  (
8
+ <Container className='d-flex text-center align-items-center justify-content-center' style={{ minHeight: '80vh' }}>
9
  <Row>
10
  <Col xs={12}>
11
  <h1>This is a demo orders page</h1>
frontend/src/pages/AdminSchedulePage.js CHANGED
@@ -5,7 +5,7 @@ export default function AdminSchedulePage() {
5
  return (
6
  <AdminTemplate content={
7
  (
8
- <Container className='d-flex text-center align-items-center justify-content-center' style={{ 'min-height': '80vh' }}>
9
  <Row>
10
  <Col xs={12}>
11
  <h1>This is a demo schedule page</h1>
 
5
  return (
6
  <AdminTemplate content={
7
  (
8
+ <Container className='d-flex text-center align-items-center justify-content-center' style={{ minHeight: '80vh' }}>
9
  <Row>
10
  <Col xs={12}>
11
  <h1>This is a demo schedule page</h1>
frontend/src/pages/AdminStaffPage.js CHANGED
@@ -5,7 +5,7 @@ export default function AdminStaffPage() {
5
  return (
6
  <AdminTemplate content={
7
  (
8
- <Container className='d-flex text-center align-items-center justify-content-center' style={{ 'min-height': '80vh' }}>
9
  <Row>
10
  <Col xs={12}>
11
  <h1>This is a demo staff page</h1>
 
5
  return (
6
  <AdminTemplate content={
7
  (
8
+ <Container className='d-flex text-center align-items-center justify-content-center' style={{minHeight: '80vh' }}>
9
  <Row>
10
  <Col xs={12}>
11
  <h1>This is a demo staff page</h1>
frontend/src/pages/AdminSummaryPage.js CHANGED
@@ -1,11 +1,23 @@
1
  import { Container, Row, Col } from "react-bootstrap";
2
  import AdminTemplate from "../templates/AdminTemplate";
 
 
 
3
 
4
  export default function AdminSummaryPage() {
 
 
 
 
 
 
 
 
 
5
  return (
6
  <AdminTemplate content={
7
  (
8
- <Container className='d-flex text-center align-items-center justify-content-center' style={{ 'min-height': '80vh' }}>
9
  <Row>
10
  <Col xs={12}>
11
  <h1>This is a demo summary page</h1>
 
1
  import { Container, Row, Col } from "react-bootstrap";
2
  import AdminTemplate from "../templates/AdminTemplate";
3
+ import { useNavigate } from "react-router-dom";
4
+ import { useEffect } from "react";
5
+ import DataStorage from "../organisms/DataStorage";
6
 
7
  export default function AdminSummaryPage() {
8
+
9
+ const navigate = useNavigate();
10
+
11
+ useEffect(() => {
12
+ if (!DataStorage.get('isLoggedInAdmin')) {
13
+ navigate('/admin-login');
14
+ }
15
+ }, [navigate]);
16
+
17
  return (
18
  <AdminTemplate content={
19
  (
20
+ <Container className='d-flex text-center align-items-center justify-content-center' style={{ minHeight: '80vh' }}>
21
  <Row>
22
  <Col xs={12}>
23
  <h1>This is a demo summary page</h1>
frontend/src/pages/AdminUserInfoPage.js ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import AdminTemplate from "../templates/AdminTemplate";
2
+ import { Container, Form, Row, Col, Card, Alert, Button, Image } from "react-bootstrap";
3
+ import React, { useState, useEffect } from "react";
4
+ import validator from "validator";
5
+ import axios from "axios";
6
+ import DataStorage from "../organisms/DataStorage";
7
+
8
+ export default function AdminUserInfoPage() {
9
+
10
+ // fetch data:
11
+
12
+ const [error, setErrors] = useState("");
13
+ const [initialData, setInitialData] = useState(null);
14
+ const [formData, setFormData] = useState({
15
+ avatar: "/default_avatar.jpg",
16
+ full_name: "",
17
+ phone_number: "",
18
+ address: "",
19
+ email: "",
20
+ password: "",
21
+ confirmPassword: "",
22
+ });
23
+
24
+ useEffect(() => {
25
+ axios
26
+ .get(process.env.REACT_APP_API_URL + '/authentication/profile', {
27
+ headers: {
28
+ Authorization: `Bearer ${DataStorage.get('accessToken')}`,
29
+ },
30
+ })
31
+ .then((response) => {
32
+ if (response.status === 401) {
33
+ // phiên hết hạn
34
+ setErrors(JSON.stringify(response));
35
+ } else {
36
+ const { avatar, full_name, address, phone_number, email } = response.data;
37
+ const fetchedData = {
38
+ avatar: avatar || "/default_avatar.jpg",
39
+ full_name: full_name || "",
40
+ phone_number: phone_number || "",
41
+ address: address || "",
42
+ email: email || "",
43
+ password: "",
44
+ confirmPassword: "",
45
+ };
46
+
47
+ if (fetchedData.full_name !== DataStorage.get('username')) DataStorage.set('username', fetchedData.full_name)
48
+ setInitialData(fetchedData);
49
+ setFormData(fetchedData);
50
+ }
51
+ })
52
+ .catch((error) => {
53
+ setErrors(JSON.stringify(error));
54
+ });
55
+ }, []);
56
+
57
+ // console.log('formdata', formData);
58
+ // console.log('initialdata', initialData);
59
+ // State để hiển thị lỗi khi validation
60
+
61
+ const [isChanged, setChanged] = useState(false);
62
+
63
+ const handleChange = (e) => {
64
+ const { name, value } = e.target;
65
+ setFormData({ ...formData, [name]: value });
66
+ };
67
+
68
+ // Hàm xử lý thay đổi avatar
69
+ const handleAvatarChange = (e) => {
70
+ const file = e.target.files[0];
71
+ if (file) {
72
+ const reader = new FileReader();
73
+ reader.onloadend = () => {
74
+ setFormData((prevData) => ({
75
+ ...prevData,
76
+ avatar: reader.result // Cập nhật ảnh đại diện
77
+ }));
78
+ };
79
+ reader.readAsDataURL(file);
80
+ setChanged(true);
81
+ }
82
+ };
83
+
84
+ useEffect(() => {
85
+ const hasChanges = () => {
86
+ if (initialData) {
87
+ for (let key in formData) {
88
+ if (formData[key] !== initialData[key]) {
89
+ return true;
90
+ }
91
+ }
92
+ return false;
93
+ } else {
94
+ return false;
95
+ }
96
+ };
97
+ setChanged(hasChanges());
98
+ }, [formData, initialData]);
99
+
100
+ const handleSubmit = async (e) => {
101
+ e.preventDefault();
102
+ setChanged(false); // prevent another click
103
+ if (formData.phone_number.trim() !== "" && !validator.isMobilePhone(formData.phone_number, 'vi-VN')) setErrors("Số điện thoại không hợp lệ");
104
+ else if (formData.email.trim() !== "" && !validator.isEmail(formData.email)) setErrors("Email không hợp lệ");
105
+ else if (formData.password !== "" && formData.confirmPassword !== formData.password) setErrors("Mật khẩu không khớp");
106
+ else {
107
+
108
+ // let avatarBase64 = null;
109
+
110
+ // if (formData.avatar) {
111
+ // // Chuyển ảnh đại diện thành chuỗi Base64
112
+ // avatarBase64 = await new Promise((resolve, reject) => {
113
+ // const reader = new FileReader();
114
+ // reader.onloadend = () => resolve(reader.result.split(",")[1]); // Chỉ lấy phần Base64
115
+ // reader.onerror = (error) => reject(error);
116
+ // reader.readAsDataURL(formData.avatar);
117
+ // });
118
+ // }
119
+
120
+ let data = {};
121
+
122
+ // if (avatarBase64) data.avatar = avatarBase64;
123
+ if (formData.full_name.trim() !== "" && formData.full_name.trim() !== initialData.full_name) data.full_name = formData.full_name.trim();
124
+ if (formData.phone_number.trim() !== "" && formData.phone_number.trim() !== initialData.phone_number) data.phone_number = formData.phone_number.trim();
125
+ if (formData.address.trim() !== "" && formData.address.trim() !== initialData.address) data.address = formData.address.trim();
126
+ if (formData.email.trim() !== "" && formData.email.trim() !== initialData.email) data.email = formData.email.trim();
127
+ if (formData.password.trim() !== "") data.hash_password = formData.password;
128
+
129
+ // gửi request ở đây
130
+ axios.post(process.env.REACT_APP_API_URL + '/users/updateUser', data, {
131
+ headers: {
132
+ Authorization: `Bearer ${DataStorage.get('accessToken')}`,
133
+ }
134
+ }).then((response) => {
135
+ if (response.status === 200 || response.status === 201) {
136
+ window.location.reload(); // cập nhật lại thông tin
137
+ } else {
138
+ setErrors(JSON.stringify(response));
139
+ }
140
+ }).catch((error) => setErrors(JSON.stringify(error)));
141
+ }
142
+ };
143
+
144
+ return <AdminTemplate content={
145
+ (
146
+ <Container fluid className="d-flex align-items-center justify-content-center mt-5">
147
+ <Row>
148
+ <Col xs={10} md={8}>
149
+ <Card style={{ width: '30rem' }} className='justify-content-center'>
150
+ <Card.Header>
151
+ <Card.Title className='mt-1 text-center'>Thông tin khách hàng</Card.Title>
152
+ </Card.Header>
153
+ <Card.Body>
154
+
155
+ <Form onSubmit={handleSubmit}>
156
+ {error && <Alert variant="danger">{error}</Alert>}
157
+ <Row className="mb-3">
158
+ <Col xs={12} md={6} className="text-center">
159
+ {/*Image*/}
160
+ <Image
161
+ src={formData.avatar} // Hiển thị ảnh đại diện từ formData
162
+ alt="User Avatar"
163
+ roundedCircle
164
+ width={120}
165
+ height={120}
166
+ />
167
+ <Form.Group controlId="formAvatar">
168
+ <Form.Label>Ảnh đại diện</Form.Label>
169
+ <Form.Control type="file" accept="image/*" onChange={handleAvatarChange} />
170
+ </Form.Group>
171
+ </Col>
172
+ <Col xs={12} md={6}>
173
+ <Form.Group controlId="formUsername">
174
+ <Form.Label>Họ tên người dùng</Form.Label>
175
+ <Form.Control
176
+ type="text"
177
+ name="full_name"
178
+ placeholder="Họ tên người dùng"
179
+ value={formData.full_name}
180
+ onChange={handleChange}
181
+ />
182
+ </Form.Group>
183
+ </Col>
184
+ </Row>
185
+
186
+ <Form.Group controlId="formPhone" className="mb-3">
187
+ <Form.Label>Số điện thoại</Form.Label>
188
+ <Form.Control
189
+ type="text"
190
+ name="phone_number"
191
+ placeholder="Số điện thoại"
192
+ value={formData.phone_number}
193
+ onChange={handleChange}
194
+ />
195
+ </Form.Group>
196
+
197
+ <Form.Group controlId="formAddress" className="mb-3">
198
+ <Form.Label>Địa chỉ giao hàng mặc định</Form.Label>
199
+ <Form.Control
200
+ type="text"
201
+ name="address"
202
+ placeholder="Địa chỉ giao hàng"
203
+ value={formData.address}
204
+ onChange={handleChange}
205
+ />
206
+ </Form.Group>
207
+
208
+ <Form.Group controlId="formEmail" className="mb-3">
209
+ <Form.Label>Email</Form.Label>
210
+ <Form.Control
211
+ type="text"
212
+ name="email"
213
+ placeholder="Email"
214
+ value={formData.email}
215
+ onChange={handleChange}
216
+ />
217
+ </Form.Group>
218
+
219
+ <Form.Group controlId="formPassword" className="mb-3">
220
+ <Form.Label>Password</Form.Label>
221
+ <Form.Control
222
+ type="password"
223
+ name="password"
224
+ placeholder="********"
225
+ value={formData.password}
226
+ onChange={handleChange}
227
+ />
228
+ </Form.Group>
229
+
230
+ <Form.Group controlId="formConfirmPassword" className="mb-3">
231
+ <Form.Label>Nhập lại Password</Form.Label>
232
+ <Form.Control
233
+ type="password"
234
+ name="confirmPassword"
235
+ placeholder="********"
236
+ value={formData.confirmPassword}
237
+ onChange={handleChange}
238
+ />
239
+ </Form.Group>
240
+
241
+ <Button variant="primary" type="submit" disabled={!isChanged}>
242
+ Cập nhật
243
+ </Button>
244
+ </Form>
245
+ </Card.Body>
246
+ </Card>
247
+
248
+ </Col>
249
+ </Row>
250
+
251
+
252
+ </Container>
253
+ )
254
+ } />
255
+ }
frontend/src/pages/CartPage.js CHANGED
@@ -12,29 +12,27 @@ export default function CartPage() {
12
  const cart = JSON.parse(DataStorage.get('cart')) || {};
13
 
14
  // Chuyển cart thành mảng chứa các món có số lượng > 0
15
- const items = Object.entries(cart)
16
- .filter(([name, amount]) => amount > 0)
17
- .map(([name, amount]) => ({
18
- name,
19
- imageSrc: '/placeholder3.jpg',
20
- price: 100, // Thêm đơn giá của món ăn (giả sử ở đây là 100 cho mỗi món)
21
- amount
22
- }));
23
-
24
  setCartItems(items);
25
  }, []);
26
 
27
-
28
  return (
29
  <BasicTemplate content={
30
  (
31
- <Container className="d-flex align-items-center justify-content-center my-5" style={{ 'min-height': '70vh' }}>
32
  {cartItems.length > 0 ? (
33
  <div className="text-center">
34
  <h2 className="text-center mb-4">Giỏ hàng của bạn</h2>
35
  <Row className="g-3">
36
  {cartItems.map((item, idx) => (
37
- <Col md={12} key={idx} className="my-3">
38
  <Card className="shadow-sm" style={{ display: 'flex', flexDirection: 'row' }}>
39
  <Card.Img
40
  variant="left"
@@ -42,22 +40,30 @@ export default function CartPage() {
42
  style={{ width: '150px', objectFit: 'cover' }}
43
  />
44
  <Card.Body>
45
- <Row xs={4} className="align-items-center justify-content-center">
46
- <Card.Title as='Col'>{item.name}</Card.Title>
47
- <Card.Text as='Col'>Đơn giá: {item.price} VND</Card.Text>
48
- <Card.Text as='Col'>Số lượng: {item.amount}</Card.Text>
49
- <Card.Text as='Col'>
50
- Tổng cộng: {item.price * item.amount} VND
51
- </Card.Text>
 
 
 
 
 
 
52
  </Row>
53
  </Card.Body>
54
  </Card>
55
  </Col>
56
  ))}
57
  </Row>
58
- <Button as='a' href='/payment' className='my-3'>
59
  Thanh toán
60
  </Button>
 
 
61
  </div>
62
  ) : (
63
  <div className="text-center">
 
12
  const cart = JSON.parse(DataStorage.get('cart')) || {};
13
 
14
  // Chuyển cart thành mảng chứa các món có số lượng > 0
15
+ const items = Object.entries(cart).map(([_, item]) => ({
16
+ name: item.name,
17
+ amount: item.amount,
18
+ imageSrc: item.imageSrc,
19
+ price: item.price
20
+ }));
21
+
22
+ console.log(items);
 
23
  setCartItems(items);
24
  }, []);
25
 
 
26
  return (
27
  <BasicTemplate content={
28
  (
29
+ <Container className="d-flex align-items-center justify-content-center my-5" style={{ minHeight: '70vh' }}>
30
  {cartItems.length > 0 ? (
31
  <div className="text-center">
32
  <h2 className="text-center mb-4">Giỏ hàng của bạn</h2>
33
  <Row className="g-3">
34
  {cartItems.map((item, idx) => (
35
+ <Col md={12} key={idx} className="m-3">
36
  <Card className="shadow-sm" style={{ display: 'flex', flexDirection: 'row' }}>
37
  <Card.Img
38
  variant="left"
 
40
  style={{ width: '150px', objectFit: 'cover' }}
41
  />
42
  <Card.Body>
43
+ <Row xs={4} >
44
+ <Col className="d-flex align-items-center justify-content-center">
45
+ <Card.Title>{item.name}</Card.Title>
46
+ </Col>
47
+ <Col className="d-flex align-items-center justify-content-center">
48
+ <Card.Text>Đơn giá: {item.price} VND</Card.Text>
49
+ </Col>
50
+ <Col className="d-flex align-items-center justify-content-center">
51
+ <Card.Text>Số lượng: {item.amount}</Card.Text>
52
+ </Col>
53
+ <Col className="d-flex align-items-center justify-content-center">
54
+ <Card.Text>Tổng cộng: {item.price * item.amount} VND</Card.Text>
55
+ </Col>
56
  </Row>
57
  </Card.Body>
58
  </Card>
59
  </Col>
60
  ))}
61
  </Row>
62
+ <Button as='a' href='/payment' className='m-3'>
63
  Thanh toán
64
  </Button>
65
+ <Button as='a' href='/menu' className="m-3">
66
+ Quay lại menu</Button>
67
  </div>
68
  ) : (
69
  <div className="text-center">
frontend/src/pages/LoginPage.js CHANGED
@@ -64,11 +64,21 @@ export default function LoginPage() {
64
 
65
  return (
66
  <BasicTemplate content={
67
- (<Container fluid className="d-flex justify-content-center mt-5">
68
- <Row>
69
- <Col xs={1} md={2}></Col>
 
 
 
 
 
 
 
 
 
 
70
  <Col xs={10} md={8}>
71
- <Card style={{ width: '30rem' }} className='justify-content-center'>
72
  <Card.Header>
73
  <Card.Title className='mt-1 text-center'>Đăng nhập</Card.Title>
74
  </Card.Header>
@@ -85,7 +95,7 @@ export default function LoginPage() {
85
  />
86
  </Form.Group>
87
 
88
- <Form.Group controlId="password" className='mb-3'>
89
  <Form.Label>Mật khẩu</Form.Label>
90
  <Form.Control
91
  type="password"
@@ -93,10 +103,10 @@ export default function LoginPage() {
93
  onChange={(e) => setPassword(e.target.value)}
94
  />
95
  </Form.Group>
96
- <div className='d-flex justify-content-between align-items-center'>
97
- <a href="/register" className='me-2'>Đăng ký</a>
98
- <Button type="submit" className='me-2'>Đăng nhập</Button>
99
- <a href="/forgot-password" className='me-2'>Quên mật khẩu</a>
100
  </div>
101
 
102
  </Form>
@@ -104,7 +114,7 @@ export default function LoginPage() {
104
  </Card>
105
 
106
  </Col>
107
- <Col xs={1} md={2}></Col>
108
  </Row>
109
  </Container>)
110
  }>
 
64
 
65
  return (
66
  <BasicTemplate content={
67
+ (<Container fluid className="d-flex justify-content-center align-items-center mt-5"
68
+ style={{
69
+ maxWidth: "100%",
70
+ minHeight: "70vh",
71
+ backgroundImage: "linear-gradient(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.3)), url('/about-us.jpg')", // Thay đường dẫn ảnh nền
72
+ backgroundSize: 'cover',
73
+ backgroundPosition: 'center',
74
+ backgroundAttachment: 'fixed',
75
+ width: '100%',
76
+ padding: '50px 0',
77
+ }}>
78
+ <Row style={{maxWidth:"90vw"}}>
79
+ {/* <Col xs={1} md={2}></Col> */}
80
  <Col xs={10} md={8}>
81
+ <Card style={{ width: '35vw' }} className='justify-content-center card-nospan'>
82
  <Card.Header>
83
  <Card.Title className='mt-1 text-center'>Đăng nhập</Card.Title>
84
  </Card.Header>
 
95
  />
96
  </Form.Group>
97
 
98
+ <Form.Group controlId="password" className='mb-4'>
99
  <Form.Label>Mật khẩu</Form.Label>
100
  <Form.Control
101
  type="password"
 
103
  onChange={(e) => setPassword(e.target.value)}
104
  />
105
  </Form.Group>
106
+ <div className='d-flex justify-content-center align-items-center'>
107
+ <Button as='a' variant='outline-primary' href="/register" className='m-2'>Đăng ký</Button>
108
+ <Button type="submit" className='m-2'>Đăng nhập</Button>
109
+ <Button as='a' variant='outline-primary' href="/forgot-password" className='m-2'>Quên mật khẩu</Button>
110
  </div>
111
 
112
  </Form>
 
114
  </Card>
115
 
116
  </Col>
117
+ {/* <Col xs={1} md={2}></Col> */}
118
  </Row>
119
  </Container>)
120
  }>
frontend/src/pages/MenuPage.js CHANGED
@@ -1,26 +1,43 @@
1
- import { useState } from 'react';
2
  import { Modal, Button, Container, Row, Col, Tab, Tabs, Form, InputGroup } from 'react-bootstrap';
3
  import MenuItem from '../molecules/MenuItem';
4
  import BasicTemplate from '../templates/BasicTemplate';
5
  import DataStorage from '../organisms/DataStorage';
 
 
 
 
 
 
 
 
6
 
7
  function MenuPage() {
8
- const [key, setKey] = useState('cat1');
9
  const [selectedDish, setSelectedDish] = useState(null);
10
  const [show, setShow] = useState(false);
11
  const [cartAmount, setCartAmount] = useState(0);
 
 
12
 
13
  function handleClose() {
14
  const cart = JSON.parse(DataStorage.get('cart')) || {}; // Đảm bảo cart không null
15
- cart[selectedDish.name] = cartAmount;
16
- const filteredCart = Object.fromEntries(
17
- Object.entries(cart).filter(([key, value]) => value > 0)
18
- );
19
- DataStorage.set('cart', JSON.stringify(filteredCart));
20
- // console.log(JSON.stringify(filteredCart));
21
- setShow(false);
22
- }
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  function notLoggedInClose() {
25
  setShow(false);
26
  }
@@ -36,7 +53,7 @@ function MenuPage() {
36
  const cart = JSON.parse(DataStorage.get('cart')) || {};
37
 
38
  // Kiểm tra số lượng món ăn trong cart
39
- const amount = cart[dish.name] !== undefined ? cart[dish.name] : 0;
40
  setCartAmount(amount); // Cập nhật số lượng
41
  setShow(true); // Hiển thị modal
42
  }
@@ -52,27 +69,35 @@ function MenuPage() {
52
  }
53
  }
54
 
55
- const menuItems1 = [
56
- { name: 'Món 1 thể loại 1', description: 'Mô tả món 1', imageSrc: '/placeholder3.jpg' },
57
- { name: 'Món 2 thể loại 1', description: 'Mô tả món 2', imageSrc: '/placeholder3.jpg' },
58
- { name: 'Món 3 thể loại 1', description: 'Mô tả món 3', imageSrc: '/placeholder3.jpg' }
59
- ];
60
-
61
- const menuItems2 = [
62
- { name: 'Món 1 thể loại 2', description: 'Mô tả món 1', imageSrc: '/placeholder3.jpg' },
63
- { name: 'Món 2 thể loại 2', description: 'Mô tả món 2', imageSrc: '/placeholder3.jpg' },
64
- { name: 'Món 3 thể loại 2', description: 'Mô tả món 3', imageSrc: '/placeholder3.jpg' },
65
- { name: 'Món 3 thể loại 2', description: 'Mô tả món 3', imageSrc: '/placeholder3.jpg' },
66
- ];
67
-
68
- const menuItems3 = [
69
- { name: 'Món 1 thể loại 3', description: 'Mô tả món 1', imageSrc: '/placeholder3.jpg' },
70
- { name: 'Món 2 thể loại 3', description: 'Mô tả món 2', imageSrc: '/placeholder3.jpg' },
71
- { name: 'Món 3 thể loại 3', description: 'Mô tả món 3', imageSrc: '/placeholder3.jpg' },
72
- { name: 'Món 4 thể loại 3', description: 'Mô tả món 1', imageSrc: '/placeholder3.jpg' },
73
- { name: 'Món 5 thể loại 3', description: 'Mô tả món 2', imageSrc: '/placeholder3.jpg' },
74
- { name: 'Món 6 thể loại 3', description: 'Mô tả món 3', imageSrc: '/placeholder3.jpg' }
75
- ];
 
 
 
 
 
 
 
 
76
 
77
  let modalContent;
78
 
@@ -86,37 +111,40 @@ function MenuPage() {
86
  Vui lòng đăng nhập để xem chi tiết món và đặt hàng
87
  </Modal.Body>
88
  <Modal.Footer>
89
- <Button variant="primary" as='a' href='/login'>
90
- Đăng nhập
91
- </Button>
92
- <Button variant="secondary" onClick={notLoggedInClose}>
93
- Đóng
94
- </Button>
95
- </Modal.Footer>
96
  </Modal>
97
  )
98
  }
99
  else {
100
  modalContent = (<Modal show={show} onHide={handleClose} className='text-center'>
101
  <Modal.Header closeButton className='text-center'>
102
- <Modal.Title >{selectedDish?.name}</Modal.Title> {/* Dish name in the title */}
103
  </Modal.Header>
104
  <Modal.Body>
105
- <img src={selectedDish?.imageSrc} alt={selectedDish?.name} style={{ width: '100%' }} /> {/* Dish image */}
106
  <p>{selectedDish?.description}</p> {/* Dish description */}
107
 
108
  <Row className='mb-5'>
109
  <Col md={2}></Col>
110
  <Col md={8}>
111
  <Form.Label>Số lượng</Form.Label>
112
- <InputGroup as='row' mb={4} className='mb-3'>
113
- <InputGroup.Text as='button' onClick={setDecrease}>-</InputGroup.Text>
114
- <Form.Control
115
- value={cartAmount}
116
- aria-label="Amount"
117
- onChange={(e) => setCartAmount(e.target.value)} />
118
- <InputGroup.Text as='button' onClick={setIncrease}>+</InputGroup.Text>
119
- </InputGroup></Col>
 
 
 
120
  <Col md={2}></Col>
121
  </Row>
122
 
@@ -129,8 +157,41 @@ function MenuPage() {
129
  </Modal>);
130
  }
131
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  return <BasicTemplate content={(
133
- <Container fluid className='my-5'>
134
  <>
135
  {modalContent}
136
  </>
@@ -138,65 +199,7 @@ function MenuPage() {
138
  <Row>
139
  <Col xs={1} md={2}></Col>
140
  <Col xs={10} md={8}>
141
- <Tabs
142
- id="controlled-tab-example"
143
- activeKey={key}
144
- onSelect={(k) => setKey(k)}
145
- className="mb-3"
146
- >
147
- <Tab eventKey="cat1" title="Thể loại 1">
148
- <Container fluid className='my-5'>
149
- <Row md={3} className="g-4">
150
- {menuItems1.map((item, idx) => (
151
- <Col key={idx} >
152
- <div onClick={() => handleShow(item)}>
153
- <MenuItem
154
- dishName={item.name}
155
- description={item.description}
156
- imageSrc={item.imageSrc}
157
- />
158
- </div>
159
-
160
- </Col>
161
- ))}
162
- </Row>
163
- </Container>
164
- </Tab>
165
- <Tab eventKey="cat2" title="Thể loại 2">
166
- <Container fluid className='my-5'>
167
- <Row md={3} className="g-4">
168
- {menuItems2.map((item, idx) => (
169
- <Col key={idx}>
170
- <div onClick={() => handleShow(item)}>
171
- <MenuItem
172
- dishName={item.name}
173
- description={item.description}
174
- imageSrc={item.imageSrc}
175
- />
176
- </div>
177
- </Col>
178
- ))}
179
- </Row>
180
- </Container>
181
- </Tab>
182
- <Tab eventKey="cat3" title="Thể loại 3">
183
- <Container fluid className='my-5'>
184
- <Row md={3} className="g-4">
185
- {menuItems3.map((item, idx) => (
186
- <Col key={idx}>
187
- <div onClick={() => handleShow(item)}>
188
- <MenuItem
189
- dishName={item.name}
190
- description={item.description}
191
- imageSrc={item.imageSrc}
192
- />
193
- </div>
194
- </Col>
195
- ))}
196
- </Row>
197
- </Container>
198
- </Tab>
199
- </Tabs>
200
  </Col>
201
  <Col xs={1} md={2}></Col>
202
  </Row>
 
1
+ import { useState, useEffect } from 'react';
2
  import { Modal, Button, Container, Row, Col, Tab, Tabs, Form, InputGroup } from 'react-bootstrap';
3
  import MenuItem from '../molecules/MenuItem';
4
  import BasicTemplate from '../templates/BasicTemplate';
5
  import DataStorage from '../organisms/DataStorage';
6
+ import CacheStorage from '../organisms/CacheStorage';
7
+ import axios from 'axios';
8
+
9
+ const categoryMapper = [
10
+ { itemType: 1, category: 'Món chính' },
11
+ { itemType: 2, category: 'Đồ uống' },
12
+ { itemType: 3, category: 'Tráng miệng' },
13
+ ];
14
 
15
  function MenuPage() {
16
+ const [key, setKey] = useState(0);
17
  const [selectedDish, setSelectedDish] = useState(null);
18
  const [show, setShow] = useState(false);
19
  const [cartAmount, setCartAmount] = useState(0);
20
+ const [loading, setLoading] = useState(true);
21
+ const [menuItems, setMenuItems] = useState({});
22
 
23
  function handleClose() {
24
  const cart = JSON.parse(DataStorage.get('cart')) || {}; // Đảm bảo cart không null
 
 
 
 
 
 
 
 
25
 
26
+ cart[selectedDish.id] = {
27
+ 'name':selectedDish.item_name,
28
+ 'amount': cartAmount,
29
+ 'imageSrc': selectedDish.image_url,
30
+ 'price': selectedDish.price
31
+ };
32
+ for (let id in cart) {
33
+ if (cart[id].amount === 0) {
34
+ delete cart[id];
35
+ }
36
+ DataStorage.set('cart', JSON.stringify(cart));
37
+ // console.log(JSON.stringify(filteredCart));
38
+ setShow(false);
39
+ }
40
+ }
41
  function notLoggedInClose() {
42
  setShow(false);
43
  }
 
53
  const cart = JSON.parse(DataStorage.get('cart')) || {};
54
 
55
  // Kiểm tra số lượng món ăn trong cart
56
+ const amount = cart[dish.id] !== undefined ? cart[dish.id] : 0;
57
  setCartAmount(amount); // Cập nhật số lượng
58
  setShow(true); // Hiển thị modal
59
  }
 
69
  }
70
  }
71
 
72
+ useEffect(() => {
73
+ const fetchMenuItems = async () => {
74
+ try {
75
+ const menuItemsByType = {};
76
+
77
+ for (const item of categoryMapper) {
78
+ // Gọi API để lấy món theo itemType
79
+ const response = await axios.get(process.env.REACT_APP_API_URL + `/menu-items?filter.item_type=${item.itemType}`);
80
+ // Lưu danh sách món theo itemType
81
+ menuItemsByType[item.itemType] = response.data.data;
82
+ }
83
+ console.log(menuItemsByType);
84
+ setMenuItems(menuItemsByType);
85
+ setLoading(false);
86
+ CacheStorage.set('menuItems',JSON.stringify(menuItemsByType));
87
+ } catch (error) {
88
+ console.error('Error fetching menu items:', error);
89
+ setLoading(false);
90
+ }
91
+ };
92
+
93
+ if (CacheStorage.get('menuItems')) {
94
+ setMenuItems(JSON.parse(CacheStorage.get('menuItems')));
95
+ setLoading(false);
96
+ } else {
97
+ fetchMenuItems();
98
+ }
99
+
100
+ }, []);
101
 
102
  let modalContent;
103
 
 
111
  Vui lòng đăng nhập để xem chi tiết món và đặt hàng
112
  </Modal.Body>
113
  <Modal.Footer>
114
+ <Button variant="primary" as='a' href='/login'>
115
+ Đăng nhập
116
+ </Button>
117
+ <Button variant="secondary" onClick={notLoggedInClose}>
118
+ Đóng
119
+ </Button>
120
+ </Modal.Footer>
121
  </Modal>
122
  )
123
  }
124
  else {
125
  modalContent = (<Modal show={show} onHide={handleClose} className='text-center'>
126
  <Modal.Header closeButton className='text-center'>
127
+ <Modal.Title >{selectedDish?.item_name}</Modal.Title> {/* Dish name in the title */}
128
  </Modal.Header>
129
  <Modal.Body>
130
+ <img src={selectedDish?.image_url} alt={selectedDish?.item_name} style={{ width: '100%' }} /> {/* Dish image */}
131
  <p>{selectedDish?.description}</p> {/* Dish description */}
132
 
133
  <Row className='mb-5'>
134
  <Col md={2}></Col>
135
  <Col md={8}>
136
  <Form.Label>Số lượng</Form.Label>
137
+ <Row>
138
+ <InputGroup mb={4} className='mb-3'>
139
+ <InputGroup.Text as='button' onClick={setDecrease}>-</InputGroup.Text>
140
+ <Form.Control
141
+ value={cartAmount}
142
+ aria-label="Amount"
143
+ onChange={(e) => setCartAmount(e.target.value)} />
144
+ <InputGroup.Text as='button' onClick={setIncrease}>+</InputGroup.Text>
145
+ </InputGroup>
146
+ </Row>
147
+ </Col>
148
  <Col md={2}></Col>
149
  </Row>
150
 
 
157
  </Modal>);
158
  }
159
 
160
+ let menuContent;
161
+
162
+ if (loading) {
163
+ menuContent = (<p>Đang tải thực đơn...</p>);
164
+ } else {
165
+ menuContent = (<Tabs
166
+ id="controlled-tab-example"
167
+ activeKey={key}
168
+ onSelect={(k) => setKey(k)}
169
+ className="mb-3 custom-tab"
170
+ >
171
+ {categoryMapper.map((category, index) => (
172
+ <Tab eventKey={index} title={category.category}>
173
+ <Container fluid className='my-5'>
174
+ <Row md={3} className="g-4">
175
+ {menuItems[category.itemType].map((item, idx) => (
176
+ <Col key={item.id}>
177
+ <div onClick={() => handleShow(item)} className="text-center">
178
+ <MenuItem
179
+ dishName={item.item_name}
180
+ description={item.description}
181
+ imageSrc={item.image_url}
182
+ />
183
+ </div>
184
+ </Col>
185
+ ))}
186
+ </Row>
187
+ </Container>
188
+ </Tab>
189
+ ))}
190
+ </Tabs>);
191
+ }
192
+
193
  return <BasicTemplate content={(
194
+ <Container fluid className='my-5' style={{ minHeight: '70vh' }}>
195
  <>
196
  {modalContent}
197
  </>
 
199
  <Row>
200
  <Col xs={1} md={2}></Col>
201
  <Col xs={10} md={8}>
202
+ {menuContent}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  </Col>
204
  <Col xs={1} md={2}></Col>
205
  </Row>
frontend/src/pages/NewsPage.js CHANGED
@@ -1,12 +1,22 @@
1
  import {Container, Row, Col} from "react-bootstrap";
2
  import BasicTemplate from "../templates/BasicTemplate";
 
 
3
 
4
- export default function NewsPage({newsTitle, newsContent, newsImageSrc}) {
5
 
6
- newsTitle = "This is a demo newstitle"
7
- newsContent = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
8
- newsImageSrc = "/placeholder3.jpg"
 
 
9
 
 
 
 
 
 
 
10
  return (
11
  <BasicTemplate content={
12
  (<Container id="about-us" className="my-5">
 
1
  import {Container, Row, Col} from "react-bootstrap";
2
  import BasicTemplate from "../templates/BasicTemplate";
3
+ import { useSearchParams } from 'react-router-dom';
4
+ import CacheStorage from "../organisms/CacheStorage";
5
 
6
+ export default function NewsPage() {
7
 
8
+
9
+ const [searchParams] = useSearchParams();
10
+ const newsId = Number(searchParams.get('id'));
11
+
12
+ const feedDetail = Array.from(JSON.parse(CacheStorage.get('feeds'))).filter((item) => item.id === newsId);
13
 
14
+
15
+
16
+ const newsTitle = feedDetail[0].title;
17
+ const newsContent = feedDetail[0].description;
18
+ const newsImageSrc = feedDetail[0].image_url;
19
+
20
  return (
21
  <BasicTemplate content={
22
  (<Container id="about-us" className="my-5">
frontend/src/pages/RegisterPage.js CHANGED
@@ -75,11 +75,21 @@ const RegisterPage = () => {
75
 
76
  return (
77
  <BasicTemplate content={
78
- (<Container fluid className="d-flex justify-content-center mt-5">
79
- <Row>
80
- <Col xs={1} md={2}></Col>
 
 
 
 
 
 
 
 
 
 
81
  <Col xs={10} md={8}>
82
- <Card style={{ width: '30rem' }} className='justify-content-center'>
83
  <Card.Header>
84
  <Card.Title className='mt-1 text-center'>Đăng ký</Card.Title>
85
  </Card.Header>
@@ -132,7 +142,7 @@ const RegisterPage = () => {
132
  />
133
  </Form.Group>
134
 
135
- <Form.Group controlId="confirm_password" className='mb-3'>
136
  <Form.Label>Xác nhận mật khẩu</Form.Label>
137
  <Form.Control
138
  type="password"
@@ -141,11 +151,11 @@ const RegisterPage = () => {
141
 
142
  />
143
  </Form.Group>
144
- <div className='d-flex justify-content-between align-items-center'>
145
- <Button variant="primary" type="submit" className='me-2'>
146
  Đăng ký
147
  </Button>
148
- <a href="/login" className='me-2'>Đăng nhập</a>
149
 
150
  </div>
151
 
@@ -154,7 +164,7 @@ const RegisterPage = () => {
154
  </Card>
155
 
156
  </Col>
157
- <Col xs={1} md={2}></Col>
158
  </Row>
159
  </Container>
160
  )
 
75
 
76
  return (
77
  <BasicTemplate content={
78
+ (<Container fluid className="d-flex justify-content-center align-items-center mt-5"
79
+ style={{
80
+ maxWidth: "100%",
81
+ minHeight: "70vh",
82
+ backgroundImage: "linear-gradient(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.3)), url('/about-us.jpg')", // Thay đường dẫn ảnh nền
83
+ backgroundSize: 'cover',
84
+ backgroundPosition: 'center',
85
+ backgroundAttachment: 'fixed',
86
+ width: '100%',
87
+ padding: '50px 0',
88
+ }}>
89
+ <Row style={{maxWidth:"90vw"}}>
90
+ {/* <Col xs={1} md={2}></Col> */}
91
  <Col xs={10} md={8}>
92
+ <Card style={{ width: '35vw' }} className='d-flex justify-content-center card-nospan'>
93
  <Card.Header>
94
  <Card.Title className='mt-1 text-center'>Đăng ký</Card.Title>
95
  </Card.Header>
 
142
  />
143
  </Form.Group>
144
 
145
+ <Form.Group controlId="confirm_password" className='mb-5'>
146
  <Form.Label>Xác nhận mật khẩu</Form.Label>
147
  <Form.Control
148
  type="password"
 
151
 
152
  />
153
  </Form.Group>
154
+ <div className='d-flex justify-content-center align-items-center text-center'>
155
+ <Button variant="primary" type="submit" className='mx-4'>
156
  Đăng ký
157
  </Button>
158
+ <Button as='a' variant='outline-primary' href="/login" className='mx-4'>Đăng nhập</Button>
159
 
160
  </div>
161
 
 
164
  </Card>
165
 
166
  </Col>
167
+ {/* <Col xs={1} md={2}></Col> */}
168
  </Row>
169
  </Container>
170
  )
frontend/src/styles/styles.css CHANGED
@@ -7,29 +7,67 @@
7
  --background-color: #222020;
8
  /* Màu nền nhẹ nhàng */
9
  --text-color: #fbf5f5;
 
10
  /* Màu chữ chính */
11
  --container-background-color: rgba(44, 41, 41, 0.5);
12
- background-color: var(--background-color);
 
 
 
 
 
 
 
 
 
 
 
13
  }
14
 
15
  /* Đặt nền cho toàn bộ trang */
16
  body {
17
  color: var(--text-color);
18
- /* background-image: url('../../public/header.jpg'); */
19
- background-color: var(--background-color); /* Màu nền */
 
20
  /* Hoặc sử dụng hình ảnh nền */
21
- background-size: cover; /* Để hình ảnh phủ đầy toàn bộ trang */
22
- background-repeat: no-repeat; /* Để hình ảnh không lặp lại */
23
- background-attachment: fixed; /* Để nền cố định khi cuộn trang */
24
- background-position: center; /* Căn giữa hình ảnh */
25
- }
26
-
 
 
 
 
27
 
28
  /* Container */
29
  .container {
30
  padding-top: 20px;
31
  padding-bottom: 20px;
32
- background-color: var(--container-background-color);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  }
34
 
35
  /* Navbar */
@@ -41,10 +79,14 @@ body {
41
  .navbar-brand,
42
  .nav-link {
43
  color: #fff !important;
44
- font-weight: bold;
45
  transition: color 0.3s;
46
  }
47
 
 
 
 
 
48
  .nav-link:hover {
49
  color: var(--secondary-color) !important;
50
  }
@@ -64,6 +106,7 @@ body {
64
  color: #fff;
65
  font-weight: bold;
66
  transition: background-color 0.3s, color 0.3s;
 
67
  }
68
 
69
  .btn-primary:hover {
@@ -84,13 +127,14 @@ body {
84
 
85
  /* Style cho nút secondary */
86
  .btn-outline-primary {
87
- background-color: var(--text-color);
88
  /* Màu nền trắng */
89
- color: var(--primary-color);
90
  /* Màu chữ đỏ */
91
  font-weight: bold;
92
  border-color: var(--secondary-color);
93
  /* Màu viền đỏ */
 
94
  }
95
 
96
  .btn-outline-primary:hover {
@@ -102,10 +146,9 @@ body {
102
  /* Giữ màu viền đỏ khi hover */
103
  }
104
 
105
-
106
-
107
  /* Card */
108
  .card {
 
109
  border: none;
110
  border-radius: 10px;
111
  box-shadow: 0px 8px 16px rgba(0, 0, 0, 0.3);
@@ -127,25 +170,55 @@ body {
127
  .card-title {
128
  font-size: 1.25rem;
129
  font-weight: bold;
130
- color: var(--primary-color);
131
  }
132
 
133
  .card-text {
134
  color: var(--text-color);
135
  }
136
 
 
 
 
 
137
  .card-footer {
138
  background-color: #fff;
139
  border-top: none;
140
  text-align: center;
141
  }
142
 
 
 
 
 
143
  /* Container cho các phần */
144
  .menu-section {
145
  padding: 40px 0;
146
  background-color: #fff;
147
  }
148
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  .header-section {
150
  padding: 60px 0;
151
  background-image: url('../../public/header.jpg');
@@ -167,4 +240,8 @@ body {
167
  max-width: 600px;
168
  margin: auto;
169
  color: #f1f1f1;
 
 
 
 
170
  }
 
7
  --background-color: #222020;
8
  /* Màu nền nhẹ nhàng */
9
  --text-color: #fbf5f5;
10
+ --text-color-disabled: #706161;
11
  /* Màu chữ chính */
12
  --container-background-color: rgba(44, 41, 41, 0.5);
13
+ --fading-background-color: linear-gradient(to bottom,
14
+ rgba(0, 0, 0, 0) 0%,
15
+ /* Bắt đầu trong suốt */
16
+ rgba(44, 41, 41, 0.8) 30%,
17
+ /* Đậm dần */
18
+ rgba(44, 41, 41, 0.8) 70%,
19
+ /* Vùng màu đậm */
20
+ rgba(0, 0, 0, 0) 100%
21
+ /* Trở lại trong suốt */
22
+ );
23
+ --card-background-color: rgba(65, 52, 53, 0.8);
24
+ --modal-background-color: rgb(32, 29, 29);
25
  }
26
 
27
  /* Đặt nền cho toàn bộ trang */
28
  body {
29
  color: var(--text-color);
30
+ background-image: linear-gradient(rgba(20, 20, 20, 0.8), rgba(20, 20, 20, 0.95)), url('../../public/header.jpg');
31
+ background-color: var(--background-color);
32
+ /* Màu nền */
33
  /* Hoặc sử dụng hình ảnh nền */
34
+ background-size: 95% auto;
35
+ /* Để hình ảnh phủ đầy toàn bộ trang */
36
+ background-repeat: no-repeat;
37
+ /* Để hình ảnh không lặp lại */
38
+ background-attachment: fixed;
39
+ /* Để nền cố định khi cuộn trang */
40
+ background-position: center;
41
+ /* Căn giữa hình ảnh */
42
+ }
43
+
44
 
45
  /* Container */
46
  .container {
47
  padding-top: 20px;
48
  padding-bottom: 20px;
49
+ background-color: var(--fading-background-color);
50
+ }
51
+
52
+ .custom-tab .nav-link {
53
+ background-color: var(--primary-color);
54
+ /* Màu nền cho các nút tab không được chọn */
55
+ color: var(--text-color);
56
+ /* Màu chữ cho các nút tab không được chọn */
57
+ }
58
+
59
+ .custom-tab .nav-link.active {
60
+ background-color: var(--secondary-color);
61
+ /* Màu nền cho tab đang được chọn */
62
+ color: var(--text-color);
63
+ /* Màu chữ cho tab đang được chọn */
64
+ }
65
+
66
+ .custom-tab .nav-link:hover {
67
+ background-color: var(--card-background-color);
68
+ /* Màu nền cho tab khi hover */
69
+ color: var(--text-color);
70
+ /* Màu chữ khi hover */
71
  }
72
 
73
  /* Navbar */
 
79
  .navbar-brand,
80
  .nav-link {
81
  color: #fff !important;
82
+ font-weight: normal;
83
  transition: color 0.3s;
84
  }
85
 
86
+ .nav-link.disabled {
87
+ color: var(--text-color-disabled) !important;
88
+ }
89
+
90
  .nav-link:hover {
91
  color: var(--secondary-color) !important;
92
  }
 
106
  color: #fff;
107
  font-weight: bold;
108
  transition: background-color 0.3s, color 0.3s;
109
+ min-width: 7vw;
110
  }
111
 
112
  .btn-primary:hover {
 
127
 
128
  /* Style cho nút secondary */
129
  .btn-outline-primary {
130
+ background-color: rgba(from var(--background-color) r g b, 0.8);
131
  /* Màu nền trắng */
132
+ color: var(--text-color);
133
  /* Màu chữ đỏ */
134
  font-weight: bold;
135
  border-color: var(--secondary-color);
136
  /* Màu viền đỏ */
137
+ min-width: 7vw;
138
  }
139
 
140
  .btn-outline-primary:hover {
 
146
  /* Giữ màu viền đỏ khi hover */
147
  }
148
 
 
 
149
  /* Card */
150
  .card {
151
+ background-color: var(--card-background-color);
152
  border: none;
153
  border-radius: 10px;
154
  box-shadow: 0px 8px 16px rgba(0, 0, 0, 0.3);
 
170
  .card-title {
171
  font-size: 1.25rem;
172
  font-weight: bold;
173
+ color: var(--text-color);
174
  }
175
 
176
  .card-text {
177
  color: var(--text-color);
178
  }
179
 
180
+ .card-body {
181
+ min-height: 150px;
182
+ }
183
+
184
  .card-footer {
185
  background-color: #fff;
186
  border-top: none;
187
  text-align: center;
188
  }
189
 
190
+ .card-nospan:hover {
191
+ transform: scale(1.00);
192
+ }
193
+
194
  /* Container cho các phần */
195
  .menu-section {
196
  padding: 40px 0;
197
  background-color: #fff;
198
  }
199
 
200
+ .form-label {
201
+ color: var(--text-color);
202
+ }
203
+
204
+ a {
205
+ color: var(--text-color);
206
+ text-decoration: none;
207
+ }
208
+
209
+ a:hover {
210
+ color: orange;
211
+ /* Màu sắc khi di chuột qua */
212
+ }
213
+
214
+ /* Khi liên kết được nhấn */
215
+ a:active {
216
+ color: red;
217
+ /* Màu sắc khi nhấn */
218
+ }
219
+
220
+ /* Khi liên kết đã được truy cập */
221
+
222
  .header-section {
223
  padding: 60px 0;
224
  background-image: url('../../public/header.jpg');
 
240
  max-width: 600px;
241
  margin: auto;
242
  color: #f1f1f1;
243
+ }
244
+
245
+ .modal-content {
246
+ background-color: var(--modal-background-color);
247
  }
frontend/test.json ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id": "xoixomai",
4
+ "item_name": "Xôi xoài",
5
+ "image_url": "https://cdn.mediamart.vn/images/news/mo-lam-xoi-xoai-thai-lan-thom-beo-hp-dn-chun-huong-v_2107a3bc.jpg",
6
+ "item_type": 3,
7
+ "description": "Xôi xoài thái thơm ngon với nước cốt dừa",
8
+ "price": 35000,
9
+ "create_at": "2024-11-02T13:42:02.009Z"
10
+ },
11
+ {
12
+ "id": "trada",
13
+ "item_name": "Trà đá",
14
+ "image_url": "https://media-cdn-v2.laodong.vn/Storage/NewsPortal/2019/7/25/746291/Tra-Da.jpg",
15
+ "item_type": 2,
16
+ "description": "Trà đá giải khát, thích hợp cho mọi món ăn",
17
+ "price": 5000,
18
+ "create_at": "2024-11-02T13:42:02.009Z"
19
+ },
20
+ {
21
+ "id": "trachanh",
22
+ "item_name": "Trà chanh",
23
+ "image_url": "https://www.bartender.edu.vn/wp-content/uploads/2020/07/cach-pha-tra-chanh-khong-bi-dang.jpg",
24
+ "item_type": 2,
25
+ "description": "Trà chanh tươi mát với chút vị chua dịu",
26
+ "price": 10000,
27
+ "create_at": "2024-11-02T13:42:02.009Z"
28
+ },
29
+ {
30
+ "id": "suongsaohate",
31
+ "item_name": "Sương sáo hạt é",
32
+ "image_url": "https://dacsannanggio.vn/image/catalog/Hat-e/cach-lam-suong-sao-hat-e.jpg",
33
+ "item_type": 3,
34
+ "description": "Món tráng miệng thanh mát với sương sáo và hạt é",
35
+ "price": 15000,
36
+ "create_at": "2024-11-02T13:42:02.009Z"
37
+ },
38
+ {
39
+ "id": "sinhtoxoai",
40
+ "item_name": "Sinh tố xoài",
41
+ "image_url": "https://beptruong.edu.vn/wp-content/uploads/2016/02/sinh-to-xoai-sua-tuoi.jpg",
42
+ "item_type": 2,
43
+ "description": "Sinh tố xoài chua ngọt tự nhiên",
44
+ "price": 30000,
45
+ "create_at": "2024-11-02T13:42:02.009Z"
46
+ },
47
+ {
48
+ "id": "sinhtobo",
49
+ "item_name": "Sinh tố bơ",
50
+ "image_url": "https://caygiongbo.com/datafiles/3/2019-02-24/99271623-sinh-to-bo-de-quoc-bao-lau-trong-tu-lanh-1.jpg",
51
+ "item_type": 2,
52
+ "description": "Sinh tố bơ béo ngậy và thơm ngon",
53
+ "price": 30000,
54
+ "create_at": "2024-11-02T13:42:02.009Z"
55
+ },
56
+ {
57
+ "id": "pho",
58
+ "item_name": "Phở bò",
59
+ "image_url": "https://cdn.tgdd.vn/Files/2017/03/18/962092/an-lien-3-bat-pho-voi-cong-thuc-nau-pho-nay-202201261419401397.jpg",
60
+ "item_type": 1,
61
+ "description": "Món phở truyền thống với nước dùng thanh ngọt",
62
+ "price": 35000,
63
+ "create_at": "2024-11-02T13:42:02.009Z"
64
+ },
65
+ {
66
+ "id": "pepsi",
67
+ "item_name": "Pepsi",
68
+ "image_url": "https://t4.ftcdn.net/jpg/02/84/65/61/360_F_284656175_G6SlGTBVl4pg8oXh6jr86cOmKUZjfrym.jpg",
69
+ "item_type": 2,
70
+ "description": "Nước ngọt Pepsi",
71
+ "price": 20000,
72
+ "create_at": "2024-11-02T13:42:02.009Z"
73
+ },
74
+ {
75
+ "id": "nuocsam",
76
+ "item_name": "Nước sâm",
77
+ "image_url": "https://assets.gia-hanoi.com/nau-nuoc-sam-bi-dao.jpg",
78
+ "item_type": 2,
79
+ "description": "Nước sâm mát lạnh, tốt cho sức khỏe",
80
+ "price": 15000,
81
+ "create_at": "2024-11-02T13:42:02.009Z"
82
+ },
83
+ {
84
+ "id": "nuocepduoi",
85
+ "item_name": "Nước ép ổi",
86
+ "image_url": "https://douongnhapkhau.com/wp-content/uploads/2024/08/nuoc-ep-oi-co-tac-dung-gi-cach-lam-nuoc-ep-oi-2.jpg",
87
+ "item_type": 2,
88
+ "description": "Nước ép ổi thơm ngon, giàu vitamin C",
89
+ "price": 25000,
90
+ "create_at": "2024-11-02T13:42:02.009Z"
91
+ },
92
+ {
93
+ "id": "nuocdua",
94
+ "item_name": "Nước dừa",
95
+ "image_url": "https://storage-vnportal.vnpt.vn/ndh-ubnd/5893/1223/uong-nuoc-dua.jpg",
96
+ "item_type": 2,
97
+ "description": "Nước dừa tươi, mát và bổ dưỡng",
98
+ "price": 25000,
99
+ "create_at": "2024-11-02T13:42:02.009Z"
100
+ },
101
+ {
102
+ "id": "nuoccam",
103
+ "item_name": "Nước cam",
104
+ "image_url": "https://baodongnai.com.vn/file/e7837c02876411cd0187645a2551379f/022024/174_mh_20240228114325.jpg",
105
+ "item_type": 2,
106
+ "description": "Nước cam tươi, cung cấp vitamin C",
107
+ "price": 25000,
108
+ "create_at": "2024-11-02T13:42:02.009Z"
109
+ },
110
+ {
111
+ "id": "miquang",
112
+ "item_name": "Mì Quảng",
113
+ "image_url": "https://helenrecipes.com/wp-content/uploads/2021/05/Screenshot-2021-05-31-142423-1200x675.png",
114
+ "item_type": 1,
115
+ "description": "Mì Quảng đặc sản với nước dùng đậm đà",
116
+ "price": 28000,
117
+ "create_at": "2024-11-02T13:42:02.009Z"
118
+ },
119
+ {
120
+ "id": "kemdua",
121
+ "item_name": "Kem dừa",
122
+ "image_url": "https://cdn.tgdd.vn/Files/2020/03/25/1244397/cach-lam-kem-dua-thom-ngon-tai-nha-bang-may-xay-sinh-to-202003250909366047.jpg",
123
+ "item_type": 3,
124
+ "description": "Kem dừa mát lạnh với hương dừa tự nhiên",
125
+ "price": 30000,
126
+ "create_at": "2024-11-02T13:42:02.009Z"
127
+ },
128
+ {
129
+ "id": "goicuon",
130
+ "item_name": "Gỏi cuốn",
131
+ "image_url": "https://khaihoanphuquoc.com.vn/wp-content/uploads/2023/11/nu%CC%9Bo%CC%9B%CC%81c-ma%CC%86%CC%81m-cha%CC%82%CC%81m-go%CC%89i-cuo%CC%82%CC%81n-1200x923.png",
132
+ "item_type": 1,
133
+ "description": "Cuốn tươi ngon với rau, tôm, thịt và bún",
134
+ "price": 15000,
135
+ "create_at": "2024-11-02T13:42:02.009Z"
136
+ },
137
+ {
138
+ "id": "comtam",
139
+ "item_name": "Cơm tấm",
140
+ "image_url": "https://static.vinwonders.com/production/com-tam-da-nang-1.jpg",
141
+ "item_type": 1,
142
+ "description": "Cơm tấm sườn, bì, chả truyền thống",
143
+ "price": 35000,
144
+ "create_at": "2024-11-02T13:42:02.009Z"
145
+ },
146
+ {
147
+ "id": "comchien",
148
+ "item_name": "Cơm chiên dương châu",
149
+ "image_url": "https://nineshield.com.vn/wp-content/uploads/2024/03/com-chien-duong-chau-ngon.jpg",
150
+ "item_type": 1,
151
+ "description": "Cơm chiên với trứng, tôm, thịt nguội và rau củ",
152
+ "price": 35000,
153
+ "create_at": "2024-11-02T13:42:02.009Z"
154
+ },
155
+ {
156
+ "id": "cocacola",
157
+ "item_name": "Coca-Cola",
158
+ "image_url": "https://t4.ftcdn.net/jpg/02/84/65/61/360_F_284656117_sPF8gVWaX627bq5qKrlrvCz1eFfowdBf.jpg",
159
+ "item_type": 2,
160
+ "description": "Nước ngọt Coca-Cola",
161
+ "price": 20000,
162
+ "create_at": "2024-11-02T13:42:02.009Z"
163
+ },
164
+ {
165
+ "id": "chethotnot",
166
+ "item_name": "Chè thốt nốt",
167
+ "image_url": "https://i.vietgiaitri.com/2022/6/19/cach-nau-che-thot-not-thanh-mat-don-gian-tai-nha-5c0-6500541.jpg",
168
+ "item_type": 3,
169
+ "description": "Chè thốt nốt tươi mát và bổ dưỡng",
170
+ "price": 18000,
171
+ "create_at": "2024-11-02T13:42:02.009Z"
172
+ },
173
+ {
174
+ "id": "chekhucbach",
175
+ "item_name": "Chè khúc bạch",
176
+ "image_url": "https://cdn.tgdd.vn/2021/10/CookDishThumb/che-khuc-bach-la-gi-che-khuc-bach-lam-tu-gi-nguyen-lieu-lam-thumb-620x620.jpg",
177
+ "item_type": 3,
178
+ "description": "Chè khúc bạch thơm béo với hạnh nhân và trái cây",
179
+ "price": 30000,
180
+ "create_at": "2024-11-02T13:42:02.009Z"
181
+ }
182
+ ]