diff --git a/package.json b/package.json index 2ef15be9..b9b62389 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "predeploy": "npm run build", "test": "jest -i", - "start": "webpack-dev-server --mode development --hot --progress --color --port 3000", + "start": "webpack-dev-server --mode development --hot --progress --color --port 3000 --host 0.0.0.0", "build": "webpack -p --progress --colors", "lint": "prettier --write \"src/**/*.{ts,tsx,css}\" \"__tests__/**/*.{ts,tsx,css}\" && tslint --project .", "lint:fix": "tslint --project . --fix" @@ -135,6 +135,7 @@ "dependencies": { "@types/commonmark": "^0.27.3", "@types/react-router-dom": "^5.1.3", + "@types/react-sidebar": "^3.0.0", "@types/reactour": "^1.13.1", "bootstrap": "^4.2.1", "classnames": "^2.2.6", @@ -158,6 +159,7 @@ "react-router": "^4.3.1", "react-router-dom": "^5.1.2", "react-router-redux": "^4.0.8", + "react-sidebar": "^3.0.2", "react-spinners": "^0.5.1", "react-split-pane": "^0.1.84", "reactour": "^1.16.0", diff --git a/src/app/actions/ProfileUser.ts b/src/app/actions/ProfileUser.ts new file mode 100644 index 00000000..3f2c3c6d --- /dev/null +++ b/src/app/actions/ProfileUser.ts @@ -0,0 +1,33 @@ +import * as ProfileInterfaces from 'app/types/ProfileUser'; +import * as UserInterfaces from 'app/types/User'; +import { action } from 'typesafe-actions'; + +export namespace ProfileUserActions { + export enum Type { + GET_PROFILE_USER_DETAILS = 'GET_USER_DETAILS', + GET_MATCH_STATS = 'GET_MATCH_STATS', + UPDATE_PROFILE_USER_DETAILS = 'UPDATE_PROFILE_USER_DETAILS', + UPDATE_MATCH_STATS = 'UPDATE_MATCH_STATS', + } + + interface ProfileUserDetails { + avatar?: string; + college?: string; + userType?: UserInterfaces.UserType; + fullName?: string; + username?: string; + email?: string; + country?: string; + } + + export const updateProfileUserDetails = (profileuserDetails: ProfileUserDetails) => + action(Type.UPDATE_PROFILE_USER_DETAILS, { profileuserDetails }); + + export const getUserDetails = (username: string) => + action(Type.GET_PROFILE_USER_DETAILS, { username }); + + export const updateMatchStats = (matchStats: ProfileInterfaces.ProfileMatchStats) => + action(Type.UPDATE_MATCH_STATS, { matchStats }); + + export const getMatchStats = (username: string) => action(Type.GET_MATCH_STATS, { username }); +} diff --git a/src/app/actions/index.ts b/src/app/actions/index.ts index dfbe67ac..77013486 100644 --- a/src/app/actions/index.ts +++ b/src/app/actions/index.ts @@ -7,3 +7,4 @@ export * from 'app/actions/User'; export * from 'app/actions/Notification'; export * from 'app/actions/code/Submission'; export * from 'app/actions/MatchView'; +export * from 'app/actions/ProfileUser'; diff --git a/src/app/apiFetch/ProfileUser.ts b/src/app/apiFetch/ProfileUser.ts new file mode 100644 index 00000000..4bb982ee --- /dev/null +++ b/src/app/apiFetch/ProfileUser.ts @@ -0,0 +1,45 @@ +/* tslint:disable:no-console*/ +import { jsonResponseWrapper } from 'app/apiFetch/utils'; +import { API_BASE_URL } from '../../config/config'; + +export const getMatchStats = (username: string) => { + const URL = `${API_BASE_URL}user/match-stats/${encodeURIComponent(username)}`; + return fetch(URL, { + credentials: 'include', + method: 'GET', + }) + .then((response) => { + console.log('fetch match response'); + console.log(response); + return jsonResponseWrapper(response); + }) + .then((data) => { + console.log('fetch match data'); + console.log(data); + return data; + }) + .catch((error) => { + console.error(error); + }); +}; + +export const getUserProfile = (username: string) => { + const URL = `${API_BASE_URL}user/${username}`; + return fetch(URL, { + credentials: 'include', + method: 'GET', + }) + .then((response) => { + console.log('fetch profile response'); + console.log(response); + return jsonResponseWrapper(response); + }) + .then((data) => { + console.log('fetch profile data'); + console.log(data); + return data; + }) + .catch((error) => { + console.error(error); + }); +}; diff --git a/src/app/components/Leaderboard/LeaderboardElement.tsx b/src/app/components/Leaderboard/LeaderboardElement.tsx index 257a59b9..d8025d3e 100644 --- a/src/app/components/Leaderboard/LeaderboardElement.tsx +++ b/src/app/components/Leaderboard/LeaderboardElement.tsx @@ -16,7 +16,12 @@ const colors = ['#FFB900', '#69797E', '#847545', '#038387']; export class LeaderboardElement extends React.Component { public render() { const { player, index, isPlayAgainstDisabled, runMatch, currentUsername } = this.props; - + let urlToProfile; + if (player.username === currentUsername) { + urlToProfile = `/profile`; + } else { + urlToProfile = `/profile/${player.username}`; + } const playerTotalMatches = player.numWin + player.numLoss + player.numTie; return ( @@ -86,9 +91,11 @@ export class LeaderboardElement extends React.Component - {`${player.username.substr(0, 15)}${ - player.username.length > 15 ? '...' : '' - }`} + + {`${player.username.substr(0, 15)}${ + player.username.length > 15 ? '...' : '' + }`} +
{ + public componentDidMount() { + this.props.getUserDetails(''); + this.props.getMatchStats(this.props.match.params.username); + } + public render() { + // console.log(this.props.profileUserDetails); + return ( +
+ { + // @ts-ignore + + } +
+ ); + } +} diff --git a/src/app/components/UserProfileModal/UserStats.tsx b/src/app/components/UserProfileModal/UserStats.tsx new file mode 100644 index 00000000..f8e58b1e --- /dev/null +++ b/src/app/components/UserProfileModal/UserStats.tsx @@ -0,0 +1,7 @@ +import * as React from 'react'; + +export class UserStats extends React.Component { + public render() { + return

This is the Page for Current user stats

; + } +} diff --git a/src/app/components/UserProfileModal/index.tsx b/src/app/components/UserProfileModal/index.tsx index 1143a63a..65b5f31e 100755 --- a/src/app/components/UserProfileModal/index.tsx +++ b/src/app/components/UserProfileModal/index.tsx @@ -1,6 +1,9 @@ +import { faChartLine, faLock, faUser } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import PopUpMenu from 'app/components/PopUpMenu'; import { EditPassword } from 'app/components/UserProfileModal/EditPassword'; import { EditProfile } from 'app/components/UserProfileModal/EditProfile'; +import { UserStats } from 'app/components/UserProfileModal/UserStats'; import * as styles from 'app/styles/UserProfileModal.module.css'; import { AvatarId } from 'app/types/Authentication/Register'; import * as UserProfileInterfaces from 'app/types/UserProfileModal'; @@ -25,6 +28,7 @@ export class UserProfileModal extends React.Component< this.state = { avatar: userDetails.avatar, country: userDetails.country, + currentPage: UserProfileInterfaces.SelectedPage.EDITPROFILE, fullName: userDetails.fullName, isPasswordPage: true, oldPassword: '', @@ -32,7 +36,58 @@ export class UserProfileModal extends React.Component< repeatPassword: '', username: userDetails.username, }; - this.props.getUserDetails(); + } + + public renderSwitch( + param: UserProfileInterfaces.SelectedPage, + username: string, + fullName: string, + // tslint:disable-next-line + userDetails: any, + country: string, + avatar: string, + oldPassword: string, + password: string, + repeatPassword: string, + ) { + switch (param) { + case UserProfileInterfaces.SelectedPage.EDITPROFILE: + return ( + + ); + break; + + case UserProfileInterfaces.SelectedPage.EDITPASSWORD: + return ( + + ); + break; + + case UserProfileInterfaces.SelectedPage.USERSTATS: + return ; + break; + + default: + return

Default

; + } } public render() { @@ -48,6 +103,55 @@ export class UserProfileModal extends React.Component< const { userDetails } = this.props; return ( +
+
{ + this.setState({ + currentPage: UserProfileInterfaces.SelectedPage.EDITPROFILE, + }); + }} + > + + +
+ +
{ + this.setState({ + currentPage: UserProfileInterfaces.SelectedPage.EDITPASSWORD, + }); + }} + > + + +
+ +
{ + this.setState({ + currentPage: UserProfileInterfaces.SelectedPage.USERSTATS, + }); + }} + > + + +
+
- {this.state.isPasswordPage ? ( - - ) : ( - + {this.renderSwitch( + this.state.currentPage, + username, + fullName, + userDetails, + country, + avatar, + oldPassword, + password, + repeatPassword, )} @@ -87,12 +179,20 @@ export class UserProfileModal extends React.Component< : classnames('labeltext', styles.passwordPageLink) } onClick={() => { - this.setState((prevState) => ({ - isPasswordPage: !prevState.isPasswordPage, - })); + const newState = + this.state.currentPage === UserProfileInterfaces.SelectedPage.EDITPROFILE + ? UserProfileInterfaces.SelectedPage.EDITPASSWORD + : UserProfileInterfaces.SelectedPage.EDITPROFILE; + this.setState({ + currentPage: newState, + }); }} > - {this.state.isPasswordPage ? 'Want to change Credentials?' : 'Want to change Info?'} + {this.state.currentPage === UserProfileInterfaces.SelectedPage.EDITPROFILE + ? 'Want to change Credentials?' + : this.state.currentPage === UserProfileInterfaces.SelectedPage.EDITPASSWORD + ? 'Want to change Info?' + : null} {this.state.isPasswordPage ? ( diff --git a/src/app/containers/ProfileUsersStats.ts b/src/app/containers/ProfileUsersStats.ts new file mode 100644 index 00000000..6384b5aa --- /dev/null +++ b/src/app/containers/ProfileUsersStats.ts @@ -0,0 +1,33 @@ +import { ProfileUserActions } from 'app/actions'; +import ProfileUserStats from 'app/components/ProfileUserStats'; +import { RootState } from 'app/reducers'; +import * as ProfileUserInterfaces from 'app/types/ProfileUser'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; + +const mapStateToProps = (rootState: RootState) => { + return { + profileUserDetails: rootState.profileUser, + }; +}; + +const mapDispatchToProps = (dispatch: Dispatch) => { + return { + getMatchStats: (username: string) => dispatch(ProfileUserActions.getMatchStats(username)), + getUserDetails: (username: string) => dispatch(ProfileUserActions.getUserDetails(username)), + updateProfileUserDetails: ( + updateProfileUserDetails: ProfileUserInterfaces.EditProfileUserDetails, + ) => dispatch(ProfileUserActions.updateProfileUserDetails(updateProfileUserDetails)), + }; +}; + +const profileUserContainer = connect< + ProfileUserInterfaces.StateProps, + ProfileUserInterfaces.DispatchProps, + {} +>( + mapStateToProps, + mapDispatchToProps, +)(ProfileUserStats); + +export default profileUserContainer; diff --git a/src/app/index.tsx b/src/app/index.tsx index d70d20c7..deb98878 100644 --- a/src/app/index.tsx +++ b/src/app/index.tsx @@ -3,6 +3,7 @@ import Login from 'app/containers/Authentication/Login'; import Register from 'app/containers/Authentication/Register'; import Dashboard from 'app/containers/Dashboard'; import Leaderboard from 'app/containers/Leaderboard'; +import ProfileUsersStats from 'app/containers/ProfileUsersStats'; import UserProfileModal from 'app/containers/UserProfileModal'; import { Routes } from 'app/routes'; // @ts-ignore @@ -25,6 +26,7 @@ export const App = hot(module)(() => ( + diff --git a/src/app/reducers/ProfileUser.ts b/src/app/reducers/ProfileUser.ts new file mode 100644 index 00000000..368f2bce --- /dev/null +++ b/src/app/reducers/ProfileUser.ts @@ -0,0 +1,61 @@ +import { ProfileUserActions } from 'app/actions'; +import * as ProfileUserInterfaces from 'app/types/ProfileUser'; + +const profileStoreIntialState: ProfileUserInterfaces.ProfileUserStoreState = { + avatar: '', + college: '', + country: 'IN', + email: '', + fullName: '', + matchStats: { + auto: { wins: 0, losses: 0, ties: 0 }, + faced: { wins: 0, losses: 0, ties: 0 }, + initiated: { wins: 0, losses: 0, ties: 0 }, + lastMatchAt: '', + numMatchches: 0, + userId: 0, + }, + type: '', + userType: ProfileUserInterfaces.ProfileUserType.STUDENT, + username: '', +}; + +export const profileuserReducer = ( + state = profileStoreIntialState, + action: ProfileUserInterfaces.ProfileUserStoreAction, +) => { + switch (action.type) { + case ProfileUserActions.Type.UPDATE_PROFILE_USER_DETAILS: { + const { + avatar, + college, + country, + email, + fullName, + userType, + username, + } = action.payload.profileuserDetails; + + return { + ...state, + avatar: avatar !== undefined ? avatar : state.avatar, + college: college !== undefined ? college : state.college, + country: country !== undefined ? country : state.country, + email: email !== undefined ? email : state.email, + fullName: fullName !== undefined ? fullName : state.fullName, + type: userType !== undefined ? userType : state.type, + username: username !== undefined ? username : state.username, + }; + } + + case ProfileUserActions.Type.UPDATE_MATCH_STATS: { + return { + ...state, + matchStats: action.payload.matchStats, + }; + } + + default: + return state; + } +}; diff --git a/src/app/reducers/index.ts b/src/app/reducers/index.ts index da6d5d64..eba805d7 100644 --- a/src/app/reducers/index.ts +++ b/src/app/reducers/index.ts @@ -6,6 +6,7 @@ import { gameLogReducer } from 'app/reducers/GameLog'; import { leaderboardReducer } from 'app/reducers/Leaderboard'; import { matchesReducer } from 'app/reducers/MatchView'; import { notificationReducer } from 'app/reducers/Notification'; +import { profileuserReducer } from 'app/reducers/ProfileUser'; import { userReducer } from 'app/reducers/User'; import * as CodeInterfaces from 'app/types/code/Code'; import * as EditorInterfaces from 'app/types/code/Editor'; @@ -15,6 +16,7 @@ import * as GameLogInterfaces from 'app/types/GameLog'; import * as LeaderboardInterfaces from 'app/types/Leaderboard'; import * as MatchInterfaces from 'app/types/MatchView'; import * as NotificationInterfaces from 'app/types/Notification'; +import * as ProfileUserInterfaces from 'app/types/ProfileUser'; import * as UserInterfaces from 'app/types/User'; import { routerReducer, RouterState } from 'react-router-redux'; import { combineReducers } from 'redux'; @@ -27,6 +29,7 @@ export const rootReducer = combineReducers({ leaderboard: leaderboardReducer, match: matchesReducer, notification: notificationReducer, + profileUser: profileuserReducer, router: routerReducer, submission: submissionReducer, user: userReducer, @@ -42,5 +45,6 @@ export interface RootState { router: RouterState; gameLog: GameLogInterfaces.GameLogStoreState; user: UserInterfaces.UserStoreState; + profileUser: ProfileUserInterfaces.ProfileUserStoreState; submission: SubmissionInterfaces.SubmissionStoreState; } diff --git a/src/app/routes.ts b/src/app/routes.ts index 7b0c8cb8..d919b842 100644 --- a/src/app/routes.ts +++ b/src/app/routes.ts @@ -7,4 +7,5 @@ export enum Routes { GITHUB_OAUTH = '/login/github', GOOGLE_OAUTH = '/login/google', USER_ACTIVATION = '/user-activate', + PROFILE_USER_STATS = '/profile::username', } diff --git a/src/app/sagas/ProfileUser.ts b/src/app/sagas/ProfileUser.ts new file mode 100644 index 00000000..f8ae3e32 --- /dev/null +++ b/src/app/sagas/ProfileUser.ts @@ -0,0 +1,48 @@ +/* tslint:disable:no-console*/ +import { ProfileUserActions, UserActions } from 'app/actions'; +import * as ProfileUserFetch from 'app/apiFetch/ProfileUser'; +import { avatarName } from 'app/types/Authentication/Register'; +import { resType } from 'app/types/sagas'; +import { all, call, put, takeEvery } from 'redux-saga/effects'; +import { ActionType } from 'typesafe-actions'; + +export function* getMatchStats(action: ActionType) { + try { + const res = yield call(ProfileUserFetch.getMatchStats, action.payload.username); + yield put(UserActions.updateErrorMessage(res.error)); + console.log('saga match res'); + console.log(res); + if (res.type !== resType.ERROR) { + const { avatarId, college, country, fullName, userType, username } = res.body; + yield put( + ProfileUserActions.updateProfileUserDetails({ + college, + country, + fullName, + userType, + username, + avatar: avatarName[avatarId], + }), + ); + } + } catch (err) { + console.error(err); + } +} + +export function* getUserProfile(action: ActionType) { + try { + const res = yield call(ProfileUserFetch.getUserProfile, action.payload.username); + console.log('saga profile res'); + console.log(res); + } catch (err) { + console.error(err); + } +} + +export function* profileSagas() { + yield all([ + takeEvery(ProfileUserActions.Type.GET_MATCH_STATS, getMatchStats), + takeEvery(ProfileUserActions.Type.GET_PROFILE_USER_DETAILS, getUserProfile), + ]); +} diff --git a/src/app/store/index.ts b/src/app/store/index.ts index 8fb448fe..1f792b4b 100644 --- a/src/app/store/index.ts +++ b/src/app/store/index.ts @@ -4,6 +4,7 @@ import { codeSagas } from 'app/sagas/Code'; import { leaderboardSagas } from 'app/sagas/Leaderboard'; import { matchSagas } from 'app/sagas/MatchView'; import { notificationSagas } from 'app/sagas/Notification'; +import { profileSagas } from 'app/sagas/ProfileUser'; import { submissionSagas } from 'app/sagas/Submission'; import { userSagas } from 'app/sagas/User'; import { applyMiddleware, createStore } from 'redux'; @@ -32,6 +33,7 @@ export function configureStore(initialState?: object) { const store = createStore(persistedReducer, initialState, middleware); sagaMiddleware.run(userSagas); sagaMiddleware.run(codeSagas); + sagaMiddleware.run(profileSagas); sagaMiddleware.run(leaderboardSagas); sagaMiddleware.run(submissionSagas); sagaMiddleware.run(matchSagas); diff --git a/src/app/styles/UserProfileModal.module.css b/src/app/styles/UserProfileModal.module.css index a318cb77..260e797c 100755 --- a/src/app/styles/UserProfileModal.module.css +++ b/src/app/styles/UserProfileModal.module.css @@ -119,7 +119,7 @@ .passwordForm { padding: 10px 25px; width: 100%; - margin-left: 5%; + margin-left: 15%; } .profileForm { diff --git a/src/app/types/ProfileUser.ts b/src/app/types/ProfileUser.ts new file mode 100644 index 00000000..3a90a834 --- /dev/null +++ b/src/app/types/ProfileUser.ts @@ -0,0 +1,70 @@ +import { ProfileUserActions } from 'app/actions'; +import { RouteComponentProps } from 'react-router-dom'; +import { ActionType } from 'typesafe-actions'; + +export enum ProfileUserType { + STUDENT = 'STUDENT', + PROFESSIONAL = 'PROFESSIONAL', +} + +export interface EditProfileUserDetails { + username?: string; + email?: string; + country?: string; + fullName?: string; + college?: string; + type?: string; + avatar?: string; +} + +const actions = { + getMatchStats: ProfileUserActions.getMatchStats, + getUserDetails: ProfileUserActions.getUserDetails, + updateMatchStats: ProfileUserActions.updateMatchStats, + updateProfileUserDetails: ProfileUserActions.updateProfileUserDetails, +}; + +export interface ProfileUserStoreState { + avatar: string; + college: string; + country: string; + email: string; + fullName: string; + type: string; + userType: ProfileUserType; + username: string; + matchStats: ProfileMatchStats; +} + +export interface MatchStatsItem { + wins: number; + losses: number; + ties: number; +} + +export interface ProfileMatchStats { + auto: MatchStatsItem; + faced: MatchStatsItem; + initiated: MatchStatsItem; + lastMatchAt: string; + numMatchches: number; + userId: number; +} + +export interface StateProps { + profileUserDetails: ProfileUserStoreState; +} + +export interface DispatchProps { + updateProfileUserDetails: (updateProfileUserDetails: EditProfileUserDetails) => void; + getUserDetails: (username: string) => void; + getMatchStats: (username: string) => void; +} + +interface UrlMatchParams { + username: string; +} + +export type Props = StateProps & DispatchProps & RouteComponentProps; + +export type ProfileUserStoreAction = ActionType; diff --git a/src/app/types/User.ts b/src/app/types/User.ts index 99e8ab1a..8af4e35c 100644 --- a/src/app/types/User.ts +++ b/src/app/types/User.ts @@ -71,4 +71,14 @@ export interface UserStoreState { avatar: string; } +export interface ProfileUserStoreState { + avatar: string; + college: string; + country: string; + email: string; + fullName: string; + type: string; + username: string; +} + export type UserStoreAction = ActionType; diff --git a/src/app/types/UserProfileModal/index.ts b/src/app/types/UserProfileModal/index.ts index 388829bf..4d72ceef 100644 --- a/src/app/types/UserProfileModal/index.ts +++ b/src/app/types/UserProfileModal/index.ts @@ -1,5 +1,11 @@ import * as UserInterfaces from 'app/types/User'; +export enum SelectedPage { + EDITPROFILE = 0, + EDITPASSWORD = 1, + USERSTATS = 2, +} + export interface StateProps { isUserProfileModalOpen: boolean; userDetails: UserInterfaces.UserStoreState; @@ -11,6 +17,7 @@ export interface State { oldPassword: string; password: string; repeatPassword: string; + currentPage: SelectedPage; country: string; fullName: string; isPasswordPage: boolean;