javascript 第11期:20180620 - no73。トップページのFV改善フェーズ1(ポイントモーダル,店铺名下の整理)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了javascript 第11期:20180620 - no73。トップページのFV改善フェーズ1(ポイントモーダル,店铺名下の整理)相关的知识,希望对你有一定的参考价值。

/**
 * BaseModalで使用しているProps
 * @param {function} closeModal モーダルを閉じる関数(必須)
 * @param {string} componentName モーダルを閉じるボタンのComponent名(default:CloseButton)
 * @param {string} style  モーダルを閉じるボタンのクラス名(default:button__close)
 * @param {string} modalCloseButtonText モーダルを閉じるボタンで表示するテキスト(default: x 閉じる)
 * @param {boolean} isModalCloseButton モーダルを閉じるボタンを表示するかどうかのFlag(default: true)
 * @param {boolean} isFade モーダルの表示・非表示する際にアニメーションするかどうかのFlag(default true)
 * @param {number} fadeTime モーダルの表示・非表示する際のアニメーションする時間(default: 300ms)
 */

import React from 'react'
import { compose, setDisplayName, defaultProps, branch, onlyUpdateForKeys, renderNothing } from 'recompose'
import BaseButton from './BaseButton'

const ButtonText = ({ text }) => <span>{text}</span>

const ModalCloseButton = props => (
  <BaseButton {...props} handler={props.closeModal}>
    <ButtonText text={props.modalCloseButtonText} />
  </BaseButton>
)

const identity = Component => Component
const modalCloseButtonStatus = ({ isModalCloseButton }) => isModalCloseButton
const withModalCloseButtonStatusCheck = branch(modalCloseButtonStatus, identity, renderNothing)

const EnhanceModalCloseButton = compose(
  defaultProps({
    componentName: 'CloseButton',
    style: 'button__close',
    modalCloseButtonText: 'x 閉じる',
  }),
  withModalCloseButtonStatusCheck,
  onlyUpdateForKeys(['componentName', 'style', 'modalCloseButtonText']),
)(ModalCloseButton)

const Component = props => {
  const cls = ['modal']
  if (props.isModalOpen) cls.push('modal__active')
  const style = cls.join(' ')
  const styleObj = props.isFade ? { opacity: 0, transition: `opacity ${props.fadeTime}ms ease-in-out` } : {}

  return (
    <div className={style}>
      <div // eslint-disable-line jsx-a11y/no-static-element-interactions
        className="modal__bg"
        onClick={props.closeModal}
      />
      <div className="modal__container" style={styleObj}>
        {props.children}
        <EnhanceModalCloseButton {...props} />
      </div>
    </div>
  )
}

const Enhance = compose(
  setDisplayName('BaseModal'),
  defaultProps({ isModalCloseButton: true, isFade: true, fadeTime: 300 }),
  onlyUpdateForKeys(['isModalCloseButton', 'isFade', 'fadeTime']),
)

export default Enhance(Component)
import React from 'react'
import { compose, setDisplayName, defaultProps, onlyUpdateForKeys } from 'recompose'

const Component = props => (
  <div className={props.style}>
    <button type={props.type} onClick={props.handler}>
      {props.children}
    </button>
  </div>
)

const dispName = ({ componentName }) => componentName

const Enhance = compose(
  setDisplayName(dispName),
  defaultProps({ type: 'button' }),
  onlyUpdateForKeys(['componentName', 'type', 'style', 'handler']),
)

export default Enhance(Component)
import forEach from 'lodash/forEach'

// 新しいtagをtarget要素の最後に追加
export const newChildInsertEnd = (tag, attr, target) => {
  const addTag = document.createElement(tag)
  forEach(attr, (val, key) => {
    addTag.setAttribute(key, val)
  })
  target.appendChild(addTag)
  return addTag
}

// 要素をラッピング
export const wrapElement = (tag, attr, target) => {
  const wrapper = document.createElement(tag)
  forEach(attr, (val, key) => {
    wrapper.setAttribute(key, val)
  })
  target.parentNode.insertBefore(wrapper, target)
  wrapper.appendChild(target)
}
// yyyymmdd - noxx.hoge  // GitLab Issueのタイトル
// https://gnavi-work.gnavi.co.jp/jira/browse/MIKAMEI_PDCA-xxx  // JIRAのチケット

