diff --git a/client/src/App.js b/client/src/App.js index 55ee4c24a..0985abb04 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -4,11 +4,14 @@ import { Toaster } from 'react-hot-toast' import { BrowserRouter as Router } from 'react-router-dom' import { GoogleOAuthProvider } from '@react-oauth/google' +import { useLoadSocket } from './api/hooks/socket/useLoadSocket' import { useRoutes } from './routes/routes' function App() { const routes = useRoutes() + useLoadSocket() + return ( <> diff --git a/client/src/api/endpoints/registration-auth.js b/client/src/api/endpoints/registration-auth.js deleted file mode 100644 index 4deb7c1b4..000000000 --- a/client/src/api/endpoints/registration-auth.js +++ /dev/null @@ -1,55 +0,0 @@ -// * Modules -import axios from 'axios' - -// * API -import http from '../../http' -// * Redux -import { registrationAuth } from '../../store/reducers/RegistrationAuth' - -const { API_URL, api } = http - -const checkRegistration = () => async (dispatch) => { - try { - dispatch(registrationAuth.actions.finishRegistration()) - const response = await axios.get(`${API_URL}/get-user-object`, { withCredentials: true }) - let { isRegistered, email, userUsername } = response.data - - dispatch( - registrationAuth.actions.setUserInitialData({ - email, - isRegistered, - userUsername, - }), - ) - } catch (err) { - dispatch(registrationAuth.actions.finishRegistrationError(err.response?.data?.message)) - } -} - -const finishRegistration = (userData) => async (dispatch) => { - try { - dispatch(registrationAuth.actions.finishRegistration()) - await api.post('/registration-checkout', userData) - dispatch(registrationAuth.actions.finishRegistrationSuccess()) - } catch (err) { - dispatch(registrationAuth.actions.finishRegistrationError(err.response?.data?.message)) - } -} - -const validateUsername = async (username, email) => { - try { - const response = await api.get('/check-username', { params: { username, email } }) - - return response.data - } catch (err) { - console.log(err) - } -} - -const registerAuthApi = Object.freeze({ - finishRegistration, - checkRegistration, - validateUsername, -}) - -export default registerAuthApi diff --git a/client/src/api/endpoints/submission.js b/client/src/api/endpoints/submission.js deleted file mode 100644 index ed4911db7..000000000 --- a/client/src/api/endpoints/submission.js +++ /dev/null @@ -1,36 +0,0 @@ -// * API -import http from '../../http' - -const { api } = http -const t_id = '638bcf732c63139d604db787' - -const makeSubmission = async (s_parts, team_id) => { - try { - const data = await api.post('/make-submission', { s_parts, team_id, t_id }) - - return data - } catch (err) { - console.log(err) - - return err.message - } -} - -const getSubmissions = async () => { - try { - const data = await api.post('/get-submissions', { t_id }) - - return data - } catch (err) { - console.log(err) - - return err.message - } -} - -const submissionApi = Object.freeze({ - makeSubmission, - getSubmissions, -}) - -export default submissionApi diff --git a/client/src/api/endpoints/team.js b/client/src/api/endpoints/team.js deleted file mode 100644 index 6aea094eb..000000000 --- a/client/src/api/endpoints/team.js +++ /dev/null @@ -1,88 +0,0 @@ -// * API -import http from '../../http' - -const { api } = http - -const createTeam = async (teamName, teamCountry, teamMembers) => { - try { - const data = await api.post('/teams/create-team', { teamName, teamCountry, teamMembers }) - - return data - } catch (err) { - console.log(err) - - return err.message - } -} - -const getAllTeams = async () => { - try { - const data = await api.get('/teams') - - return data - } catch (err) { - console.log(err) - - return err.message - } -} - -const getTeamById = async (id) => { - try { - // const data = await api.post('/get-team-byid', { teamId }) - const data = await api.get(`/teams/get-team/${id}`) - - return data - } catch (err) { - console.log(err) - - return err.message - } -} - -const getTeamMembers = async (teamMembers) => { - try { - const data = await api.post('/get-teammembers', { teamMembers }) - - return data - } catch (err) { - console.log(err) - - return err.message - } -} - -const addUserToTeam = async (userId, teamId) => { - // try { - // const data = await api.post('/add-to-team', { teamId, userId }) - // - // return data - // } catch (err) { - // console.log(err) - // - // return err.message - // } -} - -const inviteUserByEmail = async (email, teamId, userId) => { - try { - const data = await api.post('/teams/invite', { email, teamid: teamId, from_user_id: userId }) - - return data - } catch (err) { - console.log(err) - - return err.message - } -} - -const teamsApi = Object.freeze({ - createTeam, - getAllTeams, - getTeamById, - getTeamMembers, - addUserToTeam, - inviteUserByEmail, -}) - -export default teamsApi diff --git a/client/src/api/endpoints/tournament.js b/client/src/api/endpoints/tournament.js deleted file mode 100644 index 6719fdc50..000000000 --- a/client/src/api/endpoints/tournament.js +++ /dev/null @@ -1,36 +0,0 @@ -// * API -import http from '../../http' - -const { api } = http - -const addTeamToTournament = async (team_id, frontend_id, backend_id) => { - try { - const t_id = '638bcf732c63139d604db787' - const data = await api.post('/add-to-tournament', { t_id, team_id, frontend_id, backend_id }) - - return data - } catch (err) { - console.log(err) - - return err.message - } -} - -const checkUserSignedUp = async (userId) => { - try { - const data = await api.post('/check-user-exists-tournament', { userId }) - - return data - } catch (err) { - console.log(err) - - return err.message - } -} - -const tournamentApi = Object.freeze({ - addTeamToTournament, - checkUserSignedUp, -}) - -export default tournamentApi diff --git a/client/src/api/endpoints/users.js b/client/src/api/endpoints/users.js deleted file mode 100644 index b0df6cef6..000000000 --- a/client/src/api/endpoints/users.js +++ /dev/null @@ -1,39 +0,0 @@ -// * API -import qs from 'qs' - -import http from '../../http' -import filteredQueryMaker from '../../utils/filteredQueryMaker' - -const { api } = http -const getUsers = async (page) => { - try { - const data = await api.get('/users', { params: { page } }) - - return data - } catch (err) { - console.log(err) - - return err.message - } -} - -const getUsersFiltered = async (page, countries, roles, programmingLanguages) => { - try { - const filtersQuery = filteredQueryMaker(countries, roles, programmingLanguages) - let queryString = qs.stringify(filtersQuery) - const data = await api.get('/users-filtered', { - params: { page, filtersQuery: queryString }, - }) - - return data - } catch (err) { - return err.message - } -} - -const usersApi = Object.freeze({ - getUsers, - getUsersFiltered, -}) - -export default usersApi diff --git a/client/src/api/hooks/auth/useCheckAuth.js b/client/src/api/hooks/auth/useCheckAuth.js index 5a2dfd5b7..7988d203f 100644 --- a/client/src/api/hooks/auth/useCheckAuth.js +++ b/client/src/api/hooks/auth/useCheckAuth.js @@ -20,6 +20,8 @@ export const useCheckAuth = () => { onSuccess: (data) => { if (data && data.isRegistered) { dispatch(userAuth.actions.authUserSuccess()) + dispatch(userAuth.actions.setUserNotifications(data?.notifications)) + dispatch(userAuth.actions.setUserId(data?._id)) } }, onError: (error) => { diff --git a/client/src/api/hooks/auth/useLoginUser.js b/client/src/api/hooks/auth/useLoginUser.js index 61f50d4be..d049a98ba 100644 --- a/client/src/api/hooks/auth/useLoginUser.js +++ b/client/src/api/hooks/auth/useLoginUser.js @@ -36,6 +36,8 @@ export const useLoginUser = (type) => { if (user.isRegistered) { // save accessToken dispatch(userAuth.actions.authUserSuccess()) + dispatch(userAuth.actions.setUserNotifications(user?.notifications)) + dispatch(userAuth.actions.setUserId(data?._id)) navigate('/', { replace: true }) } else { // navigate user to finish registration diff --git a/client/src/api/hooks/auth/useUpdateAvatar.js b/client/src/api/hooks/shared/useUpdateAvatar.js similarity index 76% rename from client/src/api/hooks/auth/useUpdateAvatar.js rename to client/src/api/hooks/shared/useUpdateAvatar.js index 1861b3d89..b49117025 100644 --- a/client/src/api/hooks/auth/useUpdateAvatar.js +++ b/client/src/api/hooks/shared/useUpdateAvatar.js @@ -6,7 +6,7 @@ import { errorToaster } from '../../../shared/components/Toasters/Error.toaster' const { api } = http -export const useUpdateAvatar = (type) => { +export const useUpdateAvatar = (type, successHandler) => { const queryClient = useQueryClient() const updateUserAvatar = async (userData) => { @@ -18,6 +18,12 @@ export const useUpdateAvatar = (type) => { onSuccess: async () => { if (type === 'teams') { await queryClient.invalidateQueries('getTeamById', { refetchInactive: true }) + } else if (type === 'users') { + await queryClient.invalidateQueries('getUserById', { refetchInactive: true }) + } + + if (successHandler) { + successHandler() } await queryClient.invalidateQueries('checkAuth', { refetchInactive: true }) diff --git a/client/src/api/hooks/sidebar/useReadMessages.js b/client/src/api/hooks/sidebar/useReadMessages.js index cc2490885..5edcc1077 100644 --- a/client/src/api/hooks/sidebar/useReadMessages.js +++ b/client/src/api/hooks/sidebar/useReadMessages.js @@ -1,15 +1,11 @@ -import { useMutation, useQueryClient } from 'react-query' -import { useDispatch } from 'react-redux' +import { useMutation } from 'react-query' import http from '../../../http' -import { registrationAuth } from '../../../store/reducers/RegistrationAuth' +import { errorToaster } from '../../../shared/components/Toasters/Error.toaster' const { api } = http export const useReadMessages = () => { - const dispatch = useDispatch() - const queryClient = useQueryClient() - const readMessages = async (idsArr) => { return await api.put('notifications/read', { notifications: idsArr }) } @@ -21,7 +17,7 @@ export const useReadMessages = () => { }, onError: (error) => { // set error message - dispatch(registrationAuth.actions.finishRegistrationError(error.response?.data?.message)) + errorToaster(error) }, }) } diff --git a/client/src/api/hooks/socket/useLoadSocket.js b/client/src/api/hooks/socket/useLoadSocket.js new file mode 100644 index 000000000..8067a45c6 --- /dev/null +++ b/client/src/api/hooks/socket/useLoadSocket.js @@ -0,0 +1,64 @@ +import { useEffect } from 'react' +import { useDispatch, useSelector } from 'react-redux' + +import { userAuth } from '../../../store/reducers/UserAuth' +import { socket } from '../../sockets/notifications.socket' + +export const useLoadSocket = () => { + const { notifications, userId } = useSelector((state) => state.userReducer) + + const dispatch = useDispatch() + + function onConnect() { + console.log('connected') + // setIsConnected(true) + socket.emit('subscribeToNotifications', JSON.stringify({ id: userId })) + } + + function onDisconnect() { + console.log('disconnecting...') + } + + useEffect(() => { + if (userId) { + socket.connect() + + socket.on('connect', onConnect) + socket.on('disconnect', onDisconnect) + + return () => { + socket.disconnect() + + socket.off('connect', onConnect) + socket.off('disconnect', onDisconnect) + } + } + }, [userId]) + + useEffect(() => { + function onNotificationsEvent(notification) { + // Find the index of the existing notification with the same _id + const existingIndex = notifications.findIndex( + (n) => String(n._id) === String(notification._id), + ) + + // If an existing notification is found, update it + if (existingIndex !== -1) { + const updatedNotifications = [...notifications] + + updatedNotifications[existingIndex] = notification + dispatch(userAuth.actions.setUserNotifications(updatedNotifications)) + } + // If not, add the new notification to the array + else { + dispatch(userAuth.actions.setUserNotifications([...notifications, notification])) + } + } + + socket.on(`notification-${userId}`, onNotificationsEvent) + + return () => { + socket.off(`notification-${userId}`, onNotificationsEvent) + } + }, [notifications]) +} diff --git a/client/src/api/hooks/auth/useEditUserDetails.js b/client/src/api/hooks/user/useEditUserDetails.js similarity index 70% rename from client/src/api/hooks/auth/useEditUserDetails.js rename to client/src/api/hooks/user/useEditUserDetails.js index 1e7bb3b56..7787003db 100644 --- a/client/src/api/hooks/auth/useEditUserDetails.js +++ b/client/src/api/hooks/user/useEditUserDetails.js @@ -14,9 +14,12 @@ export const useEditUserDetails = (successHandler) => { return useMutation(finishRegistration, { mutationKey: 'finishRegistration', - onSuccess: async (data) => { - await queryClient.invalidateQueries('checkAuth', { refetchInactive: true }) - successHandler() + onSuccess: (data) => { + queryClient.invalidateQueries('checkAuth', { refetchInactive: true }) + queryClient.invalidateQueries('getUserById', { refetchInactive: true }) + if (successHandler) { + successHandler() + } }, onError: (error) => { // set error message diff --git a/client/src/api/hooks/user/useGetUserById.js b/client/src/api/hooks/user/useGetUserById.js new file mode 100644 index 000000000..cf18c21da --- /dev/null +++ b/client/src/api/hooks/user/useGetUserById.js @@ -0,0 +1,20 @@ +import { useMutation, useQuery, useQueryClient } from 'react-query' +import { useNavigate } from 'react-router-dom' + +import http from '../../../http' +import { errorToaster } from '../../../shared/components/Toasters/Error.toaster' +import { successToaster } from '../../../shared/components/Toasters/Success.toaster' + +const { api } = http + +export const useGetUserById = (userid) => { + const navigate = useNavigate() + + const getUserById = async () => { + return await api.get(`/users/get-by-id/${userid}`) + } + + const { data, isLoading, error } = useQuery(['getUserById', userid], getUserById, { retry: 0 }) + + return { data, isLoading, error } +} diff --git a/client/src/api/hooks/auth/useValidateUsername.js b/client/src/api/hooks/user/useValidateUsername.js similarity index 95% rename from client/src/api/hooks/auth/useValidateUsername.js rename to client/src/api/hooks/user/useValidateUsername.js index b96d27e68..21d4a6fc8 100644 --- a/client/src/api/hooks/auth/useValidateUsername.js +++ b/client/src/api/hooks/user/useValidateUsername.js @@ -7,8 +7,6 @@ import { successToaster } from '../../../shared/components/Toasters/Success.toas const { api } = http export const useValidateUsername = () => { - const queryClient = useQueryClient() - const validateUsername = async (username) => { return await api.get(`/users/get-by-username/${username}`) } diff --git a/client/src/api/sockets/notifications.socket.js b/client/src/api/sockets/notifications.socket.js index 57faa4425..493f9d0ba 100644 --- a/client/src/api/sockets/notifications.socket.js +++ b/client/src/api/sockets/notifications.socket.js @@ -2,4 +2,6 @@ import { io } from 'socket.io-client' import { LOCAL_PATH } from '../../http' -export const socket = io(LOCAL_PATH) +export const socket = io(LOCAL_PATH, { + autoConnect: false, +}) diff --git a/client/src/assets/Arrows/ArrowDown.js b/client/src/assets/Arrows/ArrowDown.js index 6451b89f0..0dc394d68 100644 --- a/client/src/assets/Arrows/ArrowDown.js +++ b/client/src/assets/Arrows/ArrowDown.js @@ -1,12 +1,11 @@ function ArrowDown() { return ( - + ) diff --git a/client/src/assets/UserProfile/Cake.js b/client/src/assets/UserProfile/Cake.js new file mode 100644 index 000000000..bd27cfab7 --- /dev/null +++ b/client/src/assets/UserProfile/Cake.js @@ -0,0 +1,43 @@ +function CakeIcon() { + return ( + + + + + + + + ) +} + +export default CakeIcon diff --git a/client/src/assets/UserProfile/CrossIcon.js b/client/src/assets/UserProfile/CrossIcon.js new file mode 100644 index 000000000..73ef9ceb2 --- /dev/null +++ b/client/src/assets/UserProfile/CrossIcon.js @@ -0,0 +1,15 @@ +function CrossIcon() { + return ( + + + + ) +} + +export default CrossIcon diff --git a/client/src/assets/UserProfile/EditIcon.js b/client/src/assets/UserProfile/EditIcon.js new file mode 100644 index 000000000..8d2f0ad36 --- /dev/null +++ b/client/src/assets/UserProfile/EditIcon.js @@ -0,0 +1,14 @@ +function EditIcon() { + return ( + + + + ) +} + +export default EditIcon diff --git a/client/src/assets/UserProfile/Email.js b/client/src/assets/UserProfile/Email.js deleted file mode 100644 index 2d934a3be..000000000 --- a/client/src/assets/UserProfile/Email.js +++ /dev/null @@ -1,22 +0,0 @@ -function Email() { - return ( - - - - - ) -} - -export default Email diff --git a/client/src/assets/UserProfile/EmailIcon.js b/client/src/assets/UserProfile/EmailIcon.js new file mode 100644 index 000000000..c0f8cf94a --- /dev/null +++ b/client/src/assets/UserProfile/EmailIcon.js @@ -0,0 +1,12 @@ +function EmailIcon() { + return ( + + + + ) +} + +export default EmailIcon diff --git a/client/src/assets/UserProfile/LinkIcon.jsx b/client/src/assets/UserProfile/LinkIcon.jsx new file mode 100644 index 000000000..36e50b227 --- /dev/null +++ b/client/src/assets/UserProfile/LinkIcon.jsx @@ -0,0 +1,21 @@ +function LinkIcon() { + return ( + + + + + + ) +} + +export default LinkIcon diff --git a/client/src/assets/UserProfile/Location.js b/client/src/assets/UserProfile/Location.js deleted file mode 100644 index 582fb3834..000000000 --- a/client/src/assets/UserProfile/Location.js +++ /dev/null @@ -1,22 +0,0 @@ -function Location() { - return ( - - - - - ) -} - -export default Location diff --git a/client/src/assets/UserProfile/LocationIcon.js b/client/src/assets/UserProfile/LocationIcon.js new file mode 100644 index 000000000..48b31aba4 --- /dev/null +++ b/client/src/assets/UserProfile/LocationIcon.js @@ -0,0 +1,12 @@ +function LocationIcon() { + return ( + + + + ) +} + +export default LocationIcon diff --git a/client/src/assets/UserProfile/MessageIcon.js b/client/src/assets/UserProfile/MessageIcon.js index 573c32902..467fb67a2 100644 --- a/client/src/assets/UserProfile/MessageIcon.js +++ b/client/src/assets/UserProfile/MessageIcon.js @@ -1,20 +1,23 @@ function MessageIcon() { return ( - + + + ) diff --git a/client/src/assets/UserProfile/PlusIconWhite.jsx b/client/src/assets/UserProfile/PlusIconWhite.jsx new file mode 100644 index 000000000..b7933d88a --- /dev/null +++ b/client/src/assets/UserProfile/PlusIconWhite.jsx @@ -0,0 +1,20 @@ +const PlusIconWhite = () => { + return ( + + + + + ) +} + +export default PlusIconWhite diff --git a/client/src/assets/UserProfile/Star.js b/client/src/assets/UserProfile/Star.js deleted file mode 100644 index d7c3fb35d..000000000 --- a/client/src/assets/UserProfile/Star.js +++ /dev/null @@ -1,15 +0,0 @@ -function Star() { - return ( - - - - ) -} - -export default Star diff --git a/client/src/assets/UserProfile/StarIcon.js b/client/src/assets/UserProfile/StarIcon.js new file mode 100644 index 000000000..8fc83c63d --- /dev/null +++ b/client/src/assets/UserProfile/StarIcon.js @@ -0,0 +1,12 @@ +function StarIcon() { + return ( + + + + ) +} + +export default StarIcon diff --git a/client/src/assets/UserProfile/TeamMembersIcon.jsx b/client/src/assets/UserProfile/TeamMembersIcon.jsx new file mode 100644 index 000000000..9f93a8771 --- /dev/null +++ b/client/src/assets/UserProfile/TeamMembersIcon.jsx @@ -0,0 +1,36 @@ +function TeamMembersIcon() { + return ( + + + + + + + ) +} + +export default TeamMembersIcon diff --git a/client/src/assets/UserProfile/UserIcon.js b/client/src/assets/UserProfile/UserIcon.js new file mode 100644 index 000000000..9f7ca0140 --- /dev/null +++ b/client/src/assets/UserProfile/UserIcon.js @@ -0,0 +1,22 @@ +function UserIcon() { + return ( + + + + + ) +} + +export default UserIcon diff --git a/client/src/components/CreateTeam/CreateTeam.js b/client/src/components/CreateTeam/CreateTeam.js index 3a888f75a..67715b8af 100644 --- a/client/src/components/CreateTeam/CreateTeam.js +++ b/client/src/components/CreateTeam/CreateTeam.js @@ -4,7 +4,7 @@ import { useNavigate } from 'react-router-dom' // API import { useCheckAuth } from '../../api/hooks/auth/useCheckAuth' -import { useUpdateAvatar } from '../../api/hooks/auth/useUpdateAvatar' +import { useUpdateAvatar } from '../../api/hooks/shared/useUpdateAvatar' import { useCreateTeam } from '../../api/hooks/team/useCreateTeam' import { defaultTeamAvatars } from '../../constants/teamFormData' // * Assets diff --git a/client/src/components/Forms/Page404Form/Page404Form.js b/client/src/components/Forms/Page404Form/Page404Form.js index 35e7d729a..6057b1719 100644 --- a/client/src/components/Forms/Page404Form/Page404Form.js +++ b/client/src/components/Forms/Page404Form/Page404Form.js @@ -28,7 +28,7 @@ const Page404Form = ({ paddingLeft = '0', findText = `Couldn't find the requeste {findText} - + diff --git a/client/src/components/Forms/Page404Form/Page404Form.styles.js b/client/src/components/Forms/Page404Form/Page404Form.styles.js index 49a800838..fbf3e25fd 100644 --- a/client/src/components/Forms/Page404Form/Page404Form.styles.js +++ b/client/src/components/Forms/Page404Form/Page404Form.styles.js @@ -8,6 +8,10 @@ export const Container = styled.div` align-items: center; justify-content: center; padding-left: ${(props) => props.paddingLeft || '0'}; + + @media screen and (max-width: 768px) { + padding-left: 0; + } ` export const InfoContainer = styled.div` display: flex; diff --git a/client/src/components/NavBar/NavBar.data.js b/client/src/components/NavBar/NavBar.data.js index c6a21e2ab..5a3646688 100644 --- a/client/src/components/NavBar/NavBar.data.js +++ b/client/src/components/NavBar/NavBar.data.js @@ -3,9 +3,7 @@ import React from 'react' // * Assets import SearchIcon from '../../assets/Sidebar/SearchIcon' -// import Team from '../../assets/Sidebar/Team' unused import TrophyIcon from '../../assets/Sidebar/TrophyIcon' -import UserIcon from '../../assets/Sidebar/UserIcon' export const NavBarData = [ { @@ -18,9 +16,4 @@ export const NavBarData = [ path: '/tournaments', icon: , }, - { - title: 'Profile', - path: '/profile', - icon: , - }, ] diff --git a/client/src/components/NavBar/NavBar.jsx b/client/src/components/NavBar/NavBar.jsx index ac380d859..4138cd38f 100644 --- a/client/src/components/NavBar/NavBar.jsx +++ b/client/src/components/NavBar/NavBar.jsx @@ -5,12 +5,12 @@ import { useNavigate } from 'react-router-dom' import { useCheckAuth } from '../../api/hooks/auth/useCheckAuth' import { useLogoutUser } from '../../api/hooks/auth/useLogoutUser' -import { socket } from '../../api/sockets/notifications.socket' // * Assets import Close from '../../assets/Sidebar/Close' import Exit from '../../assets/Sidebar/Exit' import ShortLogo from '../../assets/Sidebar/ShortLogo' import Team from '../../assets/Sidebar/Team' +import UserIcon from '../../assets/Sidebar/UserIcon' import { useOutsideClick } from '../../hooks/useOutsideClick' import IconWrapper from '../../shared/components/IconWrapper/IconWrapper' import Loader from '../../shared/components/Loader/Loader' @@ -38,9 +38,7 @@ const NavBar = () => { const [notificationModal, setNotificationModal] = useState(false) const { isAuth } = useSelector((state) => state.userReducer) - const { data: user, isFetching: isUserDataLoading } = useCheckAuth() - - const [userNotifications, setUserNotifications] = useState(user?.notifications || []) + const { data: user } = useCheckAuth() const newNavData = [ NavBarData[0], @@ -49,55 +47,18 @@ const NavBar = () => { icon: , path: user?.team ? `/team/${user.team._id}` : '/team', }, - ...NavBarData.slice(1), - ] + NavBarData[1], + user && { + title: 'Profile', + path: `/profile/${user._id}`, + icon: , + }, + ].filter(Boolean) const { mutate: logoutUser, isLoading: isUserLoggingOut } = useLogoutUser() const navigate = useNavigate() const navMenuRef = useRef(null) - useEffect(() => { - if (user) { - socket.connect() - - setUserNotifications(user?.notifications) - - console.log(`Connecting socket...`) - socket.emit('subscribeToNotifications', JSON.stringify({ id: user._id })) - - return () => { - socket.disconnect() - } - } - }, [isUserDataLoading]) - - useEffect(() => { - if (user) { - socket.on(`notification-${user._id}`, (notification) => { - // Find the index of the existing notification with the same _id - const existingIndex = userNotifications.findIndex( - (n) => String(n._id) === String(notification._id), - ) - - // If an existing notification is found, update it - if (existingIndex !== -1) { - const updatedNotifications = [...userNotifications] - - updatedNotifications[existingIndex] = notification - setUserNotifications(updatedNotifications) - } - // If not, add the new notification to the array - else { - setUserNotifications([...userNotifications, notification]) - } - }) - - return () => { - socket.off(`notification-${user._id}`) - } - } - }, [userNotifications]) - useOutsideClick(navMenuRef, () => setSidebar(false), notificationModal) const handleUseLogout = () => { @@ -148,7 +109,6 @@ const NavBar = () => { {isAuth && user && ( { - const unreadMessages = userNotifications.filter((item) => !item.read) + const { notifications: userNotifications } = useSelector((state) => state.userReducer) + + const unreadMessages = userNotifications?.filter((item) => !item.read) return ( @@ -28,22 +30,22 @@ const NotificationsContent = ({

Notifications

- {!!unreadMessages.length && !notificationModal && ( + {!!unreadMessages?.length && !notificationModal && ( <> - {unreadMessages.length} + {unreadMessages?.length} )} diff --git a/client/src/components/NavBar/NotificationsList/NotificationsList.js b/client/src/components/NavBar/NotificationsList/NotificationsList.js index 30e0b8886..59c41efc9 100644 --- a/client/src/components/NavBar/NotificationsList/NotificationsList.js +++ b/client/src/components/NavBar/NotificationsList/NotificationsList.js @@ -7,7 +7,9 @@ import { StyledNotificationsList } from './NotificationsList.styles' const NotificationsList = ({ userNotifications, setUnreadIds, closeNotificationsModal }) => { const listRef = useRef(null) - userNotifications.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + const mutableNotifications = [...userNotifications] + + mutableNotifications.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) useEffect(() => { const observer = new IntersectionObserver((entries, observer) => { @@ -34,8 +36,8 @@ const NotificationsList = ({ userNotifications, setUnreadIds, closeNotifications return ( - {userNotifications && - userNotifications.map((item) => ( + {mutableNotifications && + mutableNotifications.map((item) => ( { + console.log(data) + return { userRealName: data.fullName, userUsername: data.username, diff --git a/client/src/components/Profile/Profile.jsx b/client/src/components/Profile/Profile.jsx new file mode 100644 index 000000000..0650a1411 --- /dev/null +++ b/client/src/components/Profile/Profile.jsx @@ -0,0 +1,153 @@ +import { useEffect, useState } from 'react' +import { useParams } from 'react-router-dom' +import { Formik, useFormikContext } from 'formik' + +import { useCheckAuth } from '../../api/hooks/auth/useCheckAuth' +import { useUpdateAvatar } from '../../api/hooks/shared/useUpdateAvatar' +import { useEditUserDetails } from '../../api/hooks/user/useEditUserDetails' +import { useGetUserById } from '../../api/hooks/user/useGetUserById' +import PlatformLogo from '../../assets/Platform/TeameightsLogo' +import { useGetScreenWidth } from '../../hooks/useGetScreenWidth' +import { usePrompt } from '../../hooks/usePrompt' +import { editProfileValidation } from '../../schemas' +import Loader from '../../shared/components/Loader/Loader' +import ModalComponent from '../../shared/components/Modal/Modal' +import Page404Form from '../Forms/Page404Form/Page404Form' + +import ProfileInfo from './components/ProfileInfo/ProfileInfo' +import ResumeInfo from './components/ResumeInfo/ResumeInfo' +import { LogoWrapper, ProfileContainer, ProfileForm, ProfileWrapper } from './Profile.styles' + +const Profile = () => { + const { id } = useParams() + const { mutate: editUserDetails, isLoading } = useEditUserDetails(() => setIsEditing('')) + const { mutate: updateAvatar, isLoading: isUpdatingAvatar } = useUpdateAvatar('users', () => + setIsEditing(''), + ) + const { data, isLoading: isUserLoading, error } = useGetUserById(id) + const { data: currentUser, isFetching } = useCheckAuth() + const [isEditing, setIsEditing] = useState('') + const [showingUser, setShowingUser] = useState(null) + const width = useGetScreenWidth() + + useEffect(() => { + setShowingUser(data?.data) + }, [data]) + + // const showingUser = data?.data + + const handleSubmit = (values, { setFieldValue }) => { + const { + fullName, + description, + concentration, + country, + experience, + github, + telegram, + linkedIn, + programmingLanguages, + frameworks, + dateOfBirth, + projectData, + jobData, + universityData, + file, + } = values + const modifiedUserData = { + email: showingUser.email, + fullName, + description, + concentration, + country, + experience, + links: { + github, + telegram, + linkedIn, + }, + programmingLanguages, + frameworks, + dateOfBirth, + projectData, + jobData, + universityData, + } + + if (file) { + updateAvatar({ email: showingUser?.email, image: file.split(',')[1] }) + setFieldValue('file', null) + } else { + editUserDetails(modifiedUserData) + } + } + + if ((!isUserLoading && !data && !isFetching) || error) { + return + } + + /* If not, check if current showingUser has team and if team members have current id in array */ + + return ( + + {({ values, errors, dirty }) => { + usePrompt('You have unsaved changes. Do you want to discard them?', dirty) + + return ( + <> + + + + + + {!showingUser && 768 ? '88px' : '0'} />} + + + + + + + + ) + }} + + ) +} + +export default Profile diff --git a/client/src/components/Profile/Profile.styles.js b/client/src/components/Profile/Profile.styles.js index 32b0764be..4e8af754f 100644 --- a/client/src/components/Profile/Profile.styles.js +++ b/client/src/components/Profile/Profile.styles.js @@ -1,178 +1,75 @@ -import { Telegram } from '@mui/icons-material' +import { Form } from 'formik' import styled from 'styled-components' -import { BLACK, WHITE } from '../../constants/colors' - -export const Container = styled.div` - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - min-height: 100vh; +export const ProfileForm = styled(Form)` width: 100%; - background: ${BLACK.background}; -` - -export const LeftCard = styled.div` - min-height: 565px; - width: 370px; - background: #1a1c22; - box-shadow: 0px 4px 25px rgba(0, 0, 0, 0.25); - border-radius: 15px; + min-height: 100dvh; + padding-left: 88px; + background: #26292b; display: flex; flex-direction: column; justify-content: center; align-items: center; -` -export const TelegramIcon = styled(Telegram)` - color: #fff; - width: 1.25rem !important; - height: 1.25rem !important; + @media (max-width: 768px) { + padding-left: 0; + } ` -export const RightContainer = styled.div` - width: 470px; +export const ProfileWrapper = styled.div` display: flex; - border-radius: 15px; - justify-content: space-between; flex-direction: column; - gap: 10px; -` - -export const RightCard = styled.div` - width: 470px; - min-height: 125px; - background: #1a1c22; - box-shadow: 0px 4px 25px rgba(0, 0, 0, 0.25); - border-radius: 15px; - display: flex; - justify-content: start; - align-items: start; - flex-direction: column; - padding: 10px 10px; -` - -export const TextContainer = styled.div` - display: flex; justify-content: center; - flex-direction: column; - align-items: justify; - margin-bottom: 10px; -` - -export const Text = styled.h3` - font-weight: ${(props) => props.fontWeight || '500'}; - font-size: ${(props) => props.fontSize || '18px'}; - margin: ${(props) => props.margin || '0'}; - color: ${(props) => props.color || WHITE.main}; - text-align: ${(props) => props.alignment || 'center'}; -` - -export const ImgContainer = styled.div` - position: relative; - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; -` - -export const Img = styled.img` - width: 150px; - height: 150px; - border-radius: 50%; - margin-top: 30px; -` - -export const ProfileLine = styled.hr` - border: 1px solid rgba(42, 60, 19, 0.5); - width: 90%; -` - -export const BannerLine = styled.hr` - width: 450px; - height: 0px; - border: 1px solid rgba(42, 60, 19, 0.5); -` - -export const InformationRow = styled.div` - width: 90%; - display: flex; - justify-content: space-between; align-items: center; - margin-top: 15px; + position: relative; + width: 100%; ` -export const SocialRow = styled.div` +export const ProfileContainer = styled.div` display: flex; + /* max-width: 800px; */ + gap: 30px; + margin: 0 auto; + padding: 0 25px; + width: 100%; + justify-content: center; align-items: center; - margin-bottom: 10px; -` -export const SocialWrapper = styled.div` - margin-top: 30px; - width: 90%; -` + @media (max-width: 1024px) { + flex-direction: column; + align-items: center; + margin: 114px 0 24px 0; + } -export const IconTextContainer = styled.div` - display: flex; - gap: 15px; - align-items: center; + @media (max-width: 768px) { + margin: 96px 0 24px 0; + } ` -export const Cards = styled.div` +export const ProfileSection = styled.div` display: flex; flex-direction: column; - justify-content: center; - align-items: center; - min-height: 100vh; -` - -export const Information = styled.div` - display: flex; - flex-direction: row; - gap: 50px; - justify-content: center; - width: 100%; -` - -export const RightCardData = styled.div` - display: flex; - width: 100%; - gap: 10px; - flex-wrap: wrap; - min-height: 50px; - height: 100%; - justify-content: ${(props) => props.justify || 'start'}; - align-items: center; -` + align-items: ${(props) => props.align || 'start'}; + gap: ${(props) => props.gap || '0'}; + width: ${(props) => props.width || 'auto'}; + height: 600px; + background: #1a1c22; + border-radius: 15px; + padding: ${(props) => props.padding || '0'}; -export const ProgrammingLanguage = styled.div` - width: 40px; - height: 40px; - background: #2e3239; - border-radius: 5px; - display: flex; - justify-content: center; - align-items: center; + @media (max-width: 1024px) { + max-width: 470px; + width: 100%; + min-height: 600px; + height: 100%; + } ` -export const Framework = styled.div` - width: 65px; - height: 35px; - background: #00a4d3; - border-radius: 5px; - display: flex; - justify-content: center; - align-items: center; - background: ${(props) => props.background || '#42443B'}; -` +export const LogoWrapper = styled.div` + position: absolute; + top: 48px; -export const DetailsWrapper = styled.div` - display: flex; - flex-direction: column; - width: 90%; -` -export const InformationWrapper = styled.div` - display: flex; - flex-direction: column; + @media (max-width: 768px) { + top: 32px; + } ` diff --git a/client/src/components/Profile/components/ProfileDetails/ProfileDetails.jsx b/client/src/components/Profile/components/ProfileDetails/ProfileDetails.jsx deleted file mode 100644 index 0f3403ddf..000000000 --- a/client/src/components/Profile/components/ProfileDetails/ProfileDetails.jsx +++ /dev/null @@ -1,185 +0,0 @@ -import React from 'react' -import { useNavigate } from 'react-router-dom' - -import { useCheckAuth } from '../../../../api/hooks/auth/useCheckAuth' -import { useGetTeamData } from '../../../../api/hooks/team/useGetTeamData' -import Email from '../../../../assets/UserProfile/Email' -import Github from '../../../../assets/UserProfile/Github' -import Linkedin from '../../../../assets/UserProfile/Linkedin' -import Location from '../../../../assets/UserProfile/Location' -import Star from '../../../../assets/UserProfile/Star' -import { Framework } from '../../../../components/Teammates/components/UserCard/UserCard.styles' -import { frameworkColors, frameworkTextColors } from '../../../../constants/frameworkColors' -import { languageOptions } from '../../../../constants/programmingLanguages' -import ROUTES from '../../../../constants/routes' -import { LOCAL_PATH } from '../../../../http' -import AvatarEditButton from '../../../../shared/components/Forms/UserAvatar/AvatarEditButton/AvatarEditButton' -import { UserAvatar } from '../../../../shared/components/Forms/UserAvatar/UserAvatar.styles' -import Loader from '../../../../shared/components/Loader/Loader' -import ModalWindow from '../../../../shared/components/ModalWindow/ModalWindow' -import { Button } from '../../../../shared/styles/Button.styles' -import { CustomLink } from '../../../../shared/styles/Link.styles' -import { AvatarWrapper } from '../../../RegistrationPipeline/components/RegistrationForms/AvatarForm/AvatarForm.styles' -import { - BannerLine, - DetailsWrapper, - IconTextContainer, - Img, - ImgContainer, - Information, - InformationRow, - InformationWrapper, - LeftCard, - ProfileLine, - ProgrammingLanguage, - RightCard, - RightCardData, - RightContainer, - SocialRow, - SocialWrapper, - TelegramIcon, - Text, - TextContainer, -} from '../../Profile.styles' - -const ProfileDetails = () => { - const { data: user, isFetching: isUserDataLoading } = useCheckAuth() - - const navigate = useNavigate() - - if (isUserDataLoading) { - return - } - - if (!user?.isRegistered) { - return - } - - return ( - - - - - navigate('/profile-edit')} /> - - - {user.fullName} - - {user.username} - - {user.concentration} - - - - - - - - {user.country} - - - - - - {user.experience} years of experiences - - - - - - {user.email} - - - - - {user.links?.github && ( - - - - - Github - - - - )} - {user.links?.linkedIn && ( - - - - - Linkedin - - - - )} - {user.links?.telegram && ( - - - - - Telegram - - - - )} - - - - - - - Languages - - - - {user.programmingLanguages.map((language) => ( - {languageOptions[language]} - ))} - - - - - Tools - - - - {user.frameworks.slice(0, 5).map((framework, index) => ( - -

{index < 4 ? framework : `+${user.frameworks.length - 4}`}

-
- ))} -
-
- - - Team - - - - - {user.team ? user?.team.name : "That's where your team will come in"} - - - - - - About me - - - - - {user.description ? user.description : 'This user is humble'} - - - -
-
- ) -} - -export default ProfileDetails diff --git a/client/src/components/Profile/components/ProfileForm/ProfileForm.styles.js b/client/src/components/Profile/components/ProfileForm/ProfileForm.styles.js deleted file mode 100644 index be6dd678f..000000000 --- a/client/src/components/Profile/components/ProfileForm/ProfileForm.styles.js +++ /dev/null @@ -1,21 +0,0 @@ -import styled from 'styled-components' - -export const EditUserDetails = styled.div` - display: flex; - justify-content: center; - align-items: center; - position: absolute; - bottom: 0.5rem; - right: 1rem; - border-radius: 50%; - width: 1.875rem; - height: 1.875rem; - background: #5d9d0b; - cursor: pointer; -` - -export const ConcentrationWrapper = styled.div` - display: flex; - align-items: center; - justify-content: space-between; -` diff --git a/client/src/components/Profile/components/ProfileForm/components/Edit/Edit.jsx b/client/src/components/Profile/components/ProfileForm/components/Edit/Edit.jsx deleted file mode 100644 index 373e7aa5c..000000000 --- a/client/src/components/Profile/components/ProfileForm/components/Edit/Edit.jsx +++ /dev/null @@ -1,13 +0,0 @@ -import PlusIcon from '../../../../../../assets/UserProfile/PlusIcon' - -import { EditButton } from './Edit.styles' - -const Edit = ({ className }) => { - return ( - - - - ) -} - -export default Edit diff --git a/client/src/components/Profile/components/ProfileForm/components/Edit/Edit.styles.js b/client/src/components/Profile/components/ProfileForm/components/Edit/Edit.styles.js deleted file mode 100644 index 91a4b6e82..000000000 --- a/client/src/components/Profile/components/ProfileForm/components/Edit/Edit.styles.js +++ /dev/null @@ -1,7 +0,0 @@ -import styled from 'styled-components' - -export const EditButton = styled.div` - cursor: pointer; - width: 1.125rem; - height: 1.125rem; -` diff --git a/client/src/components/Profile/components/ProfileForm/index.jsx b/client/src/components/Profile/components/ProfileForm/index.jsx deleted file mode 100644 index deeb9f783..000000000 --- a/client/src/components/Profile/components/ProfileForm/index.jsx +++ /dev/null @@ -1,354 +0,0 @@ -import { useMemo } from 'react' -import { useNavigate } from 'react-router-dom' -import countryList from 'react-select-country-list' -import { Form, Formik } from 'formik' - -import { useCheckAuth } from '../../../../api/hooks/auth/useCheckAuth' -import { useEditUserDetails } from '../../../../api/hooks/auth/useEditUserDetails' -import { useGetTeamData } from '../../../../api/hooks/team/useGetTeamData' -import EditIcon from '../../../../assets/Shared/EditIcon' -import Email from '../../../../assets/UserProfile/Email' -import Github from '../../../../assets/UserProfile/Github' -import Linkedin from '../../../../assets/UserProfile/Linkedin' -import Location from '../../../../assets/UserProfile/Location' -import Star from '../../../../assets/UserProfile/Star' -import { Framework } from '../../../../components/Teammates/components/UserCard/UserCard.styles' -import concentrationOptions from '../../../../constants/concentrations' -import { userExperienceOptions } from '../../../../constants/finishRegistrationData' -import { frameworkColors, frameworkTextColors } from '../../../../constants/frameworkColors' -import frameworkOptions from '../../../../constants/frameworks' -import { - languageOptions, - programmingLanguageOptions, -} from '../../../../constants/programmingLanguages' -import ROUTES from '../../../../constants/routes' -import { usePrompt } from '../../../../hooks/usePrompt' -import { LOCAL_PATH } from '../../../../http' -import { editProfileValidation } from '../../../../schemas' -import CustomButton from '../../../../shared/components/CustomButton/CustomButton' -import CustomInput from '../../../../shared/components/Formik/CustomInput/CustomInput' -import CustomSelect from '../../../../shared/components/Formik/CustomSelect/CustomSelect' -import CustomTextArea from '../../../../shared/components/Formik/CustomTextArea/CustomTextArea' -import Loader from '../../../../shared/components/Loader/Loader' -import { Button } from '../../../../shared/styles/Button.styles' -import { ErrorMessage } from '../../../../shared/styles/Tpography.styles' -import { - BannerLine, - DetailsWrapper, - IconTextContainer, - Img, - ImgContainer, - Information, - InformationRow, - InformationWrapper, - LeftCard, - ProfileLine, - ProgrammingLanguage, - RightCard, - RightCardData, - RightContainer, - SocialRow, - SocialWrapper, - TelegramIcon, - Text, - TextContainer, -} from '../../Profile.styles' - -import Edit from './components/Edit/Edit' -import { ConcentrationWrapper, EditUserDetails } from './ProfileForm.styles' - -const inputStyles = { - border: '1px solid #5E5E5E', - borderRadius: '0.5rem', - padding: '0.5rem', - width: 'auto', -} - -const ProfileForm = () => { - const navigate = useNavigate() - const { mutate: editUserDetails, isLoading } = useEditUserDetails(onSuccess) - const { data: user, isFetching: isUserDataLoading } = useCheckAuth() - const teamId = user?.team?._id - - const { data: team, isLoading: isUserTeamLoading } = useGetTeamData(teamId) - const countriesOptions = useMemo(() => countryList().getData(), []) - - const stopEditing = () => navigate('/profile') - - function onSuccess() { - stopEditing() - } - - const teamSearchHandler = () => { - navigate('/teams') - } - - const handleSubmit = (values) => { - const { - fullName, - description, - concentration, - country, - experience, - github, - telegram, - linkedIn, - programmingLanguages, - frameworks, - } = values - const modifiedUserData = { - email: user.email, - fullName, - description, - concentration, - country, - experience, - links: { - github, - telegram, - linkedIn, - }, - programmingLanguages, - frameworks, - } - - editUserDetails(modifiedUserData) - } - - if (isLoading || isUserDataLoading || isUserTeamLoading) { - return - } - - if (!user) { - return - } - - return ( - - {({ values, errors, dirty }) => { - usePrompt('You have unsaved changes. Do you want to discard them?', dirty) - - return ( -
- - - - - - - - - - - - {user.userUsername} - - - {values.concentration} - <>} - width="auto" - margin="0" - name="concentration" - options={concentrationOptions} - line={false} - IconComponent={Edit} - /> - - - - - - - - - {values.country} - - <>} - width="auto" - margin="0 0 0 1rem" - name="country" - options={countriesOptions} - line={false} - IconComponent={Edit} - /> - - - - - - {values.experience} {!values.experience.includes('years') ? ' years' : ''}{' '} - of experiences - - - <>} - width="auto" - margin="0 0 0 1rem" - name="experience" - options={userExperienceOptions} - line={false} - IconComponent={Edit} - /> - - - - - {user.email} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Languages - - - - {values.programmingLanguages.map((language) => ( - - {languageOptions[language]} - - ))} - <>} - width="auto" - name="programmingLanguages" - margin="0" - multiple={true} - options={programmingLanguageOptions} - line={false} - IconComponent={Edit} - /> - {errors.programmingLanguages && ( - - {errors.programmingLanguages} - - )} - - - - - Tools - - - - {values.frameworks.slice(0, 5).map((framework, index) => ( - -

{index < 4 ? framework : `+${values.frameworks.length - 4}`}

-
- ))} - <>} - width="auto" - name="frameworks" - margin="0" - multiple={true} - options={frameworkOptions} - line={false} - IconComponent={Edit} - /> - {errors.frameworks && ( - {errors.frameworks} - )} -
-
- - - - Team - - - - {team ? ( - - {team.name} - - ) : ( - - Search team - - )} - - - - - - About me - - - - - - -
-
- - Save - -
- ) - }} -
- ) -} - -export default ProfileForm diff --git a/client/src/components/Profile/components/ProfileInfo/ProfileInfo.jsx b/client/src/components/Profile/components/ProfileInfo/ProfileInfo.jsx new file mode 100644 index 000000000..b3cca2a41 --- /dev/null +++ b/client/src/components/Profile/components/ProfileInfo/ProfileInfo.jsx @@ -0,0 +1,143 @@ +import { useNavigate } from 'react-router-dom' +import { useFormikContext } from 'formik' + +import { useInviteUser } from '../../../../api/hooks/team/useInviteUser' +import BehanceIcon from '../../../../assets/Links/BehanceIcon' +import GitHubIcon from '../../../../assets/Links/GitHubIcon' +import LinkedInIcon from '../../../../assets/Links/LinkedInIcon' +import TelegramIcon from '../../../../assets/Links/TelegramIcon' +import CakeIcon from '../../../../assets/UserProfile/Cake' +import EditIcon from '../../../../assets/UserProfile/EditIcon' +import EmailIcon from '../../../../assets/UserProfile/EmailIcon' +import LocationIcon from '../../../../assets/UserProfile/LocationIcon' +import StarIcon from '../../../../assets/UserProfile/StarIcon' +import UserIcon from '../../../../assets/UserProfile/UserIcon' +import { useGetScreenWidth } from '../../../../hooks/useGetScreenWidth' +import FlexWrapper from '../../../../shared/components/FlexWrapper/FlexWrapper' +import { calculateAge } from '../../../../utils/calculateAge' +import { checkUserStatus } from '../../../../utils/checkUserStatus' +import { truncateString } from '../../../../utils/truncateString' +import { ProfileSection } from '../../Profile.styles' + +import UserStatusButtons from './UserStatusButtons/UserStatusButtons' +import { + AvatarImg, + AvatarWrapper, + EditButton, + InfoList, + InfoListItem, + SocialList, + Text, + UserInfo, +} from './ProfileInfo.styles' + +const ProfileInfo = ({ showingUser, id, currentUser, isEditing, setIsEditing, isUpdatingUser }) => { + const width = useGetScreenWidth() + const navigate = useNavigate() + const { mutate: inviteUser, isLoading: isInviting } = useInviteUser() + + const infoListArr = [ + { icon: , infoEl: truncateString(showingUser?.concentration, width) }, + { + icon: , + infoEl: truncateString(`${showingUser?.experience} years of experience`, width), + }, + { icon: , infoEl: truncateString(showingUser?.country, width) }, + { + icon: , + infoEl: truncateString(`${calculateAge(showingUser?.dateOfBirth)} years old`, width), + }, + { + icon: , + infoEl: ( + + {truncateString(showingUser?.email, width)} + + ), + }, + ] + + const socialArr = [ + showingUser?.links?.github && { icon: , link: showingUser?.links?.github }, + showingUser?.links?.behance && { icon: , link: showingUser?.links?.behance }, + showingUser?.links?.telegram && { icon: , link: showingUser?.links?.telegram }, + showingUser?.links?.linkedIn && { icon: , link: showingUser?.links?.linkedIn }, + ].filter(Boolean) + + const handleInvite = () => { + const details = { + email: showingUser.email, + teamid: currentUser?.team?._id, + from_user_id: currentUser?._id, + } + + inviteUser(details) + } + + const { values, resetForm } = useFormikContext() + const userStatus = checkUserStatus(currentUser, id) + + const handleEdit = (target) => { + if (!isEditing) { + setIsEditing(target) + } else { + resetForm() + setIsEditing('') + } + } + + return ( + + + + + {userStatus === 'same' && ( + handleEdit('avatar')}> + + + )} + + + {showingUser?.fullName} + + @{showingUser?.username} + + + + + + {infoListArr.map((item, index) => ( + + {item.icon} + + {item.infoEl} + + + ))} + + {socialArr.length > 0 && ( + + {socialArr.map((social, index) => ( +
  • + + {social.icon} + +
  • + ))} +
    + )} +
    + ) +} + +export default ProfileInfo diff --git a/client/src/components/Profile/components/ProfileInfo/ProfileInfo.styles.js b/client/src/components/Profile/components/ProfileInfo/ProfileInfo.styles.js new file mode 100644 index 000000000..ffebad93d --- /dev/null +++ b/client/src/components/Profile/components/ProfileInfo/ProfileInfo.styles.js @@ -0,0 +1,109 @@ +import styled from 'styled-components' + +export const UserInfo = styled.div` + display: flex; + width: 100%; + flex-direction: column; + align-items: center; + gap: 24px; +` + +export const AvatarWrapper = styled.div` + position: relative; +` + +export const AvatarImg = styled.img` + display: block; + border-radius: 50%; + width: 100px; + height: 100px; + user-select: none; +` + +export const Text = styled.p` + font-weight: ${(props) => props.fontWeight || '500'}; + font-size: ${(props) => props.fontSize || '20px'}; + color: ${(props) => props.color || '#fff'}; ; +` + +export const GenericButton = styled.button` + height: 40px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + width: 100%; + max-width: 222px; + background: ${(props) => props.background || 'transparent'}; + padding: 10px 0; + border: ${(props) => props.border || '2px solid #46a11b'}; + border-radius: 10px; + transition: background-color 0.3s; + color: #fff; + font-weight: 400; + font-size: 16px; + &:hover { + background-color: rgba(250, 250, 250, 0.05); + } + @media (max-width: 1024px) { + max-width: none; + } +` + +export const InfoList = styled.ul` + display: flex; + flex-direction: column; + gap: 9px; + @media (max-width: 1024px) { + align-self: flex-start; + } + + a { + text-decoration: none; + color: white; + font-size: 16px; + font-weight: 400; + + :hover { + color: #5bd424; + } + } +` + +export const InfoListItem = styled.li` + display: flex; + align-items: center; + gap: 8px; + width: 100%; + flex-wrap: wrap; +` + +export const SocialList = styled.ul` + margin-top: auto; + display: flex; + justify-content: center; + gap: 24px; + flex-wrap: wrap; + svg { + transition: opacity 0.3s; + :hover { + opacity: 0.6; + } + } +` + +export const EditButton = styled.div` + width: 28px; + height: 28px; + position: absolute; + background: #46a11b; + right: 0px; + bottom: 0px; + display: flex; + /* padding: 4px; */ + justify-content: center; + align-items: center; + border-radius: 100px; + cursor: pointer; +` diff --git a/client/src/components/Profile/components/ProfileInfo/UserStatusButtons/UserStatusButtons.jsx b/client/src/components/Profile/components/ProfileInfo/UserStatusButtons/UserStatusButtons.jsx new file mode 100644 index 000000000..9f358dbd0 --- /dev/null +++ b/client/src/components/Profile/components/ProfileInfo/UserStatusButtons/UserStatusButtons.jsx @@ -0,0 +1,120 @@ +import React from 'react' +import { ThreeDots } from 'react-loader-spinner' +import { useFormikContext } from 'formik' + +import LongArrowLeft from '../../../../../assets/Arrows/LongArrowLeft' +import AddUserIcon from '../../../../../assets/Shared/AddUserIcon' +import MessageIcon from '../../../../../assets/UserProfile/MessageIcon' +import FlexWrapper from '../../../../../shared/components/FlexWrapper/FlexWrapper' +import { infoToaster } from '../../../../../shared/components/Toasters/Info.toaster' +import { GenericButton } from '../ProfileInfo.styles' + +const UserStatusButtons = ({ + currentUser, + id, + isEditing, + handleEdit, + navigate, + handleInvite, + isInviting, + userStatus, + isUpdatingUser, +}) => { + const renderSameUserButtons = () => { + if (isEditing && isEditing === 'profile') { + return ( + + + {isUpdatingUser ? ( + + ) : ( + 'Save changes' + )} + + handleEdit('')} + > + Cancel + + + ) + } + + return ( + handleEdit('profile')}> + Edit section + + ) + } + + const renderTeamMemberButtons = () => ( + + navigate(-1)}> + + Back + + infoToaster('Coming in the next update!')} + background="#46A11B" + > + Message + + + + ) + + const renderOtherUserButtons = () => ( + + {currentUser?.team && ( + + {isInviting ? ( + + ) : ( + <> + Invite + + + )} + + )} + infoToaster('Coming in the next update!')}> + Message + + + + ) + + return ( + <> + {userStatus === 'same' && renderSameUserButtons()} + + {userStatus === 'teammember' && renderTeamMemberButtons()} + + {userStatus === 'other' && renderOtherUserButtons()} + + ) +} + +export default UserStatusButtons diff --git a/client/src/components/Profile/components/ResumeInfo/EditingComponents/EditingComponentAvatar.jsx b/client/src/components/Profile/components/ResumeInfo/EditingComponents/EditingComponentAvatar.jsx new file mode 100644 index 000000000..f6952c52f --- /dev/null +++ b/client/src/components/Profile/components/ResumeInfo/EditingComponents/EditingComponentAvatar.jsx @@ -0,0 +1,39 @@ +import { ThreeDots } from 'react-loader-spinner' + +import ChooseAvatar from '../../../../../shared/components/ChooseAvatar/ChooseAvatar' +import FlexWrapper from '../../../../../shared/components/FlexWrapper/FlexWrapper' +import { ActionButton, Text } from '../ResumeInfo.styles' + +function EditingComponentAvatar({ handleCancel, isUpdatingAvatar }) { + return ( + + + Avatar + + + + + Cancel + + + {isUpdatingAvatar ? ( + + ) : ( + 'Save' + )} + + + + ) +} + +export default EditingComponentAvatar diff --git a/client/src/components/Profile/components/ResumeInfo/EditingComponents/EditingComponentDefault.jsx b/client/src/components/Profile/components/ResumeInfo/EditingComponents/EditingComponentDefault.jsx new file mode 100644 index 000000000..f440711f4 --- /dev/null +++ b/client/src/components/Profile/components/ResumeInfo/EditingComponents/EditingComponentDefault.jsx @@ -0,0 +1,35 @@ +import FlexWrapper from '../../../../../shared/components/FlexWrapper/FlexWrapper' +import EducationWork from '../EducationWork/EducationWork' +import ProjectsSkills from '../ProjectsSkills/ProjectsSkills' +import { ResumePartBox, ResumePartBtn } from '../ResumeInfo.styles' + +function EditingComponentDefault({ active, setActive, showingUser, setIsEditing, userStatus }) { + return ( + + + setActive('projects')}> + Projects & Skills + + setActive('education')}> + Education & Work + + + {active === 'projects' && ( + + )} + {active === 'education' && ( + + )} + + ) +} + +export default EditingComponentDefault diff --git a/client/src/components/Profile/components/ResumeInfo/EditingComponents/EditingComponentDescription.jsx b/client/src/components/Profile/components/ResumeInfo/EditingComponents/EditingComponentDescription.jsx new file mode 100644 index 000000000..e3b005644 --- /dev/null +++ b/client/src/components/Profile/components/ResumeInfo/EditingComponents/EditingComponentDescription.jsx @@ -0,0 +1,49 @@ +import { ThreeDots } from 'react-loader-spinner' +import { useFormikContext } from 'formik' + +import FlexWrapper from '../../../../../shared/components/FlexWrapper/FlexWrapper' +import CustomTextArea from '../../../../../shared/components/Formik/CustomTextArea/CustomTextArea' +import { ActionButton, Text, TextArea } from '../ResumeInfo.styles' + +function EditingComponentDescription({ handleCancel, isUpdatingUser }) { + const { values } = useFormikContext() + + return ( + + + About me + + + + + Cancel + + + {isUpdatingUser ? ( + + ) : ( + 'Save' + )} + + + + ) +} + +export default EditingComponentDescription diff --git a/client/src/components/Profile/components/ResumeInfo/EditingComponents/EditingComponentEducation.jsx b/client/src/components/Profile/components/ResumeInfo/EditingComponents/EditingComponentEducation.jsx new file mode 100644 index 000000000..78b61e3e3 --- /dev/null +++ b/client/src/components/Profile/components/ResumeInfo/EditingComponents/EditingComponentEducation.jsx @@ -0,0 +1,299 @@ +import { useState } from 'react' +import { ThreeDots } from 'react-loader-spinner' +import { FieldArray, useFormikContext } from 'formik' + +import { useEditUserDetails } from '../../../../../api/hooks/user/useEditUserDetails' +import LongArrowLeft from '../../../../../assets/Arrows/LongArrowLeft' +import CrossIcon from '../../../../../assets/UserProfile/CrossIcon' +import EditIcon from '../../../../../assets/UserProfile/EditIcon' +import PlusIconWhite from '../../../../../assets/UserProfile/PlusIconWhite' +import FlexWrapper from '../../../../../shared/components/FlexWrapper/FlexWrapper' +import CustomInput from '../../../../../shared/components/Formik/CustomInput/CustomInput' +import ModalComponent from '../../../../../shared/components/Modal/Modal' +import { ActionButton, EditIconContainer, Text } from '../ResumeInfo.styles' + +function EditingComponentEducation({ handleBack, showingUser }) { + const [modalActive, setModalActive] = useState('') + const { values, setFieldValue, resetForm } = useFormikContext() + const [currentAction, setCurrentAction] = useState('main') // 'main', 'editing', 'adding' + const [index, setIndex] = useState(0) + + const handleReset = () => { + setModalActive('') + setCurrentAction('main') + } + + const { mutate: editUserDetails, isLoading } = useEditUserDetails(handleReset) + + const handleRemoveUniversity = () => { + const universities = [...values.universityData] + + const updatedUniversityData = universities.filter((_, i) => i !== index) + + setFieldValue('universityData', updatedUniversityData) + + editUserDetails({ email: showingUser.email, universityData: updatedUniversityData }) + } + + const handleEditUniversity = (index) => { + setIndex(index) + setCurrentAction('editing') + } + + const submitEditUniversity = () => { + editUserDetails({ email: showingUser.email, universityData: values.universityData }) + } + + const handleOpenModal = (index) => { + setModalActive('DeleteUniversity') + setIndex(index) + } + + const handleClose = () => { + setModalActive('') + } + + const handleCancel = () => { + setCurrentAction('main') + resetForm() + } + + return ( + <> + + + + Education + + {currentAction === 'main' && ( + + {({ push, remove }) => ( + <> + {values?.universityData?.length > 0 ? ( + values?.universityData.map((university, index) => ( + + + + {university.degree} - {university.major} + + + {university.university} + + + {university?.addmissionDate?.split('-')[0]} -{' '} + {university?.graduationDate?.split('-')[0]} + + + + handleEditUniversity(index)} + > + + + handleOpenModal(index)} + > + + + + + )) + ) : ( + + No universities added yet. + + )} + + + + Back + + { + push({ + university: '', + degree: ``, + major: '', + addmissionDate: '', + graduationDate: '', + }) + setCurrentAction('adding') + }} + type="button" + disabled={values.universityData.length === 2 ? true : false} + > + Add new + + + + + )} + + )} + + {currentAction === 'editing' && ( + <> + + + + + + + + + + + Cancel + + + {isLoading ? ( + + ) : ( + 'Save' + )} + + + + + )} + + {currentAction === 'adding' && ( + <> + + + + + + + + + + + Cancel + + + {isLoading ? ( + + ) : ( + 'Save' + )} + + + + + )} + + + ) +} + +export default EditingComponentEducation diff --git a/client/src/components/Profile/components/ResumeInfo/EditingComponents/EditingComponentFrameworks.jsx b/client/src/components/Profile/components/ResumeInfo/EditingComponents/EditingComponentFrameworks.jsx new file mode 100644 index 000000000..200bd0dcf --- /dev/null +++ b/client/src/components/Profile/components/ResumeInfo/EditingComponents/EditingComponentFrameworks.jsx @@ -0,0 +1,49 @@ +import { ThreeDots } from 'react-loader-spinner' +import { useFormikContext } from 'formik' + +import frameworkOptions from '../../../../../constants/frameworks' +import FlexWrapper from '../../../../../shared/components/FlexWrapper/FlexWrapper' +import CustomSelectAutocomplete from '../../../../../shared/components/Formik/CustomSelectAutocomplete/CustomSelectAutocomplete' +import { ActionButton, Text } from '../ResumeInfo.styles' + +function EditingComponentFrameworks({ handleCancel, isUpdatingUser }) { + const { values } = useFormikContext() + + return ( + + + Frameworks + + + + + Cancel + + + {isUpdatingUser ? ( + + ) : ( + 'Save' + )} + + + + ) +} + +export default EditingComponentFrameworks diff --git a/client/src/components/Profile/components/ResumeInfo/EditingComponents/EditingComponentJob.jsx b/client/src/components/Profile/components/ResumeInfo/EditingComponents/EditingComponentJob.jsx new file mode 100644 index 000000000..65d5c2c5a --- /dev/null +++ b/client/src/components/Profile/components/ResumeInfo/EditingComponents/EditingComponentJob.jsx @@ -0,0 +1,279 @@ +import { useState } from 'react' +import { ThreeDots } from 'react-loader-spinner' +import { FieldArray, useFormikContext } from 'formik' + +import { useEditUserDetails } from '../../../../../api/hooks/user/useEditUserDetails' +import LongArrowLeft from '../../../../../assets/Arrows/LongArrowLeft' +import CrossIcon from '../../../../../assets/UserProfile/CrossIcon' +import EditIcon from '../../../../../assets/UserProfile/EditIcon' +import PlusIconWhite from '../../../../../assets/UserProfile/PlusIconWhite' +import FlexWrapper from '../../../../../shared/components/FlexWrapper/FlexWrapper' +import CustomInput from '../../../../../shared/components/Formik/CustomInput/CustomInput' +import ModalComponent from '../../../../../shared/components/Modal/Modal' +import { ActionButton, EditIconContainer, Text } from '../ResumeInfo.styles' + +function EditingComponentJob({ handleBack, showingUser }) { + const [modalActive, setModalActive] = useState('') + const { values, setFieldValue, resetForm } = useFormikContext() + const [currentAction, setCurrentAction] = useState('main') // 'main', 'editing', 'adding' + const [index, setIndex] = useState(0) + + const handleReset = () => { + setModalActive('') + setCurrentAction('main') + } + + const { mutate: editUserDetails, isLoading } = useEditUserDetails(handleReset) + + const handleRemoveUniversity = () => { + const jobs = [...values.jobData] + + const updatedJobData = jobs.filter((_, i) => i !== index) + + setFieldValue('jobData', updatedJobData) + + editUserDetails({ email: showingUser.email, jobData: updatedJobData }) + } + + const handleEditJob = (index) => { + setIndex(index) + setCurrentAction('editing') + } + + const submitEditJob = () => { + editUserDetails({ email: showingUser.email, jobData: values.jobData }) + } + + const handleOpenModal = (index) => { + setModalActive('DeleteJob') + setIndex(index) + } + + const handleClose = () => { + setModalActive('') + } + + const handleCancel = () => { + setCurrentAction('main') + resetForm() + } + + return ( + <> + + + + Work Experience + + {currentAction === 'main' && ( + + {({ push, remove }) => ( + <> + {values?.jobData?.length > 0 ? ( + values?.jobData.map((job, index) => ( + + + + {job.title} - {job.company} + + + {job.startDate.split('-')[0]} - {job.endDate.split('-')[0]} + + + + handleEditJob(index)} + > + + + handleOpenModal(index)} + > + + + + + )) + ) : ( + + No jobs added yet. + + )} + + + + Back + + { + push({ + title: '', + company: ``, + startDate: '', + endDate: '', + }) + setCurrentAction('adding') + }} + type="button" + disabled={values.jobData.length === 2 ? true : false} + > + Add new + + + + + )} + + )} + + {currentAction === 'editing' && ( + <> + + + + + + + + + + Cancel + + + {isLoading ? ( + + ) : ( + 'Save' + )} + + + + + )} + + {currentAction === 'adding' && ( + <> + + + + + + + + + + Cancel + + + {isLoading ? ( + + ) : ( + 'Save' + )} + + + + + )} + + + ) +} + +export default EditingComponentJob diff --git a/client/src/components/Profile/components/ResumeInfo/EditingComponents/EditingComponentLanguages.jsx b/client/src/components/Profile/components/ResumeInfo/EditingComponents/EditingComponentLanguages.jsx new file mode 100644 index 000000000..881e6bcac --- /dev/null +++ b/client/src/components/Profile/components/ResumeInfo/EditingComponents/EditingComponentLanguages.jsx @@ -0,0 +1,49 @@ +import { ThreeDots } from 'react-loader-spinner' +import { useFormikContext } from 'formik' + +import { programmingLanguageOptions } from '../../../../../constants/programmingLanguages' +import FlexWrapper from '../../../../../shared/components/FlexWrapper/FlexWrapper' +import CustomSelectAutocomplete from '../../../../../shared/components/Formik/CustomSelectAutocomplete/CustomSelectAutocomplete' +import { ActionButton, Text } from '../ResumeInfo.styles' + +function EditingComponentLanguages({ handleCancel, isUpdatingUser }) { + const { values } = useFormikContext() + + return ( + + + Languages + + + + + Cancel + + + {isUpdatingUser ? ( + + ) : ( + 'Save' + )} + + + + ) +} + +export default EditingComponentLanguages diff --git a/client/src/components/Profile/components/ResumeInfo/EditingComponents/EditingComponentProfile.jsx b/client/src/components/Profile/components/ResumeInfo/EditingComponents/EditingComponentProfile.jsx new file mode 100644 index 000000000..80d3c85f9 --- /dev/null +++ b/client/src/components/Profile/components/ResumeInfo/EditingComponents/EditingComponentProfile.jsx @@ -0,0 +1,81 @@ +import { useState } from 'react' +import { useFormikContext } from 'formik' + +import concentrationOptions from '../../../../../constants/concentrations' +import { countries } from '../../../../../constants/countries' +import { userExperienceOptions } from '../../../../../constants/finishRegistrationData' +import FlexWrapper from '../../../../../shared/components/FlexWrapper/FlexWrapper' +import CustomInput from '../../../../../shared/components/Formik/CustomInput/CustomInput' +import CustomSelectAutocomplete from '../../../../../shared/components/Formik/CustomSelectAutocomplete/CustomSelectAutocomplete' +import Links from '../../../../../shared/components/Links/Links' +import { ResumePartBox, ResumePartBtn } from '../ResumeInfo.styles' + +function EditingComponentProfile() { + const [isActive, setIsActive] = useState('general') + const { values } = useFormikContext() + + return ( + + + setIsActive('general')}> + General + + setIsActive('concentration')} + > + Concentration + + setIsActive('links')}> + Links + + + {isActive === 'general' && ( + <> + + + + + )} + {isActive === 'concentration' && ( + <> + + + + )} + {isActive === 'links' && } + + ) +} + +export default EditingComponentProfile diff --git a/client/src/components/Profile/components/ResumeInfo/EditingComponents/EditingComponentProjects.jsx b/client/src/components/Profile/components/ResumeInfo/EditingComponents/EditingComponentProjects.jsx new file mode 100644 index 000000000..22806e5f9 --- /dev/null +++ b/client/src/components/Profile/components/ResumeInfo/EditingComponents/EditingComponentProjects.jsx @@ -0,0 +1,240 @@ +import { useState } from 'react' +import { ThreeDots } from 'react-loader-spinner' +import { FieldArray, useFormikContext } from 'formik' + +import { useEditUserDetails } from '../../../../../api/hooks/user/useEditUserDetails' +import LongArrowLeft from '../../../../../assets/Arrows/LongArrowLeft' +import CrossIcon from '../../../../../assets/UserProfile/CrossIcon' +import EditIcon from '../../../../../assets/UserProfile/EditIcon' +import PlusIconWhite from '../../../../../assets/UserProfile/PlusIconWhite' +import FlexWrapper from '../../../../../shared/components/FlexWrapper/FlexWrapper' +import CustomInput from '../../../../../shared/components/Formik/CustomInput/CustomInput' +import ModalComponent from '../../../../../shared/components/Modal/Modal' +import { ActionButton, EditIconContainer, Text } from '../ResumeInfo.styles' + +function EditingComponentProjects({ handleBack, showingUser }) { + const [modalActive, setModalActive] = useState('') + const { values, setFieldValue, resetForm } = useFormikContext() + const [currentAction, setCurrentAction] = useState('main') // 'main', 'editing', 'adding' + const [index, setIndex] = useState(0) + + const handleReset = () => { + setModalActive('') + setCurrentAction('main') + } + + const { mutate: editUserDetails, isLoading } = useEditUserDetails(handleReset) + + const handleRemoveProject = () => { + const projects = [...values.projectData] + + const updatedProjects = projects.filter((_, i) => i !== index) + + setFieldValue('projectData', updatedProjects) + + editUserDetails({ email: showingUser.email, projectData: updatedProjects }) + } + + const handleEditProject = (index) => { + setIndex(index) + setCurrentAction('editing') + } + + const submitEditProject = () => { + editUserDetails({ email: showingUser.email, projectData: values.projectData }) + } + + const handleOpenModal = (index) => { + setModalActive('DeleteProject') + setIndex(index) + } + + const handleClose = () => { + setModalActive('') + } + + const handleCancel = () => { + setCurrentAction('main') + resetForm() + } + + return ( + <> + + + + Projects + + {currentAction === 'main' && ( + + {({ push, remove }) => ( + <> + {values?.projectData?.length > 0 ? ( + values?.projectData.map((project, index) => ( + + + + {project.title} + + + {project.link} + + + + handleEditProject(index)} + > + + + handleOpenModal(index)} + > + + + + + )) + ) : ( + + No projects added yet. + + )} + + + + Back + + { + push({ title: '', link: '' }) + setCurrentAction('adding') + }} + type="button" + disabled={values.projectData.length === 5 ? true : false} + > + Add new + + + + + )} + + )} + + {currentAction === 'editing' && ( + <> + + + + + + Cancel + + + {isLoading ? ( + + ) : ( + 'Save' + )} + + + + + )} + + {currentAction === 'adding' && ( + <> + + + + + + Cancel + + + {isLoading ? ( + + ) : ( + 'Save' + )} + + + + + )} + + + ) +} + +export default EditingComponentProjects diff --git a/client/src/components/Profile/components/ResumeInfo/EducationWork/EducationWork.jsx b/client/src/components/Profile/components/ResumeInfo/EducationWork/EducationWork.jsx new file mode 100644 index 000000000..b877fcf7c --- /dev/null +++ b/client/src/components/Profile/components/ResumeInfo/EducationWork/EducationWork.jsx @@ -0,0 +1,72 @@ +import EditIcon from '../../../../../assets/UserProfile/EditIcon' +import FlexWrapper from '../../../../../shared/components/FlexWrapper/FlexWrapper' +import { EditIconContainer, Text } from '../ResumeInfo.styles' + +const EducationWork = ({ showingUser, setIsEditing, userStatus }) => { + return ( + <> + + + + Education + + {userStatus === 'same' && ( + setIsEditing('education')} fill={true.toString()}> + + + )} + + {showingUser?.universityData?.length > 0 ? ( + showingUser?.universityData.map((university, index) => ( + + + {university.degree} - {university.major} + + + {university.university} + + + {university?.addmissionDate?.split('-')[0]} -{' '} + {university?.graduationDate?.split('-')[0]} + + + )) + ) : ( + + User has no education. + + )} + + + + + Work experience + + {userStatus === 'same' && ( + setIsEditing('work')} fill={true.toString()}> + + + )} + + {showingUser?.jobData?.length > 0 ? ( + showingUser?.jobData.map((job, index) => ( + + + {job.title} - {job.company} + + + {job.startDate.split('-')[0]} - {job.endDate.split('-')[0]} + + + )) + ) : ( + + User has no working experience. + + )} + + + ) +} + +export default EducationWork diff --git a/client/src/components/Profile/components/ResumeInfo/ProjectsSkills/ProjectsSkills.jsx b/client/src/components/Profile/components/ResumeInfo/ProjectsSkills/ProjectsSkills.jsx new file mode 100644 index 000000000..6718f895f --- /dev/null +++ b/client/src/components/Profile/components/ResumeInfo/ProjectsSkills/ProjectsSkills.jsx @@ -0,0 +1,137 @@ +import { useFormikContext } from 'formik' + +import EditIcon from '../../../../../assets/UserProfile/EditIcon' +import LinkIcon from '../../../../../assets/UserProfile/LinkIcon' +import TeamMembersIcon from '../../../../../assets/UserProfile/TeamMembersIcon' +import { frameworkColors, frameworkTextColors } from '../../../../../constants/frameworkColors' +import { languageOptions } from '../../../../../constants/programmingLanguages' +import FlexWrapper from '../../../../../shared/components/FlexWrapper/FlexWrapper' +import { + EditIconContainer, + FrameWorkItem, + LanguageItem, + Text, + TextArea, + WrappableList, +} from '../ResumeInfo.styles' +import TagLink from '../TagLink/TagLink' + +const ProjectsSkills = ({ showingUser, setIsEditing, userStatus }) => { + return ( + + + + + About me + + {userStatus === 'same' && ( + setIsEditing('description')} fill={true.toString()}> + + + )} + + {showingUser?.description ? ( +