curriculum/challenges/english/blocks/lab-product-landing-page/587d78af367417b2b2512b04.md
Objective: Fulfill the user stories below and get all the tests to pass to complete the lab.
User Stories:
header element with a corresponding id="header".header element with a corresponding id="header-img" (A logo would make a good image here).#header element, you should have a nav element with a corresponding id="nav-bar".nav element, each with the class nav-link..nav-link button in the nav element, you should be taken to the corresponding section of the landing page.id="video".form element with a corresponding id="form".input field with id="email" where you can enter an email address.#email input field should have placeholder text to let users know what the field is for.#email input field should use HTML5 validation to confirm that the entered text is an email address.input with a corresponding id="submit".#submit element, the email should be submitted to a static page (use this mock URL: https://www.freecodecamp.org/email-submit).Note: Be sure to link your stylesheet in your HTML and apply your CSS.
You should have a header element with an id of header.
const el = document.getElementById('header');
assert.exists(el);
assert.strictEqual(el.tagName, 'HEADER');
You should have an img element with an id of header-img.
const el = document.getElementById('header-img');
assert.exists(el);
assert.strictEqual(el.tagName, 'IMG');
Your #header-img should be a descendant of the #header.
const els = document.querySelectorAll('#header #header-img');
assert.isNotEmpty(els);
Your #header-img should have a src attribute.
const el = document.getElementById('header-img');
assert.exists(el);
assert.exists(el.src);
Your #header-img's src value should be a valid URL (starts with http).
const el = document.getElementById('header-img');
assert.exists(el);
assert.isNotEmpty(el.getAttribute('src'));
assert.match(el.src, /^http/);
You should have a nav element with an id of nav-bar.
const el = document.getElementById('nav-bar');
assert.exists(el);
assert.strictEqual(el.tagName, "NAV");
Your #nav-bar should be a descendant of the #header.
const els = document.querySelectorAll('#header #nav-bar');
assert.isNotEmpty(els);
You should have at least 3 .nav-link elements within the #nav-bar.
const els = document.querySelectorAll('#nav-bar .nav-link');
assert.isAtLeast(els.length, 3);
Each .nav-link element should have an href attribute.
const els = document.querySelectorAll('.nav-link')
els.forEach(el => {
assert.exists(el.href);
assert.isNotEmpty(el.href);
})
assert.isNotEmpty(els);
Each .nav-link element should link to a corresponding element on the landing page (has an href with a value of another element's id. e.g. #footer).
const els = document.querySelectorAll('.nav-link')
els.forEach(el => {
const linkDestination = el.getAttribute('href').slice(1)
assert.exists(document.getElementById(linkDestination));
})
assert.isNotEmpty(els);
You should have a video or iframe element with an id of video.
const el = document.getElementById('video');
assert.exists(el);
assert.oneOf(el.tagName, ['VIDEO','IFRAME']);
Your #video should have a src attribute.
let el = document.getElementById('video')
const sourceNode = el.children;
let sourceElement = null;
if (sourceNode.length) {
sourceElement = [...video.children].filter(el => el.localName === 'source')[0];
}
if (sourceElement) {
el = sourceElement;
}
assert.isTrue(el.hasAttribute('src'));
You should have a form element with an id of form.
const el = document.getElementById('form');
assert.exists(el);
assert.strictEqual(el.tagName, "FORM");
You should have an input element with an id of email.
const el = document.getElementById('email');
assert.exists(el);
assert.strictEqual(el.tagName, "INPUT");
Your #email should be a descendant of the #form.
const els = document.querySelectorAll('#form #email')
assert.isNotEmpty(els);
Your #email should have the placeholder attribute with placeholder text.
const el = document.getElementById('email');
assert.exists(el);
assert.exists(el.placeholder);
assert.isNotEmpty(el.placeholder);
Your #email should use HTML5 validation by setting its type to email.
const el = document.getElementById('email');
assert.exists(el);
assert.strictEqual(el.type, "email");
You should have an input element with an id of submit.
const el = document.getElementById('submit');
assert.exists(el);
assert.strictEqual(el.tagName, "INPUT");
Your #submit should be a descendant of the #form.
const els = document.querySelectorAll('#form #submit');
assert.isNotEmpty(els);
Your #submit should have a type of submit.
const el = document.getElementById('submit');
assert.exists(el);
assert.strictEqual(el.type, 'submit');
Your #form should have an action attribute of https://www.freecodecamp.org/email-submit.
const el = document.getElementById('form');
assert.exists(el);
assert.strictEqual(el.action, 'https://www.freecodecamp.org/email-submit');
Your #email should have a name attribute of email.
const el = document.getElementById('email');
assert.exists(el);
assert.strictEqual(el.name, 'email');
Your #nav-bar should always be at the top of the viewport.
const timeout = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
const header = document.getElementById('header');
const headerChildren = header.children;
const navbarCandidates = [header, ...headerChildren];
// Return smallest top position of all navbar candidates
const getNavbarPosition = (candidates = []) => {
return candidates.reduce(
(min, candidate) =>
Math.min(min, Math.abs(candidate?.getBoundingClientRect().top)),
Infinity
);
};
assert.approximately(
getNavbarPosition(navbarCandidates),
0,
15,
'#header or one of its children should be at the top of the viewport '
);
window.scroll(0, 500);
await timeout(1);
assert.approximately(
getNavbarPosition(navbarCandidates),
0,
15,
'#header or one of its children should be at the top of the ' +
'viewport even after scrolling '
);
window.scroll(0, 0);
Your Product Landing Page should use at least one media query.
const htmlSourceAttr = Array.from(document.querySelectorAll('source')).map(el => el.getAttribute('media'))
const cssCheck = new __helpers.CSSHelp(document).getCSSRules('media');
assert.isTrue(cssCheck.length > 0 || htmlSourceAttr.length > 0);
Your Product Landing Page should use CSS Flexbox at least once.
const hasFlex = (rule) => ["flex", "inline-flex"].includes(rule.style?.display)
const stylesheet = new __helpers.CSSHelp(document).getStyleSheet()
const cssRules = new __helpers.CSSHelp(document).styleSheetToCssRulesArray(stylesheet)
const mediaRules = new __helpers.CSSHelp(document).getCSSRules('media')
const usesFlex = cssRules.find(rule => hasFlex(rule))
const usesFlexMedia = mediaRules.find(mediaRule => {
return [...mediaRule.cssRules].find(rule => hasFlex(rule))
})
assert.exists(usesFlex || usesFlexMedia);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Product Landing Page</title>
</head>
<body>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="styles.css" />
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css"
integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous" />
</head>
<body>
<div id="page-wrapper">
<header id="header">
<div class="logo">
</div>
<nav id="nav-bar">
<ul>
<li><a class="nav-link" href="#features">Features</a></li>
<li><a class="nav-link" href="#how-it-works">How It Works</a></li>
<li><a class="nav-link" href="#pricing">Pricing</a></li>
</ul>
</nav>
</header>
<div class="container"></div>
<section id="hero">
<h2>Handcrafted, home-made masterpieces</h2>
<form id="form" action="https://www.freecodecamp.org/email-submit">
<input name="email" id="email" type="email" placeholder="Enter your email address" required />
<input id="submit" type="submit" value="Get Started" class="btn" />
</form>
</section>
<div class="container">
<section id="features">
<div class="grid">
<div class="icon"><i class="fa fa-3x fa-fire"></i></div>
<div class="desc">
<h2>Premium Materials</h2>
<p>
Our trombones use the shiniest brass which is sourced locally.
This will increase the longevity of your purchase.
</p>
</div>
</div>
<div class="grid">
<div class="icon"><i class="fa fa-3x fa-truck"></i></div>
<div class="desc">
<h2>Fast Shipping</h2>
<p>
We make sure you receive your trombone as soon as we have
finished making it. We also provide free returns if you are not
satisfied.
</p>
</div>
</div>
<div class="grid">
<div class="icon">
<i class="fa fa-3x fa-battery-full" aria-hidden="true"></i>
</div>
<div class="desc">
<h2>Quality Assurance</h2>
<p>
For every purchase you make, we will ensure there are no damages
or faults and we will check and test the pitch of your
instrument.
</p>
</div>
</div>
</section>
<section id="how-it-works">
<iframe id="video" height="315"
src="https://www.youtube-nocookie.com/embed/y8Yv4pnO7qc?rel=0&controls=0&showinfo=0"
frameborder="0" allowfullscreen></iframe>
</section>
<section id="pricing">
<div class="product" id="tenor">
<div class="level">Tenor Trombone</div>
<h2>$600</h2>
<ol>
<li>Good for beginners</li>
<li>Excellent sound quality</li>
<li>Great for Jazz Bands</li>
<li>Nice and shiny</li>
</ol>
<button class="btn">Select</button>
</div>
<div class="product" id="bass">
<div class="level">Bass Trombone</div>
<h2>$900</h2>
<ol>
<li>Sound quality is unmatched</li>
<li>Best for professionals</li>
<li>Absolutely stunning</li>
<li>Durable and long lasting</li>
</ol>
<button class="btn">Select</button>
</div>
<div class="product" id="valve">
<div class="level">Valve Trombone</div>
<h2>$1200</h2>
<ol>
<li>Plays similar to a Trumpet</li>
<li>Great for Jazz Bands</li>
<li>Beautiful in sound and appearance</li>
<li>Just amazing</li>
</ol>
<button class="btn">Select</button>
</div>
</section>
<footer>
<ul>
<li><a href="#">Privacy</a></li>
<li><a href="#">Terms</a></li>
<li><a href="#">Contact</a></li>
</ul>
<span>Copyright 2016, Original Trombones</span>
</footer>
</div>
</div>
</body>
</html>
/** global element styling **/
@import 'https://fonts.googleapis.com/css?family=Lato:400,700';
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: #eee;
font-family: 'Lato', sans-serif;
}
#page-wrapper {
position: relative;
}
li {
list-style: none;
}
a {
color: #000;
text-decoration: none;
}
/** global classes styling **/
.container {
max-width: 1000px;
width: 100%;
margin: 0 auto;
}
.btn {
padding: 0 20px;
height: 40px;
font-size: 1em;
font-weight: 900;
text-transform: uppercase;
border: 3px black solid;
border-radius: 2px;
background: transparent;
cursor: pointer;
}
.grid {
display: flex;
}
header {
position: fixed;
top: 0;
min-height: 75px;
padding: 0px 20px;
display: flex;
justify-content: space-around;
align-items: center;
background-color: #eee;
}
@media (max-width: 600px) {
header {
flex-wrap: wrap;
}
}
.logo {
width: 60vw;
}
@media (max-width: 650px) {
.logo {
margin-top: 15px;
width: 100%;
position: relative;
}
}
.logo>img {
width: 100%;
height: 100%;
max-width: 300px;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
margin-left: 20px;
}
@media (max-width: 650px) {
.logo>img {
margin: 0 auto;
}
}
nav {
font-weight: 400;
}
@media (max-width: 650px) {
nav {
margin-top: 10px;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 0 50px;
}
nav li {
padding-bottom: 5px;
}
}
nav>ul {
width: 35vw;
display: flex;
flex-direction: row;
justify-content: space-around;
}
@media (max-width: 650px) {
nav>ul {
flex-direction: column;
}
}
#hero {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
height: 200px;
margin-top: 50px;
}
#hero>h2 {
margin-bottom: 20px;
word-wrap: break-word;
}
#hero input[type='email'] {
max-width: 275px;
width: 100%;
padding: 5px;
}
#hero input[type='submit'] {
max-width: 150px;
width: 100%;
height: 30px;
margin: 15px 0;
border: 0;
background-color: #f1c40f;
}
#hero input[type='submit']:hover {
background-color: orange;
transition: background-color 1s;
}
@media (max-width: 650px) {
#hero {
margin-top: 120px;
}
}
#features {
margin-top: 30px;
}
#features .icon {
display: flex;
align-items: center;
justify-content: center;
height: 125px;
width: 20vw;
color: darkorange;
}
@media (max-width: 550px) {
#features .icon {
display: none;
}
}
#features .desc {
display: flex;
flex-direction: column;
justify-content: center;
height: 125px;
width: 80vw;
padding: 5px;
}
@media (max-width: 550px) {
#features .desc {
width: 100%;
text-align: center;
padding: 0;
height: 150px;
}
}
@media (max-width: 650px) {
#features {
margin-top: 0;
}
}
#how-it-works {
margin-top: 50px;
display: flex;
justify-content: center;
}
#how-it-works>iframe {
max-width: 560px;
width: 100%;
}
#pricing {
margin-top: 60px;
display: flex;
flex-direction: row;
justify-content: center;
}
.product {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
width: calc(100% / 3);
margin: 10px;
border: 1px solid #000;
border-radius: 3px;
}
.product>.level {
background-color: #ddd;
color: black;
padding: 15px 0;
width: 100%;
text-transform: uppercase;
font-weight: 700;
}
.product>h2 {
margin-top: 15px;
}
.product>ol {
margin: 15px 0;
}
.product>ol>li {
padding: 5px 0;
}
.product>button {
border: 0;
margin: 15px 0;
background-color: #f1c40f;
font-weight: 400;
}
.product>button:hover {
background-color: orange;
transition: background-color 1s;
}
@media (max-width: 800px) {
#pricing {
flex-direction: column;
}
.product {
max-width: 300px;
width: 100%;
margin: 0 auto;
margin-bottom: 10px;
}
}
footer {
margin-top: 30px;
background-color: #ddd;
padding: 20px;
}
footer>ul {
display: flex;
justify-content: flex-end;
}
footer>ul>li {
padding: 0 10px;
}
footer>span {
margin-top: 5px;
display: flex;
justify-content: flex-end;
font-size: 0.9em;
color: #444;
}