import abtestRun from './abTestRun'

const IssueXXX = () => {
  const removeElement = () => {
    console.log('in')
  }

  const caseMap = {
    '1': function() {
      removeElement()
    },
    '2': function() {},
    '3': function() {},
  }

  const init = caseNumber => {
    console.log('hoge')

    // 共通処理
    // $('#page').addClass('issueXXX')

    // patternごとの実装
    caseMap[caseNumber.toString()]()
  }

  window.issueXXX = { start: init }
  abtestRun()
}

export default IssueXXX
// 20180620 - no73.トップページのFV改善フェーズ1(ポイントモーダル, 店舗名下の整理)
// https://gnavi-work.gnavi.co.jp/jira/browse/MIKAMEI_PDCA-467 // JIRAのチケット
// shop_1-1b, shop_1-1bMovid

/**
 *** abテストと実装時での仕様変更内容 ***
 *
 *
   abテスト時は#page内にModalを作成し、bodyにfixedを付与して、Modalの背景スクロールを阻止
   ```
    <div id="page">
      <Modal>
    </div>
   ```
 *
 *
 *
   実装になった場合は、#pageと同じ改装にModalを作成し、#pageをdisplay:noneで非表示(「空席確認する」のカレンダーモーダルと同じ仕様へ変更)
   ```
    <div id="page">...</div>
    <Modal />
   ```
 *
 *
 *** abテストと実装時での仕様変更内容 ***
 */

import React from 'react'
import ReactDOM from 'react-dom'
import abtestRun from '../abTestRun'
import wrapVacantArea from './wrapVacantArea'
import { newChildInsertEnd, wrapElement } from '../utils/controlElement'
import App from './App'

const Issue11 = (path, extras) => {
  const init = () => {
    document.getElementById('page').classList.add('issue11')

    // 動画用
    const movieDialog = document.querySelector('#js-movie-dialog-wrap')
    const vacantArea = document.querySelector('.topHeader .topHeader__layout .topHeader__right .akiari')
    if (movieDialog !== null && vacantArea !== null) {
      const topHeaderFooter = document.querySelector('.topHeader__footer')
      if (topHeaderFooter !== null) {
        topHeaderFooter.classList.add('issue11__flexTopHeader__footer')
        const attr = {
          id: 'movieBody',
          class: 'topHeader__body',
        }
        wrapElement('div', attr, topHeaderFooter.children[0])
        const mBody = document.querySelector('#movieBody')
        while (mBody.nextElementSibling) {
          mBody.appendChild(mBody.nextElementSibling)
        }
        topHeaderFooter.appendChild(vacantArea.parentNode)
      }
    }

    // 個室空きありのwrapping
    wrapVacantArea()

    // ポイントとモーダルの描画
    const topHeader = document.querySelector('.topHeader')
    const pointUse = topHeader.querySelector('.point-use')
    if (pointUse === null) return
    const pointArea = newChildInsertEnd('section', { id: 'pointArea' }, document.querySelector('.topHeader'))
    const props = {
      imagePath: path.get('image'),
      revision: `?revision=${extras.revision}`,
    }

    ReactDOM.render(<App {...props} />, pointArea)
    pointUse.style.display = 'none'
  }

  window.issue11 = { start: init }
  abtestRun()
}

export default Issue11
/**
 * ポイント貯まる[OK]、店頭でポイント使える[NG]などをcloneし、
 * パラメーターを付与
 */

import forEach from 'lodash/forEach'

