From acc71f9dfe51ccb204cc57d7cf4cbaa8d5026ded Mon Sep 17 00:00:00 2001 From: tima101 Date: Sat, 27 Nov 2021 10:06:10 -0800 Subject: [PATCH] #459 functional components ReadChapterFunctional --- book/9-end/pages/public/read-chapter-f.jsx | 379 +++++++++++++++++++++ book/9-end/pages/public/read-chapter.jsx | 3 +- book/9-end/server/routesWithSlug.js | 5 + 3 files changed, 386 insertions(+), 1 deletion(-) create mode 100644 book/9-end/pages/public/read-chapter-f.jsx diff --git a/book/9-end/pages/public/read-chapter-f.jsx b/book/9-end/pages/public/read-chapter-f.jsx new file mode 100644 index 000000000..3dc1acc7a --- /dev/null +++ b/book/9-end/pages/public/read-chapter-f.jsx @@ -0,0 +1,379 @@ +import React, { useState, useEffect, useRef } from 'react'; + +import PropTypes from 'prop-types'; +import Error from 'next/error'; +import Head from 'next/head'; +import { withRouter } from 'next/router'; +import throttle from 'lodash/throttle'; + +import Link from 'next/link'; + +import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted'; + +import Header from '../../components/Header'; +import BuyButton from '../../components/customer/BuyButton'; + +import { getChapterDetailApiMethod } from '../../lib/api/public'; +import withAuth from '../../lib/withAuth'; +import notify from '../../lib/notify'; + +const styleIcon = { + opacity: '0.75', + fontSize: '24px', + cursor: 'pointer', +}; + +function ReadChapterFunctional({ + chapter, + user, + router, + redirectToCheckout, + checkoutCanceled, + error, +}) { + const [showTOC, setShowTOC] = useState(false); + const [hideHeader, setHideHeader] = useState(false); + const [isMobile, setIsMobile] = useState(false); + const [chapterInsideState, setChapterInsideState] = useState(chapter); + const [htmlContent, setHtmlContent] = useState( + chapter && (chapter.isPurchased || chapter.isFree) ? chapter.htmlContent : chapter.htmlExcerpt, + ); + const [activeSection, setActiveSection] = useState(null); + + useEffect(() => { + if (chapter) { + setChapterInsideState(chapter); + setHtmlContent( + chapter.isPurchased || chapter.isFree ? chapter.htmlContent : chapter.htmlExcerpt, + ); + } + }, [chapter]); + + function usePrevious(value) { + const ref = useRef(); + useEffect(() => { + ref.current = value; + }); + return ref.current; + } + + const prevChapter = usePrevious(chapter); + const prevIsMobile = usePrevious(isMobile); + + const mounted = useRef(); + + const onScrollActiveSection = () => { + const sectionElms = document.querySelectorAll('span.section-anchor'); + let activeSectionCurrent; + + let aboveSection; + for (let i = 0; i < sectionElms.length; i += 1) { + const s = sectionElms[i]; + const b = s.getBoundingClientRect(); + const anchorBottom = b.bottom; + + if (anchorBottom >= 0 && anchorBottom <= window.innerHeight) { + activeSectionCurrent = { + hash: s.attributes.getNamedItem('name').value, + }; + + break; + } + + if (anchorBottom > window.innerHeight && i > 0) { + if (aboveSection.bottom <= 0) { + activeSectionCurrent = { + hash: sectionElms[i - 1].attributes.getNamedItem('name').value, + }; + break; + } + } else if (i + 1 === sectionElms.length) { + activeSectionCurrent = { + hash: s.attributes.getNamedItem('name').value, + }; + } + + aboveSection = b; + } + + if (activeSection !== activeSectionCurrent) { + setActiveSection(activeSectionCurrent); + } + }; + + const onScrollHideHeader = () => { + const distanceFromTop = document.getElementById('main-content').scrollTop; + const hideHeaderCurrent = distanceFromTop > 500; + + if (hideHeader !== hideHeaderCurrent) { + setHideHeader(hideHeaderCurrent); + } + }; + + const onScroll = throttle(() => { + onScrollActiveSection(); + onScrollHideHeader(); + }, 500); + + useEffect(() => { + if (!mounted.current) { + document.getElementById('main-content').addEventListener('scroll', onScroll); + + const isMobileCurrent = window.innerWidth < 768; + + if (prevIsMobile !== isMobileCurrent) { + setIsMobile(isMobileCurrent); + } + + if (checkoutCanceled) { + notify('Checkout canceled'); + } + + if (error) { + notify(error); + } + + mounted.current = true; + } else { + document.getElementById('chapter-content').scrollIntoView(); + let htmlContentCurrent = ''; + if (prevChapter && (prevChapter.isPurchased || prevChapter.isFree)) { + htmlContentCurrent = prevChapter.htmlContent; + } else { + htmlContentCurrent = prevChapter.htmlExcerpt; + } + + setChapterInsideState(chapter); + setHtmlContent(htmlContentCurrent); + } + + return () => { + document.getElementById('main-content').removeEventListener('scroll', onScroll); + }; + }, [chapter]); + + const toggleChapterList = () => { + setShowTOC((prevState) => ({ showTOC: !prevState.showTOC })); + }; + + const closeTocWhenMobile = () => { + setShowTOC((prevState) => ({ showTOC: !prevState.isMobile })); + }; + + const renderMainContent = () => { + const { book } = chapterInsideState; + + let padding = '20px 20%'; + if (!isMobile && showTOC) { + padding = '20px 10%'; + } else if (isMobile) { + padding = '0px 10px'; + } + + return ( +
+

