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 (
-
-
-
-
-
-
title
- author name
-
-
- Select input
- {/*
-
- Xóa
- */}
-
-
-
-
+
+
+ {itemList.map((item) => {
+ return (
+
+
+
+
+
+
{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
-
-
-
-
-
+
+
+
+ Nhà sách{' '}
+
+ trực tuyến
+
+
+
+
+
+
+
+
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!
+
+
read more
+
-
-
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!
-
-
read more
-
-
-
+
+
)
}
-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' && (
+
+ Yêu cầu hủy đơn hàng
+
+ )}
+ {currentUser.role === 'admin' && (
+
+ Hủy đơn hàng
+
+ )}
@@ -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}`,
})
}