const createPointLayout = () => {
  const addParameterToList = clonePointLayout => {
    const targetLink = '.gnaviService__layout .gnaviService__item'
    const listLink = clonePointLayout.querySelectorAll(targetLink)

    forEach(listLink, el => {
      const linkText = el.querySelector('.gnaviService__text').innerHTML
      const list = el.children[0]
      switch (linkText) {
        case 'ポイント貯まる':
          list.setAttribute('href', list.href + '?sc_lid=r-smp_relinfo_point1')
          break
        case 'ネット予約でポイント使える':
          list.setAttribute('href', list.href + '?sc_lid=r-smp_point_use1')
          break
        case '店頭でポイント使える':
          list.setAttribute('href', list.href + '?sc_lid=r-smp_relinfo_point2')
          break
        case 'ぐるなびギフトカード使える':
          el.parentNode.removeChild(el)
          break
        // no default
      }
    })
  }

  const gnaviService = document.querySelector('.gnaviService')
  if (!gnaviService) return
  const clonePointLayout = document.querySelector('.gnaviService').cloneNode(true)
  clonePointLayout.setAttribute('class', 'modalPointLayout')

  addParameterToList(clonePointLayout)

  if (clonePointLayout.children[1]) {
    clonePointLayout.removeChild(clonePointLayout.children[1])
  }

  const wrapper = document.createElement('div')
  wrapper.setAttribute('class', '-mt4')
  wrapper.innerHTML = `<p class="text--small">※「NG」と表示されているポイントは、この店舗ではご利用いただけません。</p>`
  clonePointLayout.insertBefore(wrapper, clonePointLayout.children[1])

  const modalList = document.querySelector('.modal__inner .point-container')
  if (modalList === null) return
  modalList.parentNode.insertBefore(clonePointLayout, modalList)
}

export default createPointLayout
import React from 'react'
import { branch, renderNothing } from 'recompose'

const getTargetEls = document.querySelectorAll('.section div.button div.button__body a p.text--big span')

const filteredReservEls = els =>
  Array.prototype.filter.call(els, el => el.innerHTML.indexOf('ネット予約できるコース一覧') !== -1)

const getUrl = els => {
  const el = filteredReservEls(els)
  return el.length > 0 ? el[0].parentNode.parentNode.parentNode.href : ''
}

const ReserveButton = () => {
  const handleClick = () => {
    if (typeof window.sc_count === 'function') window.sc_count('r-smp_point_modal_coursebtn')
  }

  return (
    <div className="reserve__button">
      <div className="button">
        <div className="button__body">
          <a href={getUrl(getTargetEls)} className="button__item button__item--big" onClick={handleClick}>
            <div className="ic-net-yoyaku">
              <div>
                <p className="text--huge -clr-white">
                  <span>ネット予約できるコース一覧</span>
                </p>
              </div>
            </div>
          </a>
        </div>
      </div>
    </div>
  )
}

const isReserveButton = () => getUrl(getTargetEls) !== ''
const isDrawButton = branch(isReserveButton, component => component, renderNothing)
export default isDrawButton(ReserveButton)
import React from 'react'

const PointList = ({ imagePath, revision }) => {
  const poinText = [
    {
      id: 0,
      img: imagePath + 'img/issue11/point_get.png' + revision,
      theme: 'ポイント貯まる',
      text: 'ネット予約して来店すると、ぐるなびのポイントや、提携先のマイルや数種類のポイントが貯まります。',
    },
    {
      id: 1,
      img: imagePath + 'img/issue11/point_reserve.png' + revision,
      theme: 'ネット予約でポイント使える',
      text: '貯まったポイントは、お店のネット予約時にご利用いただけます。',
    },
    {
      id: 2,
      img: imagePath + 'img/issue11/point_use.png' + revision,
      theme: '店頭でポイント使える',
      text: '貯まったポイントは、当日店頭でのお支払時にご利用いただけます。',
    },
  ]

  const pointList = poinText.map(val => (
    <li className="list__content" key={val.id}>
      <div className="img-wrapper">
        <img src={val.img} alt={val.theme} />
      </div>
      <div className="text">
        <h3 className="text__theme">{val.theme}</h3>
        <p className="text__content">{val.text}</p>
      </div>
    </li>
  ))

  return <ul className="list">{pointList}</ul>
}

export default PointList
import React from 'react'
import { compose, renderComponent, setDisplayName, branch, renderNothing, pure, nest } from 'recompose'
import BaseModal from '../../../module/BaseModal'
import BaseButton from '../../../module/BaseButton'
import PointList from './PointList'
import ReserveButton from './ReserveBUtton'

