WikiEducationFoundation/WikiEduDashboard

View on GitHub
app/assets/javascripts/training/components/training_slide_handler.jsx

Summary

Maintainability
D
2 days
Test Coverage
A
95%
import React, { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router';
import { extend } from 'lodash-es';
import { useDispatch, useSelector } from 'react-redux';
import { fetchTrainingModule, setSlideCompleted, setCurrentSlide, toggleMenuOpen } from '../../actions/training_actions.js';
import SlideLink from './slide_link.jsx';
import SlideMenu from './slide_menu.jsx';
import Quiz from './quiz.jsx';
import Notifications from '../../components/common/notifications.jsx';
import { FastTrainingAlert, fastTrainingAlertHandler } from './fast_training_alert';



const md = require('../../utils/markdown_it.js').default({ openLinksExternally: true });

const __guard__ = (value, transform) => {
  return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;
};

const moduleId = (params) => {
  return __guard__(params, x => x.module_id);
};


const returnToLink = () => {
  return document.getElementById('react_root').getAttribute('data-return-to');
};

const userLoggedIn = () => {
  return typeof __guard__(document.getElementById('main'), x => x.getAttribute('data-user-id')) === 'string';
};

const trainingUrl = (params) => {
  return `/training/${params.library_id}/${params.module_id}/${params.slide_id}`;
};

const disableNext = (training) => {
  return Boolean(training.currentSlide.assessment) && !training.currentSlide.answeredCorrectly;
};

const getSlideInfo = (training, locale) => {
  let slideTitle;
  let assessment;
  let rawHtml;
  if (training.currentSlide.translations && training.currentSlide.translations[locale]) {
    slideTitle = training.currentSlide.translations[locale].title;
    rawHtml = md.render(training.currentSlide.translations[locale].content);
    if (training.currentSlide.translations[locale].assessment) {
      assessment = training.currentSlide.translations[locale].assessment;
    }
  } else {
    slideTitle = training.currentSlide.title;
    if (training.currentSlide.content) {
      rawHtml = md.render(training.currentSlide.content);
    }
    if (training.currentSlide.assessment) {
      assessment = training.currentSlide.assessment;
    }
  }
  return { slideTitle, assessment, rawHtml };
};

const keys = { rightKey: 39, leftKey: 37 };

const TrainingSlideHandler = () => {
  const training = useSelector(state => state.training);
  const routeParams = useParams();
  const navigate = useNavigate();
  const dispatch = useDispatch();
  const [baseTitle, setBaseTitle] = useState('');

  // useState for fastTrainingAlertHandler function
  const [isShown, setIsShown] = useState(false);

  const setSlideCompleted_FC = (slideId) => {
    const userId = __guard__(document.getElementById('main'), x => x.getAttribute('data-user-id'));
    if (!userId) { return; }
    dispatch(setSlideCompleted({
      slide_id: slideId,
      module_id: moduleId(routeParams),
      user_id: userId
    }));
  };

  const next = () => {
    const nextSlug = training.nextSlide.slug;
    dispatch(setCurrentSlide(nextSlug));
    setSlideCompleted_FC(nextSlug);
    fastTrainingAlertHandler(routeParams, setIsShown);
  };

  const prev = () => {
    dispatch(setCurrentSlide(training.previousSlide.slug));
  };

  // runs when the component is first rendered
  // fetches the initial data and sets the base title
  useEffect(() => {
    setBaseTitle(document.title);
    const slideId = __guard__(routeParams, x => x.slide_id);
    const userId = __guard__(document.getElementById('main'), x => x.getAttribute('data-user-id'));
    dispatch(fetchTrainingModule({ module_id: moduleId(routeParams), slide_id: slideId, user_id: userId }));
  }, []);

  // runs whenever the training from the redux store changes
  // which means its run essentially when the user goes from one slide to another
  // changes the page title according to the slide title and updates the event listener
  useEffect(() => {
    const handleKeyPress = (e) => {
      const navParams = { library_id: routeParams.library_id, module_id: routeParams.module_id };
      if (e.which === keys.leftKey && training.previousSlide) {
        const params = extend(navParams, { slide_id: training.previousSlide.slug });
        prev();
        return navigate(trainingUrl(params));
      }
      if (e.which === keys.rightKey && training.nextSlide) {
        if (disableNext(training)) { return; }
        const params = extend(navParams, { slide_id: training.nextSlide.slug });
        next();
        return navigate(trainingUrl(params));
      }
    };

    window.addEventListener('keyup', handleKeyPress);

    // training has changed, so update the title of the slide
    const { slideTitle } = getSlideInfo(training, I18n.locale);
    document.title = `${slideTitle} - ${baseTitle}`;

    return () => {
      // cleanup. Removes the old event listener
      return window.removeEventListener('keyup', handleKeyPress);
    };
  }, [training]);

  if (training.loading === true) {
    return (
      <div className="training-loader">
        <h1 className="h2">Loading…</h1>
        <div className="training-loader__spinner" />
      </div>
    );
  }
  if (training.valid === false) {
    return (
      <div className="training__slide__notification" key="invalid">
        <div className="container">
          <p>{I18n.t('training.invalid')}</p>
        </div>
      </div>
    );
  }
  let nextLink;
  let pendingWarning;

  if (__guard__(training.nextSlide, x1 => x1.slug)) {
    nextLink = (
      <>
        <SlideLink
          slideId={training.nextSlide.slug}
          buttonText={training.currentSlide.buttonText || I18n.t('training.next')}
          disabled={disableNext(training)}
          button={true}
          params={routeParams}
          onClick={next}
        />
        {isShown && <FastTrainingAlert/>}
      </>
    );
  } else {
    let nextHref = returnToLink();
    if (!nextHref) {
      nextHref = userLoggedIn() ? '/' : `/training/${routeParams.library_id}`;
    }
    if (training.completed) {
      nextLink = <a href={nextHref} className="slide-nav btn btn-primary pull-right"> {training.currentSlide.buttonText || I18n.t('training.done')} </a>;
    } else {
      nextLink = <a href={nextHref} className="slide-nav btn btn-primary disabled pull-right"> {training.currentSlide.buttonText || I18n.t('training.done')} </a>;
    }

    if (training.completed === false) {
      pendingWarning = (
        <div className="training__slide__notification" key="pending">
          <div className="container">
            <p>{I18n.t('training.wait')}</p>
          </div>
        </div>
      );
    }
  }

  let loginWarning;
  if (!userLoggedIn()) {
    loginWarning = (
      <div className="training__slide__notification" key="not_logged_in">
        <div className="container">
          <p>{I18n.t('training.logged_out')}</p>
        </div>
      </div>
    );
  }

  let previousLink;
  if (__guard__(training.previousSlide, x2 => x2.slug)) {
    previousLink = (
      <SlideLink
        slideId={training.previousSlide.slug}
        buttonText={I18n.t('training.previous')}
        params={routeParams}
        onClick={prev}
      />

    );
  }

  const { slideTitle, assessment, rawHtml } = getSlideInfo(training, I18n.locale);

  const menuClass = training.menuIsOpen === false ? 'hidden' : 'shown';

  let quiz;
  if (training.currentSlide.assessment) {
    quiz = (
      <Quiz
        key={training.currentSlide.index}
        question={assessment.question}
        answers={assessment.answers}
        selectedAnswer={training.currentSlide.selectedAnswer}
        correctAnswer={training.currentSlide.assessment.correct_answer_id}
      />
    );
  }

  let titlePrefix;
  if (training.currentSlide.title_prefix) {
    titlePrefix = (
      <h2 className="training__slide__title-prefix">{training.currentSlide.title_prefix}</h2>
    );
  }

 let sourceLink;
 if (training.currentSlide.wiki_page) {
   sourceLink = <span><a href={`https://meta.wikimedia.org/wiki/${training.currentSlide.wiki_page}`} target="_blank">wiki source</a></span>;
 }


  const toggleMenuOpen_FC = (e) => {
    e.stopPropagation();
    dispatch(toggleMenuOpen({ currently: training.menuIsOpen }));
  };

  const closeMenu_FC = (e) => {
    if (training.menuIsOpen) {
      e.stopPropagation();
      dispatch(toggleMenuOpen({ currently: true }));
    }
  };

  return (
    <div>
      <Notifications />
      <header>
        <div className="pull-right training__slide__nav" onClick={toggleMenuOpen_FC}>
          <div className="pull-right hamburger">
            <span className="hamburger__bar" />
            <span className="hamburger__bar" />
            <span className="hamburger__bar" />
          </div>
          <h3 className="pull-right">
            <a href="" onFocus={toggleMenuOpen_FC}>{I18n.t('training.page_number', { number: training.currentSlide.index, total: training.slides.length })}</a>
          </h3>
        </div>
        <SlideMenu
          closeMenu={closeMenu_FC}
          onClick={toggleMenuOpen_FC}
          menuClass={menuClass}
          currentSlide={training.currentSlide}
          params={routeParams}
          enabledSlides={training.enabledSlides}
          slides={training.slides}
        />
      </header>
      {loginWarning}
      {pendingWarning}
      <article className="training__slide">
        {titlePrefix}
        <h1>{slideTitle}</h1>
        <div className="markdown training__slide__content" dangerouslySetInnerHTML={{ __html: rawHtml }} />
        {quiz}
        <footer className="training__slide__footer">
          <span className="pull-left">{previousLink}</span>
          {sourceLink}
          <span className="pull-right">{nextLink}</span>
        </footer>
      </article>
    </div>
  );
};

export default TrainingSlideHandler;