diff --git a/admin-client/.gitignore b/admin-client/.gitignore new file mode 100644 index 0000000..4d29575 --- /dev/null +++ b/admin-client/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/admin-client/.vscode/settings.json b/admin-client/.vscode/settings.json new file mode 100644 index 0000000..30de70f --- /dev/null +++ b/admin-client/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.acceptSuggestionOnEnter": "on" +} \ No newline at end of file diff --git a/admin-client/README.md b/admin-client/README.md new file mode 100644 index 0000000..ce3d0bc --- /dev/null +++ b/admin-client/README.md @@ -0,0 +1,39 @@ +## DEPLOYED AT +https://adnans-react-typescript-todo.netlify.app/ + + +credentials: + +username: "AdnanKhan" +password: "AdnanKhan@4069" + + +Frontend Engineer Assignment + +Create a React Application with a login page and a dashboard page. The +dashboard should display the user’s profile information and a list of the user’s +todos. +For authentication, user profile info You can use a JSON file or a static +JavaScript object for mock username & password data. +Implement the following features in the application: +1. Implement user authentication using a secure token-based authentication +such as JWT. Without login, the user should not be able to see the dashboard. +2. The app should use React Context API to store login user information. +3. Implement the logout functionality. +4. In the dashboard, add a button through which new Todos can be added, +when creating a todo it can be stored in a global context. +5. Add a feature that allows the addition of Nested Todo, (Sub Task), It is +entirely up to you how you design this feature. get inspiration from existing +Todo apps +6. Make sure to sanitize and validate user inputs to prevent injection attacks. + +7. Implement a route guard that requires the user to be successfully +authenticated before they can view their dashboard. If the user is not +authenticated, they should be redirected to the login page. +8. Implement proper error handling in the code. +9. Use your own creativity to make a better user experience. +10. The app can be hosted on any platform i.e Netlify, or Vercel. And source +code should be publically accessible on any git repository to review. +11. The source code must be in Typescript. +Candidates can take inspiration from this screenshot below to create their own +dashboard experience. \ No newline at end of file diff --git a/admin-client/package.json b/admin-client/package.json new file mode 100644 index 0000000..1f71730 --- /dev/null +++ b/admin-client/package.json @@ -0,0 +1,58 @@ +{ + "name": "todo-app", + "version": "0.1.0", + "private": true, + "dependencies": { + "@chakra-ui/icons": "^2.1.0", + "@chakra-ui/react": "^2.8.0", + "@emotion/react": "^11.11.1", + "@emotion/styled": "^11.11.0", + "@mui/material": "^5.13.7", + "@mui/styled-engine-sc": "^5.12.0", + "@reduxjs/toolkit": "^1.9.5", + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^13.4.0", + "@testing-library/user-event": "^13.5.0", + "@types/jest": "^27.5.2", + "@types/node": "^16.18.35", + "@types/node-sass": "^4.11.3", + "@types/react": "^18.2.14", + "@types/react-dom": "^18.2.6", + "@types/react-redux": "^7.1.25", + "framer-motion": "^10.12.16", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-redux": "^8.1.1", + "react-router-dom": "^6.12.1", + "react-scripts": "^5.0.1", + "redux-thunk": "^2.4.2", + "sass": "^1.64.1", + "styled-components": "^5.3.11", + "typescript": "^4.9.5", + "web-vitals": "^2.1.4" + }, + "scripts": { + "start": "ENV=LOCAL react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/admin-client/public/_redirects b/admin-client/public/_redirects new file mode 100644 index 0000000..f824337 --- /dev/null +++ b/admin-client/public/_redirects @@ -0,0 +1 @@ +/* /index.html 200 \ No newline at end of file diff --git a/admin-client/public/favicon.ico b/admin-client/public/favicon.ico new file mode 100644 index 0000000..2911301 Binary files /dev/null and b/admin-client/public/favicon.ico differ diff --git a/admin-client/public/index.html b/admin-client/public/index.html new file mode 100644 index 0000000..3ada0ec --- /dev/null +++ b/admin-client/public/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + Todo App + + +
+ +
+ + diff --git a/admin-client/public/manifest.json b/admin-client/public/manifest.json new file mode 100644 index 0000000..1f2f141 --- /dev/null +++ b/admin-client/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/admin-client/public/robots.txt b/admin-client/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/admin-client/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/admin-client/src/App.scss b/admin-client/src/App.scss new file mode 100644 index 0000000..13e4b0d --- /dev/null +++ b/admin-client/src/App.scss @@ -0,0 +1,273 @@ +@import url('https://fonts.googleapis.com/css2?family=M+PLUS+Rounded+1c:wght@100;300;400;500;700;800;900&family=Meera+Inimai&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&family=Signika:wght@300;400;500;600;700&family=Varela+Round&display=swap'); + +@import './styles/typography/typography.scss'; +$black1: #000000; +$black2: #303030; +$black3: #606060; +$black4: #909090; +$black5: #aeaeae; +$black6: #c1c1c1; +$black7: #dcdcdc; +$white: #ffffff; + +// font-family: 'M PLUS Rounded 1c', sans-serif; +// font-family: 'Meera Inimai', sans-serif; +// font-family: 'Roboto', sans-serif; +// font-family: 'Signika', sans-serif; +// font-family: 'Varela Round', sans-serif; + + + +.truncate-text { + overflow: hidden; + text-overflow: ellipsis; +} +#root{ + width: 100%; + padding: 0 0px; + display: flex; + height: 100vh; + + .main_container{ + &.dark_mode{ + color:#dcdcdc + } + } +} +body{ + margin:0; + padding:0; + background:$black1; + color:$black1; + font-weight: 300; + display:flex; + justify-content: center; + align-items: center; + letter-spacing: 0.04em; + + + select{ + border-radius: 5px; + padding: 2px; + font-family: inherit; + background: rgba(255, 255, 255, 0.5); + border: 1px solid #ffffff57; + outline: 1px solid #00000030; + } + + button{ + border: none; + border-radius: 10px; + cursor:pointer; + padding:0 10px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.5); + border: 1px solid #ffffff57; + outline: 1px solid #00000030; + color: #000000; + + @include typography( + $subheading-size, + 500, + $default-line-height, + #000000 + ); + + .btn_text{ + @include typography( + $subheading-size, + 500, + $default-line-height, + #000000 + ); + &.dark{ + color:#000000 + } + } + + &:hover{ + background: rgba(255, 255, 255, 0.35); + } + } + + .content{ + font-weight: 400; + } + + a{ + text-decoration: none; + color:currentColor; + } + + input{ + flex:1; + border: none; + border-radius: 10px; + font-family: inherit; + background: rgba(255, 255, 255, 0.5); + border: 1px solid #ffffff57; + outline: 1px solid #00000030; + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + cursor:pointer; + @include typography( + $heading6-size, + 300, + unset, + #000000 + ); + padding:10px !important; + + } + + .truncate { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + *{ + // font-family: 'M PLUS Rounded 1c', sans-serif; +// font-family: 'Meera Inimai', sans-serif; +font-family: 'Roboto', sans-serif; +// font-family: 'Signika', sans-serif; +// font-family: 'Varela Round', sans-serif; + font-weight: 400; + // transition: transform 0.3s 0.1s ease-in-out; + &.dark{ + color:#ffffff + } + } + + .main_container{ + display: flex; + flex-direction: column; + align-items: center; + position: relative; + flex: 1 1; + height: 100vh; + justify-content: center; + position:relative; + + .image_container{ + width:100vw; + height:100vh; + top:0; + left:0; + position:absolute; + &>img{ + width: 100vw; + height: 100vh; + object-fit: cover; + object-position: center center; + position:absolute; + } + } + .image_backdrop{ + position: absolute; + width: 100vw; + height: 100vh; + background-color: transparent; + backdrop-filter: blur(21px); + } + + + + .blur_filter{ + background-color: #ffffff23; + width:100vw; + height:100vh; + position:absolute; + opacity:1; + backdrop-filter: blur(6px); + } + + .main_register_container{ + position:absolute; + .glassmorphic-background{ + &>h1{ + text-align: center; + } + } + + } + + .main_login_container{ + padding:0; + position:absolute; + .glassmorphic-background{ + &>h1{ + text-align: center; + } + } + } + + .parent_todo_head{ + + h1{ + text-align: center; + } + .main_input_container{ + display: flex; + align-items: center; + } + .main_input{ + margin: 0 20px 0 20px !important; + width: 300px; + } + } + + } + .todos_list_container{ + width: 100%; + display: flex; + flex-direction: column; + margin-top: 10px; + } + input{ + border-radius: 5px; + border: none; + font-size: 16px; + background: rgb(255 255 255 / 24%); + -webkit-backdrop-filter: blur(12px); + backdrop-filter: blur(4px); + width:calc(100% - 20px); + } + textarea{ + border-radius: 10px; + border: none; + font-size: 16px; + background: rgb(255 255 255 / 24%); + -webkit-backdrop-filter: blur(12px); + backdrop-filter: blur(4px); + padding:0 10px; + font-family: 'Roboto', sans-serif; + font-weight: 300; + width:calc(100% - 20px); + resize: none; + } + + input[type="text"], input[type="password"] { + height: 12px; + padding:0 10px; + font-family: 'Roboto', sans-serif; + font-weight: 300; + flex:1; + } + +input[type="text"]::placeholder, +textarea::placeholder { + font-family: 'Roboto', sans-serif; +} +} + +textarea { + word-wrap: break-word; + overflow-wrap: break-word; + font-size: 4px; +} + +.opacity0\&disable{ + opacity:0; + pointer-events: none; +} \ No newline at end of file diff --git a/admin-client/src/App.test.tsx b/admin-client/src/App.test.tsx new file mode 100644 index 0000000..2a68616 --- /dev/null +++ b/admin-client/src/App.test.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import App from './App'; + +test('renders learn react link', () => { + render(); + const linkElement = screen.getByText(/learn react/i); + expect(linkElement).toBeInTheDocument(); +}); diff --git a/admin-client/src/App.tsx b/admin-client/src/App.tsx new file mode 100644 index 0000000..ab5b15d --- /dev/null +++ b/admin-client/src/App.tsx @@ -0,0 +1,176 @@ +import React, { useEffect, useState } from "react"; +import "./App.scss"; +import { Navigate, Route, Routes } from "react-router-dom"; +import { useSelector } from "react-redux"; +import { RootState } from "./ReduxStore/store"; + +import bgDark from './medias/bgfinaldark.jpg' +import bgLight from './medias/bgfinallight.jpg' +import LoginPage from "./Pages/Admin/LoginPage"; +import RegisterPage from "./Pages/Admin/RegisterPage"; +import Loader from "./components/UIComponents/Loader/Loader"; +import { useDispatch } from "react-redux"; +import { /*UserLogout,*/ setAllUserData, setToken } from "./ReduxStore/UserSlice"; +import DashboardWrapper from "./components/WRAPPERS/DashboardWrapper/DashboardWrapper"; +import TodosListContainer from "./components/UIComponents/Todos/TodosListContainer/TodosListContainer"; +import {/* UILogout,*/ setAllTodos, setDarkMode, setLoading } from "./ReduxStore/UISlice"; +import { getUrl, isDarkModeFromLocalStorage } from "./CONFIG"; +import TodoDetails from "./components/UIComponents/TodoDetails/TodoDetails"; +import NotFound from "./Pages/NotFound/NotFound"; +import Profile from "./components/UIComponents/Profile/Profile"; +import ForgotPassword from "./components/Admin/ForgotPassword/ForgotPassword"; + +export interface TodoItem { + attachments: any[]; // You can specify the actual type for attachments if needed + collaborators: any[]; // You can specify the actual type for collaborators if needed + createdAt: string; + dependencies: any[]; // You can specify the actual type for dependencies if needed + description: string; + priority: string; + progress: number; + recurring: boolean; + status: string; + subtasks: any[]; // You can specify the actual type for subtasks if needed + tags: any[]; // You can specify the actual type for tags if needed + title: string; + todo: any[]; // You can specify the actual type for todo if needed + updatedAt: string; + user: string; + __v: number; + _id: string; +} +export function generateUniqueId(): string { + const timestamp = new Date().getTime(); + const randomNumber = Math.floor(Math.random() * 100000); + return `${timestamp}_${randomNumber}`; +} +const App: React.FC = () => { + + const [isAuthenticated, setIsAuthenticated] = useState(false); + + const isLoading = useSelector((state: RootState) => state.UI.loading); + // Inside your component or any other place where you want to trigger the API call + const dispatch = useDispatch(); + + const allTodos = useSelector((state: RootState) => state.User.allUserData.todos) + // const userAllData = useSelector((state: RootState) => state.User.allUserData) + const token = useSelector((state: RootState) => state.User.token) + // const userProfile = useSelector((state: RootState) => state.User.allUserData) + const theme = useSelector((state: RootState) => state.UI.theme) + + + + const fetchAllUserData = (token: string) => { + if (token) { + dispatch(setLoading(true)) + try { + if (token !== null) { + fetch(getUrl('/auth/profile'), { + method: 'GET', + headers: { + 'Authorization': token + } + }).then( + (res) => { + if (res.ok) { + return res.json() + } + } + ).then((jsonData) => { + // if(jsonData && jsonData.user){ + // console.log(jsonData) + dispatch(setAllUserData(jsonData.user)) + dispatch(setAllTodos(jsonData.user.todos)) + // } + }) + } + } catch (err) { + console.error('Error:', err); + } + dispatch(setLoading(false)) + } else { + throw new Error("Token is not present") + } + } + // Function to handle logout + const handleLogout = () => { + localStorage.removeItem("Token") + window.location.href = '/login' + }; + + useEffect(() => { + const localStorage_jwtToken = localStorage.getItem("Token") + if (!token && localStorage_jwtToken) { + dispatch(setToken(localStorage_jwtToken)) + setIsAuthenticated(true) + } else if (token && localStorage_jwtToken) { + setIsAuthenticated(true) + } else { + setIsAuthenticated(false) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [token]) + + useEffect(() => { + if (token) { + dispatch(setLoading(true)) + fetchAllUserData(token) + dispatch(setLoading(false)) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [token]) + + useEffect(() => { + // dispatch(setLoading(true)) + const darkMode = isDarkModeFromLocalStorage() + if (darkMode) { + dispatch(setDarkMode(true)) + } else { + dispatch(setDarkMode(false)) + } + // dispatch(setLoading(false)) + }, [dispatch, theme.dark]) + + return ( +
+ {theme.dark ?
bg
:
bg
} + {/*
*/} + + + {!isAuthenticated && ( + <> + } /> + } /> + } /> + } /> + + )} + + {isAuthenticated ? (<> + {/* } /> */} + + }> + {/* Nested routes for the dashboard */} + {/* } /> */} + } /> + } /> + } /> + } /> + } /> + } /> + + {/* Fallback route for any other unmatched paths */} + } /> + + ) : <>} + {/* Fallback route for other unmatched paths */} + } /> + + + +
+ ); +}; + +export default App; diff --git a/admin-client/src/CONFIG/index.ts b/admin-client/src/CONFIG/index.ts new file mode 100644 index 0000000..2a8f446 --- /dev/null +++ b/admin-client/src/CONFIG/index.ts @@ -0,0 +1,96 @@ +import { API_URL_LIVE, API_URL_LOCAL, isLive } from "../api"; + +export const getUrl =(remUrl:string) => `${isLive ? API_URL_LIVE : API_URL_LOCAL}${remUrl}` + +export function formatDateAndTime(dateObj: Date): [string,string] { + const day = dateObj.getDate(); + const month = dateObj.toLocaleString('default', { month: 'short' }); + const year = dateObj.getFullYear().toString().slice(-2); + + let daySuffix; + if (day === 1 || day === 21 || day === 31) { + daySuffix = 'st'; + } else if (day === 2 || day === 22) { + daySuffix = 'nd'; + } else if (day === 3 || day === 23) { + daySuffix = 'rd'; + } else { + daySuffix = 'th'; + } + + const hours = dateObj.getHours(); + const minutes = dateObj.getMinutes(); + const timeOfDay = hours >= 12 ? 'PM' : 'AM'; + const formattedHours = (hours % 12) || 12; // Convert to 12-hour format + + const formattedTime = `${formattedHours}:${minutes.toString().padStart(2, '0')} ${timeOfDay}`; + + // return `${day}${daySuffix} ${month} ${year} - ${formattedTime}`; + return [`${day}${daySuffix} ${month} ${year}`,`${formattedTime}`] + +} + +export const isDarkModeFromLocalStorage = () => { + const localStorageDarkMode = localStorage.getItem('darkMode') + + if (localStorageDarkMode != null && localStorageDarkMode === 'True') { + return true; + } else { + return false; + } +} + + +export const includeDarkClass =(scssClass:string,darkMode:boolean)=>{ + return `${scssClass} ${darkMode?'dark':'light'}` +} +const Todos = { + getUrl:getUrl, + formatDateAndTime:formatDateAndTime +} + + +type HttpError = { + message: string; + status?: number; +}; +interface HttpRequest { + url: string; + method: 'GET' | 'POST' | 'PUT' | 'DELETE'; + headers?: { [key: string]: string }; + body?: FormData | string | null; +} +export const doFetchCall=async(options:HttpRequest)=>{ + try { + const headers: HeadersInit = {}; + if (options && options.headers) { + for (const [key, value] of Object.entries(options.headers)) { + if (value !== null) { + headers[key] = value && value.toString(); + } + } + } + const fetchOptions: RequestInit = { + ...options, + headers, + }; + + const fetchResponse = await fetch(options.url, fetchOptions); + if (!fetchResponse.ok) { + // dispatch(setLoading(false)); + throw new Error(`Request failed with status: ${fetchResponse.status}`); + } + + const responseData = await fetchResponse.json(); + return responseData + // dispatch(setLoading(false)); + } catch (error: unknown) { + return{ + message: (error as Error).message, + status: (error as HttpError).status, + }; + // dispatch(setLoading(false)); + } +} + +export default Todos; \ No newline at end of file diff --git a/admin-client/src/Pages/Admin/LoginPage.tsx b/admin-client/src/Pages/Admin/LoginPage.tsx new file mode 100644 index 0000000..208f0d7 --- /dev/null +++ b/admin-client/src/Pages/Admin/LoginPage.tsx @@ -0,0 +1,26 @@ +import { useEffect } from "react"; +import Login from "../../components/Admin/Login/Login" +import { useNavigate } from "react-router-dom"; +// import { useSelector } from "react-redux"; +// import { RootState } from "../../ReduxStore/store"; + +export interface LoginPageProps { + setIsAuthenticated: any; + isAuthenticated: boolean; + fetchAllUserData: any; +} +const LoginPage: React.FC = ({ setIsAuthenticated, isAuthenticated, fetchAllUserData = () => { } }) => { + const navigate = useNavigate() + // const reduxToken = useSelector((state: RootState) => state.User.token) + useEffect(() => { + if (isAuthenticated) { + navigate('/dashboard') + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return( + + ) +} + +export default LoginPage; \ No newline at end of file diff --git a/admin-client/src/Pages/Admin/RegisterPage.tsx b/admin-client/src/Pages/Admin/RegisterPage.tsx new file mode 100644 index 0000000..2c2d707 --- /dev/null +++ b/admin-client/src/Pages/Admin/RegisterPage.tsx @@ -0,0 +1,26 @@ +// import React, { useEffect } from 'react' +import Register from '../../components/Admin/Register/Register' +// import { useNavigate } from 'react-router-dom'; + +interface RegisterPageProps { + setIsAuthenticated: any; +} + +const RegisterPage: React.FC = ({ setIsAuthenticated }) => { + // const navigate = useNavigate() + // useEffect(() => { + // const token = localStorage.getItem('Token'); + // if (!token) { + // setIsAuthenticated(false) + // } else { + // setIsAuthenticated(true) + // navigate('/dashboard') + // } + // // eslint-disable-next-line react-hooks/exhaustive-deps + // }, []); + return ( + + ) +} + +export default RegisterPage \ No newline at end of file diff --git a/admin-client/src/Pages/NotFound/NotFound.scss b/admin-client/src/Pages/NotFound/NotFound.scss new file mode 100644 index 0000000..b721066 --- /dev/null +++ b/admin-client/src/Pages/NotFound/NotFound.scss @@ -0,0 +1,9 @@ +.notfound_main_container{ + position:absolute; + width: 100%; + height: 100%; + backdrop-filter: blur(50px); +} +.notfound_main_container.isAuthenticated{ + background-color: aliceblue; +} \ No newline at end of file diff --git a/admin-client/src/Pages/NotFound/NotFound.tsx b/admin-client/src/Pages/NotFound/NotFound.tsx new file mode 100644 index 0000000..fde6e5d --- /dev/null +++ b/admin-client/src/Pages/NotFound/NotFound.tsx @@ -0,0 +1,40 @@ +import { useSelector } from "react-redux"; +import { includeDarkClass } from "../../CONFIG"; +import "./NotFound.scss"; +import { RootState } from "../../ReduxStore/store"; +import { useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { useDispatch } from "react-redux"; +import { setLoading } from "../../ReduxStore/UISlice"; + +interface NotFoundProps { + isAuthenticated: boolean +} + +const NotFound: React.FC = ({ isAuthenticated = false }) => { + const darkMode = useSelector((state: RootState) => state.UI.theme.dark) + const token = useSelector((state: RootState) => state.User.token) + const navigate = useNavigate() + const dispatch = useDispatch() + useEffect(() => { + dispatch(setLoading(true)) + if (token) { + navigate('/todos') + } + dispatch(setLoading(false)) + }, [dispatch, navigate, token]) + if (isAuthenticated) { + return ( +
+

NOT FOUNDsdfsdf

+
+ ) + } + return ( +
+

NOT FOUND

+
+ ) +} + +export default NotFound; \ No newline at end of file diff --git a/admin-client/src/ReduxStore/UISlice.ts b/admin-client/src/ReduxStore/UISlice.ts new file mode 100644 index 0000000..671b77d --- /dev/null +++ b/admin-client/src/ReduxStore/UISlice.ts @@ -0,0 +1,80 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +interface UISliceReducerState { + allTodos:any; + data: string[]; + loading: boolean; + token:string | null; + sideBarActiveTab:number; + theme:{ + dark:boolean + }, + currentPage:string; + isMobSidebarOpen:boolean; +} + +const initialState: UISliceReducerState = { + allTodos:null, + data: [], + loading: false, + token:null, + sideBarActiveTab:0, + theme:{ + dark:false + }, + currentPage:'', + isMobSidebarOpen:false +}; + +const UISliceReducer = createSlice({ + name: 'UI', + initialState, + reducers: { + setToken(state,action:PayloadAction) { + state.token = action.payload; + let isTokenPresent = localStorage.getItem('Token'); + if(!isTokenPresent){ + localStorage.setItem('Token',action.payload); + }else{ + localStorage.removeItem('Token'); + localStorage.setItem('Token',action.payload); + } + }, + setLoading(state, action: PayloadAction) { + state.loading = action.payload; + }, + setAllTodos(state,action:PayloadAction){ + state.allTodos = action.payload; + }, + UILogout(state){ + state.allTodos = []; + state.data = []; + state.loading = false; + state.sideBarActiveTab = 0; + state.token = null; + }, + toogleDarkLight(state){ + if(state.theme.dark){ + localStorage.setItem('darkMode','False') + }else{ + localStorage.setItem('darkMode','True') + } + state.theme.dark = !state.theme.dark; + }, + setDarkMode(state,action:PayloadAction){ + state.theme.dark = action.payload; + }, + setCurrentPage(state,action:PayloadAction){ + state.currentPage = action.payload; + }, + toggleMobSidebar(state){ + state.isMobSidebarOpen = !state.isMobSidebarOpen; + }, + setSideBarActiveTab(state,action:PayloadAction){ + state.sideBarActiveTab = action.payload + } + }, +}); + +export const { setToken, setLoading,setAllTodos ,UILogout,toogleDarkLight,setDarkMode,setCurrentPage,toggleMobSidebar,setSideBarActiveTab} = UISliceReducer.actions; +export default UISliceReducer.reducer; diff --git a/admin-client/src/ReduxStore/UserSlice.ts b/admin-client/src/ReduxStore/UserSlice.ts new file mode 100644 index 0000000..08aef96 --- /dev/null +++ b/admin-client/src/ReduxStore/UserSlice.ts @@ -0,0 +1,82 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { TodoItem } from '../App'; + +export interface Todo { + _id: string; + title: string; + user: string; + description: string; + todo: Todo[]; + createdAt: string; + updatedAt: string; + __v: number; +} + +export interface User { + _id: string; + userName: string; + password: string; + email: string; + picUrl: string; + todos: TodoItem[]; + social: {}; + statusFiltered: { + __filteredTodos: TodoItem[]; + __filteredInProgress: TodoItem[]; + __filteredCompleted: TodoItem[]; + __filteredOnHold: TodoItem[]; + }; + priorityFiltered: { + __filteredHigh: TodoItem[]; + __filteredMedium: TodoItem[]; + __filteredLow: TodoItem[]; + }; + createdAt: string; + updatedAt: string; + website: string; + bio: string; + location: string; + __v: number; +} + +// export interface ApiResponse { +// user: User; +// } + +interface UISliceReducerState { + allUserData: Partial; + token: string | null; +} + +const initialState: UISliceReducerState = { + allUserData: {}, + token: null, +}; + +const UserSliceReducer = createSlice({ + name: 'User', + initialState, + reducers: { + setAllUserData(state, action: PayloadAction) { + state.allUserData = action.payload; + }, + setToken(state, action: PayloadAction) { + state.token = action.payload + let isTokenPresent = localStorage.getItem('Token'); + if (!isTokenPresent) { + localStorage.setItem('Token', action.payload) + } else { + localStorage.removeItem('Token') + localStorage.setItem('Token', action.payload) + } + }, + UserLogout(state) { + state.token = null; + state.allUserData = {}; + }, + // + }, +}); + +export const { setAllUserData, setToken, UserLogout } = UserSliceReducer.actions; +export default UserSliceReducer.reducer; diff --git a/admin-client/src/ReduxStore/store.ts b/admin-client/src/ReduxStore/store.ts new file mode 100644 index 0000000..0f171d7 --- /dev/null +++ b/admin-client/src/ReduxStore/store.ts @@ -0,0 +1,19 @@ +// store.ts +import { configureStore } from '@reduxjs/toolkit'; +import UISliceReducer from './UISlice'; +import UserSliceReducer from './UserSlice'; +// import slice2Reducer from './slice2'; +// import slice3Reducer from './slice3'; + +const store = configureStore({ + reducer: { + UI: UISliceReducer, + User: UserSliceReducer, + // slice3: slice3Reducer, + }, +}); + +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; + +export default store; diff --git a/admin-client/src/api.ts b/admin-client/src/api.ts new file mode 100644 index 0000000..3976668 --- /dev/null +++ b/admin-client/src/api.ts @@ -0,0 +1,32 @@ + +// local one + +// export const API_URL_LOCAL = "http://192.168.0.101:3033" +// export const API_URL_LOCAL = "http://192.168.0.103:3033" +// export const API_URL_LOCAL = "http://192.168.68.63:3033/jarvis" +export const API_URL_LOCAL = "http://192.168.0.100:3033/jarvis" + +// personal host +// export const API_URL_LOCAL = "http://172.20.10.13:3033" + +// live one +// export const API_URL_LIVE = "https://adnans-todo-backend.onrender.com/jarvis" +// export const API_URL_LIVE = "http://ec2-16-170-250-205.eu-north-1.compute.amazonaws.com:3033/jarvis" +export const API_URL_LIVE = "https://backend.3621.lol/jarvis" + +// isLive +export const isLive = true; + + +export function generateUniqueID() { + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + const length = 32; + let id = ''; + + for (let i = 0; i < length; i++) { + const randomIndex = Math.floor(Math.random() * characters.length); + id += characters.charAt(randomIndex); + } + + return id; +} \ No newline at end of file diff --git a/admin-client/src/components/Admin/ForgotPassword/ForgotPassword.scss b/admin-client/src/components/Admin/ForgotPassword/ForgotPassword.scss new file mode 100644 index 0000000..e69de29 diff --git a/admin-client/src/components/Admin/ForgotPassword/ForgotPassword.tsx b/admin-client/src/components/Admin/ForgotPassword/ForgotPassword.tsx new file mode 100644 index 0000000..4725814 --- /dev/null +++ b/admin-client/src/components/Admin/ForgotPassword/ForgotPassword.tsx @@ -0,0 +1,162 @@ +import React, { /*useEffect,*/ useState } from 'react'; +import { LoginPageProps } from '../../../Pages/Admin/LoginPage'; +import PasswordInput from '../../UIComponents/PasswordInput'; +import "./ForgotPassword.scss" +import GlassmorphicBackground from '../../UIComponents/Modal/DesignComponents/GlassmorphicBackground'; +// import Loader from '../../UIComponents/Loader/Loader'; +import { Link, useNavigate } from 'react-router-dom'; +import { setLoading } from '../../../ReduxStore/UISlice'; +import { useDispatch } from 'react-redux'; +import { getUrl, includeDarkClass } from '../../../CONFIG'; +import { RootState } from '../../../ReduxStore/store'; +import { useSelector } from 'react-redux'; +import { setToken } from '../../../ReduxStore/UserSlice'; + +interface ForgotPasswordProps extends LoginPageProps { + setIsAuthenticated: any; +} + +const ForgotPassword: React.FC = ({ setIsAuthenticated, fetchAllUserData }) => { + const [email, setEmail] = useState(''); + const [OTP, setOTP] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(null); + const [OTPSent, setOTPSent] = useState(false) + + const darkMode = useSelector((state: RootState) => state.UI.theme.dark) + + const navigate = useNavigate() + + const dispatch = useDispatch() + const token = useSelector((state: RootState) => state.User.token) + + const handlEmailChange = (event: React.ChangeEvent) => { + setEmail(event.target.value); + }; + + const handleOTPChange = (event: React.ChangeEvent) => { + setOTP(event.target.value); + }; + + const handlePasswordChange = (event: React.ChangeEvent) => { + setPassword(event.target.value); + }; + + + const handleOTPFormSubmit = (event: React.FormEvent) => { + event.preventDefault(); + + dispatch(setLoading(true)) + + const formdata = new FormData(); + formdata.append('email', email); + // formdata.append('OTP', OTP); + + fetch(getUrl('/auth/forgotPassword'), { + method: 'POST', + body: formdata, + }).then((response) => { + if (response.status === 200 || response.ok) { + setOTPSent(true) + console.log('OTP sent successfully !!.') + } + return response.json() + }).then((jsonResponse) => { + dispatch(setLoading(false)) + setError(jsonResponse && jsonResponse.message) + if (jsonResponse && jsonResponse.token) { + localStorage.setItem("Token", jsonResponse && jsonResponse.token) + dispatch(setToken(jsonResponse.token)) + // dispatch(setLoading(true)) + // fetchAllUserData(jsonResponse.token) + // dispatch(setLoading(false)) + if (token) { + navigate('/todos') + } + } + }) + .catch(err => { + console.error(err) + setError(err) + dispatch(setLoading(false)) + }) + }; + const handleResetPasswordFormSubmit = (event: React.FormEvent) => { + event.preventDefault(); + + dispatch(setLoading(true)) + + const formdata = new FormData(); + formdata.append('email', email); + formdata.append('newPassword', password); + formdata.append('OTP', OTP); + + fetch(getUrl('/auth/resetPassword'), { + method: 'POST', + body: formdata, + }).then((response) => { + if (response.status === 200 || response.ok) { + // setOTPSent(true) + console.log('Password resetted successfully !!.') + navigate('/login') + } + return response.json() + }).then((jsonResponse) => { + dispatch(setLoading(false)) + setError(jsonResponse && jsonResponse.message) + if (jsonResponse && jsonResponse.token) { + localStorage.setItem("Token", jsonResponse && jsonResponse.token) + dispatch(setToken(jsonResponse.token)) + // dispatch(setLoading(true)) + // fetchAllUserData(jsonResponse.token) + // dispatch(setLoading(false)) + if (token) { + navigate('/todos') + } + } + }) + .catch(err => { + console.error(err) + setError(err) + dispatch(setLoading(false)) + }) + }; + + return ( +
+ +

