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.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' ? (
+ <>
+
- )}
-
- );
- }
-}
+
+ 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