+ {chapterInsideState.order > 1 ? `Chapter ${chapterInsideState.order - 1}: ` : null} + {chapterInsideState.title} +

+ {!chapterInsideState.isPurchased && !chapterInsideState.isFree ? ( + + ) : null} +
+ {!chapterInsideState.isPurchased && !chapterInsideState.isFree ? ( + + ) : null} +
+ ); + }; + + const renderSections = () => { + const { sections } = chapterInsideState; + + if (!sections || !sections.length === 0) { + return null; + } + + return ( + + ); + }; + + const renderSidebar = () => { + if (!showTOC) { + return null; + } + + const { book } = chapterInsideState; + const { chapters } = book; + + return ( +
+

{book.name}

+
    + {chapters.map((ch, i) => ( +
  1. + + + {ch.title} + + + {chapterInsideState._id === ch._id ? renderSections() : null} +
  2. + ))} +
+
+ ); + }; + + if (!chapterInsideState) { + return ; + } + + let left = '20px'; + if (showTOC) { + left = isMobile ? '100%' : '400px'; + } + + return ( +
+ + + {chapterInsideState.title === 'Introduction' + ? 'Introduction' + : `Chapter ${chapterInsideState.order - 1}. ${chapterInsideState.title}`} + + {chapterInsideState.seoDescription ? ( + + ) : null} + + +
+ + {renderSidebar()} + +
+ {renderMainContent()} +
+ +
+ +
+
+ ); +} + +const propTypes = { + chapter: PropTypes.shape({ + _id: PropTypes.string.isRequired, + isPurchased: PropTypes.bool, + isFree: PropTypes.bool.isRequired, + htmlContent: PropTypes.string, + htmlExcerpt: PropTypes.string, + }), + user: PropTypes.shape({ + _id: PropTypes.string.isRequired, + }), + router: PropTypes.shape({ + asPath: PropTypes.string.isRequired, + }).isRequired, + redirectToCheckout: PropTypes.bool.isRequired, + checkoutCanceled: PropTypes.bool, + error: PropTypes.string, +}; + +const defaultProps = { + chapter: null, + user: null, + checkoutCanceled: false, + error: '', +}; + +ReadChapterFunctional.propTypes = propTypes; +ReadChapterFunctional.defaultProps = defaultProps; + +ReadChapterFunctional.getInitialProps = async (ctx) => { + const { bookSlug, chapterSlug, buy, checkout_canceled, error } = ctx.query; + const { req } = ctx; + + const headers = {}; + if (req && req.headers && req.headers.cookie) { + headers.cookie = req.headers.cookie; + } + + const chapter = await getChapterDetailApiMethod({ bookSlug, chapterSlug }, { headers }); + const redirectToCheckout = !!buy; + + return { chapter, redirectToCheckout, checkoutCanceled: !!checkout_canceled, error }; +}; + +export default withAuth(withRouter(ReadChapterFunctional), { + loginRequired: false, +}); diff --git a/book/9-end/pages/public/read-chapter.jsx b/book/9-end/pages/public/read-chapter.jsx index e25a89295..f65af2d23 100644 --- a/book/9-end/pages/public/read-chapter.jsx +++ b/book/9-end/pages/public/read-chapter.jsx @@ -67,10 +67,11 @@ class ReadChapter extends React.Component { htmlContent, hideHeader: false, isMobile: false, + activeSection: null, }; } - static async getServerSideProps(ctx) { + static async getInitialProps(ctx) { const { bookSlug, chapterSlug, buy, checkout_canceled, error } = ctx.query; const { req } = ctx; diff --git a/book/9-end/server/routesWithSlug.js b/book/9-end/server/routesWithSlug.js index 521c17042..e67e7b1f3 100644 --- a/book/9-end/server/routesWithSlug.js +++ b/book/9-end/server/routesWithSlug.js @@ -4,6 +4,11 @@ function routesWithSlug({ server, app }) { app.render(req, res, '/public/read-chapter', { bookSlug, chapterSlug, ...(req.query || {}) }); }); + server.get('/books-f/:bookSlug/:chapterSlug', (req, res) => { + const { bookSlug, chapterSlug } = req.params; + app.render(req, res, '/public/read-chapter-f', { bookSlug, chapterSlug, ...(req.query || {}) }); + }); + server.get('/admin/book-detail/:slug', (req, res) => { const { slug } = req.params; app.render(req, res, '/admin/book-detail', { slug });