feat(client): shinny new landing page 🎉 (#39400)
Co-authored-by: Mrugesh Mohapatra <1884376+raisedadead@users.noreply.github.com> Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
16
client/package-lock.json
generated
16
client/package-lock.json
generated
@ -7310,6 +7310,11 @@
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz",
|
||||
"integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q=="
|
||||
},
|
||||
"eventlistener": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eventlistener/-/eventlistener-0.0.1.tgz",
|
||||
"integrity": "sha1-7Suqu4UiJ68rz4iRUscsY8pTLrg="
|
||||
},
|
||||
"events": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
|
||||
@ -17398,6 +17403,17 @@
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
},
|
||||
"react-lazy-load": {
|
||||
"version": "3.1.13",
|
||||
"resolved": "https://registry.npmjs.org/react-lazy-load/-/react-lazy-load-3.1.13.tgz",
|
||||
"integrity": "sha512-eAVNUn3vhNj79Iv04NOCwy/sCLyqDEhL3j9aJKV7VJuRBDg6rCiB+BIWHuG7VXJGCgb//6nX/soR8PTyWRhFvQ==",
|
||||
"requires": {
|
||||
"eventlistener": "0.0.1",
|
||||
"lodash.debounce": "^4.0.0",
|
||||
"lodash.throttle": "^4.0.0",
|
||||
"prop-types": "^15.5.8"
|
||||
}
|
||||
},
|
||||
"react-lifecycles-compat": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
||||
|
@ -56,6 +56,7 @@
|
||||
"react-hotkeys": "^2.0.0",
|
||||
"react-identicons": "^1.1.7",
|
||||
"react-instantsearch-dom": "^6.0.0-beta.0",
|
||||
"react-lazy-load": "^3.1.13",
|
||||
"react-monaco-editor": "^0.31.0",
|
||||
"react-redux": "^5.0.7",
|
||||
"react-reflex": "^3.0.18",
|
||||
|
34
client/src/assets/icons/LinkButton.js
Normal file
34
client/src/assets/icons/LinkButton.js
Normal file
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function LinkButton(props) {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden='true'
|
||||
height='20px'
|
||||
version='1.1'
|
||||
viewBox='0 0 16 20'
|
||||
width='18px'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
xmlnsXlink='http://www.w3.org/1999/xlink'
|
||||
{...props}
|
||||
fill='inherit'
|
||||
>
|
||||
<g>
|
||||
<polygon
|
||||
points={
|
||||
'-2.68014473e-15 -1.06357708e-13 2.01917516 ' +
|
||||
'-1.06357708e-13 8.99824941 9.00746464 2.01917516 ' +
|
||||
'18.0149293 -2.66453526e-15 18.0149293 7.00955027 9'
|
||||
}
|
||||
/>
|
||||
<polygon
|
||||
points={
|
||||
'7.99971435 -1.06357708e-13 10.0188895 ' +
|
||||
'-1.06357708e-13 16.9979638 9.00746464 ' +
|
||||
'10.0188895 18.0149293 7.99971435 18.0149293 15.0092646 9'
|
||||
}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
@ -7,7 +7,7 @@ function SpotifyLogo(props) {
|
||||
return (
|
||||
<svg
|
||||
id='amazon-logo'
|
||||
viewBox='93.907 230.751 1400 572'
|
||||
viewBox='93.907 250 1350 472'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
xmlnsXlink='http://www.w3.org/1999/xlink'
|
||||
{...props}
|
@ -7,7 +7,7 @@ function AppleLogo(props) {
|
||||
return (
|
||||
<svg
|
||||
id='apple-logo'
|
||||
viewBox='170 0 1000 572'
|
||||
viewBox='450 0 500 650'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
xmlnsXlink='http://www.w3.org/1999/xlink'
|
||||
{...props}
|
@ -1,17 +1,19 @@
|
||||
/* eslint-disable max-len */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const propTypes = {};
|
||||
const propTypes = { fill: PropTypes.string };
|
||||
|
||||
function AsSeenLogo(props) {
|
||||
const fill = props.fill === 'dark' ? 'var(--gray-75)' : 'var(--gray-15)';
|
||||
return (
|
||||
<svg
|
||||
id='featured-logos'
|
||||
viewBox='0 0 1700 340'
|
||||
viewBox='0 40 1700 200'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
xmlnsXlink='http://www.w3.org/1999/xlink'
|
||||
{...props}
|
||||
fill='var(--quaternary-color)'
|
||||
fill={fill}
|
||||
>
|
||||
<defs>
|
||||
<path
|
@ -7,7 +7,7 @@ function GoogleLogo(props) {
|
||||
return (
|
||||
<svg
|
||||
id='google-logo'
|
||||
viewBox='1635 200 1400 600'
|
||||
viewBox='1635 200 1185 600'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
xmlnsXlink='http://www.w3.org/1999/xlink'
|
||||
{...props}
|
@ -7,7 +7,7 @@ function MicrosoftLogo(props) {
|
||||
return (
|
||||
<svg
|
||||
id='microsoft-logo'
|
||||
viewBox='939.813 1145 1400 572'
|
||||
viewBox='939.813 1185 1400 472'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
xmlnsXlink='http://www.w3.org/1999/xlink'
|
||||
{...props}
|
@ -7,7 +7,7 @@ function SpotifyLogo(props) {
|
||||
return (
|
||||
<svg
|
||||
id='spotify-logo'
|
||||
viewBox='3018 235.379 1579 572'
|
||||
viewBox='3018 235.379 1525 572'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
xmlnsXlink='http://www.w3.org/1999/xlink'
|
||||
{...props}
|
BIN
client/src/assets/images/landing/Emma.png
Normal file
BIN
client/src/assets/images/landing/Emma.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 82 KiB |
BIN
client/src/assets/images/landing/Sarah.png
Normal file
BIN
client/src/assets/images/landing/Sarah.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 63 KiB |
BIN
client/src/assets/images/landing/Shawn.png
Normal file
BIN
client/src/assets/images/landing/Shawn.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 63 KiB |
BIN
client/src/assets/images/landing/wide-image.png
Normal file
BIN
client/src/assets/images/landing/wide-image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 331 KiB |
@ -23,8 +23,6 @@ import {
|
||||
} from '../redux';
|
||||
import { Spacer, Loader, FullWidthRow } from '../components/helpers';
|
||||
|
||||
import './showuser.css';
|
||||
|
||||
const propTypes = {
|
||||
email: PropTypes.string,
|
||||
isSignedIn: PropTypes.bool,
|
||||
|
@ -1,4 +0,0 @@
|
||||
/* remove bootstrap margin*/
|
||||
.row {
|
||||
margin: 0;
|
||||
}
|
@ -43,7 +43,7 @@ function Intro({
|
||||
<Row>
|
||||
<Col sm={10} smOffset={1} xs={12}>
|
||||
<Spacer />
|
||||
<h1 className='text-center big-heading'>
|
||||
<h1 className='text-center '>
|
||||
{name ? `Welcome back, ${name}.` : `Welcome to freeCodeCamp.org`}
|
||||
</h1>
|
||||
<Spacer />
|
||||
@ -101,9 +101,7 @@ function Intro({
|
||||
<Row>
|
||||
<Col sm={8} smOffset={2} xs={12}>
|
||||
<Spacer />
|
||||
<h1 className='big-heading'>
|
||||
Welcome to freeCodeCamp's curriculum.
|
||||
</h1>
|
||||
<h1>Welcome to freeCodeCamp's curriculum.</h1>
|
||||
<Spacer size={1} />
|
||||
</Col>
|
||||
<IntroDescription />
|
||||
|
@ -9,14 +9,6 @@
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.logo-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.logo-row h2 {
|
||||
height: 35px;
|
||||
padding: 0 10px 0 10px;
|
||||
@ -27,11 +19,6 @@
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
#learn-app-wrapper h2.medium-heading {
|
||||
margin-bottom: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.quote-partial .blockquote {
|
||||
font-size: 1.3rem;
|
||||
border: none;
|
||||
|
@ -15,7 +15,7 @@ function CurrentChallengeLink({ children, isLargeBtn }) {
|
||||
if (isLargeBtn) {
|
||||
classNames = 'btn btn-lg btn-primary btn-block';
|
||||
} else {
|
||||
classNames = 'btn btn-cta-big btn-primary btn-block';
|
||||
classNames = 'btn btn-primary btn-block';
|
||||
}
|
||||
return (
|
||||
<a className={classNames} href={`${apiLocation}${currentChallengeApi}`}>
|
||||
|
65
client/src/components/helpers/ImageLoader.js
Normal file
65
client/src/components/helpers/ImageLoader.js
Normal file
@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
|
||||
import './image-loader.css';
|
||||
import LazyLoad from 'react-lazy-load';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const propTypes = {
|
||||
alt: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
height: PropTypes.number,
|
||||
loadedClassName: PropTypes.string,
|
||||
loadingClassName: PropTypes.string,
|
||||
src: PropTypes.string,
|
||||
width: PropTypes.number
|
||||
};
|
||||
|
||||
class ImageLoader extends React.Component {
|
||||
// initial state: image loaded stage
|
||||
state = {
|
||||
loaded: false
|
||||
};
|
||||
|
||||
// define our loading and loaded image classes
|
||||
static defaultProps = {
|
||||
className: '',
|
||||
loadingClassName: 'img-loading',
|
||||
loadedClassName: 'img-loaded'
|
||||
};
|
||||
|
||||
// image onLoad handler to update state to loaded
|
||||
onLoad = () => {
|
||||
this.setState(() => ({ loaded: true }));
|
||||
};
|
||||
|
||||
render() {
|
||||
let {
|
||||
className,
|
||||
loadedClassName,
|
||||
loadingClassName,
|
||||
alt,
|
||||
src,
|
||||
width,
|
||||
height
|
||||
} = this.props;
|
||||
|
||||
className = `${className} ${
|
||||
this.state.loaded ? loadedClassName : loadingClassName
|
||||
}`;
|
||||
|
||||
return (
|
||||
<LazyLoad
|
||||
debounce={false}
|
||||
height={height}
|
||||
offsetVertical={100}
|
||||
width={width}
|
||||
>
|
||||
{/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */}
|
||||
<img alt={alt} className={className} onLoad={this.onLoad} src={src} />
|
||||
{/* eslint-enable jsx-a11y/no-noninteractive-element-interactions */}
|
||||
</LazyLoad>
|
||||
);
|
||||
}
|
||||
}
|
||||
ImageLoader.propTypes = propTypes;
|
||||
export default ImageLoader;
|
23
client/src/components/helpers/image-loader.css
Normal file
23
client/src/components/helpers/image-loader.css
Normal file
@ -0,0 +1,23 @@
|
||||
@keyframes fadeInImg {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.img-loading {
|
||||
opacity: 0;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.img-loaded {
|
||||
animation: fadeInImg cubic-bezier(0.23, 1, 0.32, 1) 1;
|
||||
position: relative;
|
||||
opacity: 0;
|
||||
animation-fill-mode: forwards;
|
||||
animation-duration: 0.7s;
|
||||
animation-delay: 0.1s;
|
||||
}
|
@ -6,3 +6,4 @@ export { default as SkeletonSprite } from './SkeletonSprite';
|
||||
export { default as Spacer } from './Spacer';
|
||||
export { default as Link } from './Link';
|
||||
export { default as CurrentChallengeLink } from './CurrentChallengeLink';
|
||||
export { default as ImageLoader } from './ImageLoader';
|
||||
|
18
client/src/components/landing/components/AsSeenIn.js
Normal file
18
client/src/components/landing/components/AsSeenIn.js
Normal file
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { Col, Row } from '@freecodecamp/react-bootstrap';
|
||||
|
||||
import { AsFeatureLogo } from '../../../assets/images/components';
|
||||
|
||||
const AsSeenIn = () => (
|
||||
<Row className='as-seen-in'>
|
||||
<Col sm={8} smOffset={2} xs={12}>
|
||||
<div className='text-center'>
|
||||
<h1 className='big-heading'>As seen in:</h1>
|
||||
<AsFeatureLogo fill='light' />
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
|
||||
AsSeenIn.displayName = 'AsSeenIn';
|
||||
export default AsSeenIn;
|
19
client/src/components/landing/components/BigCallToAction.js
Normal file
19
client/src/components/landing/components/BigCallToAction.js
Normal file
@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Login from '../../Header/components/Login';
|
||||
|
||||
const propTypes = {
|
||||
page: PropTypes.string
|
||||
};
|
||||
|
||||
const BigCallToAction = ({ page }) => (
|
||||
<Login block={true} data-test-label={`${page}-big-cta`}>
|
||||
{page === 'landing'
|
||||
? "Get started (it's free)"
|
||||
: "Sign in to save your progress (it's free)"}
|
||||
</Login>
|
||||
);
|
||||
|
||||
BigCallToAction.displayName = 'BigCallToAction';
|
||||
BigCallToAction.propTypes = propTypes;
|
||||
export default BigCallToAction;
|
47
client/src/components/landing/components/Certifications.js
Normal file
47
client/src/components/landing/components/Certifications.js
Normal file
@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Col, Row } from '@freecodecamp/react-bootstrap';
|
||||
import { uniq } from 'lodash';
|
||||
import { Spacer, Link } from '../../helpers';
|
||||
import LinkButton from '../../../assets/icons/LinkButton';
|
||||
import BigCallToAction from './BigCallToAction';
|
||||
|
||||
const propTypes = {
|
||||
nodes: PropTypes.array,
|
||||
page: PropTypes.string
|
||||
};
|
||||
|
||||
const Certifications = ({ nodes, page }) => {
|
||||
const superBlocks = uniq(nodes.map(node => node.superBlock)).filter(
|
||||
cert => cert !== 'Coding Interview Prep'
|
||||
);
|
||||
|
||||
return (
|
||||
<Row className='certification-section'>
|
||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<h1 className='big-heading'>Earn free verified certifications in:</h1>
|
||||
<ul data-test-label='certifications'>
|
||||
{superBlocks.map((superBlock, i) => (
|
||||
<li key={i}>
|
||||
<Link
|
||||
className='btn link-btn btn-lg'
|
||||
state={{ superBlock: superBlock }}
|
||||
to={`/learn`}
|
||||
>
|
||||
{superBlock}
|
||||
<LinkButton />
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Spacer />
|
||||
<BigCallToAction page={page} />
|
||||
<Spacer />
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
Certifications.displayName = 'Certifications';
|
||||
Certifications.propTypes = propTypes;
|
||||
export default Certifications;
|
@ -1,26 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
AmazonLogo,
|
||||
AppleLogo,
|
||||
MicrosoftLogo,
|
||||
SpotifyLogo,
|
||||
GoogleLogo
|
||||
} from '../../../assets/images';
|
||||
|
||||
import './companyLogos.css';
|
||||
|
||||
export const CompanyLogos = () => {
|
||||
return (
|
||||
<div className='logo-row'>
|
||||
<GoogleLogo />
|
||||
<MicrosoftLogo />
|
||||
<AppleLogo />
|
||||
<SpotifyLogo />
|
||||
<AmazonLogo />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CompanyLogos.displayName = 'CompanyLogos';
|
||||
export default CompanyLogos;
|
78
client/src/components/landing/components/LandingTop.js
Normal file
78
client/src/components/landing/components/LandingTop.js
Normal file
@ -0,0 +1,78 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Media from 'react-responsive';
|
||||
import { Col, Row } from '@freecodecamp/react-bootstrap';
|
||||
import { Spacer, ImageLoader } from '../../helpers';
|
||||
import wideImg from '../../../assets/images/landing/wide-image.png';
|
||||
import Login from '../../Header/components/Login';
|
||||
import {
|
||||
AmazonLogo,
|
||||
AppleLogo,
|
||||
MicrosoftLogo,
|
||||
SpotifyLogo,
|
||||
GoogleLogo
|
||||
} from '../../../assets/images/components';
|
||||
|
||||
const propTypes = {
|
||||
page: PropTypes.string
|
||||
};
|
||||
|
||||
const LARGE_SCREEN_SIZE = 1200;
|
||||
|
||||
function landingTop({ page }) {
|
||||
const landingImageSection = (
|
||||
<Media minWidth={LARGE_SCREEN_SIZE}>
|
||||
<Spacer size={2} />
|
||||
<ImageLoader
|
||||
alt='Freecodecamp students at a local study'
|
||||
className='landing-page-image'
|
||||
height={442}
|
||||
src={wideImg}
|
||||
width={750}
|
||||
/>
|
||||
<p className='text-center caption'>
|
||||
freeCodeCamp students at a local study group in South Korea.
|
||||
</p>
|
||||
</Media>
|
||||
);
|
||||
const BigCallToAction = (
|
||||
<Login block={true} data-test-label={`${page}-big-cta`}>
|
||||
{page === 'landing'
|
||||
? "Get started (it's free)"
|
||||
: "Sign in to save your progress (it's free)"}
|
||||
</Login>
|
||||
);
|
||||
return (
|
||||
<div className='landing-top'>
|
||||
<Row>
|
||||
<Spacer />
|
||||
<Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<h1 className='big-heading' data-test-label={`${page}-header`}>
|
||||
Learn to code at home.
|
||||
</h1>
|
||||
<h1 className='big-heading '>Build projects.</h1>
|
||||
<h1 className='big-heading'>Earn certifications.</h1>
|
||||
<h2>
|
||||
Since 2014, more than 40,000 freeCodeCamp.org graduates have gotten
|
||||
jobs at tech companies including:
|
||||
</h2>
|
||||
<div className='logo-row'>
|
||||
<AppleLogo />
|
||||
<GoogleLogo />
|
||||
<MicrosoftLogo />
|
||||
<AmazonLogo />
|
||||
<SpotifyLogo />
|
||||
</div>
|
||||
<Spacer />
|
||||
{BigCallToAction}
|
||||
{landingImageSection}
|
||||
<Spacer />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
landingTop.displayName = 'LandingTop';
|
||||
landingTop.propTypes = propTypes;
|
||||
export default landingTop;
|
61
client/src/components/landing/components/Testimonials.js
Normal file
61
client/src/components/landing/components/Testimonials.js
Normal file
@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { ImageLoader } from '../../helpers';
|
||||
import testimonialsMeta from '../landingMeta';
|
||||
|
||||
const propTypes = {
|
||||
page: PropTypes.string
|
||||
};
|
||||
|
||||
const Testimonials = () => {
|
||||
const campers = Object.keys(testimonialsMeta);
|
||||
|
||||
return (
|
||||
<div className='testimonials'>
|
||||
<h1 className='big-heading text-center'>
|
||||
Here is what our alumini say about freeCodeCamp:
|
||||
</h1>
|
||||
<div className='testimonials-row' data-test-label='testimonial-cards'>
|
||||
{campers.map((camper, i) => {
|
||||
let {
|
||||
name,
|
||||
country,
|
||||
position,
|
||||
company,
|
||||
testimony
|
||||
} = testimonialsMeta[camper];
|
||||
let imageSource = require(`../../../assets/images/landing/${camper}.png`);
|
||||
return (
|
||||
<div className='testimonial-card' key={i}>
|
||||
<div className='testimonial-card-header'>
|
||||
<ImageLoader
|
||||
alt={`${name}`}
|
||||
className='testimonial-image'
|
||||
src={imageSource}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='testimonials-footer'>
|
||||
<div className='testimonial-meta'>
|
||||
<p>
|
||||
{' '}
|
||||
<strong>{name}</strong> in {country}
|
||||
</p>
|
||||
<p>
|
||||
{position} at <strong>{company}</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div className='testimony'>{testimony}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Testimonials.displayName = 'Testimonals';
|
||||
Testimonials.propTypes = propTypes;
|
||||
export default Testimonials;
|
@ -1,32 +0,0 @@
|
||||
.logo-row svg {
|
||||
height: 25px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
@media (min-width: 343px) {
|
||||
.logo-row svg {
|
||||
height: 35px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 380px) {
|
||||
.logo-row svg {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 480px) {
|
||||
.logo-row svg {
|
||||
height: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
.logo-row svg {
|
||||
height: 40px;
|
||||
margin: 10px;
|
||||
}
|
||||
.logo-row #apple-logo {
|
||||
height: 35px;
|
||||
}
|
||||
}
|
@ -1,108 +1,49 @@
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
import { Grid, Row, Col } from '@freecodecamp/react-bootstrap';
|
||||
import { Grid } from '@freecodecamp/react-bootstrap';
|
||||
import Helmet from 'react-helmet';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from 'gatsby';
|
||||
import { uniq, partition } from 'lodash';
|
||||
import { Spacer } from '../helpers';
|
||||
import Login from '../Header/components/Login';
|
||||
import CompanyLogos from './components/CompanyLogos';
|
||||
import { AsFeatureLogo } from '../../assets/images';
|
||||
import { graphql, useStaticQuery } from 'gatsby';
|
||||
|
||||
import Testimonials from './components/Testimonials';
|
||||
import LandingTop from './components/LandingTop';
|
||||
import Certifications from './components/Certifications';
|
||||
import AsSeenIn from './components/AsSeenIn';
|
||||
|
||||
import './landing.css';
|
||||
import '../Map/map.css';
|
||||
|
||||
const propTypes = {
|
||||
nodes: PropTypes.array
|
||||
page: PropTypes.string
|
||||
};
|
||||
|
||||
const BigCallToAction = () => (
|
||||
<Row>
|
||||
<Col md={6} mdOffset={3} sm={8} smOffset={2} xs={12}>
|
||||
<Login block={true} data-test-label='landing-big-cta'>
|
||||
Get started (it's free)
|
||||
</Login>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
export const Landing = ({ page = 'landing' }) => {
|
||||
const data = useStaticQuery(graphql`
|
||||
query certifications {
|
||||
challenges: allChallengeNode(
|
||||
filter: { isHidden: { eq: false } }
|
||||
sort: { fields: [superOrder, order, challengeOrder] }
|
||||
) {
|
||||
nodes {
|
||||
superBlock
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const AsFeaturedSection = () => (
|
||||
<Row>
|
||||
<Col sm={8} smOffset={2} xs={12}>
|
||||
<div className='text-center'>
|
||||
<h2 className='medium-heading'>As Featured In:</h2>
|
||||
<AsFeatureLogo />
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
|
||||
export const Landing = ({ nodes }) => {
|
||||
const [superBlocks, rest] = partition(
|
||||
uniq(nodes.map(node => node.superBlock)),
|
||||
name => name !== 'Coding Interview Prep'
|
||||
);
|
||||
const interviewPrep = rest[0];
|
||||
return (
|
||||
<Fragment>
|
||||
<Helmet>
|
||||
<title>Learn to code at home | freeCodeCamp.org</title>
|
||||
</Helmet>
|
||||
<main className='landing-page'>
|
||||
<Spacer />
|
||||
<Grid>
|
||||
<Row>
|
||||
<Col sm={10} smOffset={1} xs={12}>
|
||||
<h1
|
||||
className='big-heading text-center'
|
||||
data-test-label='landing-header'
|
||||
>
|
||||
Welcome to freeCodeCamp.org
|
||||
</h1>
|
||||
<Spacer />
|
||||
<h2 className='medium-heading text-center'>
|
||||
Learn to code at home.
|
||||
</h2>
|
||||
<h2 className='medium-heading text-center'>Build projects.</h2>
|
||||
<h2 className='medium-heading text-center'>
|
||||
Earn certifications.
|
||||
</h2>
|
||||
<h2 className='medium-heading text-center'>
|
||||
Since 2014, more than 40,000 freeCodeCamp.org graduates have
|
||||
gotten jobs at tech companies including:
|
||||
</h2>
|
||||
<CompanyLogos />
|
||||
</Col>
|
||||
</Row>
|
||||
<Spacer />
|
||||
<BigCallToAction />
|
||||
<Spacer />
|
||||
<AsFeaturedSection />
|
||||
<Row>
|
||||
<Col sm={10} smOffset={1} xs={12}>
|
||||
<h2 className='medium-heading'>Certifications:</h2>
|
||||
<ul>
|
||||
{superBlocks.map((superBlock, i) => (
|
||||
<li className={'superblock'} key={i}>
|
||||
<Link state={{ superBlock: superBlock }} to={`/learn`}>
|
||||
<h2 className='medium-heading'>{superBlock}</h2>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Spacer />
|
||||
<h2 className='medium-heading'>Additional Learning:</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<Link state={{ superBlock: interviewPrep }} to={`/learn`}>
|
||||
<h2 className='medium-heading'>{interviewPrep}</h2>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</Col>
|
||||
</Row>
|
||||
<Spacer />
|
||||
<LandingTop page={page} />
|
||||
</Grid>
|
||||
<Grid fluid={true}>
|
||||
<AsSeenIn />
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Testimonials />
|
||||
<Certifications nodes={data.challenges.nodes} page={page} />
|
||||
</Grid>
|
||||
</main>
|
||||
</Fragment>
|
||||
|
@ -9,21 +9,203 @@
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.logo-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
align-content: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.landing-page ul {
|
||||
list-style: none;
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
#featured-logos {
|
||||
margin-top: 10px;
|
||||
max-width: 600px;
|
||||
}
|
||||
.landing-top h1:first-child {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.landing-page h1,
|
||||
.landing-page h2,
|
||||
.landing-page p {
|
||||
font-family: 'Lato', sans-serif;
|
||||
}
|
||||
|
||||
p.caption {
|
||||
margin: 10px 0 0 0;
|
||||
font-size: 0.8rem;
|
||||
color: var(--quaternary-color);
|
||||
}
|
||||
|
||||
.as-seen-in {
|
||||
background-color: var(--gray-75);
|
||||
color: var(--gray-15);
|
||||
}
|
||||
|
||||
.as-seen-in h1 {
|
||||
color: var(--gray-15);
|
||||
}
|
||||
|
||||
.landing-page h1 {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.campers-images {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.campers-images {
|
||||
display: inline-block !important;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.logo-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
align-content: center;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.logo-row svg {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
@media (min-width: 370px) {
|
||||
.logo-row svg {
|
||||
margin: 5px 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 480px) {
|
||||
.logo-row svg {
|
||||
height: 25px;
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 550px) {
|
||||
.logo-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.logo-row svg {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.cta-landing-section h2 {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* testimonials */
|
||||
|
||||
.testimonials-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.testimonials p,
|
||||
.testimonials strong {
|
||||
font-family: 'Lato', sans-serif;
|
||||
font-size: 1.1rem;
|
||||
margin: 0;
|
||||
color: var(--gray-75);
|
||||
}
|
||||
.testimonial-meta p:last-child {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.testimonial-card {
|
||||
max-width: 350px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 10px 10px 50px 10px;
|
||||
background: var(--gray-00);
|
||||
-webkit-box-shadow: 0 3px 13px 1px rgba(0, 0, 0, 0.09);
|
||||
box-shadow: 0 3px 13px 1px rgba(0, 0, 0, 0.09);
|
||||
}
|
||||
|
||||
.testimonial-image,
|
||||
.landing-page-image {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.testimonial-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 10px 0px 30px 0px;
|
||||
}
|
||||
|
||||
.testimonial-meta p,
|
||||
.testimonial-meta strong {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.testimonial-card-header {
|
||||
height: 100%;
|
||||
padding-bottom: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.testimonial-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.testimonials-footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 25px;
|
||||
text-align: justify;
|
||||
justify-content: center;
|
||||
}
|
||||
.landing-top,
|
||||
.as-seen-in,
|
||||
.certification-section,
|
||||
.testimonials {
|
||||
padding: 4vw 0;
|
||||
}
|
||||
|
||||
@media (min-width: 500px) {
|
||||
.cta-landing-section h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.testimonial-card {
|
||||
flex-direction: row;
|
||||
width: auto;
|
||||
max-width: none;
|
||||
}
|
||||
.testimonial-image {
|
||||
width: auto;
|
||||
height: 100%;
|
||||
}
|
||||
.testimonial-meta {
|
||||
padding: 0px 0px 30px;
|
||||
}
|
||||
.testimonial-meta p,
|
||||
.testimonial-meta strong {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
.testimonials-footer {
|
||||
padding: 40px;
|
||||
}
|
||||
.testimonial-card-header {
|
||||
height: 350px;
|
||||
min-width: 350px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
48
client/src/components/landing/landingMeta.js
Normal file
48
client/src/components/landing/landingMeta.js
Normal file
@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
|
||||
export default {
|
||||
Shawn: {
|
||||
name: 'Shawn Wang',
|
||||
country: 'Singapore',
|
||||
position: 'Software Engineer',
|
||||
company: 'Amazon',
|
||||
testimony: (
|
||||
<p>
|
||||
"It's scary to change careers. I only gained confidence that I could
|
||||
code by working through the hundreds of hours of free lessons on
|
||||
freeCodeCamp. Within a year I had a six-figure job as a Software
|
||||
Engineer.
|
||||
<strong> freeCodeCamp changed my life.</strong>"
|
||||
</p>
|
||||
)
|
||||
},
|
||||
Sarah: {
|
||||
name: 'Sarah Chima',
|
||||
country: 'Nigeria',
|
||||
position: 'Software Engineer',
|
||||
company: 'ChatDesk',
|
||||
testimony: (
|
||||
<p>
|
||||
“<strong>freeCodeCamp was the gateway to my career </strong>
|
||||
as a software developer. The well-structured curriculum took my coding
|
||||
knowledge from a total beginner level to a very confident level. It was
|
||||
everything I needed to land my first dev job at an amazing company."
|
||||
</p>
|
||||
)
|
||||
},
|
||||
Emma: {
|
||||
name: 'Emma Bostian',
|
||||
country: 'Sweden',
|
||||
position: 'Software Engineer',
|
||||
company: 'Spotify',
|
||||
testimony: (
|
||||
<p>
|
||||
"I've always struggled with learning JavaScript. I've taken many courses
|
||||
but freeCodeCamp's course was the one which stuck. Studying JavaScript
|
||||
as well as data structures and algorithms on{' '}
|
||||
<strong>freeCodeCamp gave me the skills</strong> and confidence I needed
|
||||
to land my dream job as a software engineer at Spotify."
|
||||
</p>
|
||||
)
|
||||
}
|
||||
};
|
@ -11,7 +11,7 @@ body {
|
||||
|
||||
.btn-cta-big {
|
||||
max-height: 100%;
|
||||
font-size: 1.3rem;
|
||||
font-size: 1.5rem;
|
||||
white-space: normal;
|
||||
width: 100%;
|
||||
}
|
||||
@ -64,25 +64,13 @@ p {
|
||||
}
|
||||
|
||||
.big-heading {
|
||||
font-size: 1.5rem !important;
|
||||
font-size: 2.5rem !important;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.medium-heading {
|
||||
font-size: 1.25rem !important;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.medium-heading.text-center {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.big-heading {
|
||||
font-size: 1.2rem !important;
|
||||
}
|
||||
.medium-heading {
|
||||
font-size: 1.1rem !important;
|
||||
font-size: 1.5rem !important;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.3rem;
|
||||
@ -99,6 +87,12 @@ p {
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1199px) {
|
||||
.btn-cta-big {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center !important;
|
||||
}
|
||||
@ -160,6 +154,7 @@ input[type='submit'],
|
||||
color: var(--secondary-color);
|
||||
border-radius: 0px;
|
||||
text-decoration: none;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
button:hover,
|
||||
@ -257,6 +252,33 @@ fieldset[disabled] .btn-primary.focus {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.link-btn {
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
font-size: 1rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.link-btn.btn-lg svg {
|
||||
height: 100%;
|
||||
min-height: 20px;
|
||||
min-width: 16px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.link-btn:hover svg {
|
||||
fill: var(--quaternary-background);
|
||||
}
|
||||
|
||||
@media (min-width: 700px) {
|
||||
.link-btn {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.button-group .btn:not(:last-child) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
@ -494,27 +516,3 @@ blockquote .small {
|
||||
#search::placeholder {
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
/* checkbox */
|
||||
|
||||
.checkbox-inline input[type='checkbox'] {
|
||||
margin-left: -30px;
|
||||
}
|
||||
|
||||
.checkbox-inline {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
height: 21px;
|
||||
width: 21px;
|
||||
}
|
||||
|
||||
/* general page styling */
|
||||
.default-page-wrapper {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
@ -1,16 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { graphql } from 'gatsby';
|
||||
|
||||
import Landing from '../components/landing';
|
||||
import { AllChallengeNode } from '../redux/propTypes';
|
||||
|
||||
export const IndexPage = ({
|
||||
data: {
|
||||
allChallengeNode: { nodes }
|
||||
}
|
||||
}) => {
|
||||
return <Landing nodes={nodes} />;
|
||||
export const IndexPage = () => {
|
||||
return <Landing />;
|
||||
};
|
||||
|
||||
const propTypes = {
|
||||
@ -23,16 +18,3 @@ IndexPage.propTypes = propTypes;
|
||||
IndexPage.displayName = 'IndexPage';
|
||||
|
||||
export default IndexPage;
|
||||
|
||||
export const query = graphql`
|
||||
query MyQuery {
|
||||
allChallengeNode(
|
||||
filter: { isHidden: { eq: false } }
|
||||
sort: { fields: [superOrder, order, challengeOrder] }
|
||||
) {
|
||||
nodes {
|
||||
superBlock
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -32,7 +32,7 @@ export default function layoutSelector({ element, props }) {
|
||||
</DefaultLayout>
|
||||
);
|
||||
}
|
||||
if (/^\/donation(\/.*)*|^\/donate(\/.*)*/.test(pathname)) {
|
||||
if (/^\/donation(\/.*)*|^\/$|^\/donate(\/.*)*/.test(pathname)) {
|
||||
return (
|
||||
<DefaultLayout pathname={pathname} useTheme={false}>
|
||||
{element}
|
||||
|
@ -1,16 +1,41 @@
|
||||
/* global cy */
|
||||
const selectors = {
|
||||
heading: "[data-test-label='landing-header']",
|
||||
callToAction: "[data-test-label='landing-big-cta']"
|
||||
callToAction: "[data-test-label='landing-big-cta']",
|
||||
certifications: "[data-test-label='certifications']",
|
||||
testimonials: "[data-test-label='testimonial-cards']",
|
||||
landingPageImage: '.landing-page-image'
|
||||
};
|
||||
|
||||
describe('Landing page', function() {
|
||||
it('renders', function() {
|
||||
cy.visit('/');
|
||||
|
||||
cy.title().should('eq', 'Learn to code at home | freeCodeCamp.org');
|
||||
|
||||
cy.contains(selectors.heading, 'Welcome to freeCodeCamp.org');
|
||||
cy.contains(selectors.heading, 'Learn to code at home.');
|
||||
cy.contains(selectors.callToAction, "Get started (it's free)");
|
||||
cy.get(selectors.callToAction).should('have.length', 2);
|
||||
});
|
||||
|
||||
it('has a visible large image on large viewports', function() {
|
||||
cy.viewport(1200, 660)
|
||||
.get(selectors.landingPageImage)
|
||||
.should('be.visible');
|
||||
|
||||
cy.viewport(1199, 660)
|
||||
.get(selectors.landingPageImage)
|
||||
.should('not.be.visible');
|
||||
});
|
||||
|
||||
it('has 10 certifications', function() {
|
||||
cy.get(selectors.certifications)
|
||||
.children()
|
||||
.its('length')
|
||||
.should('eq', 10);
|
||||
});
|
||||
it('has 3 testimonial cards', function() {
|
||||
cy.get(selectors.testimonials)
|
||||
.children()
|
||||
.its('length')
|
||||
.should('eq', 3);
|
||||
});
|
||||
});
|
||||
|
Reference in New Issue
Block a user