const Modal = props => {
  const buttonProps = {
    componentName: 'CloseButton',
    style: 'button__close',
  }

  return (
    <div className="modal__inner">
      <div className="modal__header">
        <h1 className="theme">このお店で使えるポイント</h1>
        <BaseButton {...buttonProps} handler={props.closeModal}>
          {props.modalCloseButtonText}
        </BaseButton>
      </div>
      <section className="point point-container">
        <h2 className="point__head">ぐるなびのポイントとは</h2>
        <p className="text">
          ぐるなびのポイント(ぐるなびスーパー「ぐ」ポイント)は、獲得対象サービスを利用することで貯まり、1ポイント=1円相当としてご利用いただけます。また、貯まったポイントは、ギフトカードなどに交換できます。
        </p>

        <PointList {...props} />

        <div className="-center -mt12">
          <p className="-bold">
            <a href="http://my.gnavi.co.jp/point/?sc_lid=r-smp_pointclub_top" className="-underline -clr-link">
              ポイントについて詳しく見る
            </a>
          </p>
        </div>
      </section>

      <ReserveButton />
    </div>
  )
}

const Component = renderComponent(nest(BaseModal, Modal))
const checkModalStatus = ({ isModalOpen }) => isModalOpen
const withModalOpenCheck = branch(checkModalStatus, Component, renderNothing)
const Enhance = compose(setDisplayName('Modal'), withModalOpenCheck, pure)

export default Enhance(Component)
import React from 'react'

const AboutPoint = () => (
  <div className="-center -mt12">
    <p className="-bold">
      <a href="http://my.gnavi.co.jp/point/?sc_lid=r-smp_pointclub_top" className="-underline -clr-link">
        ポイントについて詳しく見る
      </a>
    </p>
  </div>
)

export default AboutPoint
import React from 'react'
import { compose, withStateHandlers, lifecycle } from 'recompose'
import PointButton from './Component/PointButton'
import Modal from './Component/Modal'
import createPointLayout from './createPointLayout'

const App = props => (
  <React.Fragment>
    <PointButton {...props} />
    <Modal {...props} />
  </React.Fragment>
)

const createCloseBtn = () => <div className="planCal__close ic-close-batu js-popover-close">閉じる</div>

const ms300 = 300
const ms400 = 400

const Enhance = compose(
  withStateHandlers(
    {
      isFadeOutModal: false,
      isModalOpen: false,
      isPointLayoutComponent: false,
      positionY: 0,
      modalCloseButtonText: createCloseBtn(),
    },
    {
      renderPointLayout: () => () => ({ isPointLayoutComponent: true }),
      closeModal: ({ isFadeOutModal }) => () => ({ isFadeOutModal: !isFadeOutModal }),
      fadeOutModal: ({ positionY }) => () => {
        document.querySelector('body').style.position = 'relative'
        window.scrollTo(0, positionY)
        return {
          isFadeOutModal: false,
          isModalOpen: false,
          isPointLayoutComponent: false,
          positionY: 0,
        }
      },
      openModal: () => e => {
        e.preventDefault()
        if (typeof window.sc_count === 'function') window.sc_count('r-smp_point_pop')

        return {
          isModalOpen: true,
          positionY: window.pageYOffset || document.documentElement.scrollTop,
        }
      },
    },
  ),
  lifecycle({
    componentDidUpdate() {
      if (this.props.isModalOpen && !this.props.isPointLayoutComponent) {
        this.props.renderPointLayout()
        createPointLayout()

        setTimeout(() => {
          document.querySelector('body').style.position = 'fixed'
        }, 0)

        setTimeout(() => {
          document.querySelector('.modal__container').style.opacity = 1
        }, ms300)
      }

      if (this.props.isFadeOutModal) {
        document.querySelector('.modal__container').style.opacity = 0
        setTimeout(() => {
          this.props.fadeOutModal()
        }, ms400)
      }
    },
  }),
)

export default Enhance(App)
const abtestRun = () => {
  if (
    location.href.match(/ab_issue=(.+)/) &&
    location.href.match(/ab_pattern=(.)/) &&
    location.hostname.match(/localhost|frontend/)
  ) {
    const issueNum = parseInt(location.href.match(/ab_issue=(\d+)/)[1], 10)
    const pattern = parseInt(location.href.match(/ab_pattern=(\d+)/)[1], 10)

    window.addEventListener('load', () => window['issue' + issueNum].start(pattern))
  }
}

