diff --git a/client/.env.sample b/client/.env.sample index cc5d577..ae7d092 100755 --- a/client/.env.sample +++ b/client/.env.sample @@ -1 +1 @@ -REACT_APP_BASE_URL=http://localhost:8001.com/ +REACT_APP_BASE_URL=http://localhost:4000/ diff --git a/client/package.json b/client/package.json index 45ac860..7875039 100644 --- a/client/package.json +++ b/client/package.json @@ -1,10 +1,14 @@ { - "name": "OpenAIframework", - "version": "0.1.0", + "name": "react", + "version": "18.3.1", "private": true, "dependencies": { "@ant-design/icons": "^5.3.7", + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", "@googleapis/docs": "^3.0.2", + "@mui/icons-material": "^6.3.0", + "@mui/material": "^6.3.0", "@react-oauth/google": "^0.12.1", "@rpldy/uploader": "^1.8.1", "@testing-library/jest-dom": "^5.16.4", @@ -17,12 +21,15 @@ "bootstrap-icons": "^1.9.1", "browserify-zlib": "^0.2.0", "dompurify": "^3.0.9", + "embla-carousel-autoplay": "^8.5.1", + "embla-carousel-react": "^8.5.1", "framer-motion": "^11.0.8", "gapi-script": "^1.2.0", "google-auth-library": "^9.11.0", "googleapis": "^140.0.1", "highlight.js": "^11.9.0", "js-cookie": "^3.0.5", + "lucide-react": "^0.468.0", "marked": "^9.1.6", "moment": "^2.29.4", "openai": "^4.36.0", @@ -37,7 +44,7 @@ "react-google-login": "^5.2.2", "react-google-picker": "^0.1.0", "react-hot-toast": "^2.4.0", - "react-icons": "^4.4.0", + "react-icons": "^4.12.0", "react-infinite-scroll-component": "^6.1.0", "react-router-dom": "^6.3.0", "react-scripts": "^5.0.1", @@ -88,5 +95,9 @@ "url": "^0.11.3", "webpack": "^5.92.1", "webpack-cli": "^5.1.4" - } + }, + "main": "index.js", + "author": "", + "license": "ISC", + "description": "" } diff --git a/client/src/App.js b/client/src/App.js index 8a621cd..13229cd 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -19,9 +19,13 @@ import { AssistantsChatPage, ChatPage, AssistantFileDownloadPage, + Form, + ProjectDetails, + Upload } from "./Pages"; - +import PortfolioHome from "./Pages/PortfolioHome"; +import PlatformManagementfeature from "./Pages/PlatformMangemenrfeature/PlatformMangamentfeature"; import Layout from "./Pages/Layout"; import AssistantLayout from "./Pages/Layout/AssistantLayout"; import ConfigurationTabs from "./Pages/configration/index"; @@ -32,12 +36,16 @@ import TrackUsage from "./Pages/SuperAdmin/TrackUsage/TrackUsage"; import TrackUsageComponent from "./Pages/SuperAdmin/TrackUsage/TrackUsageComponent"; import PublicAssistant from "./Pages/ExploreGPTs"; import ProtectedRoutes from "./component/ProtectedRoute/ProtectedRoute"; +import ProtectedRouteFeature from "./component/ProtectedRoute/ProtectedRouteFeature"; import AssistantTypeList from "./Pages/AssistantType/index"; import TaskCommands from "./Pages/SuperAdmin/TaskCommands/TaskCommands";import KnowledgeBase from "./Pages/KnowledgeBase"; import { IntegrateApplications } from "./component/IntegrateApplications/IntegrateApplications"; // import { IntegrateApplications } from "./component/Assistant/IntegrateApplications/IntegrateApplications"; import ConnectionWithWorkboard from "./Pages/configration/ConnectionWithWorkboard"; +import ClientInfo from "./Pages/PortfolioManagement/ClientInfo"; +import PodInfo from "./Pages/PortfolioManagement/PodInfo"; +import ReviewsPage from "./Pages/PortfolioManagement/Reviews"; function App() { // Hook to get the current location @@ -53,8 +61,25 @@ function App() { } /> } /> } /> - - + }> + }> + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + }/> + + + } + /> {/* For connecting workboard */} } /> @@ -72,7 +97,7 @@ function App() { } /> } /> } /> - + }> @@ -127,8 +152,9 @@ function App() { path="assistants/download/:file_id" element={} /> - + + + ); } - export default App; diff --git a/client/src/Pages/Form/index.js b/client/src/Pages/Form/index.js new file mode 100644 index 0000000..c5fe5cb --- /dev/null +++ b/client/src/Pages/Form/index.js @@ -0,0 +1,13 @@ +import React from 'react'; +import ProjectForm from '../../component/ProjectForm'; + + +const Form = () => { + return ( +
+ +
+ ); + }; + + export default Form; \ No newline at end of file diff --git a/client/src/Pages/PlatformMangemenrfeature/PlatformMangamentfeature.jsx b/client/src/Pages/PlatformMangemenrfeature/PlatformMangamentfeature.jsx new file mode 100644 index 0000000..4e5c0ca --- /dev/null +++ b/client/src/Pages/PlatformMangemenrfeature/PlatformMangamentfeature.jsx @@ -0,0 +1,15 @@ +import React from 'react'; +import NewNavbar from '../../component/NewNavbar'; + +const PlatformManagementfeature = ({ children }) => { + return ( + <> + +
+ {children} +
+ + ); +}; + +export default PlatformManagementfeature; diff --git a/client/src/Pages/PortfolioHome/index.jsx b/client/src/Pages/PortfolioHome/index.jsx new file mode 100644 index 0000000..0bfbf53 --- /dev/null +++ b/client/src/Pages/PortfolioHome/index.jsx @@ -0,0 +1,18 @@ +import React from "react"; +import { HeroBanner, ContentPage } from "../../component"; +import "./style.css"; + +const PortfolioHome = () => { + return ( +
+
+ +
+
+ +
+
+ ); +}; + +export default PortfolioHome; diff --git a/client/src/Pages/PortfolioHome/style.css b/client/src/Pages/PortfolioHome/style.css new file mode 100644 index 0000000..0d34b16 --- /dev/null +++ b/client/src/Pages/PortfolioHome/style.css @@ -0,0 +1,20 @@ +* +{ + margin:0 0; +} +.scroll-container { + height: 100vh; + scroll-snap-type: y mandatory; + transition: scroll 0.5s ease-in-out; + scroll-behavior: smooth; + width:100% +} + +.scroll-section { + display: flex; + justify-content: center; + background-color: #070707; + scroll-snap-align: start; +} + + diff --git a/client/src/Pages/PortfolioManagement/ClientInfo/ClientInfo.scss b/client/src/Pages/PortfolioManagement/ClientInfo/ClientInfo.scss new file mode 100644 index 0000000..c7b775c --- /dev/null +++ b/client/src/Pages/PortfolioManagement/ClientInfo/ClientInfo.scss @@ -0,0 +1,958 @@ +.portfolio-page { + margin-left: auto; + margin-right: auto; + padding-left: 1rem; + padding-right: 1rem; + padding-top: 7rem; + padding-bottom: 2rem; + background-color: #070707; + display: flex; + flex-direction: column; + align-items: center; +} + +.profile-section { + display: flex; + max-width: 1350px; + width: 100%; + flex-direction: column; + align-items: flex-start; + gap: 2rem; + margin-bottom: 3rem; +} + +.tabs1 { + max-width: 1350px; + width: 100%; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.profile-avatar { + width: 8rem; + height: 8rem; + font-weight: bold; + font-size: 84px; +} + +.profile-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.profile-header { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.profile-name { + font-size: 1.875rem; + line-height: 2.25rem; + font-weight: 700; + color:#E5E7EB; +} + +.profile-badge { + background-color: #DBEAFE; + color: #1D4ED8; +} + +.profile-title { + color:#E5E7EB; +} + +.profile-location { + font-size: 0.775rem; + line-height: 1.25rem; + color:#E5E7EB; + +} + +.profile-actions { + display: flex; + gap: 0.75rem; +} +.profile-stats { + display: flex; + gap: 2rem; +} + +.stat { + text-align: center; +} + +.stat-value { + font-weight: 700; +} + +.stat-label { + font-size: 0.875rem; + line-height: 1.25rem; + color: #6B7280; +} + +/* Stats Grid */ +.stats-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; + margin-bottom: 2rem; +} + +.stat-card { + background-color: black; + color: white; + border-radius:25px; +} + +.stat-header { + display: flex; + color: #a1a1aa; + gap: 0.5rem; +} + +.stat-icon { + width: 1rem; + height: 1rem; +} + +.stat-title { + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 500; +} + +.stat-value { + font-size: 1.5rem; + line-height: 2rem; + font-weight: 700; + color: white; +} + + + +.tabs-list { + display: flex; + gap: 1rem; + border-bottom: 1px solid #E5E7EB; +} + +.tabs-trigger { + padding: 0.8rem 2rem; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 900; + transition: color 0.2s; + border-radius: 10px; + margin-bottom: 20px; +} + +.tabs-trigger.active { + border-bottom: 7px solid white; + color:rgb(255, 255, 255); + background-color: #27272A; + +} + +.tabs-content { + margin-top: 1rem; +} + +.tabs-content:not(.active) { + display: none; +} + +/* Search Section */ +.search-section { + margin-bottom: 1rem; + color: white; +} + +.search-form { + display: flex; + gap: 0.5rem; + justify-content: center; + +} + +.search-input { + flex-grow: 1; + background-color: rgb(0, 0, 0); + color: white; +} + +.search-button { + background-color: rgb(0, 0, 0); + color: white; +} + +.search-icon { + width: 1rem; + height: 1rem; +} + +.search-history { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 0.5rem; +} + +.history-badge { + background-color: #E5E7EB; + color: #374151; +} + +.remove-history { + margin-left: 0.5rem; + color: #6B7280; +} + +.remove-history:hover { + color: #374151; +} + +.remove-icon { + width: 0.75rem; + height: 0.75rem; +} + +/* Projects Carousel */ +.projects-carousel { + overflow: hidden; +} + +.projects-container1 { + display: flex; + padding-bottom: 10px; +} + +.project-slide { + flex: 0 0 33.33%; + min-width: 0; + padding: 0 0.5rem; +} + +.project-card1 { + overflow: hidden; +} + +.project-image-container { + padding: 0; +} + +.project-image-wrapper { + position: relative; +} + +.project-image { + width: 100%; + aspect-ratio: 3 / 2; + object-fit: cover; + transition: transform 0.2s; +} + +.project-card1:hover .project-image { + transform: scale(1.05); +} + +.project-image-overlay { + position: absolute; + inset: 0; + background-image: linear-gradient(to top, rgba(0, 0, 0, 0.6), transparent); + opacity: 0; + transition: opacity 0.2s; +} + +.project-card1:hover .project-image-overlay { + opacity: 1; +} + +.project-info { + padding: 1rem; +} + +.project-detailss { + flex: 1; +} + +.project-titles { + font-weight: 800; + color:white; +} + +.project-category { + font-size: 0.875rem; + line-height: 1.25rem; + color: #6B7280; +} + +.project-stats { + display: flex; + align-items: center; + gap: 0.75rem; + font-size: 0.875rem; + line-height: 1.25rem; +} + +.project-stat { + display: flex; + align-items: center; + gap: 0.25rem; + color:#929292; + padding-bottom: 10px; +} + +/* Reviews Carousel */ +.reviews-carousel { + overflow: hidden; +} + +.reviews-container { + display: flex; + + margin-bottom: 1rem; + +} + +.review-slide { + flex: 0 0 33.33%; + min-width: 0; + padding: 0 0.5rem; +} + +.review-card { + height: 100%; +} + +.review-content { + padding: 1rem; + display: flex; + flex-direction: column; + gap: 2rem; +} + +.reviewer-info { + display: flex; + align-items: center; + gap: 1rem; +} + +.reviewer-avatar { + width: 2.5rem; + height: 2.5rem; +} + +.reviewer-name { + color:white; + font-weight: 600; +} + +.review-headline { + font-size: 0.875rem; + line-height: 1.25rem; + color: #999ca0; +} + +.review-comment { + font-size: 0.875rem; + line-height: 1.25rem; + color: #D1D5DB; +} + +.review-rating { + display: flex; + align-items: center; +} + +.star-icon { + width: 2rem; + height: 3rem; +} + +.star-icon.filled { + color: #FBBF24; +} + +.star-icon.empty { + color: #D1D5DB; +} + +/* Milestones Carousel */ +.milestones-carousel { + overflow: hidden; +} + +.milestones-container { + display: flex; + margin-bottom: 1rem; +} + +.milestone-slide { + flex: 0 0 33.33%; + min-width: 0; + padding: 0 0.5rem; +} + +.milestone-card { + height: 100%; +} + +.milestone-content { + padding: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.milestone-title { + font-weight: 600; + color:white; +} + +.milestone-description { + font-size: 0.875rem; + line-height: 1.25rem; + color: #bcbcbc; +} + +.milestone-status { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.status-completed { + background-color: #D1FAE5; + color: #047857; +} + + +.status-pending { + background-color: yellow; + color: #000306; +} + +.milestone-owner { + font-size: 0.75rem; + line-height: 1rem; + color: #6B7280; +} + +/* Responsive Design */ +@media (min-width: 768px) { + .profile-section { + flex-direction: row; + } + + .stats-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } +} + +.button1 { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 0.375rem; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 500; + padding: 0.5rem 1rem; + transition: background-color 0.2s, color 0.2s; +} + +.button1:focus-visible { + outline: 2px solid transparent; + outline-offset: 2px; + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5); +} + +.button1:disabled { + opacity: 0.5; + pointer-events: none; +} + +.cards { + border-radius: 0.5rem; + border: 1px solid #E5E7EB; + background-color: white; + color: #1F2937; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); +} +.cardss { + + border-radius: 0.5rem; + border: 1px solid #E5E7EB; + background-color: rgba(255, 255, 255, 0.1); + color: #1F2937; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); +} + +.card-content { + padding: 1.5rem; +} + +.card-footer { + display: flex; + align-items: center; + padding: 1.5rem; + padding-top: 0; +} + +.input { + display: flex; + height: 2.5rem; + width: 100%; + border-radius: 0.375rem; + border: 1px solid #D1D5DB; + background-color: transparent; + padding: 0.5rem 0.75rem; + font-size: 0.875rem; + line-height: 1.25rem; +} + +.input:focus-visible { + outline: 2px solid transparent; + outline-offset: 2px; + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5); +} + +.input:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.avatar { + position: relative; + display: flex; + height: 150px; + width: 150px; + flex-shrink: 0; + overflow: hidden; + border-radius: 9999px; +} +.avatars { + position: relative; + display: flex; + height: 4.5rem; + width: 4.5rem; + flex-shrink: 0; + overflow: hidden; + border-radius: 9999px; +} + +.avatar-image { + aspect-ratio: 1 / 1; + height: 100%; + width: 100%; +} + +.avatar-fallback { + display: flex; + height: 100%; + width: 100%; + align-items: center; + justify-content: center; + background-color: #F3F4F6; +} + + + + + + + + + +/* Overall container styles */ +.technical-stack { + padding: 20px; + color: white; /* Make "Tech Stack:" text white */ + font-size: 16px; + font-weight: bold; + display: flex; + align-items: center; + flex-wrap: wrap; +} + +/* Container for the icons */ +.tech-stack-icons { + display: flex; + gap: 15px; /* Space between tech items */ + margin-left: 10px; /* Add a little space between "Tech Stack:" and icons */ +} + +/* Each tech stack item styled like a button */ +.tech-item { + background-color: white; + color: black; + padding: 8px 16px; + border: 1px solid #ccc; + border-radius: 5px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +/* Hover effect for tech items */ +.tech-item:hover { + transform: scale(1.05); + box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .technical-stack { + flex-direction: column; + align-items: flex-start; + } + + .tech-stack-icons { + margin-top: 10px; + } +} +.description { + + color: #c8cbce; + margin: 0 0 1rem 0; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + + + + + + + + +.tech-stack-title { + font-size: 18px; + font-weight: bold; + color: white; + margin-bottom: 10px; +} + +/* Tech stack items styled as buttons */ +.tech-stack-icons { + display: flex; + flex-wrap: wrap; + gap: 15px; +} + +.tech-item { + background-color: white; + color: black; + padding: 8px 16px; + border-radius: 5px; + font-size: 14px; + font-weight: 500; + border: 1px solid #ccc; + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; +} + +.tech-item:hover { + transform: scale(1.05); + box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); +} + +.tabs-trigger:hover { + background-color: #000000; + color: white; +} + +.buttons.milestones:hover { + background-color: #28a745; + color: white; +} + +.buttons.reviews:hover { + background-color: #dc3545; + color: white; +} + +/* Mobile styles (default) */ +.profile-section { + flex-direction: column; + align-items: center; +} + +.stats-grid { + grid-template-columns: repeat(2, 1fr); +} + +.project-slide, +.review-slide, +.milestone-slide { + flex: 0 0 100%; +} + +/* Tablet styles */ +@media (min-width: 870px) and (max-width: 1230px) { + .profile-section { + flex-direction: row; + align-items: flex-start; + } +} + +@media (min-width: 768px) and (max-width: 1024px) { + .stats-grid { + grid-template-columns: repeat(2, 1fr); + } + + .project-slide, + .review-slide, + .milestone-slide { + flex: 0 0 50%; + } +} + +/* Desktop styles */ +@media (min-width: 1225px) { + .profile-section { + flex-direction: row; + align-items: flex-start; + } + .stats-grid { + grid-template-columns: repeat(4, 1fr); + } +} +@media (min-width: 1024px) { + + + .project-slide, + .review-slide, + .milestone-slide { + flex: 0 0 33.33%; + } +} + + +/* Responsive font sizes */ +@media (max-width: 767px) { + .profile-name { + font-size: 1.5rem; + } + + .stat-value { + font-size: 1.25rem; + } +} + + + button-container { + padding: 0.5rem 1rem; + background-color: #f3f4f6; + border: none; + border-radius: 0.375rem; + font-size: 0.875rem; + cursor: pointer; + transition: background-color 0.3s ease; + color:black; +} + +button-container:hover:not(:disabled) { + background-color: #e5e7eb; +} + +button-container:disabled { + opacity: 0.5; + cursor: not-allowed; +} +/* Ensure images and cards don't overflow */ +.project-image, +.cards, +.cardss { + max-width: 100%; + height: auto; +} +/* Avatar Styles */ +.w-32 { + width: 8rem; +} + +.h-32 { + height: 8rem; +} + +.shrink-0 { + flex-shrink: 0; +} + +/* Button Styles */ +.inline-flex { + display: inline-flex; +} + +.shadow-sm { + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.focus-visible\:outline-none:focus-visible { + outline: 2px solid transparent; + outline-offset: 2px; +} + +.focus-visible\:ring-2:focus-visible { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.focus-visible\:ring-ring:focus-visible { + --tw-ring-color: hsl(var(--ring)); +} + +.focus-visible\:ring-offset-2:focus-visible { + --tw-ring-offset-width: 2px; +} + +.disabled\:opacity-50:disabled { + opacity: 0.5; +} + +.disabled\:pointer-events-none:disabled { + pointer-events: none; +} + +/* Tab Styles */ +.border-b { + border-bottom-width: 1px; +} + +.border-b-2 { + border-bottom-width: 2px; +} + +.border-primary { + --tw-border-opacity: 1; + border-color: hsl(var(--primary) / var(--tw-border-opacity)); +} + +/* Input Styles */ +.ring-offset-background { + --tw-ring-offset-color: hsl(var(--background)); +} + +.file\:border-0::file-selector-button { + border-width: 0px; +} + +.file\:bg-transparent::file-selector-button { + background-color: transparent; +} + +.file\:text-sm::file-selector-button { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.file\:font-medium::file-selector-button { + font-weight: 500; +} + +.placeholder\:text-muted-foreground::placeholder { + color: hsl(var(--muted-foreground)); +} + +/* Card Styles */ +.shadow-sm { + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +/* Embla Carousel Styles */ +.embla { + overflow: hidden; +} + +.embla__container { + display: flex; +} + +.embla__slide { + flex: 0 0 100%; + min-width: 0; +} + +/* Star Rating Styles */ +.text-yellow-400 { + --tw-text-opacity: 1; + color: rgb(250 204 21 / var(--tw-text-opacity)); +} + +.text-gray-300 { + --tw-text-opacity: 1; + color: rgb(209 213 219 / var(--tw-text-opacity)); +} + +/* Milestone Status Styles */ +.bg-green-100 { + --tw-bg-opacity: 1; + background-color: rgb(220 252 231 / var(--tw-bg-opacity)); +} + +.text-green-700 { + --tw-text-opacity: 1; + color: rgb(21 128 61 / var(--tw-text-opacity)); +} + +/* Additional Utility Classes */ +.aspect-square { + aspect-ratio: 1 / 1; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +.pointer-events-none { + pointer-events: none; +} + + + +.whitespace-nowrap { + white-space: nowrap; +} + +.break-words { + overflow-wrap: break-word; +} + +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.info-boxs { + background-color: #27272a; + border-radius: 8px; + padding: 16px; + display: flex; + align-items: flex-start; + gap: 16px; + border:1px solid #888; + width:100%; +} diff --git a/client/src/Pages/PortfolioManagement/ClientInfo/index.jsx b/client/src/Pages/PortfolioManagement/ClientInfo/index.jsx new file mode 100644 index 0000000..c2e13dc --- /dev/null +++ b/client/src/Pages/PortfolioManagement/ClientInfo/index.jsx @@ -0,0 +1,402 @@ + +import React, { useState, useEffect, useCallback } from "react"; +import { FaCode, FaDollarSign, FaFileAlt, FaBullseye } from "react-icons/fa"; +import useEmblaCarousel from 'embla-carousel-react'; +import Autoplay from 'embla-carousel-autoplay'; +import Badge from '../../../component/ClientInfo/Badge'; +import { CardContent } from '../../../component/ClientInfo/Cards'; +import { Avatar } from '../../../component/ClientInfo/Avatar'; +import { Tabs, TabsList, TabsTrigger, TabsContent } from '../../../component/ClientInfo/Tabs'; +import ReviewCard from '../../../component/ClientInfo/ReviewCard'; +import SearchBar from '../../../component/ClientInfo/SearchBar'; +import MilestoneCard from '../../../component/ClientInfo/MilestoneCard'; +import ProjectCard from '../../../component/ProjectCard' +import { getClientInfo,getAllRevenueData,getTechStackById } from '../../../api/projectApi'; +import { getAllProjects } from "../../../api/projectApi"; +import "./ClientInfo.scss"; +import { useParams } from 'react-router-dom'; +const reviews = [ + { id: 1, headline: "Amazing Project!", reviewer: "John Doe", comment: "Very well executed.", rating: 5, reviewerImage: "https://picsum.photos/50" }, + { id: 2, headline: "Great Work", reviewer: "Jane Smith", comment: "Great design and functionality!", rating: 4, reviewerImage: "/placeholder.svg?height=50&width=50" }, + { id: 3, headline: "Highly Recommended", reviewer: "Michael Lee", comment: "Fantastic work!", rating: 5, reviewerImage: "/placeholder.svg?height=50&width=50" }, + { id: 4, headline: "Outstanding Design", reviewer: "Emily Chen", comment: "Exceeded my expectations!", rating: 5, reviewerImage: "/placeholder.svg?height=50&width=50" }, + { id: 5, headline: "Efficient and Reliable", reviewer: "David Wang", comment: "Great communication throughout the project.", rating: 4, reviewerImage: "/placeholder.svg?height=50&width=50" }, + { id: 6, headline: "Impressive Results", reviewer: "Sarah Johnson", comment: "Delivered on time and with high quality.", rating: 5, reviewerImage: "/placeholder.svg?height=50&width=50" }, +]; + +const milestones = [ + { id: 1, title: "Project Kickoff", description: "Initial planning and requirements gathering", status: [{ title: "Completed", owner: "Team", completed: true }] }, + { id: 2, title: "Design Phase", description: "Creating wireframes and mockups", status: [{ title: "In Progress", owner: "Designer", completed: false }] }, + { id: 3, title: "Development Sprint 1", description: "Core functionality implementation", status: [{ title: "Pending", owner: "Developers", completed: false }] }, + { id: 4, title: "User Testing", description: "Gathering feedback from beta users", status: [{ title: "Not Started", owner: "QA Team", completed: false }] }, + { id: 5, title: "Final Delivery", description: "Project handover and documentation", status: [{ title: "Not Started", owner: "Project Manager", completed: false }] }, + { id: 6, title: "Post-Launch Support", description: "Monitoring and bug fixes", status: [{ title: "Not Started", owner: "Support Team", completed: false }] }, +]; + +const ClientInfo = () => { + const { id } = useParams(); + const [activeTab, setActiveTab] = useState("work"); + const [searchQuery, setSearchQuery] = useState(""); + const [clientInfo, setClientInfo] = useState(null); + const [projects, setProjects] = useState([]); + const [filteredProjects, setFilteredProjects] = useState([]); + const [techStack, setTechStack] = useState([]); + const [revenue, setRevenue] = useState({ total: 0, invoicesCount: 0 }); + const [loading, setLoading] = useState(true); + const [, setSearchHistory] = useState([]); + const [currentProjectPage, setCurrentProjectPage] = useState(1); + const [currentReviewPage, setCurrentReviewPage] = useState(1); + const [currentMilestonePage, setCurrentMilestonePage] = useState(1); + const [, setClientId] = useState(null); + + + + // Fetch client data + const fetchClientData = async () => { + try { + const clientData = await getClientInfo(id); + setClientId(clientData._id); // Assuming the API returns a `_id` + setClientInfo(clientData); + return clientData._id; + } catch (error) { + console.error("Error fetching client info:", error); + throw error; + } + }; + + // Fetch projects for the client + const fetchProjects = async (id) => { + try { + const allProjects = await getAllProjects(); + const filtered = allProjects.filter( + (project) => project.client_id === id + ); + setProjects(filtered); + setFilteredProjects(filtered); + } catch (error) { + console.error("Error fetching projects:", error); + throw error; + } + }; + + + const fetchRevenueAndBenefits = async (clientID) => { + try { + const allRevenue = await getAllRevenueData(); + + // Collect project IDs for the client + const projectIds = new Set(filteredProjects.map((project) => project._id)); + + // Calculate total revenue and benefits + const { totalRevenue, totalBenefits } = allRevenue.reduce( + (acc, revenueItem) => { + if (projectIds.has(revenueItem.project_id)) { + acc.totalRevenue += revenueItem.revenue || 0; + acc.totalBenefits += revenueItem.benefits?.reduce((a, b) => a + b, 0) || 0; + } + return acc; + }, + { totalRevenue: 0, totalBenefits: 0 } + ); + + setRevenue({ + total: totalRevenue, + invoicesCount: totalBenefits , + }); + } catch (error) { + console.error("Error fetching revenue and benefits:", error); + } + }; + + const fetchTechStackDetails = async (techStackIds) => { + try { + const uniqueIds = Array.from(new Set(techStackIds)); // Deduplicate IDs + const techStackPromises = uniqueIds.map((id) => getTechStackById(id)); + const techStackDetails = await Promise.all(techStackPromises); + + // Deduplicate fetched details by a unique property (e.g., `id`) + const uniqueTechStack = Array.from( + new Map(techStackDetails.map((tech) => [tech.id, tech])).values() + ); + + setTechStack(uniqueTechStack); // Save deduplicated details in state + } catch (error) { + console.error("Error fetching tech stack details:", error); + } + }; + + + + useEffect(() => { + const fetchData = async () => { + setLoading(true); + try { + const [clientData, allProjects, allRevenue] = await Promise.all([ + getClientInfo(id), + getAllProjects(), + getAllRevenueData(), + ]); + + setClientInfo(clientData); + const clientProjects = allProjects.filter(project => project.client_id._id === clientData._id); + + setProjects(clientProjects); + setFilteredProjects(clientProjects); + + const projectIds = new Set(clientProjects.map(project => project._id)); + const { totalRevenue, totalBenefits } = allRevenue.reduce( + (acc, revenueItem) => { + if (projectIds.has(revenueItem.project_id)) { + acc.totalRevenue += revenueItem.revenue || 0; + acc.totalBenefits += revenueItem.benefits?.reduce((a, b) => a + b, 0) || 0; + } + return acc; + }, + { totalRevenue: 0, totalBenefits: 0 } + ); + + setRevenue({ total: totalRevenue, invoicesCount: totalBenefits }); + + const allTechStackIds = clientProjects.flatMap(project => project.techStack); + const uniqueTechStackIds = Array.from(new Set(allTechStackIds)); + const techStackDetails = await Promise.all( + uniqueTechStackIds.map(techId => getTechStackById(techId)) + ); + + // Deduplicate tech stack details by a unique property (e.g., `id`) + const uniqueTechStackDetails = Array.from( + new Map(techStackDetails.map(tech => [tech._id, tech])).values() + ); + + setTechStack(uniqueTechStackDetails); + } catch (error) { + console.error("Error during data fetch:", error); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [id]); + + + const [projectsEmblaRef] = useEmblaCarousel({ loop: true }, [Autoplay()]); + const [reviewsEmblaRef] = useEmblaCarousel({ loop: true }, [Autoplay()]); + const [milestonesEmblaRef] = useEmblaCarousel({ loop: true }, [Autoplay()]); + + if (loading) { + return
Loading...
; + } + + if (!clientInfo) { + return
No client information found.
; + } + const getInitials = (name) => { + const words = name.split(' '); + return words.length === 1 + ? words[0][0].toUpperCase() + : `${words[0][0]}${words[1][0]}`.toUpperCase(); + }; + + const stats = [ + { title: "Projects", value: projects.length, icon: FaCode }, + { title: "Total Revenue", value: revenue.total, prefix: "$", icon: FaDollarSign }, + { title: "Total Benefits", value: revenue.invoicesCount, icon: FaFileAlt }, + { title: "Milestones", value: milestones.length, icon: FaBullseye }, + ]; + + const itemsPerPage = 3; + const projectPageCount = Math.ceil(filteredProjects.length / itemsPerPage); + const reviewPageCount = Math.ceil(reviews.length / itemsPerPage); + const milestonePageCount = Math.ceil(milestones.length / itemsPerPage); + + const currentProjects = filteredProjects.slice( + (currentProjectPage - 1) * itemsPerPage, + currentProjectPage * itemsPerPage + ); + + const currentReviews = reviews.slice( + (currentReviewPage - 1) * itemsPerPage, + currentReviewPage * itemsPerPage + ); + + const currentMilestones = milestones.slice( + (currentMilestonePage - 1) * itemsPerPage, + currentMilestonePage * itemsPerPage + ); + + + return ( +
+
+ {clientInfo.image ? ( + + ) : ( +
+ {getInitials(clientInfo.name)} +
+ )} + + +
+
+

{clientInfo.name}

+ Client +
+

Point of Contact : {clientInfo.point_of_contact}

+

{clientInfo.description}

+

{clientInfo.contact_info}

+
+ +
+
+ {stats.map(({ title, value, prefix, icon: Icon }) => ( +
+ +
+ +

{title}

+
+

+ {prefix}{value.toLocaleString()} +

+
+
+ ))} +
+
+

Tech Stack:

+
+ {techStack.map((tech, index) => ( + + {tech.name} {/* Assuming `name` is the property you want to display */} + + ))} +
+
+ +
+
+ + + + setActiveTab("work")} + > + Projects + + {/* + setActiveTab("reviews")} +> + Reviews + + setActiveTab("milestones")} +> + Milestones + +*/} + + + + + +
+
+ {currentProjects.length > 0 ? ( + currentProjects.map((project) => ( +
+ +
+ )) + ) : ( +
+

No projects found

+
+ )} +
+
+ setCurrentProjectPage(prev => Math.max(prev - 1, 1))} + disabled={currentProjectPage === 1} + > + Previous + + {currentProjectPage} of {projectPageCount} + setCurrentProjectPage(prev => Math.min(prev + 1, projectPageCount))} + disabled={currentProjectPage === projectPageCount} + > + Next + +
+
+
+ + +
+
+ {currentReviews.map((review) => ( +
+ +
+ ))} +
+
+ setCurrentReviewPage(prev => Math.max(prev - 1, 1))} + disabled={currentReviewPage === 1} + > + Previous + + {currentReviewPage} of {reviewPageCount} + setCurrentReviewPage(prev => Math.min(prev + 1, reviewPageCount))} + disabled={currentReviewPage === reviewPageCount} + > + Next + +
+
+
+ + +
+
+ {currentMilestones.map((milestone) => ( +
+ +
+ ))} +
+
+ setCurrentMilestonePage(prev => Math.max(prev - 1, 1))} + disabled={currentMilestonePage === 1} + > + Previous + + {currentMilestonePage} of {milestonePageCount} + setCurrentMilestonePage(prev => Math.min(prev + 1, milestonePageCount))} + disabled={currentMilestonePage === milestonePageCount} + > + Next + +
+
+
+
+
+ ); +} + +export default ClientInfo; + diff --git a/client/src/Pages/PortfolioManagement/PodInfo/Index.jsx b/client/src/Pages/PortfolioManagement/PodInfo/Index.jsx new file mode 100644 index 0000000..81d042d --- /dev/null +++ b/client/src/Pages/PortfolioManagement/PodInfo/Index.jsx @@ -0,0 +1,106 @@ +import { getAllTeams } from '../../../api/projectApi.js'; +import React, { useState, useEffect } from "react"; +import { FaSearch } from "react-icons/fa"; +import { useParams, useNavigate } from 'react-router-dom'; +import { TeamModal } from '../../../component/PodInfo/TeamModal.jsx'; +import { PodCard } from '../../../component/PodInfo/PodCard.jsx'; +import './PodInfo.scss' + +const PodInfo = () => { + const [teams, setTeams] = useState([]); + const [selectedTeamId, setSelectedTeamId] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); + const { id } = useParams(); // Get the id from URL parameters + const navigate = useNavigate(); + + useEffect(() => { + fetchTeams(); + // If there's an id in the URL, open the modal for that team + if (id) { + setSelectedTeamId(id); + setIsModalOpen(true); + } + }, [id]); + + const fetchTeams = async () => { + try { + const fetchedTeams = await getAllTeams(); + setTeams(fetchedTeams); + } catch (error) { + console.error('Error fetching teams:', error); + } + }; + + const handleTeamClick = (teamId) => { + setSelectedTeamId(teamId); + setIsModalOpen(true); + // Update the URL when a team is clicked + navigate(`/platform-management-feature/Pod/${teamId}`); + }; + + const handleSearchChange = (event) => { + setSearchTerm(event.target.value); + }; + + const handleCloseModal = () => { + setIsModalOpen(false); + setSelectedTeamId(null); + // Remove the team ID from the URL when closing the modal + navigate('/platform-management-feature/PodInfo'); + }; + + const filteredTeams = teams.filter(team => + team.teamTitle.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + return ( + +
+
+

