From ece024f9d104f877864076750d3070082527fdcd Mon Sep 17 00:00:00 2001 From: hahuynhvan Date: Thu, 10 Oct 2024 10:45:12 +0700 Subject: [PATCH] config mail --- client/package.json | 1 + client/src/App.jsx | 19 +- client/src/assets/css/App.css | 119 +++++++++ client/src/assets/css/Variables.css | 36 +++ client/src/assets/scss/About.scss | 102 -------- client/src/assets/scss/App.scss | 90 ------- client/src/assets/scss/AspirationCard.scss | 13 - client/src/assets/scss/Card.scss | 80 ------ client/src/assets/scss/Library.scss | 8 - client/src/assets/scss/Navbar.scss | 40 --- client/src/assets/scss/Sidebar.scss | 5 - client/src/assets/scss/Variables.scss | 34 --- client/src/components/Cart/CartItem.jsx | 4 +- .../Cart/{OrderSummary.jsx => CartTotal.jsx} | 5 +- .../src/components/Home/AspirationSegment.jsx | 91 ++++++- client/src/components/MyNavbar.jsx | 1 - .../components/Order/CustomerContactInfo.jsx | 50 +++- .../components/Order/CustomerInstruction.jsx | 41 +++ .../src/components/Order/OrderItemsList.jsx | 89 ++++--- client/src/components/Order/OrderList.jsx | 54 ++-- client/src/components/Order/OrderSummary.jsx | 59 +++++ .../src/components/Order/ShippingAddress.jsx | 49 +++- client/src/components/index.js | 8 +- client/src/features/orders/orderThunk.js | 4 +- client/src/features/users/userThunk.js | 3 - client/src/main.jsx | 2 +- client/src/pages/main/About.jsx | 168 ++++++++++--- client/src/pages/main/Cart.jsx | 4 +- client/src/pages/main/Checkout.jsx | 18 +- client/src/pages/main/Library.jsx | 20 +- client/src/pages/main/Order.jsx | 2 + client/src/pages/main/SingleOrder.jsx | 237 ++++++++++++++++-- client/src/pages/main/UserInfo.jsx | 10 +- client/src/utils/axios.js | 4 +- controllers/auth.controller.js | 43 ++-- controllers/order.controller.js | 64 +++-- controllers/user.controller.js | 2 +- dbconfig.js | 5 +- middleware/authentication.js | 9 +- routes/order.router.js | 2 +- utils/jwt.js | 4 +- utils/nodemailerConfig.js | 7 +- utils/sendEmail.js | 12 +- utils/sendResetPasswordEmail.js | 6 +- utils/sendVerificationEmail.js | 26 +- 45 files changed, 1092 insertions(+), 558 deletions(-) create mode 100644 client/src/assets/css/App.css create mode 100644 client/src/assets/css/Variables.css delete mode 100644 client/src/assets/scss/About.scss delete mode 100644 client/src/assets/scss/App.scss delete mode 100644 client/src/assets/scss/AspirationCard.scss delete mode 100644 client/src/assets/scss/Card.scss delete mode 100644 client/src/assets/scss/Library.scss delete mode 100644 client/src/assets/scss/Navbar.scss delete mode 100644 client/src/assets/scss/Sidebar.scss delete mode 100644 client/src/assets/scss/Variables.scss rename client/src/components/Cart/{OrderSummary.jsx => CartTotal.jsx} (95%) create mode 100644 client/src/components/Order/CustomerInstruction.jsx create mode 100644 client/src/components/Order/OrderSummary.jsx diff --git a/client/package.json b/client/package.json index c88426b..6f8bd29 100644 --- a/client/package.json +++ b/client/package.json @@ -21,6 +21,7 @@ "axios": "^1.7.2", "bootstrap": "^5.3.3", "bootstrap-icons": "^1.11.3", + "dateformat": "^5.0.3", "dayjs": "^1.11.13", "dotenv": "^16.4.5", "mdb-react-ui-kit": "^9.0.0", diff --git a/client/src/App.jsx b/client/src/App.jsx index 2dee735..6d38bc8 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -12,7 +12,7 @@ import { Order, UserInfo, Manager, - SingleOrder + SingleOrder, } from './pages/main' import VerifyEmail from './pages/VerifyEmail' import { ProtectedRoute, Error } from './pages' @@ -24,8 +24,9 @@ import { loader as libraryLoader } from './pages/main/Library' import { loader as verifyEmailLoader } from './pages/VerifyEmail' import { loader as singleBookLoader } from './pages/main/SingleBook' import { loader as singleAuthorLoader } from './pages/main/SingleAuthor' -import { loader as newBookLoader} from './pages/main/Home' +import { loader as newBookLoader } from './pages/main/Home' import { loader as singleUserOrder } from './pages/main/SingleOrder' +import store from './store' const queryClient = new QueryClient({ defaultOptions: { @@ -84,7 +85,7 @@ const router = createBrowserRouter([ ), - loader: singleUserOrder(queryClient), + loader: singleUserOrder(store, queryClient), }, { path: 'author/:id', @@ -93,11 +94,19 @@ const router = createBrowserRouter([ }, { path: 'user', - element: , + element: ( + + + + ), }, { path: 'manager', - element: , + element: ( + + + + ), }, ], }, diff --git a/client/src/assets/css/App.css b/client/src/assets/css/App.css new file mode 100644 index 0000000..33de575 --- /dev/null +++ b/client/src/assets/css/App.css @@ -0,0 +1,119 @@ +/* để import css trong bootstrap */ +@import 'bootstrap/dist/css/bootstrap.min.css'; +@import url('https://fonts.googleapis.com/css2?family=Cabin&family=Roboto+Condensed:wght@400;700&display=swap'); +:root { + /* color variables */ + --text-color: #822b2b; /* darken(#973131, 10%) */ + --bold-text-color: #6d2323; /* darken(#973131, 20%) */ + + /* colors */ + --primary-bg-color: #973131; + --primary-bg-color-hover: #822b2b; /* darken(#973131, 10%) */ + + --secondary-bg-color: #e0a75e; + --secondary-bg-color-hover: #c88f51; /* darken(#e0a75e, 10%) */ + + --tertiary-bg-color: #f9d689; + --tertiary-bg-color-hover: #e6c279; /* darken(#f9d689, 10%) */ + + --quaternary-bg-color: #f5e7b2; + --quaternary-bg-color-hover: #e2d29f; /* darken(#f5e7b2, 10%) */ + --quaternary-bg-color-light: #fff5cc; /* lighten(#f5e7b2, 10%) */ + + /* fonts */ + --heading-font: 'Roboto Condensed', Sans-Serif; + --body-font: 'Cabin', Sans-Serif; + --small-text: 0.875rem; + --extra-small-text: 0.7em; + --textColor: #102a43; + + /* box shadows */ + --shadow-1: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); + --shadow-2: 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --shadow-3: 0 10px 15px -3px rgba(0, 0, 0, 0.1), + 0 4px 6px -2px rgba(0, 0, 0, 0.05); + --shadow-4: 0 20px 25px -5px rgba(0, 0, 0, 0.1), + 0 10px 10px -5px rgba(0, 0, 0, 0.04); +} + +body { + margin-top: 4rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.75; + background-color: var(--quaternary-bg-color); + color: var(--text-color); + position: relative; +} + +p { + margin-bottom: 1.5rem; + max-width: 40em; + color: var(--text-color); +} + +h1, h2, h3, h4, h5 { + color: var(--text-color); +} + +.divider { + color: var(--primary-bg-color); + width: 100%; + font-size: 3rem; + text-align: center; + text-shadow: 2px 2px 3px rgb(180, 176, 176); + height: 60px; + line-height: 2rem; + top: 28.75rem; +} + +.segment { + padding-top: 30px; + padding-bottom: 80px; +} + +.segmentX { + padding: 80px 0; + background-color: var(--quaternary-bg-color-light); +} + +.segmentX .segment-heading { + margin-bottom: 3.5rem; + text-transform: uppercase; + letter-spacing: 0.25rem; + font-weight: bold; + color: var(--primary-bg-color); +} + +.segmentX .segment-heading .badge { + background-color: var(--primary-bg-color) !important; + color: var(--quaternary-bg-color-light); +} + +.segment-heading { + color: var(--primary-bg-color); + font-family: var(--bodyFont); + font-weight: 500; + text-shadow: 2px 2px 3px rgb(180, 176, 176); + font-size: 2rem; + letter-spacing: 1px; +} + +.breadcrumb { + margin: 5rem 5rem 2rem; +} + +.breadcrumb .breadcrumb-item { + font-size: 2rem; + font-weight: bold; +} + +.breadcrumb .active { + color: var(--bold-text-color); +} + +.breadcrumb a { + text-decoration: none; + color: var(--text-color); +} diff --git a/client/src/assets/css/Variables.css b/client/src/assets/css/Variables.css new file mode 100644 index 0000000..e414f78 --- /dev/null +++ b/client/src/assets/css/Variables.css @@ -0,0 +1,36 @@ +@import url('https://fonts.googleapis.com/css2?family=Cabin&family=Roboto+Condensed:wght@400;700&display=swap'); +:root { + /* color variables */ + --text-color: #822b2b; /* darken(#973131, 10%) */ + --bold-text-color: #6d2323; /* darken(#973131, 20%) */ + + /* colors */ + --primary-bg-color: #973131; + --primary-bg-color-hover: #822b2b; /* darken(#973131, 10%) */ + + --secondary-bg-color: #e0a75e; + --secondary-bg-color-hover: #c88f51; /* darken(#e0a75e, 10%) */ + + --tertiary-bg-color: #f9d689; + --tertiary-bg-color-hover: #e6c279; /* darken(#f9d689, 10%) */ + + --quaternary-bg-color: #f5e7b2; + --quaternary-bg-color-hover: #e2d29f; /* darken(#f5e7b2, 10%) */ + --quaternary-bg-color-light: #fff5cc; /* lighten(#f5e7b2, 10%) */ + + /* fonts */ + --heading-font: 'Roboto Condensed', Sans-Serif; + --body-font: 'Cabin', Sans-Serif; + --small-text: 0.875rem; + --extra-small-text: 0.7em; + --textColor: #102a43; + + /* box shadows */ + --shadow-1: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); + --shadow-2: 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --shadow-3: 0 10px 15px -3px rgba(0, 0, 0, 0.1), + 0 4px 6px -2px rgba(0, 0, 0, 0.05); + --shadow-4: 0 20px 25px -5px rgba(0, 0, 0, 0.1), + 0 10px 10px -5px rgba(0, 0, 0, 0.04); +} diff --git a/client/src/assets/scss/About.scss b/client/src/assets/scss/About.scss deleted file mode 100644 index ed47b8c..0000000 --- a/client/src/assets/scss/About.scss +++ /dev/null @@ -1,102 +0,0 @@ -@import '../scss/Variables.scss'; - -.section { - padding-top: 2rem; - padding-bottom: 3rem; - width: 100%; - .about-heading { - font-size: 2.5rem; - margin-bottom: 3.5rem; - text-align: center; - text-transform: uppercase; - letter-spacing: 0.25rem; - font-weight: bold; - color: $primary-bg-color; - .about-badge { - background-color: $primary-bg-color!important; - span { - color: $quaternary-bg-color; - } - } - } -} - -.general-container { - display: flex; - width: 95vw; - margin: auto; - justify-content: space-between; - max-width: 1170px; - flex-wrap: wrap; -} - -.about-section { - .about-container { - .about-img { - position: relative; - flex: 0 0 calc(50% - 2rem); - box-shadow: $shadow-3; - - img { - overflow: clip; - width: 100%; - object-fit: cover; - } - - @media screen and (min-width: 1170px) { - &::before { - content: ''; - position: absolute; - width: 100%; - height: 100%; - z-index: -1; - bottom: 1.5rem; - right: 1.5rem; - border: 0.5rem solid $primary-bg-color; - box-sizing: border-box; - } - } - } - - .describe-container { - display: flex; - flex-direction: column; - justify-content: center; - flex: 0 0 calc(50% - 2rem); - - h3 { - font-size: 1.75rem; - text-transform: capitalize; - letter-spacing: 0.25rem; - margin-bottom: 0.75rem; - } - - p { - letter-spacing: 0; - margin-bottom: 1.25rem; - text-align: left; - } - } - - @media (max-width: 992px) { - flex-direction: column; - width: 90vw; - } - } -} - -.general-btn { - font-size: 0.875rem; - padding: 0.375rem 0.75rem; - border: 2px solid $primary-bg-color; - background-color: $primary-bg-color; - line-height: 21px; - color: $quaternary-bg-color; - - &:hover { - cursor: pointer; - background-color: $primary-bg-color-hover; - color: #fff; - border-color: $primary-bg-color-hover; - } -} diff --git a/client/src/assets/scss/App.scss b/client/src/assets/scss/App.scss deleted file mode 100644 index 857102d..0000000 --- a/client/src/assets/scss/App.scss +++ /dev/null @@ -1,90 +0,0 @@ -@import '~bootstrap/scss/bootstrap'; -@import url('https://fonts.googleapis.com/css2?family=Cabin&family=Roboto+Condensed:wght@400;700&display=swap'); -@import '../scss/Variables.scss'; - -body { - margin-top: 4rem; - font-size: 1rem; - font-weight: 400; - line-height: 1.75; - background-color: $quaternary-bg-color; - color: $text-color; - position: relative; -} - -p { - margin-bottom: 1.5rem; - max-width: 40em; - color: $text-color; -} -h1 { - color: $text-color; -} -h2 { - color: $text-color; -} -h3 { - color: $text-color; -} -h4 { - color: $text-color; -} -h5 { - color: $text-color; -} - -.divider { - color: $primary-bg-color; - width: 100%; - font-size: 3rem; - text-align: center; - text-shadow: 2px 2px 3px rgb(180, 176, 176); - height: 60px; - line-height: 2rem; - top: 28.75rem; -} - -.segment { - padding-top: 30px; - padding-bottom: 80px; -} - -.segmentX { - padding: 80px 0; - background-color: $quaternary-bg-color-light; - .segment-heading { - margin-bottom: 3.5rem; - text-transform: uppercase; - letter-spacing: 0.25rem; - font-weight: bold; - color: $primary-bg-color; - .badge { - background-color: $primary-bg-color !important; - color: $quaternary-bg-color-light; - } - } -} - -.segment-heading { - color: $primary-bg-color; - font-family: $bodyFont; - font-weight: 500; - text-shadow: 2px 2px 3px rgb(180, 176, 176); - font-size: 2rem; - letter-spacing: 1px; -} -.breadcrumb { - margin: 5rem 5rem 2rem; - .breadcrumb-item { - font-size: 2rem; - font-weight: bold; - } - .active { - color: $bold-text-color - } - a { - text-decoration: none; - color: $text-color; - } - -} diff --git a/client/src/assets/scss/AspirationCard.scss b/client/src/assets/scss/AspirationCard.scss deleted file mode 100644 index 6680c00..0000000 --- a/client/src/assets/scss/AspirationCard.scss +++ /dev/null @@ -1,13 +0,0 @@ -@import '../scss/Variables.scss'; - -.a-card { - &:hover { - box-shadow: $shadow-4; - cursor: pointer; - } -} - -.a-card-body { - background: $tertiary-bg-color; - border-radius: 0 0 5px 5px; -} \ No newline at end of file diff --git a/client/src/assets/scss/Card.scss b/client/src/assets/scss/Card.scss deleted file mode 100644 index 01a6ce3..0000000 --- a/client/src/assets/scss/Card.scss +++ /dev/null @@ -1,80 +0,0 @@ -@import '../scss/Variables.scss'; - -.b-card { - &:hover { - box-shadow: $shadow-4; - cursor: pointer; - } -} - -.b-card-body { - background: $tertiary-bg-color; - border-radius: 0 0 5px 5px; -} - -.a-card { - height: 283px!important; - padding: 40px 32px; - gap: 1rem; - justify-content: center; - align-items: center; - background: $quaternary-bg-color !important; - &:hover { - box-shadow: $shadow-4; - cursor: pointer; - } - .card-icon { - line-height: 5rem; - text-align: center; - background-color: $quaternary-bg-color-light; - font-size: 3rem; - height: 100px; - width: 100px; - } -} - -.a-card-body { - border-radius: 0 0 5px 5px; - .card-title { - font-size: 2rem; - text-shadow: $shadow-4; - margin-bottom: 1rem; - } -} - -.card-container { - display: flex; - flex-direction: row; - gap: 5rem; - height: 18rem; -} - -.flip-card { - width: 300px; - height: 200px; - perspective: 1000px; - - &-inner { - position: relative; - width: 100%; - height: 100%; - transition: transform 0.8s; - transform-style: preserve-3d; - - &:hover { - transform: rotateY(180deg); - } - } - - &-front, &-back { - position: absolute; - width: 100%; - height: 100%; - -webkit-backface-visibility: hidden; - backface-visibility: hidden; - } - &-back { - transform: rotateY(180deg); - } -} - diff --git a/client/src/assets/scss/Library.scss b/client/src/assets/scss/Library.scss deleted file mode 100644 index d6a88d5..0000000 --- a/client/src/assets/scss/Library.scss +++ /dev/null @@ -1,8 +0,0 @@ -@import '../scss/Variables.scss'; - -.library { - .library-container { - background-color: $quaternary-bg-color-light; - padding: 0 5rem; - } -} diff --git a/client/src/assets/scss/Navbar.scss b/client/src/assets/scss/Navbar.scss deleted file mode 100644 index c1a6293..0000000 --- a/client/src/assets/scss/Navbar.scss +++ /dev/null @@ -1,40 +0,0 @@ -@import '../scss/Variables.scss'; - -.navbar-custom { - font-weight: 600; - background-color: $primary-bg-color; /* Màu nền tùy chỉnh */ - box-shadow: $shadow-4; - top: 0; - width: 100%; - z-index: 10; - .nav-link { - color: $secondary-bg-color; - margin: 0 5px; - border-radius: 10px; - &:hover { - background-color: $primary-bg-color-hover; - color: $tertiary-bg-color; - } - &:focus, - &:active { - background-color: $primary-bg-color-hover; - color: $tertiary-bg-color; - } - } - .nav-link.active { - color: $secondary-bg-color; - } - .nav-link.show { - color: $secondary-bg-color; - } - .dropdown-menu { - background-color: $quaternary-bg-color; - - .dropdown-item { - background-color: $quaternary-bg-color; - &:hover { - background-color: $quaternary-bg-color-hover; - } - } - } -} diff --git a/client/src/assets/scss/Sidebar.scss b/client/src/assets/scss/Sidebar.scss deleted file mode 100644 index 1397cea..0000000 --- a/client/src/assets/scss/Sidebar.scss +++ /dev/null @@ -1,5 +0,0 @@ -@import '../scss/Variables.scss'; - -.sidebar { - width: 12.5rem; -} \ No newline at end of file diff --git a/client/src/assets/scss/Variables.scss b/client/src/assets/scss/Variables.scss deleted file mode 100644 index c0777b0..0000000 --- a/client/src/assets/scss/Variables.scss +++ /dev/null @@ -1,34 +0,0 @@ -@import url('https://fonts.googleapis.com/css2?family=Cabin&family=Roboto+Condensed:wght@400;700&display=swap'); - -$text-color: darken(#973131, 10%); -$bold-text-color: darken(#973131, 20%); - -// color -$primary-bg-color: #973131; -$primary-bg-color-hover: darken(#973131, 10%); - - -$secondary-bg-color: #e0a75e; -$secondary-bg-color-hover: darken(#e0a75e, 10%); - -$tertiary-bg-color: #f9d689; -$tertiary-bg-color-hover: darken(#f9d689, 10%); - -$quaternary-bg-color: #f5e7b2; -$quaternary-bg-color-hover: darken(#f5e7b2, 10%); -$quaternary-bg-color-light: lighten(#f5e7b2, 10%); - -// letter -$headingFont: 'Roboto Condensed', Sans-Serif; -$bodyFont: 'Cabin', Sans-Serif; -$small-text: 0.875rem; -$extra-small-text: 0.7em; -$textColor: #102a43; - -/* box shadow*/ -$shadow-1: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); -$shadow-2: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); -$shadow-3: 0 10px 15px -3px rgba(0, 0, 0, 0.1), - 0 4px 6px -2px rgba(0, 0, 0, 0.05); -$shadow-4: 0 20px 25px -5px rgba(0, 0, 0, 0.1), - 0 10px 10px -5px rgba(0, 0, 0, 0.04); diff --git a/client/src/components/Cart/CartItem.jsx b/client/src/components/Cart/CartItem.jsx index e5268aa..b3e5cbb 100644 --- a/client/src/components/Cart/CartItem.jsx +++ b/client/src/components/Cart/CartItem.jsx @@ -69,9 +69,7 @@ const Wrapper = styled.article` border-top: solid 2px ${primaryBgColorHover}; padding: 1rem 0rem; .cart-item-img { - max-height: 8rem; - max-width: 8rem; - object-fit: cover; + width: 8rem; } .info { } diff --git a/client/src/components/Cart/OrderSummary.jsx b/client/src/components/Cart/CartTotal.jsx similarity index 95% rename from client/src/components/Cart/OrderSummary.jsx rename to client/src/components/Cart/CartTotal.jsx index 69a1a6f..2c53d90 100644 --- a/client/src/components/Cart/OrderSummary.jsx +++ b/client/src/components/Cart/CartTotal.jsx @@ -6,11 +6,10 @@ import { formatPrice } from '../../utils' import { quaternaryBgColorLight, quaternaryBgColor, - textColor, boldTextColor, } from '../../assets/js/variables' -const OrderSummary = () => { +const CartTotal = () => { const { cartTotal, shipping, tax, orderTotal } = useSelector( (store) => store.cart ) @@ -41,7 +40,7 @@ const OrderSummary = () => { ) } -export default OrderSummary +export default CartTotal const Wrapper = styled.section` margin-bottom: 1rem; diff --git a/client/src/components/Home/AspirationSegment.jsx b/client/src/components/Home/AspirationSegment.jsx index 1199954..239a072 100644 --- a/client/src/components/Home/AspirationSegment.jsx +++ b/client/src/components/Home/AspirationSegment.jsx @@ -1,11 +1,11 @@ import Badge from 'react-bootstrap/esm/Badge' import { GiTiedScroll, GiBullseye } from 'react-icons/gi' import { FaRegEye } from 'react-icons/fa' -import '../../assets/scss/Card.scss' import FlipCard from '../FlipCard' +import styled from 'styled-components' +import { boldTextColor, primaryBgColor, quaternaryBgColor, quaternaryBgColorLight, textColor } from '../../assets/js/variables' const AspirationSegment = () => { - const description = "Some quick example text to build on the card title and make up the bulk of the card's content." return (

@@ -41,3 +41,90 @@ const AspirationSegment = () => { ) } export default AspirationSegment + +const Wrapper = styled.section` + body { + margin-top: 4rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.75; + background-color: ${quaternaryBgColor}; + color: ${textColor}; + position: relative; + } + + p { + margin-bottom: 1.5rem; + max-width: 40em; + color: ${textColor}; + } + + h1, + h2, + h3, + h4, + h5 { + color: ${textColor}; + } + + .divider { + color: ${primaryBgColor}; + width: 100%; + font-size: 3rem; + text-align: center; + text-shadow: 2px 2px 3px rgb(180, 176, 176); + height: 60px; + line-height: 2rem; + top: 28.75rem; + } + + .segment { + padding-top: 30px; + padding-bottom: 80px; + } + + .segmentX { + padding: 80px 0; + background-color: ${quaternaryBgColorLight}; + } + + .segmentX .segment-heading { + margin-bottom: 3.5rem; + text-transform: uppercase; + letter-spacing: 0.25rem; + font-weight: bold; + color: ${primaryBgColor}; + } + + .segmentX .segment-heading .badge { + background-color: ${primaryBgColor} !important; + color: var(--quaternary-bg-color-light); + } + + .segment-heading { + color: ${primaryBgColor}; + font-family: var(--bodyFont); + font-weight: 500; + text-shadow: 2px 2px 3px rgb(180, 176, 176); + font-size: 2rem; + letter-spacing: 1px; + } + + .breadcrumb { + margin: 5rem 5rem 2rem; + } + + .breadcrumb .breadcrumb-item { + font-size: 2rem; + font-weight: bold; + } + + .breadcrumb .active { + color: ${boldTextColor}; + } + + .breadcrumb a { + text-decoration: none; + color: ${textColor}; + } +` \ No newline at end of file diff --git a/client/src/components/MyNavbar.jsx b/client/src/components/MyNavbar.jsx index fdab71e..f4a49c2 100644 --- a/client/src/components/MyNavbar.jsx +++ b/client/src/components/MyNavbar.jsx @@ -6,7 +6,6 @@ import { logo } from '../assets/images' import { useSelector, useDispatch } from 'react-redux' import { Link, useNavigate } from 'react-router-dom' import { useQueryClient } from '@tanstack/react-query' -import '../assets/scss/Navbar.scss' import styled from 'styled-components' import { primaryBgColorHover, diff --git a/client/src/components/Order/CustomerContactInfo.jsx b/client/src/components/Order/CustomerContactInfo.jsx index 91c5b3c..271595f 100644 --- a/client/src/components/Order/CustomerContactInfo.jsx +++ b/client/src/components/Order/CustomerContactInfo.jsx @@ -1,6 +1,50 @@ -const CustomerContactInfo = () => { +import styled from 'styled-components' +import { + MdOutlinePersonOutline, + MdMailOutline, + MdOutlinePhoneAndroid, +} from 'react-icons/md' +import { quaternaryBgColor, shadow1 } from '../../assets/js/variables' +import UserInfo from '../../pages/main/UserInfo' + +const CustomerContactInfo = ({userInfo}) => { return ( -
CustomerContactInfo
+ +
Thông tin khách hàng
+
+ + + {' '} + {userInfo.name} +
+
+ + + {' '} + {userInfo.email} +
+
+ + + {' '} + {userInfo.phone_number} +
+
) } -export default CustomerContactInfo \ No newline at end of file +export default CustomerContactInfo + +const Wrapper = styled.section` + color: #000; + margin-bottom: 1rem; + box-shadow: ${shadow1}; + padding: 1rem; + background-color: white; + border-radius: 1rem; + .icon { + font-size: 1.5rem; + } + .p-line { + border-bottom: 0.5px solid gray; + } +` diff --git a/client/src/components/Order/CustomerInstruction.jsx b/client/src/components/Order/CustomerInstruction.jsx new file mode 100644 index 0000000..d8308d7 --- /dev/null +++ b/client/src/components/Order/CustomerInstruction.jsx @@ -0,0 +1,41 @@ +import React from 'react' +import styled from 'styled-components' +import { shadow1 } from '../../assets/js/variables' + +const CustomerInstruction = ({ paymentMethod, coupon, instruction }) => { + return ( + +
Thông tin thêm
+
+ Thanh toán: + {paymentMethod} +
+
+ Mã giảm giá: + {coupon || 'Không có mã giảm giá'} +
+
+ Ghi chú: + {instruction || 'Không có yêu cầu nào'} +
+
+ ) +} + +export default CustomerInstruction + +const Wrapper = styled.section` + margin-bottom: 1rem; + box-shadow: ${shadow1}; + padding: 1rem; + background-color: white; + border-radius: 1rem; + .icon { + font-size: 1.5rem; + } + .p-line { + padding-top: 0.8rem; + color: #000; + border-bottom: 0.5px solid gray; + } +` diff --git a/client/src/components/Order/OrderItemsList.jsx b/client/src/components/Order/OrderItemsList.jsx index facb856..10d798a 100644 --- a/client/src/components/Order/OrderItemsList.jsx +++ b/client/src/components/Order/OrderItemsList.jsx @@ -1,44 +1,63 @@ -import styled from "styled-components" +import styled from 'styled-components' +import { formatPrice } from '../../utils' +import { boldTextColor, shadow1 } from '../../assets/js/variables' const OrderItemsList = ({ itemList }) => { return ( - -
- {itemList.map(item => { - return ( -
-
- {123} -
-
-

title

-

author name

-
-
- Select input - {/* - - Xóa - */} -
-
- -
+ +
+ {itemList.map((item) => { + return ( +
+
+ {'book +
+
+

{item.title}

+

{item.author}

+
+
+
x{item.amount}
+
+
{formatPrice(item.price)}
+
+ ) + })}
- ) - })} -
- - + ) } export default OrderItemsList const Wrapper = styled.section` - -` \ No newline at end of file +box-shadow: ${shadow1}; +border-radius: 1rem; +padding: 1rem; +background-color: white; + .book-img { + width: 8rem; + border-radius: 0.5rem; + } + h3 { + font-weight: bold; + } + + h3, + h4 { + font-size: 1rem; + } + .container { + display: flex; + flex-direction: column; + gap: 1rem; + justify-content: space-between; + } + .book-container { + border-bottom: 0.5px solid gray; + padding-bottom: 1rem; + } +` diff --git a/client/src/components/Order/OrderList.jsx b/client/src/components/Order/OrderList.jsx index a55d6ab..6910608 100644 --- a/client/src/components/Order/OrderList.jsx +++ b/client/src/components/Order/OrderList.jsx @@ -1,15 +1,18 @@ import day from 'dayjs' import Table from 'react-bootstrap/Table' import advancedFormat from 'dayjs/plugin/advancedFormat' -import { quaternaryBgColor } from '../../assets/js/variables' +import { primaryBgColor, quaternaryBgColor, tertiaryBgColor } from '../../assets/js/variables' import { useEffect } from 'react' import { formatPrice } from '../../utils' import Badge from 'react-bootstrap/Badge' import { useNavigate } from 'react-router-dom' import styled from 'styled-components' +import dateFormat from 'dateformat' +import { useSelector } from 'react-redux' day.extend(advancedFormat) const OrdersList = ({ orders, meta }) => { + const user = useSelector((store) => store.user) const navigate = useNavigate() const handleNavigate = (id) => { navigate(`/order/${id}`) @@ -18,8 +21,13 @@ const OrdersList = ({ orders, meta }) => { const tr = document.getElementsByTagName('tr') useEffect(() => { Array.from(tr).forEach((row) => { - if (Number(row.id) % 2 === 0) { + if ((row.id).split('-')[1] === 'true') { Array.from(row.childNodes).forEach((child) => { + child.style.backgroundColor = tertiaryBgColor + }) + } + else if (Number((row.id).split('-')[0]) % 2 === 0) { + Array.from(row.childNodes).forEach((child) => { child.style.backgroundColor = quaternaryBgColor }) } @@ -35,13 +43,13 @@ const OrdersList = ({ orders, meta }) => { {/* head */} - Họ Tên - Địa chỉ - Số điện thoại - Tổng tiền - Ngày đặt - Tình trạng - Thanh toán + Họ Tên + Địa chỉ + Số điện thoại + Tổng tiền + Ngày đặt + Tình trạng + Thanh toán @@ -58,24 +66,28 @@ const OrdersList = ({ orders, meta }) => { is_paid, } = order - const date = day(created_at).format('hh:mm a - MMM Do, YYYY ') + const date = dateFormat(created_at, 'dddd, dd mmmm, yyyy') return ( handleNavigate(order.id)} > - + {recipient_name || customer_name} - {shipping_address} - + + {shipping_address} + + {recipient_phone || customer_phone} - {formatPrice(cost)} - {date} - + + {formatPrice(cost)} + + {date} + { {status} - + {is_paid ? 'Đã thanh toán' : 'Chưa thanh toán'} diff --git a/client/src/components/Order/OrderSummary.jsx b/client/src/components/Order/OrderSummary.jsx new file mode 100644 index 0000000..1d512f5 --- /dev/null +++ b/client/src/components/Order/OrderSummary.jsx @@ -0,0 +1,59 @@ +import React from 'react' +import styled from 'styled-components' +import Card from 'react-bootstrap/Card' +import ListGroup from 'react-bootstrap/ListGroup' +import { formatPrice } from '../../utils' +import { + quaternaryBgColorLight, + boldTextColor, +} from '../../assets/js/variables' + +const OrderSummary = () => { + return ( + + + +
Thông tin đơn hàng
+
+ + + + Tổng tiền hàng + {formatPrice(12)} + + + Phí ship + {formatPrice(2)} + + + Thuế + {formatPrice(3)} + + + Tổng thanh toán + {formatPrice(23)} + + + +
+
+ ) +} + +export default OrderSummary + +const Wrapper = styled.section` + .card { + } + .list-group-item { + display: flex; + justify-content: space-between; + color: ${boldTextColor}; + } + .last-item { + background: ${quaternaryBgColorLight}; + } + .label { + font-weight: 500; + } +` diff --git a/client/src/components/Order/ShippingAddress.jsx b/client/src/components/Order/ShippingAddress.jsx index b7b98d5..7c87496 100644 --- a/client/src/components/Order/ShippingAddress.jsx +++ b/client/src/components/Order/ShippingAddress.jsx @@ -1,6 +1,49 @@ -const ShippingAddress = () => { +import styled from 'styled-components' +import { + MdOutlineLocationOn, + MdOutlinePersonOutline, + MdOutlinePhoneAndroid, +} from 'react-icons/md' +import { shadow1 } from '../../assets/js/variables' + +const ShippingAddress = ({orderInfo}) => { return ( -
ShippingAddress
+ +
Thông tin nhận hàng
+
+ + + {' '} + {orderInfo.recipient_name} +
+
+ + + {' '} + {orderInfo.recipient_phone} +
+
+ + + {' '} + {orderInfo.shipping_address} +
+
) } -export default ShippingAddress \ No newline at end of file +export default ShippingAddress + +const Wrapper = styled.section` + color: #000; + margin-bottom: 1rem; + box-shadow: ${shadow1}; + padding: 1rem; + background-color: white; + border-radius: 1rem; + .icon { + font-size: 1.5rem; + } + .p-line { + border-bottom: 0.5px solid gray; + } +` diff --git a/client/src/components/index.js b/client/src/components/index.js index cbeb2c4..cd93991 100644 --- a/client/src/components/index.js +++ b/client/src/components/index.js @@ -19,7 +19,7 @@ import SelectInput from './SelectInput' import BookList from './Library/BookList' import CartItem from './Cart/CartItem' import CartItemsList from './Cart/CartItemsList' -import OrderSummary from './Cart/OrderSummary' +import CartTotal from './Cart/CartTotal' import SectionTitle from './SectionTitle' import RadiosInput from './RadiosInput' import CheckboxInput from './CheckboxInput' @@ -34,6 +34,8 @@ import CommentSection from './CommentSection' import OrderItemsList from './Order/OrderItemsList' import CustomerContactInfo from './Order/CustomerContactInfo' import ShippingAddress from './Order/ShippingAddress' +import OrderSummary from './Order/OrderSummary' +import CustomerInstruction from './Order/CustomerInstruction' export { MyNavbar, @@ -57,7 +59,7 @@ export { BookList, CartItem, CartItemsList, - OrderSummary, + CartTotal, SectionTitle, RadiosInput, CheckboxInput, @@ -72,4 +74,6 @@ export { OrderItemsList, CustomerContactInfo, ShippingAddress, + OrderSummary, + CustomerInstruction, } diff --git a/client/src/features/orders/orderThunk.js b/client/src/features/orders/orderThunk.js index be9a006..02fa778 100644 --- a/client/src/features/orders/orderThunk.js +++ b/client/src/features/orders/orderThunk.js @@ -7,7 +7,7 @@ export const getAllOrdersThunk = async (_, thunkAPI) => { params: { page }, }) return resp.data - } catch (e) { + } catch (error) { return checkForUnauthorizedResponse(error, thunkAPI) } } @@ -19,7 +19,7 @@ export const getUserOrderThunk = async (_, thunkAPI) => { params: { page }, }) return resp.data - } catch (e) { + } catch (error) { return checkForUnauthorizedResponse(error, thunkAPI) } } diff --git a/client/src/features/users/userThunk.js b/client/src/features/users/userThunk.js index bbb0980..68a8edb 100644 --- a/client/src/features/users/userThunk.js +++ b/client/src/features/users/userThunk.js @@ -6,7 +6,6 @@ import { clearUserInfo } from './userSlice' export const registerUserThunk = async (url, user, thunkAPI) => { try { const resp = await customFetch.post(url, user) - return resp.data } catch (error) { const message = error.response?.data?.msg || 'Register failed!' @@ -17,8 +16,6 @@ export const registerUserThunk = async (url, user, thunkAPI) => { export const loginUserThunk = async (url, user, thunkAPI) => { try { const resp = await customFetch.post(url, user) - console.log(resp) - return resp.data } catch (error) { const message = error.response?.data?.msg || 'Login error!' diff --git a/client/src/main.jsx b/client/src/main.jsx index b34241d..24b8de8 100644 --- a/client/src/main.jsx +++ b/client/src/main.jsx @@ -6,7 +6,7 @@ import { Provider } from 'react-redux' import store from './store' import { ToastContainer } from 'react-toastify' import 'react-toastify/dist/ReactToastify.css' -import '../src/assets/scss/App.scss' +import '../src/assets/css/App.css' import { LocalizationProvider } from '@mui/x-date-pickers' import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' diff --git a/client/src/pages/main/About.jsx b/client/src/pages/main/About.jsx index b570789..522b3ff 100644 --- a/client/src/pages/main/About.jsx +++ b/client/src/pages/main/About.jsx @@ -1,36 +1,148 @@ -import '../../assets/scss/About.scss' import { aboutImage } from '../../assets/images' import Badge from 'react-bootstrap/Badge' +import styled from 'styled-components' +import { + primaryBgColor, + quaternaryBgColor, + primaryBgColorHover, + shadow3, +} from '../../assets/js/variables' const About = () => { return ( -
-

- Nhà sách{' '} - - trực tuyến - -

-
-
- about image + +
+

+ Nhà sách{' '} + + trực tuyến + +

+
+
+ about image +
+
+

We love book

+

+ Nhà sách trực tuyến của chúng tôi cung cấp hàng ngàn tựa sách đa + dạng, đáp ứng mọi nhu cầu đọc sách của bạn. Từ tiểu thuyết đến + sách khoa học, mọi thứ đều có sẵn chỉ với một cú nhấp chuột. +

+

+ Với mục tiêu mang tri thức đến gần hơn với mọi người, chúng tôi + cam kết mang đến dịch vụ chất lượng và sự hài lòng tuyệt đối cho + khách hàng. Hãy khám phá thế giới sách ngay hôm nay! +

+ +
-
-

We love book

-

- Nhà sách trực tuyến của chúng tôi cung cấp hàng ngàn tựa sách đa - dạng, đáp ứng mọi nhu cầu đọc sách của bạn. Từ tiểu thuyết đến sách - khoa học, mọi thứ đều có sẵn chỉ với một cú nhấp chuột. -

-

- Với mục tiêu mang tri thức đến gần hơn với mọi người, chúng tôi - cam kết mang đến dịch vụ chất lượng và sự hài lòng tuyệt đối cho - khách hàng. Hãy khám phá thế giới sách ngay hôm nay! -

- -
-
-
+ + ) } -export default About \ No newline at end of file +export default About + +const Wrapper = styled.section` + .section { + padding-top: 2rem; + padding-bottom: 3rem; + width: 100%; + } + + .section .about-heading { + font-size: 2.5rem; + margin-bottom: 3.5rem; + text-align: center; + text-transform: uppercase; + letter-spacing: 0.25rem; + font-weight: bold; + color: ${primaryBgColor}; + } + + .section .about-heading .about-badge { + background-color: ${primaryBgColor} !important; + } + + .section .about-heading .about-badge span { + color: ${quaternaryBgColor}; + } + + .general-container { + display: flex; + width: 95vw; + margin: auto; + justify-content: space-between; + max-width: 1170px; + flex-wrap: wrap; + } + + .about-section .about-container .about-img { + position: relative; + flex: 0 0 calc(50% - 2rem); + box-shadow: ${shadow3}; + } + + .about-section .about-container .about-img img { + overflow: clip; + width: 100%; + object-fit: cover; + } + + @media screen and (min-width: 1170px) { + .about-section .about-container .about-img::before { + content: ''; + position: absolute; + width: 100%; + height: 100%; + z-index: -1; + bottom: 1.5rem; + right: 1.5rem; + border: 0.5rem solid ${primaryBgColor}; + box-sizing: border-box; + } + } + + .about-section .about-container .describe-container { + display: flex; + flex-direction: column; + justify-content: center; + flex: 0 0 calc(50% - 2rem); + } + + .about-section .about-container .describe-container h3 { + font-size: 1.75rem; + text-transform: capitalize; + letter-spacing: 0.25rem; + margin-bottom: 0.75rem; + } + + .about-section .about-container .describe-container p { + letter-spacing: 0; + margin-bottom: 1.25rem; + text-align: left; + } + + @media (max-width: 992px) { + .about-section .about-container { + flex-direction: column; + width: 90vw; + } + } + + .general-btn { + font-size: 0.875rem; + padding: 0.375rem 0.75rem; + border: 2px solid ${primaryBgColor}; + background-color: ${primaryBgColor}; + line-height: 21px; + color: ${quaternaryBgColor}; + } + + .general-btn:hover { + cursor: pointer; + background-color: ${primaryBgColorHover}; + color: #fff; + border-color: ${primaryBgColorHover}; + } +` diff --git a/client/src/pages/main/Cart.jsx b/client/src/pages/main/Cart.jsx index 0f58ae5..2d19460 100644 --- a/client/src/pages/main/Cart.jsx +++ b/client/src/pages/main/Cart.jsx @@ -1,6 +1,6 @@ import { useSelector } from 'react-redux' import { Link } from 'react-router-dom' -import { SectionTitle, OrderSummary, CartItemsList } from '../../components' +import { SectionTitle, CartTotal, CartItemsList } from '../../components' import styled from 'styled-components' import { quaternaryBgColorLight, @@ -35,7 +35,7 @@ const Cart = () => { {/* cart total chiếm 4 cột */}
- + {user ? ( Đặt hàng diff --git a/client/src/pages/main/Checkout.jsx b/client/src/pages/main/Checkout.jsx index bcc3e10..46f5d76 100644 --- a/client/src/pages/main/Checkout.jsx +++ b/client/src/pages/main/Checkout.jsx @@ -1,6 +1,6 @@ import { SectionTitle, - OrderSummary, + CartTotal, FormInput, RadiosInput, Loading, @@ -22,7 +22,7 @@ import { PayPalScriptProvider, PayPalButtons } from '@paypal/react-paypal-js' import { customFetch } from '../../utils/axios' import { toast } from 'react-toastify' import { useQuery } from '@tanstack/react-query' - +import { clearStore } from '../../features/users/userSlice' const client_id = import.meta.env.VITE_PAYPAL_CLIENT_ID const client_secret = import.meta.env.VITE_PAYPAL_CLIENT_SECRET @@ -125,8 +125,8 @@ const Checkout = () => { recipient_name: values.recipientName || userData?.name, recipient_phone: values.recipientPhoneNumber || userData?.phone_number, } - console.log(order); - + console.log(order) + if ( !order.shipping_address || !order.recipient_name || @@ -258,7 +258,13 @@ const Checkout = () => { if (isLoading) { return } - if (isError) return

{error.message}

+ if (isError) { + if (error?.response?.status === 401) { + dispatch(clearStore()) + return + } + return

{error.message}

+ } if (numItemsInCart === 0) return ( @@ -344,7 +350,7 @@ const Checkout = () => { {/* cart total chiếm 4 cột */}
- +
diff --git a/client/src/pages/main/Library.jsx b/client/src/pages/main/Library.jsx index 14f51b1..ca554a5 100644 --- a/client/src/pages/main/Library.jsx +++ b/client/src/pages/main/Library.jsx @@ -1,7 +1,8 @@ import React from 'react' import { LibraryContainer, MyBreadCrumb } from '../../components' -import '../../assets/scss/Library.scss' import { customFetch } from '../../utils/axios' +import styled from 'styled-components' +import { quaternaryBgColorLight } from '../../assets/js/variables' const url = '/books' @@ -42,11 +43,20 @@ const Library = () => { { label: 'Sách', path: '/library', active: true }, ] return ( -
- - -
+ +
+ + +
+
) } export default Library + +const Wrapper = styled.section` + .library .library-container { + background-color: ${quaternaryBgColorLight}; + padding: 0 5rem; + } +` diff --git a/client/src/pages/main/Order.jsx b/client/src/pages/main/Order.jsx index 318b3bb..3e21796 100644 --- a/client/src/pages/main/Order.jsx +++ b/client/src/pages/main/Order.jsx @@ -32,6 +32,8 @@ const Orders = () => { }, [page]) const meta = { page, totalOrders, numOfPages } + + return (

diff --git a/client/src/pages/main/SingleOrder.jsx b/client/src/pages/main/SingleOrder.jsx index 987c7b9..79fa443 100644 --- a/client/src/pages/main/SingleOrder.jsx +++ b/client/src/pages/main/SingleOrder.jsx @@ -1,14 +1,72 @@ -import { useQuery } from '@tanstack/react-query' +import dateFormat, { masks } from 'dateformat' import { customFetch } from '../../utils/axios' import { useLoaderData } from 'react-router-dom' import styled from 'styled-components' import { CustomerContactInfo, OrderSummary, - SectionTitle, ShippingAddress, OrderItemsList, + CustomerInstruction, + Loading, } from '../../components' +import { + boldTextColor, + shadow1, + quaternaryBgColorLight, + primaryBgColorHover, + textColor, +} from '../../assets/js/variables' +import { i18n } from 'dateformat' +import { toast } from 'react-toastify' +import { useState } from 'react' +import { useSelector } from 'react-redux' + +i18n.dayNames = [ + 'Sun', + 'Mon', + 'Tue', + 'Wed', + 'Thu', + 'Fri', + 'Sat', + 'CN', + 'T2', + 'T3', + 'T4', + 'T5', + 'T6', + 'T7', +] + +i18n.monthNames = [ + 'Th 1', + 'Th 2', + 'Th 3', + 'Th 4', + 'Th 5', + 'Th 6', + 'Th 7', + 'Th 8', + 'Th 9', + 'Th 10', + 'Th 11', + 'Th 12', + 'Tháng 1', + 'Tháng 2', + 'Tháng 3', + 'Tháng 4', + 'Tháng 5', + 'Tháng 6', + 'Tháng 7', + 'Tháng 8', + 'Tháng 9', + 'Tháng 10', + 'Tháng 11', + 'Tháng 12', +] + +i18n.timeNames = ['a', 'p', 'am', 'pm', 'A', 'P', 'AM', 'PM'] const getUserSingleOrder = (id) => { return { @@ -17,24 +75,123 @@ const getUserSingleOrder = (id) => { } } +const getSingleUser = (id) => { + return { + queryKey: ['showMe,', id || ''], + queryFn: async () => customFetch(`/users/${id}`), + } +} + export const loader = - (queryClient) => + (store, queryClient) => async ({ params }) => { const { id } = params - const response = await queryClient.ensureQueryData(getUserSingleOrder(id)) - console.log(response) - const order = response.data.order - return { order } + const responseOrder = await queryClient.ensureQueryData( + getUserSingleOrder(id) + ) + const order = responseOrder.data.order + const responseUser = await queryClient.ensureQueryData( + getSingleUser(order.user_id) + ) + const user = responseUser.data.user + return { order, user } } const SingleOrder = () => { - const { order } = useLoaderData() + const { order, user } = useLoaderData() + const { user: currentUser } = useSelector((store) => store.user) + + const [loading, setLoading] = useState(false) + const [requestCancel, setRequestCancel] = useState(false) + const handleRequestCancelOrder = async (e) => { + e.preventDefault() + try { + setLoading(true) + const response = await customFetch.patch( + `/orders/requestCancelOrder/${order.id}` + ) + toast.success(response?.data?.msg) + setRequestCancel(true) + } catch (error) { + console.log(error) + toast.error(error?.response?.data?.msg) + } finally { + setLoading(false) + } + } + + const handleCancelOrder = async (e) => { + e.preventDefault() + try { + setLoading(true) + await customFetch.patch(`/orders/${order.id}`, { + status: 'đã hủy', + request_cancel: false, + }) + toast.success('Cập nhật đơn hàng thành công') + setRequestCancel(false) + } catch (error) { + console.log(error) + toast.error(error?.response?.data?.msg) + } finally { + setLoading(false) + } + } + if (loading) { + return + } return ( - -
+
+
+

Mã đơn hàng : {order.id}

+
+ Ngày đặt hàng :{' '} + + {dateFormat(order.created_at, 'dddd, dd mmmm, yyyy')} + +
+ {order.status} +
+ {(requestCancel || order.request_cancel) && ( +
+ Yêu cầu hủy +
+ )} +
+
+
+ {/* CustomerContactInfo */} +
+ +
+ {/* ShippingAddress */} +
+ +
+
+ +
+
{/* chia thành 12 cột */}
{/* Item chiếm 8 cột */} @@ -43,13 +200,32 @@ const SingleOrder = () => {
{/* cart total chiếm 4 cột */}
-
- {/* CustomerContactInfo */} - - {/* ShippingAddress */} - +
{/* OrderSummary */} - + + {currentUser.role === 'user' && ( + + )} + {currentUser.role === 'admin' && ( + + )}
@@ -57,8 +233,35 @@ const SingleOrder = () => { ) } + export default SingleOrder const Wrapper = styled.section` - + margin-top: 6rem; + padding-bottom: 2rem; + .order-code { + font-weight: 600; + font-size: 2rem; + color: ${boldTextColor}; + } + .order-date { + color: ${textColor}; + } + .order-detail { + background-color: ${quaternaryBgColorLight}; + border-radius: 2rem; + padding: 2rem; + margin-top: 1rem; + box-shadow: ${shadow1}; + } + .header { + border-bottom: solid 2px ${primaryBgColorHover}; + margin-bottom: 1rem; + } + .info-container { + color: #000; + } + h5 { + color: #000; + } ` diff --git a/client/src/pages/main/UserInfo.jsx b/client/src/pages/main/UserInfo.jsx index 80fcb6e..4f872f2 100644 --- a/client/src/pages/main/UserInfo.jsx +++ b/client/src/pages/main/UserInfo.jsx @@ -9,6 +9,7 @@ import Form from 'react-bootstrap/Form' import Modal from 'react-bootstrap/Modal' import { toast } from 'react-toastify' import { customFetch } from '../../utils/axios' +import { clearStore } from '../../features/users/userSlice' import { textColor, @@ -142,7 +143,14 @@ const UserInfo = () => { if (isLoading) { return } - if (isError) return

{error.message}

+ + if (isError) { + if (error?.response?.status === 401) { + dispatch(clearStore()) + return + } + return

{error.message}

+ } return ( diff --git a/client/src/utils/axios.js b/client/src/utils/axios.js index 1a72ee2..dcfe151 100644 --- a/client/src/utils/axios.js +++ b/client/src/utils/axios.js @@ -8,6 +8,8 @@ export const customFetch = axios.create({ withCredentials: true, }) + + export const checkForUnauthorizedResponse = (error, thunkAPI) => { if (error.response.status === 401) { thunkAPI.dispatch(clearStore()) @@ -19,7 +21,7 @@ export const checkForUnauthorizedResponse = (error, thunkAPI) => { export const checkForForbiddenResponse = (error, thunkAPI) => { if (error.response.status === 403) { thunkAPI.dispatch(clearStore()) - return thunkAPI.rejectWithValue("You are not allowed to use this service!") + return thunkAPI.rejectWithValue('You are not allowed to use this service!') } return thunkAPI.rejectWithValue(error.response.data.msg) } diff --git a/controllers/auth.controller.js b/controllers/auth.controller.js index be37390..8a9f863 100644 --- a/controllers/auth.controller.js +++ b/controllers/auth.controller.js @@ -10,6 +10,7 @@ const { const crypto = require('crypto') const { StatusCodes } = require('http-status-codes') const CustomError = require('../errors') +const { isTokenValid } = require('../utils/jwt') const register = async (req, res) => { const { email, name, password, gender } = req.body @@ -32,10 +33,10 @@ const register = async (req, res) => { password, role, verificationToken, - gender + gender, }) - const origin = 'http://localhost:5173' // origin là host front-end ko nên nhầm lẫn với host phía back-end + const origin = process.env.ORIGIN // origin là host front-end ko nên nhầm lẫn với host phía back-end // sau khi host lên nền tảng hỗ trợ sẽ thay đổi origin const tempOrigin = req.get('origin') @@ -82,7 +83,9 @@ const login = async (req, res) => { let refreshToken = '' // Tìm token của user nếu đã tồn tại - const existingToken = await Token.findOne({ where: { user: user.id } }) + const existingToken = await Token.findOne({ + where: { user: user.id, ip: req.ip }, + }) // Kiểm tra token đã tồn tại trong db chưa if (existingToken) { // Kiểm tra xem token có valid ko @@ -90,11 +93,20 @@ const login = async (req, res) => { throw new CustomError.UnauthenticatedError('Invalid Credentials') refreshToken = existingToken.refreshToken attachCookiesToResponse({ res, user: tokenUser, refreshToken }) // tiếp tọc sử dụng refresh token đó - res - .status(StatusCodes.OK) - .json({ user: { ...tokenUser, email_is_verified: user. email_is_verified } }) + res.status(StatusCodes.OK).json({ + user: { ...tokenUser, email_is_verified: user.email_is_verified }, + }) return } + + const activeSessions = await Token.count({ where: { user: user.id } }) + if (activeSessions >= process.env.MAX_SESSIONS) { + await Token.destroy({ + where: { user: user.id }, + order: [['created_at', 'ASC']], + limit: 1, // Xóa session cũ nhất + }) + } // trường hợp chưa tồn tại refresh token refreshToken = crypto.randomBytes(40).toString('hex') const ip = req.ip @@ -110,12 +122,6 @@ const login = async (req, res) => { res.status(StatusCodes.OK).json({ user: tokenUser }) } const logout = async (req, res) => { - // Xóa token trong db khi logout - const tokens = await Token.findAll({ where: { user: req.user.userId } }) - for (const token of tokens) { - token.destroy() - } - // Xóa 2 cookies token khi logout res.cookie('refreshToken', 'logout', { httpOnly: true, expires: new Date(Date.now()), @@ -124,6 +130,15 @@ const logout = async (req, res) => { httpOnly: true, expires: new Date(Date.now()), }) + const { refreshToken } = req.signedCookies + const payload = isTokenValid(refreshToken) + const existingToken = await Token.findOne({ + where: { + user: payload.user.userId, + refreshToken: payload.refreshToken, + }, + }) + existingToken.destroy() res.status(StatusCodes.OK).json({ msg: 'user logged out!' }) } @@ -133,7 +148,7 @@ const verifyEmail = async (req, res) => { const user = await User.findOne({ where: { email } }) if (!user) throw new CustomError.UnauthenticatedError(`No user with email: ${email}`) - if (user. email_is_verified) { + if (user.email_is_verified) { throw new CustomError.BadRequestError( `Your email has already been verified` ) @@ -143,7 +158,7 @@ const verifyEmail = async (req, res) => { throw new CustomError.UnauthenticatedError( `User token does not match verification token: ${verificationToken}` ) - user. email_is_verified = true + user.email_is_verified = true user.email_verified_date = Date.now() user.verificationToken = '' await user.save() diff --git a/controllers/order.controller.js b/controllers/order.controller.js index 0b58178..ddbf5f1 100644 --- a/controllers/order.controller.js +++ b/controllers/order.controller.js @@ -1,5 +1,5 @@ -const Order = require('../models/order.model') -const Book = require('../models/book.model') +const { Book, Order, Author } = require('../models') + const { v4: uuidv4 } = require('uuid') const fetch = require('node-fetch') @@ -60,17 +60,26 @@ const createPaypalOrder = async (cart) => { let orderItems = [] let subtotal = 0 for (const book of book_list) { - const dbBook = await Book.findByPk(book.bookId) + const dbBook = await Book.findOne({ + where: { id: book.bookId }, + include: [ + { + model: Author, + attributes: ['name'], + }, + ], + }) if (!dbBook) { throw new CustomAPIError.NotFoundError(`No book with id : ${book.bookId}`) } - const { title, price, book_img, id } = dbBook + const { title, price, book_img, id, author } = dbBook const singleOrderItem = { amount: book.amount, title, price, book_img, bookID: id, + author: author.name, } // add item to order orderItems = [...orderItems, singleOrderItem] @@ -78,7 +87,7 @@ const createPaypalOrder = async (cart) => { subtotal += book.amount * price } // calculate total - const total = tax + shipping_fee + subtotal + const total = tax + shipping_fee + subtotal if (Math.ceil(total) !== Math.ceil(cart_total)) throw new CustomAPIError.BadRequestError( @@ -149,8 +158,7 @@ const capturePaypalOrder = async (orderID, userId) => { }) const invoice = await response.json() - console.log(invoice.status); - + console.log(invoice.status) // // Cập nhật order vào postgres const order = await Order.findByPk(invoice?.purchase_units[0]?.reference_id) @@ -203,17 +211,26 @@ const createOrder = async (req, res) => { let orderItems = [] let subtotal = 0 for (const book of book_list) { - const dbBook = await Book.findByPk(book.bookId) + const dbBook = await Book.findOne({ + where: { id: book.bookId }, + include: [ + { + model: Author, + attributes: ['name'], + }, + ], + }) if (!dbBook) { throw new CustomAPIError.NotFoundError(`No book with id : ${book.bookId}`) } - const { title, price, book_img, id } = dbBook + const { title, price, book_img, id, author } = dbBook const singleOrderItem = { amount: book.amount, title, price, book_img, bookID: id, + author: author.name, } // add item to order orderItems = [...orderItems, singleOrderItem] @@ -272,9 +289,11 @@ const createOrder = async (req, res) => { .json({ order, clientSecret: order.clientSecret }) } - const getAllOrders = async (req, res) => { - const order = [['created_at', 'DESC']] + const order = [ + ['request_cancel', 'DESC'], + ['created_at', 'DESC'], + ] const page = Number(req.query.page) || 1 const limit = Number(req.query.limit) || 12 @@ -327,7 +346,7 @@ const getCurrentUserOrders = async (req, res) => { .json({ orders, meta: { page, numOfPages, totalOrders } }) } // đã thanh toán -const updateOrder = async (req, res) => { +const updatePaidOrder = async (req, res) => { const { id: orderId } = req.params const order = await Order.findByPk(orderId) if (!order) { @@ -351,12 +370,14 @@ const updateOrder = async (req, res) => { const requestCancelOrder = async (req, res) => { const { id: orderId } = req.params - checkPermissions(req.user, order.user_id) const order = await Order.findByPk(orderId) if (!order) { - throw new CustomAPIError.NotFoundError(`Không thể tìm được đơn hàng ${orderId}`) + throw new CustomAPIError.NotFoundError( + `Không thể tìm được đơn hàng ${orderId}` + ) } - if(order.status !== 'chờ xác nhận') + checkPermissions(req.user, order.user_id) + if (order.status !== 'chờ xác nhận') throw new CustomAPIError.BadRequestError( `Đơn hàng đã được xác nhận. Vui lòng liên hệ nhân viên để xử lý!` ) @@ -365,13 +386,24 @@ const requestCancelOrder = async (req, res) => { res.status(StatusCodes.OK).json({ msg: 'Yêu cầu hủy đơn hàng đã được gửi!' }) } + const updateOrder = async (req, res) => { + const { id: orderId } = req.params + const order = await Order.findByPk(orderId) + await order.update({ ...req.body }) + + return res + .status(StatusCodes.OK) + .json({ msg: 'Cập nhật đơn hàng thành công' }) + } + module.exports = { getAllOrders, getSingleOrder, getCurrentUserOrders, createOrder, createPaypalOrder, - updateOrder, + updatePaidOrder, capturePaypalOrder, requestCancelOrder, + updateOrder, } diff --git a/controllers/user.controller.js b/controllers/user.controller.js index 194780b..c026034 100644 --- a/controllers/user.controller.js +++ b/controllers/user.controller.js @@ -20,7 +20,7 @@ const getAllUsers = asyncWrapper(async (req, res) => { }) const getSingleUser = async (req, res) => { - const user = await User.findOne({ _id: req.params.id }) + const user = await User.findOne({ where: { id: req.params.id } }) if (!user) { throw new CustomError.NotFoundError(`No user with id : ${req.params.id}`) } diff --git a/dbconfig.js b/dbconfig.js index 62f19c0..2a0607f 100644 --- a/dbconfig.js +++ b/dbconfig.js @@ -2,11 +2,14 @@ require('dotenv').config() const { Sequelize } = require('sequelize') // connect db -const sequelize = new Sequelize(process.env.DB_CONNECTION_STRING) +const sequelize = new Sequelize(process.env.DB_CONNECTION_STRING, { + timezone: '+07:00', +}) // const sequelize = new Sequelize(process.env.DB_CONNECTION_STRING, { // host: process.env.DB_HOST, // dialect: process.env.DB_DIALECT, // schema: process.env.DB_SCHEMA, +// timezone: '+07:00', // dialectOptions: { // searchPath: process.env.DB_SCHEMA, // }, diff --git a/middleware/authentication.js b/middleware/authentication.js index 7a1c7a4..c3a91d9 100644 --- a/middleware/authentication.js +++ b/middleware/authentication.js @@ -1,6 +1,7 @@ const CustomError = require('../errors') const { isTokenValid, attachCookiesToResponse } = require('../utils') const Token = require('../models/token.model') +const { logout } = require('../controllers/auth.controller') const authenticateUser = async (req, res, next) => { const { accessToken, refreshToken } = req.signedCookies @@ -8,7 +9,7 @@ const authenticateUser = async (req, res, next) => { // Kiểm tra access token có valid ko if (accessToken) { const payload = isTokenValid(accessToken) - req.user = payload.user + req.user = payload.user return next() } // Kiểm tra refresh token có valid ko @@ -19,10 +20,16 @@ const authenticateUser = async (req, res, next) => { refreshToken: payload.refreshToken, }, }) + // Kiểm tra token có tôn tại nếu có thì có valid ko if (!existingToken || !existingToken?.isValid) throw new CustomError.UnauthenticatedError('Authentication Invalid') + if(existingToken.ip !== req.ip) { + existingToken.destroy() + throw new CustomError.UnauthenticatedError('Có ai đó đã đăng nhập vào tài khoản bằng thiết bị khác!') + } + attachCookiesToResponse({ res, user: payload.user, diff --git a/routes/order.router.js b/routes/order.router.js index e3d2413..04358b8 100644 --- a/routes/order.router.js +++ b/routes/order.router.js @@ -21,7 +21,7 @@ router .post(authenticateUser, createOrder) .get(authenticateUser, authorizePermissions('admin'), getAllOrders) -router.route('/requestCancelOrder').get(authenticateUser, requestCancelOrder) +router.route('/requestCancelOrder/:id').patch(authenticateUser, requestCancelOrder) router.route('/showAllMyOrders').get(authenticateUser, getCurrentUserOrders) router.route('/paypal/createOrder').post(authenticateUser, async (req, res) => { diff --git a/utils/jwt.js b/utils/jwt.js index 2996861..96d2c6b 100644 --- a/utils/jwt.js +++ b/utils/jwt.js @@ -2,7 +2,9 @@ const jwt = require('jsonwebtoken') // tạo token const createJWT = ({ payload }) => { - const token = jwt.sign(payload, process.env.JWT_SECRET) // ko cần set lifetime vì cookies sẽ giải quyết điều này + const token = jwt.sign(payload, process.env.JWT_SECRET, { + algorithm: 'HS256', + }) // ko cần set lifetime vì cookies sẽ giải quyết điều này return token } diff --git a/utils/nodemailerConfig.js b/utils/nodemailerConfig.js index 3b1479e..9ed9a1f 100644 --- a/utils/nodemailerConfig.js +++ b/utils/nodemailerConfig.js @@ -1,8 +1,9 @@ module.exports = { - host: 'smtp.ethereal.email', + host: 'smtp.gmail.com', + // host: 'smtp.ethereal.email', port: 587, auth: { - user: 'cali.turner@ethereal.email', - pass: 'RHAt5WtcYAKXhfttDV', + user: process.env.GMAIL_USER, + pass: process.env.GMAIL_PASSWORD, }, } diff --git a/utils/sendEmail.js b/utils/sendEmail.js index 076b3b5..4829240 100644 --- a/utils/sendEmail.js +++ b/utils/sendEmail.js @@ -4,8 +4,18 @@ const nodemailerConfig = require('./nodemailerConfig') const sendEmail = async ({ to, subject, text, html }) => { const transporter = nodemailer.createTransport(nodemailerConfig) + // return transporter.sendMail({ + // from: '"Raven" ', // sender address + // to, // list of receivers + // subject, // Subject line + // text, // plain text body + // html, // html body + // }) return transporter.sendMail({ - from: '"Raven" ', // sender address + from: { + name: 'Raven', + address: process.env.GMAIL_USER, + }, // sender address to, // list of receivers subject, // Subject line text, // plain text body diff --git a/utils/sendResetPasswordEmail.js b/utils/sendResetPasswordEmail.js index 3a7ddd7..339b499 100644 --- a/utils/sendResetPasswordEmail.js +++ b/utils/sendResetPasswordEmail.js @@ -2,11 +2,11 @@ const sendEmail = require('./sendEmail.js') const sendResetPasswordEmail = async ({ name, email, token, origin }) => { const resetUrl = `${origin}/user/reset-password?token=${token}&email=${email}` - const message = `

Please reset your password here: Reset Password

` + const message = `

Nhấn vào để thay đổi mật khẩu: Reset Password

` return sendEmail({ to: email, - subject: 'Reset Password', - html: `

Hello ${name}

${message}`, + subject: 'Thay đổi mật khẩu', + html: `

Xin chào ${name}

${message}`, }) } diff --git a/utils/sendVerificationEmail.js b/utils/sendVerificationEmail.js index 8566a89..fbe05d7 100644 --- a/utils/sendVerificationEmail.js +++ b/utils/sendVerificationEmail.js @@ -9,12 +9,32 @@ const sendVerificationEmail = async ({ // tạo đường dẫn xác thực const verifyEmail = `${origin}/user/verify-email?token=${verificationToken}&email=${email}` // message trỏ tới đường dẫn đó - const message = `

Please verify your email here: Verify Email

` + const message = ` + + + + + Email Verification + + +
+

Xác thực email

+

Xin chào

+

Cảm ơn bạn đã đăng ký. Vui lòng xác thực email của bạn bằng cách nhấn vào nút xác thực:

+

+ Xác thực +

+

Nếu bạn không yêu cầu xác thực thì bạn có thể bỏ qua mail này.

+

Xác thực hết hạn trong 24 giờ.

+

Chúc một ngày tốt lành,
Bookstore3v2t

+
+ +` return sendEmail({ to: email, - subject: 'Email Confirmation', - html: `

Hello, ${name}

${message}`, + subject: 'Xác thực tài khoản', + html: `

Xin chào ${name}

${message}`, }) }