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 @@ - React App + Flearning diff --git a/FrontEnd/public/manifest.json b/FrontEnd/public/manifest.json index 080d6c7..558dc67 100644 --- a/FrontEnd/public/manifest.json +++ b/FrontEnd/public/manifest.json @@ -1,6 +1,7 @@ { - "short_name": "React App", - "name": "Create React App Sample", + "short_name": "Flearning", + "name": "Flearning", + "description": "A platform for learning and sharing knowledge.", "icons": [ { "src": "favicon.ico", diff --git a/FrontEnd/src/App.js b/FrontEnd/src/App.js index 3784575..59b96e2 100644 --- a/FrontEnd/src/App.js +++ b/FrontEnd/src/App.js @@ -1,25 +1,16 @@ import logo from './logo.svg'; import './App.css'; - +import Header from './components/header/Header'; +import SumaryHeader from './components/sumaryCourse/SumaryHeader'; +import Footer from './components/footer/Footer'; function App() { return ( -
-
- logo -

- Edit src/App.js and save to reload. -

- - Learn React - -
+
+
+ +
); } -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} +
  • + ))} +
+
+ + + +
+
+); + +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 ( + + ); +}; + +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 ( +