From 4ad799da3ba8b96ca2185d084abae4fca00ca3aa Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Sat, 7 Feb 2026 14:53:35 +0000 Subject: [PATCH] feat: Add QuoteInfoModal for displaying detailed quote information and integrate it with the Quote component --- src/features/quote/Quote.jsx | 24 +- src/features/quote/components/AuthorInfo.jsx | 1 + .../quote/components/AuthorInfoLegacy.jsx | 1 + .../quote/components/QuoteInfoModal.jsx | 184 ++++++++++++++ src/features/quote/components/index.js | 1 + src/features/quote/hooks/useQuoteLoader.js | 12 + src/features/quote/hooks/useQuoteState.js | 12 + src/features/quote/scss/index.scss | 225 ++++++++++++++++++ 8 files changed, 458 insertions(+), 2 deletions(-) create mode 100644 src/features/quote/components/QuoteInfoModal.jsx diff --git a/src/features/quote/Quote.jsx b/src/features/quote/Quote.jsx index 40992a75..6d4c4913 100644 --- a/src/features/quote/Quote.jsx +++ b/src/features/quote/Quote.jsx @@ -1,9 +1,11 @@ import { useEffect, useRef, useState, useMemo } from 'react'; import Modal from 'react-modal'; +import { MdInfoOutline } from 'react-icons/md'; import { ShareModal } from 'components/Elements'; import { useQuoteState, useQuoteLoader, useQuoteActions } from './hooks'; import { AuthorInfo, AuthorInfoLegacy } from './components'; +import QuoteInfoModal from './components/QuoteInfoModal'; import EventBus from 'utils/eventbus'; import { useFrequencyInterval } from '../../hooks/useFrequencyInterval'; @@ -14,7 +16,7 @@ import './scss/index.scss'; * Supports: API quotes, custom quotes, quote packs, and offline quotes */ export default function Quote() { - const { quoteData, uiState, updateQuote, toggleShareModal } = useQuoteState(); + const { quoteData, uiState, updateQuote, toggleShareModal, toggleInfoModal } = useQuoteState(); const { getQuote } = useQuoteLoader(updateQuote); const { copyQuote, toggleFavourite } = useQuoteActions(quoteData); @@ -90,7 +92,7 @@ export default function Quote() {
+ toggleInfoModal(false)} + > + toggleInfoModal(false)} /> + + {quoteData.quote} @@ -115,6 +128,7 @@ export default function Quote() { onCopy={copyQuote} onFavourite={handleFavourite} onShare={() => toggleShareModal(true)} + onInfo={() => toggleInfoModal(true)} isFavourited={isFavourited} /> ) : ( @@ -127,9 +141,15 @@ export default function Quote() { onCopy={copyQuote} onFavourite={handleFavourite} onShare={() => toggleShareModal(true)} + onInfo={() => toggleInfoModal(true)} isFavourited={isFavourited} /> ))} + +
toggleInfoModal(true)}> + + About this quote +
); } diff --git a/src/features/quote/components/AuthorInfo.jsx b/src/features/quote/components/AuthorInfo.jsx index 942da815..ef043385 100644 --- a/src/features/quote/components/AuthorInfo.jsx +++ b/src/features/quote/components/AuthorInfo.jsx @@ -16,6 +16,7 @@ export default function AuthorInfo({ onCopy, onFavourite, onShare, + onInfo, isFavourited, }) { const t = useT(); diff --git a/src/features/quote/components/AuthorInfoLegacy.jsx b/src/features/quote/components/AuthorInfoLegacy.jsx index 81ab4bab..2faf1cf6 100644 --- a/src/features/quote/components/AuthorInfoLegacy.jsx +++ b/src/features/quote/components/AuthorInfoLegacy.jsx @@ -10,6 +10,7 @@ export default function AuthorInfoLegacy({ onCopy, onFavourite, onShare, + onInfo, isFavourited, }) { const t = useT(); diff --git a/src/features/quote/components/QuoteInfoModal.jsx b/src/features/quote/components/QuoteInfoModal.jsx new file mode 100644 index 00000000..664e3d53 --- /dev/null +++ b/src/features/quote/components/QuoteInfoModal.jsx @@ -0,0 +1,184 @@ +import { memo } from 'react'; +import { useNavigate } from 'react-router'; +import { MdClose, MdWarning, MdSource, MdPerson, MdCategory } from 'react-icons/md'; +import { HiMiniArrowUpRight } from 'react-icons/hi2'; +import { Tooltip, Button } from 'components/Elements'; +import variables from 'config/variables'; + +/** + * QuoteInfoModal - displays information about the current quote + */ +function QuoteInfoModal({ modalClose, quoteData }) { + const navigate = useNavigate(); + const getQuoteSource = () => { + const type = localStorage.getItem('quoteType') || 'quote_pack'; + const offline = localStorage.getItem('offlineMode') === 'true'; + + if (offline) { + return variables.getMessage('widgets.quote.info.source_offline') || 'Offline Mode'; + } + + switch (type) { + case 'custom': + return variables.getMessage('widgets.quote.info.source_custom') || 'Custom Quote'; + case 'quote_pack': + return ( + quoteData.packName || + variables.getMessage('widgets.quote.info.source_pack') || + 'Quote Pack' + ); + default: + return variables.getMessage('widgets.quote.info.source_unknown') || 'Unknown'; + } + }; + + const getQuoteType = () => { + const type = localStorage.getItem('quoteType') || 'quote_pack'; + const offline = localStorage.getItem('offlineMode') === 'true'; + + if (offline) { + return variables.getMessage('widgets.quote.info.type_offline') || 'Offline'; + } + + switch (type) { + case 'custom': + return variables.getMessage('widgets.quote.info.type_custom') || 'Custom'; + case 'quote_pack': + return variables.getMessage('widgets.quote.info.type_pack') || 'Quote Pack'; + default: + return variables.getMessage('widgets.quote.info.type_unknown') || 'Unknown'; + } + }; + + const hasWarning = quoteData.authorimg || quoteData.authorOccupation; + + return ( +
+
+

{variables.getMessage('widgets.quote.info.title') || 'Quote Information'}

+ +
+ +
+
+
+
+ {/* Warning about automatic data */} + {hasWarning && ( +
+
+ +
+
+

+ {variables.getMessage('widgets.quote.info.warning') || + 'Author image and occupation are automatically fetched and may be incorrect.'} +

+
+
+ )} + + {/* Quote Information */} +
+
+
+ +
+
+ + {variables.getMessage('widgets.quote.info.type') || 'Type'} + + {getQuoteType()} +
+
+ + {quoteData.realAuthor && quoteData.realAuthor !== quoteData.author && ( +
+
+ +
+
+ + {variables.getMessage('widgets.quote.info.original_author') || 'Original Author'} + + {quoteData.realAuthor} +
+
+ )} + +
+
+ +
+
+ + {variables.getMessage('widgets.quote.info.source') || 'Source'} + + + {quoteData.packId ? ( + navigate(`/discover/item/${quoteData.packId}`)} + > + {getQuoteSource()} + + + ) : ( + getQuoteSource() + )} + +
+
+ + {quoteData.authorOccupation && quoteData.authorOccupation !== 'Unknown' && ( +
+
+ +
+
+ + {variables.getMessage('widgets.quote.info.occupation') || 'Occupation'} + + {quoteData.authorOccupation} +
+
+ )} + + {quoteData.authorlink && ( +
+
+ +
+
+ + {variables.getMessage('widgets.quote.info.wikipedia') || 'Wikipedia'} + + + + {variables.getMessage('widgets.quote.info.view_article') || 'View Article'} + + + +
+
+ )} +
+ +
+
+
+
+ ); +} + +export default memo(QuoteInfoModal); diff --git a/src/features/quote/components/index.js b/src/features/quote/components/index.js index b3ead78d..038eed92 100644 --- a/src/features/quote/components/index.js +++ b/src/features/quote/components/index.js @@ -1,3 +1,4 @@ export { default as QuoteButtons } from './QuoteButtons'; export { default as AuthorInfo } from './AuthorInfo'; export { default as AuthorInfoLegacy } from './AuthorInfoLegacy'; +export { default as QuoteInfoModal } from './QuoteInfoModal'; diff --git a/src/features/quote/hooks/useQuoteLoader.js b/src/features/quote/hooks/useQuoteLoader.js index d11b1b01..4d6674e9 100644 --- a/src/features/quote/hooks/useQuoteLoader.js +++ b/src/features/quote/hooks/useQuoteLoader.js @@ -119,6 +119,9 @@ export function useQuoteLoader(updateQuote) { quote: `"${selected.quote}"`, author: selected.author || 'Unknown', authorlink: getAuthorLink(selected.author), + packName: null, + packId: null, + realAuthor: null, needsAuthorData: true, noQuote: false, }; @@ -130,6 +133,9 @@ export function useQuoteLoader(updateQuote) { quote: '"' + quote.quote + '"', author: quote.author, authorlink: getAuthorLink(quote.author), + packName: null, + packId: null, + realAuthor: null, needsAuthorData: false, }; } @@ -149,6 +155,7 @@ export function useQuoteLoader(updateQuote) { ...quote, fallbackauthorimg: item.icon_url, packName: item.display_name || item.name, + packId: item.id, noAuthorImg: item.noAuthorImg || quote.noAuthorImg, })), ); @@ -159,6 +166,9 @@ export function useQuoteLoader(updateQuote) { quote: '"' + quote.quote + '"', author: quote.author, authorlink: getAuthorLink(quote.author), + packName: null, + packId: null, + realAuthor: null, needsAuthorData: false, }; } @@ -173,6 +183,8 @@ export function useQuoteLoader(updateQuote) { realAuthor: hasAuthor ? data.author : null, authorlink: hasAuthor ? getAuthorLink(data.author) : null, fallbackauthorimg: data.fallbackauthorimg, + packName: data.packName, + packId: data.packId, needsAuthorData: hasAuthor && !data.noAuthorImg, }; }, [getAuthorLink]); diff --git a/src/features/quote/hooks/useQuoteState.js b/src/features/quote/hooks/useQuoteState.js index 0f80e07d..e7367f3c 100644 --- a/src/features/quote/hooks/useQuoteState.js +++ b/src/features/quote/hooks/useQuoteState.js @@ -11,11 +11,15 @@ export function useQuoteState() { authorlink: null, authorimg: null, authorimglicense: null, + packName: null, + packId: null, + realAuthor: null, noQuote: false, }); const [uiState, setUiState] = useState({ shareModal: false, + infoModal: false, }); const updateQuote = useCallback((newData) => { @@ -32,10 +36,18 @@ export function useQuoteState() { })); }, []); + const toggleInfoModal = useCallback((isOpen) => { + setUiState((prev) => ({ + ...prev, + infoModal: isOpen, + })); + }, []); + return { quoteData, uiState, updateQuote, toggleShareModal, + toggleInfoModal, }; } diff --git a/src/features/quote/scss/index.scss b/src/features/quote/scss/index.scss index 34133442..40ba2f4a 100644 --- a/src/features/quote/scss/index.scss +++ b/src/features/quote/scss/index.scss @@ -181,6 +181,231 @@ h1.quoteauthor { } } +.quote-info-text { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + font-size: 0.75rem; + margin-top: -5px; + cursor: pointer; + transition: opacity 0.2s ease; + user-select: none; + align-self: center; + + @include themed { + color: t($subColor); + } + + svg { + font-size: 0.8rem; + } + + &:hover { + opacity: 0.7; + } +} + +.quote-info-button { + @include basicIconButton(11px, 1.3rem, ui); +} + +// Quote Info Modal Styles +.quoteInfoModal { + @extend %tabText; + + display: flex; + flex-flow: column; + gap: 15px; + padding: 15px; + width: 550px; + + @include themed { + background: t($modal-secondaryColour); + } + + .shareHeader { + display: flex; + justify-content: space-between; + align-items: center; + + h4 { + margin: 0; + font-size: 1.25rem; + } + + .close { + cursor: pointer; + padding: 0.5rem; + border-radius: 8px; + transition: background 0.2s ease; + + &:hover { + @include themed { + background: t($modal-sidebarActive); + } + } + + svg { + font-size: 1.5rem; + } + } + } +} + +.quoteInfoContent { + padding: 1rem 0; + display: grid; + grid-template-columns: 1fr; + gap: 1.5rem; +} + +.quoteInfoWarning { + display: flex; + gap: 1rem; + padding: 1rem; + border-radius: 8px; + align-items: flex-start; + + @include themed { + background: rgba(255, 152, 0, 0.1); + border: 1px solid rgba(255, 152, 0, 0.3); + } + + .warningIcon { + flex-shrink: 0; + + svg { + font-size: 1.5rem; + color: #ff9800; + } + } + + .warningText { + flex: 1; + word-wrap: break-word; + + p { + margin: 0; + font-size: 0.9rem; + line-height: 1.5; + + @include themed { + color: t($color); + } + } + } +} + +.quoteInfoSection { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.75rem; +} + +.quoteInfoRow { + display: flex; + gap: 1rem; + align-items: flex-start; + padding: 0.75rem; + border-radius: 8px; + transition: background 0.2s ease; + + &:hover { + @include themed { + background: t($modal-sidebarActive); + } + } + + .quoteInfoIcon { + display: flex; + align-items: center; + justify-content: center; + padding: 0.5rem; + border-radius: 50%; + + @include themed { + background: t($modal-sidebar); + } + + svg { + font-size: 1.2rem; + + @include themed { + color: t($color); + } + } + } + + .quoteInfoText { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.25rem; + + .quoteInfoLabel { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.5px; + font-weight: 600; + + @include themed { + color: t($subColor); + } + } + + .quoteInfoValue { + font-size: 0.95rem; + + @include themed { + color: t($color); + } + + .quoteInfoLink { + text-decoration: none; + display: inline-flex; + align-items: center; + gap: 4px; + transition: opacity 0.2s ease; + cursor: pointer; + + @include themed { + color: t($link); + } + + svg { + font-size: 0.9rem; + transition: transform 0.2s ease; + } + + &:hover { + opacity: 0.8; + text-decoration: underline; + + svg { + transform: translateX(2px); + } + } + } + } + } +} + +.quoteInfoFooter { + display: flex; + justify-content: flex-end; + padding-top: 1rem; + border-top: 1px solid; + + @include themed { + border-color: t($modal-sidebarActive); + } + + button { + @include modal-button(standard); + } +} + .deleteButton { height: auto !important;