State management

This commit is contained in:
Sean Edmunds
2025-05-02 14:50:14 -05:00
parent 16787969fc
commit 97a59d6a15
21 changed files with 525 additions and 320 deletions

View File

@@ -19,7 +19,6 @@ import { Loading, MainLayout } from './components';
import '@mantine/core/styles.css';
import '@mantine/notifications/styles.css';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import './index.css';
import { Homepage, Settings } from 'pages';
import type { Theme, ThemePlaylist } from 'types';

View File

@@ -29,7 +29,8 @@ export default function WeatherGraphs({
{payload.map((item: any) => (
<Text
key={item.name}
size='sm'
size='md'
c={item.stroke}
>{`${item.name} - ${item.value}${item.unit}`}</Text>
))}
</Stack>

View File

@@ -41,7 +41,6 @@ export function useCommonHooks() {
key: 'theme',
defaultValue: 'blackboard',
});
const [currentThemePlaylist, setCurrentThemePlaylist] = useLocalStorage<string>({
key: 'themePlaylist',
defaultValue: 'All',

View File

@@ -1 +0,0 @@
@import url("https://fonts.googleapis.com/css2?family=Jost:ital,wght@0,100;0,200;0,400;1,100;1,200&display=swap");

View File

@@ -24,6 +24,7 @@ import * as vsc from 'react-icons/vsc';
import { popoverClasses, selectClasses, textInputClasses } from 'styles';
import type { Application } from 'types';
import AddItemModal from '../Shared/AddItemModal';
import NoItemsFound from '../Shared/NoItemsFound';
import SettingsHeader from '../Shared/SettingsHeader';
import AppSettingsItem from './AppSettingsItem';
import DebouncedApplication from './DebouncedApplication';
@@ -54,6 +55,7 @@ const ApplicationSettings = () => {
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const [dragging, setDragging] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
function onDragEnd(result: any) {
if (!result.destination) {
@@ -81,17 +83,22 @@ const ApplicationSettings = () => {
closeModal();
setNewApplication(defaultApplication);
};
useEffect(() => {
if (applicationData && !isAppsFetching) {
setApplications(
applicationData
.filter((a: Application) =>
a.name.toLowerCase().includes(debouncedSearch.toLowerCase()),
.filter(
(a: Application) =>
a.name.toLowerCase().includes(debouncedSearch.toLowerCase()) ||
a.url.toLowerCase().includes(debouncedSearch.toLowerCase()) ||
a.baseUrl.toLowerCase().includes(debouncedSearch.toLowerCase()),
)
.sort((a: Application, b: Application) => (a.index > b.index ? 1 : -1)),
);
}
if (debouncedSearch === searchValue) {
setIsLoading(false);
}
}, [applicationData, isAppsFetching, debouncedSearch]);
useEffect(() => {
@@ -103,7 +110,6 @@ const ApplicationSettings = () => {
});
}
}, [applicationList]);
return (
<Stack w={'100%'}>
<AddItemModal
@@ -253,11 +259,13 @@ const ApplicationSettings = () => {
title={'Applications'}
tooltip={{
title:
'Applications are the applicationData that will be displayed on the homepage.',
'Applications are the app buttons that will be displayed on the homepage.',
highlightList: [
'Applications',
'applicationData',
'application',
'app',
'apps',
'name',
'base url',
'url',
@@ -270,6 +278,8 @@ const ApplicationSettings = () => {
searchValue={searchValue}
setSearchValue={setSearchValue}
searchPlaceholder={'Search Applications...'}
isLoading={isLoading}
setIsLoading={setIsLoading}
/>
{dragging ? (
<DragDropContext onDragEnd={onDragEnd}>
@@ -361,39 +371,49 @@ const ApplicationSettings = () => {
align='center'
>
<Grid columns={18} w={'75%'}>
{applications?.map((application: Application) => (
<GridCol span={6} key={application.id}>
<Popover
withArrow
withOverlay
closeOnClickOutside={!isOpen}
classNames={popoverClasses}
>
<Popover.Target>
<Box
onClick={(e) => {
e.stopPropagation();
}}
>
<AppSettingsItem
url={application.url}
icon={application.icon}
name={application.name}
enabled={application.enabled}
{applications.length > 0 ? (
applications?.map((application: Application) => (
<GridCol span={6} key={application.id}>
<Popover
withArrow
withOverlay
closeOnClickOutside={!isOpen}
classNames={popoverClasses}
>
<Popover.Target>
<Box
onClick={(e) => {
e.stopPropagation();
}}
>
<AppSettingsItem
url={application.url}
icon={application.icon}
name={application.name}
enabled={application.enabled}
allIcons={allIcons}
/>
</Box>
</Popover.Target>
<Popover.Dropdown w={isMobile ? '100%' : undefined}>
<DebouncedApplication
application={application}
setIsOpen={setIsOpen}
allIcons={allIcons}
/>
</Box>
</Popover.Target>
<Popover.Dropdown w={isMobile ? '100%' : undefined}>
<DebouncedApplication
application={application}
setIsOpen={setIsOpen}
allIcons={allIcons}
/>
</Popover.Dropdown>
</Popover>
</Popover.Dropdown>
</Popover>
</GridCol>
))
) : (
<GridCol
w={'100%'}
span={18}
style={{ display: 'flex', justifyContent: 'center' }}
>
<NoItemsFound />
</GridCol>
))}
)}
</Grid>
</Stack>
)}

View File

@@ -6,6 +6,7 @@ import { useEffect, useState } from 'react';
import { textInputClasses } from 'styles';
import type { Bookmark, BookmarkGroup } from 'types';
import AddItemModal from '../Shared/AddItemModal';
import NoItemsFound from '../Shared/NoItemsFound';
import SettingsHeader from '../Shared/SettingsHeader';
import DebouncedGroupSettingsItem from './DebouncedGroupSettingsItem';
const BookmarkSettings = () => {
@@ -33,6 +34,7 @@ const BookmarkSettings = () => {
const [searchValue, setSearchValue] = useState('');
const [debouncedGroupSearch] = useDebouncedValue(searchValue, 500);
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const [isLoading, setIsLoading] = useState(false);
const handleClose = () => {
setNewGroup(defaultGroup);
@@ -94,6 +96,9 @@ const BookmarkSettings = () => {
);
setGroups(groupList);
}
if (debouncedGroupSearch === searchValue) {
setIsLoading(false);
}
}, [groupData, groupsIsFetched, debouncedGroupSearch]);
useEffect(() => {
@@ -194,6 +199,8 @@ const BookmarkSettings = () => {
searchValue={searchValue}
setSearchValue={setSearchValue}
searchPlaceholder={'Search Bookmark Groups...'}
isLoading={isLoading}
setIsLoading={setIsLoading}
/>
{dragging ? (
<DragDropContext onDragEnd={onDragEnd}>
@@ -285,16 +292,22 @@ const BookmarkSettings = () => {
align='center'
>
<Group w={isMobile ? undefined : '75%'} align='start' justify='center'>
{groups?.map((group: BookmarkGroup) => (
<DebouncedGroupSettingsItem
key={group.id}
bookmarkGroup={group}
bookmarks={bookmarks
.filter((a: Bookmark) => a.category === group.categoryName)
.sort((a: Bookmark, b: Bookmark) => (a.index > b.index ? 1 : -1))}
searchTerm={searchValue}
/>
))}
{groups.length > 0 ? (
groups?.map((group: BookmarkGroup) => (
<DebouncedGroupSettingsItem
key={group.id}
bookmarkGroup={group}
bookmarks={bookmarks
.filter((a: Bookmark) => a.category === group.categoryName)
.sort((a: Bookmark, b: Bookmark) => (a.index > b.index ? 1 : -1))}
searchTerm={searchValue}
/>
))
) : (
<Group w={'100%'} style={{ display: 'flex', justifyContent: 'center' }}>
<NoItemsFound />
</Group>
)}
</Group>
</Stack>
)}

View File

@@ -286,7 +286,7 @@ function DebouncedGroupSettingsItem({
>
<Popover.Target>
<Button
className={buttonClasses.tableButton}
className={buttonClasses.newItemButton}
classNames={buttonClasses}
w={'min-content'}
onClick={() => {

View File

@@ -5,6 +5,7 @@ import { useEffect, useState } from 'react';
import { textInputClasses } from 'styles';
import type { Message } from 'types';
import AddItemModal from '../Shared/AddItemModal';
import NoItemsFound from '../Shared/NoItemsFound';
import SettingsHeader from '../Shared/SettingsHeader';
import DebouncedMessage from './DebouncedMessage';
@@ -23,6 +24,7 @@ const MessageSettings = () => {
const [searchValue, setSearchValue] = useState('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const [isLoading, setIsLoading] = useState(false);
const handleClose = () => {
setNewMessage('');
@@ -38,6 +40,9 @@ const MessageSettings = () => {
),
);
}
if (debouncedSearch === searchValue) {
setIsLoading(false);
}
}, [messageData, isMessagesFetched, debouncedSearch]);
useEffect(() => {
@@ -67,6 +72,8 @@ const MessageSettings = () => {
searchValue={searchValue}
setSearchValue={setSearchValue}
searchPlaceholder={'Search Message content...'}
isLoading={isLoading}
setIsLoading={setIsLoading}
/>
<AddItemModal
modalOpened={modalOpened}
@@ -122,15 +129,25 @@ const MessageSettings = () => {
align='center'
>
<Grid w={'75%'}>
{messages?.map((message: Message) => (
<GridCol span={isMobile ? 12 : 6} key={message.id}>
<DebouncedMessage
label={`ID# ${message.id}`}
message={message}
setMessage={setMessage}
/>
{messages.length > 0 ? (
messages?.map((message: Message) => (
<GridCol span={isMobile ? 12 : 6} key={message.id}>
<DebouncedMessage
label={`ID# ${message.id}`}
message={message}
setMessage={setMessage}
/>
</GridCol>
))
) : (
<GridCol
w={'100%'}
span={12}
style={{ display: 'flex', justifyContent: 'center' }}
>
<NoItemsFound />
</GridCol>
))}
)}
</Grid>
</Stack>
</Stack>

View File

@@ -17,8 +17,9 @@ const PersonalizationSettings = () => {
const [personal, setPersonal] = useState<PersonalData>();
const [update, setUpdate] = useState<any>({});
const [debounced] = useDebouncedValue(update, 1000);
const { mutateAsync: editRow } = useMutationAPI();
const { mutateAsync } = useMutationAPI();
const { isMobile } = useCommonHooks();
useEffect(() => {
if (personalData && personalIsFetched) {
setPersonal(personalData[0]);
@@ -28,7 +29,7 @@ const PersonalizationSettings = () => {
useEffect(() => {
if (Object.keys(debounced).length) {
setPersonal(update);
editRow({ method: 'PATCH', endpoint: endpoint, body: debounced });
mutateAsync({ method: 'PATCH', endpoint: endpoint, body: debounced });
setUpdate({});
}
}, [debounced]);
@@ -61,6 +62,8 @@ const PersonalizationSettings = () => {
searchValue={undefined}
setSearchValue={() => {}}
searchPlaceholder={undefined}
isLoading={false}
setIsLoading={() => {}}
/>
<Stack
align='center'

View File

@@ -15,6 +15,7 @@ import { useEffect, useState } from 'react';
import { popoverClasses, textInputClasses } from 'styles';
import type { Provider } from 'types';
import AddItemModal from '../Shared/AddItemModal';
import NoItemsFound from '../Shared/NoItemsFound';
import SettingsHeader from '../Shared/SettingsHeader';
import DebouncedSearchProvider from './DebouncedSearchProvider';
import SearchProviderSettingsItem from './SearchProviderSettingsItem';
@@ -38,6 +39,7 @@ const SearchProviderSettings = () => {
const [searchValue, setSearchValue] = useState('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const handleClose = () => {
setNewProvider(defaultProvider);
@@ -48,12 +50,17 @@ const SearchProviderSettings = () => {
if (providerData && providerDataIsFetched) {
setProviders(
providerData
.filter((a: Provider) =>
a.name.toLowerCase().includes(debouncedSearch.toLowerCase()),
.filter(
(a: Provider) =>
a.name.toLowerCase().includes(debouncedSearch.toLowerCase()) ||
a.url.toLowerCase().includes(debouncedSearch.toLowerCase()),
)
.sort((a: Provider, b: Provider) => (a.prefix > b.prefix ? 1 : -1)),
);
}
if (debouncedSearch === searchValue) {
setIsLoading(false);
}
}, [providerData, providerDataIsFetched, debouncedSearch]);
return (
@@ -138,16 +145,18 @@ const SearchProviderSettings = () => {
<SettingsHeader
setDragging={undefined}
openModal={openModal}
title={'Search providerData'}
title={'Search Providers'}
tooltip={{
title: 'Search providerData and shortcuts can be configured here.',
title: 'Search providers and shortcuts can be configured here.',
highlightList: ['providerData', 'search', 'provider', 'prefix'],
description:
'providerData can be used in the search bar. By starting a search with the prefix, it will be redirected to the url of the provider to complete the search.',
'Providers can be used in the search bar. By starting a search with the prefix, it will be redirected to the url of the provider to complete the search.',
}}
searchValue={searchValue}
setSearchValue={setSearchValue}
searchPlaceholder={'Search Search providerData...'}
isLoading={isLoading}
setIsLoading={setIsLoading}
/>
<Stack
mt={isMobile ? '9rem' : '6rem'}
@@ -156,32 +165,42 @@ const SearchProviderSettings = () => {
align='center'
>
<Grid columns={18} w={'75%'}>
{providers?.map((provider: Provider) => (
<GridCol span={isMobile ? 18 : 6} key={provider.id}>
<Popover
withArrow
withOverlay
closeOnClickOutside={!isOpen}
classNames={popoverClasses}
>
<Popover.Target>
<Box
onClick={(e) => {
e.stopPropagation();
}}
>
<SearchProviderSettingsItem provider={provider} />
</Box>
</Popover.Target>
<Popover.Dropdown w={isMobile ? '100%' : undefined}>
<DebouncedSearchProvider
searchProvider={provider}
setIsOpen={setIsOpen}
/>
</Popover.Dropdown>
</Popover>
{providers.length > 0 ? (
providers?.map((provider: Provider) => (
<GridCol span={isMobile ? 18 : 6} key={provider.id}>
<Popover
withArrow
withOverlay
closeOnClickOutside={!isOpen}
classNames={popoverClasses}
>
<Popover.Target>
<Box
onClick={(e) => {
e.stopPropagation();
}}
>
<SearchProviderSettingsItem provider={provider} />
</Box>
</Popover.Target>
<Popover.Dropdown w={isMobile ? '100%' : undefined}>
<DebouncedSearchProvider
searchProvider={provider}
setIsOpen={setIsOpen}
/>
</Popover.Dropdown>
</Popover>
</GridCol>
))
) : (
<GridCol
w={'100%'}
span={18}
style={{ display: 'flex', justifyContent: 'center' }}
>
<NoItemsFound />
</GridCol>
))}
)}
</Grid>
</Stack>
</Stack>

View File

@@ -0,0 +1,22 @@
import { Card, Text } from '@mantine/core';
import { cardClasses } from 'styles';
function NoItemsFound() {
return (
<Card
className={cardClasses.settingsItem}
classNames={cardClasses}
w={'50%'}
h={'100%'}
p={'xl'}
m={'xl'}
style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}
>
<Text fw={300} size={'xl'}>
No items found
</Text>
</Card>
);
}
export default NoItemsFound;

View File

@@ -10,7 +10,7 @@ import {
TextInput,
} from '@mantine/core';
import { useCommonHooks } from 'hooks';
import { FiHelpCircle, FiPlus } from 'react-icons/fi';
import { FiHelpCircle, FiLoader, FiPlus } from 'react-icons/fi';
import {
actionIconClasses,
buttonClasses,
@@ -28,6 +28,8 @@ function SettingsHeader({
searchValue,
setSearchValue,
searchPlaceholder,
isLoading,
setIsLoading,
}: {
setDragging: undefined | ((dragging: boolean) => void);
openModal: () => void;
@@ -40,6 +42,8 @@ function SettingsHeader({
searchValue: string | undefined;
setSearchValue: (searchValue: string) => void;
searchPlaceholder: string | undefined;
isLoading: boolean;
setIsLoading: (isLoading: boolean) => void;
}) {
const { isMobile } = useCommonHooks();
return (
@@ -105,7 +109,7 @@ function SettingsHeader({
</Popover>
{searchValue !== undefined && (
<Button
className={buttonClasses.tableButton}
className={buttonClasses.newItemButton}
classNames={buttonClasses}
onClick={openModal}
>
@@ -123,11 +127,22 @@ function SettingsHeader({
placeholder={searchPlaceholder ?? 'Search...'}
value={searchValue}
onInput={(event: any) => {
setIsLoading(true);
setSearchValue(event.target.value);
}}
style={{
zIndex: 100,
}}
rightSection={
isLoading && (
<ActionIcon
className={actionIconClasses.loadingSpinner}
classNames={actionIconClasses}
>
<FiLoader />
</ActionIcon>
)
}
/>
)}
{setDragging && (

View File

@@ -1,10 +1,11 @@
import { Box, Group, Stack, Text } from '@mantine/core';
import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { FiChevronRight } from 'react-icons/fi';
import { textClasses } from 'styles';
import type { ThemePlaylist } from 'types';
import NoItemsFound from '../Shared/NoItemsFound';
import SettingsHeader from '../Shared/SettingsHeader';
import ThemePlaylistCard from './ThemePlaylistCard';
import ThemePlaylistModal from './ThemePlaylistModal';
@@ -15,9 +16,10 @@ function ThemePlaylists() {
endpoint: themesEndpoint,
});
const themePlaylistsEndpoint = 'api/themePlaylists';
const { data: themePlaylistData } = useQueryAPI({
const { data: themePlaylistData, isFetched: isThemePlaylistDataFetched } = useQueryAPI({
endpoint: themePlaylistsEndpoint,
});
const [themePlaylists, setThemePlaylists] = useState<ThemePlaylist[]>([]);
const defaultPlaylist = {
id: undefined,
default: 0,
@@ -31,6 +33,7 @@ function ThemePlaylists() {
const { currentThemePlaylist, theme, isMobile } = useCommonHooks();
const [searchValue, setSearchValue] = useState('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const [isLoading, setIsLoading] = useState(false);
const handleOpen = (playlist: ThemePlaylist) => {
setSelectedPlaylist(playlist);
@@ -51,6 +54,19 @@ function ThemePlaylists() {
close();
};
useEffect(() => {
if (themePlaylistData && isThemePlaylistDataFetched) {
setThemePlaylists(
themePlaylistData.filter((a: ThemePlaylist) =>
a.name.toLowerCase().includes(debouncedSearch.toLowerCase()),
),
);
}
if (debouncedSearch === searchValue) {
setIsLoading(false);
}
}, [themePlaylistData, isThemePlaylistDataFetched, debouncedSearch]);
return (
<Stack>
<ThemePlaylistModal
@@ -77,6 +93,8 @@ function ThemePlaylists() {
searchValue={searchValue}
setSearchValue={setSearchValue}
searchPlaceholder={'Search Theme Playlists...'}
isLoading={isLoading}
setIsLoading={setIsLoading}
/>
<Group mt={'6rem'} justify='center' align='center' pos={'relative'}>
<Text p={0} className={textClasses.settingsSubTitle} classNames={textClasses}>
@@ -84,41 +102,44 @@ function ThemePlaylists() {
</Text>
</Group>
<Group justify='center' align='center' pos={'relative'}>
{themePlaylistData
?.filter((playlist: ThemePlaylist) => playlist.default === 1)
?.filter((a: ThemePlaylist) =>
a.name.toLowerCase().includes(debouncedSearch.toLowerCase()),
)
.map((cardPlaylist: ThemePlaylist) => {
return (
<Group key={cardPlaylist.name} pos={'relative'}>
{cardPlaylist.name === currentThemePlaylist && (
<FiChevronRight
style={{
position: 'absolute',
left: -14,
zIndex: 10,
fontSize: '2em',
fontWeight: 'bold',
borderRadius: '50%',
backgroundColor: 'var(--mantine-color-border-0)',
color:
theme.borderColor === theme.primaryColor
? 'var(--mantine-color-accent-0)'
: 'var(--mantine-color-background-0)',
}}
/>
)}
<Box>
<ThemePlaylistCard
cardPlaylist={cardPlaylist}
handleOpen={handleOpen}
allThemes={allThemeData}
/>
</Box>
</Group>
);
})}
{themePlaylists.length > 0 ? (
themePlaylists
.filter((playlist: ThemePlaylist) => playlist.default === 1)
.map((cardPlaylist: ThemePlaylist) => {
return (
<Group key={cardPlaylist.name} pos={'relative'}>
{cardPlaylist.name === currentThemePlaylist && (
<FiChevronRight
style={{
position: 'absolute',
left: -14,
zIndex: 10,
fontSize: '2em',
fontWeight: 'bold',
borderRadius: '50%',
backgroundColor: 'var(--mantine-color-border-0)',
color:
theme.borderColor === theme.primaryColor
? 'var(--mantine-color-accent-0)'
: 'var(--mantine-color-background-0)',
}}
/>
)}
<Box>
<ThemePlaylistCard
cardPlaylist={cardPlaylist}
handleOpen={handleOpen}
allThemes={allThemeData}
/>
</Box>
</Group>
);
})
) : (
<Group w={'100%'} style={{ display: 'flex', justifyContent: 'center' }}>
<NoItemsFound />
</Group>
)}
</Group>
<Group justify='center' align='center' pos={'relative'}>
<Text p={0} className={textClasses.settingsSubTitle} classNames={textClasses}>
@@ -131,42 +152,45 @@ function ThemePlaylists() {
pos={'relative'}
mb={isMobile ? '6rem' : undefined}
>
{themePlaylistData
?.filter((playlist: ThemePlaylist) => playlist.default === 0)
?.filter((a: ThemePlaylist) =>
a.name.toLowerCase().includes(debouncedSearch.toLowerCase()),
)
.sort((a: ThemePlaylist, b: ThemePlaylist) => (a.name > b.name ? 1 : -1))
.map((cardPlaylist: ThemePlaylist) => {
return (
<Group key={cardPlaylist.name} pos={'relative'}>
{cardPlaylist.name === currentThemePlaylist && (
<FiChevronRight
style={{
position: 'absolute',
left: -14,
zIndex: 10,
fontSize: '2em',
fontWeight: 'bold',
borderRadius: '50%',
backgroundColor: 'var(--mantine-color-border-0)',
color:
theme.borderColor === theme.primaryColor
? 'var(--mantine-color-accent-0)'
: 'var(--mantine-color-primary-0)',
}}
/>
)}
<Box>
<ThemePlaylistCard
cardPlaylist={cardPlaylist}
handleOpen={handleOpen}
allThemes={allThemeData}
/>
</Box>
</Group>
);
})}
{themePlaylists.length > 0 ? (
themePlaylists
.filter((playlist: ThemePlaylist) => playlist.default === 0)
.sort((a: ThemePlaylist, b: ThemePlaylist) => (a.name > b.name ? 1 : -1))
.map((cardPlaylist: ThemePlaylist) => {
return (
<Group key={cardPlaylist.name} pos={'relative'}>
{cardPlaylist.name === currentThemePlaylist && (
<FiChevronRight
style={{
position: 'absolute',
left: -14,
zIndex: 10,
fontSize: '2em',
fontWeight: 'bold',
borderRadius: '50%',
backgroundColor: 'var(--mantine-color-border-0)',
color:
theme.borderColor === theme.primaryColor
? 'var(--mantine-color-accent-0)'
: 'var(--mantine-color-primary-0)',
}}
/>
)}
<Box>
<ThemePlaylistCard
cardPlaylist={cardPlaylist}
handleOpen={handleOpen}
allThemes={allThemeData}
/>
</Box>
</Group>
);
})
) : (
<Group w={'100%'} style={{ display: 'flex', justifyContent: 'center' }}>
<NoItemsFound />
</Group>
)}
</Group>
</Stack>
</Stack>

View File

@@ -6,6 +6,7 @@ import { FaDice } from 'react-icons/fa';
import { FiChevronRight } from 'react-icons/fi';
import { switchClasses } from 'styles';
import type { Theme, ThemePlaylist } from 'types';
import NoItemsFound from '../Shared/NoItemsFound';
import SettingsHeader from '../Shared/SettingsHeader';
import ThemeCard from './ThemeCard';
import ThemeModal from './ThemeModal';
@@ -13,9 +14,10 @@ import ThemeModal from './ThemeModal';
function ThemesSettings() {
const themesEndpoint = 'api/themes';
const backgroundsEndpoint = 'api/backgrounds';
const { data: allThemeData } = useQueryAPI({
const { data: allThemeData, isFetched: isAllThemeDataFetched } = useQueryAPI({
endpoint: 'api/themes',
});
const [allThemes, setAllThemes] = useState<Theme[]>([]);
const { data: backgroundData, isFetched: isBackgroundFetched } = useQueryAPI({
endpoint: backgroundsEndpoint,
});
@@ -42,6 +44,7 @@ function ThemesSettings() {
const [pickerOpen, { open: openPicker, close: closePicker }] = useDisclosure(false);
const [searchValue, setSearchValue] = useState('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const [isLoading, setIsLoading] = useState(false);
const { currentTheme, setCurrentTheme, theme, isMobile, setCurrentThemePlaylist } =
useCommonHooks();
@@ -87,6 +90,19 @@ function ThemesSettings() {
}
}, [themePlaylistData]);
useEffect(() => {
if (allThemeData && isAllThemeDataFetched) {
setAllThemes(
allThemeData.filter((a: Theme) =>
a.name.toLowerCase().includes(debouncedSearch.toLowerCase()),
),
);
}
if (debouncedSearch === searchValue) {
setIsLoading(false);
}
}, [allThemeData, isAllThemeDataFetched, debouncedSearch]);
return (
<Stack w={'100%'} align='center'>
<ThemeModal
@@ -117,6 +133,8 @@ function ThemesSettings() {
searchValue={searchValue}
setSearchValue={setSearchValue}
searchPlaceholder={'Search Themes...'}
isLoading={isLoading}
setIsLoading={setIsLoading}
/>
<Switch
label={
@@ -150,7 +168,7 @@ function ThemesSettings() {
mb={isMobile ? '6rem' : '1rem'}
>
<Overlay
display={currentTheme === 'random' ? 'flex' : 'none'}
display={currentTheme === 'random' && debouncedSearch === '' ? 'flex' : 'none'}
blur={5}
style={{
zIndex: 190,
@@ -161,11 +179,8 @@ function ThemesSettings() {
}}
pos={'absolute'}
/>
{allThemeData
?.filter((a: Theme) =>
a.name.toLowerCase().includes(debouncedSearch.toLowerCase()),
)
.map((cardTheme: Theme) => (
{allThemes.length > 0 ? (
allThemes.map((cardTheme: Theme) => (
<Group key={cardTheme.name} pos={'relative'}>
{cardTheme.name === currentTheme && (
<FiChevronRight
@@ -198,7 +213,15 @@ function ThemesSettings() {
/>
</Box>
</Group>
))}
))
) : (
<Group
w={'100%'}
style={{ zIndex: 1000, display: 'flex', justifyContent: 'center' }}
>
<NoItemsFound />
</Group>
)}
</Group>
</Stack>
);

View File

@@ -14,6 +14,7 @@ import { useEffect, useState } from 'react';
import { popoverClasses, textInputClasses } from 'styles';
import type { WolClient } from 'types';
import AddItemModal from '../Shared/AddItemModal';
import NoItemsFound from '../Shared/NoItemsFound';
import SettingsHeader from '../Shared/SettingsHeader';
import DebouncedWolClient from './DebouncedWolClient';
import WolClientSettingsItem from './WolClientSettingsItem';
@@ -36,6 +37,7 @@ const WolClientSettings = () => {
const [searchValue, setSearchValue] = useState('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const handleClose = () => {
setNewWolClient(defaultWolClient);
@@ -50,6 +52,9 @@ const WolClientSettings = () => {
),
);
}
if (debouncedSearch === searchValue) {
setIsLoading(false);
}
}, [wolClientsData, wolClientsIsFetched, debouncedSearch]);
return (
@@ -131,6 +136,8 @@ const WolClientSettings = () => {
searchValue={searchValue}
setSearchValue={setSearchValue}
searchPlaceholder={'Search Clients...'}
isLoading={isLoading}
setIsLoading={setIsLoading}
/>
<Stack
mt={isMobile ? '9rem' : '6rem'}
@@ -139,30 +146,40 @@ const WolClientSettings = () => {
align='center'
>
<Grid columns={18} w={'75%'}>
{wolClients?.map((client: WolClient) => (
<GridCol w={'100%'} span={isMobile ? 18 : 6} key={client.id}>
<Popover
withArrow
withOverlay
closeOnClickOutside={!isOpen}
classNames={popoverClasses}
>
<Popover.Target>
<Group
onClick={(e) => {
e.stopPropagation();
}}
justify='center'
>
<WolClientSettingsItem client={client} />
</Group>
</Popover.Target>
<Popover.Dropdown w={isMobile ? '100%' : undefined}>
<DebouncedWolClient wolClient={client} setIsOpen={setIsOpen} />
</Popover.Dropdown>
</Popover>
{wolClients.length > 0 ? (
wolClients?.map((client: WolClient) => (
<GridCol w={'100%'} span={isMobile ? 18 : 6} key={client.id}>
<Popover
withArrow
withOverlay
closeOnClickOutside={!isOpen}
classNames={popoverClasses}
>
<Popover.Target>
<Group
onClick={(e) => {
e.stopPropagation();
}}
justify='center'
>
<WolClientSettingsItem client={client} />
</Group>
</Popover.Target>
<Popover.Dropdown w={isMobile ? '100%' : undefined}>
<DebouncedWolClient wolClient={client} setIsOpen={setIsOpen} />
</Popover.Dropdown>
</Popover>
</GridCol>
))
) : (
<GridCol
w={'100%'}
span={18}
style={{ display: 'flex', justifyContent: 'center' }}
>
<NoItemsFound />
</GridCol>
))}
)}
</Grid>
</Stack>
</Stack>

View File

@@ -93,3 +93,36 @@
}
}
}
.loadingSpinner {
&.root {
transition: all .1s ease;
width: 30px;
height: 30px;
border-radius: 100%;
border: none;
background-color: transparent;
color: var(--mantine-color-primary-0);
.icon {
font-size: 24px;
}
.loader {
display: none;
}
:hover {
background-color: var(--mantine-color-primary-0) !important;
color: var(--mantine-color-accent-0) !important;
transition: all .1s ease;
}
animation: rotation 1s linear infinite;
}
}
@keyframes rotation {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@@ -42,7 +42,7 @@
}
}
.tableButton {
.newItemButton {
&.root {
&.loader {
display: none;