+ Team Pods Overview +

+

+ Cross-functional teams working together to achieve our goals +

+
+
+ + +
+
+
+ {filteredTeams.length > 0 ? ( + filteredTeams.map((team) => ( + handleTeamClick(team._id)} + /> + )) + ) : ( +
No pod team found
+ )} +
+
+ + {isModalOpen && selectedTeamId && ( + + )} +
+ + ); +}; + +export default PodInfo; + diff --git a/client/src/Pages/PortfolioManagement/PodInfo/PodInfo.scss b/client/src/Pages/PortfolioManagement/PodInfo/PodInfo.scss new file mode 100644 index 0000000..7c9703d --- /dev/null +++ b/client/src/Pages/PortfolioManagement/PodInfo/PodInfo.scss @@ -0,0 +1,1068 @@ +.teamPage { + margin-left: auto; + margin-right: auto; + padding-left: 1rem; + padding-right: 1rem; + padding-top: 7rem; + padding-bottom: 2rem; + background-color: #070707 !important; + color: #18181b !important; + min-height: 100vh; +} + +.container1 { + max-width: 1200px; + margin: 0 auto; + text-align: center; + padding: 0 1rem; + height:100%; + +} + +.title { + font-size: 2.5rem; + font-weight: 700; + margin-bottom: 1rem; + color: white; +} + +.subtitle { + font-size: 1.125rem; + color: #bcc1c9; + max-width: 36rem; + margin: 0 auto 4rem; +} + +.podGrid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1.5rem; + margin-bottom: 2rem; +} + +@media (max-width: 1024px) { + .podGrid { + grid-template-columns: repeat(2, 1fr); + } + +} + +@media (max-width: 640px) { + .podGrid { + grid-template-columns: 1fr; + } + .searchAndFilters { + flex-direction: column; + } +} + +.podCard { + flex: 0 0 300px; + background: rgba(255, 255, 255, 0.1); + border-radius: 16px; + border: 2px rgb(71 68 68) solid; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + padding: 1.25rem; + cursor: pointer; + transition: all 0.2s ease; + + width: 100%; + height: 100%; +} + +.podCard:hover { + transform: translateY(-2px); + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); +} + +.podHeader { + display: flex; + align-items: flex-start; + gap: 1rem; + margin-bottom: 1rem; +} + +.podAvatar { + width: 2.5rem; + height: 2.5rem; + border-radius: 0.5rem; + background-color: #e5e7eb; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + color: #4b5563; +} + +.podInfo { + flex: 1; +} + +.podName { + font-size: 1rem; + font-weight: 600; + color: #ffffff; + margin-bottom: 0.25rem; +} + +.podDescription { + font-size: 0.875rem; + color: #bcbcbc; +} + +.moreButton { + background: none; + border: none; + color: #6b7280; + cursor: pointer; + padding: 0.25rem; +} + +.podTime { + font-size: 0.875rem; + color: #6b7280; + margin-bottom: 1rem; +} + +.podStats { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.memberCount { + font-size: 0.875rem; + color: #6b7280; +} + +.statNumber { + color: #d4d4d4; +} + +.statDivider { + margin: 0 0.5rem; + color: #9ca3af; +} + +.memberAvatars { + display: flex; + align-items: center; + margin-left: 0.5rem; +} + +.smallAvatar { + width: 1.5rem; + height: 1.5rem; + border-radius: 9999px; + background-color: #e5e7eb; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + font-weight: bold; + color: #4b5563; + border: 2px solid white; + margin-left: -0.5rem; +} + +.smallAvatar:first-child { + margin-left: 0; +} + +.moreMembers { + width: 1.5rem; + height: 1.5rem; + background-color: #f3f4f6; + border-radius: 9999px; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + color: #6b7280; + margin-left: -0.5rem; + border: 2px solid white; +} + +.viewDetails { + width: 100%; + padding: 0.5rem; + background-color: #f9fafb; + border: 1px solid #e5e7eb; + border-radius: 0.375rem; + color: #374151; + font-size: 0.875rem; + font-weight: 500; + transition: all 0.2s ease; +} + +.viewDetails:hover { + background-color: #f3f4f6; +} + + +.imageContainer { + height: 200px; + overflow: hidden; + position: relative; +} + +.image { + width: 100%; + height: 100%; + object-fit: cover; +} + +.activeIndicator { + position: absolute; + top: 10px; + right: 10px; + width: 12px; + height: 12px; + border-radius: 50%; + background-color: #9ca3af; +} + +.activeIndicator.active { + background-color: #10b981; + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0% { + box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); + } + 70% { + box-shadow: 0 0 0 10px rgba(16, 185, 129, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); + } +} + +.cardContent { + padding: 1rem; + display: flex; + flex-direction: column; + justify-content: space-between; + flex-grow: 1; +} + +.name { + + font-weight: 300; + color: #dedede; + margin: 0 0 1rem 0; +} + +.jobTitle { + font-size: 0.875rem; + color: #9d9fa2; + margin: 0 0 1rem 0; +} + + +.designation { + font-size: 0.875rem; + color: #bebebe; + margin: 0 0 1rem 0; +} + +.description { + + color: #c8cbce; + margin: 0 0 1rem 0; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.skills { + display: flex; + flex-wrap: wrap; + gap: 0.25rem; +} + +.skill { + font-size: 0.800rem; + color: #4b5563; + background-color: black; + padding: 0.25rem 0.25rem; + border-radius: 9999px; +} + +.projects { + font-size: 0.875rem; + color: #4b5563; +} + +.footer { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; +} + +.icons { + width: 2.5rem; + height: 2.5rem; + background-color: rgb(0, 0, 0); + border-radius: 9999px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1rem; + margin-right: 0.5rem; +} + +.footerTitle { + font-size: 1.5rem; + font-weight: 600; + color: #111827; +} + +.footerText { + font-size: 1rem; + color: #6b7280; + max-width: 36rem; +} + +.modalOverlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + padding: 1rem; /* Update: Added padding to modalOverlay */ +} + +.modal1 { + margin-top: 6rem; + background-color: #070707; + border-radius: 0.5rem; + width: 100%; + max-width: 1280px; + max-height: 90vh; + overflow-y: auto; + box-shadow: 1px 1px 1px 1px rgb(103 87 87), 1px 1px 1px 2px rgb(103 88 88); + display: flex; + flex-direction: column; +} + +.modalHeader { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 1.5rem; + border-bottom: 1px solid #333; +} + +.modalHeader h2 { + font-size: 1.5rem; + font-weight: 600; + color: #fcfcfc; + margin: 0; +} + +.closeButton { + background: none; + border: none; + font-size: 1.5rem; + color: #ffffff; + cursor: pointer; +} + +.modalContent { + padding: 1.5rem; + flex-grow: 1; + overflow-y: auto; +} + +.teamMembers { + display: flex; + flex-wrap: wrap; + + margin-bottom: 1.5rem; +} + +.teamMembers.listView { + flex-direction: column; +} + +.searchAndFilters { + display: flex; + gap: 1rem; + margin-bottom: 1.5rem; +} + +.searchWrapper { + position: relative; + flex: 1; + display: flex; + align-items: center; +} + +.searchWrapper svg { + + left: 0.75rem; + top: 50%; + +} + + +.searchInput { + width: 100%; + padding: 0.5rem 1rem 0.5rem 0.5rem; /* Update: Removed unnecessary padding */ + border: 1px solid #d1d5db; + border-radius: 0.375rem; + font-size: 0.875rem; +} + +.viewToggle { + display: flex; + border: 1px solid #d1d5db; + border-radius: 0.375rem; + overflow: hidden; +} + +.viewButton { + padding: 0.5rem 1rem; + background-color: #ffffff; + border: none; + font-size: 0.875rem; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.viewButton.active { + background-color: #e5e7eb; +} + +.pagination1 { + display: flex; + justify-content: center; + align-items: center; + gap: 1rem; +} + +.pagination1 button1{ + padding: 0.5rem 1rem; + background-color: #f3f4f6; + border: none; + border-radius: 0.375rem; + font-size: 0.875rem; + cursor: pointer; + transition: background-color 0.3s ease; + color:black; +} + +.pagination1 button1:hover:not(:disabled) { + background-color: #e5e7eb; +} + +.pagination1 button1:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.pagination1 span { + font-size: 0.875rem; + color: #ffffff; +} + +.cardImageContainer { + width: 120px; + height: 120px; + position: relative; + flex-shrink: 0; +} + +.cardImage { + width: 100%; + height: 100%; + object-fit: cover; +} + + + +.viewToggleContainer { + display: flex; + justify-content: center; + gap: 1rem; + margin-bottom: 1.5rem; + padding-bottom: 1rem; +} + + +.viewToggleButton { + padding: 0.5rem 2rem; + font-size: 0.875rem; + font-weight: 500; + color: #6b7280; + background: none; + border: none; + border-bottom: 2px solid transparent; + cursor: pointer; + transition: all 0.2s ease; +} + + + +.viewToggleButton.active { + color: #fdfeff; + border-bottom-color: #ffffff; + background-color: #000000; +} + +.projectsGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1.5rem; + margin-bottom: 1.5rem; +} + +.projectCard { + background: white; + border: 1px solid #e5e7eb; + border-radius: 0.5rem; + overflow: hidden; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.projectCard:hover { + transform: translateY(-2px); + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); +} + +.projectImageContainer { + height: 160px; + overflow: hidden; +} + +.projectImage { + width: 100%; + height: 100%; + object-fit: cover; +} + +.projectContent { + padding: 1rem; +} + +.projectName { + font-size: 1rem; + font-weight: 600; + color: #111827; + margin-bottom: 0.5rem; +} + +.projectDescription { + font-size: 0.875rem; + color: #6b7280; + margin-bottom: 1rem; +} + +.projectStatus { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.status { + display: inline-flex; + align-items: center; + padding: 0.25rem 0.75rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 500; +} + +.status.in-progress { + background-color: #dbeafe; + color: #1d4ed8; +} + +.status.completed { + background-color: #dcfce7; + color: #15803d; +} + +.status.pending { + background-color: #fef3c7; + color: #92400e; +} + + +.projectCardLink { + text-decoration: none; + color: inherit; + display: block; +} + +.projectCard { + background-color: #ffffff; + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + overflow: hidden; + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.projectCard:hover { + transform: translateY(-5px); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); +} + +.projectImageContainer { + height: 200px; + overflow: hidden; +} + +.projectImage { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; +} + +.projectCard:hover .projectImage { + transform: scale(1.05); +} + +.projectContent { + padding: 1.5rem; +} + +.projectName { + font-size: 1.25rem; + font-weight: 600; + color: #333; + margin-bottom: 0.5rem; +} + +.projectDescription { + font-size: 0.875rem; + color: #666; + margin-bottom: 1rem; + line-height: 1.4; +} + +.projectStatus { + display: flex; + justify-content: space-between; + align-items: center; +} + +.status { + font-size: 0.75rem; + font-weight: 500; + padding: 0.25rem 0.5rem; + border-radius: 12px; + text-transform: capitalize; +} + +.status.in-progress { + background-color: #e6f7ff; + color: #0070f3; +} + +.status.completed { + background-color: #e6fffa; + color: #00c853; +} + +.status.pending { + background-color: #fff7e6; + color: #ffa000; +} + +.skills { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.skill { + font-size: 0.75rem; + background-color: #f0f0f0; + color: #333; + padding: 0.25rem 0.5rem; + border-radius: 12px; +} + +.project-card1 { + background: rgba(255, 255, 255, 0.1); + border-radius: 16px; + overflow: hidden; + transition: transform 0.3s ease; + margin: 1rem; + max-width: 375px; + border: 2px rgb(71 68 68) solid; + } + + + .project-card1:hover { + transform: translateY(-5px); + } + + .project-image { + width: 100%; + height: 200px; + object-fit: cover; + } + + .project-content1 { + padding: 1.5rem; + } + + .project-content1 h3 { + margin: 0 0 1rem 0; + color: #ffffff; + } + + .project-content1 p { + color: #929292; + margin-bottom: 1rem; + } + + .technologies { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 1rem; + } + + .tech-tag { + background: #383838; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.8rem; + color: #ffffff; + } + + .project-links { + display: flex; + gap: 1rem; + } + + .project-links a { + text-decoration: none; + color: #007bff; + font-weight: 500; + } + + .project-links a:hover { + text-decoration: underline; + } + + /* List view styles */ + .projects-container1.list-view { + display: flex; + flex-direction: column; + gap: 1rem; + max-width: 1200px; + margin: 0 auto; + } + + .project-card1.list-view { + display: flex; + width: 100%; + } + + .project-card1.list-view .project-image { + width: 200px; + height: 150px; + } + + .project-card1.list-view .project-content1 { + flex: 1; + display: flex; + flex-direction: column; + justify-content: space-between; + } + + /* Keep existing card view styles */ + .projects-container1.card-view { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 1rem; + } + + + .searchBarContainer { + display: flex; + justify-content: center; + margin: 20px 0; + } + + .searchInput { + width: 50%; + max-width: 400px; + padding: 10px; + font-size: 16px; + border: 1px solid #ccc; + border-radius: 4px; + outline: none; + } + + .searchInput:focus { + border-color: #007bff; + box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); + } + .searchBarContainer1 { + display: flex; + justify-content: center; + margin: 20px 0; + } + + .searchContainer1 { + display: flex; + justify-content: center; + margin: 2rem 0; + padding: 0 1rem; + gap:20px; + } + + .searchWrapper1 { + position: relative; + width: 100%; + max-width: 500px; + } + + .searchIcon1 { + position: absolute; + left: 1rem; + top: 50%; + transform: translateY(-50%); + color: #888; + } + + + + .searchInput1:focus { + border-color: #007bff; + } + .teamMembers, +.projectsGrid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; +} + + + +@media (max-width: 768px) { + .modal1 { + width: 100%; + height: 100%; + max-height: none; + border-radius: 0; + } + + .modalHeader { + position: sticky; + top: 0; + + z-index: 10; + } + + .teamMembers, .projectsGrid { + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + } + + + + .viewToggleButton { + margin: 0.25rem 0; + } +} + +@media (max-width: 480px) { + .modalHeader h2 { + font-size: 1.2rem; + } + + .teamMembers, .projectsGrid { + grid-template-columns: 1fr; + } + + .searchWrapper1 { + flex-direction: column; + align-items: stretch; + } + + .searchIcon1 { + margin-right: 0; + margin-bottom: 0.5rem; + } + + .pagination1 button-container { + margin: 0.25rem 0; + } +} + + + +.searchIcon1 { + position: absolute; + left: 1rem; + top: 50%; + transform: translateY(-50%); + color: #888; +} + +.searchInput1 { + width: 100%; + padding: 0.75rem 1rem 0.75rem 2.5rem; + font-size: 1rem; + border: 1px solid #333; + border-radius: 2rem; + outline: none; + transition: border-color 0.3s ease; + background-color: #1a1a1a; + color: #fff; +} + +.teamMembers, +.projectsGrid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; +} + +/* Responsive styles */ +@media (max-width: 1024px) { + .modal1 { + width: 95%; + } + + .teamMembers, + .projectsGrid { + grid-template-columns: repeat(3, 1fr); + } +} + +@media (max-width: 768px) { + .modal1 { + width: 100%; + height: 100%; + max-height: none; + border-radius: 0; + } + + .modalHeader { + position: sticky; + top: 0; + + z-index: 10; + } + + .teamMembers, + .projectsGrid { + grid-template-columns: repeat(2, 1fr); + } + + .viewToggleButton { + margin: 0.25rem 0; + } +} + +@media (max-width: 480px) { + .modalHeader h2 { + font-size: 1.2rem; + } + + .teamMembers, + .projectsGrid { + grid-template-columns: 1fr; + } + + .searchWrapper1 { + flex-direction: column; + align-items: stretch; + } + + .searchIcon1 { + margin-right: 0; + margin-bottom: 0.5rem; + } + + .pagination1 button-container { + ext-align: center; + margin: 0.25rem 0; + } + + /* New styles for pagination buttons under 480px */ + .pagination1 button1 { + width: 100%; + padding: 0.75rem; + font-size: 1rem; + margin: 0.5rem 0; + } + + .pagination1 span { + display: block; + text-align: center; + margin: 0.5rem 0; + } +} + +/* Additional styles for cards to ensure proper sizing */ +.project-card1, +.teamMemberCard { + width: 100%; + max-width: none; + margin: 0.5rem 0; +} + +.project-image, +.cardImageContainer { + height: 200px; + width: 100%; + object-fit: cover; +} + +@media (max-width: 768px) { + .project-image, + .cardImageContainer { + height: 180px; + } +} + +@media (max-width: 480px) { + .project-image, + .cardImageContainer { + height: 150px; + } + + .project-content1, + .cardContent { + padding: 1rem; + } +} + +.image-placeholder { + display: flex; + align-items: center; + justify-content: center; + width: 100%; /* Adjust as per your card's layout */ + height: 100%; + background-color: #ccc; /* Neutral background color */ + border-radius: 0%; /* Makes it circular */ + font-size: 84px; /* Adjust font size */ + font-weight: bold; + color: #333; /* Text color */ +} +.noTeamsMessage { + text-align: center; + color: #ffffff; + font-size: 1.2em; + margin-top: 20px; +} diff --git a/client/src/Pages/PortfolioManagement/Reviews/ReviewsPage.scss b/client/src/Pages/PortfolioManagement/Reviews/ReviewsPage.scss new file mode 100644 index 0000000..49979b9 --- /dev/null +++ b/client/src/Pages/PortfolioManagement/Reviews/ReviewsPage.scss @@ -0,0 +1,34 @@ + + .header { + text-align: center; + margin-bottom: 3rem; + } + + .title { + font-size: 2.25rem; + font-weight: bold; + margin-bottom: 1rem; + } + + .description { + color: #4b5563; + } + + .link { + color: #2563eb; + text-decoration: underline; + } + + .carouselWrapper { + margin-bottom: 3rem; + transition: all 0.5s ease-in-out; + } + + .carouselWrapper:hover { + transform: scale(1.05); + } + + .reviewsHeader + { + align-text: center; + } \ No newline at end of file diff --git a/client/src/Pages/PortfolioManagement/Reviews/index.jsx b/client/src/Pages/PortfolioManagement/Reviews/index.jsx new file mode 100644 index 0000000..ca914f5 --- /dev/null +++ b/client/src/Pages/PortfolioManagement/Reviews/index.jsx @@ -0,0 +1,162 @@ +import { useState, useMemo } from 'react'; +import { SearchBar } from '../../../component/Reviews/SearchBar'; +import { ReviewsCarousel } from '../../../component/Reviews/ReviewCarosoul'; +import './ReviewsPage.scss'; + +const clients = [ + { + _id: '1', + name: 'Sandra Long', + description: 'Regular client', + contact_info: 'sandra@example.com', + point_of_contact: 'Sandra', + image: '/placeholder.svg?height=40&width=40' + }, + { + _id: '2', + name: 'Jamie Atkins', + description: 'Premium client', + contact_info: 'jamie@example.com', + point_of_contact: 'Jamie', + image: '/placeholder.svg?height=40&width=40' + } + ] + + const projects = [ + { + _id: '1', + name: 'Website Redesign', + description: 'Complete website overhaul', + status: 'Completed', + start_time: new Date('2023-01-01'), + end_time: new Date('2023-06-30'), + budget: 50000, + hr_taken: 500, + client_id: '1', + techStack: ['React', 'Node.js', 'MongoDB'], + links: { + links: 'https://example.com', + github: 'https://github.com/example' + }, + image_link: '/placeholder.svg' + }, + { + _id: '2', + name: 'Mobile App Development', + description: 'iOS and Android app', + status: 'In Progress', + start_time: new Date('2023-07-01'), + end_time: new Date('2024-01-31'), + budget: 75000, + hr_taken: 300, + client_id: '2', + techStack: ['React Native', 'Firebase'], + links: { + links: 'https://example.com', + github: 'https://github.com/example' + }, + image_link: '/placeholder.svg' + } + ] + const reviews = [ + { + _id: '1', + project_id: '1', + header: 'Excellent service', + comment: 'My 1st time booking online was simple, staff was very friendly.', + rating: 5, + client_name: 'Sandra Long', + client_image: '/placeholder.svg?height=40&width=40', + project_name: projects.find(p => p._id === '1')?.name || 'Unknown Project' + }, + { + _id: '2', + project_id: '1', + header: 'Great service', + comment: 'Great service, was running late, but the company really helped on my arrival.', + rating: 5, + client_name: 'Sandra Long', + client_image: '/placeholder.svg?height=40&width=40', + project_name: projects.find(p => p._id === '1')?.name || 'Unknown Project' + }, + { + _id: '3', + project_id: '2', + header: 'Excellent service', + comment: 'Excellent service, easy booking and parking. Will use again..', + rating: 5, + client_name: 'Jamie Atkins', + client_image: '/placeholder.svg?height=40&width=40', + project_name: projects.find(p => p._id === '2')?.name || 'Unknown Project' + }, + { + _id: '4', + project_id: '2', + header: 'Excellent service', + comment: 'Excellent service, easy booking and parking. Will use again..', + rating: 5, + client_name: 'Jamie Atkins', + client_image: '/placeholder.svg?height=40&width=40', + project_name: projects.find(p => p._id === '2')?.name || 'Unknown Project' + }, + { + _id: '5', + project_id: '1', + header: 'Great service', + comment: 'Great service, was running late, but the company really helped on my arrival.', + rating: 5, + client_name: 'Sandra Long', + client_image: '/placeholder.svg?height=40&width=40', + project_name: projects.find(p => p._id === '1')?.name || 'Unknown Project' + } + ] + + export default function ReviewsPage() { + const [searchTerm, setSearchTerm] = useState(''); + + const filteredReviews = useMemo(() => { + if (!searchTerm) return reviews; + + const lowercaseSearch = searchTerm.toLowerCase(); + return reviews.filter((review) => + review.client_name.toLowerCase().includes(lowercaseSearch) || + review.header.toLowerCase().includes(lowercaseSearch) || + review.comment.toLowerCase().includes(lowercaseSearch) || + review.project_name.toLowerCase().includes(lowercaseSearch) || + projects.find((p) => p._id === review.project_id)?.techStack.some((tech) => + tech.toLowerCase().includes(lowercaseSearch) + ) + ); + }, [searchTerm]); + + const averageRating = useMemo(() => { + const total = reviews.reduce((acc, review) => acc + review.rating, 0); + return (total / reviews.length).toFixed(1); + }, []); + + return ( +
+
+

+ Company score {averageRating} out of 5, from {reviews.length.toLocaleString()} reviews. +

+

+ You can trust us to get it where it needs to be, but don't take our word for it. Read our reviews at{' '} + + Trustpilot.com + +

+
+ + + +
+ +
+ +
+ +
+
+ ); + } diff --git a/client/src/Pages/PortfolioManagement/Reviews/reviews.scss b/client/src/Pages/PortfolioManagement/Reviews/reviews.scss new file mode 100644 index 0000000..e69de29 diff --git a/client/src/Pages/ProjectDetails/index.jsx b/client/src/Pages/ProjectDetails/index.jsx new file mode 100644 index 0000000..936ca5c --- /dev/null +++ b/client/src/Pages/ProjectDetails/index.jsx @@ -0,0 +1,20 @@ +import React from 'react'; +import './styles.css' + +import {DetailsContent,DetailsBanner} from '../../component'; + + +const ProjectDetails=()=>{ +return( +
+ +
+ +
+ +
+ +) +} + +export default ProjectDetails; \ No newline at end of file diff --git a/client/src/Pages/ProjectDetails/styles.css b/client/src/Pages/ProjectDetails/styles.css new file mode 100644 index 0000000..ec7504d --- /dev/null +++ b/client/src/Pages/ProjectDetails/styles.css @@ -0,0 +1,13 @@ +.content +{ + background: #18181b !important; + height: 100%; +} +.scroll-container { + height: 100vh; + /* overflow-y: scroll; */ + scroll-snap-type: y mandatory; + transition: scroll 0.5s ease-in-out; + scroll-behavior: smooth; + /* width: 100%; */ +} \ No newline at end of file diff --git a/client/src/Pages/Upload/index.jsx b/client/src/Pages/Upload/index.jsx new file mode 100644 index 0000000..8cb725b --- /dev/null +++ b/client/src/Pages/Upload/index.jsx @@ -0,0 +1,13 @@ + +import { Imports } from "../../component" +import './style.css' +export default function Upload() { + + return ( +
+

Import Excel Data

+ +
+ ) +} + diff --git a/client/src/Pages/Upload/style.css b/client/src/Pages/Upload/style.css new file mode 100644 index 0000000..ef3a6a2 --- /dev/null +++ b/client/src/Pages/Upload/style.css @@ -0,0 +1,6 @@ +.containerr { + max-width: 1350px; + margin: 0 auto; + padding: 100px 1rem; + text-align: center; +} \ No newline at end of file diff --git a/client/src/Pages/index.js b/client/src/Pages/index.js index e812385..160c08e 100644 --- a/client/src/Pages/index.js +++ b/client/src/Pages/index.js @@ -18,6 +18,10 @@ import AssistantsList from './SuperAdmin/Assistant/AssistantsList'; import ChatPage from "./ChatPage"; import AssistantsChatPage from "./AssistantsChatPage"; import AssistantFileDownloadPage from "./AssistantFileDownload"; +import PortfolioHome from "./PortfolioHome"; +import ProjectDetails from "./ProjectDetails"; +import Form from "./Form"; +import Upload from "./Upload"; export { LoginForm, @@ -37,5 +41,9 @@ export { AssistantsList, ChatPage, AssistantsChatPage, - AssistantFileDownloadPage + AssistantFileDownloadPage, + PortfolioHome, + Form, + ProjectDetails, + Upload, }; diff --git a/client/src/api/projectApi.js b/client/src/api/projectApi.js new file mode 100644 index 0000000..f9019ea --- /dev/null +++ b/client/src/api/projectApi.js @@ -0,0 +1,255 @@ +import { axiosSecureInstance } from "./axios"; + +export const getAllProjects = async (sortBy='',search='') => { + try { + const response = await axiosSecureInstance.get('/api/projects',{params:{sortBy,search}}); + return response.data; + } catch (error) { + console.error('Error fetching projects:', error); + throw error; + } +}; + +// Fetch project by ID +export const getProjectById = async (id) => { + try { + const response = await axiosSecureInstance.get(`/api/projects/client/${id}`); + return response.data; + } catch (error) { + console.error('Error fetching project:', error); + throw error; + } +}; +// Create a new project +export const createProject = async (projectData) => { + try { + const response = await axiosSecureInstance.post('/api/projects', projectData); + return response.data; + } catch (error) { + console.error('Error creating project:', error); + throw error; + } +}; +// Update an existing project +export const updateProject = async (id, projectData) => { + try { + const response = await axiosSecureInstance.put(`/api/projects/${id}`, projectData); + return response.data; + } catch (error) { + console.error('Error updating project:', error); + throw error; + } +}; +// Delete a project +export const deleteProject = async (id) => { + try { + const response = await axiosSecureInstance.delete(`/api/projects/${id}`); + return response.data; + } catch (error) { + console.error('Error deleting project:', error); + throw error; + } +}; +// Fetch client information +export const getClientInfo = async (clientId) => { + try { + const response = await axiosSecureInstance.get(`/api/clients/${clientId}`); + return response.data; + } catch (error) { + console.error('Error fetching client info:', error); + handleApiError(error); + throw error; + } +}; +// Fetch all projects for a specific client +export const getClientProjects = async (clientId) => { + try { + const response = await axiosSecureInstance.get(`/api/projects/client/${clientId}`); + return response.data; + } catch (error) { + console.error('Error fetching client projects:', error); + throw error; + } +}; +// Fetch total revenue and benefits for a client +export const getAllRevenueData = async () => { + try { + const response = await axiosSecureInstance.get('/api/revenue'); + return response.data.revenues; + } catch (error) { + console.error('Error fetching revenue data:', error); + throw error; + } +}; +export const getTechStackById = async (id) => { + try { + if (typeof id === 'object' && id !== null) { + id = id._id || id.id; // Use _id or id property if available + } + const response = await axiosSecureInstance.get(`/api/techStacks/${id}`); + return response.data; + } catch (error) { + console.error('Error fetching tech stack by ID:', error); + if (error.response) { + console.error('Response status:', error.response.status); + console.error('Response data:', error.response.data); + } + throw error; + } +}; +// Handle API error details +export const getTeamById = async (id) => { + try { + const response = await axiosSecureInstance.get(`/api/teams/${id}`); + return response.data.team; + } catch (error) { + console.error('Error fetching team:', error); + throw error; + } +}; +export const getProjectTeamMembers = async (projectId) => { + try { + const response = await axiosSecureInstance.get(`/api/project-team/project/${projectId}`); + return response.data; + } catch (error) { + console.error('Error fetching project team members:', error); + throw error; + } +}; +export const getAllTeams = async () => { + try { + const response = await axiosSecureInstance.get('/api/teams'); + return response.data.teams; + } catch (error) { + console.error('Error fetching teams:', error); + throw error; + } +}; +export const getTeamMembers = async (teamId) => { + try { + const response = await axiosSecureInstance.get(`/api/teams/${teamId}/members`); + return response.data.members; + } catch (error) { + console.error('Error fetching team members:', error); + throw error; + } +} +export const getProjectTeamMembersByTeam = async (teamId) => { + try { + const timestamp = new Date().getTime(); + const response = await axiosSecureInstance.get(`/api/project-team/team/${teamId}?_=${timestamp}`); + return response.data; + } catch (error) { + console.error('Error fetching project team members:', error); + if (error.response) { + console.error('Response status:', error.response.status); + console.error('Response data:', error.response.data); + } + throw error; + } +}; +export const getAllUsers = async () => { + try { + const response = await axiosSecureInstance.get('/api/user/get-all-users'); + return response.data; + } catch (error) { + console.error('Error fetching all users:', error); + if (error.response) { + console.error('Response status:', error.response.status); + console.error('Response data:', error.response.data); + } + throw error; + } +}; +export const getUsersByTeamId = async (teamId) => { + try { + const [usersResponse, projectTeamsResponse] = await Promise.all([ + getAllUsers(), + getAllProjectTeams() + ]); + const allUsers = usersResponse.user; + const allProjectTeams = projectTeamsResponse; + if (!Array.isArray(allUsers)) { + throw new TypeError('Expected allUsers to be an array'); + } + const teamUsers = allUsers.filter(user => user.teamId === teamId); + const usersWithRoles = teamUsers.map(user => { + const userProjects = allProjectTeams.filter(pt => pt.user_id === user._id && pt.team_id === teamId); + const roles = userProjects.map(up => up.role_in_project); + return { ...user, roles_in_project: roles }; + }) + return usersWithRoles; + } catch (error) { + console.error('Error fetching users by team ID:', error); + throw error; + } +}; +export const fetchProjectById = async (projectId) => { + try { + const response = await axiosSecureInstance.get(`/api/projects/project/${projectId}`); + return response.data; + } catch (error) { + console.error("Error fetching project:", error); + return null; // Return null to handle potential issues gracefully + } +}; +export const getAllProjectTeams = async () => { + try { + const response = await axiosSecureInstance.get('/api/project-team/all'); + return response.data; + } catch (error) { + console.error('Error fetching all project teams:', error); + throw error; + } +}; +export const getProjectsByTeam = async (teamId) => { + try { + const allProjectTeams = await getAllProjectTeams(); + const teamProjects = allProjectTeams.filter(pt => pt.team_id === teamId); + if (teamProjects.length === 0) { + return []; + } + const projectIds = teamProjects.map(tp => tp.project_id); + + const projectPromises = projectIds.map(id => fetchProjectById(id)); + const projects = await Promise.all(projectPromises); + const validProjects = projects.filter(project => project !== null); + // Remove duplicates by project id + // const uniqueProjects = Array.from( + // new Map(validProjects.map(project => [project.id, project])).values() + // ); + //console.log('Unique projects:', uniqueProjects); + return validProjects; + } catch (error) { + console.error('Error fetching projects by team ID:', error); + throw error; + } +}; +const handleApiError = (error) => { + if (error.response) { + // The request was made and the server responded with a status code that falls out of the range of 2xx + console.error('Response data:', error.response.data); + console.error('Response status:', error.response.status); + console.error('Response headers:', error.response.headers); + } else if (error.request) { + // The request was made but no response was received + console.error('No response received:', error.request); + } else { + // Something happened in setting up the request that triggered an Error + console.error('Error setting up request:', error.message); + } +}; + + +export const searchByAllFields = async (searchTerm='') => { + try { + const response = await axiosSecureInstance.get('/api/projects/search', { + params: { searchTerm } + }); + // const response = await axiosSecureInstance.get('/api/projects/search'); + return response.data; + } catch (error) { + console.error('Frontend: Error searching by all fields:', error); + throw error; + } +} \ No newline at end of file diff --git "a/client/src/assests/images/DALL\302\267E 2024-12-08 10.51.40 - Generate an image of a modern AI collaboration platform featuring abstract connections and nodes on a gradient background. The design should look slee.webp" "b/client/src/assests/images/DALL\302\267E 2024-12-08 10.51.40 - Generate an image of a modern AI collaboration platform featuring abstract connections and nodes on a gradient background. The design should look slee.webp" new file mode 100644 index 0000000..77eb75c Binary files /dev/null and "b/client/src/assests/images/DALL\302\267E 2024-12-08 10.51.40 - Generate an image of a modern AI collaboration platform featuring abstract connections and nodes on a gradient background. The design should look slee.webp" differ diff --git "a/client/src/assests/images/DALL\302\267E 2024-12-08 10.53.56 - Generate an image of an e-commerce dashboard showcasing data visualization, including charts, graphs, and statistics on a sleek interface. The design .webp" "b/client/src/assests/images/DALL\302\267E 2024-12-08 10.53.56 - Generate an image of an e-commerce dashboard showcasing data visualization, including charts, graphs, and statistics on a sleek interface. The design .webp" new file mode 100644 index 0000000..1217724 Binary files /dev/null and "b/client/src/assests/images/DALL\302\267E 2024-12-08 10.53.56 - Generate an image of an e-commerce dashboard showcasing data visualization, including charts, graphs, and statistics on a sleek interface. The design .webp" differ diff --git "a/client/src/assests/images/DALL\302\267E 2024-12-08 10.54.02 - Generate a set of 5 distinct images representing diverse software development projects. Each image should feature a modern, professional style and dep.webp" "b/client/src/assests/images/DALL\302\267E 2024-12-08 10.54.02 - Generate a set of 5 distinct images representing diverse software development projects. Each image should feature a modern, professional style and dep.webp" new file mode 100644 index 0000000..8263bd1 Binary files /dev/null and "b/client/src/assests/images/DALL\302\267E 2024-12-08 10.54.02 - Generate a set of 5 distinct images representing diverse software development projects. Each image should feature a modern, professional style and dep.webp" differ diff --git a/client/src/component/Chat/BotResponse.jsx b/client/src/component/Chat/BotResponse.jsx index 36ddac2..d233abc 100644 --- a/client/src/component/Chat/BotResponse.jsx +++ b/client/src/component/Chat/BotResponse.jsx @@ -7,7 +7,8 @@ import { HiCheck, HiOutlineClipboard, HiShare } from "react-icons/hi2"; import { marked } from "marked"; import CodeHighlighter from "../common/CodeHighlighter"; import { copyToClipboard } from "../../Utility/helper"; -import * as DOMPurify from "dompurify"; +import DOMPurify from 'dompurify'; + import { Link } from "react-router-dom"; // components diff --git a/client/src/component/ClientInfo/Avatar.jsx b/client/src/component/ClientInfo/Avatar.jsx new file mode 100644 index 0000000..ba4ff44 --- /dev/null +++ b/client/src/component/ClientInfo/Avatar.jsx @@ -0,0 +1,25 @@ +import React from 'react'; + +export const Avatar = ({ src, alt, fallback, className }) => ( +
+ {src ? ( + {alt} + ) : ( +
+ {fallback} +
+ )} +
+); + +export const Avatars = ({ src, alt, fallback, className }) => ( +
+ {src ? ( + {alt} + ) : ( +
+ {fallback} +
+ )} +
+); diff --git a/client/src/component/ClientInfo/Badge.jsx b/client/src/component/ClientInfo/Badge.jsx new file mode 100644 index 0000000..c102220 --- /dev/null +++ b/client/src/component/ClientInfo/Badge.jsx @@ -0,0 +1,9 @@ +import React from 'react'; + +const Badge = ({ children, className }) => ( + + {children} + +); + +export default Badge; \ No newline at end of file diff --git a/client/src/component/ClientInfo/Button1.jsx b/client/src/component/ClientInfo/Button1.jsx new file mode 100644 index 0000000..f5facc4 --- /dev/null +++ b/client/src/component/ClientInfo/Button1.jsx @@ -0,0 +1,13 @@ +import React from 'react'; + +const Button1 = ({ children, className, ...props }) => ( + + {children} + +); + +export default Button1; + diff --git a/client/src/component/ClientInfo/Cards.jsx b/client/src/component/ClientInfo/Cards.jsx new file mode 100644 index 0000000..89dbd2b --- /dev/null +++ b/client/src/component/ClientInfo/Cards.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +export const Cardss = ({ children, className }) => ( +
+ {children} +
+); + +export const Cards = ({ children, className }) => ( +
+ {children} +
+); + +export const CardContent = ({ children, className }) => ( +
{children}
+); + +export const CardFooter = ({ children, className }) => ( +
{children}
+); + diff --git a/client/src/component/ClientInfo/Input.jsx b/client/src/component/ClientInfo/Input.jsx new file mode 100644 index 0000000..33535d9 --- /dev/null +++ b/client/src/component/ClientInfo/Input.jsx @@ -0,0 +1,11 @@ +import React from 'react'; + +const Input = ({ className, ...props }) => ( + +); + +export default Input; + diff --git a/client/src/component/ClientInfo/MilestoneCard.jsx b/client/src/component/ClientInfo/MilestoneCard.jsx new file mode 100644 index 0000000..8dcdf59 --- /dev/null +++ b/client/src/component/ClientInfo/MilestoneCard.jsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { Cardss, CardContent } from './Cards'; +import Badge from './Badge'; + +const MilestoneCard = ({ milestone }) => ( + + +

{milestone.title}

+

{milestone.description}

+
+ + {milestone.status[0].title} + + Owner: {milestone.status[0].owner} +
+
+
+); + +export default MilestoneCard; diff --git a/client/src/component/ClientInfo/ReviewCard.jsx b/client/src/component/ClientInfo/ReviewCard.jsx new file mode 100644 index 0000000..b3ca789 --- /dev/null +++ b/client/src/component/ClientInfo/ReviewCard.jsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { FaStar } from "react-icons/fa"; +import { Cardss, CardContent } from './Cards'; +import { Avatars } from './Avatar'; + +const ReviewCard = ({ review }) => ( + + +
+ +
+

{review.reviewer}

+

{review.headline}

+
+
+

{review.comment}

+
+ {[...Array(5)].map((_, index) => ( + + ))} +
+
+
+); + +export default ReviewCard; diff --git a/client/src/component/ClientInfo/SearchBar.jsx b/client/src/component/ClientInfo/SearchBar.jsx new file mode 100644 index 0000000..f0e5c90 --- /dev/null +++ b/client/src/component/ClientInfo/SearchBar.jsx @@ -0,0 +1,48 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { FaSearch } from "react-icons/fa"; + +const SearchBar = ({ searchQuery, setSearchQuery, projects, setFilteredProjects }) => { + const handleSearchChange = useCallback( + (e) => { + const query = e.target.value.trim().toLowerCase(); + setSearchQuery(query); + + if (query) { + const filtered = projects.filter((project) => { + const techStackIncludes = project.techStack.some((tech) => + tech.name.toLowerCase().includes(query) + ); + return ( + project.name.toLowerCase().includes(query) || + project.description.toLowerCase().includes(query) || + project.status.toLowerCase().includes(query) || + techStackIncludes + ); + }); + setFilteredProjects(filtered); + } else { + setFilteredProjects(projects); + } + }, + [setSearchQuery, setFilteredProjects, projects] + ); + + return ( +
+
+
+ + +
+
+
+ ); +}; + +export default SearchBar; diff --git a/client/src/component/ClientInfo/Tabs.jsx b/client/src/component/ClientInfo/Tabs.jsx new file mode 100644 index 0000000..9b15d3e --- /dev/null +++ b/client/src/component/ClientInfo/Tabs.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import '../../Pages/PortfolioManagement/ClientInfo/ClientInfo.scss' + +export const Tabs = ({ children }) => ( +
{children}
+); + +export const TabsList = ({ children }) => ( +
{children}
+); + +export const TabsTrigger = ({ children, isActive, onClick }) => ( + + {children} + +); + +export const TabsContent = ({ children, isActive }) => ( +
{children}
+); + diff --git a/client/src/component/ContentPage/index.jsx b/client/src/component/ContentPage/index.jsx new file mode 100644 index 0000000..13fb05e --- /dev/null +++ b/client/src/component/ContentPage/index.jsx @@ -0,0 +1,277 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import axios from 'axios'; +import "./styles.css"; +import Projects from '../Projects'; +import Pagination from 'react-bootstrap/Pagination'; +import { getAllProjects } from '../../api/projectApi'; + +export default function ContentPage() { + const [isListView, setIsListView] = useState(false); + const [search, setSearch] = useState(''); + const [isModalOpen, setIsModalOpen] = useState(false); + const [selectedTags, setSelectedTags] = useState([]); + const [activePage, setActivePage] = useState(1); + const [isSortModalOpen, setIsSortModalOpen] = useState(false); + const [sortBy, setSortBy] = useState(''); + const [fetchedProjects, setFetchedProjects] = useState([]); + const [allTags, setAllTags] = useState({}); + const [projectsPerPage, setProjectsPerPage] = useState(6); + const [items, setItems] = useState([]); + const [totalPages, setTotalPages] = useState(0); + const [isLoading, setIsLoading] = useState(false); + + const handleSearchChange = (e) => { + setSearch(e.target.value); + }; + + const toggleTag = (tag) => { + setSelectedTags((prevTags) => { + const newTags = prevTags.includes(tag) + ? prevTags.filter((t) => t !== tag) + : [...prevTags, tag]; + return newTags; + }); + }; + + useEffect(() => { + const handleResize = () => { + const newProjectsPerPage = window.innerWidth < 1017 ? 6 : 6; + setProjectsPerPage(newProjectsPerPage); + }; + + window.addEventListener('resize', handleResize); + + return () => window.removeEventListener('resize', handleResize); + }, []); + + useEffect(() => { + const fetchData = async () => { + setIsLoading(true); + try { + const response = await getAllProjects(sortBy); + + const getUniqueValues = (arr) => [...new Set(arr.map(JSON.stringify))].map(JSON.parse); + + // Process tags more effectively + const techStackTags = getUniqueValues(response.flatMap((project) => project.techStack || []).filter(Boolean)); + const clientTags = getUniqueValues(response.map((project) => project.client_id).filter(Boolean)); + const teamsTags = getUniqueValues(response.map((project) => project.team_id).filter(Boolean)); + const featuresTags = getUniqueValues(response.flatMap((project) => project.feature || []).filter(Boolean)); + + setAllTags({ + techStack: techStackTags, + client: clientTags, + teams: teamsTags, + features: featuresTags, + }); + + setFetchedProjects(response); + + const initialTotalPages = Math.ceil(response.length / projectsPerPage); + setTotalPages(initialTotalPages); + + const generateItems = () => { + const newItems = []; + for (let number = 1; number <= initialTotalPages; number++) { + newItems.push( + setActivePage(number)}> + {number} + + ); + } + setItems(newItems); + }; + + generateItems(); + } catch (error) { + console.error("Error fetching projects:", error); + } finally { + setIsLoading(false); + } + }; + + fetchData(); + }, [sortBy]); + + const filteredProjects = useMemo(() => { + return fetchedProjects.filter((project) => { + const matchesSearch = project.name.toLowerCase().includes(search.toLowerCase()); + if (!matchesSearch) return false; + + if (selectedTags.length === 0) return true; + return selectedTags.some(tag => + (project.techStack && project.techStack.some(tech => tech._id === tag || tech.name === tag)) || + (project.feature && project.feature.some(feature => feature._id === tag || feature === tag)) || + (project.team_id && (project.team_id._id === tag || project.team_id.teamTitle === tag)) || + (project.client_id && (project.client_id._id === tag || project.client_id.name === tag)) + ); + }); + }, [fetchedProjects, selectedTags, search]); + + useEffect(() => { + const filteredTotalPages = Math.ceil(filteredProjects.length / projectsPerPage); + setTotalPages(filteredTotalPages); + + const generateItems = () => { + const newItems = []; + for (let number = 1; number <= filteredTotalPages; number++) { + newItems.push( + setActivePage(number)}> + {number} + + ); + } + setItems(newItems); + }; + + generateItems(); + setActivePage(1); // Reset to first page when filters or search changes + }, [filteredProjects, projectsPerPage, search]); + + const startIndex = (activePage - 1) * projectsPerPage; + const currentProjects = filteredProjects.slice(startIndex, startIndex + projectsPerPage); + + return ( +
+
+
+

Portfolio

+
+ +
+
+ +
+ +
+ + + {isSortModalOpen && ( +
+
    + {['default', 'budget', 'recent'].map((option) => ( +
  • { + setSortBy(option); + setIsSortModalOpen(false); + }} + onMouseEnter={(e) => (e.target.style.backgroundColor = '#444')} + onMouseLeave={(e) => (e.target.style.backgroundColor = 'transparent')} + > + {option.charAt(0).toUpperCase() + option.slice(1)} +
  • + ))} +
+
+ )} +
+ + setIsModalOpen(true)}> + Filter + +
+
+ + {isModalOpen && ( +
+
+
+

Filter Projects

+ setIsModalOpen(false)}>× +
+
+ {Object.entries(allTags).map(([category, tags]) => ( +
+

{category.charAt(0).toUpperCase() + category.slice(1)}

+
+ {tags.map((tag) => { + const tagId = tag._id || tag.id || tag; + const tagName = tag.name || tag.teamTitle || tag; + return ( + + ); + })} +
+
+ ))} +
+
+ + +
+
+
+ )} + + {!isLoading && ( + + )} + +
+ setActivePage(prev => Math.max(prev - 1, 1))} + disabled={activePage === 1} + > + Previous + + {activePage} of {totalPages} + setActivePage(prev => Math.min(prev + 1, totalPages))} + disabled={activePage === totalPages} + > + Next + +
+
+ ); +} \ No newline at end of file diff --git a/client/src/component/ContentPage/styles.css b/client/src/component/ContentPage/styles.css new file mode 100644 index 0000000..a66825e --- /dev/null +++ b/client/src/component/ContentPage/styles.css @@ -0,0 +1,317 @@ +.container{ + height : 100%; + + margin: 0 0; + width:100%; + +} +.container h1{ + color:white; + font-weight:500; +} + +.panel{ + display:flex; + /* padding-top: 80px !important; */ + justify-content: space-between; + align-items: center; + + +} + +.title +{ + margin-bottom: 0px !important; +} + +.filterbar +{ + display: flex; + gap:2rem; + align-items: center; + font-size: 1rem; +} + +.filterbar input { + color:white; + background-color: #000000; + padding-inline:15px; + border-radius: 8px; + border:1px #86858b solid; +} + +a{ + font-size: 1.2rem; +} + +.switch { + position: relative; + display: inline-block; + width: 60px; + height: 34px; + } + + .switch input { + opacity: 0; + width: 0; + height: 0; + } + + .dropdown + { + border-radius: 16px; + background-color: #070707; + list-style-type:none; + } + + /* The slider */ + .slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(112, 109, 109, 0.801); + -webkit-transition: .4s; + transition: .4s; + } + + + .slider:before { + position: absolute; + content: ""; + height: 26px; + width: 26px; + left: 4px; + bottom: 4px; + background-color: white; + -webkit-transition: .4s; + transition: .4s; + } + + input:checked + .slider { + background-color: #2196F3; + } + + input:focus + .slider { + box-shadow: 0 0 1px #2196F3; + } + + input:checked + .slider:before { + -webkit-transform: translateX(26px); + -ms-transform: translateX(26px); + transform: translateX(26px); + } + + /* Rounded sliders */ + .slider.round { + border-radius: 34px; + } + + .slider.round:before { + border-radius: 50%; + } + + .modal { + display: flex; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0, 0, 0, 0.5); + align-items: center; + justify-content: center; +} + +.modal-content { + background-color: #ffffff; + margin: 0 auto; + padding: 20px; + border: 1px solid #888; + width: 90%; + max-width: 600px; + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); + color: #333; + max-height: 90vh; + display: flex; + flex-direction: column; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding-bottom: 10px; + border-bottom: 1px solid #e0e0e0; +} + +.modal-title { + font-size: 1.5rem; + font-weight: bold; + margin: 0; +} + +.close { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; + cursor: pointer; + transition: color 0.3s ease; +} + +.close:hover, +.close:focus { + color: #ff0000; + text-decoration: none; +} + +.modal-body { + flex-grow: 1; + overflow-y: auto; + padding-right: 10px; +} + +.tag-section { + margin-bottom: 20px; +} + +.tag-title { + font-size: 1.2rem; + margin-bottom: 10px; + color: #444; +} + +.tag-list { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.tag-item { + display: flex; + align-items: center; + margin-bottom: 5px; +} + +.tag-checkbox { + margin-right: 5px; +} + +.tag-label { + font-size: 0.9rem; + color: #666; +} + +.modal-footer { + display: flex; + justify-content: flex-end; + margin-top: 20px; + padding-top: 10px; + border-top: 1px solid #e0e0e0; +} + +button { + background-color: #2196F3; + color: white; + border: none; + border-radius: 4px; + padding: 10px 15px; + cursor: pointer; + transition: background-color 0.3s; + font-size: 1rem; +} + +button:hover { + background-color: #1976D2; +} + +button.secondary { + background-color: #f0f0f0; + color: #333; + margin-right: 10px; +} + +button.secondary:hover { + background-color: #e0e0e0; +} + +@media (max-width: 600px) { + .modal-content { + width: 95%; + padding: 15px; + } + + .tag-list { + flex-direction: column; + } +} + +.page-item +{ + background: rgba(255, 255, 255, 0.1) !important; +} + +@media (max-width: 991px) +{ + .filterbar + { + gap:1rem; + } +} + + +@media (max-width: 786px) { + .filterbar input + { + width:150px !important; + } + + .switch + { + display: none; + } + +} + +@media (max-width:628px) +{ + .panel{ + display:block; + justify-content:center; + } + .filterbar + { + justify-content: center; + } + .title + { + display: flex; + justify-content: center; + } + .filterbar + { + gap:2rem; + } +} + +.loading { + text-align: center; + font-size: 1.5rem; + color: white; +} + +.projects { + opacity: 0; + transition: opacity 1s ease-in-out; +} + +.projects.loaded { + opacity: 1; +} \ No newline at end of file diff --git a/client/src/component/DetailsBanner/index.jsx b/client/src/component/DetailsBanner/index.jsx new file mode 100644 index 0000000..2e56b3e --- /dev/null +++ b/client/src/component/DetailsBanner/index.jsx @@ -0,0 +1,53 @@ +import React from 'react'; +import './styles.css'; +import { useParams } from 'react-router-dom'; +import {InitialsAvatar} from '../../component/InitialsAvatar/InitialsAvatar' +import { useEffect ,useState} from 'react'; +import { fetchProjectById } from '../../api/projectApi'; + +const DetailsBanner=()=> +{ + const { id } = useParams(); + const [project, setProject] = useState(null); + useEffect(()=>{ + const fetchData=async ()=> + { + try{ + const response=await fetchProjectById(id); + setProject(response); + } + catch(error){ + console.error('Error fetching project:', error); + setProject(null); // Handle potential issues gracefully + } + + } + fetchData(); + },[id]); + if (!project) { + return
Loading project details...
; + } + + return( + <> +
+
+ {project.image_link ? ( + {project.name} + ) : ( + + )} + +
+ +
+ + + ) +} + +export default DetailsBanner; \ No newline at end of file diff --git a/client/src/component/DetailsBanner/styles.css b/client/src/component/DetailsBanner/styles.css new file mode 100644 index 0000000..0e81c86 --- /dev/null +++ b/client/src/component/DetailsBanner/styles.css @@ -0,0 +1,20 @@ +.details-banner +{ + height:250px; + width:100%; + /* background: #0000009c; */ + + +} + +.image-styles +{ + height:100%; + width:100%; + object-fit: cover; + +} +.project-image-wrappers +{ + height:100%; +} \ No newline at end of file diff --git a/client/src/component/DetailsContent/index.jsx b/client/src/component/DetailsContent/index.jsx new file mode 100644 index 0000000..44a2d20 --- /dev/null +++ b/client/src/component/DetailsContent/index.jsx @@ -0,0 +1,179 @@ +import React, { useState, useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { Circle, Calendar, Flag, FileText, DollarSign, Clock, Users } from 'lucide-react'; +import './styles.css'; +import { fetchProjectById, getProjectTeamMembers } from '../../api/projectApi'; + +const InfoBox = ({ icon: Icon, label, value }) => ( +
+
+
+ + {label} +
+ {value} +
+
+); + +const Badge = ({ children, className }) => ( + {children} +); + +export default function ProjectDetails() { + const { id } = useParams(); + const navigate = useNavigate(); + const [project, setProject] = useState(null); + const [teammember, setTeammember] = useState([]); + + const handleProjectClick = (id) => { + navigate(`/platform-management-feature/Client/${id}`); + }; + + const handleTeamClick = (id) => { + navigate(`/platform-management-feature/Pod/${id}`); + }; + + useEffect(() => { + const fetchData = async () => { + try { + const response = await fetchProjectById(id); + setProject(response); + } catch (error) { + console.error('Error fetching project:', error); + } + }; + fetchData(); + }, [id]); + + useEffect(() => { + const fetchData = async () => { + try { + const response = await getProjectTeamMembers(id); + setTeammember(response); + } catch (error) { + console.error('Error fetching project team members:', error); + } + }; + fetchData(); + }, [id]); + + if (!project) { + return ( +
+
+
Loading project details...
+
+
+ ); + } + + return ( +
+
+

{project.name}

+
+ +
+
+

{project.description}

+ +
+
+ + Status + {project.status || 'In Progress'} +
+ +
+ Client +
handleProjectClick(project.client_id?._id)} + > +
+ {project.client_id?.name || 'Unknown'} +
+
+ +
+ + Timeline + + {project.start_time ? new Date(project.start_time).toLocaleDateString() : 'N/A'} + {' → '} + {project.end_time ? new Date(project.end_time).toLocaleDateString() : 'N/A'} + +
+ +
+ + Tech Stack +
+ {project.techStack?.map((tech) => ( + + {tech.name} + + ))} +
+
+ +
+ + Team +
handleTeamClick(project.team_id?._id)} + > + {project.team_id?.teamTitle || 'No team assigned'} +
+
+ +
+ + Members +
+ {teammember?.map((member) => ( +
+ {member.user_id?.fname} {member.user_id?.lname} + - {member.role_in_project} +
+ ))} +
+
+
+ +
+ + + +
+
+ +
+

Features

+
    + {project.feature?.map((feature, index) => ( +
  • {feature.name}
  • + ))} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/component/DetailsContent/styles.css b/client/src/component/DetailsContent/styles.css new file mode 100644 index 0000000..05950c6 --- /dev/null +++ b/client/src/component/DetailsContent/styles.css @@ -0,0 +1,242 @@ +.project-page { + padding-bottom: 50px; + background-color: #070707; + min-height: 100vh; +} + +.project-details { + color: #f4f4f5; + padding: 24px; + display: flex; + background: #070707; + gap: 32px; + max-width: 1400px; + margin: 0 auto; + justify-content: center; +} + +.project-content { + flex: 1; + max-width: 800px; +} + +.project-contents { + margin-bottom: 2rem; + line-height: 1.6; + color: #a1a1aa; +} + +.project-title { + padding: 50px 0; + background: linear-gradient(to right, #1f1f23, #27272a); + text-align: center; +} + +.project-title h1 { + margin: 0; + color: white; + font-size: 2.5rem; + font-weight: 700; +} + +.details-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; + margin-bottom: 32px; +} + +.detail-item { + background: #27272a; + padding: 16px; + border-radius: 8px; + display: flex; + align-items: center; + gap: 12px; + border: 1px solid #3f3f46; +} + +.detail-item .label { + color: #a1a1aa; + font-weight: 500; + min-width: 80px; +} + +.badge { + padding: 4px 12px; + border-radius: 9999px; + font-size: 0.875rem; + font-weight: 600; +} + +.status-badge { + background-color: rgba(217, 119, 6, 0.2); + color: #f59e0b; +} + +.priority-badge { + background-color: rgba(38, 220, 90, 0.1); + color: #22c55e; +} + +.details-grids { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; + margin-bottom: 32px; +} + +.info-box { + background-color: #27272a; + border-radius: 8px; + padding: 20px; + border: 1px solid #3f3f46; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.info-box:hover { + transform: translateY(-2px); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); +} + +.info-content { + display: flex; + flex-direction: column; + gap: 8px; +} + +.info-header { + display: flex; + align-items: center; + gap: 8px; +} + +.info-label { + color: #a1a1aa; + font-size: 0.875rem; +} + +.info-value { + font-size: 1.5rem; + font-weight: 700; + color: #f4f4f5; +} + +.feature-list { + width: 300px; + background: #27272a; + border-radius: 16px; + padding: 24px; + border: 1px solid #3f3f46; +} + +.feature-list h2 { + font-size: 1.5rem; + margin-bottom: 20px; + color: #f4f4f5; + text-align: center; + font-weight: 600; +} + +.feature-list ul { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 12px; +} + +.feature-list li { + background: #3f3f46; + padding: 12px 16px; + border-radius: 8px; + color: #f4f4f5; + transition: transform 0.2s ease; +} + +.feature-list li:hover { + transform: translateX(4px); + background: #52525b; +} + +.owner-info { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + padding: 4px 12px; + border-radius: 9999px; + background: #3f3f46; + transition: background-color 0.2s ease; +} + +.owner-info:hover { + background: #52525b; +} + +.avatar-placeholder { + width: 24px; + height: 24px; + background: #52525b; + border-radius: 50%; +} + +/* Responsive Styles */ +@media (max-width: 1200px) { + .project-details { + flex-direction: column; + padding: 24px 16px; + } + + .project-content { + max-width: 100%; + } + + .feature-list { + width: 100%; + max-width: none; + } +} + +@media (max-width: 768px) { + .project-title h1 { + font-size: 2rem; + } + + .details-grid { + grid-template-columns: 1fr; + } + + .details-grids { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 480px) { + .project-title { + padding: 32px 0; + } + + .project-title h1 { + font-size: 1.75rem; + } + + .details-grids { + grid-template-columns: 1fr; + } + + .detail-item { + flex-direction: column; + text-align: center; + } + + .detail-item .label { + margin-bottom: 4px; + } + + .owner-info { + width: 100%; + justify-content: center; + } +} \ No newline at end of file diff --git a/client/src/component/HeroBanner/index.js b/client/src/component/HeroBanner/index.js new file mode 100644 index 0000000..20d9b86 --- /dev/null +++ b/client/src/component/HeroBanner/index.js @@ -0,0 +1,167 @@ +import React, { useEffect, useState, useCallback } from "react" +import { useNavigate } from 'react-router-dom' +import typeWriter from './script.js' +import './style.css' +import { searchByAllFields } from "../../api/projectApi.js" +import debounce from 'lodash.debounce' + +export default function HeroBanner() { + const [searchTerm, setSearchTerm] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [showDropdown, setShowDropdown] = useState(false); + const navigate = useNavigate(); + + const handleSearchChange = useCallback( + debounce((value) => setSearchTerm(value), 300), + [] + ); + + useEffect(() => { + const cleanup = typeWriter(); + return () => cleanup(); + }, []); + + useEffect(() => { + let isMounted = true; + setError(null); + + const searchProjects = async () => { + setIsLoading(true); + try { + const results = await searchByAllFields(searchTerm); + if (isMounted) { + setSearchResults(results.data || []); + setShowDropdown(results.data && results.data.length > 0); + } + } catch (error) { + if (isMounted) { + setError(error.message); + console.error('Search error:', error); + } + } finally { + if (isMounted) { + setIsLoading(false); + } + } + }; + + if (searchTerm) { + searchProjects(); + } else { + setSearchResults([]); + setShowDropdown(false); + } + + return () => { + isMounted = false; + }; + }, [searchTerm]); + + const filteredResults = { + projects: [], + clients: [], + teams: [] + }; + + const clientSet = new Set(); + + searchResults.forEach(item => { + if (item.name && item.name.toLowerCase().startsWith(searchTerm.toLowerCase())) { + filteredResults.projects.push({ id: item._id, name: item.name, type: 'project' }); + } + if (item.client_id && item.client_id.name && item.client_id.name.toLowerCase().startsWith(searchTerm.toLowerCase())) { + if (!clientSet.has(item.client_id._id)) { + filteredResults.clients.push({ id: item.client_id._id, name: item.client_id.name, type: 'client' }); + clientSet.add(item.client_id._id); + } + } + if (item.team_id && item.team_id.teamTitle && item.team_id.teamTitle.toLowerCase().startsWith(searchTerm.toLowerCase())) { + filteredResults.teams.push({ id: item.team_id._id, name: item.team_id.teamTitle, type: 'team' }); + } + }); + + const allResults = [...filteredResults.projects, ...filteredResults.clients, ...filteredResults.teams]; + const hasResults = allResults.length > 0; + + const handleItemClick = (item) => { + switch (item.type) { + case 'project': + navigate(`/platform-management-feature/projectdetails/${item.id}`); + break; + case 'client': + navigate(`/platform-management-feature/Client/${item.id}`); + break; + case 'team': + navigate(`/platform-management-feature/Pod/${item.id}`); + break; + default: + console.error('Unknown item type:', item.type); + } + setShowDropdown(false); + }; + + + return ( +
+
+
+
+

+ +
+ +
+ handleSearchChange(e.target.value)} + style={{ + borderRadius: "8px", + height: "56px", + fontSize: "1.5rem", + paddingInline: "30px", + background: "transparent", + border: "1px #86858b solid", + color: "white" + }} + /> + {showDropdown && hasResults && ( +
+
    + {allResults.map((item, index) => ( +
  • handleItemClick(item)} + style={{ + padding: '10px', + borderBottom: '1px solid #eee', + cursor: 'pointer', + color: 'black' + }} + > + {item.name} +
  • + ))} +
+
+ )} +
+
+
+ ) +} + diff --git a/client/src/component/HeroBanner/script.js b/client/src/component/HeroBanner/script.js new file mode 100644 index 0000000..e927f14 --- /dev/null +++ b/client/src/component/HeroBanner/script.js @@ -0,0 +1,56 @@ +let i = 0; +const text = "Welcome to Portfolio Management"; +const typingSpeed = 200; +const pauseDuration = 3000; +let activeTimeout = null; +let isActive = false; + +export default function typeWriter() { + // If already running, clean up first + if (isActive) { + if (activeTimeout) { + clearTimeout(activeTimeout); + } + const typingElement = document.getElementById("typing"); + if (typingElement) { + typingElement.innerHTML = ''; + } + i = 0; + } + + isActive = true; + const typingElement = document.getElementById("typing"); + if (!typingElement) return; + + function type() { + if (!typingElement || !isActive) return; + + if (i < text.length) { + typingElement.innerHTML += text.charAt(i); + i++; + activeTimeout = setTimeout(type, typingSpeed); + } else { + activeTimeout = setTimeout(() => { + if (typingElement && isActive) { + typingElement.innerHTML = ''; + i = 0; + type(); + } + }, pauseDuration); + } + } + + type(); + + + return () => { + isActive = false; + if (activeTimeout) { + clearTimeout(activeTimeout); + } + if (typingElement) { + typingElement.innerHTML = ''; + } + i = 0; + }; +} \ No newline at end of file diff --git a/client/src/component/HeroBanner/style.css b/client/src/component/HeroBanner/style.css new file mode 100644 index 0000000..52417db --- /dev/null +++ b/client/src/component/HeroBanner/style.css @@ -0,0 +1,158 @@ +.hero-container { + height: 55vh; + width: 100%; + background-color: #070707; + background: radial-gradient(closest-side at left,#0e0a0a,); + display: flex; + justify-content: center; + align-items: center; +} + +.content-wrapper { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + max-width: 1350px; + gap: 3rem; + position: relative; + +} + +#typing { + font-size: 56px; + color: #ffffff; + min-height: 50px; + transition: all 0.4s ease; + margin-bottom: 2rem; + text-align: center; + +} + +.carousel-container { + width: 100%; + overflow: hidden; +} + +.carousel-wrapper { + display: flex; + transition: transform 0.5s ease-in-out; +} + +.carousel-slide { + flex: 0 0 calc(100% / 3); + padding: 0 10px; + box-sizing: border-box; +} + +.carousel-slide-content { + background-color: rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 20px; + color: #ffffff; + text-align: center; + height: 300px; +} + +.carousel-slide-content h3 { + font-size: 1.5rem; + margin-bottom: 1rem; +} + +.carousel-slide-content p { + font-size: 1rem; +} + + +.typing-container { + display: flex; + align-items: center; + z-index:10 +} + +.cursor { + display: inline-block; + width: 4px; + height: 4rem; + background-color: white; + margin-left: 4px; + margin-bottom:25px; + animation: blink 1s step-end infinite; +} + + + +.oval-background { + width: 300px; + height: 300px; + background-color:rgb(73, 68, 105); + background-size: 100% 100%; + background-repeat: no-repeat; + border-radius: 50%; + position: absolute; + filter: blur(11.5rem); + } + + .search-bar input{ + width: 700px; + } + + .search + { + width:700px; + } + + @media (max-width: 1024px) +{ + .search-bar .search + { + width:500px !important; + } +} + +@media (max-width: 606px) { + .search-bar .search + { + width:300px !important; + } +} + + @media (max-width: 768px) { + .carousel-slide { + flex: 0 0 100%; + } + +} + +@media (max-width:510px) +{ + .oval-background + { + width: 300px; + height: 300px; + } +} + +@media (max-width:1100px) +{ + #typing { + font-size: 3rem; + } + +} + +@media (max-width: 400px) +{ + #typing { + font-size: 2rem; + } +} + +@keyframes blink { + from, to { + background-color: transparent; + } + 50% { + background-color: white; + } + } \ No newline at end of file diff --git a/client/src/component/Imports/index.jsx b/client/src/component/Imports/index.jsx new file mode 100644 index 0000000..b4a2a46 --- /dev/null +++ b/client/src/component/Imports/index.jsx @@ -0,0 +1,128 @@ +import React, { useState, useEffect } from 'react'; +import './styles.css'; +import { useNavigate } from "react-router-dom"; +const PopupAlert = ({ message, type }) => ( +
+ {message} +
+); + +export default function ImportForm() { + const navigate = useNavigate(); + const [file, setFile] = useState(null); + const [importing, setImporting] = useState(false); + const [error, setError] = useState(null); + const [successMessages, setSuccessMessages] = useState([]); + const [, setImportComplete] = useState(false); + + + useEffect(() => { + if (successMessages.length > 0) { + const timer = setTimeout(() => { + setSuccessMessages([]); + }, 5000); + return () => clearTimeout(timer); + } + }, [successMessages]); + + const handleFileChange = (e) => { + if (e.target.files && e.target.files[0]) { + setFile(e.target.files[0]); + setError(null); + } + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + if (!file) { + setError('Please select a file to import'); + return; + } + + setImporting(true); + setError(null); + setSuccessMessages([]); + + const formData = new FormData(); + formData.append('file', file); + + try { + const response = await fetch('http://localhost:4000/api/import', { + method: 'POST', + body: formData, + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'An error occurred during import'); + } + + setSuccessMessages(data.results); + setImportComplete(true); + + // Navigate to success page after 5 seconds + setTimeout(() => { + navigate(`/platform-management-feature/portfolio`); + }, 5000); + } catch (err) { + setError(err.message || 'An unknown error occurred'); + setImportComplete(false); + } finally { + setImporting(false); + } + }; + + return ( +
+
+

Import Projects

+
+
+
📁
+
+ + or drag and drop your file here +
+ + {file && ( +
+ Selected: {file.name} +
+ )} +
+ + {error && } + + {successMessages.map((message, index) => ( + + ))} + +
+ + +
+ +
+
+ ); +} + diff --git a/client/src/component/Imports/styles.css b/client/src/component/Imports/styles.css new file mode 100644 index 0000000..25090c3 --- /dev/null +++ b/client/src/component/Imports/styles.css @@ -0,0 +1,104 @@ +.containerss { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + } + + .cardsss { + width: 100%; + max-width: 800px; + padding: 2rem; + background-color: white; + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + text-align: center; /* Center the title */ + } + + .titless { + font-size: 1.5rem; + font-weight: bold; + margin-bottom: 1rem; + color:black; + } + + .formss { + display: flex; + flex-direction: column; + gap: 1rem; + } + + .dropzoness { + border: 2px dashed #ccc; + border-radius: 4px; + padding: 2rem; + text-align: center; + cursor: pointer; + transition: border-color 0.3s ease; + } + + .dropzoneActivess { + border-color: #007bff; + } + + .uploadIconss { + font-size: 3rem; + margin-bottom: 1rem; + color: #666; + } + + .fileNamess { + margin-top: 1rem; + font-size: 0.9rem; + color: #666; + } + + .buttonContainerss { + display: flex; + justify-content: center; /* Center buttons */ + gap: 1rem; + margin-top: 1rem; + } + + .buttonss { + padding: 0.5rem 1rem; + border-radius: 4px; + border: none; + cursor: pointer; + font-size: 1rem; + } + + .cancelButtonss { + background-color: #f0f0f0; + color: #333; + } + + .submitButtonss { + background-color: #007bff; + color: white; + } + + .disabledButtonss { + opacity: 0.5; + cursor: not-allowed; + } + + .alertss { + padding: 1rem; + border-radius: 4px; + margin-bottom: 1rem; + } + + .errorAlertss { + background-color: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; + } + + .successAlertss { + background-color: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; + } + \ No newline at end of file diff --git a/client/src/component/InitialsAvatar/InitialsAvatar.jsx b/client/src/component/InitialsAvatar/InitialsAvatar.jsx new file mode 100644 index 0000000..e174ff9 --- /dev/null +++ b/client/src/component/InitialsAvatar/InitialsAvatar.jsx @@ -0,0 +1,34 @@ +import React from 'react'; + +export const InitialsAvatar = ({ name, className = '', style = {} }) => { + const getInitials = (name) => { + if (!name) return ''; + const words = name.split(' '); + return words.length === 1 + ? words[0][0].toUpperCase() + : `${words[0][0]}${words[1][0]}`.toUpperCase(); + }; + + return ( +
+ {getInitials(name)} +
+ ); +}; + +export default InitialsAvatar; diff --git a/client/src/component/NewNavbar/index.js b/client/src/component/NewNavbar/index.js new file mode 100644 index 0000000..bcec070 --- /dev/null +++ b/client/src/component/NewNavbar/index.js @@ -0,0 +1,60 @@ +import React from "react"; +import "./style.css"; +import { useNavigate } from "react-router-dom"; + +const NewNavbar = () => { + const navigate = useNavigate(); + + React.useEffect(() => { + let prevScrollPos = window.scrollY; + let isScrolling = false; + + const handleScroll = () => { + if (!isScrolling) { + window.requestAnimationFrame(() => { + const currentScrollPos = window.scrollY; + const navbar = document.querySelector('.new-navbar'); + + // Always show navbar at the top of the page + if (currentScrollPos === 0) { + navbar.classList.add('show'); + } + // Show navbar when scrolling up + else if (prevScrollPos > currentScrollPos + 5) { + navbar.classList.add('show'); + } + // Hide navbar when scrolling down + else if (prevScrollPos < currentScrollPos - 5) { + navbar.classList.remove('show'); + } + + prevScrollPos = currentScrollPos; + isScrolling = false; + }); + } + isScrolling = true; + }; + + // Show navbar initially + document.querySelector('.new-navbar').classList.add('show'); + + // Add scroll event listener to the window + window.addEventListener('scroll', handleScroll, { passive: true }); + + return () => window.removeEventListener('scroll', handleScroll); + }, []); + + return
+
navigate('/platform-management-feature/portfolio')}> + Portfolio +
+
+
navigate('/platform-management-feature/portfolio')}>Home
+
navigate('/platform-management-feature/import')}>Add
+
navigate('/')}>Collab AI
+ +
+
; +}; + +export default NewNavbar; diff --git a/client/src/component/NewNavbar/style.css b/client/src/component/NewNavbar/style.css new file mode 100644 index 0000000..ae00c61 --- /dev/null +++ b/client/src/component/NewNavbar/style.css @@ -0,0 +1,29 @@ +.new-navbar{ + background-color: #000; + width: 100%; + height: 70px; + color: #fff; + display: flex; + align-items: center; + padding: 0 2rem; + font-size: 20px; + position: fixed; + z-index: 10000; + top: 0; + transition: transform 0.3s ease-in-out; + transform: translateY(-100%); + border-bottom:1px solid #86858b; +} + +.new-navbar.show { + transform: translateY(0); +} + +.tabs +{ + display:flex; + justify-content: right; + align-items: center; + gap: 20px; + +} diff --git a/client/src/component/PodInfo/PodCard.jsx b/client/src/component/PodInfo/PodCard.jsx new file mode 100644 index 0000000..da92c2e --- /dev/null +++ b/client/src/component/PodInfo/PodCard.jsx @@ -0,0 +1,76 @@ +import React, { useState, useEffect } from "react"; +import { FaEllipsisH } from "react-icons/fa"; +import { getUsersByTeamId } from "../../api/projectApi.js"; +import '../../Pages/PortfolioManagement/PodInfo/PodInfo.scss'; + +export const PodCard = ({ team, onClick }) => { + const [activeMembers, setActiveMembers] = useState(0); + const [inactiveMembers, setInactiveMembers] = useState(0); + const [teamMembers, setTeamMembers] = useState([]); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchTeamMembers = async () => { + try { + const members = await getUsersByTeamId(team._id); + + if (!Array.isArray(members)) { + throw new Error('Fetched data is not an array'); + } + + const active = members.filter(member => member.status === 'active').length; + const inactive = members.length - active; + + setActiveMembers(active); + setInactiveMembers(inactive); + setTeamMembers(members); + setError(null); + } catch (error) { + console.error('Error fetching team members:', error); + setError(error.message); + } + }; + + fetchTeamMembers(); + }, [team._id]); + + if (error) { + return
Error: {error}
; + } + + return ( +
+
+
+ {team.teamTitle[0]} +
+
+

{team.teamTitle}

+

{team.teamDescriptions}

+
+ +
+
+
+ {activeMembers} Active + + {inactiveMembers} Inactive +
+
+ {teamMembers.slice(0, 3).map((member, index) => ( +
+ {member.fname ? member.fname[0] : '?'} +
+ ))} + {teamMembers.length > 3 && ( +
+{teamMembers.length - 3}
+ )} +
+
+ +
+ ); +}; + diff --git a/client/src/component/PodInfo/SearchBar.jsx b/client/src/component/PodInfo/SearchBar.jsx new file mode 100644 index 0000000..593ab32 --- /dev/null +++ b/client/src/component/PodInfo/SearchBar.jsx @@ -0,0 +1,16 @@ +import React from 'react'; + +export const SearchBar = ({ searchTerm, onSearchChange }) => { + return ( +
+ +
+ ); +}; + diff --git a/client/src/component/PodInfo/TeamMemberCard.jsx b/client/src/component/PodInfo/TeamMemberCard.jsx new file mode 100644 index 0000000..ee6eb63 --- /dev/null +++ b/client/src/component/PodInfo/TeamMemberCard.jsx @@ -0,0 +1,26 @@ +import React from 'react'; +import '../../Pages/PortfolioManagement/PodInfo/PodInfo.scss' +import {InitialsAvatar} from '../../component/InitialsAvatar/InitialsAvatar' +export function TeamMemberCard({ member }) { + return ( +
+
+
+ { + + } +
+
+

{`${member.fname} ${member.lname}`}

+

+ {member.roles_in_project && member.roles_in_project.length > 0 + ? member.roles_in_project.join(', ') + : 'No assigned role'} +

+

{member.email}

+
+
+ ) +} + + diff --git a/client/src/component/PodInfo/TeamModal.jsx b/client/src/component/PodInfo/TeamModal.jsx new file mode 100644 index 0000000..a9235a8 --- /dev/null +++ b/client/src/component/PodInfo/TeamModal.jsx @@ -0,0 +1,225 @@ +import React, { useState, useEffect } from 'react' +import { FaSearch } from 'react-icons/fa' +import { TeamMemberCard } from './TeamMemberCard' +import ProjectCard from '../../component/ProjectCard' +import { getUsersByTeamId, getProjectsByTeam, getTeamById } from '../../api/projectApi' + +export const TeamModal = ({ teamId, onClose }) => { + const [searchTerm, setSearchTerm] = useState('') + const [projectSearchTerm, setProjectSearchTerm] = useState('') + const [currentMemberPage, setCurrentMemberPage] = useState(1) + const [currentProjectPage, setCurrentProjectPage] = useState(1) + const [activeView, setActiveView] = useState('members') + const [teamMembers, setTeamMembers] = useState([]) + const [projects, setProjects] = useState([]); + const [teamName, setTeamName] = useState('') + const [error, setError] = useState(null) + const itemsPerPage = 3 + + useEffect(() => { + const fetchTeamData = async () => { + if (!teamId) { + setError('Team ID is undefined'); + return; + } + + try { + const [membersData, projectsData, teamData] = await Promise.all([ + getUsersByTeamId(teamId), + getProjectsByTeam(teamId), + getTeamById(teamId) + ]); + + setTeamMembers(membersData); + + // Deduplicate projects + const uniqueProjects = Array.from(new Set(projectsData.map(p => p._id))) + .map(id => projectsData.find(p => p._id === id)); + setProjects(uniqueProjects); + + setTeamName(teamData.teamTitle); + } catch (error) { + console.error('Error fetching team data:', error); + setError('Failed to load team data. Please try again later.'); + } + }; + + fetchTeamData(); + }, [teamId]); + const filteredMembers = teamMembers + .map((member) => ({ + ...member, + roles_in_project: member.roles_in_project + ? [...new Set(member.roles_in_project)] // Deduplicate roles + : [], + })) + .filter(member => + member.fname.toLowerCase().includes(searchTerm.toLowerCase()) || + member.lname.toLowerCase().includes(searchTerm.toLowerCase()) || + (member.roles_in_project && member.roles_in_project.some(role => + role.toLowerCase().includes(searchTerm.toLowerCase()))) || + (member.username && member.username.toLowerCase().includes(searchTerm.toLowerCase())) + ); + + + const memberPageCount = Math.ceil(filteredMembers.length / itemsPerPage) + const currentMembers = filteredMembers.slice( + (currentMemberPage - 1) * itemsPerPage, + currentMemberPage * itemsPerPage + ) + + const filteredProjects = projects.filter(project => + project.name.toLowerCase().includes(projectSearchTerm.toLowerCase()) + ) + const projectPageCount = Math.ceil(filteredProjects.length / itemsPerPage) + const currentProjects = filteredProjects.slice( + (currentProjectPage - 1) * itemsPerPage, + currentProjectPage * itemsPerPage + ) + + const resetPagination = () => { + setCurrentMemberPage(1) + setCurrentProjectPage(1) + setProjectSearchTerm('') + } + + if (error) { + return ( +
+
+
+

Error

+ +
+
+

{error}

+
+
+
+ ) + } + + return ( +
+
+
+

{teamName}

+ +
+
+
+ + +
+ + {activeView === 'members' ? ( + <> +
+
+ + setSearchTerm(e.target.value)} + className="searchInput1" + /> +
+
+
+ {currentMembers.length > 0 ? ( + currentMembers.map((member) => ( + + )) + ) : ( +
No team member found
+ )} +
+ +
+ setCurrentMemberPage(prev => Math.max(prev - 1, 1))} + disabled={currentMemberPage === 1} + > + Previous + + {currentMemberPage} of {memberPageCount} + setCurrentMemberPage(prev => Math.min(prev + 1, memberPageCount))} + disabled={currentMemberPage === memberPageCount} + > + Next + +
+ + ) : ( + <> +
+
+ + setProjectSearchTerm(e.target.value)} + className="searchInput1" + /> +
+
+
+
+ {currentProjects.length > 0 ? ( + currentProjects.map((project, index) => ( + + )) + ) : ( +
No projects found
+ )} +
+ +
+
+ setCurrentProjectPage(prev => Math.max(prev - 1, 1))} + disabled={currentProjectPage === 1} + > + Previous + + {currentProjectPage} of {projectPageCount} + setCurrentProjectPage(prev => Math.min(prev + 1, projectPageCount))} + disabled={currentProjectPage === projectPageCount} + > + Next + +
+ + )} +
+
+
+ ) +} + diff --git a/client/src/component/ProjectCard/index.jsx b/client/src/component/ProjectCard/index.jsx new file mode 100644 index 0000000..29b5edd --- /dev/null +++ b/client/src/component/ProjectCard/index.jsx @@ -0,0 +1,63 @@ +import React from 'react'; +import './style.css'; +import { FaCalendarAlt } from 'react-icons/fa'; +import { useNavigate } from 'react-router-dom'; +import {InitialsAvatar} from '../../component/InitialsAvatar/InitialsAvatar'; +import Badge from '../ClientInfo/Badge'; + +const ProjectCard = ({ project, viewType = 'card' }) => { + const navigate = useNavigate(); + + // Function to handle project click + const handleProjectClick = (id) => { + navigate(`/platform-management-feature/projectdetails/${id}`); + }; + + return ( +
handleProjectClick(project._id)}> +
+ {project.image_link ? ( + {project.name} + ) : ( + + )} +
+
+
+

