From 85a9150be185c547e6c02558068573b108943a79 Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Tue, 17 Dec 2024 21:14:46 +0000 Subject: [PATCH] refactor(quote): functional component --- src/features/quote/Quote.jsx | 486 +++++++----------- src/features/quote/components/QuoteAuthor.jsx | 59 +++ .../quote/components/QuoteButtons.jsx | 48 ++ src/features/quote/hooks/useQuoteState.js | 0 4 files changed, 287 insertions(+), 306 deletions(-) create mode 100644 src/features/quote/components/QuoteAuthor.jsx create mode 100644 src/features/quote/components/QuoteButtons.jsx create mode 100644 src/features/quote/hooks/useQuoteState.js diff --git a/src/features/quote/Quote.jsx b/src/features/quote/Quote.jsx index a8cd2aef..3f5dc080 100644 --- a/src/features/quote/Quote.jsx +++ b/src/features/quote/Quote.jsx @@ -1,128 +1,52 @@ +import { useState, useEffect, useRef } from 'react'; import variables from 'config/variables'; -import { PureComponent, createRef } from 'react'; -import { - MdContentCopy, - MdStarBorder, - MdStar, - MdPerson, - MdOpenInNew, - MdIosShare, -} from 'react-icons/md'; - import { toast } from 'react-toastify'; -import Stats from 'features/stats/api/stats'; - -import { Tooltip } from 'components/Elements'; - import Modal from 'react-modal'; import { ShareModal } from 'components/Elements'; -import offline_quotes from './offline_quotes.json'; - +import Stats from 'features/stats/api/stats'; import EventBus from 'utils/eventbus'; import defaults from './options/default'; +import offline_quotes from './offline_quotes.json'; + +import QuoteButtons from './components/QuoteButtons'; +import QuoteAuthor from './components/QuoteAuthor'; import './quote.scss'; -class Quote extends PureComponent { - buttons = { - share: ( - - - - ), - copy: ( - - - - ), - unfavourited: ( - - - - ), - favourited: ( - - - - ), +const Quote = () => { + const [quoteState, setQuoteState] = useState({ + quote: null, + author: null, + authorOccupation: null, + authorLink: null, + authorImg: null, + authorImgLicense: null, + quoteLanguage: '', + type: localStorage.getItem('quoteType') || defaults.quoteType, + shareModal: false, + isFavourited: !!localStorage.getItem('favouriteQuote'), + noQuote: false, + }); + + const quoteRef = useRef(); + const quoteDivRef = useRef(); + const quoteAuthorRef = useRef(); + + const stripHTML = (html) => { + const tmpdoc = new DOMParser().parseFromString(html, 'text/html'); + return tmpdoc.body.textContent || ''; }; - constructor() { - super(); - this.state = { - quote: null, - author: null, - authorOccupation: null, - favourited: this.useFavourite(), - share: localStorage.getItem('quoteShareButton') === 'false' ? null : this.buttons.share, - copy: localStorage.getItem('copyButton') === 'false' ? null : this.buttons.copy, - quoteLanguage: '', - type: localStorage.getItem('quoteType') || defaults.quoteType, - shareModal: false, - }; - this.quote = createRef(); - this.quotediv = createRef(); - this.quoteauthor = createRef(); - } - - useFavourite() { - if (localStorage.getItem('favouriteQuoteEnabled') === 'true') { - return localStorage.getItem('favouriteQuote') - ? this.buttons.favourited - : this.buttons.unfavourited; - } else { - return null; - } - } - - doOffline() { - // Get a random quote from our local JSON - const quote = offline_quotes[Math.floor(Math.random() * offline_quotes.length)]; - - this.setState({ - quote: '"' + quote.quote + '"', - author: quote.author, - authorlink: this.getAuthorLink(quote.author), - authorimg: '', - }); - Stats.postEvent('feature', 'quote', 'shown'); - } - - getAuthorLink(author) { + const getAuthorLink = (author) => { return localStorage.getItem('authorLink') === 'false' || author === 'Unknown' ? null : `https://${variables.locale_id.split('-')[0]}.wikipedia.org/wiki/${author .split(' ') .join('_')}`; - } + }; - stripHTML(html) { - const tmpdoc = new DOMParser().parseFromString(html, 'text/html'); - return tmpdoc.body.textContent || ''; - } - - async getAuthorImg(author) { + const getAuthorImg = async (author) => { if (localStorage.getItem('authorImg') === 'false') { return { authorimg: null, @@ -157,9 +81,7 @@ class Quote extends PureComponent { authorimglicensedata.query.pages[Object.keys(authorimglicensedata.query.pages)[0]]; const metadata = authorImagePage?.imageinfo?.[0]?.extmetadata; const license = metadata?.LicenseShortName; - const photographer = this.stripHTML( - metadata?.Attribution?.value || metadata?.Artist?.value || '', - ) + const photographer = stripHTML(metadata?.Attribution?.value || metadata?.Artist?.value || '') .replace(/©\s/, '') .replace(/ \(talk\)/, ''); // talk page link (if applicable) is only removed for English @@ -191,27 +113,66 @@ class Quote extends PureComponent { authorimg, authorimglicense, }; - } + }; - async getQuote() { + const copyQuote = () => { + Stats.postEvent('feature', 'quote', 'copied'); + navigator.clipboard.writeText(`${quoteState.quote} - ${quoteState.author}`); + toast(variables.getMessage('toasts.quote')); + }; + + const handleFavourite = () => { + const newIsFavourited = !quoteState.isFavourited; + if (newIsFavourited) { + localStorage.setItem('favouriteQuote', quoteState.quote + ' - ' + quoteState.author); + Stats.postEvent('feature', 'quote', 'favourited'); + } else { + localStorage.removeItem('favouriteQuote'); + Stats.postEvent('feature', 'quote', 'favourite_removed'); + } + setQuoteState((prev) => ({ ...prev, isFavourited: newIsFavourited })); + }; + + const setZoom = () => { + const zoomQuote = Number((localStorage.getItem('zoomQuote') || defaults.zoomQuote) / 100); + if (quoteRef.current && quoteAuthorRef.current) { + quoteRef.current.style.fontSize = `${0.8 * zoomQuote}em`; + quoteAuthorRef.current.style.fontSize = `${0.9 * zoomQuote}em`; + } + }; + + const doOffline = () => { + // Get a random quote from our local JSON + const quote = offline_quotes[Math.floor(Math.random() * offline_quotes.length)]; + + setQuoteState({ + quote: '"' + quote.quote + '"', + author: quote.author, + authorLink: getAuthorLink(quote.author), + authorImg: '', + }); + Stats.postEvent('feature', 'quote', 'shown'); + }; + + const getQuote = async () => { const offline = localStorage.getItem('offlineMode') === 'true'; const favouriteQuote = localStorage.getItem('favouriteQuote'); if (favouriteQuote) { let author = favouriteQuote.split(' - ')[1]; - const authorimgdata = await this.getAuthorImg(author); - this.setState({ + const authorimgdata = await getAuthorImg(author); + setQuoteState({ quote: favouriteQuote.split(' - ')[0], author, - authorlink: this.getAuthorLink(author), - authorimg: authorimgdata.authorimg, - authorimglicense: authorimgdata.authorimglicense, + authorLink: getAuthorLink(author), + authorImg: authorimgdata.authorimg, + authorImgLicense: authorimgdata.authorimglicense, }); Stats.postEvent('feature', 'quote', 'shown'); return; } - switch (this.state.type) { + switch (quoteState.type) { case 'custom': let customQuote; try { @@ -233,23 +194,23 @@ class Quote extends PureComponent { : null; if (customQuote !== undefined && customQuote !== null) { - this.setState({ + setQuoteState({ quote: '"' + customQuote.quote + '"', author: customQuote.author, - authorlink: this.getAuthorLink(customQuote.author), - authorimg: await this.getAuthorImg(customQuote.author), + authorLink: getAuthorLink(customQuote.author), + authorImg: await getAuthorImg(customQuote.author), noQuote: false, }); Stats.postEvent('feature', 'quote', 'shown'); } else { - this.setState({ + setQuoteState({ noQuote: true, }); } break; case 'quote_pack': if (offline) { - this.doOffline(); + doOffline(); return; } @@ -267,20 +228,20 @@ class Quote extends PureComponent { if (quotePack) { const data = quotePack[Math.floor(Math.random() * quotePack.length)]; - this.setState({ + setQuoteState({ quote: '"' + data.quote + '"', author: data.author, - authorlink: this.getAuthorLink(data.author), - authorimg: data.fallbackauthorimg, + authorLink: getAuthorLink(data.author), + authorImg: data.fallbackauthorimg, }); Stats.postEvent('feature', 'quote', 'shown'); } else { - this.doOffline(); + doOffline(); } break; case 'api': if (offline) { - this.doOffline(); + doOffline(); return; } @@ -293,13 +254,13 @@ class Quote extends PureComponent { if (data.statusCode === 429) { return null; } - const authorimgdata = await this.getAuthorImg(data.author); + const authorimgdata = await getAuthorImg(data.author); return { quote: '"' + data.quote.replace(/\s+$/g, '') + '"', author: data.author, - authorlink: this.getAuthorLink(data.author), - authorimg: authorimgdata.authorimg, - authorimglicense: authorimgdata.authorimglicense, + authorLink: getAuthorLink(data.author), + authorImg: authorimgdata.authorimg, + authorImgLicense: authorimgdata.authorimglicense, quoteLanguage: quoteLanguage, authorOccupation: data.author_occupation, }; @@ -310,210 +271,123 @@ class Quote extends PureComponent { let data = JSON.parse(localStorage.getItem('nextQuote')) || (await getAPIQuoteData()); localStorage.setItem('nextQuote', null); if (data) { - this.setState(data); + setQuoteState(data); localStorage.setItem('currentQuote', JSON.stringify(data)); localStorage.setItem('nextQuote', JSON.stringify(await getAPIQuoteData())); // pre-fetch data about the next quote Stats.postEvent('feature', 'quote', 'shown'); } else { - this.doOffline(); + doOffline(); } } catch (e) { // ...and if that fails we load one locally - this.doOffline(); + doOffline(); } break; default: break; } - } + }; - copyQuote() { - Stats.postEvent('feature', 'quote', 'copied'); - navigator.clipboard.writeText(`${this.state.quote} - ${this.state.author}`); - toast(variables.getMessage('toasts.quote')); - } + useEffect(() => { + // Initialize quote + getQuote(); + setZoom(); - favourite() { - if (localStorage.getItem('favouriteQuote')) { - localStorage.removeItem('favouriteQuote'); - this.setState({ - favourited: this.buttons.unfavourited, - }); - Stats.postEvent('feature', 'quote', 'favourite_removed'); - } else { - localStorage.setItem('favouriteQuote', this.state.quote + ' - ' + this.state.author); - this.setState({ - favourited: this.buttons.favourited, - }); - Stats.postEvent('feature', 'quote', 'favourited'); - } - } - - init() { - this.setZoom(); - - const quoteType = localStorage.getItem('quoteType'); - - if ( - this.state.type !== quoteType || - localStorage.getItem('quoteLanguage') !== this.state.quoteLanguage || - (quoteType === 'custom' && this.state.quote !== localStorage.getItem('customQuote')) || - (quoteType === 'custom' && this.state.author !== localStorage.getItem('customQuoteAuthor')) - ) { - this.getQuote(); - } - } - - setZoom() { - const zoomQuote = Number((localStorage.getItem('zoomQuote') || defaults.zoomQuote) / 100); - this.quote.current.style.fontSize = `${0.8 * zoomQuote}em`; - this.quoteauthor.current.style.fontSize = `${0.9 * zoomQuote}em`; - } - - componentDidMount() { - this.setZoom(); - - EventBus.on('refresh', (data) => { + // Event listener setup + const handleRefresh = (data) => { if (data === 'quote') { if (localStorage.getItem('quote') === 'false') { - return (this.quotediv.current.style.display = 'none'); + quoteDivRef.current.style.display = 'none'; + return; } - this.quotediv.current.style.display = 'block'; - this.init(); - - // buttons hot reload - this.setState({ - favourited: this.useFavourite(), - share: localStorage.getItem('quoteShareButton') === 'false' ? null : this.buttons.share, - copy: localStorage.getItem('copyButton') === 'false' ? null : this.buttons.copy, - }); + quoteDivRef.current.style.display = 'block'; + getQuote(); + setZoom(); } - // uninstall quote pack reverts the quote to what you had previously if (data === 'marketplacequoteuninstall') { - this.init(); + getQuote(); } if (data === 'quoterefresh') { - this.getQuote(); + getQuote(); } - }); + }; - if ( - localStorage.getItem('quotechange') === 'refresh' || - localStorage.getItem('quotechange') === null - ) { - this.setZoom(); - this.getQuote(); - localStorage.setItem('quoteStartTime', Date.now()); - } + EventBus.on('refresh', handleRefresh); + return () => EventBus.off('refresh', handleRefresh); + }, []); + + if (quoteState.noQuote) { + return null; } - componentWillUnmount() { - EventBus.off('refresh'); - } + return ( +
+ setQuoteState((prev) => ({ ...prev, shareModal: false }))} + > + setQuoteState((prev) => ({ ...prev, shareModal: false }))} + /> + - render() { - if (this.state.noQuote === true) { - return <>; - } + + {quoteState.quote} + - return ( -
- this.setState({ shareModal: false })} - > - this.setState({ shareModal: false })} - /> - - - {this.state.quote} - - - {localStorage.getItem('widgetStyle') === 'legacy' ? ( - <> -
-

- - {this.state.author} - -

-
-
- {this.state.copy} {this.state.share} {this.state.favourited} -
- - ) : ( -
-
- {localStorage.getItem('authorImg') !== 'false' ? ( -
- {this.state.authorimg === undefined || this.state.authorimg ? '' : } -
- ) : null} - {this.state.author !== null ? ( -
- {this.state.author} - {this.state.authorOccupation !== 'Unknown' && ( - {this.state.authorOccupation} - )} - - {this.state.authorimglicense && - this.state.authorimglicense.substring(0, 40) + - (this.state.authorimglicense.length > 40 ? '…' : '')} - -
- ) : ( -
- {/* these are placeholders for skeleton and as such don't need translating */} - loading - loading -
- )} - {(this.state.authorOccupation !== 'Unknown' && this.state.authorlink !== null) || - this.state.copy || - this.state.share || - this.state.favourited ? ( -
- {this.state.authorOccupation !== 'Unknown' && this.state.authorlink !== null ? ( - - - - {' '} - - ) : null} - {this.state.copy} {this.state.share} {this.state.favourited} -
- ) : null} -
+ {localStorage.getItem('widgetStyle') === 'legacy' ? ( + <> +
+

+ + {quoteState.author} + +

- )} -
- ); - } -} +
+ setQuoteState((prev) => ({ ...prev, shareModal: true }))} + isFavourited={quoteState.isFavourited} + /> +
+ + ) : ( +
+ setQuoteState((prev) => ({ ...prev, shareModal: true }))} + isFavourited={quoteState.isFavourited} + /> + } + /> +
+ )} +
+ ); +}; export { Quote as default, Quote }; diff --git a/src/features/quote/components/QuoteAuthor.jsx b/src/features/quote/components/QuoteAuthor.jsx new file mode 100644 index 00000000..72ac8e2b --- /dev/null +++ b/src/features/quote/components/QuoteAuthor.jsx @@ -0,0 +1,59 @@ +import { MdPerson, MdOpenInNew } from 'react-icons/md'; +import variables from 'config/variables'; +import { Tooltip } from 'components/Elements'; + +const QuoteAuthor = ({ + author, + authorOccupation, + authorImg, + authorImgLicense, + authorLink, + buttons, +}) => { + if (!author) { + return ( +
+ loading + loading +
+ ); + } + + return ( +
+ {localStorage.getItem('authorImg') !== 'false' && ( +
+ {!authorImg && } +
+ )} +
+ {author} + {authorOccupation !== 'Unknown' && {authorOccupation}} + + {authorImgLicense && + authorImgLicense.substring(0, 40) + (authorImgLicense.length > 40 ? '…' : '')} + +
+ {(authorOccupation !== 'Unknown' || authorLink || buttons) && ( +
+ {authorLink && authorOccupation !== 'Unknown' && ( + + + + + + )} + {buttons} +
+ )} +
+ ); +}; + +export default QuoteAuthor; diff --git a/src/features/quote/components/QuoteButtons.jsx b/src/features/quote/components/QuoteButtons.jsx new file mode 100644 index 00000000..f0d828f8 --- /dev/null +++ b/src/features/quote/components/QuoteButtons.jsx @@ -0,0 +1,48 @@ +import { MdContentCopy, MdStarBorder, MdStar, MdIosShare } from 'react-icons/md'; +import variables from 'config/variables'; +import { Tooltip } from 'components/Elements'; + +const QuoteButtons = ({ onCopy, onFavourite, onShare, isFavourited }) => { + const buttons = { + share: ( + + + + ), + copy: ( + + + + ), + favourite: isFavourited ? ( + + + + ) : ( + + + + ), + }; + + return ( +
+ {localStorage.getItem('copyButton') === 'true' && buttons.copy} + {localStorage.getItem('quoteShareButton') === 'true' && buttons.share} + {localStorage.getItem('favouriteQuoteEnabled') === 'true' && buttons.favourite} +
+ ); +}; + +export default QuoteButtons; diff --git a/src/features/quote/hooks/useQuoteState.js b/src/features/quote/hooks/useQuoteState.js new file mode 100644 index 00000000..e69de29b