123

Pen Settings

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

You're using npm packages, so we've auto-selected Babel for you here, which we require to process imports and make it all work. If you need to use a different JavaScript preprocessor, remove the packages in the npm tab.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Use npm Packages

We can make npm packages available for you to use in your JavaScript. We use webpack to prepare them and make them available to import. We'll also process your JavaScript with Babel.

⚠️ This feature can only be used by logged in users.

Code Indentation

     

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

HTML Settings

Here you can Sed posuere consectetur est at lobortis. Donec ullamcorper nulla non metus auctor fringilla. Maecenas sed diam eget risus varius blandit sit amet non magna. Donec id elit non mi porta gravida at eget metus. Praesent commodo cursus magna, vel scelerisque nisl consectetur et.

            
              <!-- html structure
    .app, wrapping container
        button, to trigger the display of a new joke
        .app__joke, displaying the joke
            h1, the joke itself
            h2, the tone-deaf reaction
-->
<div class="app">
    <button class="app__button">Fetching a joke...</button>
    <div class="app__joke">
        <h1></h1>
        <h2></h2>
    </div>
</div>
            
          
!
            
              @import url("https://fonts.googleapis.com/css?family=Roboto+Mono:500&display=swap");

* {
  box-sizing: border-box;
  padding: 0;
  margin: 0;
}
body {
  font-family: "Roboto Mono", monospace;
  /* with repeating linear gradients build a grid as if the project is laid on graph paper */
  background: repeating-linear-gradient(
      to right,
      hsl(0, 0%, 95%) 0px,
      hsl(0, 0%, 95%) 1px,
      transparent 0px,
      transparent 20px
    ),
    repeating-linear-gradient(
      to bottom,
      hsl(0, 0%, 95%) 0px,
      hsl(0, 0%, 95%) 1px,
      transparent 0px,
      transparent 20px
    ),
    hsl(0, 0%, 100%);
  min-height: 100vh;
}
/* display the button and joke in a column, horizontally centered */
.app {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0.5rem;
}

/* style the button to have a solid border */
.app__button {
  margin: 1rem;
  padding: 0.5rem 1rem;
  border: 3px solid hsl(0, 0%, 5%);
  background: hsl(0, 0%, 100%);
  box-shadow: 0 1px 2px -1px hsl(0, 0%, 5%);
  font-family: inherit;
  color: inherit;
  font-size: 0.9rem;
  letter-spacing: 0.1rem;
  text-transform: uppercase;
  /* clip the corners of the border */
  clip-path: polygon(
    3px 3px,
    3px 0%,
    calc(100% - 3px) 0%,
    calc(100% - 3px) 3px,
    100% 3px,
    100% calc(100% - 3px),
    calc(100% - 3px) calc(100% - 3px),
    calc(100% - 3px) 100%,
    3px 100%,
    3px calc(100% - 3px),
    0% calc(100% - 3px),
    0% 3px
  );
  /* animate the button into view */
  animation: showButton 0.3s 0.2s ease-out both;
}
@keyframes showButton {
  from {
    opacity: 0;
    visibility: hidden;
  }
  to {
    opacity: 1;
    visibility: visible;
  }
}
/* when hovering or focusing the button flip the color and background values */
.app__button:hover,
.app__button:focus {
  color: hsl(0, 0%, 100%);
  background: hsl(0, 0%, 5%);
}
/* when pressing the button animate it downwards and out of sight */
.app__button.active {
  animation: hideButton 0.3s ease-out forwards;
}
@keyframes hideButton {
  to {
    transform: translateY(5px);
    opacity: 0;
    visibility: hidden;
  }
}

/* display the joke in a column, positioning the reaction on the right end of the container */

.app__joke {
  max-width: 350px;
  display: flex;
  flex-direction: column;
}

.app__joke h1 {
  font-weight: 500;
  font-size: 1.5rem;
  margin: 1rem 0 2rem;
}
.app__joke h1 span:first-of-type {
  position: relative;
}
.app__joke h2 {
  font-size: 1rem;
  font-weight: 500;
  align-self: flex-end;
  position: relative;
  opacity: 0;
}

/* with pseudo elements include two lines connecting the joke and the reaction to imaginary voices coming from either side */
.app__joke h1 span:first-of-type:after,
.app__joke h2:after {
  content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 50" width="100" height="50"><path fill="none" stroke="%23333333" stroke-width="2" stroke-linejoin="round" stroke-linecap="round" d="M 1 1 q 49 48 98 48"></path></svg>');
  position: absolute;
  margin: 1rem;
  top: 100%;
}

.app__joke h1 span:first-of-type:after {
  right: 100%;
  transform: scaleX(-1);
}

.app__joke h2:after {
  left: 100%;
}

            
          
!
            
              // endpoint and headers
const url = 'https://icanhazdadjoke.com';
const headers = new Headers({
  Accept: 'application/json',
});

// array describing the possible reactions to the joke
// the idea is to use the utterance for the joke and then for the reaction, after a brief pause
const reactions = [
  'lol',
  'hilarious',
  'funny',
  'ah ah',
  'uh',
  'wow',
];
// utility function returning a random item from an array
const randomItem = arr => arr[Math.floor(Math.random() * arr.length)];

