refactor: split quicklinks, move features to functional components

This commit is contained in:
David Ralph
2025-10-28 23:04:19 +00:00
parent 2eed0f7307
commit 293cc93500
39 changed files with 2847 additions and 3021 deletions

View File

@@ -1,5 +1,5 @@
import variables from 'config/variables';
import { PureComponent } from 'react';
import { useState, useEffect } from 'react';
import Modal from 'react-modal';
import { MainModal } from 'components/Elements';
@@ -11,28 +11,21 @@ import { parseDeepLink, shouldAutoOpenModal } from 'utils/deepLinking';
import Welcome from 'features/welcome/Welcome';
export default class Modals extends PureComponent {
constructor() {
super();
this.state = {
mainModal: false,
updateModal: false,
welcomeModal: false,
appsModal: false,
preview: false,
deepLinkData: null,
};
}
const Modals = () => {
const [mainModal, setMainModal] = useState(false);
const [updateModal, setUpdateModal] = useState(false);
const [welcomeModal, setWelcomeModal] = useState(false);
const [appsModal, setAppsModal] = useState(false);
const [preview, setPreview] = useState(false);
const [deepLinkData, setDeepLinkData] = useState(null);
componentDidMount() {
useEffect(() => {
// Check for deep link first (has priority)
if (shouldAutoOpenModal()) {
const deepLinkData = parseDeepLink();
this.setState({
mainModal: true,
deepLinkData,
});
variables.stats.postEvent('modal', `Opened via deep link: ${deepLinkData.tab}`);
const linkData = parseDeepLink();
setMainModal(true);
setDeepLinkData(linkData);
variables.stats.postEvent('modal', `Opened via deep link: ${linkData.tab}`);
return;
}
@@ -40,9 +33,7 @@ export default class Modals extends PureComponent {
localStorage.getItem('showWelcome') === 'true' &&
window.location.search !== '?nointro=true'
) {
this.setState({
welcomeModal: true,
});
setWelcomeModal(true);
variables.stats.postEvent('modal', 'Opened welcome');
}
@@ -56,71 +47,69 @@ export default class Modals extends PureComponent {
// hide refresh reminder once the user has refreshed the page
localStorage.setItem('showReminder', false);
}
}, []);
closeWelcome() {
const closeWelcome = () => {
localStorage.setItem('showWelcome', false);
this.setState({
welcomeModal: false,
});
setWelcomeModal(false);
EventBus.emit('refresh', 'widgetsWelcomeDone');
EventBus.emit('refresh', 'widgets');
EventBus.emit('refresh', 'backgroundwelcome');
}
};
previewWelcome() {
const previewWelcome = () => {
localStorage.setItem('showWelcome', false);
localStorage.setItem('welcomePreview', true);
this.setState({
welcomeModal: false,
preview: true,
});
setWelcomeModal(false);
setPreview(true);
EventBus.emit('refresh', 'widgetsWelcome');
}
};
toggleModal(type, action) {
this.setState({
[type]: action,
});
const toggleModal = (type, action) => {
const modalSetters = {
mainModal: setMainModal,
updateModal: setUpdateModal,
welcomeModal: setWelcomeModal,
appsModal: setAppsModal,
};
if (modalSetters[type]) {
modalSetters[type](action);
}
if (action !== false) {
variables.stats.postEvent('modal', `Opened ${type.replace('Modal', '')}`);
}
}
};
render() {
return (
<>
{this.state.welcomeModal === false && (
<Navbar openModal={(modal) => this.toggleModal(modal, true)} />
)}
<Modal
closeTimeoutMS={300}
id="modal"
onRequestClose={() => this.toggleModal('mainModal', false)}
isOpen={this.state.mainModal}
className="Modal mainModal"
overlayClassName="Overlay"
ariaHideApp={false}
>
<MainModal
modalClose={() => this.toggleModal('mainModal', false)}
deepLinkData={this.state.deepLinkData}
/>
</Modal>
<Modal
closeTimeoutMS={300}
onRequestClose={() => this.closeWelcome()}
isOpen={this.state.welcomeModal}
className="Modal welcomemodal mainModal"
overlayClassName="Overlay mainModal"
shouldCloseOnOverlayClick={false}
ariaHideApp={false}
>
<Welcome modalClose={() => this.closeWelcome()} modalSkip={() => this.previewWelcome()} />
</Modal>
{this.state.preview && <Preview setup={() => window.location.reload()} />}
</>
);
}
}
return (
<>
{welcomeModal === false && <Navbar openModal={(modal) => toggleModal(modal, true)} />}
<Modal
closeTimeoutMS={300}
id="modal"
onRequestClose={() => toggleModal('mainModal', false)}
isOpen={mainModal}
className="Modal mainModal"
overlayClassName="Overlay"
ariaHideApp={false}
>
<MainModal modalClose={() => toggleModal('mainModal', false)} deepLinkData={deepLinkData} />
</Modal>
<Modal
closeTimeoutMS={300}
onRequestClose={() => closeWelcome()}
isOpen={welcomeModal}
className="Modal welcomemodal mainModal"
overlayClassName="Overlay mainModal"
shouldCloseOnOverlayClick={false}
ariaHideApp={false}
>
<Welcome modalClose={() => closeWelcome()} modalSkip={() => previewWelcome()} />
</Modal>
{preview && <Preview setup={() => window.location.reload()} />}
</>
);
};
export { Modals as default, Modals };

View File

@@ -1,24 +1,24 @@
import variables from 'config/variables';
import { PureComponent, createRef } from 'react';
import { useState, useEffect, useRef } from 'react';
import { MdOutlineWifiOff } from 'react-icons/md';
import Modal from 'react-modal';
import Lightbox from '../../marketplace/components/Elements/Lightbox/Lightbox';
class Changelog extends PureComponent {
constructor() {
super();
this.state = {
title: null,
showLightbox: false,
lightboxImg: null,
};
this.offlineMode = localStorage.getItem('offlineMode') === 'true';
this.controller = new AbortController();
this.changelog = createRef();
}
const Changelog = () => {
const [title, setTitle] = useState(null);
const [content, setContent] = useState(null);
const [date, setDate] = useState(null);
const [image, setImage] = useState(null);
const [error, setError] = useState(false);
const [showLightbox, setShowLightbox] = useState(false);
const [lightboxImg, setLightboxImg] = useState(null);
const offlineMode = localStorage.getItem('offlineMode') === 'true';
const controllerRef = useRef(new AbortController());
const changelog = useRef();
parseMarkdown = (text) => {
const parseMarkdown = (text) => {
if (typeof text !== 'string') {
throw new Error('Input must be a string');
}
@@ -46,15 +46,15 @@ class Changelog extends PureComponent {
return text;
};
async getUpdate() {
const getUpdate = async () => {
const releases = await fetch(
`https://api.github.com/repos/${variables.constants.ORG_NAME}/${variables.constants.REPO_NAME}/releases`,
{
signal: this.controller.signal,
signal: controllerRef.current.signal,
},
);
if (this.controller.signal.aborted === true) {
if (controllerRef.current.signal.aborted === true) {
return;
}
@@ -67,109 +67,105 @@ class Changelog extends PureComponent {
}
// request the changelog
const res = await fetch(release.url, { signal: this.controller.signal });
const res = await fetch(release.url, { signal: controllerRef.current.signal });
if (res.status === 404) {
this.setState({ error: true });
setError(true);
return;
}
if (this.controller.signal.aborted === true) {
if (controllerRef.current.signal.aborted === true) {
return;
}
const changelog = await res.json();
this.setState({
title: changelog.name,
content: this.parseMarkdown(changelog.body),
date: new Date(changelog.published_at).toLocaleDateString(),
});
}
setTitle(changelog.name);
setContent(parseMarkdown(changelog.body));
setDate(new Date(changelog.published_at).toLocaleDateString());
};
componentDidMount() {
if (navigator.onLine === false || this.offlineMode) {
useEffect(() => {
if (navigator.onLine === false || offlineMode) {
return;
}
this.getUpdate();
}
getUpdate();
componentWillUnmount() {
// stop making requests
this.controller.abort();
}
render() {
const errorMessage = (msg) => {
return (
<div className="emptyItems">
<div className="emptyMessage">{msg}</div>
</div>
);
return () => {
// stop making requests
controllerRef.current.abort();
};
}, []);
if (navigator.onLine === false || this.offlineMode) {
return errorMessage(
<>
<MdOutlineWifiOff />
<h1>{variables.getMessage('modals.main.marketplace.offline.title')}</h1>
<p className="description">
{variables.getMessage('modals.main.marketplace.offline.description')}
</p>
</>,
);
}
if (this.state.error === true) {
return errorMessage(
<>
<MdOutlineWifiOff />
<span className="title">{variables.getMessage('modals.main.error_boundary.title')}</span>
<span className="subtitle">
{variables.getMessage('modals.main.error_boundary.message')}
</span>
</>,
);
}
if (!this.state.title) {
return errorMessage(
<div className="loaderHolder">
<div id="loader"></div>
<span className="subtitle">{variables.getMessage('modals.main.loading')}</span>
</div>,
);
}
const errorMessage = (msg) => {
return (
<div className="modalInfoPage changelogtab" ref={this.changelog}>
<span className="mainTitle">{this.state.title}</span>
<span className="subtitle">Released on {this.state.date}</span>
{this.state.image && (
<img
draggable={false}
src={this.state.image}
alt={this.state.title}
className="updateImage"
/>
)}
<div className="updateChangelog" dangerouslySetInnerHTML={{ __html: this.state.content }} />
<Modal
closeTimeoutMS={100}
onRequestClose={() => this.setState({ showLightbox: false })}
isOpen={this.state.showLightbox}
className="Modal lightBoxModal"
overlayClassName="Overlay resetoverlay"
ariaHideApp={false}
>
<Lightbox
modalClose={() => this.setState({ showLightbox: false })}
img={this.state.lightboxImg}
/>
</Modal>
<div className="emptyItems">
<div className="emptyMessage">{msg}</div>
</div>
);
};
if (navigator.onLine === false || offlineMode) {
return errorMessage(
<>
<MdOutlineWifiOff />
<h1>{variables.getMessage('modals.main.marketplace.offline.title')}</h1>
<p className="description">
{variables.getMessage('modals.main.marketplace.offline.description')}
</p>
</>,
);
}
}
if (error === true) {
return errorMessage(
<>
<MdOutlineWifiOff />
<span className="title">{variables.getMessage('modals.main.error_boundary.title')}</span>
<span className="subtitle">
{variables.getMessage('modals.main.error_boundary.message')}
</span>
</>,
);
}
if (!title) {
return errorMessage(
<div className="loaderHolder">
<div id="loader"></div>
<span className="subtitle">{variables.getMessage('modals.main.loading')}</span>
</div>,
);
}
return (
<div className="modalInfoPage changelogtab" ref={changelog}>
<span className="mainTitle">{title}</span>
<span className="subtitle">Released on {date}</span>
{image && (
<img
draggable={false}
src={image}
alt={title}
className="updateImage"
/>
)}
<div className="updateChangelog" dangerouslySetInnerHTML={{ __html: content }} />
<Modal
closeTimeoutMS={100}
onRequestClose={() => setShowLightbox(false)}
isOpen={showLightbox}
className="Modal lightBoxModal"
overlayClassName="Overlay resetoverlay"
ariaHideApp={false}
>
<Lightbox
modalClose={() => setShowLightbox(false)}
img={lightboxImg}
/>
</Modal>
</div>
);
};
export { Changelog as default, Changelog };

View File

@@ -1,5 +1,5 @@
import variables from 'config/variables';
import { PureComponent } from 'react';
import { useState, useEffect, useRef } from 'react';
import { MdOutlineOpenInNew } from 'react-icons/md';
@@ -7,32 +7,28 @@ import { Radio } from 'components/Form/Settings';
import languages from '@/i18n/languages.json';
class LanguageOptions extends PureComponent {
constructor() {
super();
this.state = {
quoteLanguages: [
{
name: variables.getMessage('modals.main.loading'),
value: 'loading',
},
],
};
this.controller = new AbortController();
}
const LanguageOptions = () => {
const [quoteLanguages, setQuoteLanguages] = useState([
{
name: variables.getMessage('modals.main.loading'),
value: 'loading',
},
]);
async getquoteLanguages() {
const controllerRef = useRef(new AbortController());
const getquoteLanguages = async () => {
const data = await (
await fetch(variables.constants.API_URL + '/quotes/languages', {
signal: this.controller.signal,
signal: controllerRef.current.signal,
})
).json();
if (this.controller.signal.aborted === true) {
if (controllerRef.current.signal.aborted === true) {
return;
}
const quoteLanguages = data.map((language) => {
const fetchedQuoteLanguages = data.map((language) => {
return {
name: languages.find((l) => l.value === language.name)
? languages.find((l) => l.value === language.name).name
@@ -41,69 +37,64 @@ class LanguageOptions extends PureComponent {
};
});
this.setState({
quoteLanguages,
});
}
setQuoteLanguages(fetchedQuoteLanguages);
};
componentDidMount() {
useEffect(() => {
if (navigator.onLine === false || localStorage.getItem('offlineMode') === 'true') {
return this.setState({
quoteLanguages: [
{
name: variables.getMessage('modals.main.marketplace.offline.description'),
value: 'loading',
},
],
});
setQuoteLanguages([
{
name: variables.getMessage('modals.main.marketplace.offline.description'),
value: 'loading',
},
]);
return;
}
this.getquoteLanguages();
}
getquoteLanguages();
componentWillUnmount() {
// stop making requests
this.controller.abort();
}
return () => {
// stop making requests
controllerRef.current.abort();
};
}, []);
render() {
return (
<>
<div className="modalHeader">
<span className="mainTitle">
{variables.getMessage('modals.main.settings.sections.language.title')}
</span>
<div className="headerActions">
<a
className="link"
href="https://hosted.weblate.org/new-lang/mue/mue-tab/"
target="_blank"
rel="noopener noreferrer"
>
Add translation
<MdOutlineOpenInNew />
</a>
</div>
</div>
<div className="languageSettings">
<Radio name="language" options={languages} element=".other" />
</div>
return (
<>
<div className="modalHeader">
<span className="mainTitle">
{variables.getMessage('modals.main.settings.sections.language.quote')}
{variables.getMessage('modals.main.settings.sections.language.title')}
</span>
<div className="languageSettings">
<Radio
name="quoteLanguage"
options={this.state.quoteLanguages.map((language) => {
return { name: language.name, value: language.value.name };
})}
defaultValue={this.state.quoteLanguages[0].name}
category="quote"
/>
<div className="headerActions">
<a
className="link"
href="https://hosted.weblate.org/new-lang/mue/mue-tab/"
target="_blank"
rel="noopener noreferrer"
>
Add translation
<MdOutlineOpenInNew />
</a>
</div>
</>
);
}
}
</div>
<div className="languageSettings">
<Radio name="language" options={languages} element=".other" />
</div>
<span className="mainTitle">
{variables.getMessage('modals.main.settings.sections.language.quote')}
</span>
<div className="languageSettings">
<Radio
name="quoteLanguage"
options={quoteLanguages.map((language) => {
return { name: language.name, value: language.value.name };
})}
defaultValue={quoteLanguages[0].name}
category="quote"
/>
</div>
</>
);
};
export { LanguageOptions as default, LanguageOptions };

View File

@@ -1,4 +1,4 @@
import { PureComponent, Fragment, Suspense, lazy } from 'react';
import { useState, useEffect, Fragment, Suspense, lazy, useMemo } from 'react';
import Clock from '../../time/Clock';
import Greeting from '../../greeting/Greeting';
@@ -17,74 +17,69 @@ import EventBus from 'utils/eventbus';
// as seen here it is ridiculously large
const Weather = lazy(() => import('../../weather/Weather'));
export default class Widgets extends PureComponent {
online = localStorage.getItem('offlineMode') === 'false';
constructor() {
super();
this.state = {
order: JSON.parse(localStorage.getItem('order')),
welcome: localStorage.getItem('showWelcome'),
};
// widgets we can re-order
this.widgets = {
time: this.enabled('time') && <Clock />,
greeting: this.enabled('greeting') && <Greeting />,
quote: this.enabled('quote') && <Quote />,
date: this.enabled('date') && <Date />,
quicklinks: this.enabled('quicklinksenabled') && this.online ? <QuickLinks /> : null,
message: this.enabled('message') && <Message />,
};
}
const Widgets = () => {
const online = localStorage.getItem('offlineMode') === 'false';
const [order, setOrder] = useState(JSON.parse(localStorage.getItem('order')));
const [welcome, setWelcome] = useState(localStorage.getItem('showWelcome'));
enabled(key) {
const enabled = (key) => {
return localStorage.getItem(key) === 'true';
}
};
componentDidMount() {
EventBus.on('refresh', (data) => {
// widgets we can re-order
const widgets = useMemo(
() => ({
time: enabled('time') && <Clock />,
greeting: enabled('greeting') && <Greeting />,
quote: enabled('quote') && <Quote />,
date: enabled('date') && <Date />,
quicklinks: enabled('quicklinksenabled') && online ? <QuickLinks /> : null,
message: enabled('message') && <Message />,
}),
[order], // Re-create widgets when order changes
);
useEffect(() => {
const handleRefresh = (data) => {
switch (data) {
case 'widgets':
return this.setState({
order: JSON.parse(localStorage.getItem('order')),
});
return setOrder(JSON.parse(localStorage.getItem('order')));
case 'widgetsWelcome':
this.setState({
welcome: localStorage.getItem('showWelcome'),
});
setWelcome(localStorage.getItem('showWelcome'));
localStorage.setItem('showWelcome', true);
window.onbeforeunload = () => {
localStorage.clear();
};
break;
case 'widgetsWelcomeDone':
this.setState({
welcome: localStorage.getItem('showWelcome'),
});
return (window.onbeforeunload = null);
setWelcome(localStorage.getItem('showWelcome'));
window.onbeforeunload = null;
break;
default:
break;
}
});
}
};
componentWillUnmount() {
EventBus.off('refresh');
}
EventBus.on('refresh', handleRefresh);
return () => {
EventBus.off('refresh');
};
}, []);
render() {
// don't show when welcome is there
return this.state.welcome !== 'false' ? (
<WidgetsLayout />
) : (
<WidgetsLayout>
<Suspense fallback={<></>}>
{this.enabled('searchBar') && <Search />}
{this.state.order.map((element, key) => (
<Fragment key={key}>{this.widgets[element]}</Fragment>
))}
{this.enabled('weatherEnabled') && this.online ? <Weather /> : null}
</Suspense>
</WidgetsLayout>
);
}
}
// don't show when welcome is there
return welcome !== 'false' ? (
<WidgetsLayout />
) : (
<WidgetsLayout>
<Suspense fallback={<></>}>
{enabled('searchBar') && <Search />}
{order.map((element, key) => (
<Fragment key={key}>{widgets[element]}</Fragment>
))}
{enabled('weatherEnabled') && online ? <Weather /> : null}
</Suspense>
</WidgetsLayout>
);
};
export { Widgets as default, Widgets };