refactor(MainModal): and related components for improved tab management and loading states

This commit is contained in:
alexsparkes
2025-10-28 12:39:11 +00:00
parent 16b113f7eb
commit ae3c135660
9 changed files with 290 additions and 187 deletions

View File

@@ -4,45 +4,24 @@ import { MdClose } from 'react-icons/md';
import './scss/index.scss';
import { Tooltip } from 'components/Elements';
import ModalLoader from './components/ModalLoader';
import { TAB_TYPES } from './constants/tabConfig';
const Settings = lazy(() => import('../../../features/misc/views/Settings'));
const Addons = lazy(() => import('../../../features/misc/views/Addons'));
const Marketplace = lazy(() => import('../../../features/misc/views/Marketplace'));
const renderLoader = () => (
<div style={{ display: 'flex', width: '100%', minHeight: '100%' }}>
<div className="modalSidebar">
<span className="mainTitle">Mue</span>
</div>
<div className="modalTabContent">
<div className="emptyItems">
<div className="emptyMessage">
<div className="loaderHolder">
<div id="loader"></div>
<span className="subtitle">{variables.getMessage('modals.main.loading')}</span>
</div>
</div>
</div>
</div>
</div>
);
// Map tab types to their corresponding components
const TAB_COMPONENTS = {
[TAB_TYPES.SETTINGS]: Settings,
[TAB_TYPES.ADDONS]: Addons,
[TAB_TYPES.MARKETPLACE]: Marketplace,
};
function MainModal({ modalClose }) {
const [currentTab, setCurrentTab] = useState('settings');
const [currentTab, setCurrentTab] = useState(TAB_TYPES.SETTINGS);
const changeTab = (type) => {
setCurrentTab(type);
};
const renderTab = () => {
switch (currentTab) {
case 'addons':
return <Addons changeTab={changeTab} />;
case 'marketplace':
return <Marketplace changeTab={changeTab} />;
default:
return <Settings changeTab={changeTab} />;
}
};
const TabComponent = TAB_COMPONENTS[currentTab] || Settings;
return (
<div className="frame">
@@ -55,10 +34,11 @@ function MainModal({ modalClose }) {
<MdClose />
</span>
</Tooltip>
<Suspense fallback={renderLoader()}>{renderTab()}</Suspense>
<Suspense fallback={<ModalLoader />}>
<TabComponent changeTab={setCurrentTab} />
</Suspense>
</div>
);
}
const MemoizedMainModal = memo(MainModal);
export { MemoizedMainModal as default, MemoizedMainModal as MainModal };
export default memo(MainModal);

View File

@@ -1,62 +1,6 @@
import variables from 'config/variables';
import { memo, useState, useEffect } from 'react';
import {
MdSettings as Settings,
MdWidgets as Addons,
MdShoppingBasket as Marketplace,
MdMenu as Navbar,
MdEmojiPeople as Greeting,
MdAccessAlarm as Time,
MdOutlineFormatQuote as Quote,
MdLink as QuickLinks,
MdDateRange as Date,
MdOutlineTextsms as Message,
MdOutlinePhoto as Background,
MdSearch,
MdCloudQueue as Weather,
MdFormatPaint as Appearance,
MdTranslate as Language,
MdOutlineSettings as Advanced,
MdBugReport as Experimental,
MdOutlineAssessment as Stats,
MdOutlineNewReleases as Changelog,
MdInfoOutline as About,
MdOutlineExtension as Added,
MdAddCircleOutline as Create,
MdViewAgenda as Overview,
MdCollectionsBookmark as Collections,
} from 'react-icons/md';
const iconMapping = {
[variables.getMessage('modals.main.marketplace.product.overview')]: <Overview />,
[variables.getMessage('modals.main.navbar.settings')]: <Settings />,
[variables.getMessage('modals.main.navbar.addons')]: <Addons />,
[variables.getMessage('modals.main.navbar.marketplace')]: <Marketplace />,
[variables.getMessage('modals.main.settings.sections.appearance.navbar.title')]: <Navbar />,
[variables.getMessage('modals.main.settings.sections.greeting.title')]: <Greeting />,
[variables.getMessage('modals.main.settings.sections.time.title')]: <Time />,
[variables.getMessage('modals.main.settings.sections.quicklinks.title')]: <QuickLinks />,
[variables.getMessage('modals.main.settings.sections.quote.title')]: <Quote />,
[variables.getMessage('modals.main.settings.sections.date.title')]: <Date />,
[variables.getMessage('modals.main.settings.sections.message.title')]: <Message />,
[variables.getMessage('modals.main.settings.sections.background.title')]: <Background />,
[variables.getMessage('modals.main.settings.sections.search.title')]: <MdSearch />,
[variables.getMessage('modals.main.settings.sections.weather.title')]: <Weather />,
[variables.getMessage('modals.main.settings.sections.appearance.title')]: <Appearance />,
[variables.getMessage('modals.main.settings.sections.language.title')]: <Language />,
[variables.getMessage('modals.main.settings.sections.advanced.title')]: <Advanced />,
[variables.getMessage('modals.main.settings.sections.stats.title')]: <Stats />,
[variables.getMessage('modals.main.settings.sections.experimental.title')]: <Experimental />,
[variables.getMessage('modals.main.settings.sections.changelog.title')]: <Changelog />,
[variables.getMessage('modals.main.settings.sections.about.title')]: <About />,
[variables.getMessage('modals.main.addons.added')]: <Added />,
[variables.getMessage('modals.main.addons.create.title')]: <Create />,
[variables.getMessage('modals.main.marketplace.all')]: <Addons />,
[variables.getMessage('modals.main.marketplace.photo_packs')]: <Background />,
[variables.getMessage('modals.main.marketplace.quote_packs')]: <Quote />,
[variables.getMessage('modals.main.marketplace.preset_settings')]: <Advanced />,
[variables.getMessage('modals.main.marketplace.collections')]: <Collections />,
};
import { getIconComponent, DIVIDER_LABELS, MUE_TITLE_LABELS } from '../constants/tabConfig';
function Tab({ label, currentTab, onClick, navbarTab }) {
const [isExperimental, setIsExperimental] = useState(true);
@@ -65,39 +9,34 @@ function Tab({ label, currentTab, onClick, navbarTab }) {
setIsExperimental(localStorage.getItem('experimental') !== 'false');
}, []);
let className = navbarTab ? 'navbar-item' : 'tab-list-item';
if (currentTab === label) {
className += navbarTab ? ' navbar-item-active' : ' tab-list-active';
}
// Get the icon component for this label
const IconComponent = getIconComponent(label, variables);
const icon = iconMapping[label];
const divider = [
variables.getMessage('modals.main.settings.sections.weather.title'),
variables.getMessage('modals.main.settings.sections.language.title'),
variables.getMessage('modals.main.marketplace.all'),
variables.getMessage('modals.main.settings.sections.experimental.title'),
].includes(label);
// Determine if this label should have a divider after it
const hasDivider = DIVIDER_LABELS.some((key) => variables.getMessage(key) === label);
const mue = [
variables.getMessage('modals.main.marketplace.product.overview'),
variables.getMessage('modals.main.addons.added'),
variables.getMessage('modals.main.marketplace.all'),
].includes(label);
// Determine if this label should have "Mue" title before it
const hasMueTitle = MUE_TITLE_LABELS.some((key) => variables.getMessage(key) === label);
if (
label === variables.getMessage('modals.main.settings.sections.experimental.title') &&
!isExperimental
) {
// Build className
const baseClass = navbarTab ? 'navbar-item' : 'tab-list-item';
const activeClass = navbarTab ? 'navbar-item-active' : 'tab-list-active';
const className = `${baseClass}${currentTab === label ? ` ${activeClass}` : ''}`;
// Hide experimental tab if experimental mode is disabled
const isExperimentalTab =
label === variables.getMessage('modals.main.settings.sections.experimental.title');
if (isExperimentalTab && !isExperimental) {
return <hr />;
}
return (
<>
{mue && <span className="mainTitle">Mue</span>}
{hasMueTitle && <span className="mainTitle">Mue</span>}
<button className={className} onClick={() => onClick(label)}>
{icon} <span>{label}</span>
{IconComponent && <IconComponent />} <span>{label}</span>
</button>
{divider && <hr />}
{hasDivider && <hr />}
</>
);
}

View File

@@ -1,21 +1,16 @@
import variables from 'config/variables';
import React, { useState, useEffect } from 'react';
import {
MdSettings,
MdOutlineShoppingBasket,
MdOutlineExtension,
MdRefresh,
MdClose,
} from 'react-icons/md';
import { useState } from 'react';
import Tab from './Tab';
import { Button } from 'components/Elements';
import ModalNavbar from '../components/ModalNavbar';
import ReminderInfo from '../components/ReminderInfo';
import ErrorBoundary from '../../../../features/misc/modals/ErrorBoundary';
const Tabs = (props) => {
const [currentTab, setCurrentTab] = useState(props.children[0].props.label);
const [currentName, setCurrentName] = useState(props.children[0].props.name);
const Tabs = ({ children, changeTab, current, navbar = false }) => {
const [currentTab, setCurrentTab] = useState(children[0]?.props.label);
const [currentName, setCurrentName] = useState(children[0]?.props.name);
const [showReminder, setShowReminder] = useState(localStorage.getItem('showReminder') === 'true');
const onClick = (tab, name) => {
const handleTabClick = (tab, name) => {
if (name !== currentName) {
variables.stats.postEvent('tab', `Opened ${name}`);
}
@@ -24,77 +19,30 @@ const Tabs = (props) => {
setCurrentName(name);
};
const hideReminder = () => {
localStorage.setItem('showReminder', false);
document.querySelector('.reminder-info').style.display = 'none';
const handleHideReminder = () => {
localStorage.setItem('showReminder', 'false');
setShowReminder(false);
};
const navbarButtons = [
{
tab: 'settings',
icon: <MdSettings />,
},
{
tab: 'addons',
icon: <MdOutlineExtension />,
},
{
tab: 'marketplace',
icon: <MdOutlineShoppingBasket />,
},
];
const reminderInfo = (
<div
className="reminder-info"
style={{ display: localStorage.getItem('showReminder') === 'true' ? 'flex' : 'none' }}
>
<div className="shareHeader">
<span className="title">{variables.getMessage('modals.main.settings.reminder.title')}</span>
<span className="closeModal" onClick={hideReminder}>
<MdClose />
</span>
</div>
<span className="subtitle">
{variables.getMessage('modals.main.settings.reminder.message')}
</span>
<button onClick={() => window.location.reload()}>
<MdRefresh />
{variables.getMessage('modals.main.error_boundary.refresh')}
</button>
</div>
);
return (
<div style={{ display: 'flex', width: '100%', minHeight: '100%' }}>
<div className="modalSidebar">
{props.children.map((tab, index) => (
{children.map((tab, index) => (
<Tab
currentTab={currentTab}
key={index}
currentTab={currentTab}
label={tab.props.label}
onClick={(nextTab) => onClick(nextTab, tab.props.name)}
navbarTab={props.navbar || false}
onClick={(nextTab) => handleTabClick(nextTab, tab.props.name)}
navbarTab={navbar}
/>
))}
{reminderInfo}
<ReminderInfo isVisible={showReminder} onHide={handleHideReminder} />
</div>
<div className="modalTabContent">
<div className="modalNavbar">
{navbarButtons.map(({ tab, icon }, index) => (
<Button
type="navigation"
onClick={() => props.changeTab(tab)}
icon={icon}
label={variables.getMessage(`modals.main.navbar.${tab}`)}
active={props.current === tab}
key={`${tab}-${index}`}
/>
))}
</div>
{props.children.map((tab, index) => {
<ModalNavbar currentTab={current} onChangeTab={changeTab} />
{children.map((tab, index) => {
if (tab.props.label !== currentTab) {
return undefined;
return null;
}
return (

View File

@@ -0,0 +1,21 @@
import variables from 'config/variables';
const ModalLoader = () => (
<div style={{ display: 'flex', width: '100%', minHeight: '100%' }}>
<div className="modalSidebar">
<span className="mainTitle">Mue</span>
</div>
<div className="modalTabContent">
<div className="emptyItems">
<div className="emptyMessage">
<div className="loaderHolder">
<div id="loader"></div>
<span className="subtitle">{variables.getMessage('modals.main.loading')}</span>
</div>
</div>
</div>
</div>
</div>
);
export default ModalLoader;

View File

@@ -0,0 +1,20 @@
import variables from 'config/variables';
import { Button } from 'components/Elements';
import { NAVBAR_BUTTONS } from '../constants/tabConfig';
const ModalNavbar = ({ currentTab, onChangeTab }) => (
<div className="modalNavbar">
{NAVBAR_BUTTONS.map(({ tab, icon: Icon, messageKey }) => (
<Button
key={tab}
type="navigation"
onClick={() => onChangeTab(tab)}
icon={<Icon />}
label={variables.getMessage(messageKey)}
active={currentTab === tab}
/>
))}
</div>
);
export default ModalNavbar;

View File

@@ -0,0 +1,28 @@
import variables from 'config/variables';
import { MdRefresh, MdClose } from 'react-icons/md';
const ReminderInfo = ({ isVisible, onHide }) => {
if (!isVisible) {
return null;
}
return (
<div className="reminder-info">
<div className="shareHeader">
<span className="title">{variables.getMessage('modals.main.settings.reminder.title')}</span>
<span className="closeModal" onClick={onHide}>
<MdClose />
</span>
</div>
<span className="subtitle">
{variables.getMessage('modals.main.settings.reminder.message')}
</span>
<button onClick={() => window.location.reload()}>
<MdRefresh />
{variables.getMessage('modals.main.error_boundary.refresh')}
</button>
</div>
);
};
export default ReminderInfo;

View File

@@ -0,0 +1,3 @@
export { default as ModalLoader } from './ModalLoader';
export { default as ModalNavbar } from './ModalNavbar';
export { default as ReminderInfo } from './ReminderInfo';

View File

@@ -0,0 +1,164 @@
import {
MdSettings,
MdWidgets,
MdShoppingBasket,
MdMenu,
MdEmojiPeople,
MdAccessAlarm,
MdOutlineFormatQuote,
MdLink,
MdDateRange,
MdOutlineTextsms,
MdOutlinePhoto,
MdSearch,
MdCloudQueue,
MdFormatPaint,
MdTranslate,
MdOutlineSettings,
MdBugReport,
MdOutlineAssessment,
MdOutlineNewReleases,
MdInfoOutline,
MdOutlineExtension,
MdAddCircleOutline,
MdViewAgenda,
MdCollectionsBookmark,
} from 'react-icons/md';
// Tab type constants
export const TAB_TYPES = {
SETTINGS: 'settings',
ADDONS: 'addons',
MARKETPLACE: 'marketplace',
};
// Icon component mapping - using component references instead of elements
export const ICON_COMPONENTS = {
SETTINGS: MdSettings,
ADDONS: MdWidgets,
MARKETPLACE: MdShoppingBasket,
NAVBAR: MdMenu,
GREETING: MdEmojiPeople,
TIME: MdAccessAlarm,
QUOTE: MdOutlineFormatQuote,
QUICKLINKS: MdLink,
DATE: MdDateRange,
MESSAGE: MdOutlineTextsms,
BACKGROUND: MdOutlinePhoto,
SEARCH: MdSearch,
WEATHER: MdCloudQueue,
APPEARANCE: MdFormatPaint,
LANGUAGE: MdTranslate,
ADVANCED: MdOutlineSettings,
EXPERIMENTAL: MdBugReport,
STATS: MdOutlineAssessment,
CHANGELOG: MdOutlineNewReleases,
ABOUT: MdInfoOutline,
ADDED: MdOutlineExtension,
CREATE: MdAddCircleOutline,
OVERVIEW: MdViewAgenda,
COLLECTIONS: MdCollectionsBookmark,
};
// Message keys for icon mapping
export const MESSAGE_KEYS = {
OVERVIEW: 'modals.main.marketplace.product.overview',
SETTINGS: 'modals.main.navbar.settings',
ADDONS: 'modals.main.navbar.addons',
MARKETPLACE: 'modals.main.navbar.marketplace',
NAVBAR: 'modals.main.settings.sections.appearance.navbar.title',
GREETING: 'modals.main.settings.sections.greeting.title',
TIME: 'modals.main.settings.sections.time.title',
QUICKLINKS: 'modals.main.settings.sections.quicklinks.title',
QUOTE: 'modals.main.settings.sections.quote.title',
DATE: 'modals.main.settings.sections.date.title',
MESSAGE: 'modals.main.settings.sections.message.title',
BACKGROUND: 'modals.main.settings.sections.background.title',
SEARCH: 'modals.main.settings.sections.search.title',
WEATHER: 'modals.main.settings.sections.weather.title',
APPEARANCE: 'modals.main.settings.sections.appearance.title',
LANGUAGE: 'modals.main.settings.sections.language.title',
ADVANCED: 'modals.main.settings.sections.advanced.title',
STATS: 'modals.main.settings.sections.stats.title',
EXPERIMENTAL: 'modals.main.settings.sections.experimental.title',
CHANGELOG: 'modals.main.settings.sections.changelog.title',
ABOUT: 'modals.main.settings.sections.about.title',
ADDED: 'modals.main.addons.added',
CREATE: 'modals.main.addons.create.title',
ALL_MARKETPLACE: 'modals.main.marketplace.all',
PHOTO_PACKS: 'modals.main.marketplace.photo_packs',
QUOTE_PACKS: 'modals.main.marketplace.quote_packs',
PRESET_SETTINGS: 'modals.main.marketplace.preset_settings',
COLLECTIONS: 'modals.main.marketplace.collections',
};
// Helper to get icon component by translated label
// This function builds a map at runtime using variables.getMessage
export const getIconComponent = (label, variables) => {
const iconMap = {
[variables.getMessage(MESSAGE_KEYS.OVERVIEW)]: ICON_COMPONENTS.OVERVIEW,
[variables.getMessage(MESSAGE_KEYS.SETTINGS)]: ICON_COMPONENTS.SETTINGS,
[variables.getMessage(MESSAGE_KEYS.ADDONS)]: ICON_COMPONENTS.ADDONS,
[variables.getMessage(MESSAGE_KEYS.MARKETPLACE)]: ICON_COMPONENTS.MARKETPLACE,
[variables.getMessage(MESSAGE_KEYS.NAVBAR)]: ICON_COMPONENTS.NAVBAR,
[variables.getMessage(MESSAGE_KEYS.GREETING)]: ICON_COMPONENTS.GREETING,
[variables.getMessage(MESSAGE_KEYS.TIME)]: ICON_COMPONENTS.TIME,
[variables.getMessage(MESSAGE_KEYS.QUICKLINKS)]: ICON_COMPONENTS.QUICKLINKS,
[variables.getMessage(MESSAGE_KEYS.QUOTE)]: ICON_COMPONENTS.QUOTE,
[variables.getMessage(MESSAGE_KEYS.DATE)]: ICON_COMPONENTS.DATE,
[variables.getMessage(MESSAGE_KEYS.MESSAGE)]: ICON_COMPONENTS.MESSAGE,
[variables.getMessage(MESSAGE_KEYS.BACKGROUND)]: ICON_COMPONENTS.BACKGROUND,
[variables.getMessage(MESSAGE_KEYS.SEARCH)]: ICON_COMPONENTS.SEARCH,
[variables.getMessage(MESSAGE_KEYS.WEATHER)]: ICON_COMPONENTS.WEATHER,
[variables.getMessage(MESSAGE_KEYS.APPEARANCE)]: ICON_COMPONENTS.APPEARANCE,
[variables.getMessage(MESSAGE_KEYS.LANGUAGE)]: ICON_COMPONENTS.LANGUAGE,
[variables.getMessage(MESSAGE_KEYS.ADVANCED)]: ICON_COMPONENTS.ADVANCED,
[variables.getMessage(MESSAGE_KEYS.STATS)]: ICON_COMPONENTS.STATS,
[variables.getMessage(MESSAGE_KEYS.EXPERIMENTAL)]: ICON_COMPONENTS.EXPERIMENTAL,
[variables.getMessage(MESSAGE_KEYS.CHANGELOG)]: ICON_COMPONENTS.CHANGELOG,
[variables.getMessage(MESSAGE_KEYS.ABOUT)]: ICON_COMPONENTS.ABOUT,
[variables.getMessage(MESSAGE_KEYS.ADDED)]: ICON_COMPONENTS.ADDED,
[variables.getMessage(MESSAGE_KEYS.CREATE)]: ICON_COMPONENTS.CREATE,
[variables.getMessage(MESSAGE_KEYS.ALL_MARKETPLACE)]: ICON_COMPONENTS.ADDONS,
[variables.getMessage(MESSAGE_KEYS.PHOTO_PACKS)]: ICON_COMPONENTS.BACKGROUND,
[variables.getMessage(MESSAGE_KEYS.QUOTE_PACKS)]: ICON_COMPONENTS.QUOTE,
[variables.getMessage(MESSAGE_KEYS.PRESET_SETTINGS)]: ICON_COMPONENTS.ADVANCED,
[variables.getMessage(MESSAGE_KEYS.COLLECTIONS)]: ICON_COMPONENTS.COLLECTIONS,
};
return iconMap[label];
};
// Navbar configuration
export const NAVBAR_BUTTONS = [
{
tab: TAB_TYPES.SETTINGS,
icon: ICON_COMPONENTS.SETTINGS,
messageKey: 'modals.main.navbar.settings',
},
{
tab: TAB_TYPES.ADDONS,
icon: ICON_COMPONENTS.ADDONS,
messageKey: 'modals.main.navbar.addons',
},
{
tab: TAB_TYPES.MARKETPLACE,
icon: ICON_COMPONENTS.MARKETPLACE,
messageKey: 'modals.main.navbar.marketplace',
},
];
// Labels that should have dividers after them
export const DIVIDER_LABELS = [
'modals.main.settings.sections.weather.title',
'modals.main.settings.sections.language.title',
'modals.main.marketplace.all',
'modals.main.settings.sections.experimental.title',
];
// Labels that should have "Mue" title before them
export const MUE_TITLE_LABELS = [
'modals.main.marketplace.product.overview',
'modals.main.addons.added',
'modals.main.marketplace.all',
];

View File

@@ -1 +1 @@
export * from './Main';
export { default as MainModal } from './Main';