refactor(frontend): convert contexts layer to TS and align page integration

This commit is contained in:
Yiannis
2026-03-11 02:35:19 +02:00
parent 499196b0f6
commit c8b47a7922
26 changed files with 336 additions and 306 deletions

View File

@@ -55,7 +55,7 @@ export const HistoryPage: React.FC = () => {
const anonymousPage = isAnonymous || !PageStore.get('config-options').pages.profile.includeHistory; const anonymousPage = isAnonymous || !PageStore.get('config-options').pages.profile.includeHistory;
if (!anonymousPage) { if (!anonymousPage) {
addClassname(document.getElementById('page-history'), 'profile-page-history'); addClassname(document.getElementById('page-history')!, 'profile-page-history');
window.MediaCMS.profileId = username; window.MediaCMS.profileId = username;
} }

View File

@@ -76,7 +76,7 @@ export const HomePage: React.FC<HomePageProps> = ({
<MediaListRow <MediaListRow
title={featured_title} title={featured_title}
style={!visibleFeatured ? { display: 'none' } : undefined} style={!visibleFeatured ? { display: 'none' } : undefined}
viewAllLink={featured_view_all_link ? links.featured : null} viewAllLink={featured_view_all_link ? links.featured : undefined}
> >
<InlineSliderItemListAsync <InlineSliderItemListAsync
requestUrl={apiUrl.featured} requestUrl={apiUrl.featured}
@@ -93,7 +93,7 @@ export const HomePage: React.FC<HomePageProps> = ({
<MediaListRow <MediaListRow
title={recommended_title} title={recommended_title}
style={!visibleRecommended ? { display: 'none' } : undefined} style={!visibleRecommended ? { display: 'none' } : undefined}
viewAllLink={recommended_view_all_link ? links.recommended : null} viewAllLink={recommended_view_all_link ? links.recommended : undefined}
> >
<InlineSliderItemListAsync <InlineSliderItemListAsync
requestUrl={apiUrl.recommended} requestUrl={apiUrl.recommended}
@@ -108,7 +108,7 @@ export const HomePage: React.FC<HomePageProps> = ({
<MediaListRow <MediaListRow
title={latest_title} title={latest_title}
style={!visibleLatest ? { display: 'none' } : undefined} style={!visibleLatest ? { display: 'none' } : undefined}
viewAllLink={latest_view_all_link ? links.latest : null} viewAllLink={latest_view_all_link ? links.latest : undefined}
> >
<ItemListAsync <ItemListAsync
pageItems={30} pageItems={30}

View File

@@ -55,7 +55,7 @@ export const LikedMediaPage: React.FC = () => {
const anonymousPage = isAnonymous || !PageStore.get('config-options').pages.profile.includeLikedMedia; const anonymousPage = isAnonymous || !PageStore.get('config-options').pages.profile.includeLikedMedia;
if (!anonymousPage) { if (!anonymousPage) {
addClassname(document.getElementById('page-liked'), 'profile-page-liked'); addClassname(document.getElementById('page-liked')!, 'profile-page-liked');
window.MediaCMS.profileId = username; window.MediaCMS.profileId = username;
} }

View File

@@ -1,5 +0,0 @@
import React, { createContext } from 'react';
import { config as mediacmsConfig } from '../settings/config.js';
export const ApiUrlContext = createContext(mediacmsConfig(window.MediaCMS).api);
export const ApiUrlConsumer = ApiUrlContext.Consumer;

View File

@@ -0,0 +1,5 @@
import { createContext } from 'react';
import { config as mediacmsConfig } from '../settings/config';
export const ApiUrlContext = createContext(mediacmsConfig(window.MediaCMS).api);
export const ApiUrlConsumer = ApiUrlContext.Consumer;

View File

@@ -1,130 +0,0 @@
import React, { createContext } from 'react';
import { config as mediacmsConfig } from '../settings/config.js';
import { translateString } from '../../utils/helpers/';
const config = mediacmsConfig(window.MediaCMS);
const links = config.url;
const theme = config.theme;
const user = config.member;
const hasThemeSwitcher = theme.switch.enabled && 'header' === theme.switch.position;
function popupTopNavItems() {
const items = [];
if (!user.is.anonymous) {
if (user.can.addMedia) {
items.push({
link: links.user.addMedia,
icon: 'video_call',
text: translateString('Upload media'),
itemAttr: {
className: 'visible-only-in-small',
},
});
if (user.pages.media) {
items.push({
link: user.pages.media,
icon: 'video_library',
text: translateString('My media'),
});
}
}
items.push({
link: links.signout,
icon: 'exit_to_app',
text: translateString('Sign out'),
});
}
return items;
}
function popupMiddleNavItems() {
const items = [];
if (hasThemeSwitcher) {
items.push({
itemType: 'open-subpage',
icon: 'brightness_4',
iconPos: 'left',
text: 'Switch theme',
buttonAttr: {
className: 'change-page',
'data-page-id': 'switch-theme',
},
});
}
if (user.is.anonymous) {
if (user.can.login) {
items.push({
itemType: 'link',
icon: 'login',
iconPos: 'left',
text: translateString('Sign in'),
link: links.signin,
linkAttr: {
className: hasThemeSwitcher ? 'visible-only-in-small' : 'visible-only-in-extra-small',
},
});
}
if (user.can.register) {
items.push({
itemType: 'link',
icon: 'person_add',
iconPos: 'left',
text: translateString('Register'),
link: links.register,
linkAttr: {
className: hasThemeSwitcher ? 'visible-only-in-small' : 'visible-only-in-extra-small',
},
});
}
} else {
items.push({
link: links.user.editProfile,
icon: 'brush',
text: translateString('Edit profile'),
});
if (user.can.changePassword) {
items.push({
link: links.changePassword,
icon: 'lock',
text: translateString('Change password'),
});
}
}
return items;
}
function popupBottomNavItems() {
const items = [];
if (user.is.admin) {
items.push({
link: links.admin,
icon: 'admin_panel_settings',
text: 'MediaCMS administration',
});
}
return items;
}
export const HeaderContext = createContext({
hasThemeSwitcher,
popupNavItems: {
top: popupTopNavItems(),
middle: popupMiddleNavItems(),
bottom: popupBottomNavItems(),
},
});
export const HeaderConsumer = HeaderContext.Consumer;

View File

@@ -0,0 +1,130 @@
import { createContext } from 'react';
import { config as mediacmsConfig } from '../settings/config';
import { translateString } from '../helpers';
const config = mediacmsConfig(window.MediaCMS);
const links = config.url;
const theme = config.theme;
const user = config.member;
const hasThemeSwitcher = theme.switch.enabled && 'header' === theme.switch.position;
function popupTopNavItems() {
const items = [];
if (!user.is.anonymous) {
if (user.can.addMedia) {
items.push({
link: links.user.addMedia,
icon: 'video_call',
text: translateString('Upload media'),
itemAttr: {
className: 'visible-only-in-small',
},
});
if (user.pages.media) {
items.push({
link: user.pages.media,
icon: 'video_library',
text: translateString('My media'),
});
}
}
items.push({
link: links.signout,
icon: 'exit_to_app',
text: translateString('Sign out'),
});
}
return items;
}
function popupMiddleNavItems() {
const items = [];
if (hasThemeSwitcher) {
items.push({
itemType: 'open-subpage',
icon: 'brightness_4',
iconPos: 'left',
text: 'Switch theme',
buttonAttr: {
className: 'change-page',
'data-page-id': 'switch-theme',
},
});
}
if (user.is.anonymous) {
if (user.can.login) {
items.push({
itemType: 'link',
icon: 'login',
iconPos: 'left',
text: translateString('Sign in'),
link: links.signin,
linkAttr: {
className: hasThemeSwitcher ? 'visible-only-in-small' : 'visible-only-in-extra-small',
},
});
}
if (user.can.register) {
items.push({
itemType: 'link',
icon: 'person_add',
iconPos: 'left',
text: translateString('Register'),
link: links.register,
linkAttr: {
className: hasThemeSwitcher ? 'visible-only-in-small' : 'visible-only-in-extra-small',
},
});
}
} else {
items.push({
link: links.user.editProfile,
icon: 'brush',
text: translateString('Edit profile'),
});
if (user.can.changePassword) {
items.push({
link: links.changePassword,
icon: 'lock',
text: translateString('Change password'),
});
}
}
return items;
}
function popupBottomNavItems() {
const items = [];
if (user.is.admin) {
items.push({
link: links.admin,
icon: 'admin_panel_settings',
text: 'MediaCMS administration',
});
}
return items;
}
export const HeaderContext = createContext({
hasThemeSwitcher,
popupNavItems: {
top: popupTopNavItems(),
middle: popupMiddleNavItems(),
bottom: popupBottomNavItems(),
},
});
export const HeaderConsumer = HeaderContext.Consumer;

View File

@@ -1,13 +1,15 @@
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react'; import React, { createContext, ReactNode, useContext, useEffect, useMemo, useState } from 'react';
import { BrowserCache } from '../classes/'; import { BrowserCache } from '../classes';
import { PageStore } from '../stores/'; import { PageStore } from '../stores';
import { addClassname, removeClassname, inEmbeddedApp } from '../helpers/'; import { addClassname, removeClassname, inEmbeddedApp } from '../helpers';
import SiteContext from './SiteContext'; import SiteContext from './SiteContext';
let slidingSidebarTimeout; let slidingSidebarTimeout: NodeJS.Timeout | null = null;
function onSidebarVisibilityChange(visibleSidebar) { function onSidebarVisibilityChange(visibleSidebar: boolean) {
clearTimeout(slidingSidebarTimeout); if (slidingSidebarTimeout) {
clearTimeout(slidingSidebarTimeout);
}
addClassname(document.body, 'sliding-sidebar'); addClassname(document.body, 'sliding-sidebar');
@@ -39,18 +41,29 @@ function onSidebarVisibilityChange(visibleSidebar) {
}, 20); }, 20);
} }
export const LayoutContext = createContext(); export const LayoutContext = createContext({
enabledSidebar: true,
visibleSidebar: true,
setVisibleSidebar: (_: boolean) => {},
visibleMobileSearch: false,
toggleMobileSearch: () => {},
toggleSidebar: () => {},
});
export const LayoutProvider = ({ children }) => { export const LayoutProvider = ({ children }: { children: ReactNode }) => {
const site = useContext(SiteContext); const site = useContext(SiteContext);
const cache = new BrowserCache('MediaCMS[' + site.id + '][layout]', 86400); const cache = BrowserCache('MediaCMS[' + site.id + '][layout]', 86400);
const isMediaPage = useMemo(() => PageStore.get('current-page') === 'media', []); const isMediaPage = useMemo(() => PageStore.get('current-page') === 'media', []);
const isEmbeddedApp = useMemo(() => inEmbeddedApp(), []); const isEmbeddedApp = useMemo(() => inEmbeddedApp(), []);
const enabledSidebar = Boolean(document.getElementById('app-sidebar') || document.querySelector('.page-sidebar')); const enabledSidebar = Boolean(document.getElementById('app-sidebar') || document.querySelector('.page-sidebar'));
const [visibleSidebar, setVisibleSidebar] = useState(cache.get('visible-sidebar')); const [visibleSidebar, setVisibleSidebar] = useState<boolean>(
cache instanceof Error
? true // @todo: Check this again
: cache.get('visible-sidebar')
);
const [visibleMobileSearch, setVisibleMobileSearch] = useState(false); const [visibleMobileSearch, setVisibleMobileSearch] = useState(false);
const toggleMobileSearch = () => { const toggleMobileSearch = () => {
@@ -71,7 +84,9 @@ export const LayoutProvider = ({ children }) => {
} }
if (!isEmbeddedApp && !isMediaPage && 1023 < window.innerWidth) { if (!isEmbeddedApp && !isMediaPage && 1023 < window.innerWidth) {
cache.set('visible-sidebar', visibleSidebar); if (!(cache instanceof Error)) {
cache.set('visible-sidebar', visibleSidebar);
}
} }
}, [isEmbeddedApp, isMediaPage, visibleSidebar]); }, [isEmbeddedApp, isMediaPage, visibleSidebar]);

View File

@@ -1,5 +1,5 @@
import React, { createContext } from 'react'; import { createContext } from 'react';
import { config as mediacmsConfig } from '../settings/config.js'; import { config as mediacmsConfig } from '../settings/config';
export const LinksContext = createContext(mediacmsConfig(window.MediaCMS).url); export const LinksContext = createContext(mediacmsConfig(window.MediaCMS).url);
export const LinksConsumer = LinksContext.Consumer; export const LinksConsumer = LinksContext.Consumer;

View File

@@ -1,5 +1,5 @@
import React, { createContext } from 'react'; import { createContext } from 'react';
import { config as mediacmsConfig } from '../settings/config.js'; import { config as mediacmsConfig } from '../settings/config';
export const MemberContext = createContext(mediacmsConfig(window.MediaCMS).member); export const MemberContext = createContext(mediacmsConfig(window.MediaCMS).member);
export const MemberConsumer = MemberContext.Consumer; export const MemberConsumer = MemberContext.Consumer;

View File

@@ -1,4 +0,0 @@
import React, { createContext } from 'react';
import { config as mediacmsConfig } from '../settings/config.js';
export const PlaylistsContext = createContext(mediacmsConfig(window.MediaCMS).playlists);

View File

@@ -0,0 +1,4 @@
import { createContext } from 'react';
import { config as mediacmsConfig } from '../settings/config';
export const PlaylistsContext = createContext(mediacmsConfig(window.MediaCMS).playlists);

View File

@@ -1,5 +0,0 @@
import React, { createContext } from 'react';
import { config as mediacmsConfig } from '../settings/config.js';
export const ShareOptionsContext = createContext(mediacmsConfig(window.MediaCMS).media.share.options);

View File

@@ -0,0 +1,4 @@
import { createContext } from 'react';
import { config as mediacmsConfig } from '../settings/config';
export const ShareOptionsContext = createContext(mediacmsConfig(window.MediaCMS).media.share.options);

View File

@@ -1,5 +0,0 @@
import React, { createContext } from 'react';
import { config as mediacmsConfig } from '../settings/config.js';
export const SidebarContext = createContext(mediacmsConfig(window.MediaCMS).sidebar);
export const SidebarConsumer = SidebarContext.Consumer;

View File

@@ -0,0 +1,5 @@
import { createContext } from 'react';
import { config as mediacmsConfig } from '../settings/config';
export const SidebarContext = createContext(mediacmsConfig(window.MediaCMS).sidebar);
export const SidebarConsumer = SidebarContext.Consumer;

View File

@@ -1,5 +1,5 @@
import React, { createContext } from 'react'; import { createContext } from 'react';
import { config as mediacmsConfig } from '../settings/config.js'; import { config as mediacmsConfig } from '../settings/config';
export const SiteContext = createContext(mediacmsConfig(window.MediaCMS).site); export const SiteContext = createContext(mediacmsConfig(window.MediaCMS).site);
export const SiteConsumer = SiteContext.Consumer; export const SiteConsumer = SiteContext.Consumer;

View File

@@ -1,10 +1,10 @@
import React, { createContext } from 'react'; import { createContext } from 'react';
import { config as mediacmsConfig } from '../settings/config.js'; import { config as mediacmsConfig } from '../settings/config';
const notifications = mediacmsConfig(window.MediaCMS).notifications.messages; const notifications = mediacmsConfig(window.MediaCMS).notifications.messages;
const texts = { const texts = {
notifications, notifications,
}; };
export const TextsContext = createContext(texts); export const TextsContext = createContext(texts);

View File

@@ -1,80 +0,0 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
import { BrowserCache } from '../classes/';
import { addClassname, removeClassname, supportsSvgAsImg } from '../helpers/';
import { config as mediacmsConfig } from '../settings/config.js';
import SiteContext from './SiteContext';
const config = mediacmsConfig(window.MediaCMS);
function initLogo(logo) {
let light = null;
let dark = null;
if (void 0 !== logo.darkMode) {
if (supportsSvgAsImg() && void 0 !== logo.darkMode.svg && '' !== logo.darkMode.svg) {
dark = logo.darkMode.svg;
} else if (void 0 !== logo.darkMode.img && '' !== logo.darkMode.img) {
dark = logo.darkMode.img;
}
}
if (void 0 !== logo.lightMode) {
if (supportsSvgAsImg() && void 0 !== logo.lightMode.svg && '' !== logo.lightMode.svg) {
light = logo.lightMode.svg;
} else if (void 0 !== logo.lightMode.img && '' !== logo.lightMode.img) {
light = logo.lightMode.img;
}
}
if (null !== light || null !== dark) {
if (null === light) {
light = dark;
} else if (null === dark) {
dark = light;
}
}
return {
light,
dark,
};
}
function initMode(cachedValue, defaultValue) {
return 'light' === cachedValue || 'dark' === cachedValue ? cachedValue : defaultValue;
}
export const ThemeContext = createContext();
export const ThemeProvider = ({ children }) => {
const site = useContext(SiteContext);
const cache = new BrowserCache('MediaCMS[' + site.id + '][theme]', 86400);
const [themeMode, setThemeMode] = useState(initMode(cache.get('mode'), config.theme.mode));
const logos = initLogo(config.theme.logo);
const [logo, setLogo] = useState(logos[themeMode]);
const changeMode = () => {
setThemeMode('light' === themeMode ? 'dark' : 'light');
};
useEffect(() => {
if ('dark' === themeMode) {
addClassname(document.body, 'dark_theme');
} else {
removeClassname(document.body, 'dark_theme');
}
cache.set('mode', themeMode);
setLogo(logos[themeMode]);
}, [themeMode]);
const value = {
logo,
currentThemeMode: themeMode,
changeThemeMode: changeMode,
themeModeSwitcher: config.theme.switch,
};
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
};
export const ThemeConsumer = ThemeContext.Consumer;

View File

@@ -0,0 +1,95 @@
import React, { createContext, ReactNode, useContext, useEffect, useState } from 'react';
import { GlobalMediaCMS } from '../../types';
import { BrowserCache } from '../classes';
import { addClassname, removeClassname, supportsSvgAsImg } from '../helpers';
import { config as mediacmsConfig } from '../settings/config';
import SiteContext from './SiteContext';
const config = mediacmsConfig(window.MediaCMS);
function initLogo(logo: GlobalMediaCMS['site']['logo']) {
let light = null;
let dark = null;
if (void 0 !== logo.darkMode) {
if (supportsSvgAsImg() && void 0 !== logo.darkMode.svg && '' !== logo.darkMode.svg) {
dark = logo.darkMode.svg;
} else if (void 0 !== logo.darkMode.img && '' !== logo.darkMode.img) {
dark = logo.darkMode.img;
}
}
if (void 0 !== logo.lightMode) {
if (supportsSvgAsImg() && void 0 !== logo.lightMode.svg && '' !== logo.lightMode.svg) {
light = logo.lightMode.svg;
} else if (void 0 !== logo.lightMode.img && '' !== logo.lightMode.img) {
light = logo.lightMode.img;
}
}
if (null !== light || null !== dark) {
if (null === light) {
light = dark;
} else if (null === dark) {
dark = light;
}
}
return {
light,
dark,
};
}
function initMode(cachedValue: string | undefined, defaultValue: GlobalMediaCMS['site']['theme']['mode']) {
return 'light' === cachedValue || 'dark' === cachedValue ? cachedValue : defaultValue;
}
export const ThemeContext = createContext({
logo: initLogo(config.theme.logo)[config.theme.mode],
currentThemeMode: config.theme.mode,
changeThemeMode: () => {},
themeModeSwitcher: config.theme.switch,
});
export const ThemeProvider = ({ children }: { children: ReactNode }) => {
const site = useContext(SiteContext);
const cache = BrowserCache('MediaCMS[' + site.id + '][theme]', 86400);
const [themeMode, setThemeMode] = useState(
initMode(cache instanceof Error ? undefined : cache.get('mode'), config.theme.mode)
);
const logos = initLogo(config.theme.logo);
const [logo, setLogo] = useState(logos[themeMode]);
const changeMode = () => {
setThemeMode('light' === themeMode ? 'dark' : 'light');
};
useEffect(() => {
if ('dark' === themeMode) {
addClassname(document.body, 'dark_theme');
} else {
removeClassname(document.body, 'dark_theme');
}
if (!(cache instanceof Error)) {
cache.set('mode', themeMode);
}
setLogo(logos[themeMode]);
}, [themeMode]);
const value = {
logo,
currentThemeMode: themeMode,
changeThemeMode: changeMode,
themeModeSwitcher: config.theme.switch,
};
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
};
export const ThemeConsumer = ThemeContext.Consumer;

View File

@@ -1,22 +0,0 @@
import React, { createContext } from 'react';
import { config as mediacmsConfig } from '../settings/config.js';
export const UserContext = createContext();
const member = mediacmsConfig(window.MediaCMS).member;
export const UserProvider = ({ children }) => {
const value = {
isAnonymous: member.is.anonymous,
username: member.username,
thumbnail: member.thumbnail,
userCan: member.can,
pages: member.pages,
};
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
};
export const UserConsumer = UserContext.Consumer;
export default UserContext;

View File

@@ -0,0 +1,28 @@
import React from 'react';
import { createContext, ReactNode } from 'react';
import { config as mediacmsConfig } from '../settings/config';
const member = mediacmsConfig(window.MediaCMS).member;
export const UserContext = createContext({
isAnonymous: member.is.anonymous,
username: member.username,
thumbnail: member.thumbnail,
userCan: member.can,
pages: member.pages,
});
export function UserProvider({ children }: { children: ReactNode }) {
const value = {
isAnonymous: member.is.anonymous,
username: member.username,
thumbnail: member.thumbnail,
userCan: member.can,
pages: member.pages,
};
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}
export const UserConsumer = UserContext.Consumer;
export default UserContext;

View File

@@ -1,19 +0,0 @@
import React from 'react';
import { useBulkActions } from '../hooks/useBulkActions';
/**
* Higher-Order Component that provides bulk actions functionality
* to class components via props
*/
export function withBulkActions(WrappedComponent) {
return function WithBulkActionsComponent(props) {
const bulkActions = useBulkActions();
return (
<WrappedComponent
{...props}
bulkActions={bulkActions}
/>
);
};
}

View File

@@ -0,0 +1,15 @@
import React from 'react';
import { useBulkActions } from '../hooks/useBulkActions';
/**
* Higher-Order Component that provides bulk actions functionality
* to class components via props
*/
export function withBulkActions<P extends { bulkActions: ReturnType<typeof useBulkActions> }>(
WrappedComponent: React.ComponentType<P>
) {
return function WithBulkActionsComponent(props: Omit<P, 'bulkActions'>) {
const bulkActions = useBulkActions();
return <WrappedComponent {...(props as P)} bulkActions={bulkActions} />;
};
}

View File

@@ -18,10 +18,6 @@ jest.mock('../../../src/static/js/utils/classes/', () => ({
})), })),
})); }));
jest.mock('../../../src/static/js/utils/dispatcher.js', () => ({
register: jest.fn(),
}));
jest.mock('../../../src/static/js/utils/settings/config', () => ({ jest.mock('../../../src/static/js/utils/settings/config', () => ({
config: jest.fn(() => jest.requireActual('../../tests-constants').sampleMediaCMSConfig), config: jest.fn(() => jest.requireActual('../../tests-constants').sampleMediaCMSConfig),
})); }));
@@ -54,7 +50,7 @@ describe('utils/hooks', () => {
}); });
}); });
test('Returns undefined value when used without a Provider', () => { test('Returns default context value when used without a Provider', () => {
let received: any = 'init'; let received: any = 'init';
const Comp: React.FC = () => { const Comp: React.FC = () => {
@@ -64,7 +60,14 @@ describe('utils/hooks', () => {
render(<Comp />); render(<Comp />);
expect(received).toBe(undefined); expect(received).toStrictEqual({
enabledSidebar: true,
visibleSidebar: true,
visibleMobileSearch: false,
setVisibleSidebar: expect.any(Function),
toggleMobileSearch: expect.any(Function),
toggleSidebar: expect.any(Function),
});
}); });
test('Toggle sidebar', () => { test('Toggle sidebar', () => {

View File

@@ -12,10 +12,6 @@ jest.mock('../../../src/static/js/utils/classes/', () => ({
})), })),
})); }));
jest.mock('../../../src/static/js/utils/dispatcher.js', () => ({
register: jest.fn(),
}));
function getRenderers(ThemeProvider: React.FC<{ children: React.ReactNode }>, useTheme: typeof useThemeHook) { function getRenderers(ThemeProvider: React.FC<{ children: React.ReactNode }>, useTheme: typeof useThemeHook) {
const data: { current: any } = { current: undefined }; const data: { current: any } = { current: undefined };