mirror of
https://github.com/mue/mue.git
synced 2026-06-05 23:45:53 +02:00
feat: Add QuoteInfoModal for displaying detailed quote information and integrate it with the Quote component
This commit is contained in:
@@ -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() {
|
||||
<div className="quotediv" style={{ display, fontSize }}>
|
||||
<Modal
|
||||
closeTimeoutMS={300}
|
||||
open={uiState.shareModal}
|
||||
isOpen={uiState.shareModal}
|
||||
className="Modal mainModal"
|
||||
overlayClassName="Overlay"
|
||||
ariaHideApp={false}
|
||||
@@ -102,6 +104,17 @@ export default function Quote() {
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
closeTimeoutMS={300}
|
||||
isOpen={uiState.infoModal}
|
||||
className="Modal mainModal"
|
||||
overlayClassName="Overlay"
|
||||
ariaHideApp={false}
|
||||
onRequestClose={() => toggleInfoModal(false)}
|
||||
>
|
||||
<QuoteInfoModal quoteData={quoteData} modalClose={() => toggleInfoModal(false)} />
|
||||
</Modal>
|
||||
|
||||
<span className="quote" ref={quoteRef}>
|
||||
{quoteData.quote}
|
||||
</span>
|
||||
@@ -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}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div className="quote-info-text" onClick={() => toggleInfoModal(true)}>
|
||||
<MdInfoOutline />
|
||||
<span>About this quote</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function AuthorInfo({
|
||||
onCopy,
|
||||
onFavourite,
|
||||
onShare,
|
||||
onInfo,
|
||||
isFavourited,
|
||||
}) {
|
||||
const t = useT();
|
||||
|
||||
@@ -10,6 +10,7 @@ export default function AuthorInfoLegacy({
|
||||
onCopy,
|
||||
onFavourite,
|
||||
onShare,
|
||||
onInfo,
|
||||
isFavourited,
|
||||
}) {
|
||||
const t = useT();
|
||||
|
||||
184
src/features/quote/components/QuoteInfoModal.jsx
Normal file
184
src/features/quote/components/QuoteInfoModal.jsx
Normal file
@@ -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 (
|
||||
<div className="quoteInfoModal">
|
||||
<div className="shareHeader">
|
||||
<h4>{variables.getMessage('widgets.quote.info.title') || 'Quote Information'}</h4>
|
||||
<Tooltip title={variables.getMessage('modals.welcome.buttons.close')}>
|
||||
<div className="close" onClick={modalClose}>
|
||||
<MdClose />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="quoteInfoContent">
|
||||
{/* Warning about automatic data */}
|
||||
{hasWarning && (
|
||||
<div className="quoteInfoWarning">
|
||||
<div className="warningIcon">
|
||||
<MdWarning />
|
||||
</div>
|
||||
<div className="warningText">
|
||||
<p>
|
||||
{variables.getMessage('widgets.quote.info.warning') ||
|
||||
'Author image and occupation are automatically fetched and may be incorrect.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quote Information */}
|
||||
<div className="quoteInfoSection">
|
||||
<div className="quoteInfoRow">
|
||||
<div className="quoteInfoIcon">
|
||||
<MdCategory />
|
||||
</div>
|
||||
<div className="quoteInfoText">
|
||||
<span className="quoteInfoLabel">
|
||||
{variables.getMessage('widgets.quote.info.type') || 'Type'}
|
||||
</span>
|
||||
<span className="quoteInfoValue">{getQuoteType()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{quoteData.realAuthor && quoteData.realAuthor !== quoteData.author && (
|
||||
<div className="quoteInfoRow">
|
||||
<div className="quoteInfoIcon">
|
||||
<MdPerson />
|
||||
</div>
|
||||
<div className="quoteInfoText">
|
||||
<span className="quoteInfoLabel">
|
||||
{variables.getMessage('widgets.quote.info.original_author') || 'Original Author'}
|
||||
</span>
|
||||
<span className="quoteInfoValue">{quoteData.realAuthor}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="quoteInfoRow">
|
||||
<div className="quoteInfoIcon">
|
||||
<MdSource />
|
||||
</div>
|
||||
<div className="quoteInfoText">
|
||||
<span className="quoteInfoLabel">
|
||||
{variables.getMessage('widgets.quote.info.source') || 'Source'}
|
||||
</span>
|
||||
<span className="quoteInfoValue">
|
||||
{quoteData.packId ? (
|
||||
<span
|
||||
className="quoteInfoLink"
|
||||
onClick={() => navigate(`/discover/item/${quoteData.packId}`)}
|
||||
>
|
||||
{getQuoteSource()}
|
||||
<HiMiniArrowUpRight />
|
||||
</span>
|
||||
) : (
|
||||
getQuoteSource()
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{quoteData.authorOccupation && quoteData.authorOccupation !== 'Unknown' && (
|
||||
<div className="quoteInfoRow">
|
||||
<div className="quoteInfoIcon">
|
||||
<MdPerson />
|
||||
</div>
|
||||
<div className="quoteInfoText">
|
||||
<span className="quoteInfoLabel">
|
||||
{variables.getMessage('widgets.quote.info.occupation') || 'Occupation'}
|
||||
</span>
|
||||
<span className="quoteInfoValue">{quoteData.authorOccupation}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{quoteData.authorlink && (
|
||||
<div className="quoteInfoRow">
|
||||
<div className="quoteInfoIcon">
|
||||
<MdSource />
|
||||
</div>
|
||||
<div className="quoteInfoText">
|
||||
<span className="quoteInfoLabel">
|
||||
{variables.getMessage('widgets.quote.info.wikipedia') || 'Wikipedia'}
|
||||
</span>
|
||||
<span className="quoteInfoValue">
|
||||
<a
|
||||
href={quoteData.authorlink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="quoteInfoLink"
|
||||
>
|
||||
{variables.getMessage('widgets.quote.info.view_article') || 'View Article'}
|
||||
<HiMiniArrowUpRight />
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="quoteInfoFooter">
|
||||
<Button
|
||||
type="settings"
|
||||
onClick={modalClose}
|
||||
label={variables.getMessage('modals.welcome.buttons.close') || 'Close'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(QuoteInfoModal);
|
||||
@@ -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';
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user