Back to Freecodecamp

Build a Product Landing Page

curriculum/challenges/english/blocks/lab-product-landing-page/587d78af367417b2b2512b04.md

latest17.9 KB
Original Source

--description--

Objective: Fulfill the user stories below and get all the tests to pass to complete the lab.

User Stories:

  1. Your product landing page should have a header element with a corresponding id="header".
  2. You should have an image within the header element with a corresponding id="header-img" (A logo would make a good image here).
  3. Within the #header element, you should have a nav element with a corresponding id="nav-bar".
  4. You should have at least three clickable elements inside the nav element, each with the class nav-link.
  5. When you click a .nav-link button in the nav element, you should be taken to the corresponding section of the landing page.
  6. You should have an embedded product video with id="video".
  7. Your landing page should have a form element with a corresponding id="form".
  8. Within the form, there should be an input field with id="email" where you can enter an email address.
  9. The #email input field should have placeholder text to let users know what the field is for.
  10. The #email input field should use HTML5 validation to confirm that the entered text is an email address.
  11. Within the form, there should be a submit input with a corresponding id="submit".
  12. When you click the #submit element, the email should be submitted to a static page (use this mock URL: https://www.freecodecamp.org/email-submit).
  13. The navbar should always be at the top of the viewport.
  14. Your product landing page should have at least one media query.
  15. Your product landing page should utilize CSS flexbox at least once.

Note: Be sure to link your stylesheet in your HTML and apply your CSS.

--hints--

You should have a header element with an id of header.

js
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.

js
const el = document.getElementById('header-img');
assert.exists(el); 
assert.strictEqual(el.tagName, 'IMG');

Your #header-img should be a descendant of the #header.

js
const els = document.querySelectorAll('#header #header-img');
assert.isNotEmpty(els);

Your #header-img should have a src attribute.

js
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).

js
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.

js
const el = document.getElementById('nav-bar');
assert.exists(el);
assert.strictEqual(el.tagName, "NAV");

Your #nav-bar should be a descendant of the #header.

js
const els = document.querySelectorAll('#header #nav-bar');
assert.isNotEmpty(els);

You should have at least 3 .nav-link elements within the #nav-bar.

js
const els = document.querySelectorAll('#nav-bar .nav-link');
assert.isAtLeast(els.length, 3); 

Each .nav-link element should have an href attribute.

js
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).

js
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.

js
const el = document.getElementById('video');
assert.exists(el); 
assert.oneOf(el.tagName, ['VIDEO','IFRAME']);

Your #video should have a src attribute.

js
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.

js
const el = document.getElementById('form');
assert.exists(el); 
assert.strictEqual(el.tagName, "FORM"); 

You should have an input element with an id of email.

js
const el = document.getElementById('email');
assert.exists(el); 
assert.strictEqual(el.tagName, "INPUT"); 

Your #email should be a descendant of the #form.

js
const els = document.querySelectorAll('#form #email')
assert.isNotEmpty(els);

Your #email should have the placeholder attribute with placeholder text.

js
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.

js
const el = document.getElementById('email');
assert.exists(el); 
assert.strictEqual(el.type, "email");

You should have an input element with an id of submit.

js
const el = document.getElementById('submit');
assert.exists(el); 
assert.strictEqual(el.tagName, "INPUT");

Your #submit should be a descendant of the #form.

js
const els = document.querySelectorAll('#form #submit');
assert.isNotEmpty(els);

Your #submit should have a type of submit.

js
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.

js
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.

js
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.

js
  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.

js
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.

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

--seed--

--seed-contents--

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Product Landing Page</title>
  </head>

<body>

</body>

</html>
css

--solutions--

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&amp;controls=0&amp;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>
css
/** 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;
}