feat(profile): Add Profile and components

This commit is contained in:
Bouncey
2018-11-07 18:15:27 +00:00
committed by mrugesh mohapatra
parent 491912448b
commit 987da19254
24 changed files with 1358 additions and 83 deletions

View File

@ -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",

View File

@ -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",

View 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);

View 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&#x27;t find what you were looking for, but here is a
quote:
</p>
<div className='quote-wrapper'>
<p className='quote'>
<span>&#8220;</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;

View 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);

View 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;

View 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;

View 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);

View 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;

View 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;

View 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;

View 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.&nbsp;
<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);

View File

@ -0,0 +1,8 @@
.avatar-container .avatar {
height: 180px;
}
.avatar-container {
display: flex;
justify-content: center;
}

View 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;
}

View File

@ -0,0 +1,7 @@
.social-media-icons > a {
margin: 0 10px 0 0;
}
.social-media-icons > a:last-child {
margin: 0;
}

View File

@ -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&#x27;t find what you were looking for, but here is a
quote:
</p>
<div className='quote-wrapper'>
<p className='quote'>
<span>&#8220;</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;

View File

@ -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)
];
} }

View File

@ -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

View File

@ -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]);
} }

View 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)
];
}

View File

@ -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

View File

@ -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}`);
} }

View 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;
}