{project.name}

+

{project.client_id.name}

+ {project.description.length > 20 ? ( + <> +

{project.description.substring(0, 50)}......

+ + + ) : ( +

{project.description}

+ )} +
+ {project.status || 'In progress'} +
+ +
+ + {new Date(project.start_time).toLocaleDateString()}{new Date(project.end_time).toLocaleDateString()} +
+
+ {project.techStack.map((tech, index) => ( + {tech.name} + ))} +
+ +
+
+ ); +}; + +export default ProjectCard; \ No newline at end of file diff --git a/client/src/component/ProjectCard/style.css b/client/src/component/ProjectCard/style.css new file mode 100644 index 0000000..283b041 --- /dev/null +++ b/client/src/component/ProjectCard/style.css @@ -0,0 +1,104 @@ +.card-container { + background: rgba(255, 255, 255, 0.1); + border-radius: 16px; + overflow: hidden; + transition: transform 0.3s ease; + border: 2px rgb(71 68 68) solid; +} + +.card-container:hover { + transform: translateY(-5px); +} + +.image-style { + width: 100%; + height: 200px; + object-fit: cover; +} + +.content-area { + padding: 1.5rem; +} + +.content-area h3 { + margin: 0 0 1rem 0; + color: #ffffff; +} + +.content-area p { + color: #929292; + margin-bottom: 1rem; +} + +.technologies { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 1rem; +} + +.technology-label { + background: #383838; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.8rem; + color: #ffffff; +} + +.links-section { + display: flex; + gap: 1rem; +} + +.links-section a { + text-decoration: none; + color: #007bff; + font-weight: 500; +} + +.links-section a:hover { + text-decoration: underline; +} + +/* List view styles */ +.projects-container.list-view { + display: flex; + flex-direction: column; + gap: 1rem; + max-width: 1200px; + margin: 0 auto; +} + +.card-container.list-view { + display: flex; + width: 100%; +} + +.card-container.list-view .image-style { + width: 200px; + height: 150px; +} + +.card-container.list-view .content-area { + flex: 1; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +/* Keep existing card view styles */ +.projects-container.card-view { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 1rem; +} + +.project-start #text +{ + color:white !important; +} + +@media (max-width: 768px) { + /* ... existing styles ... */ +} \ No newline at end of file diff --git a/client/src/component/ProjectForm/ProjectForm.css b/client/src/component/ProjectForm/ProjectForm.css new file mode 100644 index 0000000..343f8c4 --- /dev/null +++ b/client/src/component/ProjectForm/ProjectForm.css @@ -0,0 +1,115 @@ +.project-form { + max-width: 600px; + margin: 0 auto; + padding: 20px; + border: 1px solid #ccc; + border-radius: 8px; + background-color: #f9f9f9; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +.project-form-title { + text-align: center; + color: #333; + margin-bottom: 20px; +} + +.form-group { + margin-bottom: 15px; +} + +.form-label { + display: block; + margin-bottom: 5px; + font-weight: bold; +} + +.form-input, +.form-textarea { + width: 100%; + padding: 10px; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; +} + +.form-input:focus, +.form-textarea:focus { + border-color: #007bff; + outline: none; +} + +.form-submit-btn { + width: 100%; + padding: 10px; + background-color: #007bff; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.form-submit-btn:hover { + background-color: #0056b3; +} + +/* Dropdown Styles */ +.dropdown-check-list { + position: relative; + display: inline-block; + width: 100%; + cursor: pointer; +} + +.dropdown-anchor { + padding: 10px; + border: 1px solid #ccc; + border-radius: 4px; + background-color: #f9f9f9; + display: flex; + justify-content: space-between; + align-items: center; +} + +.dropdown-items { + display: none; + position: absolute; + background-color: white; + border: 1px solid #ccc; + border-radius: 4px; + z-index: 1000; + max-height: 200px; + overflow-y: auto; + width: 100%; + list-style-type: none; + padding: 0; + margin: 0; +} + +.dropdown-check-list:hover .dropdown-items, +.dropdown-items.show { + display: block; +} + +.dropdown-item { + padding: 8px 12px; + cursor: pointer; + display: flex; + align-items: center; +} + +.dropdown-item:hover { + background-color: #f0f0f0; +} + +.dropdown-checkbox { + margin-right: 10px; +} + +/* Specific Dropdown Variations */ +.client-dropdown, +.features-dropdown, +.tech-stack-dropdown { + margin-bottom: 15px; +} \ No newline at end of file diff --git a/client/src/component/ProjectForm/index.jsx b/client/src/component/ProjectForm/index.jsx new file mode 100644 index 0000000..8264a75 --- /dev/null +++ b/client/src/component/ProjectForm/index.jsx @@ -0,0 +1,285 @@ +import React, { useState, useEffect, useRef } from 'react'; +import axios from 'axios'; +import './ProjectForm.css'; // Import the CSS file + +const ProjectForm = () => { + const [formData, setFormData] = useState({ + name: '', + description: '', + status: '', + start_time: '', + end_time: '', + budget: '', + hr_taken: '', + client_id: '', + techStack: [], + links: { + links: '', + github: '', + }, + image_link: '', + team_id: '', + feature: [], + }); + + const [features, setFeatures] = useState([]); // State for features + const [clients, setClients] = useState([]); // State for clients + const [selectedClients, setSelectedClients] = useState([]); // State for selected clients + const [isDropdownVisible, setIsDropdownVisible] = useState(false); + + const featuresRef = useRef(); // Ref for features dropdown + const techStackRef = useRef(); // Ref for tech stack dropdown + + const handleClickOutside = (event) => { + if (featuresRef.current && !featuresRef.current.contains(event.target)) { + setIsFeaturesDropdownVisible(false); + } + if (techStackRef.current && !techStackRef.current.contains(event.target)) { + setIsTechStackDropdownVisible(false); + } + if (isDropdownVisible && !event.target.closest('.dropdown-check-list')) { + setIsDropdownVisible(false); + } + }; + + useEffect(() => { + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + const [isFeaturesDropdownVisible, setIsFeaturesDropdownVisible] = useState(false); + const [isTechStackDropdownVisible, setIsTechStackDropdownVisible] = useState(false); + + const toggleFeaturesDropdown = () => { + setIsFeaturesDropdownVisible(!isFeaturesDropdownVisible); + }; + + const toggleTechStackDropdown = () => { + setIsTechStackDropdownVisible(!isTechStackDropdownVisible); + }; + + const handleFeatureSelect = (featureId) => { + setFormData((prevData) => ({ + ...prevData, + feature: prevData.feature.includes(featureId) + ? prevData.feature.filter(id => id !== featureId) + : [...prevData.feature, featureId], + })); + }; + + const handleTechStackSelect = (techId) => { + setFormData((prevData) => ({ + ...prevData, + techStack: prevData.techStack.includes(techId) + ? prevData.techStack.filter(id => id !== techId) + : [...prevData.techStack, techId], + })); + }; + + const handleClientSelect = (clientId) => { + setSelectedClients((prevSelected) => { + if (prevSelected.includes(clientId)) { + return prevSelected.filter(id => id !== clientId); // Remove if already selected + } else { + return [...prevSelected, clientId]; // Add if not selected + } + }); + }; + + useEffect(() => { + const fetchData = async () => { + try { + const featuresResponse = await axios.get('http://localhost:4000/api/features'); + console.log(featuresResponse.data); + const clientsResponse = await axios.get('http://localhost:4000/api/clients'); + console.log(clientsResponse); + const techStackResponse = await axios.get('http://localhost:4000/api/techStacks'); + console.log(techStackResponse); + const teamsResponse= await axios.get('http://localhost:4000/api/teams'); + + setFeatures(featuresResponse.data || []); + setClients(clientsResponse.data || []); + setFormData((prevData) => ({ + ...prevData, + techStack: techStackResponse.data || [], + })); + } catch (error) { + console.error('Error fetching data:', error); + } + }; + + fetchData(); + }, []); + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData((prevData) => ({ + ...prevData, + [name]: value, + })); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + try { + const response = await axios.post('/api/projects', formData); + console.log('Project created:', response.data); + } catch (error) { + console.error('Error creating project:', error); + } + }; + + return ( +
+

Create Project

+
+ + +
+
+ +