export default abtestRun
// import React from 'react'
import { wrapElement } from '../utils/controlElement'

const werapVacantArea = () => {
  const vacantArea = document.querySelector('.topHeader .topHeader__right .akiari')
  if (vacantArea === null) return

  wrapElement('div', { class: 'wrapper' }, vacantArea.children[0])
  const title = vacantArea.querySelector('.akiari__title')
  vacantArea.querySelector('.wrapper').appendChild(title)

  // 「本日の18時~20時」を変更
  vacantArea.querySelector('.akiari__small').innerHTML = '本日 18 - 20時'
}

export default werapVacantArea
// 既存(登録済み)のイベント削除
// issue11用のポイントモーダル

import forEach from 'lodash/forEach'

const removeEvent = triggers => {
  forEach(triggers, el => {
    el.classList.toggle('js-point-use-modal-trigger')
    el.classList.add('issue11-pointModal')
    const clone = el.cloneNode(true)
    const parent = el.parentNode
    el.parentNode.removeChild(el)
    parent.appendChild(clone)
  })
}

export default removeEvent
import forEach from 'lodash/forEach'
import React from 'react'

// -enableクラスの判別とクラスの生成
const hasEnable = (hasEnableClass, baseClass) => (hasEnableClass ? baseClass + ' -enable' : baseClass)

const isEnable = target => target.indexOf('-enable') !== -1

const PointButton = ({ imagePath, revision, openModal }) => {
  const pointEl = document.querySelector('.topHeader .point-use')

  // -save: ポイント貯まる
  const pointSaveEl = pointEl.querySelector('.-save .point-use__state.-enable')
  const saveStateClass = hasEnable(pointSaveEl, 'point-use__state')
  const saveTextStateClass = hasEnable(pointSaveEl, 'point-use__box__save-text')

  // -use-list: ポイント使える
  const useList = pointEl.querySelectorAll('.-use-list .point-use__list__item')
  const useClassArr = []
  const useListLabelArr = []
  forEach(useList, el => {
    const stateText = el.querySelector('.point-use__state-text.-enable')
    const state = hasEnable(stateText, 'point-use__state')
    // ネット予約、店頭文言
    const text = el.querySelector('.point-use__state-text').innerHTML
    // [OK]のみ取得
    if (state.indexOf('-enable') !== -1) useListLabelArr.push(text)
    useClassArr.push(state)
  })
  // 長いクラス名を取得(.-enableがある方が長い)
  const useListClass = useClassArr[0].length > useClassArr[1].length ? useClassArr[0] : useClassArr[1]
  const useListTextClass = hasEnable(isEnable(useListClass), 'point-use__box__use-list-text')
  const useListLabel = isEnable(useListTextClass) ? useListLabelArr.join('・') : 'ネット予約・店頭'

  return (
    <div className="point-use-inner">
      <a href="" className="point-use__list__item__link js-issue11-pointModal" onClick={openModal}>
        <p className="theme">ポイント</p>

        <div className="point-use__box -save">
          <span className={saveStateClass} />
          <span className={saveTextStateClass}>貯まる</span>
        </div>

        <div className="point-use__box -use-list">
          <span className={useListClass} />
          <span className={useListTextClass}>
            使える ( <span className="label">{useListLabel}</span> )
          </span>
        </div>

        <div className="questionIcon">
          <img src={imagePath + 'img/issue11/question.png' + revision} alt="?" />
        </div>
      </a>
    </div>
  )
}

export default PointButton

以上是关于javascript 第11期:20180620 - no73。トップページのFV改善フェーズ1(ポイントモーダル,店铺名下の整理)的主要内容,如果未能解决你的问题,请参考以下文章

javascript Issue12:20180620 - no74。ナビゲーションメニュー改修

第93期5 分钟掌握 JavaScript 实用窍门

第910期No JQuery! 原生JavaScript操作DOM

20180620随笔

传智播客 PHP基础班+就业班高清完整版教学视频 第28期 9月份版

第2145期JavaScript诞生25周年