Spaces:
Sleeping
Sleeping
Update: add api menu&feed
Browse files- frontend/public/desserts.jpg +0 -0
- frontend/public/dishes.jpg +0 -0
- frontend/public/drinks.jpg +0 -0
- frontend/src/index.js +13 -1
- frontend/src/molecules/AdminNavBar.js +14 -8
- frontend/src/molecules/Navbar.js +16 -12
- frontend/src/organisms/CacheStorage.js +91 -0
- frontend/src/organisms/MenuSection.js +144 -51
- frontend/src/organisms/NewsSection.js +51 -19
- frontend/src/organisms/StoreSection.js +58 -23
- frontend/src/pages/AdminFeedPage.js +2 -1
- frontend/src/pages/AdminLoginPage.js +120 -0
- frontend/src/pages/AdminMenuPage.js +2 -2
- frontend/src/pages/AdminOrderPage.js +1 -1
- frontend/src/pages/AdminSchedulePage.js +1 -1
- frontend/src/pages/AdminStaffPage.js +1 -1
- frontend/src/pages/AdminSummaryPage.js +13 -1
- frontend/src/pages/AdminUserInfoPage.js +255 -0
- frontend/src/pages/CartPage.js +26 -20
- frontend/src/pages/LoginPage.js +20 -10
- frontend/src/pages/MenuPage.js +112 -109
- frontend/src/pages/NewsPage.js +14 -4
- frontend/src/pages/RegisterPage.js +19 -9
- frontend/src/styles/styles.css +93 -16
- frontend/test.json +182 -0
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="/
|
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">
|
55 |
-
<Nav.Link href="/admin-menu">
|
56 |
-
<Nav.Link href="/admin-staff">
|
57 |
-
<Nav.Link href="/admin-schedule">Lịch làm việc</Nav.Link>
|
58 |
-
<Nav.Link href="/admin-orders"
|
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={{
|
53 |
-
<Container fluid style={{maxWidth:"90%"}}>
|
54 |
<Navbar.Brand href="/">
|
55 |
-
<
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
|
|
|
|
|
|
|
|
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 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
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
|
7 |
-
|
8 |
-
|
9 |
-
|
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 |
-
|
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 |
-
|
8 |
-
|
9 |
-
|
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={
|
17 |
-
|
18 |
</Col>
|
19 |
|
20 |
-
|
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 |
+
Là 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={{
|
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
|
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={{
|
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={{
|
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={{
|
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={{
|
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={{
|
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 |
-
.
|
17 |
-
.
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
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={{
|
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="
|
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}
|
46 |
-
<
|
47 |
-
<Card.
|
48 |
-
|
49 |
-
<
|
50 |
-
|
51 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
52 |
</Row>
|
53 |
</Card.Body>
|
54 |
</Card>
|
55 |
</Col>
|
56 |
))}
|
57 |
</Row>
|
58 |
-
<Button as='a' href='/payment' className='
|
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 |
-
|
69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
70 |
<Col xs={10} md={8}>
|
71 |
-
<Card style={{ width: '
|
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-
|
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-
|
97 |
-
<a href="/register" className='
|
98 |
-
<Button type="submit" className='
|
99 |
-
<a href="/forgot-password" className='
|
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(
|
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.
|
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 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
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 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
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?.
|
103 |
</Modal.Header>
|
104 |
<Modal.Body>
|
105 |
-
<img src={selectedDish?.
|
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 |
-
<
|
113 |
-
<InputGroup
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
|
|
|
|
|
|
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 |
-
|
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(
|
5 |
|
6 |
-
|
7 |
-
|
8 |
-
|
|
|
|
|
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 |
-
|
80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
81 |
<Col xs={10} md={8}>
|
82 |
-
<Card style={{ width: '
|
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-
|
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-
|
145 |
-
<Button variant="primary" type="submit" className='
|
146 |
Đăng ký
|
147 |
</Button>
|
148 |
-
<a href="/login" className='
|
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:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
}
|
14 |
|
15 |
/* Đặt nền cho toàn bộ trang */
|
16 |
body {
|
17 |
color: var(--text-color);
|
18 |
-
|
19 |
-
background-color: var(--background-color);
|
|
|
20 |
/* Hoặc sử dụng hình ảnh nền */
|
21 |
-
background-size:
|
22 |
-
|
23 |
-
background-
|
24 |
-
|
25 |
-
|
26 |
-
|
|
|
|
|
|
|
|
|
27 |
|
28 |
/* Container */
|
29 |
.container {
|
30 |
padding-top: 20px;
|
31 |
padding-bottom: 20px;
|
32 |
-
background-color: var(--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
}
|
34 |
|
35 |
/* Navbar */
|
@@ -41,10 +79,14 @@ body {
|
|
41 |
.navbar-brand,
|
42 |
.nav-link {
|
43 |
color: #fff !important;
|
44 |
-
font-weight:
|
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(--
|
88 |
/* Màu nền trắng */
|
89 |
-
color: var(--
|
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(--
|
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 |
+
]
|