feat(profile): Add Profile and components
This commit is contained in:
committed by
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": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
|
||||
|
@ -19,6 +19,7 @@
|
||||
"axios": "^0.18.0",
|
||||
"browser-cookies": "^1.2.0",
|
||||
"chai": "^4.2.0",
|
||||
"date-fns": "^1.29.0",
|
||||
"enzyme": "^3.6.0",
|
||||
"enzyme-adapter-react-16": "^1.5.0",
|
||||
"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 Helmet from 'react-helmet';
|
||||
import Spinner from 'react-spinkit';
|
||||
import { Link } from 'gatsby';
|
||||
import { Router } from '@reach/router';
|
||||
// eslint-disable-next-line max-len
|
||||
import ShowProfileOrFourOhFour from '../client-only-routes/ShowProfileOrFourOhFour';
|
||||
|
||||
import Layout from '../components/layouts/Default';
|
||||
|
||||
import notFoundLogo from '../images/freeCodeCamp-404.svg';
|
||||
import { quotes } from '../resources/quotes.json';
|
||||
|
||||
import './404.css';
|
||||
|
||||
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() {
|
||||
function FourOhFourPage() {
|
||||
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>
|
||||
<Router>
|
||||
<ShowProfileOrFourOhFour path='/:maybeUser/:splat' />
|
||||
<ShowProfileOrFourOhFour path='/:maybeUser' />
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default NotFoundPage;
|
||||
FourOhFourPage.displayName = 'FourOhFourPage';
|
||||
|
||||
export default FourOhFourPage;
|
||||
|
@ -1,7 +1,12 @@
|
||||
import { call, put, takeEvery } from 'redux-saga/effects';
|
||||
|
||||
import { fetchUserComplete, fetchUserError } from './';
|
||||
import { getSessionUser } from '../utils/ajax';
|
||||
import {
|
||||
fetchUserComplete,
|
||||
fetchUserError,
|
||||
fetchProfileForUserError,
|
||||
fetchProfileForUserComplete
|
||||
} from './';
|
||||
import { getSessionUser, getUserProfile } from '../utils/ajax';
|
||||
import { jwt } from './cookieValues';
|
||||
|
||||
function* fetchSessionUser() {
|
||||
@ -13,13 +18,30 @@ function* fetchSessionUser() {
|
||||
const {
|
||||
data: { user = {}, result = '' }
|
||||
} = yield call(getSessionUser);
|
||||
const appUser = user[result];
|
||||
const appUser = user[result] || {};
|
||||
yield put(fetchUserComplete({ user: appUser, username: result }));
|
||||
} catch (e) {
|
||||
yield put(fetchUserError(e));
|
||||
}
|
||||
}
|
||||
|
||||
export function createFetchUserSaga(types) {
|
||||
return [takeEvery(types.fetchUser, fetchSessionUser)];
|
||||
function* fetchOtherUser({ payload: maybeUser }) {
|
||||
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: {
|
||||
...defaultFetchState
|
||||
},
|
||||
userProfileFetchState: {
|
||||
...defaultFetchState
|
||||
},
|
||||
showDonationModal: false,
|
||||
isOnline: true
|
||||
};
|
||||
@ -53,6 +56,7 @@ export const types = createTypes(
|
||||
'updateComplete',
|
||||
'updateFailed',
|
||||
...createAsyncTypes('fetchUser'),
|
||||
...createAsyncTypes('fetchProfileForUser'),
|
||||
...createAsyncTypes('acceptTerms'),
|
||||
...createAsyncTypes('showCert'),
|
||||
...createAsyncTypes('reportUser')
|
||||
@ -60,11 +64,7 @@ export const types = createTypes(
|
||||
ns
|
||||
);
|
||||
|
||||
export const epics = [
|
||||
hardGoToEpic,
|
||||
failedUpdatesEpic,
|
||||
updateCompleteEpic
|
||||
];
|
||||
export const epics = [hardGoToEpic, failedUpdatesEpic, updateCompleteEpic];
|
||||
|
||||
export const sagas = [
|
||||
...createAcceptTermsSaga(types),
|
||||
@ -98,6 +98,14 @@ export const fetchUser = createAction(types.fetchUser);
|
||||
export const fetchUserComplete = createAction(types.fetchUserComplete);
|
||||
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 reportUserComplete = createAction(types.reportUserComplete);
|
||||
export const reportUserError = createAction(types.reportUserError);
|
||||
@ -144,6 +152,8 @@ export const userByNameSelector = username => state => {
|
||||
return username in user ? user[username] : {};
|
||||
};
|
||||
export const userFetchStateSelector = state => state[ns].userFetchState;
|
||||
export const userProfileFetchStateSelector = state =>
|
||||
state[ns].userProfileFetchState;
|
||||
export const usernameSelector = state => state[ns].appUsername;
|
||||
export const userSelector = state => {
|
||||
const username = usernameSelector(state);
|
||||
@ -170,11 +180,15 @@ export const reducer = handleActions(
|
||||
...state,
|
||||
userFetchState: { ...defaultFetchState }
|
||||
}),
|
||||
[types.fetchProfileForUser]: state => ({
|
||||
...state,
|
||||
userProfileFetchState: { ...defaultFetchState }
|
||||
}),
|
||||
[types.fetchUserComplete]: (state, { payload: { user, username } }) => ({
|
||||
...state,
|
||||
user: {
|
||||
...state.user,
|
||||
[username]: user
|
||||
[username]: { ...user, sessionUser: true }
|
||||
},
|
||||
appUsername: username,
|
||||
userFetchState: {
|
||||
@ -193,6 +207,35 @@ export const reducer = handleActions(
|
||||
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 }) => ({
|
||||
...state,
|
||||
isOnline
|
||||
|
@ -2,7 +2,8 @@ import { all } from 'redux-saga/effects';
|
||||
|
||||
import { sagas as appSagas } from './';
|
||||
import { sagas as settingsSagas } from './settings';
|
||||
import { sagas as challengeSagas } from '../templates/Challenges/redux';
|
||||
|
||||
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 { createTypes } from '../../../../utils/stateManagement';
|
||||
import { createAsyncTypes } from '../../../utils/createTypes';
|
||||
|
||||
import { createPoly } from '../utils/polyvinyl';
|
||||
import challengeModalEpic from './challenge-modal-epic';
|
||||
import completionEpic from './completion-epic';
|
||||
@ -11,11 +13,14 @@ import createQuestionEpic from './create-question-epic';
|
||||
import codeStorageEpic from './code-storage-epic';
|
||||
import currentChallengeEpic from './current-challenge-epic';
|
||||
|
||||
import { createIdToNameMapSaga } from './id-to-name-map-saga';
|
||||
|
||||
const ns = 'challenge';
|
||||
export const backendNS = 'backendChallenge';
|
||||
|
||||
const initialState = {
|
||||
challengeFiles: {},
|
||||
challengeIdToNameMap: {},
|
||||
challengeMeta: {
|
||||
id: '',
|
||||
nextChallengePath: '/'
|
||||
@ -34,16 +39,6 @@ const initialState = {
|
||||
successMessage: 'Happy Coding!'
|
||||
};
|
||||
|
||||
export const epics = [
|
||||
challengeModalEpic,
|
||||
codeLockEpic,
|
||||
completionEpic,
|
||||
createQuestionEpic,
|
||||
executeChallengeEpic,
|
||||
codeStorageEpic,
|
||||
currentChallengeEpic
|
||||
];
|
||||
|
||||
export const types = createTypes(
|
||||
[
|
||||
'createFiles',
|
||||
@ -76,11 +71,25 @@ export const types = createTypes(
|
||||
'executeChallenge',
|
||||
'resetChallenge',
|
||||
'submitChallenge',
|
||||
'submitComplete'
|
||||
'submitComplete',
|
||||
|
||||
...createAsyncTypes('fetchIdToNameMap')
|
||||
],
|
||||
ns
|
||||
);
|
||||
|
||||
export const epics = [
|
||||
challengeModalEpic,
|
||||
codeLockEpic,
|
||||
completionEpic,
|
||||
createQuestionEpic,
|
||||
executeChallengeEpic,
|
||||
codeStorageEpic,
|
||||
currentChallengeEpic
|
||||
];
|
||||
|
||||
export const sagas = [...createIdToNameMapSaga(types)];
|
||||
|
||||
export const createFiles = createAction(types.createFiles, challengeFiles =>
|
||||
Object.keys(challengeFiles)
|
||||
.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 initTests = createAction(types.initTests);
|
||||
export const updateTests = createAction(types.updateTests);
|
||||
@ -131,6 +147,8 @@ export const submitChallenge = createAction(types.submitChallenge);
|
||||
export const submitComplete = createAction(types.submitComplete);
|
||||
|
||||
export const challengeFilesSelector = state => state[ns].challengeFiles;
|
||||
export const challengeIdToNameMapSelector = state =>
|
||||
state[ns].challengeIdToNameMap;
|
||||
export const challengeMetaSelector = state => state[ns].challengeMeta;
|
||||
export const challengeTestsSelector = state => state[ns].challengeTests;
|
||||
export const consoleOutputSelector = state => state[ns].consoleOut;
|
||||
@ -149,6 +167,10 @@ export const projectFormValuesSelector = state =>
|
||||
|
||||
export const reducer = handleActions(
|
||||
{
|
||||
[types.fetchIdToNameMapComplete]: (state, { payload }) => ({
|
||||
...state,
|
||||
challengeIdToNameMap: payload
|
||||
}),
|
||||
[types.createFiles]: (state, { payload }) => ({
|
||||
...state,
|
||||
challengeFiles: payload
|
||||
|
@ -24,6 +24,14 @@ export function getSessionUser() {
|
||||
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) {
|
||||
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