Forgot Password

+
+ {!OTPSent ?
+ + +
: ''} + + + {OTPSent ? <> +
+ +
+
+ +
+ : ''} + +
+ {error &&

{JSON.stringify(error)}

} +
+ Don't have an account ?  +  Sign Up / Register +
+
+
+ ); +}; + +export default ForgotPassword; diff --git a/admin-client/src/components/Admin/Login/Login.scss b/admin-client/src/components/Admin/Login/Login.scss new file mode 100644 index 0000000..2010749 --- /dev/null +++ b/admin-client/src/components/Admin/Login/Login.scss @@ -0,0 +1,46 @@ +.main_login_container{ + width: 400px; + padding: 35px; + + h2{ + margin-top: 0; + } + + .glassmorphic-background{ + padding:20px 30px + } +} + +.login__form{ + display: flex; + flex-direction: column; + align-items: stretch; + row-gap: 20px; + + div.inputdiv{ + input{ + width:93%; + margin-top: 6px; + } + } +} + +.login__btn{ + background-color: #e7f0ff; + border-radius: 5px; + font-family: 'Signika', sans-serif; + font-size: 20px; + // width:50px; + + &.dark{ + color:black + } +} + +.sign_in_redirect{ + margin: 10px 0; + + span{ + color:rgb(28, 167, 253) + } +} \ No newline at end of file diff --git a/admin-client/src/components/Admin/Login/Login.tsx b/admin-client/src/components/Admin/Login/Login.tsx new file mode 100644 index 0000000..3f3922a --- /dev/null +++ b/admin-client/src/components/Admin/Login/Login.tsx @@ -0,0 +1,105 @@ +import React, { /*useEffect,*/ useState } from 'react'; +import { LoginPageProps } from '../../../Pages/Admin/LoginPage'; +import PasswordInput from '../../UIComponents/PasswordInput'; +import "./Login.scss" +import GlassmorphicBackground from '../../UIComponents/Modal/DesignComponents/GlassmorphicBackground'; +// import Loader from '../../UIComponents/Loader/Loader'; +import { Link, useNavigate } from 'react-router-dom'; +import { setLoading } from '../../../ReduxStore/UISlice'; +import { useDispatch } from 'react-redux'; +import { getUrl, includeDarkClass } from '../../../CONFIG'; +import { RootState } from '../../../ReduxStore/store'; +import { useSelector } from 'react-redux'; +import { setToken } from '../../../ReduxStore/UserSlice'; + +interface LoginProps extends LoginPageProps{ + setIsAuthenticated: any; +} + +const Login: React.FC = ({ setIsAuthenticated, fetchAllUserData }) => { + const [userName, setUserName] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(null); + + const darkMode = useSelector((state: RootState) => state.UI.theme.dark) + + const navigate = useNavigate() + + const dispatch = useDispatch() + const token = useSelector((state: RootState) => state.User.token) + + const handleUsernameChange = (event: React.ChangeEvent) => { + setUserName(event.target.value); + }; + + const handlePasswordChange = (event: React.ChangeEvent) => { + setPassword(event.target.value); + }; + + + const handleLoginFormSubmit = (event: React.FormEvent) => { + event.preventDefault(); + + dispatch(setLoading(true)) + + const formdata = new FormData(); + formdata.append('userName', userName); + formdata.append('password', password); + + fetch(getUrl('/auth/login'), { + method: 'POST', + body: formdata, + }).then((response) => { + if (response.status === 200 || response.ok) { + console.log("user logged in") + } + return response.json() + }).then((jsonResponse) => { + dispatch(setLoading(false)) + setError(jsonResponse && jsonResponse.message) + if (jsonResponse && jsonResponse.token) { + localStorage.setItem("Token", jsonResponse && jsonResponse.token) + dispatch(setToken(jsonResponse.token)) + // dispatch(setLoading(true)) + // fetchAllUserData(jsonResponse.token) + // dispatch(setLoading(false)) + if (token) { + navigate('/todos') + } + } + }) + .catch(err => { + console.error(err) + setError(err) + dispatch(setLoading(false)) + }) + }; + + return ( +
+ +

Login

+
+
+ + +
+
+ +
+ +
+ {error &&

{JSON.stringify(error)}

} +
+ Don't have an account ?  +  Sign Up / Register +
+
+  Forgot Password ?? +
+
+
+ ); +}; + +export default Login; diff --git a/admin-client/src/components/Admin/Register/Register.scss b/admin-client/src/components/Admin/Register/Register.scss new file mode 100644 index 0000000..88fe6f7 --- /dev/null +++ b/admin-client/src/components/Admin/Register/Register.scss @@ -0,0 +1,17 @@ +.main_register_container{ + width: 400px; + + .sign_in_redirect{ + width:100%; + + a{ + text-decoration: none; + } + + span{ + color:rgb(28, 167, 253); + font-weight: 500; + padding-left: 10px; + } + } +} \ No newline at end of file diff --git a/admin-client/src/components/Admin/Register/Register.tsx b/admin-client/src/components/Admin/Register/Register.tsx new file mode 100644 index 0000000..23a5c4c --- /dev/null +++ b/admin-client/src/components/Admin/Register/Register.tsx @@ -0,0 +1,93 @@ +import React, { useState } from 'react' +import "./Register.scss" +import { Link, useNavigate } from 'react-router-dom'; +import GlassmorphicBackground from '../../UIComponents/Modal/DesignComponents/GlassmorphicBackground'; +import PasswordInput from '../../UIComponents/PasswordInput'; +import { setLoading } from '../../../ReduxStore/UISlice'; +import { useDispatch } from 'react-redux'; +import { getUrl } from '../../../CONFIG'; + + +interface RegisterProps { +} + +const Register: React.FC = () => { + const [userName, setUserName] = useState(''); + const [password, setPassword] = useState(''); + const [email, setEmail] = useState(''); + const [profilePicUrl, setProfilePicUrl] = useState(''); + + const navigate = useNavigate() + + const dispatch = useDispatch() + + const handleUserNameChange = (event: React.ChangeEvent) => { + setUserName(event.target.value); + }; + + const handlePasswordChange = (event: React.ChangeEvent) => { + setPassword(event.target.value); + }; + const handleEmailChange = (event: React.ChangeEvent) => { + setEmail(event.target.value); + }; + const handleProfilePicUrlChange = (event: React.ChangeEvent) => { + setProfilePicUrl(event.target.value); + }; + + const handleLoginFormSubmit = (event: React.FormEvent) => { + event.preventDefault(); + dispatch(setLoading(true)) + const formdata = new FormData(); + formdata.append('userName', userName); + formdata.append('email', email); + formdata.append('password', password); + formdata.append('profilePicUrl', profilePicUrl); + + fetch(getUrl('/auth/register'), { + method: 'POST', + body: formdata, + }).then((response) => { + if (response.ok) { + dispatch(setLoading(false)) + navigate('/login') + } + dispatch(setLoading(false)) + }).catch(err => { + console.error(err) + dispatch(setLoading(false)) + }) + }; + + return ( +
+ +

Register

+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+ Already have an account ? + Sign In / Log In +
+
+
+
+ ) +} + +export default Register \ No newline at end of file diff --git a/admin-client/src/components/UIComponents/AddIcon/AddIcon.scss b/admin-client/src/components/UIComponents/AddIcon/AddIcon.scss new file mode 100644 index 0000000..4bb5daa --- /dev/null +++ b/admin-client/src/components/UIComponents/AddIcon/AddIcon.scss @@ -0,0 +1,73 @@ +@import '../../../styles/typography/typography.scss'; + +// AddIcon component styles +.add_icon { + position: relative; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + // transition: background-color 0.2s ease-in-out; + border: 2px solid rgba(255, 255, 255, 0.24); + + + // Default size + width: 25px; + height: 25px; + + // Optional size passed as a prop + + &.large { + width: 40px; + height: 40px; + } + + // Optional background color passed as a prop + &.custom-background { + background-color: lightblue; + } + + &.darkMode{ + border: 3px solid $link_blue; + } + + .line1, .line2 { + width: 70%; + height: 4px; + background: rgba(255, 255, 255, 0.68); + backdrop-filter: blur(4px); + border-radius: 10px; + + &.darkMode{ + background: $link_blue; + } + } + + .line2 { + position: absolute; + transform: rotate(90deg); + } + + #tooltip { + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + background: rgba(0, 0, 0, 0.8); + color: #fff; + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + white-space: nowrap; + pointer-events: none; + opacity: 0; + // transition: opacity 0.2s ease-in-out; + } + + + &:hover #tooltip { + opacity: 1; + } + } + \ No newline at end of file diff --git a/admin-client/src/components/UIComponents/AddIcon/AddIcon.tsx b/admin-client/src/components/UIComponents/AddIcon/AddIcon.tsx new file mode 100644 index 0000000..a455af0 --- /dev/null +++ b/admin-client/src/components/UIComponents/AddIcon/AddIcon.tsx @@ -0,0 +1,82 @@ +import React, { FC, CSSProperties } from 'react'; +import './AddIcon.scss' + +interface AddIconProps { + onClick?: Function; + darkMode?: boolean; + backgroundColor?: string; + size?: number; + tooltipText?: string; + showToolTip: boolean; +} + +const AddIcon: FC = ({ + backgroundColor = 'none', + size = 25, + tooltipText = 'Add', + darkMode = false, + showToolTip = false, +}) => { + const iconStyle: CSSProperties = { + width: size, + height: size, + borderRadius: '50%', + background: backgroundColor, + backdropFilter: 'blur(4px)', + position: 'relative', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }; + + const tooltipStyle: CSSProperties = { + position: 'absolute', + bottom: '-112%', + left: '50%', + transform: 'translateX(-50%)', + background: 'rgba(0, 0, 0, 0.8)', + color: '#fff', + padding: '4px 8px', + borderRadius: '4px', + fontSize: '12px', + fontWeight: '400', + whiteSpace: 'nowrap', + pointerEvents: 'none', + opacity: 0, + // transition: 'opacity 0.2s ease-in-out', + }; + + const handleMouseEnter = () => { + const tooltip = document.getElementById('tooltip'); + if (tooltip) { + tooltip.style.opacity = '1'; + } + }; + + const handleMouseLeave = () => { + const tooltip = document.getElementById('tooltip'); + if (tooltip) { + tooltip.style.opacity = '0'; + } + }; + + return ( +
+
+
+ {showToolTip ? +
+ {tooltipText} +
: <> + } + +
+ ); +}; + +export default AddIcon; diff --git a/admin-client/src/components/UIComponents/Chevron/ChevronIcon.scss b/admin-client/src/components/UIComponents/Chevron/ChevronIcon.scss new file mode 100644 index 0000000..9dacce5 --- /dev/null +++ b/admin-client/src/components/UIComponents/Chevron/ChevronIcon.scss @@ -0,0 +1,82 @@ +@import '../../../styles/typography/typography.scss'; + +// AddIcon component styles +.chevron_icon { + position: relative; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + // transition: background-color 0.2s ease-in-out; + border: 2px solid rgba(255, 255, 255, 0.24); + + + // Default size + width: 25px; + height: 25px; + + // Optional size passed as a prop + + &.large { + width: 40px; + height: 40px; + } + + // Optional background color passed as a prop + &.custom-background { + background-color: lightblue; + } + + &.darkMode{ + border: 3px solid $link_blue; + } + + .line1, .line2 { + width: 50%; + height: 4px; + background: rgba(255, 255, 255, 0.68); + backdrop-filter: blur(4px); + border-radius: 10px; + + &.darkMode{ + background: $link_blue; + } + } + + .line2 { + position: absolute; + transform-origin: right center; + transform: rotate(-39deg); + top: 10px; + } + .line1 { + position: absolute; + transform-origin: right center; + transform: rotate(39deg); + top: 10.5px; + + } + + #chev_tooltip { + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + background: rgba(0, 0, 0, 0.8); + color: #fff; + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + white-space: nowrap; + pointer-events: none; + opacity: 0; + // transition: opacity 0.2s ease-in-out; + } + + + &:hover #chev_tooltip { + opacity: 1; + } + } + \ No newline at end of file diff --git a/admin-client/src/components/UIComponents/Chevron/ChevronIcon.tsx b/admin-client/src/components/UIComponents/Chevron/ChevronIcon.tsx new file mode 100644 index 0000000..5d35ab1 --- /dev/null +++ b/admin-client/src/components/UIComponents/Chevron/ChevronIcon.tsx @@ -0,0 +1,96 @@ +import React, { FC, CSSProperties, MouseEventHandler } from 'react'; +import './ChevronIcon.scss' +import { generateUniqueID } from '../../../api'; + +interface ChevronIconProps { + onClick?: MouseEventHandler; + darkMode?: boolean; + backgroundColor?: string; + size?: number; + tooltipText?: string; +} + +const ChevronIcon: FC = ({ + backgroundColor = 'none', + size = 25, + tooltipText = 'More', + darkMode = false, + onClick = () => { } +}) => { + const iconStyle: CSSProperties = { + width: size, + height: size, + borderRadius: '50%', + background: backgroundColor, + backdropFilter: 'blur(4px)', + position: 'relative', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }; + + const chevRandomUnique32Id = generateUniqueID() + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const tooltipStyle: CSSProperties = { + position: 'absolute', + bottom: '-112%', + left: '50%', + transform: 'translateX(-50%)', + background: 'rgba(0, 0, 0, 0.8)', + color: '#fff', + padding: '4px 8px', + borderRadius: '4px', + fontSize: '12px', + fontWeight: '400', + whiteSpace: 'nowrap', + pointerEvents: 'none', + opacity: 0, + // transition: 'opacity 0.2s ease-in-out', + }; + + const handleMouseEnter = () => { + const tooltip = document.getElementById(chevRandomUnique32Id); + if (tooltip) { + tooltip.style.opacity = '1'; + } + }; + + + + const handleMouseLeave = () => { + const tooltip = document.getElementById(chevRandomUnique32Id); + if (tooltip) { + tooltip.style.opacity = '0'; + } + }; + + // const Tooltip = ({ tooltipText = '' }) => { + // return ( + // <> + // {ReactDOM.createPortal(
+ // {tooltipText} + //
, document.body)} + // + // ) + // } + + return ( +
+
+
+ {/*
+ {tooltipText} +
*/} +
+ ); +}; + +export default ChevronIcon; diff --git a/admin-client/src/components/UIComponents/CrossIcon/CrossIcon.scss b/admin-client/src/components/UIComponents/CrossIcon/CrossIcon.scss new file mode 100644 index 0000000..df90f1c --- /dev/null +++ b/admin-client/src/components/UIComponents/CrossIcon/CrossIcon.scss @@ -0,0 +1,86 @@ +@import '../../../styles/typography/typography.scss'; + +// AddIcon component styles +.cross_icon { + position: relative; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + // transition: background-color 0.2s ease-in-out; + border: 2px solid rgb(255, 0, 0); + opacity:0.4; + + + // Default size + width: 25px; + height: 25px; + + // Optional size passed as a prop + + &.large { + width: 40px; + height: 40px; + } + + // Optional background color passed as a prop + &.custom-background { + background-color: lightblue; + } + + &.darkMode{ + border: 3px solid $link_blue; + } + + .line1, .line2 { + width: 70%; + height: 4px; + background: rgba(255, 0, 0, 0.68); + backdrop-filter: blur(4px); + border-radius: 10px; + + &.darkMode{ + background: $link_blue; + } + + &.danger{ + background: rgba(231, 70, 70); + } + } + + .line1 { + position: absolute; + transform: rotate(45deg); + } + .line2 { + position: absolute; + transform: rotate(-45deg); + } + + #cross_tooltip { + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + background: rgba(0, 0, 0, 0.8); + color: #fff; + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + white-space: nowrap; + pointer-events: none; + opacity: 0; + // transition: opacity 0.2s ease-in-out; + } + + + &:hover #cross_tooltip { + opacity: 1; + } + + &:hover{ + opacity:0.7 + } + } + \ No newline at end of file diff --git a/admin-client/src/components/UIComponents/CrossIcon/CrossIcon.tsx b/admin-client/src/components/UIComponents/CrossIcon/CrossIcon.tsx new file mode 100644 index 0000000..ca80684 --- /dev/null +++ b/admin-client/src/components/UIComponents/CrossIcon/CrossIcon.tsx @@ -0,0 +1,85 @@ +import React, { FC, CSSProperties } from 'react'; +import './CrossIcon.scss' + +interface CrossIconProps { + onClick: (event: React.MouseEvent) => void; + darkMode?: boolean; + backgroundColor?: string; + size?: number; + tooltipText?: string; + showToolTip?: boolean; +} + +const CrossIcon: FC = ({ + backgroundColor = 'none', + size = 25, + tooltipText = 'Cross', + darkMode = false, + showToolTip = false, + onClick = () => { }, +}) => { + const iconStyle: CSSProperties = { + width: size, + height: size, + borderRadius: '50%', + background: backgroundColor, + backdropFilter: 'blur(4px)', + position: 'relative', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }; + + const tooltipStyle: CSSProperties = { + position: 'absolute', + bottom: '-112%', + left: '50%', + transform: 'translateX(-50%)', + background: 'rgba(0, 0, 0, 0.8)', + color: '#fff', + padding: '4px 8px', + borderRadius: '4px', + fontSize: '12px', + fontWeight: '400', + whiteSpace: 'nowrap', + pointerEvents: 'none', + opacity: 0, + // transition: 'opacity 0.2s ease-in-out', + }; + + const handleMouseEnter = () => { + const tooltip = document.getElementById('cross_tooltip'); + if (tooltip) { + tooltip.style.opacity = '1'; + } + }; + + const handleMouseLeave = () => { + const tooltip = document.getElementById('cross_tooltip'); + if (tooltip) { + tooltip.style.opacity = '0'; + } + }; + + return ( +
+
+
+ {showToolTip ? +
+ {tooltipText} +
: <> + } + +
+ ); +}; + +export default CrossIcon; diff --git a/admin-client/src/components/UIComponents/Loader/Loader.scss b/admin-client/src/components/UIComponents/Loader/Loader.scss new file mode 100644 index 0000000..89dbd7a --- /dev/null +++ b/admin-client/src/components/UIComponents/Loader/Loader.scss @@ -0,0 +1,45 @@ +/* Loader.css */ +.loader-backdrop { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + background-color: rgba(0, 0, 0, 0.5); + z-index:999999; + } + + .loader-container { + display: flex; + flex-direction: column; + width: 106px; + align-items: center; + padding: 16px; + background: rgb(255 255 255 / 39%); + -webkit-backdrop-filter: blur(12px); + backdrop-filter: blur(16px); + border-radius: 8px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); + } + + .ios-gear-loading { + width: 50px; + height: 50px; + border-radius: 50%; + border: 5px solid rgba(0, 0, 0, 0.2); + border-top: 5px solid #2196f3; + animation: spin 1s linear infinite; + margin-bottom: 10px; + } + + @keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } \ No newline at end of file diff --git a/admin-client/src/components/UIComponents/Loader/Loader.tsx b/admin-client/src/components/UIComponents/Loader/Loader.tsx new file mode 100644 index 0000000..e6ca1a1 --- /dev/null +++ b/admin-client/src/components/UIComponents/Loader/Loader.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import './Loader.scss'; + +interface LoaderProps { + isLoading: boolean; +} + +const Loader: React.FC = ({ isLoading }) => { + if (!isLoading) return null; + + return ReactDOM.createPortal( +
+
+
+

Loading...

+

Free service takes longer than usual to load

+
+
, + document.getElementById('loader')! + ); +}; + +export default Loader; diff --git a/admin-client/src/components/UIComponents/LoaderComponent/LoaderComponent.scss b/admin-client/src/components/UIComponents/LoaderComponent/LoaderComponent.scss new file mode 100644 index 0000000..e905c49 --- /dev/null +++ b/admin-client/src/components/UIComponents/LoaderComponent/LoaderComponent.scss @@ -0,0 +1,46 @@ + +/* Loader.css */ +.loader-backdrop { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + background-color: rgba(123, 123, 123, 0.5); + z-index:999999; + } + + .loader-container { + display: flex; + flex-direction: column; + width: 106px; + align-items: center; + padding: 16px; + background: rgb(255 255 255 / 39%); + -webkit-backdrop-filter: blur(12px); + backdrop-filter: blur(16px); + border-radius: 8px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); + } + + .ios-gear-loading { + width: 50px; + height: 50px; + border-radius: 50%; + border: 5px solid rgba(0, 0, 0, 0.2); + border-top: 5px solid #2196f3; + animation: spin 1s linear infinite; + margin-bottom: 10px; + } + + @keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } \ No newline at end of file diff --git a/admin-client/src/components/UIComponents/LoaderComponent/LoaderComponent.tsx b/admin-client/src/components/UIComponents/LoaderComponent/LoaderComponent.tsx new file mode 100644 index 0000000..fb4b956 --- /dev/null +++ b/admin-client/src/components/UIComponents/LoaderComponent/LoaderComponent.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import './LoaderComponent.scss'; + +const LoaderComponent: React.FC = () => { + + return ( +
+
+
+

Loading...

+

** Free service takes longer than usual to load **

+
+
+ ); +}; + +export default LoaderComponent; diff --git a/admin-client/src/components/UIComponents/MenuButton/MenuButton.scss b/admin-client/src/components/UIComponents/MenuButton/MenuButton.scss new file mode 100644 index 0000000..e69de29 diff --git a/admin-client/src/components/UIComponents/MenuButton/MenuButton.tsx b/admin-client/src/components/UIComponents/MenuButton/MenuButton.tsx new file mode 100644 index 0000000..40d050b --- /dev/null +++ b/admin-client/src/components/UIComponents/MenuButton/MenuButton.tsx @@ -0,0 +1,111 @@ +import React from "react"; +import { motion, Transition } from "framer-motion"; +import './MenuButton.scss' +import { useSelector } from "react-redux"; +import { RootState } from "../../../ReduxStore/store"; +import { useDispatch } from "react-redux"; +import { toggleMobSidebar } from "../../../ReduxStore/UISlice"; + +interface Props { + color?: string; + strokeWidth?: number; + transition?: Transition; + lineProps?: any; + width?: number; + height?: number; +} + +const MenuButton: React.FC = ({ + width = 24, + height = 24, + strokeWidth = 1, + color = "#000", + transition = null, + lineProps = null, + ...props +}) => { + const isMobSidebarOpen = useSelector((state: RootState) => state.UI.isMobSidebarOpen); + const dispatch = useDispatch() + const variant = isMobSidebarOpen ? "opened" : "closed"; + const top = { + closed: { + rotate: 0, + translateY: 0, + }, + opened: { + rotate: 45, + translateY: 2, + }, + }; + const center = { + closed: { + opacity: 1, + }, + opened: { + opacity: 0, + }, + }; + const bottom = { + closed: { + rotate: 0, + translateY: 0, + }, + opened: { + rotate: -45, + translateY: -2, + }, + }; + lineProps = { + stroke: color, + strokeWidth: strokeWidth, + vectorEffect: "non-scaling-stroke", + initial: "closed", + animate: variant, + transition, + ...lineProps, + }; + const unitHeight = 4; + const unitWidth = (unitHeight * (width)) / (height); + + + + + return ( + dispatch(toggleMobSidebar())} + > + + + + + ); +}; + +export { MenuButton }; diff --git a/admin-client/src/components/UIComponents/MinusIcon/MinusIcon.scss b/admin-client/src/components/UIComponents/MinusIcon/MinusIcon.scss new file mode 100644 index 0000000..b44ac94 --- /dev/null +++ b/admin-client/src/components/UIComponents/MinusIcon/MinusIcon.scss @@ -0,0 +1,73 @@ +@import '../../../styles/typography/typography.scss'; + +// AddIcon component styles +.minus_icon { + position: relative; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + // transition: background-color 0.2s ease-in-out; + border: 2px solid rgba(255, 255, 255, 0.24); + + + // Default size + width: 25px; + height: 25px; + + // Optional size passed as a prop + + &.large { + width: 40px; + height: 40px; + } + + // Optional background color passed as a prop + &.custom-background { + background-color: lightblue; + } + + &.darkMode{ + border: 3px solid $link_blue; + } + + .minus_line1{ + width: 70%; + height: 4px; + background: rgba(255, 255, 255, 0.68); + backdrop-filter: blur(4px); + border-radius: 10px; + + &.darkMode{ + background: $link_blue; + } + } + + .line2 { + position: absolute; + transform: rotate(90deg); + } + + #minus_tooltip { + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + background: rgba(0, 0, 0, 0.8); + color: #fff; + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + white-space: nowrap; + pointer-events: none; + opacity: 0; + // transition: opacity 0.2s ease-in-out; + } + + + &:hover #minus_tooltip { + opacity: 1; + } + } + \ No newline at end of file diff --git a/admin-client/src/components/UIComponents/MinusIcon/MinusIcon.tsx b/admin-client/src/components/UIComponents/MinusIcon/MinusIcon.tsx new file mode 100644 index 0000000..f152f1a --- /dev/null +++ b/admin-client/src/components/UIComponents/MinusIcon/MinusIcon.tsx @@ -0,0 +1,78 @@ +import React, { FC, CSSProperties, MouseEventHandler } from 'react'; +import './MinusIcon.scss' + +interface MinusIconProps { + onClick?: MouseEventHandler; + darkMode?: boolean; + backgroundColor?: string; + size?: number; + tooltipText?: string; +} + +const MinusIcon: FC = ({ + backgroundColor = 'none', + size = 25, + tooltipText = 'Remove', + darkMode = false, + onClick = () => { }, +}) => { + const iconStyle: CSSProperties = { + width: size, + height: size, + borderRadius: '50%', + background: backgroundColor, + backdropFilter: 'blur(4px)', + position: 'relative', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }; + + const tooltipStyle: CSSProperties = { + position: 'absolute', + bottom: '-112%', + left: '50%', + transform: 'translateX(-50%)', + background: 'rgba(0, 0, 0, 0.8)', + color: '#fff', + padding: '4px 8px', + borderRadius: '4px', + fontSize: '12px', + fontWeight: '400', + whiteSpace: 'nowrap', + pointerEvents: 'none', + opacity: 0, + // transition: 'opacity 0.2s ease-in-out', + }; + + const handleMouseEnter = () => { + const tooltip = document.getElementById('minus_tooltip'); + if (tooltip) { + tooltip.style.opacity = '1'; + } + }; + + const handleMouseLeave = () => { + const tooltip = document.getElementById('minus_tooltip'); + if (tooltip) { + tooltip.style.opacity = '0'; + } + }; + + return ( +
+
+
+ {tooltipText} +
+
+ ); +}; + +export default MinusIcon; diff --git a/admin-client/src/components/UIComponents/Modal/DesignComponents/GlassmorphicBackground.scss b/admin-client/src/components/UIComponents/Modal/DesignComponents/GlassmorphicBackground.scss new file mode 100644 index 0000000..c51b104 --- /dev/null +++ b/admin-client/src/components/UIComponents/Modal/DesignComponents/GlassmorphicBackground.scss @@ -0,0 +1,14 @@ +.glassmorphic-background { + background: rgb(255 255 255 / 68%); + -webkit-backdrop-filter: blur(12px); + backdrop-filter: blur(4px); + border: 1px solid rgb(141 141 141); + outline: 1px solid black; + border-radius: 10px; + padding: 20px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + + &.dark{ + background: rgb(55 54 54 / 25%); + } +} diff --git a/admin-client/src/components/UIComponents/Modal/DesignComponents/GlassmorphicBackground.tsx b/admin-client/src/components/UIComponents/Modal/DesignComponents/GlassmorphicBackground.tsx new file mode 100644 index 0000000..101435c --- /dev/null +++ b/admin-client/src/components/UIComponents/Modal/DesignComponents/GlassmorphicBackground.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import './GlassmorphicBackground.scss'; +import { includeDarkClass } from '../../../../CONFIG'; +import { useSelector } from 'react-redux'; +import { RootState } from '../../../../ReduxStore/store'; + +interface GlassmorphicBackgroundProps { + children: React.ReactNode; +} + +const GlassmorphicBackground: React.FC = ({ children }) => { + const darkMode = useSelector((state: RootState) => state.UI.theme.dark) + return ( +
+ {children} +
+ ); +}; + +export default GlassmorphicBackground; diff --git a/admin-client/src/components/UIComponents/Modal/Modal.scss b/admin-client/src/components/UIComponents/Modal/Modal.scss new file mode 100644 index 0000000..e46272e --- /dev/null +++ b/admin-client/src/components/UIComponents/Modal/Modal.scss @@ -0,0 +1,114 @@ +// Mobile-first styles +@import "../../WRAPPERS/DashboardWrapper/DashboardWrapper.scss"; + +.modal-overlay { + display: flex; + justify-content: center; + align-items: flex-start; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgb(0 0 0 / 40%); + -webkit-backdrop-filter: blur(15px); + backdrop-filter: blur(px); + z-index: 1; +} + +.modal { + background: rgb(255 255 255 / 30%); + -webkit-backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, 0.3490196078); + outline: 1px solid rgba(0, 0, 0, 0.2117647059); + padding: 35px 15px; + border-radius: 10px; + min-width: 300px; + max-width: 80%; + position: relative; + top: 80px; + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + + &.dark { + background: $dash_contents_dark_mode_bgColor; + border: $dash_contents_dark_mode_border; + outline: $dash_contents_dark_mode_outline; + } + + .header_and_cosebtn_container { + .truncate { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + h3 { + margin: 0 0 10px 0; + font-weight: 400; + } + + .modal_close_btn { + position: absolute; + top: 5px; + right: 5px; + } + } + + form { + display: flex; + flex-direction: column; + row-gap: 10px; + + .input_field { + display: flex; + flex-direction: column; + row-gap: 10px; + } + + .btn_grp { + display: flex; + justify-content: center; + column-gap: 10px; + + button:nth-child(1) { + background-color: rgb(156, 255, 145); + } + } + } + + .modal_main_content_container { + -webkit-backdrop-filter: blur(12px); + border-radius: 10px; + backdrop-filter: blur(1px); + padding: 10px; + + &.dark { + background-color: #ffffff4f; + } + } + + @media screen and (max-device-width: 650px) { + max-width: 90%; + } +} + +.close-button { + position: absolute; + top: 10px; + right: 10px; + cursor: pointer; +} + +// Responsive styles +// @media screen and (min-width: 768px) { +// .modal { +// width: 60%; +// } +// } + +// @media screen and (min-width: 1024px) { +// .modal { +// width: 80%; +// } +// } \ No newline at end of file diff --git a/admin-client/src/components/UIComponents/Modal/Modal.tsx b/admin-client/src/components/UIComponents/Modal/Modal.tsx new file mode 100644 index 0000000..4c17e50 --- /dev/null +++ b/admin-client/src/components/UIComponents/Modal/Modal.tsx @@ -0,0 +1,60 @@ +import React, { FC, ReactNode } from 'react'; +import ReactDOM from 'react-dom'; +import './Modal.scss'; +// import AddIcon from '../AddIcon/AddIcon'; +import CrossIcon from '../CrossIcon/CrossIcon'; +import { includeDarkClass } from '../../../CONFIG'; +import { RootState } from '../../../ReduxStore/store'; +import { useSelector } from 'react-redux'; + +interface ModalProps { + isOpen: Boolean; + onClose: () => void; + children: ReactNode; + heading: string; + mountOnPortal?: boolean; +} + +const Modal: FC = ({ isOpen, onClose, children, heading = "MOdal header Modal header" }, mountOnPortal) => { + const darkMode = useSelector((state: RootState) => state.UI.theme.dark) + if (!isOpen) return null; + + if (mountOnPortal) { + return ReactDOM.createPortal( +
+
e.stopPropagation()}> +
+
+

