diff --git a/common/app/App.jsx b/common/app/App.jsx index 11dde49f91..f4e17b61f6 100644 --- a/common/app/App.jsx +++ b/common/app/App.jsx @@ -16,6 +16,7 @@ import Toasts from './Toasts'; import NotFound from './NotFound'; import { mainRouteSelector } from './routes/redux'; import Challenges from './routes/Challenges'; +import Profile from './routes/Profile'; import Settings from './routes/Settings'; const mapDispatchToProps = { @@ -44,6 +45,7 @@ const propTypes = { const routes = { challenges: Challenges, + profile: Profile, settings: Settings }; diff --git a/common/app/Nav/Nav.jsx b/common/app/Nav/Nav.jsx index 930c6feb05..222c511196 100644 --- a/common/app/Nav/Nav.jsx +++ b/common/app/Nav/Nav.jsx @@ -5,7 +5,6 @@ import { connect } from 'react-redux'; import capitalize from 'lodash/capitalize'; import { createSelector } from 'reselect'; import FCCSearchBar from 'react-freecodecamp-search'; - import { MenuItem, Nav, @@ -15,6 +14,7 @@ import { NavbarBrand } from 'react-bootstrap'; +import NoPropsPassThrough from '../utils/No-Props-Passthrough.jsx'; import { Link } from '../Router'; import navLinks from './links.json'; import SignUp from './Sign-Up.jsx'; @@ -30,6 +30,7 @@ import { } from './redux'; import { isSignedInSelector, signInLoadingSelector } from '../redux'; import { panesSelector } from '../Panes/redux'; +import { onRouteCurrentChallenge } from '../routes/Challenges/redux'; const fCClogo = 'https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg'; @@ -224,11 +225,16 @@ export class FCCNav extends React.Component { )) } { shouldShowMapButton ? - : + +
  • + + Map + +
  • +
    : null } { diff --git a/common/app/entities/index.js b/common/app/entities/index.js index 455ab1a8ac..d8dae89a97 100644 --- a/common/app/entities/index.js +++ b/common/app/entities/index.js @@ -151,7 +151,17 @@ export const isChallengeLoaded = (state, { dashedName }) => export default composeReducers( ns, function metaReducer(state = defaultState, action) { - if (action.meta && action.meta.entities) { + const { meta } = action; + if (meta && meta.entities) { + if (meta.entities.user) { + return { + ...state, + user: { + ...state.user, + ...meta.entities.user + } + }; + } return { ...state, ...action.meta.entities @@ -159,7 +169,7 @@ export default composeReducers( } return state; }, - function(state = defaultState, action) { + function entitiesReducer(state = defaultState, action) { if (getEntityAction(action)) { const { payload: { username, theme } } = getEntityAction(action); return { diff --git a/common/app/redux/fetch-user-epic.js b/common/app/redux/fetch-user-epic.js index 447e8366f2..512aaf907e 100644 --- a/common/app/redux/fetch-user-epic.js +++ b/common/app/redux/fetch-user-epic.js @@ -1,13 +1,18 @@ -import { ofType } from 'redux-epic'; +import { Observable } from 'rx'; +import { ofType, combineEpics } from 'redux-epic'; + +import { getJSON$ } from '../../utils/ajax-stream'; import { types, fetchUserComplete, + fetchOtherUserComplete, createErrorObservable, showSignIn } from './'; +import { userFound } from '../routes/Profile/redux'; -export default function getUserEpic(actions, _, { services }) { +function getUserEpic(actions, _, { services }) { return actions::ofType('' + types.fetchUser) .flatMap(() => { return services.readService$({ service: 'user' }) @@ -17,3 +22,21 @@ export default function getUserEpic(actions, _, { services }) { .catch(createErrorObservable); }); } + +function getOtherUserEpic(actions$) { + return actions$::ofType(types.fetchOtherUser.start) + .distinctUntilChanged() + .flatMap(({ payload: otherUser }) => { + return getJSON$(`/api/users/get-public-profile?username=${otherUser}`) + .flatMap(response => Observable.of( + fetchOtherUserComplete(response), + userFound(!!response.result) + )) + .catch(createErrorObservable); + }); +} + +export default combineEpics( + getUserEpic, + getOtherUserEpic +); diff --git a/common/app/redux/index.js b/common/app/redux/index.js index ef5159b4c9..b0a634f0b7 100644 --- a/common/app/redux/index.js +++ b/common/app/redux/index.js @@ -17,6 +17,7 @@ import nightModeEpic from './night-mode-epic.js'; import { createFilesMetaCreator } from '../files'; import { updateThemeMetacreator, entitiesSelector } from '../entities'; import { utils } from '../Flash/redux'; +import { paramsSelector } from '../Router/redux'; import { types as challenges } from '../routes/Challenges/redux'; import { challengeToFiles } from '../routes/Challenges/utils'; @@ -41,6 +42,7 @@ export const types = createTypes([ createAsyncTypes('fetchChallenge'), createAsyncTypes('fetchChallenges'), 'updateChallenges', + createAsyncTypes('fetchOtherUser'), createAsyncTypes('fetchUser'), 'showSignIn', @@ -109,9 +111,20 @@ export const fetchChallengesCompleted = createAction( entities => ({ entities }) ); export const updateChallenges = createAction(types.updateChallenges); + // updateTitle(title: String) => Action export const updateTitle = createAction(types.updateTitle); +// fetchOtherUser() => Action +// used in combination with fetch-user-epic +// to fetch another users profile +export const fetchOtherUser = createAction(types.fetchOtherUser.start); +export const fetchOtherUserComplete = createAction( + types.fetchOtherUser.complete, + ({ result }) => result, + _.identity +); + // fetchUser() => Action // used in combination with fetch-user-epic export const fetchUser = createAction(types.fetchUser); @@ -190,6 +203,12 @@ export const userSelector = createSelector( (username, userMap) => userMap[username] || {} ); +export const userByNameSelector = state => { + const username = paramsSelector(state).username; + const userMap = entitiesSelector(state).user; + return userMap[username] || {}; +}; + export const themeSelector = _.flow( userSelector, user => user.theme || themes.default diff --git a/common/app/routes/Profile/Profile.jsx b/common/app/routes/Profile/Profile.jsx new file mode 100644 index 0000000000..416dde77b1 --- /dev/null +++ b/common/app/routes/Profile/Profile.jsx @@ -0,0 +1,227 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { + Alert, + Button, + Grid +} from 'react-bootstrap'; + + +import { + updateTitle, + isSignedInSelector, + signInLoadingSelector, + usernameSelector, + userByNameSelector, + fetchOtherUser +} from '../../redux'; +import { userFoundSelector } from './redux'; +import { paramsSelector } from '../../Router/redux'; +import ns from './ns.json'; +import ChildContainer from '../../Child-Container.jsx'; +import { Link } from '../../Router'; +import CamperHOC from './components/CamperHOC.jsx'; +import Portfolio from './components/Portfolio.jsx'; +import Certificates from './components/Certificates.jsx'; +import Timeline from './components/Timeline.jsx'; +import HeatMap from './components/HeatMap.jsx'; +import { FullWidthRow, Loader } from '../../helperComponents'; + +const mapStateToProps = createSelector( + isSignedInSelector, + userByNameSelector, + paramsSelector, + usernameSelector, + signInLoadingSelector, + userFoundSelector, + ( + isSignedIn, + { isLocked, username: requestedUsername }, + { username: paramsUsername, lang }, + currentUsername, + showLoading, + isUserFound + ) => ({ + isSignedIn, + currentUsername, + isCurrentUserProfile: paramsUsername === currentUsername, + isLocked, + isUserFound, + fetchOtherUserCompleted: typeof isUserFound === 'boolean', + paramsUsername, + lang, + requestedUsername, + showLoading + }) +); + +const mapDispatchToProps = { + fetchOtherUser, + updateTitle +}; + +const propTypes = { + currentUsername: PropTypes.string, + fetchOtherUser: PropTypes.func.isRequired, + fetchOtherUserCompleted: PropTypes.bool, + isCurrentUserProfile: PropTypes.bool, + isLocked: PropTypes.bool, + isSignedIn: PropTypes.bool, + isUserFound: PropTypes.bool, + lang: PropTypes.string, + paramsUsername: PropTypes.string, + requestedUsername: PropTypes.string, + showLoading: PropTypes.bool, + updateTitle: PropTypes.func.isRequired +}; + +class Profile extends Component { + + componentWillMount() { + this.props.updateTitle('Profile'); + } + componentDidUpdate() { + const { requestedUsername, currentUsername, paramsUsername } = this.props; + if (!requestedUsername && paramsUsername !== currentUsername) { + this.props.fetchOtherUser(paramsUsername); + } + } + + renderRequestedProfile() { + const { + fetchOtherUserCompleted, + isLocked, + isUserFound, + isCurrentUserProfile, + lang = 'en', + paramsUsername + } = this.props; + const takeMeToChallenges = ( + + + + ); + if (isLocked) { + return ( +
    +

    + { + `${paramsUsername} has not made their profile public. ` + } +

    + +

    + { + 'In order to view their progress through the freeCodeCamp ' + + 'curriculum, they need to make all of thie solutions public' + } +

    +
    + { takeMeToChallenges } +
    + ); + } + if (!isCurrentUserProfile && (fetchOtherUserCompleted && !isUserFound)) { + return ( +
    + +

    + { `We could not find a user by the name of "${paramsUsername}"` } +

    +
    + { takeMeToChallenges } +
    + ); + } + return ( +
    + + + + + +
    + ); + } + + renderReportUserButton() { + const { + isSignedIn, + fetchOtherUserCompleted, + isCurrentUserProfile, + isUserFound, + paramsUsername + } = this.props; + + return ( + !isSignedIn || + isCurrentUserProfile || + (fetchOtherUserCompleted && !isUserFound) + ) ? + null : + ( + + + + ); + } + + renderSettingsLink() { + const { isCurrentUserProfile } = this.props; + return isCurrentUserProfile ? + + + + + : + null; + } + + render() { + const { + isCurrentUserProfile, + showLoading, + fetchOtherUserCompleted + } = this.props; + if (isCurrentUserProfile && showLoading) { + return ; + } + if (!isCurrentUserProfile && !fetchOtherUserCompleted) { + return ; + } + return ( + + + { this.renderSettingsLink() } + { this.renderRequestedProfile() } + { this.renderReportUserButton() } + + + ); + } +} + +Profile.displayName = 'Profile'; +Profile.propTypes = propTypes; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(Profile); diff --git a/common/app/routes/Profile/components/CamperHOC.jsx b/common/app/routes/Profile/components/CamperHOC.jsx new file mode 100644 index 0000000000..d719fe1ed7 --- /dev/null +++ b/common/app/routes/Profile/components/CamperHOC.jsx @@ -0,0 +1,64 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { createSelector } from 'reselect'; +import { connect } from 'react-redux'; + +import { userByNameSelector } from '../../../redux'; +import Camper from '../../Settings/components/Camper.jsx'; + +const mapStateToProps = createSelector( + userByNameSelector, + ({ + name, + username, + location, + points, + picture, + about + }) => ({ + name, + username, + location, + points, + picture, + about + }) +); + +const propTypes = { + about: PropTypes.string, + location: PropTypes.string, + name: PropTypes.string, + picture: PropTypes.string, + points: PropTypes.number, + username: PropTypes.string +}; + +function CamperHOC({ + name, + username, + location, + points, + picture, + about +}) { + + return ( +
    + +
    +
    + ); +} + +CamperHOC.displayName = 'CamperHOC'; +CamperHOC.propTypes = propTypes; + +export default connect(mapStateToProps)(CamperHOC); diff --git a/common/app/routes/Profile/components/Certificates.jsx b/common/app/routes/Profile/components/Certificates.jsx new file mode 100644 index 0000000000..06d3ed6891 --- /dev/null +++ b/common/app/routes/Profile/components/Certificates.jsx @@ -0,0 +1,184 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import _ from 'lodash'; +import { createSelector } from 'reselect'; +import { connect } from 'react-redux'; +import { + Button, + Row, + Col +} from 'react-bootstrap'; + +import { userByNameSelector } from '../../../redux'; + +const mapStateToProps = createSelector( + userByNameSelector, + ({ + isRespWebDesignCert, + is2018DataVisCert, + isFrontEndLibsCert, + isJsAlgoDataStructCert, + isApisMicroservicesCert, + isInfosecQaCert, + isFrontEndCert, + isBackEndCert, + isDataVisCert, + isFullStackCert, + is2018FullStackCert, + username + }) => ({ + username, + hasModernCert: ( + isRespWebDesignCert || + is2018DataVisCert || + isFrontEndLibsCert || + isJsAlgoDataStructCert || + isApisMicroservicesCert || + isInfosecQaCert + ), + hasLegacyCert: (isFrontEndCert || isBackEndCert || isDataVisCert), + currentCerts: [ + { + show: is2018FullStackCert, + title: 'Full Stack Certificate:', + showURL: '2018-full-stack' + }, + { + show: isRespWebDesignCert, + title: 'Responsive Web Design Certificate:', + showURL: 'responsive-web-design' + }, + { + show: isJsAlgoDataStructCert, + title: 'JavaScript Algorithms and Data Structures Certificate:', + showURL: 'javascript-algorithms-and-data-structures' + }, + { + show: isFrontEndLibsCert, + title: 'Front End Libraries Certificate:', + showURL: 'front-end-libraries' + }, + { + show: is2018DataVisCert, + title: 'Data Visualization Certificate:', + showURL: 'data-visualization-2018' + }, + { + show: isApisMicroservicesCert, + title: 'APIs and Microservices Certificate:', + showURL: 'apis-and-microservices' + }, + { + show: isInfosecQaCert, + title: 'Information Security and Quality Assurance Certificate:', + showURL: 'information-security-and-quality-assurance' + } + ], + legacyCerts: [ + { + show: isFullStackCert, + title: 'Full Stack Certificate:', + showURL: 'full-stack' + }, + { + show: isFrontEndCert, + title: 'Front End Certificate:', + showURL: 'front-end' + }, + { + show: isBackEndCert, + title: 'Back End Certificate:', + showURL: 'back-end' + }, + { + show: isDataVisCert, + title: 'Data Visualization Certificate:', + showURL: 'data-visualization' + } + ] + }) +); + +function mapDispatchToProps() { + return {}; +} + +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 ? ( + + +

    + + { cert.title } + +

    + + + + +
    + ) : + null; +} + +function Certificates({ + currentCerts, + legacyCerts, + hasLegacyCert, + hasModernCert, + username +}) { + const renderCertShowWithUsername = _.curry(renderCertShow)(username); + return ( +
    +

    freeCodeCamp Certificates

    +
    + { + hasModernCert ? + currentCerts.map(renderCertShowWithUsername) : +

    + No certificates have been earned under the current curriculum +

    + } + { + hasLegacyCert ? +
    +

    Legacy Certifications

    + { + legacyCerts.map(renderCertShowWithUsername) + } +
    : + null + } +
    +
    + ); +} + +Certificates.propTypes = propTypes; +Certificates.displayName = 'Certificates'; + +export default connect(mapStateToProps, mapDispatchToProps)(Certificates); diff --git a/common/app/routes/Profile/components/HeatMap.jsx b/common/app/routes/Profile/components/HeatMap.jsx new file mode 100644 index 0000000000..ce8d395ba9 --- /dev/null +++ b/common/app/routes/Profile/components/HeatMap.jsx @@ -0,0 +1,121 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import d3 from 'react-d3'; +import CalHeatMap from 'cal-heatmap'; +import { createSelector } from 'reselect'; +import { connect } from 'react-redux'; +import { Helmet } from 'react-helmet'; +import differenceInCalendarMonths from 'date-fns/difference_in_calendar_months'; + +import { FullWidthRow } from '../../../helperComponents'; +import { userByNameSelector } from '../../../redux'; + +function ensureD3() { + // CalHeatMap requires d3 to be available on window + if (typeof window !== 'undefined') { + if ('d3' in window) { + return; + } else { + window.d3 = d3; + } + return; + } + return; +} + +const mapStateToProps = createSelector( + userByNameSelector, + ({ calendar, streak }) => ({ calendar, streak }) +); + +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); + } + componentDidMount() { + ensureD3(); + this.renderMap(); + } + + renderMap() { + const { calendar = {} } = this.props; + if (Object.keys(calendar).length === 0) { + return null; + } + const today = new Date(); + const cal = new CalHeatMap(); + const rectSelector = '#cal-heatmap > svg > svg.graph-legend > g > rect.r'; + const calLegendTitles = ['less', '', '', 'more']; + const firstTS = Object.keys(calendar)[0]; + let start = new Date(firstTS * 1000); + const monthsSinceFirstActive = differenceInCalendarMonths( + today, + start + ); + cal.init({ + itemSelector: '#cal-heatmap', + domain: 'month', + subDomain: 'day', + domainDynamicDimension: true, + domainGutter: 5, + data: calendar, + cellSize: 15, + cellRadius: 3, + cellPadding: 2, + tooltip: true, + range: monthsSinceFirstActive < 12 ? monthsSinceFirstActive + 1 : 12, + start, + legendColors: ['#cccccc', '#006400'], + legend: [1, 2, 3], + label: { + position: 'top' + } + }); + calLegendTitles.forEach(function(title, i) { + document + .querySelector(rectSelector + (i + 1).toString() + '> title') + .innerHTML = title; + }); + return null; + } + + render() { + const { streak = {} } = this.props; + return ( +
    + + + + +
    + + +
    + + Longest Streak: { streak.longest || 1 } + + + Current Streak: { streak.current || 1 } + +
    +
    +
    +
    + ); + } +} + +HeatMap.displayName = 'HeatMap'; +HeatMap.propTypes = propTypes; + +export default connect(mapStateToProps)(HeatMap); diff --git a/common/app/routes/Profile/components/Portfolio.jsx b/common/app/routes/Profile/components/Portfolio.jsx new file mode 100644 index 0000000000..76a66bd95d --- /dev/null +++ b/common/app/routes/Profile/components/Portfolio.jsx @@ -0,0 +1,74 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { createSelector } from 'reselect'; +import { connect } from 'react-redux'; +import { Thumbnail, Media } from 'react-bootstrap'; + +import { FullWidthRow } from '../../../helperComponents'; +import { userByNameSelector } from '../../../redux'; + +const mapStateToProps = createSelector( + userByNameSelector, + ({ portfolio }) => ({ portfolio }) +); + +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 ( +
    + +

    Portfolio

    + { + portfolio.map(({ title, url, image, description, id}) => ( + + + { + image && ( + + + + ) + + } + + + + + { title } + + +

    + { description } +

    +
    +
    + )) + } +
    +
    +
    + ); +} + +Portfolio.displayName = 'Portfolio'; +Portfolio.propTypes = propTypes; + +export default connect(mapStateToProps)(Portfolio); diff --git a/common/app/routes/Settings/components/SocialIcons.jsx b/common/app/routes/Profile/components/SocialIcons.jsx similarity index 91% rename from common/app/routes/Settings/components/SocialIcons.jsx rename to common/app/routes/Profile/components/SocialIcons.jsx index 1d85c83bcf..d8b1b3bb78 100644 --- a/common/app/routes/Settings/components/SocialIcons.jsx +++ b/common/app/routes/Profile/components/SocialIcons.jsx @@ -8,7 +8,7 @@ import { } from 'react-bootstrap'; import FontAwesome from 'react-fontawesome'; -import { userSelector } from '../../../redux'; +import { userByNameSelector } from '../../../redux'; const propTypes = { email: PropTypes.string, @@ -18,12 +18,13 @@ const propTypes = { isTwitter: PropTypes.bool, isWebsite: PropTypes.bool, linkedIn: PropTypes.string, + show: PropTypes.bool, twitter: PropTypes.string, website: PropTypes.string }; const mapStateToProps = createSelector( - userSelector, + userByNameSelector, ({ githubURL, isLinkedIn, @@ -40,6 +41,7 @@ const mapStateToProps = createSelector( isTwitter, isWebsite, linkedIn, + show: (isLinkedIn || isGithub || isTwitter || isWebsite), twitter, website }) @@ -101,9 +103,13 @@ function SocialIcons(props) { isTwitter, isWebsite, linkedIn, + show, twitter, website } = props; + if (!show) { + return null; + } return ( ({ + completedMap, + idToNameMap, + username + }) +); + +const mapDispatchToProps = { fetchChallenges }; + +const propTypes = { + completedMap: PropTypes.shape({ + id: PropTypes.string, + completedDate: PropTypes.number, + lastUpdated: PropTypes.number + }), + fetchChallenges: PropTypes.func.isRequired, + idToNameMap: PropTypes.objectOf(PropTypes.string), + username: PropTypes.string +}; + +class Timeline extends PureComponent { + 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 (!Object.keys(this.props.idToNameMap).length) { + this.props.fetchChallenges(); + } + } + + renderCompletion(completed) { + const { idToNameMap } = this.props; + const { id, completedDate, lastUpdated, files } = completed; + return ( + + { blockNameify(idToNameMap[id]) } + + + + + { + lastUpdated ? + : + '' + } + + + { + files ? + : + '' + } + + + ); + } + + 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 (!Object.keys(idToNameMap).length) { + return null; + } + return ( + +

    Timeline

    + { + Object.keys(completedMap).length === 0 ? +

    + No challenges have been completed yet.  + + Get started here. + +

    : + + + + + + + + + + { + reverse( + sortBy( + Object.keys(completedMap) + .filter(key => key in idToNameMap) + .map(key => completedMap[key]), + [ 'completedDate' ] + ) + ) + .map(this.renderCompletion) + } + +
    ChallengeFirst CompletedLast Changed +
    + } + { + id && + + + + { `${username}'s Solution to ${blockNameify(idToNameMap[id])}` } + + + + + + + + + + } +
    + ); + } +} + + +Timeline.displayName = 'Timeline'; +Timeline.propTypes = propTypes; + +export default connect(mapStateToProps, mapDispatchToProps)(Timeline); diff --git a/common/app/routes/Profile/index.js b/common/app/routes/Profile/index.js new file mode 100644 index 0000000000..2ae2f541bf --- /dev/null +++ b/common/app/routes/Profile/index.js @@ -0,0 +1,7 @@ +import { types } from './redux'; + +export { default } from './Profile.jsx'; + +export const routes = { + [types.onRouteProfile]: '/:username' +}; diff --git a/common/app/routes/Profile/ns.json b/common/app/routes/Profile/ns.json new file mode 100644 index 0000000000..2c3185e42e --- /dev/null +++ b/common/app/routes/Profile/ns.json @@ -0,0 +1 @@ +"profile" diff --git a/common/app/routes/Profile/profile.less b/common/app/routes/Profile/profile.less new file mode 100644 index 0000000000..ddf759a335 --- /dev/null +++ b/common/app/routes/Profile/profile.less @@ -0,0 +1,84 @@ +// should be the same as the filename and ./ns.json +@ns: profile; + +.avatar-container { + margin: 0 auto; + text-align: center; + .avatar { + height: 180px; + } +} + +.solution-viewer { + background-color: #fff; +} + + +.@{ns}-container { + + a:hover { + text-decoration: none; + text-decoration-line: none; + } + + .full-size { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + } + .row { + margin-bottom: 15px; + } + + .social-media-icons { + a { + &:first-child { + margin-left: 0; + } + + margin-left: 32px; + } + } + + .bio { + text-align: center; + } + + h2, h3 { + &.name, &.username, &.points, &.location { + margin-top: 0; + margin-bottom: 0; + } + } + + .portfolio-description { + height: 62px; + } + + .timeline-container { + display: flex; + align-items: center; + } + + #cal-heatmap { + display: flex; + justify-content: center; + } + + .streak-container { + display: flex; + justify-content: space-around; + align-items: center; + font-size: 18px; + } + + .streak { + color: @brand-primary; + + strong { + color: #333; + } + } + +} diff --git a/common/app/routes/Profile/redux/index.js b/common/app/routes/Profile/redux/index.js new file mode 100644 index 0000000000..920fb930b3 --- /dev/null +++ b/common/app/routes/Profile/redux/index.js @@ -0,0 +1,31 @@ +import { + createAction, + createTypes +} from 'berkeleys-redux-utils'; + +import ns from '../ns.json'; +import handleActions from 'berkeleys-redux-utils/lib/handle-actions'; + +export const types = createTypes([ + 'onRouteProfile', + 'userFound' +], 'profile'); + +export const onRouteProfile = createAction(types.onRouteProfile); +export const userFound = createAction(types.userFound); +const initialState = { + isUserFound: null +}; + +export const userFoundSelector = state => state[ns].isUserFound; + +export default handleActions(() => ( + { + [types.userFound]: (state, { payload }) => ({ + ...state, + isUserFound: payload + }) + }), + initialState, + ns +); diff --git a/common/app/routes/Settings/Settings.jsx b/common/app/routes/Settings/Settings.jsx index 6954a701d1..6b6389254e 100644 --- a/common/app/routes/Settings/Settings.jsx +++ b/common/app/routes/Settings/Settings.jsx @@ -8,6 +8,7 @@ import FA from 'react-fontawesome'; import ns from './ns.json'; import { FullWidthRow, Spacer, Loader } from '../../helperComponents'; +import { Link } from '../../Router'; import AboutSettings from './components/About-Settings.jsx'; import InternetSettings from './components/Internet-Settings.jsx'; import EmailSettings from './components/Email-Settings.jsx'; @@ -70,6 +71,7 @@ export class Settings extends React.Component { componentWillMount() { this.props.updateTitle('Settings'); } + componentWillReceiveProps({ username, showLoading, hardGoTo }) { if (!username && !showLoading) { hardGoTo('/signup'); @@ -87,16 +89,17 @@ export class Settings extends React.Component { return (
    - + + +