feat(profile): Add Profile and components
This commit is contained in:
		
				
					committed by
					
						 mrugesh mohapatra
						mrugesh mohapatra
					
				
			
			
				
	
			
			
			
						parent
						
							491912448b
						
					
				
				
					commit
					987da19254
				
			
							
								
								
									
										5
									
								
								client/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								client/package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -4428,6 +4428,11 @@ | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "date-fns": { | ||||||
|  |       "version": "1.29.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.29.0.tgz", | ||||||
|  |       "integrity": "sha512-lbTXWZ6M20cWH8N9S6afb0SBm6tMk+uUg6z3MqHPKE9atmsY3kJkTm8vKe93izJ2B2+q5MV990sM2CHgtAZaOw==" | ||||||
|  |     }, | ||||||
|     "date-now": { |     "date-now": { | ||||||
|       "version": "0.1.4", |       "version": "0.1.4", | ||||||
|       "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", |       "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", | ||||||
|   | |||||||
| @@ -19,6 +19,7 @@ | |||||||
|     "axios": "^0.18.0", |     "axios": "^0.18.0", | ||||||
|     "browser-cookies": "^1.2.0", |     "browser-cookies": "^1.2.0", | ||||||
|     "chai": "^4.2.0", |     "chai": "^4.2.0", | ||||||
|  |     "date-fns": "^1.29.0", | ||||||
|     "enzyme": "^3.6.0", |     "enzyme": "^3.6.0", | ||||||
|     "enzyme-adapter-react-16": "^1.5.0", |     "enzyme-adapter-react-16": "^1.5.0", | ||||||
|     "fetchr": "^0.5.37", |     "fetchr": "^0.5.37", | ||||||
|   | |||||||
							
								
								
									
										98
									
								
								client/src/client-only-routes/ShowProfileOrFourOhFour.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								client/src/client-only-routes/ShowProfileOrFourOhFour.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | |||||||
|  | import React, { Component } from 'react'; | ||||||
|  | import PropTypes from 'prop-types'; | ||||||
|  | import { bindActionCreators } from 'redux'; | ||||||
|  | import { connect } from 'react-redux'; | ||||||
|  | import { isEmpty } from 'lodash'; | ||||||
|  |  | ||||||
|  | import Loader from '../components/helpers/Loader'; | ||||||
|  | import Layout from '../components/layouts/Default'; | ||||||
|  | import { | ||||||
|  |   userByNameSelector, | ||||||
|  |   userProfileFetchStateSelector, | ||||||
|  |   fetchProfileForUser, | ||||||
|  |   usernameSelector | ||||||
|  | } from '../redux'; | ||||||
|  | import FourOhFourPage from '../components/FourOhFour'; | ||||||
|  | import Profile from '../components/profile/Profile'; | ||||||
|  |  | ||||||
|  | const propTypes = { | ||||||
|  |   fetchProfileForUser: PropTypes.func.isRequired, | ||||||
|  |   isSessionUser: PropTypes.bool, | ||||||
|  |   maybeUser: PropTypes.string, | ||||||
|  |   requestedUser: PropTypes.shape({ | ||||||
|  |     username: PropTypes.string, | ||||||
|  |     profileUI: PropTypes.object | ||||||
|  |   }), | ||||||
|  |   showLoading: PropTypes.bool, | ||||||
|  |   splat: PropTypes.string | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const createRequestedUserSelector = () => (state, { maybeUser }) => | ||||||
|  |   userByNameSelector(maybeUser)(state); | ||||||
|  | const createIsSessionUserSelector = () => (state, { maybeUser }) => | ||||||
|  |   maybeUser === usernameSelector(state); | ||||||
|  |  | ||||||
|  | const makeMapStateToProps = () => (state, props) => { | ||||||
|  |   const requestedUserSelector = createRequestedUserSelector(); | ||||||
|  |   const isSessionUserSelector = createIsSessionUserSelector(); | ||||||
|  |   const fetchState = userProfileFetchStateSelector(state, props); | ||||||
|  |   return { | ||||||
|  |     requestedUser: requestedUserSelector(state, props), | ||||||
|  |     isSessionUser: isSessionUserSelector(state, props), | ||||||
|  |     showLoading: fetchState.pending, | ||||||
|  |     fetchState | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const mapDispatchToProps = dispatch => | ||||||
|  |   bindActionCreators({ fetchProfileForUser }, dispatch); | ||||||
|  |  | ||||||
|  | class ShowFourOhFour extends Component { | ||||||
|  |   componentDidMount() { | ||||||
|  |     const { requestedUser, maybeUser, splat, fetchProfileForUser } = this.props; | ||||||
|  |     if (!splat && isEmpty(requestedUser)) { | ||||||
|  |       console.log(requestedUser); | ||||||
|  |       return fetchProfileForUser(maybeUser); | ||||||
|  |     } | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   render() { | ||||||
|  |     const { isSessionUser, requestedUser, showLoading, splat } = this.props; | ||||||
|  |     if (splat) { | ||||||
|  |       // the uri path for this component is /:maybeUser/:splat | ||||||
|  |       // if splat is defined then we on a route that is not a profile | ||||||
|  |       // and we should just 404 | ||||||
|  |       return <FourOhFourPage />; | ||||||
|  |     } | ||||||
|  |     if (showLoading) { | ||||||
|  |       // We don't know if /:maybeUser is a user or not, we will show the loader | ||||||
|  |       // until we get a response from the API | ||||||
|  |       return ( | ||||||
|  |         <Layout> | ||||||
|  |           <div className='loader-wrapper'> | ||||||
|  |             <Loader /> | ||||||
|  |           </div> | ||||||
|  |         </Layout> | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     if (isEmpty(requestedUser)) { | ||||||
|  |       // We have a response from the API, but there is nothing in the store | ||||||
|  |       // for /:maybeUser. We can derive from this state the /:maybeUser is not | ||||||
|  |       // a user the API recognises, so we 404 | ||||||
|  |       return <FourOhFourPage />; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // We have a response from the API, and we have some state in the | ||||||
|  |     // store for /:maybeUser, we now handover rendering to the Profile component | ||||||
|  |     return <Profile isSessionUser={isSessionUser} user={requestedUser} />; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | ShowFourOhFour.displayName = 'ShowFourOhFour'; | ||||||
|  | ShowFourOhFour.propTypes = propTypes; | ||||||
|  |  | ||||||
|  | export default connect( | ||||||
|  |   makeMapStateToProps, | ||||||
|  |   mapDispatchToProps | ||||||
|  | )(ShowFourOhFour); | ||||||
							
								
								
									
										64
									
								
								client/src/components/FourOhFour/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								client/src/components/FourOhFour/index.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | |||||||
|  | import React, { Component } from 'react'; | ||||||
|  | import Helmet from 'react-helmet'; | ||||||
|  | import Spinner from 'react-spinkit'; | ||||||
|  | import { Link } from 'gatsby'; | ||||||
|  |  | ||||||
|  | import Layout from '../layouts/Default'; | ||||||
|  |  | ||||||
|  | import notFoundLogo from '../../images/freeCodeCamp-404.svg'; | ||||||
|  | import { quotes } from '../../resources/quotes.json'; | ||||||
|  |  | ||||||
|  | import './404.css'; | ||||||
|  |  | ||||||
|  | class NotFoundPage extends Component { | ||||||
|  |   constructor(props) { | ||||||
|  |     super(props); | ||||||
|  |     this.state = { | ||||||
|  |       randomQuote: null | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   componentDidMount() { | ||||||
|  |     this.updateQuote(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   updateQuote() { | ||||||
|  |     this.setState({ | ||||||
|  |       randomQuote: quotes[Math.floor(Math.random() * quotes.length)] | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   render() { | ||||||
|  |     return ( | ||||||
|  |       <Layout> | ||||||
|  |         <div className='notfound-page-wrapper'> | ||||||
|  |           <Helmet title='Page Not Found | freeCodeCamp' /> | ||||||
|  |           <img alt='404 Not Found' src={notFoundLogo} /> | ||||||
|  |           <h1>NOT FOUND</h1> | ||||||
|  |           {this.state.randomQuote ? ( | ||||||
|  |             <div> | ||||||
|  |               <p> | ||||||
|  |                 We couldn't find what you were looking for, but here is a | ||||||
|  |                 quote: | ||||||
|  |               </p> | ||||||
|  |               <div className='quote-wrapper'> | ||||||
|  |                 <p className='quote'> | ||||||
|  |                   <span>“</span> | ||||||
|  |                   {this.state.randomQuote.quote} | ||||||
|  |                 </p> | ||||||
|  |                 <p className='author'>- {this.state.randomQuote.author}</p> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |           ) : ( | ||||||
|  |             <Spinner color='#006400' name='ball-clip-rotate-multiple' /> | ||||||
|  |           )} | ||||||
|  |           <Link className='btn-curriculum' to='/learn'> | ||||||
|  |             View the Curriculum | ||||||
|  |           </Link> | ||||||
|  |         </div> | ||||||
|  |       </Layout> | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default NotFoundPage; | ||||||
							
								
								
									
										40
									
								
								client/src/components/helpers/CurrentChallengeLink.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								client/src/components/helpers/CurrentChallengeLink.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | import React from 'react'; | ||||||
|  | import PropTypes from 'prop-types'; | ||||||
|  | import { bindActionCreators } from 'redux'; | ||||||
|  | import { connect } from 'react-redux'; | ||||||
|  |  | ||||||
|  | import { apiLocation } from '../../../config/env.json'; | ||||||
|  |  | ||||||
|  | import { hardGoTo } from '../../redux'; | ||||||
|  |  | ||||||
|  | const currentChallengeApi = '/challenges/current-challenge'; | ||||||
|  |  | ||||||
|  | const propTypes = { | ||||||
|  |   children: PropTypes.any, | ||||||
|  |   hardGoTo: PropTypes.func.isRequired | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const mapStateToProps = () => ({}); | ||||||
|  | const mapDispatchToProps = dispatch => | ||||||
|  |   bindActionCreators({ hardGoTo }, dispatch); | ||||||
|  |  | ||||||
|  | const createClickHandler = hardGoTo => e => { | ||||||
|  |   e.preventDefault(); | ||||||
|  |   return hardGoTo(`${apiLocation}${currentChallengeApi}`); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | function CurrentChallengeLink({ children, hardGoTo }) { | ||||||
|  |   return ( | ||||||
|  |     <a href={currentChallengeApi} onClick={createClickHandler(hardGoTo)}> | ||||||
|  |       {children} | ||||||
|  |     </a> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | CurrentChallengeLink.displayName = 'CurrentChallengeLink'; | ||||||
|  | CurrentChallengeLink.propTypes = propTypes; | ||||||
|  |  | ||||||
|  | export default connect( | ||||||
|  |   mapStateToProps, | ||||||
|  |   mapDispatchToProps | ||||||
|  | )(CurrentChallengeLink); | ||||||
							
								
								
									
										176
									
								
								client/src/components/profile/Profile.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								client/src/components/profile/Profile.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,176 @@ | |||||||
|  | import React, { Fragment } from 'react'; | ||||||
|  | import PropTypes from 'prop-types'; | ||||||
|  | import { Alert, Button, Grid, Row, Col } from '@freecodecamp/react-bootstrap'; | ||||||
|  | import Helmet from 'react-helmet'; | ||||||
|  | import { Link } from 'gatsby'; | ||||||
|  |  | ||||||
|  | import Layout from '../layouts/Default'; | ||||||
|  | import CurrentChallengeLink from '../helpers/CurrentChallengeLink'; | ||||||
|  | import FullWidthRow from '../helpers/FullWidthRow'; | ||||||
|  | import Spacer from '../helpers/Spacer'; | ||||||
|  | import Camper from './components/Camper'; | ||||||
|  | import HeatMap from './components/HeatMap'; | ||||||
|  | import Certifications from './components/Certifications'; | ||||||
|  | import Portfolio from './components/Portfolio'; | ||||||
|  | import Timeline from './components/TimeLine'; | ||||||
|  |  | ||||||
|  | const propTypes = { | ||||||
|  |   isSessionUser: PropTypes.bool, | ||||||
|  |   user: PropTypes.shape({ | ||||||
|  |     profileUI: PropTypes.shape({ | ||||||
|  |       isLocked: PropTypes.bool, | ||||||
|  |       showAbout: PropTypes.bool, | ||||||
|  |       showCerts: PropTypes.bool, | ||||||
|  |       showHeatMap: PropTypes.bool, | ||||||
|  |       showLocation: PropTypes.bool, | ||||||
|  |       showName: PropTypes.bool, | ||||||
|  |       showPoints: PropTypes.bool, | ||||||
|  |       showPortfolio: PropTypes.bool, | ||||||
|  |       showTimeLine: PropTypes.bool | ||||||
|  |     }), | ||||||
|  |     username: PropTypes.string | ||||||
|  |   }) | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | function TakeMeToTheChallenges() { | ||||||
|  |   return ( | ||||||
|  |     <CurrentChallengeLink> | ||||||
|  |       <Button block={true} bsSize='lg' bsStyle='primary'> | ||||||
|  |         Take me to the Challenges | ||||||
|  |       </Button> | ||||||
|  |     </CurrentChallengeLink> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function renderIsLocked(username) { | ||||||
|  |   return ( | ||||||
|  |     <Layout> | ||||||
|  |       <Helmet> | ||||||
|  |         <title>{username} | freeCodeCamp.org</title> | ||||||
|  |       </Helmet> | ||||||
|  |       <Spacer size={2} /> | ||||||
|  |       <Grid> | ||||||
|  |         <FullWidthRow> | ||||||
|  |           <h2 className='text-center'> | ||||||
|  |             {username} has not made their profile public. | ||||||
|  |           </h2> | ||||||
|  |         </FullWidthRow> | ||||||
|  |         <FullWidthRow> | ||||||
|  |           <Alert bsStyle='info'> | ||||||
|  |             <p> | ||||||
|  |               {username} needs to change their privacy setting in order for you | ||||||
|  |               to view their profile | ||||||
|  |             </p> | ||||||
|  |           </Alert> | ||||||
|  |         </FullWidthRow> | ||||||
|  |         <FullWidthRow> | ||||||
|  |           <TakeMeToTheChallenges /> | ||||||
|  |         </FullWidthRow> | ||||||
|  |       </Grid> | ||||||
|  |     </Layout> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function renderSettingsButton() { | ||||||
|  |   return ( | ||||||
|  |     <Fragment> | ||||||
|  |       <Row> | ||||||
|  |         <Col sm={4} smOffset={4}> | ||||||
|  |           <Link to='/settings'> | ||||||
|  |             <Button | ||||||
|  |               block={true} | ||||||
|  |               bsSize='lg' | ||||||
|  |               bsStyle='primary' | ||||||
|  |               className='btn-invert' | ||||||
|  |               > | ||||||
|  |               Update my settings | ||||||
|  |             </Button> | ||||||
|  |           </Link> | ||||||
|  |         </Col> | ||||||
|  |       </Row> | ||||||
|  |       <Spacer size={2} /> | ||||||
|  |     </Fragment> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function Profile({ user, isSessionUser }) { | ||||||
|  |   const { | ||||||
|  |     profileUI: { | ||||||
|  |       isLocked = true, | ||||||
|  |       showAbout = false, | ||||||
|  |       showCerts = false, | ||||||
|  |       showHeatMap = false, | ||||||
|  |       showLocation = false, | ||||||
|  |       showName = false, | ||||||
|  |       showPoints = false, | ||||||
|  |       showPortfolio = false, | ||||||
|  |       showTimeLine = false | ||||||
|  |     }, | ||||||
|  |     calendar, | ||||||
|  |     completedChallenges, | ||||||
|  |     streak, | ||||||
|  |     githubProfile, | ||||||
|  |     isLinkedIn, | ||||||
|  |     isGithub, | ||||||
|  |     isTwitter, | ||||||
|  |     isWebsite, | ||||||
|  |     linkedin, | ||||||
|  |     twitter, | ||||||
|  |     website, | ||||||
|  |     name, | ||||||
|  |     username, | ||||||
|  |     location, | ||||||
|  |     points, | ||||||
|  |     picture, | ||||||
|  |     portfolio, | ||||||
|  |     about, | ||||||
|  |     yearsTopContributor | ||||||
|  |   } = user; | ||||||
|  |  | ||||||
|  |   if (isLocked) { | ||||||
|  |     return renderIsLocked(username); | ||||||
|  |   } | ||||||
|  |   return ( | ||||||
|  |     <Layout> | ||||||
|  |       <Helmet> | ||||||
|  |         <title>{username} | freeCodeCamp.org</title> | ||||||
|  |       </Helmet> | ||||||
|  |       <Spacer size={2} /> | ||||||
|  |       <Grid> | ||||||
|  |         {isSessionUser ? renderSettingsButton() : null} | ||||||
|  |         <Camper | ||||||
|  |           about={showAbout && about} | ||||||
|  |           githubProfile={githubProfile} | ||||||
|  |           isGithub={isGithub} | ||||||
|  |           isLinkedIn={isLinkedIn} | ||||||
|  |           isTwitter={isTwitter} | ||||||
|  |           isWebsite={isWebsite} | ||||||
|  |           linkedin={linkedin} | ||||||
|  |           location={showLocation && location} | ||||||
|  |           name={showName && name} | ||||||
|  |           picture={picture} | ||||||
|  |           points={showPoints && points} | ||||||
|  |           twitter={twitter} | ||||||
|  |           username={username} | ||||||
|  |           website={website} | ||||||
|  |           yearsTopContributor={yearsTopContributor} | ||||||
|  |         /> | ||||||
|  |       </Grid> | ||||||
|  |       {showHeatMap ? <HeatMap calendar={calendar} streak={streak} /> : null} | ||||||
|  |       {showCerts ? <Certifications /> : null} | ||||||
|  |       {showPortfolio ? <Portfolio portfolio={portfolio} /> : null} | ||||||
|  |       {showTimeLine ? ( | ||||||
|  |         <Timeline | ||||||
|  |           className='timelime-container' | ||||||
|  |           completedMap={completedChallenges} | ||||||
|  |           username={username} | ||||||
|  |         /> | ||||||
|  |       ) : null} | ||||||
|  |     </Layout> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Profile.displayName = 'Profile'; | ||||||
|  | Profile.propTypes = propTypes; | ||||||
|  |  | ||||||
|  | export default Profile; | ||||||
							
								
								
									
										113
									
								
								client/src/components/profile/components/Camper.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								client/src/components/profile/components/Camper.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | |||||||
|  | import React from 'react'; | ||||||
|  | import PropTypes from 'prop-types'; | ||||||
|  | import { Col, Row, Image } from '@freecodecamp/react-bootstrap'; | ||||||
|  | import FontAwesomeIcon from '@fortawesome/react-fontawesome'; | ||||||
|  | import { faAward } from '@fortawesome/free-solid-svg-icons'; | ||||||
|  |  | ||||||
|  | import SocialIcons from './SocialIcons'; | ||||||
|  |  | ||||||
|  | import './camper.css'; | ||||||
|  |  | ||||||
|  | const propTypes = { | ||||||
|  |   about: PropTypes.string, | ||||||
|  |   githubProfile: PropTypes.string, | ||||||
|  |   isGithub: PropTypes.bool, | ||||||
|  |   isLinkedIn: PropTypes.bool, | ||||||
|  |   isTwitter: PropTypes.bool, | ||||||
|  |   isWebsite: PropTypes.bool, | ||||||
|  |   linkedin: PropTypes.string, | ||||||
|  |   location: PropTypes.string, | ||||||
|  |   name: PropTypes.string, | ||||||
|  |   picture: PropTypes.string, | ||||||
|  |   points: PropTypes.number, | ||||||
|  |   twitter: PropTypes.string, | ||||||
|  |   username: PropTypes.string, | ||||||
|  |   website: PropTypes.string, | ||||||
|  |   yearsTopContributor: PropTypes.array | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | function pluralise(word, condition) { | ||||||
|  |   return condition ? word + 's' : word; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function joinArray(array) { | ||||||
|  |   return array.reduce((string, item, index, array) => { | ||||||
|  |     if (string.length > 0) { | ||||||
|  |       if (index === array.length - 1) { | ||||||
|  |         return `${string} and ${item}`; | ||||||
|  |       } else { | ||||||
|  |         return `${string}, ${item}`; | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       return item; | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function Camper({ | ||||||
|  |   name, | ||||||
|  |   username, | ||||||
|  |   location, | ||||||
|  |   points, | ||||||
|  |   picture, | ||||||
|  |   about, | ||||||
|  |   yearsTopContributor, | ||||||
|  |   githubProfile, | ||||||
|  |   isLinkedIn, | ||||||
|  |   isGithub, | ||||||
|  |   isTwitter, | ||||||
|  |   isWebsite, | ||||||
|  |   linkedin, | ||||||
|  |   twitter, | ||||||
|  |   website | ||||||
|  | }) { | ||||||
|  |   return ( | ||||||
|  |     <div> | ||||||
|  |       <Row> | ||||||
|  |         <Col className='avatar-container' xs={12}> | ||||||
|  |           <Image | ||||||
|  |             alt={username + "'s avatar"} | ||||||
|  |             className='avatar' | ||||||
|  |             responsive={true} | ||||||
|  |             src={picture} | ||||||
|  |           /> | ||||||
|  |         </Col> | ||||||
|  |       </Row> | ||||||
|  |       <SocialIcons | ||||||
|  |         githubProfile={githubProfile} | ||||||
|  |         isGithub={isGithub} | ||||||
|  |         isLinkedIn={isLinkedIn} | ||||||
|  |         isTwitter={isTwitter} | ||||||
|  |         isWebsite={isWebsite} | ||||||
|  |         linkedin={linkedin} | ||||||
|  |         twitter={twitter} | ||||||
|  |         website={website} | ||||||
|  |       /> | ||||||
|  |       <br /> | ||||||
|  |       <h2 className='text-center username'>@{username}</h2> | ||||||
|  |       {name && <p className='text-center name'>{name}</p>} | ||||||
|  |       {location && <p className='text-center location'>{location}</p>} | ||||||
|  |       {about && <p className='bio text-center'>{about}</p>} | ||||||
|  |       {typeof points === 'number' ? ( | ||||||
|  |         <p className='text-center points'> | ||||||
|  |           {`${points} ${pluralise('point', points !== 1)}`} | ||||||
|  |         </p> | ||||||
|  |       ) : null} | ||||||
|  |       {yearsTopContributor.filter(Boolean).length > 0 && ( | ||||||
|  |         <div> | ||||||
|  |           <br /> | ||||||
|  |           <p className='text-center yearsTopContributor'> | ||||||
|  |             <FontAwesomeIcon icon={faAward} /> Top Contributor | ||||||
|  |           </p> | ||||||
|  |           <p className='text-center'>{joinArray(yearsTopContributor)}</p> | ||||||
|  |         </div> | ||||||
|  |       )} | ||||||
|  |       <br /> | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Camper.displayName = 'Camper'; | ||||||
|  | Camper.propTypes = propTypes; | ||||||
|  |  | ||||||
|  | export default Camper; | ||||||
							
								
								
									
										165
									
								
								client/src/components/profile/components/Certifications.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								client/src/components/profile/components/Certifications.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,165 @@ | |||||||
|  | import React from 'react'; | ||||||
|  | import PropTypes from 'prop-types'; | ||||||
|  | import { curry } from 'lodash'; | ||||||
|  | import { createSelector } from 'reselect'; | ||||||
|  | import { connect } from 'react-redux'; | ||||||
|  | import { Button, Row, Col } from '@freecodecamp/react-bootstrap'; | ||||||
|  |  | ||||||
|  | import { userByNameSelector } from '../../../redux'; | ||||||
|  | import FullWidthRow from '../../helpers/FullWidthRow'; | ||||||
|  |  | ||||||
|  | const mapStateToProps = (state, props) => | ||||||
|  |   createSelector( | ||||||
|  |     userByNameSelector(props.username), | ||||||
|  |     ({ | ||||||
|  |       isRespWebDesignCert, | ||||||
|  |       is2018DataVisCert, | ||||||
|  |       isFrontEndLibsCert, | ||||||
|  |       isJsAlgoDataStructCert, | ||||||
|  |       isApisMicroservicesCert, | ||||||
|  |       isInfosecQaCert, | ||||||
|  |       isFrontEndCert, | ||||||
|  |       isBackEndCert, | ||||||
|  |       isDataVisCert, | ||||||
|  |       isFullStackCert | ||||||
|  |     }) => ({ | ||||||
|  |       hasModernCert: | ||||||
|  |         isRespWebDesignCert || | ||||||
|  |         is2018DataVisCert || | ||||||
|  |         isFrontEndLibsCert || | ||||||
|  |         isJsAlgoDataStructCert || | ||||||
|  |         isApisMicroservicesCert || | ||||||
|  |         isInfosecQaCert || | ||||||
|  |         isFullStackCert, | ||||||
|  |       hasLegacyCert: isFrontEndCert || isBackEndCert || isDataVisCert, | ||||||
|  |       currentCerts: [ | ||||||
|  |         { | ||||||
|  |           show: isFullStackCert, | ||||||
|  |           title: 'Full Stack Certification', | ||||||
|  |           showURL: 'full-stack' | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           show: isRespWebDesignCert, | ||||||
|  |           title: 'Responsive Web Design Certification', | ||||||
|  |           showURL: 'responsive-web-design' | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           show: isJsAlgoDataStructCert, | ||||||
|  |           title: 'JavaScript Algorithms and Data Structures Certification', | ||||||
|  |           showURL: 'javascript-algorithms-and-data-structures' | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           show: isFrontEndLibsCert, | ||||||
|  |           title: 'Front End Libraries Certification', | ||||||
|  |           showURL: 'front-end-libraries' | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           show: is2018DataVisCert, | ||||||
|  |           title: 'Data Visualization Certification', | ||||||
|  |           showURL: 'data-visualization' | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           show: isApisMicroservicesCert, | ||||||
|  |           title: 'APIs and Microservices Certification', | ||||||
|  |           showURL: 'apis-and-microservices' | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           show: isInfosecQaCert, | ||||||
|  |           title: 'Information Security and Quality Assurance Certification', | ||||||
|  |           showURL: 'information-security-and-quality-assurance' | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       legacyCerts: [ | ||||||
|  |         { | ||||||
|  |           show: isFullStackCert, | ||||||
|  |           title: 'Full Stack Certification', | ||||||
|  |           showURL: 'legacy-full-stack' | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           show: isFrontEndCert, | ||||||
|  |           title: 'Front End Certification', | ||||||
|  |           showURL: 'legacy-front-end' | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           show: isBackEndCert, | ||||||
|  |           title: 'Back End Certification', | ||||||
|  |           showURL: 'legacy-back-end' | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           show: isDataVisCert, | ||||||
|  |           title: 'Data Visualization Certification', | ||||||
|  |           showURL: 'legacy-data-visualization' | ||||||
|  |         } | ||||||
|  |       ] | ||||||
|  |     }) | ||||||
|  |   )(state, props); | ||||||
|  |  | ||||||
|  | const certArrayTypes = PropTypes.arrayOf( | ||||||
|  |   PropTypes.shape({ | ||||||
|  |     show: PropTypes.bool, | ||||||
|  |     title: PropTypes.string, | ||||||
|  |     showURL: PropTypes.string | ||||||
|  |   }) | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | const propTypes = { | ||||||
|  |   currentCerts: certArrayTypes, | ||||||
|  |   hasLegacyCert: PropTypes.bool, | ||||||
|  |   hasModernCert: PropTypes.bool, | ||||||
|  |   legacyCerts: certArrayTypes, | ||||||
|  |   username: PropTypes.string | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | function renderCertShow(username, cert) { | ||||||
|  |   return cert.show ? ( | ||||||
|  |     <Row key={cert.showURL}> | ||||||
|  |       <Col sm={10} smPush={1}> | ||||||
|  |         <Button | ||||||
|  |           block={true} | ||||||
|  |           bsSize='lg' | ||||||
|  |           bsStyle='primary' | ||||||
|  |           href={`/certification/${username}/${cert.showURL}`} | ||||||
|  |           > | ||||||
|  |           View {cert.title} | ||||||
|  |         </Button> | ||||||
|  |       </Col> | ||||||
|  |     </Row> | ||||||
|  |   ) : null; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function Certificates({ | ||||||
|  |   currentCerts, | ||||||
|  |   legacyCerts, | ||||||
|  |   hasLegacyCert, | ||||||
|  |   hasModernCert, | ||||||
|  |   username | ||||||
|  | }) { | ||||||
|  |   const renderCertShowWithUsername = curry(renderCertShow)(username); | ||||||
|  |   return ( | ||||||
|  |       <FullWidthRow> | ||||||
|  |         <h2 className='text-center'>freeCodeCamp Certifications</h2> | ||||||
|  |         <br /> | ||||||
|  |         {hasModernCert ? ( | ||||||
|  |           currentCerts.map(renderCertShowWithUsername) | ||||||
|  |         ) : ( | ||||||
|  |           <p className='text-center'> | ||||||
|  |             No certifications have been earned under the current curriculum | ||||||
|  |           </p> | ||||||
|  |         )} | ||||||
|  |         {hasLegacyCert ? ( | ||||||
|  |           <div> | ||||||
|  |             <br /> | ||||||
|  |             <h3 className='text-center'>Legacy Certifications</h3> | ||||||
|  |             <br /> | ||||||
|  |             {legacyCerts.map(renderCertShowWithUsername)} | ||||||
|  |           </div> | ||||||
|  |         ) : null} | ||||||
|  |         <hr /> | ||||||
|  |       </FullWidthRow> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Certificates.propTypes = propTypes; | ||||||
|  | Certificates.displayName = 'Certificates'; | ||||||
|  |  | ||||||
|  | export default connect(mapStateToProps)(Certificates); | ||||||
							
								
								
									
										60
									
								
								client/src/components/profile/components/HeatMap.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								client/src/components/profile/components/HeatMap.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | |||||||
|  | import React, { Component } from 'react'; | ||||||
|  | import PropTypes from 'prop-types'; | ||||||
|  | import { Helmet } from 'react-helmet'; | ||||||
|  |  | ||||||
|  | import FullWidthRow from '../../helpers/FullWidthRow'; | ||||||
|  |  | ||||||
|  | import './heatmap.css'; | ||||||
|  |  | ||||||
|  | const propTypes = { | ||||||
|  |   calendar: PropTypes.object, | ||||||
|  |   streak: PropTypes.shape({ | ||||||
|  |     current: PropTypes.number, | ||||||
|  |     longest: PropTypes.number | ||||||
|  |   }) | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class HeatMap extends Component { | ||||||
|  |   constructor(props) { | ||||||
|  |     super(props); | ||||||
|  |  | ||||||
|  |     this.renderMap = this.renderMap.bind(this); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   render() { | ||||||
|  |     const { streak = {} } = this.props; | ||||||
|  |     return ( | ||||||
|  |       <FullWidthRow id='cal-heatmap-container'> | ||||||
|  |         <Helmet> | ||||||
|  |           <script | ||||||
|  |             src='https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js' | ||||||
|  |             type='text/javascript' | ||||||
|  |           /> | ||||||
|  |           <link href='/css/cal-heatmap.css' rel='stylesheet' /> | ||||||
|  |         </Helmet> | ||||||
|  |         <FullWidthRow> | ||||||
|  |           <h3>This needs a refactor to use something like</h3> | ||||||
|  |           <a href='https://www.npmjs.com/package/react-calendar-heatmap'> | ||||||
|  |             react-calendar-heatmap | ||||||
|  |           </a> | ||||||
|  |         </FullWidthRow> | ||||||
|  |         <FullWidthRow> | ||||||
|  |           <div className='streak-container'> | ||||||
|  |             <span className='streak'> | ||||||
|  |               <strong>Longest Streak:</strong> {streak.longest || 1} | ||||||
|  |             </span> | ||||||
|  |             <span className='streak'> | ||||||
|  |               <strong>Current Streak:</strong> {streak.current || 1} | ||||||
|  |             </span> | ||||||
|  |           </div> | ||||||
|  |         </FullWidthRow> | ||||||
|  |         <hr /> | ||||||
|  |       </FullWidthRow> | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | HeatMap.displayName = 'HeatMap'; | ||||||
|  | HeatMap.propTypes = propTypes; | ||||||
|  |  | ||||||
|  | export default HeatMap; | ||||||
							
								
								
									
										59
									
								
								client/src/components/profile/components/Portfolio.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								client/src/components/profile/components/Portfolio.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | |||||||
|  | import React from 'react'; | ||||||
|  | import PropTypes from 'prop-types'; | ||||||
|  | import { Thumbnail, Media } from '@freecodecamp/react-bootstrap'; | ||||||
|  |  | ||||||
|  | import { FullWidthRow } from '../../helpers'; | ||||||
|  |  | ||||||
|  | const propTypes = { | ||||||
|  |   portfolio: PropTypes.arrayOf( | ||||||
|  |     PropTypes.shape({ | ||||||
|  |       description: PropTypes.string, | ||||||
|  |       id: PropTypes.string, | ||||||
|  |       image: PropTypes.string, | ||||||
|  |       title: PropTypes.string, | ||||||
|  |       url: PropTypes.string | ||||||
|  |     }) | ||||||
|  |   ) | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | function Portfolio({ portfolio = [] }) { | ||||||
|  |   if (!portfolio.length) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |   return ( | ||||||
|  |     <div> | ||||||
|  |       <FullWidthRow> | ||||||
|  |         <h2 className='text-center'>Portfolio</h2> | ||||||
|  |         {portfolio.map(({ title, url, image, description, id }) => ( | ||||||
|  |           <Media key={id}> | ||||||
|  |             <Media.Left align='middle'> | ||||||
|  |               {image && ( | ||||||
|  |                 <a href={url} rel='nofollow'> | ||||||
|  |                   <Thumbnail | ||||||
|  |                     alt={`A screen shot of ${title}`} | ||||||
|  |                     src={image} | ||||||
|  |                     style={{ width: '150px' }} | ||||||
|  |                   /> | ||||||
|  |                 </a> | ||||||
|  |               )} | ||||||
|  |             </Media.Left> | ||||||
|  |             <Media.Body> | ||||||
|  |               <Media.Heading> | ||||||
|  |                 <a href={url} rel='nofollow'> | ||||||
|  |                   {title} | ||||||
|  |                 </a> | ||||||
|  |               </Media.Heading> | ||||||
|  |               <p>{description}</p> | ||||||
|  |             </Media.Body> | ||||||
|  |           </Media> | ||||||
|  |         ))} | ||||||
|  |       </FullWidthRow> | ||||||
|  |       <hr /> | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Portfolio.displayName = 'Portfolio'; | ||||||
|  | Portfolio.propTypes = propTypes; | ||||||
|  |  | ||||||
|  | export default Portfolio; | ||||||
							
								
								
									
										90
									
								
								client/src/components/profile/components/SocialIcons.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								client/src/components/profile/components/SocialIcons.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | |||||||
|  | import React from 'react'; | ||||||
|  | import PropTypes from 'prop-types'; | ||||||
|  | import { Row, Col } from '@freecodecamp/react-bootstrap'; | ||||||
|  | import FontAwesomeIcon from '@fortawesome/react-fontawesome'; | ||||||
|  | import { | ||||||
|  |   faLinkedin, | ||||||
|  |   faGithub, | ||||||
|  |   faTwitter | ||||||
|  | } from '@fortawesome/free-brands-svg-icons'; | ||||||
|  | import { faLink } from '@fortawesome/free-solid-svg-icons'; | ||||||
|  |  | ||||||
|  | import './social-icons.css'; | ||||||
|  |  | ||||||
|  | const propTypes = { | ||||||
|  |   email: PropTypes.string, | ||||||
|  |   githubProfile: PropTypes.string, | ||||||
|  |   isGithub: PropTypes.bool, | ||||||
|  |   isLinkedIn: PropTypes.bool, | ||||||
|  |   isTwitter: PropTypes.bool, | ||||||
|  |   isWebsite: PropTypes.bool, | ||||||
|  |   linkedin: PropTypes.string, | ||||||
|  |   show: PropTypes.bool, | ||||||
|  |   twitter: PropTypes.string, | ||||||
|  |   website: PropTypes.string | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | function LinkedInIcon(linkedIn) { | ||||||
|  |   return ( | ||||||
|  |     <a href={linkedIn} rel='noopener noreferrer' target='_blank'> | ||||||
|  |       <FontAwesomeIcon icon={faLinkedin} size='2x' /> | ||||||
|  |     </a> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function GithubIcon(ghURL) { | ||||||
|  |   return ( | ||||||
|  |     <a href={ghURL} rel='noopener noreferrer' target='_blank'> | ||||||
|  |       <FontAwesomeIcon icon={faGithub} size='2x' /> | ||||||
|  |     </a> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function WebsiteIcon(website) { | ||||||
|  |   return ( | ||||||
|  |     <a href={website} rel='noopener noreferrer' target='_blank'> | ||||||
|  |       <FontAwesomeIcon icon={faLink} size='2x' /> | ||||||
|  |     </a> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function TwitterIcon(handle) { | ||||||
|  |   return ( | ||||||
|  |     <a href={handle} rel='noopener noreferrer' target='_blank'> | ||||||
|  |       <FontAwesomeIcon icon={faTwitter} size='2x' /> | ||||||
|  |     </a> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function SocialIcons(props) { | ||||||
|  |   const { | ||||||
|  |     githubProfile, | ||||||
|  |     isLinkedIn, | ||||||
|  |     isGithub, | ||||||
|  |     isTwitter, | ||||||
|  |     isWebsite, | ||||||
|  |     linkedin, | ||||||
|  |     twitter, | ||||||
|  |     website | ||||||
|  |   } = props; | ||||||
|  |   const show = isLinkedIn || isGithub || isTwitter || isWebsite; | ||||||
|  |   if (!show) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Row> | ||||||
|  |       <Col className='text-center social-media-icons' sm={6} smOffset={3}> | ||||||
|  |         {isLinkedIn ? LinkedInIcon(linkedin) : null} | ||||||
|  |         {isGithub ? GithubIcon(githubProfile) : null} | ||||||
|  |         {isWebsite ? WebsiteIcon(website) : null} | ||||||
|  |         {isTwitter ? TwitterIcon(twitter) : null} | ||||||
|  |       </Col> | ||||||
|  |     </Row> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | SocialIcons.displayName = 'SocialIcons'; | ||||||
|  | SocialIcons.propTypes = propTypes; | ||||||
|  |  | ||||||
|  | export default SocialIcons; | ||||||
							
								
								
									
										170
									
								
								client/src/components/profile/components/TimeLine.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								client/src/components/profile/components/TimeLine.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,170 @@ | |||||||
|  | import React, { Component } from 'react'; | ||||||
|  | import PropTypes from 'prop-types'; | ||||||
|  | import { createSelector } from 'reselect'; | ||||||
|  | import { bindActionCreators } from 'redux'; | ||||||
|  | import { connect } from 'react-redux'; | ||||||
|  | import format from 'date-fns/format'; | ||||||
|  | import { find, reverse, sortBy, isEmpty } from 'lodash'; | ||||||
|  | import { Button, Modal, Table } from '@freecodecamp/react-bootstrap'; | ||||||
|  | import { Link } from 'gatsby'; | ||||||
|  |  | ||||||
|  | import { | ||||||
|  |   challengeIdToNameMapSelector, | ||||||
|  |   fetchIdToNameMap | ||||||
|  | } from '../../../templates/Challenges/redux'; | ||||||
|  | import { blockNameify } from '../../../../utils/blockNameify'; | ||||||
|  | import { FullWidthRow } from '../../helpers'; | ||||||
|  | import SolutionViewer from '../../settings/SolutionViewer'; | ||||||
|  |  | ||||||
|  | const mapStateToProps = createSelector( | ||||||
|  |   challengeIdToNameMapSelector, | ||||||
|  |   idToNameMap => ({ | ||||||
|  |     idToNameMap | ||||||
|  |   }) | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | const mapDispatchToProps = dispatch => | ||||||
|  |   bindActionCreators({ fetchIdToNameMap }, dispatch); | ||||||
|  |  | ||||||
|  | const propTypes = { | ||||||
|  |   completedMap: PropTypes.arrayOf( | ||||||
|  |     PropTypes.shape({ | ||||||
|  |       id: PropTypes.string, | ||||||
|  |       completedDate: PropTypes.number, | ||||||
|  |       challengeType: PropTypes.number, | ||||||
|  |       solution: PropTypes.string, | ||||||
|  |       files: PropTypes.shape({ | ||||||
|  |         ext: PropTypes.string, | ||||||
|  |         contents: PropTypes.string | ||||||
|  |       }) | ||||||
|  |     }) | ||||||
|  |   ), | ||||||
|  |   fetchIdToNameMap: PropTypes.func.isRequired, | ||||||
|  |   idToNameMap: PropTypes.objectOf(PropTypes.string), | ||||||
|  |   username: PropTypes.string | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class Timeline extends Component { | ||||||
|  |   constructor(props) { | ||||||
|  |     super(props); | ||||||
|  |  | ||||||
|  |     this.state = { | ||||||
|  |       solutionToView: null, | ||||||
|  |       solutionOpen: false | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     this.closeSolution = this.closeSolution.bind(this); | ||||||
|  |     this.renderCompletion = this.renderCompletion.bind(this); | ||||||
|  |     this.viewSolution = this.viewSolution.bind(this); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   componentDidMount() { | ||||||
|  |     if (isEmpty(this.props.idToNameMap)) { | ||||||
|  |       return this.props.fetchIdToNameMap(); | ||||||
|  |     } | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   renderCompletion(completed) { | ||||||
|  |     const { idToNameMap } = this.props; | ||||||
|  |     const { id, completedDate } = completed; | ||||||
|  |     const challengeDashedName = idToNameMap[id]; | ||||||
|  |     return ( | ||||||
|  |       <tr key={id}> | ||||||
|  |         <td> | ||||||
|  |           <a href={`/challenges/${challengeDashedName}`}> | ||||||
|  |             {blockNameify(challengeDashedName)} | ||||||
|  |           </a> | ||||||
|  |         </td> | ||||||
|  |         <td className='text-center'> | ||||||
|  |           <time dateTime={format(completedDate, 'YYYY-MM-DDTHH:MM:SSZ')}> | ||||||
|  |             {format(completedDate, 'MMMM D, YYYY')} | ||||||
|  |           </time> | ||||||
|  |         </td> | ||||||
|  |         <td /> | ||||||
|  |       </tr> | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |   viewSolution(id) { | ||||||
|  |     this.setState(state => ({ | ||||||
|  |       ...state, | ||||||
|  |       solutionToView: id, | ||||||
|  |       solutionOpen: true | ||||||
|  |     })); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   closeSolution() { | ||||||
|  |     this.setState(state => ({ | ||||||
|  |       ...state, | ||||||
|  |       solutionToView: null, | ||||||
|  |       solutionOpen: false | ||||||
|  |     })); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   render() { | ||||||
|  |     const { completedMap, idToNameMap, username } = this.props; | ||||||
|  |     const { solutionToView: id, solutionOpen } = this.state; | ||||||
|  |     if (isEmpty(idToNameMap)) { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |     return ( | ||||||
|  |       <FullWidthRow> | ||||||
|  |         <h2 className='text-center'>Timeline</h2> | ||||||
|  |         {completedMap.length === 0 ? ( | ||||||
|  |           <p className='text-center'> | ||||||
|  |             No challenges have been completed yet.  | ||||||
|  |             <Link to='/learn'>Get started here.</Link> | ||||||
|  |           </p> | ||||||
|  |         ) : ( | ||||||
|  |           <Table condensed={true} striped={true}> | ||||||
|  |             <thead> | ||||||
|  |               <tr> | ||||||
|  |                 <th>Challenge</th> | ||||||
|  |                 <th className='text-center'>Completed</th> | ||||||
|  |               </tr> | ||||||
|  |             </thead> | ||||||
|  |             <tbody> | ||||||
|  |               {reverse( | ||||||
|  |                 sortBy(completedMap, ['completedDate']).filter( | ||||||
|  |                   ({ id }) => id in idToNameMap | ||||||
|  |                 ) | ||||||
|  |               ).map(this.renderCompletion)} | ||||||
|  |             </tbody> | ||||||
|  |           </Table> | ||||||
|  |         )} | ||||||
|  |         {id && ( | ||||||
|  |           <Modal | ||||||
|  |             aria-labelledby='contained-modal-title' | ||||||
|  |             onHide={this.closeSolution} | ||||||
|  |             show={solutionOpen} | ||||||
|  |             > | ||||||
|  |             <Modal.Header closeButton={true}> | ||||||
|  |               <Modal.Title id='contained-modal-title'> | ||||||
|  |                 {`${username}'s Solution to ${blockNameify(idToNameMap[id])}`} | ||||||
|  |               </Modal.Title> | ||||||
|  |             </Modal.Header> | ||||||
|  |             <Modal.Body> | ||||||
|  |               <SolutionViewer | ||||||
|  |                 solution={find( | ||||||
|  |                   completedMap, | ||||||
|  |                   ({ id: completedId }) => completedId === id | ||||||
|  |                 )} | ||||||
|  |               /> | ||||||
|  |             </Modal.Body> | ||||||
|  |             <Modal.Footer> | ||||||
|  |               <Button onClick={this.closeSolution}>Close</Button> | ||||||
|  |             </Modal.Footer> | ||||||
|  |           </Modal> | ||||||
|  |         )} | ||||||
|  |       </FullWidthRow> | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Timeline.displayName = 'Timeline'; | ||||||
|  | Timeline.propTypes = propTypes; | ||||||
|  |  | ||||||
|  | export default connect( | ||||||
|  |   mapStateToProps, | ||||||
|  |   mapDispatchToProps | ||||||
|  | )(Timeline); | ||||||
							
								
								
									
										8
									
								
								client/src/components/profile/components/camper.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								client/src/components/profile/components/camper.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | .avatar-container .avatar { | ||||||
|  |   height: 180px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .avatar-container { | ||||||
|  |   display: flex; | ||||||
|  |   justify-content: center; | ||||||
|  | } | ||||||
							
								
								
									
										15
									
								
								client/src/components/profile/components/heatmap.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								client/src/components/profile/components/heatmap.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | .streak-container { | ||||||
|  |   display: flex; | ||||||
|  |   justify-content: space-around; | ||||||
|  |   align-items: center; | ||||||
|  |   font-size: 18px; | ||||||
|  |   color: #006400; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .streak strong { | ||||||
|  |   color: #333; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .night .streak strong { | ||||||
|  |   color: #ccc; | ||||||
|  | } | ||||||
| @@ -0,0 +1,7 @@ | |||||||
|  | .social-media-icons > a { | ||||||
|  |   margin: 0 10px 0 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .social-media-icons > a:last-child { | ||||||
|  |   margin: 0; | ||||||
|  | } | ||||||
| @@ -1,64 +1,17 @@ | |||||||
| import React from 'react'; | import React from 'react'; | ||||||
| import Helmet from 'react-helmet'; | import { Router } from '@reach/router'; | ||||||
| import Spinner from 'react-spinkit'; | // eslint-disable-next-line  max-len | ||||||
| import { Link } from 'gatsby'; | import ShowProfileOrFourOhFour from '../client-only-routes/ShowProfileOrFourOhFour'; | ||||||
|  |  | ||||||
| import Layout from '../components/layouts/Default'; | function FourOhFourPage() { | ||||||
|  |   return ( | ||||||
| import notFoundLogo from '../images/freeCodeCamp-404.svg'; |     <Router> | ||||||
| import { quotes } from '../resources/quotes.json'; |       <ShowProfileOrFourOhFour path='/:maybeUser/:splat' /> | ||||||
|  |       <ShowProfileOrFourOhFour path='/:maybeUser' /> | ||||||
| import './404.css'; |     </Router> | ||||||
|  |   ); | ||||||
| class NotFoundPage extends React.Component { |  | ||||||
|   constructor(props) { |  | ||||||
|     super(props); |  | ||||||
|     this.state = { |  | ||||||
|       randomQuote: null |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   componentDidMount() { |  | ||||||
|     this.updateQuote(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   updateQuote() { |  | ||||||
|     this.setState({ |  | ||||||
|       randomQuote: quotes[Math.floor(Math.random() * quotes.length)] |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   render() { |  | ||||||
|     return ( |  | ||||||
|       <Layout> |  | ||||||
|         <div className='notfound-page-wrapper'> |  | ||||||
|           <Helmet title='Page Not Found | freeCodeCamp' /> |  | ||||||
|           <img alt='404 Not Found' src={notFoundLogo} /> |  | ||||||
|           <h1>NOT FOUND</h1> |  | ||||||
|           {this.state.randomQuote ? ( |  | ||||||
|             <div> |  | ||||||
|               <p> |  | ||||||
|                 We couldn't find what you were looking for, but here is a |  | ||||||
|                 quote: |  | ||||||
|               </p> |  | ||||||
|               <div className='quote-wrapper'> |  | ||||||
|                 <p className='quote'> |  | ||||||
|                   <span>“</span> |  | ||||||
|                   {this.state.randomQuote.quote} |  | ||||||
|                 </p> |  | ||||||
|                 <p className='author'>- {this.state.randomQuote.author}</p> |  | ||||||
|               </div> |  | ||||||
|             </div> |  | ||||||
|           ) : ( |  | ||||||
|             <Spinner color='#006400' name='ball-clip-rotate-multiple' /> |  | ||||||
|           )} |  | ||||||
|           <Link className='btn-curriculum' to='/'> |  | ||||||
|             View the Curriculum |  | ||||||
|           </Link> |  | ||||||
|         </div> |  | ||||||
|       </Layout> |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| export default NotFoundPage; | FourOhFourPage.displayName = 'FourOhFourPage'; | ||||||
|  |  | ||||||
|  | export default FourOhFourPage; | ||||||
|   | |||||||
| @@ -1,7 +1,12 @@ | |||||||
| import { call, put, takeEvery } from 'redux-saga/effects'; | import { call, put, takeEvery } from 'redux-saga/effects'; | ||||||
|  |  | ||||||
| import { fetchUserComplete, fetchUserError } from './'; | import { | ||||||
| import { getSessionUser } from '../utils/ajax'; |   fetchUserComplete, | ||||||
|  |   fetchUserError, | ||||||
|  |   fetchProfileForUserError, | ||||||
|  |   fetchProfileForUserComplete | ||||||
|  | } from './'; | ||||||
|  | import { getSessionUser, getUserProfile } from '../utils/ajax'; | ||||||
| import { jwt } from './cookieValues'; | import { jwt } from './cookieValues'; | ||||||
|  |  | ||||||
| function* fetchSessionUser() { | function* fetchSessionUser() { | ||||||
| @@ -13,13 +18,30 @@ function* fetchSessionUser() { | |||||||
|     const { |     const { | ||||||
|       data: { user = {}, result = '' } |       data: { user = {}, result = '' } | ||||||
|     } = yield call(getSessionUser); |     } = yield call(getSessionUser); | ||||||
|     const appUser = user[result]; |     const appUser = user[result] || {}; | ||||||
|     yield put(fetchUserComplete({ user: appUser, username: result })); |     yield put(fetchUserComplete({ user: appUser, username: result })); | ||||||
|   } catch (e) { |   } catch (e) { | ||||||
|     yield put(fetchUserError(e)); |     yield put(fetchUserError(e)); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| export function createFetchUserSaga(types) { | function* fetchOtherUser({ payload: maybeUser }) { | ||||||
|   return [takeEvery(types.fetchUser, fetchSessionUser)]; |   try { | ||||||
|  |     const { data } = yield call(getUserProfile, maybeUser); | ||||||
|  |  | ||||||
|  |     const { entities: { user = {} } = {}, result = '' } = data; | ||||||
|  |     const otherUser = user[result] || {}; | ||||||
|  |     yield put( | ||||||
|  |       fetchProfileForUserComplete({ user: otherUser, username: result }) | ||||||
|  |     ); | ||||||
|  |   } catch (e) { | ||||||
|  |     yield put(fetchProfileForUserError(e)); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function createFetchUserSaga(types) { | ||||||
|  |   return [ | ||||||
|  |     takeEvery(types.fetchUser, fetchSessionUser), | ||||||
|  |     takeEvery(types.fetchProfileForUser, fetchOtherUser) | ||||||
|  |   ]; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -39,6 +39,9 @@ const initialState = { | |||||||
|   userFetchState: { |   userFetchState: { | ||||||
|     ...defaultFetchState |     ...defaultFetchState | ||||||
|   }, |   }, | ||||||
|  |   userProfileFetchState: { | ||||||
|  |     ...defaultFetchState | ||||||
|  |   }, | ||||||
|   showDonationModal: false, |   showDonationModal: false, | ||||||
|   isOnline: true |   isOnline: true | ||||||
| }; | }; | ||||||
| @@ -53,6 +56,7 @@ export const types = createTypes( | |||||||
|     'updateComplete', |     'updateComplete', | ||||||
|     'updateFailed', |     'updateFailed', | ||||||
|     ...createAsyncTypes('fetchUser'), |     ...createAsyncTypes('fetchUser'), | ||||||
|  |     ...createAsyncTypes('fetchProfileForUser'), | ||||||
|     ...createAsyncTypes('acceptTerms'), |     ...createAsyncTypes('acceptTerms'), | ||||||
|     ...createAsyncTypes('showCert'), |     ...createAsyncTypes('showCert'), | ||||||
|     ...createAsyncTypes('reportUser') |     ...createAsyncTypes('reportUser') | ||||||
| @@ -60,11 +64,7 @@ export const types = createTypes( | |||||||
|   ns |   ns | ||||||
| ); | ); | ||||||
|  |  | ||||||
| export const epics = [ | export const epics = [hardGoToEpic, failedUpdatesEpic, updateCompleteEpic]; | ||||||
|   hardGoToEpic, |  | ||||||
|   failedUpdatesEpic, |  | ||||||
|   updateCompleteEpic |  | ||||||
| ]; |  | ||||||
|  |  | ||||||
| export const sagas = [ | export const sagas = [ | ||||||
|   ...createAcceptTermsSaga(types), |   ...createAcceptTermsSaga(types), | ||||||
| @@ -98,6 +98,14 @@ export const fetchUser = createAction(types.fetchUser); | |||||||
| export const fetchUserComplete = createAction(types.fetchUserComplete); | export const fetchUserComplete = createAction(types.fetchUserComplete); | ||||||
| export const fetchUserError = createAction(types.fetchUserError); | export const fetchUserError = createAction(types.fetchUserError); | ||||||
|  |  | ||||||
|  | export const fetchProfileForUser = createAction(types.fetchProfileForUser); | ||||||
|  | export const fetchProfileForUserComplete = createAction( | ||||||
|  |   types.fetchProfileForUserComplete | ||||||
|  | ); | ||||||
|  | export const fetchProfileForUserError = createAction( | ||||||
|  |   types.fetchProfileForUserError | ||||||
|  | ); | ||||||
|  |  | ||||||
| export const reportUser = createAction(types.reportUser); | export const reportUser = createAction(types.reportUser); | ||||||
| export const reportUserComplete = createAction(types.reportUserComplete); | export const reportUserComplete = createAction(types.reportUserComplete); | ||||||
| export const reportUserError = createAction(types.reportUserError); | export const reportUserError = createAction(types.reportUserError); | ||||||
| @@ -144,6 +152,8 @@ export const userByNameSelector = username => state => { | |||||||
|   return username in user ? user[username] : {}; |   return username in user ? user[username] : {}; | ||||||
| }; | }; | ||||||
| export const userFetchStateSelector = state => state[ns].userFetchState; | export const userFetchStateSelector = state => state[ns].userFetchState; | ||||||
|  | export const userProfileFetchStateSelector = state => | ||||||
|  |   state[ns].userProfileFetchState; | ||||||
| export const usernameSelector = state => state[ns].appUsername; | export const usernameSelector = state => state[ns].appUsername; | ||||||
| export const userSelector = state => { | export const userSelector = state => { | ||||||
|   const username = usernameSelector(state); |   const username = usernameSelector(state); | ||||||
| @@ -170,11 +180,15 @@ export const reducer = handleActions( | |||||||
|       ...state, |       ...state, | ||||||
|       userFetchState: { ...defaultFetchState } |       userFetchState: { ...defaultFetchState } | ||||||
|     }), |     }), | ||||||
|  |     [types.fetchProfileForUser]: state => ({ | ||||||
|  |       ...state, | ||||||
|  |       userProfileFetchState: { ...defaultFetchState } | ||||||
|  |     }), | ||||||
|     [types.fetchUserComplete]: (state, { payload: { user, username } }) => ({ |     [types.fetchUserComplete]: (state, { payload: { user, username } }) => ({ | ||||||
|       ...state, |       ...state, | ||||||
|       user: { |       user: { | ||||||
|         ...state.user, |         ...state.user, | ||||||
|         [username]: user |         [username]: { ...user, sessionUser: true } | ||||||
|       }, |       }, | ||||||
|       appUsername: username, |       appUsername: username, | ||||||
|       userFetchState: { |       userFetchState: { | ||||||
| @@ -193,6 +207,35 @@ export const reducer = handleActions( | |||||||
|         error: payload |         error: payload | ||||||
|       } |       } | ||||||
|     }), |     }), | ||||||
|  |     [types.fetchProfileForUserComplete]: ( | ||||||
|  |       state, | ||||||
|  |       { payload: { user, username } } | ||||||
|  |     ) => { | ||||||
|  |       const previousUserObject = | ||||||
|  |         username in state.user ? state.user[username] : {}; | ||||||
|  |       return { | ||||||
|  |         ...state, | ||||||
|  |         user: { | ||||||
|  |           ...state.user, | ||||||
|  |           [username]: { ...previousUserObject, ...user } | ||||||
|  |         }, | ||||||
|  |         userProfileFetchState: { | ||||||
|  |           pending: false, | ||||||
|  |           complete: true, | ||||||
|  |           errored: false, | ||||||
|  |           error: null | ||||||
|  |         } | ||||||
|  |       }; | ||||||
|  |     }, | ||||||
|  |     [types.fetchProfileForUserError]: (state, { payload }) => ({ | ||||||
|  |       ...state, | ||||||
|  |       userFetchState: { | ||||||
|  |         pending: false, | ||||||
|  |         complete: false, | ||||||
|  |         errored: true, | ||||||
|  |         error: payload | ||||||
|  |       } | ||||||
|  |     }), | ||||||
|     [types.onlineStatusChange]: (state, { payload: isOnline }) => ({ |     [types.onlineStatusChange]: (state, { payload: isOnline }) => ({ | ||||||
|       ...state, |       ...state, | ||||||
|       isOnline |       isOnline | ||||||
|   | |||||||
| @@ -2,7 +2,8 @@ import { all } from 'redux-saga/effects'; | |||||||
|  |  | ||||||
| import { sagas as appSagas } from './'; | import { sagas as appSagas } from './'; | ||||||
| import { sagas as settingsSagas } from './settings'; | import { sagas as settingsSagas } from './settings'; | ||||||
|  | import { sagas as challengeSagas } from '../templates/Challenges/redux'; | ||||||
|  |  | ||||||
| export default function* rootSaga() { | export default function* rootSaga() { | ||||||
|   yield all([...appSagas, ...settingsSagas]); |   yield all([...appSagas, ...challengeSagas, ...settingsSagas]); | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										22
									
								
								client/src/templates/Challenges/redux/id-to-name-map-saga.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								client/src/templates/Challenges/redux/id-to-name-map-saga.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | import { call, put, takeEvery } from 'redux-saga/effects'; | ||||||
|  |  | ||||||
|  | import { getIdToNameMap } from '../../../utils/ajax'; | ||||||
|  | import { fetchIdToNameMapComplete, fetchIdToNameMapError } from './'; | ||||||
|  |  | ||||||
|  | function* fetchIdToNameMapSaga() { | ||||||
|  |   try { | ||||||
|  |     const { data } = yield call(getIdToNameMap); | ||||||
|  |  | ||||||
|  |     yield put( | ||||||
|  |       fetchIdToNameMapComplete(data) | ||||||
|  |     ); | ||||||
|  |   } catch (e) { | ||||||
|  |     yield put(fetchIdToNameMapError(e)); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function createIdToNameMapSaga(types) { | ||||||
|  |   return [ | ||||||
|  |     takeEvery(types.fetchIdToNameMap, fetchIdToNameMapSaga) | ||||||
|  |   ]; | ||||||
|  | } | ||||||
| @@ -2,6 +2,8 @@ import { createAction, handleActions } from 'redux-actions'; | |||||||
| import { reducer as reduxFormReducer } from 'redux-form'; | import { reducer as reduxFormReducer } from 'redux-form'; | ||||||
|  |  | ||||||
| import { createTypes } from '../../../../utils/stateManagement'; | import { createTypes } from '../../../../utils/stateManagement'; | ||||||
|  | import { createAsyncTypes } from '../../../utils/createTypes'; | ||||||
|  |  | ||||||
| import { createPoly } from '../utils/polyvinyl'; | import { createPoly } from '../utils/polyvinyl'; | ||||||
| import challengeModalEpic from './challenge-modal-epic'; | import challengeModalEpic from './challenge-modal-epic'; | ||||||
| import completionEpic from './completion-epic'; | import completionEpic from './completion-epic'; | ||||||
| @@ -11,11 +13,14 @@ import createQuestionEpic from './create-question-epic'; | |||||||
| import codeStorageEpic from './code-storage-epic'; | import codeStorageEpic from './code-storage-epic'; | ||||||
| import currentChallengeEpic from './current-challenge-epic'; | import currentChallengeEpic from './current-challenge-epic'; | ||||||
|  |  | ||||||
|  | import { createIdToNameMapSaga } from './id-to-name-map-saga'; | ||||||
|  |  | ||||||
| const ns = 'challenge'; | const ns = 'challenge'; | ||||||
| export const backendNS = 'backendChallenge'; | export const backendNS = 'backendChallenge'; | ||||||
|  |  | ||||||
| const initialState = { | const initialState = { | ||||||
|   challengeFiles: {}, |   challengeFiles: {}, | ||||||
|  |   challengeIdToNameMap: {}, | ||||||
|   challengeMeta: { |   challengeMeta: { | ||||||
|     id: '', |     id: '', | ||||||
|     nextChallengePath: '/' |     nextChallengePath: '/' | ||||||
| @@ -34,16 +39,6 @@ const initialState = { | |||||||
|   successMessage: 'Happy Coding!' |   successMessage: 'Happy Coding!' | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const epics = [ |  | ||||||
|   challengeModalEpic, |  | ||||||
|   codeLockEpic, |  | ||||||
|   completionEpic, |  | ||||||
|   createQuestionEpic, |  | ||||||
|   executeChallengeEpic, |  | ||||||
|   codeStorageEpic, |  | ||||||
|   currentChallengeEpic |  | ||||||
| ]; |  | ||||||
|  |  | ||||||
| export const types = createTypes( | export const types = createTypes( | ||||||
|   [ |   [ | ||||||
|     'createFiles', |     'createFiles', | ||||||
| @@ -76,11 +71,25 @@ export const types = createTypes( | |||||||
|     'executeChallenge', |     'executeChallenge', | ||||||
|     'resetChallenge', |     'resetChallenge', | ||||||
|     'submitChallenge', |     'submitChallenge', | ||||||
|     'submitComplete' |     'submitComplete', | ||||||
|  |  | ||||||
|  |     ...createAsyncTypes('fetchIdToNameMap') | ||||||
|   ], |   ], | ||||||
|   ns |   ns | ||||||
| ); | ); | ||||||
|  |  | ||||||
|  | export const epics = [ | ||||||
|  |   challengeModalEpic, | ||||||
|  |   codeLockEpic, | ||||||
|  |   completionEpic, | ||||||
|  |   createQuestionEpic, | ||||||
|  |   executeChallengeEpic, | ||||||
|  |   codeStorageEpic, | ||||||
|  |   currentChallengeEpic | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | export const sagas = [...createIdToNameMapSaga(types)]; | ||||||
|  |  | ||||||
| export const createFiles = createAction(types.createFiles, challengeFiles => | export const createFiles = createAction(types.createFiles, challengeFiles => | ||||||
|   Object.keys(challengeFiles) |   Object.keys(challengeFiles) | ||||||
|     .filter(key => challengeFiles[key]) |     .filter(key => challengeFiles[key]) | ||||||
| @@ -96,6 +105,13 @@ export const createFiles = createAction(types.createFiles, challengeFiles => | |||||||
|       {} |       {} | ||||||
|     ) |     ) | ||||||
| ); | ); | ||||||
|  |  | ||||||
|  | export const fetchIdToNameMap = createAction(types.fetchIdToNameMap); | ||||||
|  | export const fetchIdToNameMapComplete = createAction( | ||||||
|  |   types.fetchIdToNameMapComplete | ||||||
|  | ); | ||||||
|  | export const fetchIdToNameMapError = createAction(types.fetchIdToNameMapError); | ||||||
|  |  | ||||||
| export const createQuestion = createAction(types.createQuestion); | export const createQuestion = createAction(types.createQuestion); | ||||||
| export const initTests = createAction(types.initTests); | export const initTests = createAction(types.initTests); | ||||||
| export const updateTests = createAction(types.updateTests); | export const updateTests = createAction(types.updateTests); | ||||||
| @@ -131,6 +147,8 @@ export const submitChallenge = createAction(types.submitChallenge); | |||||||
| export const submitComplete = createAction(types.submitComplete); | export const submitComplete = createAction(types.submitComplete); | ||||||
|  |  | ||||||
| export const challengeFilesSelector = state => state[ns].challengeFiles; | export const challengeFilesSelector = state => state[ns].challengeFiles; | ||||||
|  | export const challengeIdToNameMapSelector = state => | ||||||
|  |   state[ns].challengeIdToNameMap; | ||||||
| export const challengeMetaSelector = state => state[ns].challengeMeta; | export const challengeMetaSelector = state => state[ns].challengeMeta; | ||||||
| export const challengeTestsSelector = state => state[ns].challengeTests; | export const challengeTestsSelector = state => state[ns].challengeTests; | ||||||
| export const consoleOutputSelector = state => state[ns].consoleOut; | export const consoleOutputSelector = state => state[ns].consoleOut; | ||||||
| @@ -149,6 +167,10 @@ export const projectFormValuesSelector = state => | |||||||
|  |  | ||||||
| export const reducer = handleActions( | export const reducer = handleActions( | ||||||
|   { |   { | ||||||
|  |     [types.fetchIdToNameMapComplete]: (state, { payload }) => ({ | ||||||
|  |       ...state, | ||||||
|  |       challengeIdToNameMap: payload | ||||||
|  |     }), | ||||||
|     [types.createFiles]: (state, { payload }) => ({ |     [types.createFiles]: (state, { payload }) => ({ | ||||||
|       ...state, |       ...state, | ||||||
|       challengeFiles: payload |       challengeFiles: payload | ||||||
|   | |||||||
| @@ -24,6 +24,14 @@ export function getSessionUser() { | |||||||
|   return get('/user/get-session-user'); |   return get('/user/get-session-user'); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export function getIdToNameMap() { | ||||||
|  |   return get('/api/challenges/get-id-to-name'); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function getUserProfile(username) { | ||||||
|  |   return get(`/api/users/get-public-profile?username=${username}`); | ||||||
|  | } | ||||||
|  |  | ||||||
| export function getShowCert(username, cert) { | export function getShowCert(username, cert) { | ||||||
|   return get(`/certificate/showCert/${username}/${cert}`); |   return get(`/certificate/showCert/${username}/${cert}`); | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										133
									
								
								client/static/css/cal-heatmap.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								client/static/css/cal-heatmap.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,133 @@ | |||||||
|  | /* Cal-HeatMap CSS */ | ||||||
|  |  | ||||||
|  | .cal-heatmap-container { | ||||||
|  |   display: block; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .cal-heatmap-container .graph { | ||||||
|  |   font-family: 'Lucida Grande', Lucida, Verdana, sans-serif; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .cal-heatmap-container .graph-label { | ||||||
|  |   fill: #999; | ||||||
|  |   font-size: 10px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .cal-heatmap-container .graph, | ||||||
|  | .cal-heatmap-container .graph-legend rect { | ||||||
|  |   shape-rendering: crispedges; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .cal-heatmap-container .graph-rect { | ||||||
|  |   fill: #ededed; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .cal-heatmap-container .graph-subdomain-group rect:hover { | ||||||
|  |   stroke: #000; | ||||||
|  |   stroke-width: 1px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .cal-heatmap-container .subdomain-text { | ||||||
|  |   font-size: 8px; | ||||||
|  |   fill: #999; | ||||||
|  |   pointer-events: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .cal-heatmap-container .hover_cursor:hover { | ||||||
|  |   cursor: pointer; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .cal-heatmap-container .qi { | ||||||
|  |   background-color: #999; | ||||||
|  |   fill: #999; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* | ||||||
|  | 	Remove comment to apply this style to date with value equal to 0 | ||||||
|  | 	.q0 | ||||||
|  | 	{ | ||||||
|  | 		background-color: #fff; | ||||||
|  | 		fill: #fff; | ||||||
|  | 		stroke: #ededed | ||||||
|  | 	} | ||||||
|  | 	*/ | ||||||
|  |  | ||||||
|  | .cal-heatmap-container .q1 { | ||||||
|  |   background-color: #dae289; | ||||||
|  |   fill: #dae289; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .cal-heatmap-container .q2 { | ||||||
|  |   background-color: #cedb9c; | ||||||
|  |   fill: #9cc069; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .cal-heatmap-container .q3 { | ||||||
|  |   background-color: #b5cf6b; | ||||||
|  |   fill: #669d45; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .cal-heatmap-container .q4 { | ||||||
|  |   background-color: #637939; | ||||||
|  |   fill: #637939; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .cal-heatmap-container .q5 { | ||||||
|  |   background-color: #3b6427; | ||||||
|  |   fill: #3b6427; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .cal-heatmap-container rect.highlight { | ||||||
|  |   stroke: #444; | ||||||
|  |   stroke-width: 1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .cal-heatmap-container text.highlight { | ||||||
|  |   fill: #444; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .cal-heatmap-container rect.now { | ||||||
|  |   stroke: red; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .cal-heatmap-container text.now { | ||||||
|  |   fill: red; | ||||||
|  |   font-weight: 800; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .cal-heatmap-container .domain-background { | ||||||
|  |   fill: none; | ||||||
|  |   shape-rendering: crispedges; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .ch-tooltip { | ||||||
|  |   padding: 10px; | ||||||
|  |   background: #222; | ||||||
|  |   color: #bbb; | ||||||
|  |   font-size: 12px; | ||||||
|  |   line-height: 1.4; | ||||||
|  |   width: 140px; | ||||||
|  |   position: absolute; | ||||||
|  |   z-index: 99999; | ||||||
|  |   text-align: center; | ||||||
|  |   border-radius: 2px; | ||||||
|  |   box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.2); | ||||||
|  |   display: none; | ||||||
|  |   box-sizing: border-box; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .ch-tooltip::after { | ||||||
|  |   position: absolute; | ||||||
|  |   width: 0; | ||||||
|  |   height: 0; | ||||||
|  |   border-color: transparent; | ||||||
|  |   border-style: solid; | ||||||
|  |   content: ''; | ||||||
|  |   padding: 0; | ||||||
|  |   display: block; | ||||||
|  |   bottom: -6px; | ||||||
|  |   left: 50%; | ||||||
|  |   margin-left: -6px; | ||||||
|  |   border-width: 6px 6px 0; | ||||||
|  |   border-top-color: #222; | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user