{heading}

+
+
+ { }} /> +
+
+
{children}
+
+
, + document.getElementById('modal-root')! + ); + } else { + return ( +
+
e.stopPropagation()}> +
+
+

{heading}

+
+
+ { }} /> +
+
+
{children}
+
+
+ ); + } + + +}; + +export default Modal; diff --git a/admin-client/src/components/UIComponents/NoofSubTodos.scss b/admin-client/src/components/UIComponents/NoofSubTodos.scss new file mode 100644 index 0000000..8bc18f0 --- /dev/null +++ b/admin-client/src/components/UIComponents/NoofSubTodos.scss @@ -0,0 +1,10 @@ +.NoofSubTodos_maindiv{ + display: flex; + background-color: #ffffff; + color: #8a8122; + font-size: 28px; + border-radius: 5px; + padding: 0 5px; + gap: 5px; + border:solid 1px rgb(46, 45, 45) +} \ No newline at end of file diff --git a/admin-client/src/components/UIComponents/NoofSubtodos.tsx b/admin-client/src/components/UIComponents/NoofSubtodos.tsx new file mode 100644 index 0000000..b121037 --- /dev/null +++ b/admin-client/src/components/UIComponents/NoofSubtodos.tsx @@ -0,0 +1,15 @@ +import { childTodo } from "../../medias" +import "./NoofSubTodos.scss" + +interface NoofSubtodosProps{ + subTodoNumber:Number +} + +const NoofSubtodos : React.FC = ({subTodoNumber})=>{ + return( +
child +
{subTodoNumber.toLocaleString()}
+ ) +} + +export default NoofSubtodos; \ No newline at end of file diff --git a/admin-client/src/components/UIComponents/PasswordInput.scss b/admin-client/src/components/UIComponents/PasswordInput.scss new file mode 100644 index 0000000..f178a91 --- /dev/null +++ b/admin-client/src/components/UIComponents/PasswordInput.scss @@ -0,0 +1,21 @@ +.passwordInput { + position: relative; + .inputFeild{ + position:relative; + .show_hide { + position: absolute; + top: 3px; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + margin: 0 5px; + right: 5px; + img { + width: 20px; + height:20px; + cursor:pointer + } + } + } +} diff --git a/admin-client/src/components/UIComponents/PasswordInput.tsx b/admin-client/src/components/UIComponents/PasswordInput.tsx new file mode 100644 index 0000000..60a63a5 --- /dev/null +++ b/admin-client/src/components/UIComponents/PasswordInput.tsx @@ -0,0 +1,42 @@ +import React, { useState } from 'react'; +import "./PasswordInput.scss" +import { hide, show } from '../../medias'; + +interface PasswordInputProps { + label: string; + id:string; + value:string; + onChange:any; + required?: any; +} + +const PasswordInput: React.FC = ({ label, id, value, onChange, required = false }) => { + const [showPassword, setShowPassword] = useState(false); + + const handleInputChange = (event: React.ChangeEvent) => { + onChange(event) + }; + + const togglePasswordVisibility = () => { + setShowPassword(!showPassword); + }; + + return ( +
+ +
+ +
+ {showPassword ? show : hide} +
+
+ ); +}; + +export default PasswordInput; diff --git a/admin-client/src/components/UIComponents/Profile/Profile.scss b/admin-client/src/components/UIComponents/Profile/Profile.scss new file mode 100644 index 0000000..fe8bf86 --- /dev/null +++ b/admin-client/src/components/UIComponents/Profile/Profile.scss @@ -0,0 +1,123 @@ +.profile_main_container { + flex: 1; + border-radius: 10px; + display: flex; + justify-content: center; + align-items: flex-start; + + @media screen and (min-device-width: 350px) and (max-device-width: 650px) { + width: 100%; + } + + .header { + font-size: 20px; + font-weight: 400; + color: rgba(0, 0, 0, 0.49); + &.dark { + color: rgb(255 255 255 / 49%); + } + } + .content { + word-wrap: break-word; + font-weight: 400; + } + + .horizontal_divider { + width: 100%; + height: 1px; + background: rgba(255, 255, 255, 0.446); + -webkit-backdrop-filter: blur(12px); + backdrop-filter: blur(16px); + } + + .profile_main_card { + background-color: rgba(255, 255, 255, 0.52); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + padding: 10px; + border-radius: 10px; + margin: 5% 0; + display: flex; + flex-direction: column; + align-items: center; + min-width: 300px; + max-width: 365px; + box-shadow: 0px 0px 2px; + outline: 1px solid rgb(0 0 0 / 61%); + border: 1px solid #00000040; + + .edit_profile_icon { + position: absolute; + width: 24px; + height: 24px; + top: 6px; + right: 7px; + background-color: rgba(255, 255, 255, 0.513); + padding: 5px; + border-radius: 5px; + path { + fill: #0080ff; + } + &.dark { + background-color: unset; + } + } + + &.dark { + background-color: rgba(0, 0, 0, 0.52); + } + + .profile_pic_con { + display: flex; + flex-direction: column; + align-items: center; + row-gap: 10px; + margin: 10px 0px; + width: 100%; + img, + svg { + width: 120px; + height: 120px; + border-radius: 50%; + outline: 1px solid #0000003b; + margin-bottom: 10px; + } + &.light { + svg { + fill: white; + } + } + .modal{ + form{ + display:flex; + .input_feild{ + display:flex; + } + } + } + @media screen and (min-device-width: 350px) and (max-device-width: 650px) { + display: flex; + flex-direction: column; + align-items: center; + row-gap: 10px; + margin: 10px 0 40px 0; + width: 100%; + } + } + + .profile_userName_con, + .profile_email, + .profile_picUrl { + width: 100%; + display: flex; + flex-direction: column; + row-gap: 5px; + margin: 5px 0 20px 0; + } + @media screen and (min-device-width: 350px) and (max-device-width: 650px) { + min-width: unset; + max-width: unset; + width: 88%; + } + } +} diff --git a/admin-client/src/components/UIComponents/Profile/Profile.tsx b/admin-client/src/components/UIComponents/Profile/Profile.tsx new file mode 100644 index 0000000..edc8876 --- /dev/null +++ b/admin-client/src/components/UIComponents/Profile/Profile.tsx @@ -0,0 +1,195 @@ +import { useSelector } from "react-redux"; +import "./Profile.scss"; +import { RootState } from "../../../ReduxStore/store"; +import { getUrl, includeDarkClass } from "../../../CONFIG"; +import { useDispatch } from "react-redux"; +import { useEffect, useState } from "react"; +import { setCurrentPage, setLoading } from "../../../ReduxStore/UISlice"; +import { motion } from "framer-motion"; +import LoaderComponent from "../LoaderComponent/LoaderComponent"; +import UserProfile from "../../../medias/UserProfile"; +import EditUserProfile from "../../../medias/EditUserProfile"; +import Modal from "../Modal/Modal"; + +interface ProfileProps { + fetchAllUserData: any; + handleLogout: any; +} + +const Profile: React.FC = ({ fetchAllUserData = () => { } ,handleLogout=() => { }}) => { + const userProfile = useSelector((state: RootState) => state.User.allUserData); + const darkMode = useSelector((state: RootState) => state.UI.theme.dark); + const token = useSelector((state: RootState) => state.User.token); + const [isOpen, setIsOpen] = useState(false) + const [userName, setUserName] = useState(userProfile.userName); + const [userEmail, setUserEmail] = useState(userProfile.email); + const [userPicUrl, setUserPicUrl] = useState(userProfile.picUrl); + + + const dispatch = useDispatch(); + + const handleProfileUpdate = (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(); + if (userName && userName !== userProfile.userName) { + formData.append('userName', userName) + } + if (userEmail && userEmail !== userProfile.email) { + formData.append('email', userEmail) + } + if (userPicUrl && userPicUrl !== userProfile.picUrl) { + formData.append('picUrl', userPicUrl) + } + console.log({ + userName: userName, + email: userEmail, + picUrl: userPicUrl + }) + dispatch(setLoading(true)) + fetch(getUrl('/auth/profile'), { + method: 'PUT', + body: formData, + headers: { + 'Authorization': `${token}`, + } + }).then(res => { + if (res.ok) { + console.log("user updated successfully") + fetchAllUserData(token) + setIsOpen(false) + } + console.log("error in updating user") + return res.json() + }).then((jsonRes) => { + console.log(jsonRes) + }).finally(() => { + console.log("finally") + }) + dispatch(setLoading(false)) + } + useEffect(() => { + dispatch(setCurrentPage("Your Profile Info")); + }, [dispatch]); + useEffect(() => { + console.log("Profile component Called"); + }, []); + + useEffect(() => { + if (userProfile) { + setUserName(userProfile.userName); + setUserEmail(userProfile.email); + setUserPicUrl(userProfile.picUrl); + } + }, [userProfile]); + + return ( + + {userProfile && + userProfile.userName && + userProfile.email && + userProfile.picUrl ? ( +
+
+
+ +
+
+ {userProfile.picUrl ? ( + profile pic + ) : ( + + )} + + {/*
*/} +
+ Profile Picture +
+
+
+
+
+ UserName :  +
+
+ {userProfile.userName} +
+
+
+
+
+ Email :  +
+
+ {userProfile.email} +
+
+
+
+
+ Profile Pic Url  +
+
+ {userProfile.picUrl} +
+
+
+
setIsOpen(!isOpen)} className={includeDarkClass("edit_profile_icon", darkMode)}> + +
+ {userProfile && userProfile.userName && userProfile.email && userProfile.picUrl ? setIsOpen(!isOpen)} heading="Edit Profile" > +
handleProfileUpdate(e)}> +
+ + setUserName(e.target.value)} /> +
+
+ + setUserEmail(e.target.value)} /> +
+
+ + setUserPicUrl(e.target.value)} /> +
+ {/* + */} +
+ + +
+
+
: <>} + +
+ ) : ( + + )} + + ); +}; + +export default Profile; diff --git a/admin-client/src/components/UIComponents/TodoDetails/TodoDetails.scss b/admin-client/src/components/UIComponents/TodoDetails/TodoDetails.scss new file mode 100644 index 0000000..5791c6d --- /dev/null +++ b/admin-client/src/components/UIComponents/TodoDetails/TodoDetails.scss @@ -0,0 +1,117 @@ +.todo_details_container { + border-radius: 10px; + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + height: auto; + background-color: rgb(255 255 255 / 10%); + // padding:10px; + + .todo_id { + font-size: 16px; + font-weight: 500; + display: flex; + align-items: center; + width: calc(100% - 20px); + border-radius: 10px 10px 0 0; + padding: 10px; + background: rgba(255, 255, 255, 0.5); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + flex-direction: row; + justify-content: space-between; + + &.dark { + background: rgb(125 125 125 / 50%); + } + } + + .horizontal_line { + width: 100%; + height: 1px; + background: rgba(255, 255, 255, 0.19); + -webkit-backdrop-filter: blur(12px); + backdrop-filter: blur(16px); + } + + .todo_contents_container { + width: calc(100% - 20px); + margin: 5px; + -webkit-backdrop-filter: blur(1px); + backdrop-filter: blur(10px); + border-radius: 5px; + padding: 5px; + display: flex; + flex-direction: column; + row-gap: 10px; + flex: 1 1; + background-color: rgba(255, 255, 255, 0.4); + + &.dark{ + background-color: unset; + } + + .header { + font-size: 12px; + font-weight: 400; + color: rgb(0 0 0 / 71%); + &.dark{ + color:rgb(255 255 255 / 44%) + } + } + + .content { + padding: 3px 0 10px 0; + font-size: 16px; + word-wrap: break-word; + } + + + + .todo_date_and_time_container { + display: flex; + column-gap: 20px; + + .todo_createdAt, + .todo_updatedAt { + background-color: rgb(255 255 255 / 10%); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + padding: 7px; + border-radius: 5px; + display: flex; + font-size: 12px; + flex-direction: column; + align-items: center; + + .header{ + margin-bottom: 5px; + } + + + .time { + padding: 0 10px; + } + } + + } + + .todo_subTodos { + .subTodo_addbtn { + display: flex; + align-items: center; + justify-content: space-between; + } + } + + .btn_grp { + display: flex; + justify-content: center; + column-gap: 10px; + button:nth-child(1){ + background-color: rgb(156, 255, 145); + } + } + } +} \ No newline at end of file diff --git a/admin-client/src/components/UIComponents/TodoDetails/TodoDetails.tsx b/admin-client/src/components/UIComponents/TodoDetails/TodoDetails.tsx new file mode 100644 index 0000000..592a8e6 --- /dev/null +++ b/admin-client/src/components/UIComponents/TodoDetails/TodoDetails.tsx @@ -0,0 +1,494 @@ +import { useParams } from "react-router-dom"; +import "./TodoDetails.scss"; +import { useSelector } from "react-redux"; +import { RootState } from "../../../ReduxStore/store"; +import { formatDateAndTime, getUrl, includeDarkClass } from "../../../CONFIG"; +import { useEffect, useState } from "react"; +import TodosListContainer from "../Todos/TodosListContainer/TodosListContainer"; +import Modal from "../Modal/Modal"; +import AddIcon from "../AddIcon/AddIcon"; +import { useDispatch } from "react-redux"; +import { setCurrentPage, setLoading } from "../../../ReduxStore/UISlice"; +import LoaderComponent from "../LoaderComponent/LoaderComponent"; +import CTAIconWrapper from "../../WRAPPERS/CTAIconWrapper/CTAIconWrapper"; +import Editsvg from "../../../medias/Editsvg"; + +interface TodoItem { + createdAt: string; + description: string; + possibleStatus: object; + possiblePriority: object; + title: string; + todo: TodoItem[]; + updatedAt: string; + status: string; + priority: string; + user: string; + todoId?:string; + parentTodo_id?:string; + __v: number; + _id: string; +} +interface TodoDetailsProps { + parentTodo_id?:string; +} +const status = ['Todo', 'InProgress', 'Completed', 'OnHold']; +const priority = ['High', 'Medium', 'Low']; +const TodoDetails: React.FC>= ({parentTodo_id=''}) => { + const [todo, setTodo] = useState(null); + const [createdAtdateAndTime, setCreatedAtDateAndTime] = + useState | null>(null); + const [updatedAtdateAndTime, setUpdatedAtDateAndTime] = + useState | null>(null); + const [isOpen, setIsOpen] = useState(false); + const [isEditing, setIsEditing] = useState(false); + const [subTodoTitleInput, setSubTodoTitleInput] = useState( + null + ); + const [subTodoDescInput, setSubTodoDescInput] = useState(null); + // State for input fields + const [titleInput, setTitleInput] = useState(""); + const [descriptionInput, setDescriptionInput] = useState(""); + const params = useParams(); + const dispatch = useDispatch(); + const token = useSelector((state: RootState) => state.User.token); + const darkMode = useSelector((state: RootState) => state.UI.theme.dark); + const finalParentTodo_id = params.parentTodo_id ? params.parentTodo_id : parentTodo_id + const update = async (changeObj: any,) => { + dispatch(setLoading(true)); + try { + if (token !== null) { + const formData = new FormData(); + if (params.childTodo_id) { + // Update a childTodo + formData.append("todoId", params.childTodo_id); + formData.append( + "changeObj", + changeObj + ); + const response = await fetch(getUrl("/admin/putSubTodo"), { + method: "PUT", + body: formData, + headers: { + Authorization: token, + }, + }); + if (!response.ok) { + dispatch(setLoading(false)); + setIsEditing(false); + setIsOpen(false); + throw new Error("Request failed"); + } + // Fetch updated childTodo after successful API call + fetchChildTodo(params.childTodo_id, token); + } else if (finalParentTodo_id) { + // Update a parentTodo + formData.append("todoId", finalParentTodo_id); + formData.append( + "changeObj", + changeObj + ); + const response = await fetch(getUrl("/admin/putTodo"), { + method: "PUT", + body: formData, + headers: { + Authorization: token, + }, + }); + if (!response.ok) { + dispatch(setLoading(false)); + setIsEditing(false); + setIsOpen(false); + throw new Error("Request failed"); + } + // Fetch updated parentTodo after successful API call + fetchParentTodo(finalParentTodo_id, token); + } + dispatch(setLoading(false)); + setIsEditing(false); + setIsOpen(false); + } + } catch (err) { + console.error("Error:", err); + dispatch(setLoading(false)); + setIsEditing(false); + setIsOpen(false); + } + } + + const updateStatus = async ( + event: React.ChangeEvent + ) => { + event.preventDefault(); + try { + if (token !== null) { + if (params.childTodo_id) { + // Update a childTodo + const chnageObj = JSON.stringify({ + status: event.target.value, + }) + update(chnageObj) + } else if (finalParentTodo_id) { + // Update a parentTodo + const chnageObj = JSON.stringify({ + status: event.target.value, + }) + update(chnageObj) + } + } + } catch (err) { + console.error("Error:", err); + } + }; + const updatePriority = async ( + event: React.ChangeEvent + ) => { + event.preventDefault(); + try { + if (token !== null) { + if (params.childTodo_id) { + // Update a childTodo + const chnageObj = JSON.stringify({ + priority: event.target.value, + }) + update(chnageObj) + } else if (finalParentTodo_id) { + // Update a parentTodo + const chnageObj = JSON.stringify({ + priority: event.target.value, + }) + update(chnageObj) + } + } + } catch (err) { + console.error("Error:", err); + } + }; + + const fetchParentTodo = (TodoId: string, token: string) => { + const formData = new FormData(); + formData.append("todoId", TodoId); + // console.log("Called"); + if (token) + fetch(getUrl("/admin/postGetTodo"), { + method: "POST", + body: formData, + headers: { + Authorization: token, + }, + }) + .then((res) => { + if (res.ok) { + return res.json(); + } else { + throw new Error("something went wrong"); + } + }) + .then((jsonData) => { + // console.log(jsonData); + setTodo(jsonData); + setCreatedAtDateAndTime( + formatDateAndTime(new Date(jsonData.createdAt)) + ); + setUpdatedAtDateAndTime( + formatDateAndTime(new Date(jsonData.updatedAt)) + ); + }); + }; + const fetchChildTodo = (TodoId: string, token: string) => { + const formData = new FormData(); + formData.append("todoId", TodoId); + if (token) + fetch(getUrl("/admin/postGetSubTodo"), { + method: "POST", + body: formData, + headers: { + Authorization: token, + }, + }) + .then((res) => { + if (res.ok) { + return res.json(); + } else { + throw new Error("something went wrong"); + } + }) + .then((jsonData) => { + setTodo(jsonData); + setCreatedAtDateAndTime( + formatDateAndTime(new Date(jsonData.createdAt)) + ); + setUpdatedAtDateAndTime( + formatDateAndTime(new Date(jsonData.updatedAt)) + ); + }); + }; + + const handleAddSubTodo = async (event: React.FormEvent) => { + event.preventDefault(); + dispatch(setLoading(true)); + if (subTodoTitleInput && subTodoDescInput && finalParentTodo_id) { + const formData = new FormData(); + formData.append("parentId", finalParentTodo_id); + formData.append("subTodoTitle", subTodoTitleInput); + formData.append("subTodoDescription", subTodoDescInput); + + try { + if (token !== null) { + const response = await fetch(getUrl("/admin/postSubTodo"), { + method: "POST", + body: formData, + headers: { + Authorization: token, + }, + }); + if (!response.ok) { + dispatch(setLoading(false)); + + setIsOpen(false); + throw new Error("Request failed"); + } + dispatch(setLoading(false)); + setIsOpen(false); + fetchParentTodo(finalParentTodo_id, token); + } + } catch (err) { + console.error("Error:", err); + dispatch(setLoading(false)); + setIsOpen(false); + } + } + }; + + useEffect(() => { + if (todo && todo.title && todo.description) { + setTitleInput(todo.title); + setDescriptionInput(todo.description); + } + }, [todo]); + + const handleUpdateSubmit = async ( + event: React.FormEvent + ) => { + event.preventDefault(); + try { + if (token !== null) { + if (params.childTodo_id) { + // Update a childTodo + const chnageObj = JSON.stringify({ + title: titleInput, + description: descriptionInput, + }) + update(chnageObj) + } else if (finalParentTodo_id) { + // Update a parentTodo + const chnageObj = JSON.stringify({ + title: titleInput, + description: descriptionInput, + }) + update(chnageObj) + } + } + } catch (err) { + console.error("Error:", err); + } + }; + + useEffect(() => { + if (token) { + if (finalParentTodo_id && !params.childTodo_id) { + fetchParentTodo(finalParentTodo_id, token); + } else if (finalParentTodo_id && params.childTodo_id) { + fetchChildTodo(params.childTodo_id, token); + dispatch(setCurrentPage("Sub-Todo Details")); + } + } + }, [dispatch, params.childTodo_id, finalParentTodo_id, token]); + if (!todo) { + return ; + } + return ( +
+
+
+ Todo ID: + {finalParentTodo_id} + setIsEditing(!isEditing)}> + + +
+
+
+
+
Title :
+
+ {!isEditing ? ( + todo.title + ) : ( + setTitleInput(e.target.value)} + > + )} +
+
+
+
+
+ Description : +
+
+ {!isEditing ? ( + todo.description + ) : ( + + )} +
+
+
+ {isEditing ? ( + <> +
+ + +
+
+ ) : ( + <> + )} + {createdAtdateAndTime && updatedAtdateAndTime && ( +
+
+
+ CreatedAt +
+
+ {createdAtdateAndTime[0]} +
+
+ {createdAtdateAndTime[1]} +
+
+
+
+ UpdatedAt +
+
+ {updatedAtdateAndTime[0]} +
+
+ {updatedAtdateAndTime[1]} +
+
+ {todo && todo.status ? +
+ + +
: <> + } + {todo && todo.priority ? +
+ + +
: <> + } +
+ )} +
+ {finalParentTodo_id && !params.childTodo_id ? ( +
+
+
+ SubTodos : +
+
+ +
+
+ +
+ +
+ {/*
*/} +
+ ) : ( + <> + )} +
+ setIsOpen(!isOpen)} + > +
+ + setSubTodoTitleInput(e.target.value)} + type="text" + placeholder="Title for your new subTodo ." + /> + setSubTodoDescInput(e.target.value)} + type="textarea" + placeholder="Description for your new subTodo ." + /> +
+ +
+ +
+
+ +
+ ); +}; + +export default TodoDetails; diff --git a/admin-client/src/components/UIComponents/Todos/TodoItem/TodoItem.scss b/admin-client/src/components/UIComponents/Todos/TodoItem/TodoItem.scss new file mode 100644 index 0000000..722df93 --- /dev/null +++ b/admin-client/src/components/UIComponents/Todos/TodoItem/TodoItem.scss @@ -0,0 +1,133 @@ +@import '../../../WRAPPERS/./DashboardWrapper/DashboardWrapper.scss'; + +.todo_item_individual { + font-size: 24px; + font-weight: 400; + width: calc(100% - 20px); + background: rgba(255, 255, 255, 0.4); + -webkit-backdrop-filter: blur(12px); + backdrop-filter: blur(10px); + border-radius: 10px; + padding: 3px 10px; + display: flex; + align-items: center; + margin: 5px 0; + box-shadow: inset 0px 0px 2px rgba(0, 0, 0, 0.4); + overflow: hidden; + flex-direction: column; + padding-top: 10px; + padding-bottom: 10px; + gap: 10px; + + + .status_mark { + width: 100%; + + position: absolute; + height: 2px; + bottom: 0px; + left: 0; + + &.Todo { + background-color: rgba(255, 225, 0, 0.684); + } + + &.InProgress { + background-color: #0080ff; + } + + &.Completed { + background-color: rgba(77, 255, 0, 0.684); + } + + &.OnHold { + background-color: rgba(255, 136, 0, 0.684); + } + } + + &.dark { + box-shadow: inset 0px 0px 2px rgba(255, 255, 255, 0.4); + background: $dash_contents_dark_mode_bgColor; + outline: $dash_contents_dark_mode_outline; + border: $dash_contents_dark_mode_border; + border: 0.5px; + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + } + + &:hover { + box-shadow: inset 0px 0px 5px; + } + + .date_and_time_ctas_container { + display: flex; + width: -webkit-fill-available; + justify-content: space-evenly; + + .date_and_time { + background-color: rgba(255, 255, 255, 0.2); + padding: 3px 3px; + border-radius: 10px; + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + display: flex; + flex-direction: column; + align-items: center; + width: 80px; + justify-content: center; + border-radius: 50px; + background: linear-gradient(145deg, #cacaca, #f0f0f0); + box-shadow: 20px 20px 60px #bebebe, + -20px -20px 60px #ffffff; + + + .text { + font-size: 8px; + font-weight: 900; + color: #3c3c3c; + + &.dark { + color: rgb(255, 255, 255, 90%); + } + } + } + + .todo_CTAs_container { + max-width: 100px; + width: 80px; + display: flex; + flex-direction: row; + align-items: center; + margin-left: 5px; + justify-content: space-between; + + + } + } + + + + .todo_item_title { + flex-grow: 1; + width: calc(100% - 0px); + font-weight: 400; + color: rgb(0 0 0); + font-size: 16px; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + display: -webkit-box; + white-space: wrap !important; + + &.dark { + color: rgb(255, 255, 255, 90%); + } + } + + +} + + +.del_cncl { + display: flex; + justify-content: space-evenly; +} \ No newline at end of file diff --git a/admin-client/src/components/UIComponents/Todos/TodoItem/TodoListItem.tsx b/admin-client/src/components/UIComponents/Todos/TodoItem/TodoListItem.tsx new file mode 100644 index 0000000..9c54673 --- /dev/null +++ b/admin-client/src/components/UIComponents/Todos/TodoItem/TodoListItem.tsx @@ -0,0 +1,148 @@ +import React, { useState } from 'react'; +import './TodoItem.scss'; +// import ChevronIcon from '../../Chevron/ChevronIcon'; +import { formatDateAndTime, getUrl, includeDarkClass } from '../../../../CONFIG'; +// import CrossIcon from '../../CrossIcon/CrossIcon'; +import { useSelector } from 'react-redux'; +import { RootState } from '../../../../ReduxStore/store'; +import { setLoading } from '../../../../ReduxStore/UISlice'; +import { useDispatch } from 'react-redux'; +import Modal from '../../Modal/Modal'; +import { useNavigate } from 'react-router-dom'; +import { motion } from 'framer-motion' +import CTAIconWrapper from '../../../WRAPPERS/CTAIconWrapper/CTAIconWrapper'; +import ChevronRight from '../../../../medias/ChevronRight'; +import CrossIcon from '../../../../medias/CrossIcon'; +import { TodoItem } from '../../../../App'; +import TodoDetails from '../../TodoDetails/TodoDetails'; + +interface PartialTodo extends Partial { } + +interface TodoListItemProps { + item: PartialTodo, + fetchAllUserData: any, + isSubTodo: boolean; + parentTodoId: string; + fetchParentTodo?: any; +} + +const TodoListItem: React.FC> = ({ item, fetchAllUserData, isSubTodo, parentTodoId = "", fetchParentTodo = () => { } }) => { + const token = useSelector((state: RootState) => state.User.token) + const theme = useSelector((state: RootState) => state.UI.theme) + const [todoItemModalIsOpen,setTodoItemModalIsOpen]= useState(false) + const dispatch = useDispatch() + const navigate = useNavigate() + const [isOpen, setIsOpen] = useState(false) + if (!item || !item.title || !item.createdAt) { + return null + } + const createdAtDate = new Date(item.createdAt) + const [date, time] = formatDateAndTime(createdAtDate) + + + const handleParentDelete = () => { + if (token) { + dispatch(setLoading(true)) + var formdata = new FormData(); + if (item && item._id) { + formdata.append("todoId", item._id); + } + fetch(getUrl("/admin/deleteTodo"), { + method: 'DELETE', + body: formdata, + headers: { + 'Authorization': token + } + }).then(response => response.json()) + .then(result => { + fetchAllUserData(token) + dispatch(setLoading(false)); + setIsOpen(false) + }) + .catch(error => console.log('error', error)); + dispatch(setLoading(false)); + setIsOpen(false) + } else { + console.error('No token Present') + } + } + const handleChildTodoDelete = () => { + if (token) { + dispatch(setLoading(true)) + var formdata = new FormData(); + if (item && item._id) { + formdata.append("subTodoId", item._id); + } + formdata.append("parentTodoId", parentTodoId); + fetch(getUrl("/admin/deleteSubTodo"), { + method: 'DELETE', + body: formdata, + headers: { + 'Authorization': token + } + }).then(response => response.json()) + .then(result => { + // fetchAllUserData(token) + fetchParentTodo(parentTodoId, token) + dispatch(setLoading(false)); + setIsOpen(false) + }) + .catch(error => console.log('error', error)); + dispatch(setLoading(false)); + setIsOpen(false) + } else { + console.error('No token Present') + } + } + const handleRedirect = () => { + const id = item._id; + if (!isSubTodo) { + navigate(`/todos/${id}`) + } else { + navigate(`/todos/${parentTodoId}/subTodo/${id}`) + } + } + return ( + setTodoItemModalIsOpen(!todoItemModalIsOpen)} + id={item._id} className={`todo_item_individual ${item && item.status ? `${item.status}` : ''} ${theme.dark ? 'dark' : 'light'}`}> +
{item && item.title}
+
+
+
{date}
+
{time}
+
+
+ { + setIsOpen(!isOpen); + }} > + + + + + +
+
+
+ setIsOpen(!isOpen)} heading={`Are you sure you want to delete the "${item.title}" TODO ???`}> +
+ +
+
+ setTodoItemModalIsOpen(!todoItemModalIsOpen)} heading={`Todo Details!! `}> + + +
+ ); +}; + +export default TodoListItem; diff --git a/admin-client/src/components/UIComponents/Todos/TodosListContainer/TodosListContainer.scss b/admin-client/src/components/UIComponents/Todos/TodosListContainer/TodosListContainer.scss new file mode 100644 index 0000000..6f9c888 --- /dev/null +++ b/admin-client/src/components/UIComponents/Todos/TodosListContainer/TodosListContainer.scss @@ -0,0 +1,46 @@ +.todoListItems_container { + display: flex; + flex-direction: row; + width: 100%; + gap: 20px; + overflow: scroll; + padding: 1px; + + .status_container { + display: flex; + min-width: 325px; + max-width: 500px; + flex-direction: column; + align-items: center; + justify-content: flex-start; + background: rgba(206, 206, 206, 0.27); + backdrop-filter: blur(15px); + outline: 0.5px solid #1f2025; + border: 1px solid #6d6d6d; + border-radius: 10px; + + .title { + width: calc(100% - 10px); + background-color: #1f2025; + padding: 5px; + border-radius: 10px 10px 0 0; + background-color: rgba(255, 255, 255, 0.099); + } + + .all_todos { + width: calc(100% - 7px); + } + + &.Todo { + background-color: rgba(255, 255, 255, 0.099); + outline: 0.5px solid #1f2025; + border: 1px solid #6d6d6d; + } + + &.dark { + background-color: rgba(255, 255, 255, 0.099); + outline: 0.5px solid #1f2025; + border: 1px solid #6d6d6d; + } + } +} \ No newline at end of file diff --git a/admin-client/src/components/UIComponents/Todos/TodosListContainer/TodosListContainer.tsx b/admin-client/src/components/UIComponents/Todos/TodosListContainer/TodosListContainer.tsx new file mode 100644 index 0000000..bd7a53a --- /dev/null +++ b/admin-client/src/components/UIComponents/Todos/TodosListContainer/TodosListContainer.tsx @@ -0,0 +1,98 @@ +import React, { useEffect } from "react"; +import './TodosListContainer.scss' +import TodoItem from "../TodoItem/TodoListItem"; +import { useDispatch } from "react-redux"; +// import { setLoading } from "../../../../ReduxStore/UISlice"; +import LoaderComponent from "../../LoaderComponent/LoaderComponent"; +import { setCurrentPage } from "../../../../ReduxStore/UISlice"; +import { includeDarkClass } from "../../../../CONFIG"; +import { useSelector } from "react-redux"; +import { RootState } from "../../../../ReduxStore/store"; + +interface TodoListContainerProps { + todosArray: { + createdAt: string; + title: string; + todo: any[]; + updatedAt: string; + user: string; + __v: number; + _id: string; + }[], + isAllTodosContainer: boolean; + fetchAllUserData: any; + isSubTodoContainer?: boolean; + parentTodoId?: string; + fetchParentTodo?: any; + className?: string; + title?: string; + hideParent?: boolean; +} + +// const NoTodosSvg: any = () =>
dsfdsvsd
+ +export const Container: React.FC> = ({hideParent=false, title = '', className = '', todosArray = [], isSubTodoContainer = false, parentTodoId = '', fetchAllUserData = () => { }, fetchParentTodo = () => { } }) => { + const darkMode = useSelector((state: RootState) => state.UI.theme.dark) + return ( +
+
{title}
+
+ {todosArray && todosArray.length === 0 ? 'NoTodosSvg' : todosArray ? todosArray.map((item, index) => { + return ( + <> + + + ) + }) + : + } +
+
+ ) +}; + +const TodosListContainer: React.FC> = ({ isAllTodosContainer, todosArray, fetchAllUserData, isSubTodoContainer, parentTodoId = "", fetchParentTodo = () => { } }) => { + + const dispatch = useDispatch() + + const darkMode = useSelector((state: RootState) => state.UI.theme.dark) + const User = useSelector((state: RootState) => state.User.allUserData) + + useEffect(() => { + if (isSubTodoContainer) { + dispatch(setCurrentPage('Todo Details')) + } else { + dispatch(setCurrentPage('All Todos')) + } + }, [dispatch, isSubTodoContainer]) + return ( +
+ { + isAllTodosContainer && todosArray ? + + : isSubTodoContainer && todosArray ? + + : <> + {User && User.statusFiltered && User.statusFiltered.__filteredTodos ? + + : <> + } + {User && User.statusFiltered && User.statusFiltered.__filteredInProgress ? + + : <> + } + {User && User.statusFiltered && User.statusFiltered.__filteredCompleted ? + + : <> + } + {User && User.statusFiltered && User.statusFiltered.__filteredOnHold ? + + : <> + } + } + +
+ ) +} + +export default TodosListContainer \ No newline at end of file diff --git a/admin-client/src/components/WRAPPERS/CTAIconWrapper/CTAIconWrapper.scss b/admin-client/src/components/WRAPPERS/CTAIconWrapper/CTAIconWrapper.scss new file mode 100644 index 0000000..aecfa44 --- /dev/null +++ b/admin-client/src/components/WRAPPERS/CTAIconWrapper/CTAIconWrapper.scss @@ -0,0 +1,14 @@ +.icon_Wrapper{ + width: 24px; + height: 24px; + background-color: rgba(255, 255, 255, 0.513); + padding: 5px; + border-radius: 5px; + path { + fill: #0080ff; + } + &.dark { + background-color: unset; + } + +} \ No newline at end of file diff --git a/admin-client/src/components/WRAPPERS/CTAIconWrapper/CTAIconWrapper.tsx b/admin-client/src/components/WRAPPERS/CTAIconWrapper/CTAIconWrapper.tsx new file mode 100644 index 0000000..24e5f8d --- /dev/null +++ b/admin-client/src/components/WRAPPERS/CTAIconWrapper/CTAIconWrapper.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import "./CTAIconWrapper.scss" +import { includeDarkClass } from '../../../CONFIG' +import { useSelector } from 'react-redux' +import { RootState } from '../../../ReduxStore/store' + +type Props = { + children: React.ReactNode, + onClick: any +} + +const CTAIconWrapper: React.FC = ({ children, onClick }) => { + const darkMode = useSelector((state: RootState) => state.UI.theme.dark) + return ( +
+ {children} +
+ ) +} + +export default CTAIconWrapper \ No newline at end of file diff --git a/admin-client/src/components/WRAPPERS/DashboardWrapper/DashboardWrapper.scss b/admin-client/src/components/WRAPPERS/DashboardWrapper/DashboardWrapper.scss new file mode 100644 index 0000000..f9d33e8 --- /dev/null +++ b/admin-client/src/components/WRAPPERS/DashboardWrapper/DashboardWrapper.scss @@ -0,0 +1,424 @@ +@import "../../../styles/typography/typography.scss"; + +$dash_contents_dark_mode_outline: 0.5px solid #1f2025; +$dash_contents_dark_mode_border: 1px solid #6d6d6d; +$dash_contents_dark_mode_bgColor: rgb(55 55 55 / 69%); + +.main_dashboard_container { + position: absolute; + width: 100vw; + height: 100vh; + display: flex; + flex-direction: column; + min-width: 350px; + max-width: 1200px; + + .dashboard_navbar { + display: flex; + width: calc(100% - 20px); + height: 65px; + background: rgba(255, 255, 255, 0.2); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + border-bottom: 1px solid rgb(255, 255, 255); + box-shadow: 0 5px 4px rgba(0, 0, 0, 0.2); + flex-direction: row; + align-items: center; + padding: 0px 10px; + + @media screen and (min-device-width:1200px) { + border-radius: 40px; + + } + + .menu_btn { + width: 30px; + height: 30px; + padding: 5px; + display: flex; + justify-content: center; + align-items: center; + border-radius: 10px; + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + border-radius: 10px; + margin-right: 10px; + background: rgba(0, 0, 0, 0.3); + display: none; + + @media screen and (min-device-width: 350px) and (max-device-width: 650px) { + display: flex; + } + } + + &.dark { + border-bottom: 1px solid rgb(136 136 136); + } + + .theme_toggle { + display: flex; + align-items: center; + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + border-radius: 10px; + + // &.dark{ + // background: rgba(255, 255, 255, 0.3); + // } + &.light { + background: rgba(0, 0, 0, 0.3); + } + + div { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + + &.dark { + svg { + width: 80%; + + path { + fill: white; + } + } + } + + &.light { + svg { + width: 100%; + + path { + fill: #f1ff00; + } + } + } + } + } + + .logo_image { + width: 40px; + height: 40px; + background: rgba(255, 255, 255, 0.68); + -webkit-backdrop-filter: blur(12px); + border: 1px solid #dadada; + outline: 1px solid #00000036; + backdrop-filter: blur(4px); + display: flex; + justify-content: center; + align-items: center; + border-radius: 50%; + margin-right: 10px; + + img { + width: 25px; + } + } + + .navbar_heading { + @include typography($heading3-size, 500, $default-line-height, #000000); + + &.dark { + color: #dadada; + } + } + + .navbar_void { + flex: 1; + } + + .navbar_navlinks { + display: flex; + align-items: center; + flex-direction: row; + gap: 20px; + margin-right: 20px; + + @media screen and (min-device-width: 350px) and (max-device-width: 650px) { + display: none; + // &.open { + // display: flex; + // position: absolute; + // z-index: 1; + // flex: 1; + // height: calc(100vh - 86px); + // } + } + + .navbar_navlink_item { + font-weight: 400; + position: relative; + + a { + text-decoration: none; + color: currentcolor; + } + + &:hover { + color: $link_blue; + cursor: pointer; + + &:before { + content: ""; + width: 100%; + background-color: $link_blue; + height: 2px; + position: absolute; + border-radius: 2px; + bottom: -5px; + box-shadow: 0px 0px 14px; + } + } + } + } + + .navbar_right { + width: 40px; + height: 40px; + background: rgba(255, 255, 255, 0.68); + -webkit-backdrop-filter: blur(12px); + backdrop-filter: blur(4px); + display: flex; + justify-content: center; + align-items: center; + border-radius: 50%; + border: 1px solid #dadada; + outline: 1px solid #00000036; + margin-right: 10px; + + .profile_pic { + display: flex; + justify-content: center; + align-items: center; + + img { + width: 100%; + border-radius: 50%; + } + + svg { + width: 42px !important; + } + } + } + } + + .dashboard_sidebar_and_contents { + width: 100%; + flex: 1; + display: flex; + flex-direction: row; + overflow: hidden; + + .dashboard_sidebar { + display: flex; + flex-direction: column; + width: 230px; + background: rgb(255 255 255 / 10%); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + padding: 10px; + + @media screen and (min-device-width: 350px) and (max-device-width: 650px) { + display: none; + + &.open { + display: flex; + position: absolute; + z-index: 1; + flex: 1; + height: calc(100vh - 86px); + } + } + + @media screen and (min-device-width:1200px) { + background: unset; + -webkit-backdrop-filter: unset; + backdrop-filter: unset; + } + + .dashboard_sidebar_contents { + width: 100%; + flex-grow: unset; + display: flex; + flex-direction: column; + align-items: center; + border-radius: 10px; + background: rgba(255, 255, 255, 0.45); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + overflow: auto; + padding: 2px 0; + + &.dark { + background: rgb(0 0 0 / 54%); + outline: 1px solid #1f2025; + border: 1px solid #6d6d6d; + } + + .sidebar_item_container { + display: flex; + flex-direction: column; + width: calc(100% - 5%); + align-items: center; + + .sidebar_item { + display: flex; + flex-direction: column; + cursor: pointer; + width: calc(100% - 10px); + padding: 5px; + border-radius: 5px; + margin: 4px 0; + color: black; + + a { + font-size: 16px; + font-weight: 400; + } + + &:hover, + &.selected { + font-weight: 400; + background-color: rgb(255 255 255 / 26%); + -webkit-backdrop-filter: blur(12px); + backdrop-filter: blur(16px); + color: $link_blue; + } + + &.dark { + color: $link_blue; + } + } + + .horizontal_divider { + width: 100%; + height: 1px; + background: rgba(255, 255, 255, 0.19); + -webkit-backdrop-filter: blur(12px); + backdrop-filter: blur(16px); + } + } + } + + + } + + .dashboard_sidebar_logoutbtn { + padding-top: 10px; + width: 60%; + + .logoutBtn { + width: 100%; + border: none; + @include typography($subheading-size, + 500, + $default-line-height, + #fb7575); + border-radius: 10px; + } + } + + .dashboard_contents_main_container { + flex: 1; + display: flex; + overflow: auto !important; + flex-direction: column; + padding: 10px; + row-gap: 10px; + position: relative !important; + // background: rgb(255 255 255 / 10%); + // -webkit-backdrop-filter: blur(10px); + // backdrop-filter: blur(10px); + + .contents_header { + display: flex; + position: sticky; + top: 0; + z-index: 1; + + div.contents_header_container { + background: rgba(255, 255, 255, 0.1); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(5px); + box-shadow: rgb(40, 40, 40) 0px 4px 10px; + border-radius: 10px; + padding: 10px; + width: 100%; + height: 29.5px; + display: flex; + align-items: center; + justify-content: flex-start; + + &.dark { + background-color: unset; + outline: $dash_contents_dark_mode_outline; + border: $dash_contents_dark_mode_border; + } + + h1 { + font-size: 24px; + margin: 0; + flex-grow: 1; + } + } + + .addTodo_btn_div { + button { + background-color: green; + } + } + } + + .contents_container { + border-radius: 10px; + row-gap: 10px; + flex: 1; + display: flex; + + &.dark {} + } + } + } +} + +.button_wrapper { + button { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + + .btn_text { + padding: 0 5px; + } + + .add_icon { + margin-left: 5px; + border: 2px solid rgb(20 232 0 / 42%); + + .line1, + .line2 { + background-color: rgb(110 255 78); + } + } + } +} + +.addTodo_form_container { + form { + display: flex; + flex-direction: column; + row-gap: 10px; + width: 100%; + + div { + display: flex; + align-items: center; + justify-content: center; + } + } +} \ No newline at end of file diff --git a/admin-client/src/components/WRAPPERS/DashboardWrapper/DashboardWrapper.tsx b/admin-client/src/components/WRAPPERS/DashboardWrapper/DashboardWrapper.tsx new file mode 100644 index 0000000..ebd0e74 --- /dev/null +++ b/admin-client/src/components/WRAPPERS/DashboardWrapper/DashboardWrapper.tsx @@ -0,0 +1,315 @@ +import React, { ReactNode, useState } from "react"; +import "./DashboardWrapper.scss"; +import logo from "../../../medias/logo.png"; +// import { useNavigate } from "react-router-dom"; +import { useSelector } from "react-redux"; +import { RootState } from "../../../ReduxStore/store"; +import { useDispatch } from "react-redux"; +import { + setLoading, + setSideBarActiveTab, + toggleMobSidebar, + toogleDarkLight, +} from "../../../ReduxStore/UISlice"; +import Modal from "../../UIComponents/Modal/Modal"; +import AddIcon from "../../UIComponents/AddIcon/AddIcon"; +import { getUrl, includeDarkClass } from "../../../CONFIG"; +import { Link, Outlet, useNavigate, useParams } from "react-router-dom"; +import { MenuButton } from "../../UIComponents/MenuButton/MenuButton"; +import UserProfile from "../../../medias/UserProfile"; + +interface DashboardWrapperProps { + handleLogout: any; + children?: ReactNode; + heading: string; + fetchAllUserData: any; +} + +const lightModeSvgContent = + ''; + +const darkModeSvgContent = + ''; + +const sideBarData = [ + { name: "Todos", url: "/todos" }, + { name: null, url: null }, + { name: "All Todos", url: "/all-todos" }, + { name: "Profile", url: "/profile" }, +]; +const navLinks = [ + { name: null, url: null }, +]; + +const DashboardWrapper: React.FC = ({ + children, + fetchAllUserData, +}) => { + const [isOpen, setIsOpen] = useState(false); + const [todoTitleInput, setTodoTitleInput] = useState(null); + const [todoDescInput, setTodoDescInput] = useState(null); + const [selectedStatus, setSelectedStatus] = useState('Todo'); + + const handleStatusChange = (event: React.ChangeEvent) => { + const newStatus = event.target.value; + setSelectedStatus(newStatus); + // onChange(newStatus); + }; + const [selectedPriority, setSelectedPriority] = useState('Medium'); + + const handlePriorityChange = (event: React.ChangeEvent) => { + const newStatus = event.target.value; + setSelectedPriority(newStatus); + // onChange(newStatus); + }; + + + const navigate = useNavigate(); + const params = useParams(); + const token = useSelector((state: RootState) => state && state.User && state.User.token); + const allUserData = useSelector((state: RootState) => state && state.User && state.User.allUserData); + const currentPage = useSelector((state: RootState) => state && state.UI && state.UI.currentPage); + const currSideBarIdx = useSelector((state: RootState) => state && state.UI && state.UI.sideBarActiveTab) + const theme = useSelector((state: RootState) => state && state.UI && state.UI.theme); + const darkMode = useSelector((state: RootState) => state && state.UI && state.UI.theme.dark); + const isMobSidebarOpen = useSelector( + (state: RootState) => state && state.UI && state.UI.isMobSidebarOpen + ); + const isDarkMode = () => { + const localStorageDarkMode = localStorage && localStorage.getItem("darkMode"); + + if ( + (localStorageDarkMode != null && localStorageDarkMode === "True") || + (theme.dark && theme.dark === true) + ) { + return true; + } + return false; + }; + + const dispatch = useDispatch(); + + const handleAddTodo = async (event: React.FormEvent) => { + event.preventDefault(); + dispatch(setLoading(true)); + if (todoTitleInput && todoDescInput) { + const formData = new FormData(); + formData.append("title", todoTitleInput ? todoTitleInput : 'Your title'); + formData.append("description", todoDescInput ? todoDescInput : 'Your desc'); + formData.append("status", selectedStatus ? selectedStatus : 'your status'); + formData.append("priority", selectedPriority ? selectedPriority : 'priority'); + + try { + if (token !== null) { + const response = await fetch(getUrl("/admin/postTodo"), { + method: "POST", + body: formData, + headers: { + Authorization: token, + }, + }); + if (!response.ok) { + dispatch(setLoading(false)); + + setIsOpen(false); + throw new Error("Request failed"); + } + // const jsonData = await response.json(); + dispatch(setLoading(false)); + setIsOpen(false); + dispatch(setLoading(true)); + fetchAllUserData(token); + dispatch(setLoading(false)); + // console.log(jsonData) + } + } catch (err) { + console.error("Error:", err); + dispatch(setLoading(false)); + setIsOpen(false); + } + } + }; + return ( +
+
+
+ +
+
+ logo +
+
+ Todos +
+
+
+ {navLinks && navLinks.length !== 0 && navLinks.map((item, index) => { + return ( +
+ {item && item.url && item.name && {item.name}} +
+ ); + })} +
+
+
navigate("/profile")} + className={includeDarkClass("profile_pic", darkMode)} + > + {allUserData && allUserData.picUrl ? + dispatch(setSideBarActiveTab(sideBarData && sideBarData.length && sideBarData.length - 1))} src={allUserData.picUrl} alt="profile pic" /> : + + } +
+
+
+ {isDarkMode() ? ( +
dispatch(toogleDarkLight())} + className={includeDarkClass(" ", darkMode)} + dangerouslySetInnerHTML={{ __html: darkModeSvgContent }} + >
+ ) : ( +
dispatch(toogleDarkLight())} + className={includeDarkClass(" ", darkMode)} + dangerouslySetInnerHTML={{ __html: lightModeSvgContent }} + >
+ )} +
+
+
+
+
+ {sideBarData && sideBarData.length !== 0 && sideBarData.map((Item, index) => { + return ( + <>{ + Item && Item.name && Item.name !== null ?
dispatch(setSideBarActiveTab(index))} + > +
+ {Item && Item.url && Item.name && dispatch(toggleMobSidebar())} + to={Item.url} + > + {Item.name} + } +
+ {index !== sideBarData.length - 1 ?
: <>} +
: <>} + + ); + })} +
+
+
+
+
+

