diff --git a/.gitignore b/.gitignore
index 4d29575..f1fd6c5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,7 +2,9 @@
# dependencies
/node_modules
-/.pnp
+/Frontend/node_modules
+/Backend/node_modules
+/Backend/.env
.pnp.js
# testing
diff --git a/BackEnd/package.json b/BackEnd/package.json
deleted file mode 100644
index cc8237b..0000000
--- a/BackEnd/package.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "name": "backend",
- "version": "1.0.0",
- "main": "server.js",
- "scripts": {
- "test": "echo \"Error: no test specified\" && exit 1",
- "start": "node server.js"
- },
- "keywords": [],
- "author": "",
- "license": "ISC",
- "description": ""
-}
diff --git a/BackEnd/server.js b/BackEnd/server.js
deleted file mode 100644
index e69de29..0000000
diff --git a/FrontEnd/.gitignore b/FrontEnd/.gitignore
new file mode 100644
index 0000000..4d29575
--- /dev/null
+++ b/FrontEnd/.gitignore
@@ -0,0 +1,23 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
diff --git a/FrontEnd/package.json b/FrontEnd/package.json
index d8bfe50..1d28763 100644
--- a/FrontEnd/package.json
+++ b/FrontEnd/package.json
@@ -3,13 +3,23 @@
"version": "0.1.0",
"private": true,
"dependencies": {
+ "@fortawesome/fontawesome-free": "^6.7.2",
+ "@fortawesome/fontawesome-svg-core": "^6.7.2",
+ "@fortawesome/free-brands-svg-icons": "^6.7.2",
+ "@fortawesome/free-regular-svg-icons": "^6.7.2",
+ "@fortawesome/free-solid-svg-icons": "^6.7.2",
+ "@fortawesome/react-fontawesome": "^0.2.2",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0",
+ "bootstrap": "^5.3.6",
+ "bootstrap-icons": "^1.13.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
+ "react-router-dom": "^7.6.0",
"react-scripts": "5.0.1",
+ "styled-components": "^6.1.18",
"web-vitals": "^2.1.4"
},
"scripts": {
@@ -35,5 +45,17 @@
"last 1 firefox version",
"last 1 safari version"
]
- }
+ },
+
+ "devDependencies": {
+ "css-loader": "^7.1.2",
+ "postcss-loader": "^8.1.1",
+ "resolve-url-loader": "^5.0.0",
+ "sass": "^1.89.0",
+ "sass-loader": "^16.0.5"
+ },
+ "main": "index.js",
+ "author": "",
+ "license": "ISC",
+ "description": ""
}
diff --git a/FrontEnd/public/favicon.ico b/FrontEnd/public/favicon.ico
index a11777c..6fba6d1 100644
Binary files a/FrontEnd/public/favicon.ico and b/FrontEnd/public/favicon.ico differ
diff --git a/FrontEnd/public/icons/appStore.png b/FrontEnd/public/icons/appStore.png
new file mode 100644
index 0000000..9cc5c41
Binary files /dev/null and b/FrontEnd/public/icons/appStore.png differ
diff --git a/FrontEnd/public/icons/bell.png b/FrontEnd/public/icons/bell.png
new file mode 100644
index 0000000..7eda544
Binary files /dev/null and b/FrontEnd/public/icons/bell.png differ
diff --git a/FrontEnd/public/icons/cart.png b/FrontEnd/public/icons/cart.png
new file mode 100644
index 0000000..fd27b67
Binary files /dev/null and b/FrontEnd/public/icons/cart.png differ
diff --git a/FrontEnd/public/icons/chplay.png b/FrontEnd/public/icons/chplay.png
new file mode 100644
index 0000000..e61cf2a
Binary files /dev/null and b/FrontEnd/public/icons/chplay.png differ
diff --git a/FrontEnd/public/icons/clock.png b/FrontEnd/public/icons/clock.png
new file mode 100644
index 0000000..c993e38
Binary files /dev/null and b/FrontEnd/public/icons/clock.png differ
diff --git a/FrontEnd/public/icons/folder.png b/FrontEnd/public/icons/folder.png
new file mode 100644
index 0000000..afb7ca2
Binary files /dev/null and b/FrontEnd/public/icons/folder.png differ
diff --git a/FrontEnd/public/icons/heart.png b/FrontEnd/public/icons/heart.png
new file mode 100644
index 0000000..9360416
Binary files /dev/null and b/FrontEnd/public/icons/heart.png differ
diff --git a/FrontEnd/public/icons/play.png b/FrontEnd/public/icons/play.png
new file mode 100644
index 0000000..71d797d
Binary files /dev/null and b/FrontEnd/public/icons/play.png differ
diff --git a/FrontEnd/public/images/Logo.png b/FrontEnd/public/images/Logo.png
new file mode 100644
index 0000000..f24724a
Binary files /dev/null and b/FrontEnd/public/images/Logo.png differ
diff --git a/FrontEnd/public/images/defaultImageUser.png b/FrontEnd/public/images/defaultImageUser.png
new file mode 100644
index 0000000..69cf863
Binary files /dev/null and b/FrontEnd/public/images/defaultImageUser.png differ
diff --git a/FrontEnd/public/images/logo_black_bg.png b/FrontEnd/public/images/logo_black_bg.png
new file mode 100644
index 0000000..048834b
Binary files /dev/null and b/FrontEnd/public/images/logo_black_bg.png differ
diff --git a/FrontEnd/public/index.html b/FrontEnd/public/index.html
index aa069f2..cabbc4d 100644
--- a/FrontEnd/public/index.html
+++ b/FrontEnd/public/index.html
@@ -7,7 +7,7 @@
-
-
+
+
+
+
);
}
-export default App;
+export default App;
\ No newline at end of file
diff --git a/FrontEnd/src/assets/footer/footer.css b/FrontEnd/src/assets/footer/footer.css
new file mode 100644
index 0000000..2e42414
--- /dev/null
+++ b/FrontEnd/src/assets/footer/footer.css
@@ -0,0 +1,30 @@
+.social-icon{
+ background-color: rgb(68, 66, 66) !important;
+ transition: all 0.3s ease-in-out !important;
+}
+.social-icon:hover{
+ background-color: #FF6F32 !important;
+}
+.footer-link{
+ text-decoration: none !important;
+}
+.list-links li{
+ margin-bottom: 10px;
+}
+@media screen and (min-width: 768px) {
+
+ .mobile-view{
+ display: none;
+ }
+ .desktop-view{
+ display: block;
+ }
+}
+@media screen and (max-width: 768px) {
+ .mobile-view{
+ display: block;
+ }
+ .desktop-view{
+ display: none;
+ }
+}
\ No newline at end of file
diff --git a/FrontEnd/src/assets/header/header.css b/FrontEnd/src/assets/header/header.css
new file mode 100644
index 0000000..e9a5568
--- /dev/null
+++ b/FrontEnd/src/assets/header/header.css
@@ -0,0 +1,142 @@
+.nav--link {
+ padding: 10px 20px !important;
+}
+
+.nav--link.active {
+ border-top: 3px solid #FF6F32;
+}
+
+.nav--link:not(.active) {
+ padding-top: 13px !important;
+}
+
+.logo {
+ width: 100%;
+}
+
+.navbar--brand {
+ width: 14%;
+}
+
+.dropdown-1 {
+ margin-left: 3%;
+}
+
+.dropdown-1 button {
+ border-radius: 0px;
+ color: #565353;
+}
+
+/* for SearchBar */
+.search-btn {
+ background-color: unset;
+ border: none;
+ position: absolute;
+ left: 3%;
+ top: 30%;
+}
+.search-input:focus {
+ box-shadow: none !important;
+ border-color: #FF6F32 !important;
+}
+/* for HeaderRight */
+.icon{
+ width: 20px;
+ height: 20px;
+}
+.icon-btn{
+ width: 45px;
+ height: 45px;
+}
+.create-account-btn{
+ background-color: #FFEEE8;
+ border: none;
+ color: #FF6F32;
+ font-weight: 500;
+}
+.sign-in-btn{
+ background-color: #FF6F32;
+ border: none;
+ color: white;
+ font-weight: 500;
+}
+.fade{
+ box-shadow: 0px 0px 6px #7c736f;
+}
+.dropdown-item-hover:hover{
+ background-color: #FFEEE8!important;
+ color: #FF6F32!important;
+ transition: 0.4s ease;
+}
+
+/* sumary header */
+.summary-header{
+ background-color: #f7f5f4f9;
+ padding: 15px 0px;
+}
+.mini-icon{
+ width: 20px;
+ height: 20px;
+}
+.review-course-btn{
+ background-color: white;
+ border: none;
+ color: #FF6F32;
+ font-weight: 500;
+}
+/* end sumary header */
+/* For mobile navbar */
+.offcanvas-menu{
+ width: 80%;
+ background-color: #5f5e5ee9 !important;
+}
+.nav-mobile-link{
+ padding: 10px 20px !important;
+ border-radius: 5px;
+}
+.nav-mobile-link.active{
+ background-color: #FF6F32;
+}
+/* end */
+
+@media screen and (max-width: 768px) {
+ .mobile-item-view{
+ width: 300%;
+ }
+ .desktop-item-view{
+ display: none;
+ }
+}
+@media screen and (min-width: 768px) {
+ .mobile-item-view{
+ display: none;
+ }
+ .desktop-item-view{
+ display: block;
+ }
+}
+@media screen and (min-width: 1024px) {
+ .tablet-view-nav{
+ display: none;
+ }
+ .desktop-view-nav{
+ display: block;
+ }
+ .search-input{
+ border-radius: 0px;
+ }
+}
+@media screen and (max-width: 1024px) {
+ .tablet-view-nav{
+ display: block;
+ }
+ .desktop-view-nav{
+ display: none;
+ }
+ .navbar--brand {
+ width: 25%;
+ }
+ .search-input{
+ border-radius: 20px !important;
+ }
+}
\ No newline at end of file
diff --git a/FrontEnd/src/components/common/Card/Card.css b/FrontEnd/src/components/common/Card/Card.css
new file mode 100644
index 0000000..15a1716
--- /dev/null
+++ b/FrontEnd/src/components/common/Card/Card.css
@@ -0,0 +1,501 @@
+/* Card Styles */
+.orange-gradient {
+ background: linear-gradient(90deg, #ff7a00 0%, #ff9a00 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.card-container {
+ width: 100%;
+ max-width: 312px;
+ min-height: 388px;
+ height: auto;
+ background: #fff;
+ border-radius: 16px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
+}
+
+.card-container:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.12);
+}
+
+.card-image {
+ width: 100%;
+ height: 234px;
+ background-size: cover;
+ background-position: center;
+ position: relative;
+ overflow: hidden;
+}
+
+.card-image::after {
+ content: "";
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 40%;
+ background: linear-gradient(transparent, rgba(0, 0, 0, 0.1));
+}
+
+.divider {
+ width: 100%;
+ height: 1px;
+ background: linear-gradient(90deg, transparent 0%,
+ rgba(255, 122, 0, 0.3) 50%, transparent 100%);
+}
+
+.custom-card-body {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ padding: 8px 20px !important;
+ min-height: 120px;
+ height: 180px;
+}
+
+.custom-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 8px;
+}
+
+.card-category {
+ display: inline-block;
+ background: linear-gradient(90deg, #fff4e6 0%, #ffe7d7 100%);
+ color: #ff7a00;
+ font-size: 12px;
+ font-weight: 600;
+ /* border-radius: 16px; */
+ padding: 4px 12px;
+ letter-spacing: 0.5px;
+ text-transform: uppercase;
+ box-shadow: 0 2px 4px rgba(255, 122, 0, 0.1);
+}
+
+.card-price {
+ color: #ff7a00;
+ font-size: 20px;
+ font-weight: 700;
+ text-shadow: 0 2px 4px rgba(255, 122, 0, 0.1);
+}
+
+.card-title {
+ font-size: 16px;
+ font-weight: 500;
+ color: #1d2026;
+ margin: 16px 0;
+ line-height: 1.4;
+ text-align: left;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: normal;
+ transition: color 0.2s ease;
+}
+
+/* .title {
+ font-size: 18px;
+ font-weight: 700;
+ color: #1d2026;
+ margin: 16px 0;
+ line-height: 1.4;
+ text-align: left;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: normal;
+ transition: color 0.2s ease;
+} */
+
+.card-container:hover .card-title {
+ color: #ff7a00;
+}
+
+.card-footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ /* This will push items to opposite sides */
+ gap: 24px;
+ margin-top: auto;
+ padding: 16px 0 8px 0;
+ border-top: 1px dashed rgba(123, 123, 123, 0.2);
+}
+
+.rating {
+ display: flex;
+ align-items: center;
+ font-size: 16px;
+ color: #ffb400;
+ font-weight: 600;
+ gap: 4px;
+}
+
+.students {
+ display: flex;
+ align-items: center;
+ font-size: 16px;
+ color: #7b7b7b;
+ font-weight: 500;
+ gap: 4px;
+ margin-left: auto;
+ /* This will push it to the right */
+}
+
+/* Detailed Card Styles */
+.detailed-card-container {
+ width: 400px;
+ background: #fff;
+ border-radius: 20px;
+ box-shadow: 0 12px 48px rgba(0, 0, 0, 0.12);
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ transition: transform 0.3s ease;
+}
+
+.detailed-card-container:hover {
+ transform: translateY(-4px);
+}
+
+.detailed-card-image {
+ width: 100%;
+ height: 240px;
+ background-size: cover;
+ background-position: center;
+ position: relative;
+}
+
+.detailed-card-image::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(135deg,
+ rgba(255, 122, 0, 0.1) 0%,
+ rgba(255, 122, 0, 0) 100%);
+}
+
+.detailed-card-body {
+ display: flex;
+ flex-direction: column;
+ padding: 24px;
+ text-align: left;
+}
+
+.detailed-title {
+ font-size: 22px;
+ font-weight: 700;
+ color: #1d2026;
+ margin: 0 0 16px 0;
+ line-height: 1.3;
+ position: relative;
+ padding-bottom: 12px;
+}
+
+.detailed-title::after {
+ content: "";
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 48px;
+ height: 3px;
+ background: linear-gradient(90deg, #ff7a00 0%, #ff9a00 100%);
+ border-radius: 3px;
+}
+
+.author-row {
+ display: flex;
+ align-items: center;
+ margin-bottom: 20px;
+ justify-content: space-between;
+ width: 100%;
+}
+
+.author-info {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.author-avatar {
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ background-size: cover;
+ background-position: center;
+ border: 2px solid #fff4e6;
+ box-shadow: 0 2px 8px rgba(255, 122, 0, 0.1);
+}
+
+.author-name {
+ display: flex;
+ flex-direction: column;
+ font-size: 14px;
+ color: #6e7a8a;
+}
+
+.author-name strong {
+ color: #1d2026;
+ font-weight: 600;
+}
+
+.rating-info {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ color: #ffb400;
+ font-size: 14px;
+ font-weight: 600;
+ margin-left: auto;
+}
+
+.rating-info span span {
+ margin-left: 2px;
+}
+
+.stats-row {
+ display: flex;
+ align-items: center;
+ gap: 24px;
+ margin-bottom: 20px;
+}
+
+.stat-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 14px;
+ color: #6e7a8a;
+}
+
+.stat-item svg {
+ width: 16px;
+ height: 16px;
+}
+
+/* Icon color changes */
+/* Student icon (blue) */
+.stats-row .stat-item:nth-child(1) svg path {
+ stroke: #3b82f6;
+}
+
+.stats-row .stat-item:nth-child(1) svg circle {
+ stroke: #3b82f6;
+}
+
+/* Level icon (red) */
+.stats-row .stat-item:nth-child(2) svg path {
+ stroke: #ef4444;
+}
+
+/* Clock icon (green) */
+.stats-row .stat-item:nth-child(3) svg path {
+ stroke: #22c55e;
+}
+
+.stats-row .stat-item:nth-child(3) svg circle {
+ stroke: #22c55e;
+}
+
+.price-row {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ margin-bottom: 24px;
+ padding: 12px 0;
+ border-top: 1px dashed rgba(110, 122, 138, 0.2);
+ border-bottom: 1px dashed rgba(110, 122, 138, 0.2);
+}
+
+.current-price {
+ color: #ff7a00;
+ font-size: 28px;
+ font-weight: 700;
+}
+
+.old-price {
+ color: #6e7a8a;
+ font-size: 16px;
+ text-decoration: line-through;
+}
+
+.discount {
+ background: linear-gradient(90deg, #ffe7d7 0%, #fff4e6 100%);
+ color: #ff7a00;
+ padding: 4px 12px;
+ border-radius: 20px;
+ font-size: 14px;
+ font-weight: 700;
+ box-shadow: 0 2px 4px rgba(255, 122, 0, 0.1);
+}
+
+.learn-list {
+ padding-left: 0;
+ margin: 16px 0 24px 0;
+ list-style: none;
+}
+
+.learn-item {
+ font-size: 14px;
+ color: #6e7a8a;
+ margin-bottom: 12px;
+ line-height: 1.4;
+ position: relative;
+ padding-left: 20px;
+ display: flex;
+ align-items: flex-start;
+}
+
+.learn-item::before {
+ display: none;
+}
+
+.learn-item::after {
+ content: "";
+ position: absolute;
+ left: 0;
+ top: 5px;
+ width: 8px;
+ height: 5px;
+ border: solid #22c55e;
+ border-width: 0 0 2px 2px;
+ transform: rotate(-45deg);
+}
+
+.card-button {
+ width: 100%;
+ padding: 14px;
+ background: linear-gradient(90deg, #ff7a00 0%, #ff9a00 100%);
+ color: white;
+ border: none;
+ border-radius: 12px;
+ font-size: 16px;
+ font-weight: 600;
+ margin-top: 8px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ box-shadow: 0 4px 12px rgba(255, 122, 0, 0.3);
+}
+
+.card-button:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 6px 16px rgba(255, 122, 0, 0.4);
+}
+
+.card-button:active {
+ transform: translateY(0);
+}
+
+.card-detail-button {
+ width: 100%;
+ padding: 14px;
+ background: white;
+ color: #ff7a00;
+ border: 1px solid #ff7a00;
+ border-radius: 12px;
+ font-size: 16px;
+ font-weight: 600;
+ margin-top: 12px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.card-detail-button:hover {
+ background: rgba(255, 122, 0, 0.05);
+}
+
+/* PopupCard Styles */
+.card-wrapper {
+ position: relative;
+ /* change fixed width */
+ /* width: 312px; */
+ width: 100%;
+ height: 388px;
+ display: inline-block;
+}
+
+.popup-wrapper {
+ position: absolute;
+ left: calc(100% + 8px);
+ top: 0;
+ transform: translateY(0);
+ z-index: 1000;
+ transition: all 0.2s cubic-bezier(0.2, 0, 0.1, 1);
+ filter: drop-shadow(0 8px 30px rgba(0, 0, 0, 0.15));
+}
+
+.popup-wrapper.visible {
+ opacity: 1;
+ pointer-events: auto;
+ transform: translateY(0) translateX(0);
+}
+
+.popup-wrapper.hidden {
+ opacity: 0;
+ pointer-events: none;
+ transform: translateY(0) translateX(-10px);
+}
+
+.popup-wrapper::before {
+ content: "";
+ position: absolute;
+ right: 100%;
+ top: 0;
+ width: 12px;
+ height: 100%;
+}
+
+.pointer {
+ position: absolute;
+ left: -8px;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 0;
+ height: 0;
+ border-top: 8px solid transparent;
+ border-bottom: 8px solid transparent;
+ border-right: 8px solid white;
+ z-index: 1001;
+ filter: drop-shadow(-2px 0 2px rgba(0, 0, 0, 0.05));
+}
+
+.detailed-title+div {
+ margin-top: 24px;
+ font-size: 14px;
+ font-weight: 600;
+ color: #1d2026;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.popup-wrapper.right {
+ left: calc(100% + 8px);
+ right: auto;
+}
+
+.popup-wrapper.left {
+ right: calc(100% + 8px);
+ left: auto;
+}
+
+.popup-wrapper.left .pointer {
+ left: auto;
+ right: -8px;
+ border-right: none;
+ border-left: 8px solid white;
+}
\ No newline at end of file
diff --git a/FrontEnd/src/components/common/Card/Card.js b/FrontEnd/src/components/common/Card/Card.js
new file mode 100644
index 0000000..bf73c49
--- /dev/null
+++ b/FrontEnd/src/components/common/Card/Card.js
@@ -0,0 +1,183 @@
+import React from "react";
+import "./Card.css";
+
+// SVG Components
+export const StarIcon = () => (
+
+
+
+);
+
+export const UserIcon = () => (
+
+
+
+
+
+
+);
+
+export const ClockIcon = () => (
+
+
+
+
+);
+
+export const LevelIcon = () => (
+
+
+
+
+
+);
+
+const Card = ({ image, category, price, title, rating, students }) => (
+
+
+
+
+
+
{category}
+
+ {price}
+
+
+
{title}
+
+
+
+ {rating}
+
+
+
+ {students}
+
+
+
+
+);
+
+export const DetailedCard = ({
+ title,
+ author,
+ authorAvatar,
+ rating,
+ ratingCount,
+ students,
+ level,
+ duration,
+ price,
+ oldPrice,
+ discount,
+ learnList,
+}) => (
+
+
+
{title}
+
+
+
+
+
+ Course by
+ {author}
+
+
+
+
+ {rating}
+ ({ratingCount})
+
+
+
+
+
+
+ {students} students
+
+
+
+
+ {level}
+
+
+
+
+ {duration}
+
+
+
+
+
+ {price}
+
+
{oldPrice}
+
{discount}
+
+
+
+
+ What you'll learn
+
+
+ {learnList.map((item, index) => (
+
+ {item}
+
+ ))}
+
+
+
+
Add To Cart
+
Course Detail
+
+
+);
+
+export default Card;
diff --git a/FrontEnd/src/components/common/Card/PopupCard.js b/FrontEnd/src/components/common/Card/PopupCard.js
new file mode 100644
index 0000000..6d28ed6
--- /dev/null
+++ b/FrontEnd/src/components/common/Card/PopupCard.js
@@ -0,0 +1,91 @@
+import React, { useState, useRef, useEffect } from "react";
+import Card, { DetailedCard } from "./Card";
+import "./Card.css";
+
+const PopupCard = ({ cardProps, detailedProps, hoverDelay = 200 }) => {
+ const [showPopup, setShowPopup] = useState(false);
+ const [popupPosition, setPopupPosition] = useState("right");
+ const timerRef = useRef(null);
+ const wrapperRef = useRef(null);
+ const cardRef = useRef(null);
+
+ const handleMouseEnter = () => {
+ timerRef.current = setTimeout(() => {
+ // Xác định vị trí card
+ if (cardRef.current) {
+ const rect = cardRef.current.getBoundingClientRect();
+ const spaceRight = window.innerWidth - rect.right;
+ const spaceLeft = rect.left;
+ // Giả sử popup rộng 400px, margin 8px
+ if (spaceRight < 420 && spaceLeft > 420) {
+ setPopupPosition("left");
+ } else {
+ setPopupPosition("right");
+ }
+ }
+ setShowPopup(true);
+ }, hoverDelay);
+ };
+
+ const handleMouseLeave = (e) => {
+ // Check if we're moving to the popup or its children
+ const relatedTarget = e.relatedTarget;
+ if (
+ !relatedTarget ||
+ !(relatedTarget instanceof Node) ||
+ !wrapperRef.current ||
+ !cardRef.current
+ ) {
+ clearTimer();
+ setShowPopup(false);
+ return;
+ }
+
+ const isMovingToPopup =
+ wrapperRef.current.contains(relatedTarget) ||
+ relatedTarget === wrapperRef.current;
+
+ const isMovingToCard =
+ cardRef.current.contains(relatedTarget) ||
+ relatedTarget === cardRef.current;
+
+ if (!isMovingToPopup && !isMovingToCard) {
+ clearTimer();
+ setShowPopup(false);
+ }
+ };
+
+ const clearTimer = () => {
+ if (timerRef.current) {
+ clearTimeout(timerRef.current);
+ timerRef.current = null;
+ }
+ };
+
+ useEffect(() => {
+ return () => {
+ clearTimer();
+ };
+ }, []);
+
+ return (
+
+ );
+};
+
+export default PopupCard;
diff --git a/FrontEnd/src/components/common/CustomButton/CustomButton.jsx b/FrontEnd/src/components/common/CustomButton/CustomButton.jsx
new file mode 100644
index 0000000..2f8a0e9
--- /dev/null
+++ b/FrontEnd/src/components/common/CustomButton/CustomButton.jsx
@@ -0,0 +1,61 @@
+import React from "react";
+import PropTypes from "prop-types";
+import "./CustomButton.scss";
+
+const CustomButton = ({
+ size = "medium", //small, medium, large
+ color = "primary", // primary, secondary, success,gray, error, warning
+ disabled = false, // true, false
+ iconUrl = null, // URL of the icon
+ iconPosition = "left", // left, right
+ filter = false, // true, false
+ filterCount = 0, // number of items in the filter
+ type = "normal", // normal, underline, square, circle
+ isTransparent = false, // true, false
+ onClick = () => {},
+ children,
+}) => {
+ const className = `btn ${size} ${color} ${filter ? "filter" : ""} ${type} ${
+ isTransparent ? "transparent" : ""
+ }`;
+
+ return (
+
+ {(type === "normal" || type === "underline") && (
+
+ {iconUrl && iconPosition === "left" && (
+
+ )}
+ {children}
+ {iconUrl && iconPosition === "right" && (
+
+ )}
+
+ )}
+
+ {(type === "square" || type === "circle") && iconUrl && (
+
+ )}
+
+ {filter && filterCount > 0 && (
+ {filterCount}
+ )}
+
+ );
+};
+
+CustomButton.propTypes = {
+ size: PropTypes.oneOf(["small", "medium", "large"]),
+ color: PropTypes.string,
+ disabled: PropTypes.bool,
+ iconUrl: PropTypes.string,
+ iconPosition: PropTypes.oneOf(["left", "right"]),
+ filter: PropTypes.bool,
+ filterCount: PropTypes.number,
+ type: PropTypes.oneOf(["normal", "underline", "square", "circle"]),
+ isTransparent: PropTypes.bool,
+ onClick: PropTypes.func,
+ children: PropTypes.node,
+};
+
+export default CustomButton;
diff --git a/FrontEnd/src/components/common/CustomButton/CustomButton.scss b/FrontEnd/src/components/common/CustomButton/CustomButton.scss
new file mode 100644
index 0000000..8907e3e
--- /dev/null
+++ b/FrontEnd/src/components/common/CustomButton/CustomButton.scss
@@ -0,0 +1,251 @@
+@use "sass:color";
+
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 500;
+ border: none;
+ border-radius: 2px;
+ padding: 0.5rem 1rem;
+ cursor: pointer;
+ transition: background-color 0.3s, opacity 0.3s;
+ position: relative;
+
+ &.filter {
+ backdrop-filter: blur(5px);
+ border: 1.5px solid #ff6636;
+ background-color: white;
+ color: #ff6636;
+ font-weight: 600;
+ }
+
+ &.large {
+ font-size: 1.125rem;
+ padding: 0.75rem 1.5rem;
+ }
+
+ &.medium {
+ font-size: 1rem;
+ padding: 0.5rem 1rem;
+ }
+
+ &.small {
+ font-size: 0.875rem;
+ padding: 0.25rem 0.75rem;
+ }
+
+ // Color Variants
+ &.primary {
+ --underline-color: #ff6636;
+ --main-color-rgb: 255, 102, 54;
+ background-color: #ff6636;
+ color: white;
+
+ &:hover:not(:disabled) {
+ background-color: color.adjust(#ff6636, $lightness: -10%);
+ }
+ }
+
+ &.secondary {
+ --underline-color: #564ffd;
+ --main-color-rgb: 86, 79, 253;
+ background-color: #564ffd;
+ color: white;
+
+ &:hover:not(:disabled) {
+ background-color: color.adjust(#564ffd, $lightness: -10%);
+ }
+ }
+
+ &.grey {
+ --underline-color: #1d2026;
+ --main-color-rgb: 29, 32, 38;
+ background-color: #1d2026;
+ color: white;
+
+ &:hover:not(:disabled) {
+ background-color: #363b47;
+ }
+ }
+
+ &.success {
+ --underline-color: #23bd33;
+ --main-color-rgb: 35, 189, 51;
+ background-color: #23bd33;
+ color: white;
+
+ &:hover:not(:disabled) {
+ background-color: color.adjust(#23bd33, $lightness: -10%);
+ }
+ }
+
+ &.warning {
+ --underline-color: #fd8e1f;
+ --main-color-rgb: 253, 142, 31;
+ background-color: #fd8e1f;
+ color: white;
+
+ &:hover:not(:disabled) {
+ background-color: color.adjust(#fd8e1f, $lightness: -10%);
+ }
+ }
+
+ &.error {
+ --underline-color: #e34444;
+ --main-color-rgb: 227, 68, 68;
+ background-color: #e34444;
+ color: white;
+
+ &:hover:not(:disabled) {
+ background-color: color.adjust(#e34444, $lightness: -10%);
+ }
+ }
+
+ &:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+
+ .btn-text {
+ display: inline-block;
+ }
+
+ .icon {
+ width: 18px;
+ height: 18px;
+ object-fit: contain;
+ color: inherit;
+
+ &.left {
+ margin-right: 0.5rem;
+ }
+
+ &.right {
+ margin-left: 0.5rem;
+ }
+
+ &.only-icon {
+ margin: 0;
+ }
+ }
+
+ .filter-badge {
+ background-color: #ff6636;
+ color: white;
+ font-size: 0.75rem;
+ font-weight: bold;
+ border-radius: 4px;
+ padding: 2px 6px;
+ margin-left: 8px;
+ }
+
+ &.underline {
+ background-color: transparent;
+ border: none;
+ color: var(--underline-color, #ff6636);
+ padding: 0.5rem;
+ position: relative;
+
+ &:hover {
+ background-color: transparent !important;
+ }
+
+ .btn-content {
+ display: inline-flex;
+ align-items: center;
+ position: relative;
+
+ &::after {
+ content: "";
+ position: absolute;
+ bottom: -2px;
+ left: 0;
+ width: 100%;
+ height: 2px;
+ background-color: var(--underline-color, #ff6636);
+ transform: scaleX(0);
+ transform-origin: left;
+ transition: transform 0.3s ease;
+ }
+ }
+
+ &:hover .btn-content::after {
+ transform: scaleX(1);
+ }
+ }
+
+ &.transparent {
+ background-color: transparent;
+ color: var(--underline-color, #ff6636);
+ border: none;
+
+ &:hover:not(:disabled) {
+ background-color: rgba(var(--main-color-rgb, 255, 102, 54), 0.15);
+ }
+
+ .icon {
+ filter: none;
+ color: inherit;
+
+ .btn:hover .only-icon {
+ filter: brightness(0) invert(1);
+ }
+ }
+
+ &.underline {
+ .btn-content::after {
+ background-color: var(--underline-color, #ff6636);
+ }
+
+ &:hover {
+ background-color: transparent !important;
+ }
+ }
+ }
+
+ // Square and circle - transparent version with transparent text & icon
+ &.square.transparent,
+ &.circle.transparent {
+ background-color: rgba(var(--main-color-rgb, 255, 102, 54), 0);
+ color: transparent;
+ box-shadow: none;
+ padding: 0.5rem;
+
+ .icon.only-icon {
+ width: 16px;
+ height: 16px;
+ filter: none;
+ color: transparent;
+ }
+
+ &:hover:not(:disabled) {
+ background-color: rgba(var(--main-color-rgb, 255, 102, 54), 0.3);
+ }
+ }
+
+ // Square and circle - solid version
+ &.square:not(.transparent),
+ &.circle:not(.transparent) {
+ background-color: rgba(var(--main-color-rgb), 0.2);
+ color: white;
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
+ padding: 0.5rem;
+ }
+
+ &.square {
+ border-radius: 4px;
+ }
+
+ &.circle {
+ border-radius: 50%;
+ }
+
+ // Fix: Properly nest hover icon invert filter
+ &:not(.transparent):not(.underline):hover {
+ .icon.only-icon {
+ filter: brightness(0) invert(1);
+ transition: filter 0.3s ease;
+ }
+ }
+}
diff --git a/FrontEnd/src/components/common/Input.jsx b/FrontEnd/src/components/common/Input.jsx
new file mode 100644
index 0000000..73a5740
--- /dev/null
+++ b/FrontEnd/src/components/common/Input.jsx
@@ -0,0 +1,234 @@
+import React, { useState, useRef, useEffect } from "react";
+import "bootstrap/dist/css/bootstrap.min.css";
+import "./input.css";
+
+const Input = ({
+ text = "Input",
+ variant,
+ icon,
+ counter,
+ maxCount,
+ rightIcon,
+ price,
+ currency,
+ placeholder,
+ options,
+ success,
+ error,
+ type = "text",
+ textarea,
+ value,
+ onChange,
+ className,
+ ...props
+}) => {
+ const [selectedText, setSelectedText] = useState(text);
+ const [showDropdown, setShowDropdown] = useState(false);
+ const dropdownRef = useRef(null);
+
+ useEffect(() => {
+ const handleClickOutside = (e) => {
+ if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
+ setShowDropdown(false);
+ }
+ };
+ document.addEventListener("mousedown", handleClickOutside);
+ return () => document.removeEventListener("mousedown", handleClickOutside);
+ }, []);
+
+ const handleSelect = (option) => {
+ setSelectedText(option);
+ setShowDropdown(false);
+ };
+
+ const renderInput = () => {
+ if (textarea) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+ };
+
+ if (!variant) {
+ return
{renderInput()}
;
+ }
+
+ if (variant === "label") {
+ return (
+
+ {text}
+ {renderInput()}
+
+ );
+ }
+
+ if (variant === "counter") {
+ return (
+
+
{text}
+
+
+
+ {counter}/{maxCount}
+
+
+
+ );
+ }
+
+ if (variant === "iconDivider") {
+ return (
+
+ );
+ }
+
+ if (variant === "priceInput") {
+ return (
+
+ );
+ }
+
+ if (variant === "iconInside") {
+ return (
+
+ {rightIcon && text &&
{text} }
+
+ {icon && (
+
+ )}
+
+ {rightIcon && (
+
+ )}
+
+
+ );
+ }
+
+ if (variant === "success" || variant === "error") {
+ const iconClass =
+ variant === "success"
+ ? "fas fa-check-circle text-success"
+ : "fas fa-exclamation-triangle text-danger";
+ const boxClass = variant === "success" ? "input-box-success" : "input-box-error";
+
+ return (
+
+ );
+ }
+
+ if (variant === "dropdown") {
+ return (
+
+
+
setShowDropdown(!showDropdown)}
+ >
+ {selectedText}
+
+ {options && showDropdown && (
+
+ {options.map((option, index) => (
+
+ handleSelect(option)}
+ >
+ {option}
+
+
+ ))}
+
+ )}
+
+
+ );
+ }
+
+ return (
+
+ {text}
+ {renderInput()}
+
+ );
+};
+
+export default Input;
diff --git a/FrontEnd/src/components/common/input.css b/FrontEnd/src/components/common/input.css
new file mode 100644
index 0000000..0548f9a
--- /dev/null
+++ b/FrontEnd/src/components/common/input.css
@@ -0,0 +1,85 @@
+.form-control:focus-within {
+ border-color: #86b7fe;
+ box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
+}
+
+.counter-text {
+ font-size: 0.875rem;
+}
+
+.input-price,
+.input-icon-divider {
+ height: 2.5rem;
+}
+
+.input-icon-divider {
+ gap: 0.5rem;
+}
+
+.icon-box {
+ min-width: 3rem;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.input-icon-left,
+.input-icon-right,
+.input-icon-right-feedback {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ font-size: 1rem;
+ color: #6c757d;
+ pointer-events: none;
+}
+
+.input-icon-left {
+ left: 1rem;
+}
+
+.input-icon-right,
+.input-icon-right-feedback {
+ right: 1rem;
+}
+
+.input-box-success {
+ background-color: rgba(25, 135, 84, 0.1);
+ border-color: #198754;
+}
+
+.input-box-error {
+ background-color: rgba(220, 53, 69, 0.1);
+ border-color: #dc3545;
+}
+
+.outline-none {
+ outline: none !important;
+}
+
+/* Giá & tiền tệ */
+.input-price {
+ white-space: nowrap;
+ overflow: hidden;
+ min-width: 0;
+}
+
+.input-price input {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.currency-label {
+ flex-shrink: 0;
+ white-space: nowrap;
+}
+
+/* Responsive cho tablet */
+@media (min-width: 768px) and (max-width: 1023px) {
+ .input-price {
+ font-size: 14px;
+ }
+}
diff --git a/FrontEnd/src/components/common/search/CategoryButton/CategoryButton.js b/FrontEnd/src/components/common/search/CategoryButton/CategoryButton.js
new file mode 100644
index 0000000..79e75cc
--- /dev/null
+++ b/FrontEnd/src/components/common/search/CategoryButton/CategoryButton.js
@@ -0,0 +1,32 @@
+import React from 'react';
+import styles from './CategoryButton.module.css';
+
+const ChevronDownIcon = () => (
+
+
+
+);
+
+const CategoryButton = ({ label = "Browse", onClick }) => {
+ return (
+
+ {label}
+
+
+ );
+};
+
+export default CategoryButton;
\ No newline at end of file
diff --git a/FrontEnd/src/components/common/search/CategoryButton/CategoryButton.module.css b/FrontEnd/src/components/common/search/CategoryButton/CategoryButton.module.css
new file mode 100644
index 0000000..be98c18
--- /dev/null
+++ b/FrontEnd/src/components/common/search/CategoryButton/CategoryButton.module.css
@@ -0,0 +1,20 @@
+.categoryButton {
+ display: flex;
+ align-items: center;
+ padding: 0 16px; /* Vertical padding will be matched by SearchInputWrapper */
+ background-color: transparent; /* Background will be on parent */
+ border: none;
+ cursor: pointer;
+ font-size: 14px;
+ color: #333; /* Text color for "Browse" */
+ white-space: nowrap;
+ height: 100%; /* Fill height of parent */
+}
+
+.categoryButton:hover {
+ background-color: #f8f8f8; /* Slight hover effect */
+}
+
+.categoryButton svg {
+ color: #777; /* Arrow color */
+}
\ No newline at end of file
diff --git a/FrontEnd/src/components/common/search/DropdownItem/DropdownItem.js b/FrontEnd/src/components/common/search/DropdownItem/DropdownItem.js
new file mode 100644
index 0000000..e859a94
--- /dev/null
+++ b/FrontEnd/src/components/common/search/DropdownItem/DropdownItem.js
@@ -0,0 +1,12 @@
+import React from 'react';
+import styles from './DropdownItem.module.css';
+
+const DropdownItem = ({ item, onClick }) => {
+ return (
+
onClick(item)}>
+ {item.label}
+
+ );
+};
+
+export default DropdownItem;
\ No newline at end of file
diff --git a/FrontEnd/src/components/common/search/DropdownItem/DropdownItem.module.css b/FrontEnd/src/components/common/search/DropdownItem/DropdownItem.module.css
new file mode 100644
index 0000000..8e82a61
--- /dev/null
+++ b/FrontEnd/src/components/common/search/DropdownItem/DropdownItem.module.css
@@ -0,0 +1,9 @@
+.dropdownItem {
+ padding: 10px 15px;
+ cursor: pointer;
+ list-style: none;
+}
+
+.dropdownItem:hover {
+ background-color: #f0f0f0;
+}
\ No newline at end of file
diff --git a/BackEnd/.env b/FrontEnd/src/components/common/search/DropdownList/DropdownList.js
similarity index 100%
rename from BackEnd/.env
rename to FrontEnd/src/components/common/search/DropdownList/DropdownList.js
diff --git a/FrontEnd/src/components/common/search/DropdownList/DropdownList.module.css b/FrontEnd/src/components/common/search/DropdownList/DropdownList.module.css
new file mode 100644
index 0000000..8e82a61
--- /dev/null
+++ b/FrontEnd/src/components/common/search/DropdownList/DropdownList.module.css
@@ -0,0 +1,9 @@
+.dropdownItem {
+ padding: 10px 15px;
+ cursor: pointer;
+ list-style: none;
+}
+
+.dropdownItem:hover {
+ background-color: #f0f0f0;
+}
\ No newline at end of file
diff --git a/FrontEnd/src/components/common/search/Input/Input.js b/FrontEnd/src/components/common/search/Input/Input.js
new file mode 100644
index 0000000..cd572d7
--- /dev/null
+++ b/FrontEnd/src/components/common/search/Input/Input.js
@@ -0,0 +1,19 @@
+import React from 'react';
+import styles from './Input.module.css';
+
+const Input = ({ value, onChange, onFocus, onBlur, placeholder, ...props }) => {
+ return (
+
+ );
+};
+
+export default Input;
\ No newline at end of file
diff --git a/FrontEnd/src/components/common/search/Input/Input.module.css b/FrontEnd/src/components/common/search/Input/Input.module.css
new file mode 100644
index 0000000..e44acf9
--- /dev/null
+++ b/FrontEnd/src/components/common/search/Input/Input.module.css
@@ -0,0 +1,14 @@
+.input {
+ border: none;
+ outline: none;
+ font-size: 14px; /* Match "Browse" or typical input size */
+ width: 100%;
+ padding: 0; /* Remove padding, parent containers will handle it */
+ background-color: transparent;
+ height: 100%; /* Fill height of container */
+ color: #333;
+}
+
+.input::placeholder {
+ color: #999; /* Lighter placeholder */
+}
\ No newline at end of file
diff --git a/FrontEnd/src/components/common/search/SearchBox/SearchBox.js b/FrontEnd/src/components/common/search/SearchBox/SearchBox.js
new file mode 100644
index 0000000..b47796e
--- /dev/null
+++ b/FrontEnd/src/components/common/search/SearchBox/SearchBox.js
@@ -0,0 +1,81 @@
+import React, { useState, useEffect, useRef } from 'react';
+import SearchInput from '../SearchInput/SearchInput';
+import DropdownList from '../DropdownList/DropdownList';
+import styles from './SearchBox.module.css';
+
+const SearchBox = ({
+ data = [],
+ placeholder = "Search...",
+ onSelect,
+ onCategoryClick, // New prop
+ categoryLabel = "Browse", // New prop
+ containerClassName // For custom styling from parent
+}) => {
+ const [searchTerm, setSearchTerm] = useState('');
+ const [filteredData, setFilteredData] = useState([]);
+ const [isDropdownVisible, setIsDropdownVisible] = useState(false);
+ const searchBoxRef = useRef(null);
+
+ useEffect(() => {
+ if (searchTerm) {
+ const results = data.filter((item) =>
+ item.label.toLowerCase().includes(searchTerm.toLowerCase())
+ );
+ setFilteredData(results);
+ setIsDropdownVisible(results.length > 0);
+ } else {
+ setFilteredData([]);
+ setIsDropdownVisible(false);
+ }
+ }, [searchTerm, data]);
+
+ useEffect(() => {
+ const handleClickOutside = (event) => {
+ if (searchBoxRef.current && !searchBoxRef.current.contains(event.target)) {
+ setIsDropdownVisible(false);
+ }
+ };
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, []);
+
+ const handleInputChange = (event) => {
+ setSearchTerm(event.target.value);
+ };
+
+ const handleInputFocus = () => {
+ if (searchTerm && filteredData.length > 0) {
+ setIsDropdownVisible(true);
+ }
+ };
+
+ const handleItemClick = (item) => {
+ setSearchTerm(item.label);
+ setIsDropdownVisible(false);
+ if (onSelect) {
+ onSelect(item);
+ }
+ };
+
+ return (
+
+
+
+
+ );
+};
+
+export default SearchBox;
\ No newline at end of file
diff --git a/FrontEnd/src/components/common/search/SearchBox/SearchBox.module.css b/FrontEnd/src/components/common/search/SearchBox/SearchBox.module.css
new file mode 100644
index 0000000..08d7a01
--- /dev/null
+++ b/FrontEnd/src/components/common/search/SearchBox/SearchBox.module.css
@@ -0,0 +1,5 @@
+.searchBoxContainer {
+ position: relative;
+ width: 100%; /* Take full width of its navbar cell */
+ max-width: 500px; /* Optional: prevent it from becoming too wide */
+}
\ No newline at end of file
diff --git a/FrontEnd/src/components/common/search/SearchInput/SearchInput.js b/FrontEnd/src/components/common/search/SearchInput/SearchInput.js
new file mode 100644
index 0000000..fdd4229
--- /dev/null
+++ b/FrontEnd/src/components/common/search/SearchInput/SearchInput.js
@@ -0,0 +1,52 @@
+import React from 'react';
+import Input from '../Input/Input';
+import CategoryButton from '../CategoryButton/CategoryButton';
+import styles from './SearchInput.module.css';
+
+// Simple SVG Search Icon
+const SearchIcon = () => (
+
+
+
+
+);
+
+const SearchInput = ({
+ value,
+ onChange,
+ onFocus,
+ onBlur,
+ placeholder,
+ onCategoryClick,
+ categoryLabel = "Browse",
+}) => {
+ return (
+
+ );
+};
+
+export default SearchInput;
\ No newline at end of file
diff --git a/FrontEnd/src/components/common/search/SearchInput/SearchInput.module.css b/FrontEnd/src/components/common/search/SearchInput/SearchInput.module.css
new file mode 100644
index 0000000..4d1dc36
--- /dev/null
+++ b/FrontEnd/src/components/common/search/SearchInput/SearchInput.module.css
@@ -0,0 +1,37 @@
+.searchInputWrapper {
+ display: flex;
+ align-items: center; /* Vertically align items */
+ border: 1px solid #e0e0e0;
+ border-radius: 6px; /* Slightly less rounded corners */
+ background-color: #fff;
+ width: 100%;
+ box-sizing: border-box;
+ overflow: hidden; /* To make sure children respect border-radius */
+ height: 44px; /* Fixed height like typical navbar inputs */
+}
+
+.searchInputWrapper:focus-within {
+ border-color: #ff7f50; /* Example: Orange focus color, change as needed */
+ box-shadow: 0 0 0 1px rgba(255, 127, 80, 0.3);
+}
+
+.separator {
+ width: 1px;
+ background-color: #e0e0e0;
+ align-self: stretch;
+ margin: 8px 0; /* Vertical margin for the separator */
+}
+
+.inputFieldContainer {
+ display: flex;
+ align-items: center;
+ flex-grow: 1;
+ padding: 0 12px; /* Padding for icon and space before input text */
+ height: 100%;
+}
+
+.icon {
+ margin-right: 10px;
+ color: #888;
+ flex-shrink: 0;
+}
\ No newline at end of file
diff --git a/FrontEnd/src/components/footer/DownloadApp.jsx b/FrontEnd/src/components/footer/DownloadApp.jsx
new file mode 100644
index 0000000..ed472f3
--- /dev/null
+++ b/FrontEnd/src/components/footer/DownloadApp.jsx
@@ -0,0 +1,36 @@
+import React from 'react'
+import { Link } from 'react-router-dom'
+
+function DownloadApp() {
+ return (
+ <>
+
+
DOWNLOAD OUR APP
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+export default DownloadApp
\ No newline at end of file
diff --git a/FrontEnd/src/components/footer/Footer.jsx b/FrontEnd/src/components/footer/Footer.jsx
new file mode 100644
index 0000000..05ec10b
--- /dev/null
+++ b/FrontEnd/src/components/footer/Footer.jsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import '../../assets/footer/footer.css';
+import QuickLink from './QuickLink';
+import DownloadApp from './DownloadApp';
+import LogoLeft from './LogoLeft';
+import QuickLinkMobile from './QuickLinkMobile';
+
+function Footer() {
+ return (
+
+ );
+}
+
+export default Footer;
\ No newline at end of file
diff --git a/FrontEnd/src/components/footer/LogoLeft.jsx b/FrontEnd/src/components/footer/LogoLeft.jsx
new file mode 100644
index 0000000..e9a8b23
--- /dev/null
+++ b/FrontEnd/src/components/footer/LogoLeft.jsx
@@ -0,0 +1,37 @@
+import React from 'react'
+import { Link } from 'react-router-dom';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faFacebookF, faInstagram, faLinkedinIn, faTwitter, faYoutube } from '@fortawesome/free-brands-svg-icons';
+function LogoLeft() {
+ return (
+ <>
+
+
+
+
+
+ Flearning. Empowering your future, one course at a time.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+export default LogoLeft
\ No newline at end of file
diff --git a/FrontEnd/src/components/footer/QuickLink.jsx b/FrontEnd/src/components/footer/QuickLink.jsx
new file mode 100644
index 0000000..3cef52c
--- /dev/null
+++ b/FrontEnd/src/components/footer/QuickLink.jsx
@@ -0,0 +1,41 @@
+import React from 'react'
+import { Link } from 'react-router-dom'
+
+function QuickLink() {
+ return (
+ <>
+
+
+
TOP 4 CATEGORY
+
+ Development
+ Finance & Accounting
+ Design
+ Business
+
+
+ {/* Quick Links */}
+
+
QUICK LINKS
+
+ About
+ Become Instructor
+ Contact
+ Career
+
+
+ {/* Support */}
+
+
SUPPORT
+
+ Help Center
+ FAQs
+ Terms & Condition
+ Privacy Policy
+
+
+ >
+ )
+}
+
+export default QuickLink
\ No newline at end of file
diff --git a/FrontEnd/src/components/footer/QuickLinkMobile.jsx b/FrontEnd/src/components/footer/QuickLinkMobile.jsx
new file mode 100644
index 0000000..9b9cb6d
--- /dev/null
+++ b/FrontEnd/src/components/footer/QuickLinkMobile.jsx
@@ -0,0 +1,65 @@
+import React from 'react'
+import { Link } from 'react-router-dom'
+function QuickLinkMobile() {
+ return (
+ <>
+
+
+
+
+
+
+
+ About
+ Become Instructor
+ Contact
+ Career
+
+
+
+
+
+
+
+
+
+ Help Center
+ FAQs
+ Terms & Condition
+ Privacy Policy
+
+
+
+
+
+
+ >
+ )
+}
+
+export default QuickLinkMobile
\ No newline at end of file
diff --git a/FrontEnd/src/components/header/Browse.jsx b/FrontEnd/src/components/header/Browse.jsx
new file mode 100644
index 0000000..f55f24e
--- /dev/null
+++ b/FrontEnd/src/components/header/Browse.jsx
@@ -0,0 +1,24 @@
+import React from 'react'
+import { Link } from 'react-router-dom'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faChevronDown } from '@fortawesome/free-solid-svg-icons'
+function Browse() {
+ return (
+ <>
+
+
+ Browse
+
+
+
+ All Courses
+ Popular
+ New
+
+
+ >
+ )
+}
+
+export default Browse
\ No newline at end of file
diff --git a/FrontEnd/src/components/header/Header.jsx b/FrontEnd/src/components/header/Header.jsx
new file mode 100644
index 0000000..61bea6f
--- /dev/null
+++ b/FrontEnd/src/components/header/Header.jsx
@@ -0,0 +1,61 @@
+import React from 'react';
+import '../../assets/header/header.css';
+import SearchBar from './SearchBar';
+import HeaderRight from './HeaderRight';
+import LogoHeader from './LogoHeader';
+import Browse from './Browse';
+import MobileHeader from './MobileHeader';
+import NavigationBar from './NavigationBar';
+
+
+function Header() {
+ const userExample = {//NOTE: This is sample data, replace API's data here
+ username: 'admin',
+ password: '123',
+ userImage: '/images/defaultImageUser.png'
+ }
+ return (
+ <>
+
+ {/* for desktop */}
+
+ {/*
+
+ */}
+
+ {/* when already login */}
+ {/*
Sau khi login */}
+
+
+
+
+ {/*
Navbar cho tablet (nhỏ hơn 1024) */}
+ {/* for mobile */}
+
+ >
+ )
+}
+
+export default Header
diff --git a/FrontEnd/src/components/header/HeaderRight.jsx b/FrontEnd/src/components/header/HeaderRight.jsx
new file mode 100644
index 0000000..fbe30b1
--- /dev/null
+++ b/FrontEnd/src/components/header/HeaderRight.jsx
@@ -0,0 +1,54 @@
+import { faBell, faHeart, faUser } from '@fortawesome/free-regular-svg-icons'
+import { faCog, faShoppingCart, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import React from 'react'
+import { Link } from 'react-router-dom'
+
+function HeaderRight({ user }) {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ {user ? (
+
+
+
+ Profile
+ Settings
+
+ Logout
+
+
+ ) : (
+
+
+ Create account
+
+
+ Sign in
+
+
+ )}
+
+ >
+ )
+}
+
+export default HeaderRight
\ No newline at end of file
diff --git a/FrontEnd/src/components/header/LogoHeader.jsx b/FrontEnd/src/components/header/LogoHeader.jsx
new file mode 100644
index 0000000..b09109d
--- /dev/null
+++ b/FrontEnd/src/components/header/LogoHeader.jsx
@@ -0,0 +1,13 @@
+import React from 'react'
+import { Link } from 'react-router-dom'
+function LogoHeader() {
+ return (
+ <>
+
+
+
+ >
+ )
+}
+
+export default LogoHeader
\ No newline at end of file
diff --git a/FrontEnd/src/components/header/MobileHeader.jsx b/FrontEnd/src/components/header/MobileHeader.jsx
new file mode 100644
index 0000000..2deb004
--- /dev/null
+++ b/FrontEnd/src/components/header/MobileHeader.jsx
@@ -0,0 +1,73 @@
+import React from 'react'
+import { NavLink } from 'react-router-dom';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faBook, faChalkboardTeacher, faChevronDown, faHome, faPhoneSquareAlt, faQuestionCircle } from '@fortawesome/free-solid-svg-icons';
+import SearchBar from './SearchBar';
+import HeaderRight from './HeaderRight';
+import LogoHeader from './LogoHeader';
+function MobileHeader({user}) {
+ return (
+ <>
+
+
+ >
+ )
+}
+
+export default MobileHeader
\ No newline at end of file
diff --git a/FrontEnd/src/components/header/NavigationBar.jsx b/FrontEnd/src/components/header/NavigationBar.jsx
new file mode 100644
index 0000000..f7e3d69
--- /dev/null
+++ b/FrontEnd/src/components/header/NavigationBar.jsx
@@ -0,0 +1,44 @@
+import React from 'react'
+import { NavLink } from 'react-router-dom';
+
+function NavigationBar() {
+ return (
+ <>
+
+
+
+
+
+
+ Home
+
+
+
+
+ Courses
+
+
+
+
+ About
+
+
+
+
+ Contact
+
+
+
+
+ Become an instructor
+
+
+
+
+
+
+ >
+ )
+}
+
+export default NavigationBar
\ No newline at end of file
diff --git a/FrontEnd/src/components/header/SearchBar.jsx b/FrontEnd/src/components/header/SearchBar.jsx
new file mode 100644
index 0000000..258064b
--- /dev/null
+++ b/FrontEnd/src/components/header/SearchBar.jsx
@@ -0,0 +1,20 @@
+import { faSearch } from '@fortawesome/free-solid-svg-icons'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import React from 'react'
+
+function SearchBar() {
+ return (
+ <>
+
+ {/* add search suggestion component here*/}
+ >
+ )
+}
+
+export default SearchBar
diff --git a/FrontEnd/src/components/sumaryCourse/SumaryHeader.jsx b/FrontEnd/src/components/sumaryCourse/SumaryHeader.jsx
new file mode 100644
index 0000000..894553e
--- /dev/null
+++ b/FrontEnd/src/components/sumaryCourse/SumaryHeader.jsx
@@ -0,0 +1,53 @@
+import { faArrowLeft } from '@fortawesome/free-solid-svg-icons'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import React from 'react'
+import { Link } from 'react-router-dom'
+import '../../assets/header/header.css';
+
+function SumaryHeader() {
+ return (
+
+
+
+
+
+
+
+
Complete Website Responsive Design: from Figma to Webflow to Website Design
+
+
+
+
6 Sections
+
+
202 lecture
+
+
19h37m
+
+
+
+
+
+
+
+
+
6 Sections
+
+
202 lecture
+
+
19h37m
+
+
+
+
+ View a Review
+
+
+ Next lecture
+
+
+
+
+ )
+}
+
+export default SumaryHeader
\ No newline at end of file
diff --git a/FrontEnd/src/index.js b/FrontEnd/src/index.js
index d563c0f..05b8f71 100644
--- a/FrontEnd/src/index.js
+++ b/FrontEnd/src/index.js
@@ -3,11 +3,16 @@ import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
+import { BrowserRouter } from 'react-router-dom';
+import 'bootstrap/dist/css/bootstrap.min.css';
+import 'bootstrap/dist/js/bootstrap.bundle.min.js'
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
-
+
+
+
);
diff --git a/FrontEnd/src/routes/TestInputComponent.jsx b/FrontEnd/src/routes/TestInputComponent.jsx
new file mode 100644
index 0000000..b11fcb7
--- /dev/null
+++ b/FrontEnd/src/routes/TestInputComponent.jsx
@@ -0,0 +1,80 @@
+import React from "react";
+import Input from "./components/common/Input"; // Adjust path as needed
+import "bootstrap/dist/css/bootstrap.min.css";
+import "bootstrap-icons/font/bootstrap-icons.css";
+import '@fortawesome/fontawesome-free/css/all.min.css';
+
+function App() {
+ return (
+
+ );
+}
+
+export default App;
\ No newline at end of file
diff --git a/BackEnd/config/a.js b/package.json
similarity index 100%
rename from BackEnd/config/a.js
rename to package.json