/*
project flow
- retrieve the voices for the synthesizer
- fetch a joke
- use the joke in the utterance and in the heading of the .joke container
- listen for a click event on the only button on the page
- as the button gets clicked, have the synthesizer relate the joke, as the words are displayed in unison
- once the joke is told, have the synthesizer relate a reaction
- listen for a click event on the same button to reset the environment and fetch a new joke
*/

// global variables referring to the app container, the synthesizers and the instance of the synth utterance
const app = document.querySelector('.app');
const synth = window.speechSynthesis;
const utterance = new SpeechSynthesisUtterance();

// RETRIEVE VOICES
function retrieveVoices() {
  // retrieve the voices from the instance of the speechSynthesis object
  const voices = synth.getVoices();
  // ! the voices might not be available immediately, but following the onvoiceschanged event
  // continue only if voices is a non-null value
  if (voices) {
    // retrieve the first english-speaking voice and set it for the instance of the utterance
    const voice = voices.find(item => /en/gi.test(item.lang));
    utterance.voice = voice;
    // proceed to fetch a joke
    fetchJoke();
  }
}

// retrieve the voices
retrieveVoices();
// ! the voices might be available only at a later moment, and when they do they prop the onvoiceschanged event
synth.addEventListener('voiceschanged', retrieveVoices);


// FETCH A JOKE
// for the selected endpoint and the chosen headers retrieve a json object with the joke
function fetchJoke() {
  fetch(url, { headers })
    .then(response => response.json())
    // upon retrieving the joke call the function to set up the joke and the button
    .then(({ joke }) => setJoke(joke));
}

// SET UP JOKE
function setJoke(joke) {
  // include the joke for the text of the utterance
  utterance.text = joke;

  /* destructure the string into an array of span elements, to show the words one at a time
  the idea is to show the words as they are spoken by the synth
  the synth provides a reference to the number of characters being spoken, so the span elements include a data-attribute for this cumulative value
  */

  /* array of objects
  {
    word,           // the actual word
    length,         // the length of the word
    count,          // the cumulative number of characters
  }

  */
  const jokeArray = joke
    .split(' ')
    .reduce((wordArray, word) => {
      const count = wordArray.reduce((acc, curr) => acc + curr.length + 1, 0); // + 1 to account for the white space between words
      const { length } = word;
      const wordObject = {
        word,
        length,
        count,
      };
      return [...wordArray, wordObject];
    }, []);

  // string of span elements wrapping the words and specifying the count in a data attribute
  // ! by default have the span hidden from sight
  const jokeMarkup = jokeArray
    .map(({ word, count }) => `
      <span data-count=${count} style="opacity: 0;">${word}</span>
    `)
    .join(' ');

  const h1 = app.querySelector('h1');
  h1.innerHTML = jokeMarkup;

  // hide the h2 element as well
  const h2 = app.querySelector('h2');
  h2.style.opacity = 0;
  h2.textContent = '';

  // change the text of the button to highlight the presence of a joke and attach an event listener
  const button = app.querySelector('button');
  button.textContent = 'Tell me a joke';
  // on click call a function to tell the joke
  button.addEventListener('click', tellJoke, { once: true });
}

// function called when the button is clicked
function tellJoke() {
  // hide the button from sight
  const button = app.querySelector('button');
  button.classList.add('active');

  // listen to the boundary event, to show the words as they are spoken
  utterance.addEventListener('boundary', showJoke);
  // listen to the end event, to speak/show the reaction as the joke ends
  utterance.addEventListener('end', showReaction, { once: true });

  // use the synth to tell the joke
  synth.speak(utterance);
}


// function called following the boundary event
// retrieve the number of characters spoken in the utterance
function showJoke({ charIndex }) {
  // loop through the span and show the elements with a data-count smaller than the found reference
  const spans = app.querySelectorAll('h1 span');
  spans.forEach((span) => {
    const dataCount = span.getAttribute('data-count');
    if (dataCount <= charIndex) {
      span.style.opacity = 1;
    }
  });
}

// function following the end event
function showReaction() {
  // loop through the span elements and set the opacity to 1 (precaution against a word being cut out from the word count)
  const spans = app.querySelectorAll('h1 span');
  spans.forEach((span) => {
    span.style.opacity = 1;
  });

  // after a brief delay retrieve a reaction and show it in the h2 element
  // the pause adds to the hilarity of the joke
  const timeoutReaction = setTimeout(() => {
    const reaction = randomItem(reactions);
    const h2 = app.querySelector('h2');

    h2.textContent = reaction;
    h2.style.opacity = 1;

    // change the text of the utterance and have the synth produce the reaction
    utterance.text = reaction;
    synth.speak(utterance);

    clearTimeout(timeoutReaction);
  }, 500);

  // after a longer delay show the button by removing the prescribed class
  const timeoutButton = setTimeout(() => {
    const button = app.querySelector('button');
    button.textContent = 'Fetch another joke';
    button.classList.remove('active');
    button.addEventListener('click', fetchJoke, { once: true });
    clearTimeout(timeoutButton);
  }, 2500);
}

            
          
!
999px
🕑 One or more of the npm packages you are using needs to be built. You're the first person to ever need it! We're building it right now and your preview will start updating again when it's ready.

Console