{currentPage}

+ {!params.parentTodo_id ? ( +
+ +
+ ) : ( + <> + )} +
+
+
+ {children} + {/* FOR rendering the underlying children components */} + +
+ setIsOpen(!isOpen)} + > +
+
+ setTodoTitleInput(e.target.value)} + type="text" + placeholder="Title for your new Todo ." + /> + setTodoDescInput(e.target.value)} + type="textarea" + placeholder="Description for your new Todo ." + /> +
+ + +
+
+ + +
+
+ +
+
+
+
+
+
+
+ ); +}; + +export default DashboardWrapper; diff --git a/admin-client/src/components/WRAPPERS/ModalWrapper/ModalWrapper.scss b/admin-client/src/components/WRAPPERS/ModalWrapper/ModalWrapper.scss new file mode 100644 index 0000000..e69de29 diff --git a/admin-client/src/components/WRAPPERS/ModalWrapper/ModalWrapper.tsx b/admin-client/src/components/WRAPPERS/ModalWrapper/ModalWrapper.tsx new file mode 100644 index 0000000..552def9 --- /dev/null +++ b/admin-client/src/components/WRAPPERS/ModalWrapper/ModalWrapper.tsx @@ -0,0 +1,23 @@ +import { ReactNode, useState } from "react" +import Modal from "../../UIComponents/Modal/Modal" + +interface ModalWrapperProps { + heading: string, + children: ReactNode +} + +const ModalWrapper: React.FC = ({ heading, children }) => { + + const [isOpen, setIsOpen] = useState(false) + + const handleToggle = () => { + setIsOpen(!isOpen) + } + return ( + + {children} + + ) +} + +export default ModalWrapper; \ No newline at end of file diff --git a/admin-client/src/hooks/useGETAllTodos.tsx b/admin-client/src/hooks/useGETAllTodos.tsx new file mode 100644 index 0000000..43e0655 --- /dev/null +++ b/admin-client/src/hooks/useGETAllTodos.tsx @@ -0,0 +1,49 @@ +import { useState, useEffect } from 'react'; +import { useDispatch } from 'react-redux'; +import { setAllTodos /* setLoading */ } from '../ReduxStore/UISlice'; +import { RootState } from '../ReduxStore/store'; +import { useSelector } from 'react-redux'; + +type HttpError = { + message: string; + status?: number; +}; + +function useUserProfileCall(url: string, options: object): [HttpError | null] { + const [err, setErr] = useState(null); + const dispatch = useDispatch(); + + const token = useSelector((state: RootState) => state.User.token) + + useEffect(() => { + const fetchData = async (token: string | null) => { + // dispatch(setLoading(true)); + try { + if (token !== null) { + const response = await fetch(url, options); + if (!response.ok) { + // dispatch(setLoading(false)); + throw new Error('Request failed'); + } + const jsonData = await response.json(); + dispatch(setAllTodos(jsonData)); + // dispatch(setLoading(false)); + // console.log(jsonData) + } + } catch (err) { + console.error('Error:', err); + setErr({ + message: (err as Error).message, + }); + // dispatch(setLoading(false)); + } + }; + + fetchData(token); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dispatch, token]); + + return [err]; +} + +export default useUserProfileCall; diff --git a/admin-client/src/hooks/useHTTP.tsx b/admin-client/src/hooks/useHTTP.tsx new file mode 100644 index 0000000..29d53cd --- /dev/null +++ b/admin-client/src/hooks/useHTTP.tsx @@ -0,0 +1,60 @@ +import { useState, useEffect } from 'react'; +import { setLoading } from '../ReduxStore/UISlice'; +import { useDispatch } from 'react-redux'; + +type HttpError = { + message: string; + status?: number; +}; + +function useHttp( + url: string, + options?: RequestInit +): [T | null, HttpError | null] { + const [response, setResponse] = useState(null); + const [error, setError] = useState(null); + + const dispatch = useDispatch(); + + useEffect(() => { + const fetchData = async () => { + // dispatch(setLoading(true)); + try { + const headers: HeadersInit = {}; + if (options?.headers) { + for (const [key, value] of Object.entries(options.headers)) { + if (value !== null) { + headers[key] = value.toString(); + } + } + } + const fetchOptions: RequestInit = { + ...options, + headers, + }; + + const fetchResponse = await fetch(url, fetchOptions); + if (!fetchResponse.ok) { + // dispatch(setLoading(false)); + throw new Error(`Request failed with status: ${fetchResponse.status}`); + } + + const responseData: T = await fetchResponse.json(); + setResponse(responseData); + // dispatch(setLoading(false)); + } catch (error: unknown) { + setError({ + message: (error as Error).message, + status: (error as HttpError).status, + }); + // dispatch(setLoading(false)); + } + }; + + fetchData(); + }, [url, options, dispatch]); + + return [response, error]; +} + +export default useHttp; diff --git a/admin-client/src/hooks/useUserProfileAPICall.tsx b/admin-client/src/hooks/useUserProfileAPICall.tsx new file mode 100644 index 0000000..066465e --- /dev/null +++ b/admin-client/src/hooks/useUserProfileAPICall.tsx @@ -0,0 +1,50 @@ +import { useState, useEffect } from 'react'; +import { useDispatch } from 'react-redux'; +// import { setLoading } from '../ReduxStore/UISlice'; +import { RootState } from '../ReduxStore/store'; +import { useSelector } from 'react-redux'; +import { setAllUserData } from '../ReduxStore/UserSlice'; + +type HttpError = { + message: string; + status?: number; +}; + +function useUserProfileCall(url: string, options: object): [HttpError | null] { + const [err, setErr] = useState(null); + const dispatch = useDispatch(); + + const token = useSelector((state: RootState) => state.User.token) + + useEffect(() => { + const fetchData = async (token: string | null) => { + // dispatch(setLoading(true)); + try { + if (token !== null) { + const response = await fetch(url, options); + if (!response.ok) { + // dispatch(setLoading(false)); + throw new Error('Request failed'); + } + const jsonData = await response.json(); + dispatch(setAllUserData(jsonData)); + // dispatch(setLoading(false)); + // console.log(jsonData) + } + } catch (err) { + console.error('Error:', err); + setErr({ + message: (err as Error).message, + }); + // dispatch(setLoading(false)); + } + }; + + fetchData(token); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dispatch, token]); + + return [err]; +} + +export default useUserProfileCall; diff --git a/admin-client/src/index.css b/admin-client/src/index.css new file mode 100644 index 0000000..ec2585e --- /dev/null +++ b/admin-client/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/admin-client/src/index.tsx b/admin-client/src/index.tsx new file mode 100644 index 0000000..e9a7895 --- /dev/null +++ b/admin-client/src/index.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; +import { Provider } from 'react-redux'; +import store from './ReduxStore/store'; +import { BrowserRouter } from "react-router-dom"; + +const ReduxProvider = Provider + +ReactDOM.render( + + + + + + + + , document.getElementById('root')); + +// Before +// ReactDOM.render(, document.getElementById('root')); + +// After +// createRoot(document.getElementById('root')).render(); \ No newline at end of file diff --git a/admin-client/src/medias/404 Error (1).gif b/admin-client/src/medias/404 Error (1).gif new file mode 100644 index 0000000..e3479a7 Binary files /dev/null and b/admin-client/src/medias/404 Error (1).gif differ diff --git a/admin-client/src/medias/404 Error.gif b/admin-client/src/medias/404 Error.gif new file mode 100644 index 0000000..09ed376 Binary files /dev/null and b/admin-client/src/medias/404 Error.gif differ diff --git a/admin-client/src/medias/ChevronRight copy.jsx b/admin-client/src/medias/ChevronRight copy.jsx new file mode 100644 index 0000000..f0c0c56 --- /dev/null +++ b/admin-client/src/medias/ChevronRight copy.jsx @@ -0,0 +1,8 @@ +const Nothing = () => { + return ( + + ) +} + +export default Nothing; \ No newline at end of file diff --git a/admin-client/src/medias/ChevronRight.jsx b/admin-client/src/medias/ChevronRight.jsx new file mode 100644 index 0000000..d6f5f98 --- /dev/null +++ b/admin-client/src/medias/ChevronRight.jsx @@ -0,0 +1,7 @@ +const ChevronRight = () => { + return ( + + ) +} + +export default ChevronRight; \ No newline at end of file diff --git a/admin-client/src/medias/CrossIcon.jsx b/admin-client/src/medias/CrossIcon.jsx new file mode 100644 index 0000000..3d6d495 --- /dev/null +++ b/admin-client/src/medias/CrossIcon.jsx @@ -0,0 +1,11 @@ +const CrossIcon = () => { + return ( + + ) +} + +export default CrossIcon; \ No newline at end of file diff --git a/admin-client/src/medias/EditUserProfile.jsx b/admin-client/src/medias/EditUserProfile.jsx new file mode 100644 index 0000000..c36ee33 --- /dev/null +++ b/admin-client/src/medias/EditUserProfile.jsx @@ -0,0 +1,7 @@ +const EditUserProfile = () => { + return ( + + ) +} + +export default EditUserProfile \ No newline at end of file diff --git a/admin-client/src/medias/Editsvg.jsx b/admin-client/src/medias/Editsvg.jsx new file mode 100644 index 0000000..0a85199 --- /dev/null +++ b/admin-client/src/medias/Editsvg.jsx @@ -0,0 +1,7 @@ +const Editsvg = () => { + return ( + + ) +} + +export default Editsvg; \ No newline at end of file diff --git a/admin-client/src/medias/UserProfile.jsx b/admin-client/src/medias/UserProfile.jsx new file mode 100644 index 0000000..c216d22 --- /dev/null +++ b/admin-client/src/medias/UserProfile.jsx @@ -0,0 +1,12 @@ +const UserProfile = () => { + return + + + + + + + +} + +export default UserProfile; \ No newline at end of file diff --git a/admin-client/src/medias/add.png b/admin-client/src/medias/add.png new file mode 100644 index 0000000..6f5a6c8 Binary files /dev/null and b/admin-client/src/medias/add.png differ diff --git a/admin-client/src/medias/bgfinaldark.jpg b/admin-client/src/medias/bgfinaldark.jpg new file mode 100644 index 0000000..6e0ef2d Binary files /dev/null and b/admin-client/src/medias/bgfinaldark.jpg differ diff --git a/admin-client/src/medias/bgfinallight.jpg b/admin-client/src/medias/bgfinallight.jpg new file mode 100644 index 0000000..49da791 Binary files /dev/null and b/admin-client/src/medias/bgfinallight.jpg differ diff --git a/admin-client/src/medias/child-todos.svg b/admin-client/src/medias/child-todos.svg new file mode 100644 index 0000000..8884c44 --- /dev/null +++ b/admin-client/src/medias/child-todos.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-client/src/medias/hidden.png b/admin-client/src/medias/hidden.png new file mode 100644 index 0000000..7d71841 Binary files /dev/null and b/admin-client/src/medias/hidden.png differ diff --git a/admin-client/src/medias/icons8-edit-512.svg b/admin-client/src/medias/icons8-edit-512.svg new file mode 100644 index 0000000..f5a10aa --- /dev/null +++ b/admin-client/src/medias/icons8-edit-512.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-client/src/medias/icons8-edit.gif b/admin-client/src/medias/icons8-edit.gif new file mode 100644 index 0000000..476ae47 Binary files /dev/null and b/admin-client/src/medias/icons8-edit.gif differ diff --git a/admin-client/src/medias/index.js b/admin-client/src/medias/index.js new file mode 100644 index 0000000..ffc1dda --- /dev/null +++ b/admin-client/src/medias/index.js @@ -0,0 +1,10 @@ +import AddImg from "./add.png" +import DeleteImg from "./remove.png" +import hide from "./hidden.png" +import show from "./view.png" +import rightArrow from "./rightarrow.svg" +import edit from "./icons8-edit-512.svg" +import childTodo from "./subtask.svg" + + +export { AddImg , DeleteImg , hide ,show, rightArrow,edit,childTodo} \ No newline at end of file diff --git a/admin-client/src/medias/light.svg b/admin-client/src/medias/light.svg new file mode 100644 index 0000000..1ae3e1f --- /dev/null +++ b/admin-client/src/medias/light.svg @@ -0,0 +1 @@ +Created by Annisa Aulia Rahmanfrom the Noun Project \ No newline at end of file diff --git a/admin-client/src/medias/logo.png b/admin-client/src/medias/logo.png new file mode 100644 index 0000000..3d79fd0 Binary files /dev/null and b/admin-client/src/medias/logo.png differ diff --git a/admin-client/src/medias/noun-moon-4951761.svg b/admin-client/src/medias/noun-moon-4951761.svg new file mode 100644 index 0000000..cb41bf2 --- /dev/null +++ b/admin-client/src/medias/noun-moon-4951761.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-client/src/medias/remove.png b/admin-client/src/medias/remove.png new file mode 100644 index 0000000..2b4c356 Binary files /dev/null and b/admin-client/src/medias/remove.png differ diff --git a/admin-client/src/medias/right-arrow.png b/admin-client/src/medias/right-arrow.png new file mode 100644 index 0000000..d3fc6c6 Binary files /dev/null and b/admin-client/src/medias/right-arrow.png differ diff --git a/admin-client/src/medias/rightarrow.svg b/admin-client/src/medias/rightarrow.svg new file mode 100644 index 0000000..6a8533d --- /dev/null +++ b/admin-client/src/medias/rightarrow.svg @@ -0,0 +1 @@ +92-Arrow Right \ No newline at end of file diff --git a/admin-client/src/medias/subtask.svg b/admin-client/src/medias/subtask.svg new file mode 100644 index 0000000..ce93af6 --- /dev/null +++ b/admin-client/src/medias/subtask.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/admin-client/src/medias/user.svg b/admin-client/src/medias/user.svg new file mode 100644 index 0000000..266c95b --- /dev/null +++ b/admin-client/src/medias/user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/admin-client/src/medias/view.png b/admin-client/src/medias/view.png new file mode 100644 index 0000000..2810628 Binary files /dev/null and b/admin-client/src/medias/view.png differ diff --git a/admin-client/src/react-app-env.d.ts b/admin-client/src/react-app-env.d.ts new file mode 100644 index 0000000..6431bc5 --- /dev/null +++ b/admin-client/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/admin-client/src/reportWebVitals.ts b/admin-client/src/reportWebVitals.ts new file mode 100644 index 0000000..49a2a16 --- /dev/null +++ b/admin-client/src/reportWebVitals.ts @@ -0,0 +1,15 @@ +import { ReportHandler } from 'web-vitals'; + +const reportWebVitals = (onPerfEntry?: ReportHandler) => { + if (onPerfEntry && onPerfEntry instanceof Function) { + import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(onPerfEntry); + getFID(onPerfEntry); + getFCP(onPerfEntry); + getLCP(onPerfEntry); + getTTFB(onPerfEntry); + }); + } +}; + +export default reportWebVitals; diff --git a/admin-client/src/setupTests.ts b/admin-client/src/setupTests.ts new file mode 100644 index 0000000..8f2609b --- /dev/null +++ b/admin-client/src/setupTests.ts @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom'; diff --git a/admin-client/src/styles/typography/typography.scss b/admin-client/src/styles/typography/typography.scss new file mode 100644 index 0000000..18f2dc1 --- /dev/null +++ b/admin-client/src/styles/typography/typography.scss @@ -0,0 +1,75 @@ +// _typography.scss + +@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&family=Signika:wght@300;400;500;600;700&display=swap'); + +// Font families +$font-primary: 'Helvetica Neue', Arial, sans-serif; +$font-secondary: 'Roboto', sans-serif; + +// Heading styles +$heading1-size: 2.5rem; +$heading2-size: 2rem; +$heading3-size: 1.75rem; +$heading4-size: 1.5rem; +$heading5-size: 1.25rem; +$heading6-size: 1rem; + +$heading1-font: $font-primary; +$heading2-font: $font-primary; +$heading3-font: $font-primary; +$heading4-font: $font-primary; +$heading5-font: $font-primary; +$heading6-font: $font-primary; + +$heading1-line-height: 1.2; +$heading2-line-height: 1.3; +$heading3-line-height: 1.4; +$heading4-line-height: 1.5; +$heading5-line-height: 1.6; +$heading6-line-height: 1.7; + +$heading1-font-weight: 700; +$heading2-font-weight: 600; +$heading3-font-weight: 600; +$heading4-font-weight: 600; +$heading5-font-weight: 600; +$heading6-font-weight: 600; + +$heading1-color: #333; +$heading2-color: #333; +$heading3-color: #333; +$heading4-color: #333; +$heading5-color: #333; +$heading6-color: #333; + +// Subheading styles +$subheading-size: 1.1rem; +$subheading-font: $font-secondary; +$subheading-line-height: 1.4; +$subheading-font-weight: 600; +$subheading-color: #666; + +// Body text styles +$body-size: 1rem; +$body-font: $font-primary; +$body-line-height: 1.6; +$body-font-weight: 400; +$body-color: #444; + + +$default-font-size: 1rem; +$default-font-weight: 300; +$default-line-height: 1.6; + +$link_blue:#0080ff; + +$dark_mode_outline:1px solid #1f2025; +$dark_mode_border:1px solid #6d6d6d; +$dark_mode_bgColor:rgba(0, 0, 0, 0.54); + +@mixin typography($font-size: $default-font-size, $font-weight: $default-font-weight, $line-height: $default-line-height, $color: inherit) { + font-size: $font-size; + font-weight: $font-weight; + line-height: $line-height; + color: $color; +} diff --git a/admin-client/src/utilFuncs/getRandomColor.tsx b/admin-client/src/utilFuncs/getRandomColor.tsx new file mode 100644 index 0000000..2052175 --- /dev/null +++ b/admin-client/src/utilFuncs/getRandomColor.tsx @@ -0,0 +1,9 @@ +function getRandomColor(): string { + const randomValue = () => Math.floor(Math.random() * 156) + 100; // Range: 100-255 + const r = randomValue(); + const g = randomValue(); + const b = randomValue(); + return `rgb(${r}, ${g}, ${b})`; +} + +export default getRandomColor \ No newline at end of file diff --git a/admin-client/tsconfig.json b/admin-client/tsconfig.json new file mode 100644 index 0000000..c787c37 --- /dev/null +++ b/admin-client/tsconfig.json @@ -0,0 +1,32 @@ + +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "es6", + "esnext" + ], + "allowJs": true, + "downlevelIteration": true, + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": [ + "src" + ] +}