Standardization

This commit is contained in:
Sean Edmunds
2025-11-11 16:04:34 -06:00
parent 0397c0e1b0
commit 8794422a47
24 changed files with 365 additions and 209 deletions

8
package-lock.json generated
View File

@@ -29,7 +29,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.10.0", "@types/node": "^24.10.0",
"@types/react": "^19.2.2", "@types/react": "^19.2.3",
"@types/react-dom": "^19.2.2", "@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^5.1.0", "@vitejs/plugin-react": "^5.1.0",
"@vitest/coverage-v8": "^4.0.8", "@vitest/coverage-v8": "^4.0.8",
@@ -1868,9 +1868,9 @@
} }
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "19.2.2", "version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.3.tgz",
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "integrity": "sha512-k5dJVszUiNr1DSe8Cs+knKR6IrqhqdhpUwzqhkS8ecQTSf3THNtbfIp/umqHMpX2bv+9dkx3fwDv/86LcSfvSg==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,

View File

@@ -31,7 +31,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.10.0", "@types/node": "^24.10.0",
"@types/react": "^19.2.2", "@types/react": "^19.2.3",
"@types/react-dom": "^19.2.2", "@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^5.1.0", "@vitejs/plugin-react": "^5.1.0",
"@vitest/coverage-v8": "^4.0.8", "@vitest/coverage-v8": "^4.0.8",

View File

@@ -29,7 +29,7 @@ export function AppItem({
const { ref, width, height } = useElementSize(); const { ref, width, height } = useElementSize();
const allIcons: any = { ...fa6, ...vsc, ...si }; const allIcons: any = { ...fa6, ...vsc, ...si };
const Icon = allIcons[icon]; const Icon = allIcons[icon];
const { isMobile, filter } = useCommonHooks(); const { isMobile, iconFilter } = useCommonHooks();
const [textDecoration, setTextDecoration] = useState<string>('none'); const [textDecoration, setTextDecoration] = useState<string>('none');
const [hover, setHover] = useState<boolean>(false); const [hover, setHover] = useState<boolean>(false);
const validUrl = const validUrl =
@@ -102,7 +102,7 @@ export function AppItem({
style={{ style={{
height: scale * 32, height: scale * 32,
width: scale * 32, width: scale * 32,
filter: filter(selectedTheme), filter: iconFilter(selectedTheme),
}} }}
/> />
)} )}

View File

@@ -22,6 +22,7 @@ export function DataTable({
filtersEnabled, filtersEnabled,
globalFilter, globalFilter,
setGlobalFilter, setGlobalFilter,
isRefreshing,
}: { }: {
data: any[]; data: any[];
columns: any[]; columns: any[];
@@ -30,6 +31,7 @@ export function DataTable({
filtersEnabled: boolean; filtersEnabled: boolean;
globalFilter: string; globalFilter: string;
setGlobalFilter: Dispatch<SetStateAction<string>>; setGlobalFilter: Dispatch<SetStateAction<string>>;
isRefreshing?: boolean;
}) { }) {
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]); const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const table = useReactTable({ const table = useReactTable({
@@ -192,7 +194,11 @@ export function DataTable({
</tr> </tr>
))} ))}
</tbody> </tbody>
<tfoot> <tfoot
style={{
transition: 'all 1s ease',
}}
>
{table.getFooterGroups().map((footerGroup) => ( {table.getFooterGroups().map((footerGroup) => (
<tr key={footerGroup.id}> <tr key={footerGroup.id}>
{footerGroup.headers.map((header) => ( {footerGroup.headers.map((header) => (
@@ -223,6 +229,29 @@ export function DataTable({
</Text> </Text>
</td> </td>
</tr> </tr>
<tr
style={{
width: '100%',
backgroundColor: 'var(--mantine-color-accent-0)',
position: 'absolute',
bottom: isRefreshing ? 0 : -30,
transition: 'all 1.5s ease-in-out',
placeContent: 'center',
display: 'flex',
}}
>
<td colSpan={5} width={'100%'} style={{}}>
<Text
style={{
color: 'var(--mantine-color-background-0)',
fontWeight: 'bold',
textAlign: 'center',
}}
>
Updating
</Text>
</td>
</tr>
</tfoot> </tfoot>
</table> </table>
</ScrollArea> </ScrollArea>

View File

@@ -7,7 +7,7 @@ import { useMutationAPI } from './useAPI';
export function useCommonHooks() { export function useCommonHooks() {
const { mutateAsync } = useMutationAPI(); const { mutateAsync } = useMutationAPI();
const isMobile = window.matchMedia('(max-width: 600px)').matches; const isMobile = globalThis.matchMedia('(max-width: 600px)').matches;
const [theme, setTheme] = useAtom<Theme>(appTheme); const [theme, setTheme] = useAtom<Theme>(appTheme);
const [themeName, setThemeName] = useLocalStorage<string>({ const [themeName, setThemeName] = useLocalStorage<string>({
key: 'theme', key: 'theme',
@@ -109,12 +109,12 @@ export function useCommonHooks() {
}); });
const urlPattern = new RegExp( const urlPattern = new RegExp(
'^(https?:\\/\\/)?' + // protocol String.raw`^(https?:\/\/)?` +
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name String.raw`((([a-z\d]([a-z\d-]*[a-z\d])*)\.)+[a-z]{2,}|` +
'((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address String.raw`((\d{1,3}\.){3}\d{1,3}))` +
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path String.raw`(\:\d+)?(\/[-a-z\d%_.~+]*)*` +
'(\\?[;&a-z\\d%_.~+=-]*)?' + // query string String.raw`(\?[;&a-z\d%_.~+=-]*)?` +
'(\\#[-a-z\\d_]*)?$', String.raw`(\#[-a-z\d_]*)?$`,
'i', 'i',
); );
@@ -152,8 +152,8 @@ export function useCommonHooks() {
mutateAsync({ method: 'DELETE', endpoint: endpoint, body: item }); mutateAsync({ method: 'DELETE', endpoint: endpoint, body: item });
}; };
const filter = (selectedTheme?: Theme) => { const iconFilter = (selectedTheme?: Theme) => {
if (selectedTheme && selectedTheme.appShadowsEnabled === 1) { if (selectedTheme?.appShadowsEnabled === 1) {
return 'drop-shadow(2px 2px color-mix(in srgb, var(--mantine-color-accent-0), transparent 30%))'; return 'drop-shadow(2px 2px color-mix(in srgb, var(--mantine-color-accent-0), transparent 30%))';
} }
if (!selectedTheme && theme.appShadowsEnabled === 1) { if (!selectedTheme && theme.appShadowsEnabled === 1) {
@@ -163,14 +163,14 @@ export function useCommonHooks() {
return 'none'; return 'none';
}; };
function fixDigits(k: number) { const fixDigits = (k: number) => {
if (k < 10) { if (k < 10) {
return `0${k}`; return `0${k}`;
} }
return k; return k;
} };
function getObjectDiff(original: any, current: any) { const getObjectDiff = (original: any, current: any) => {
const changes: any = {}; const changes: any = {};
// Check current object's properties // Check current object's properties
@@ -210,14 +210,14 @@ export function useCommonHooks() {
} }
return Object.keys(changes).length === 0 ? null : changes; return Object.keys(changes).length === 0 ? null : changes;
} };
return { return {
isMobile, isMobile,
theme, theme,
setTheme, setTheme,
urlPattern, urlPattern,
filter, iconFilter,
fixDigits, fixDigits,
reorder, reorder,
getObjectDiff, getObjectDiff,

View File

@@ -61,7 +61,7 @@ function SettingsCard({
{sectionTitle} {sectionTitle}
</Text> </Text>
<Group w={'100%'} justify='center'> <Group w={'100%'} justify='center'>
<Stack w={scale !== undefined ? '60%' : '90%'} align='center'> <Stack w={scale === undefined ? '90%' : '60%'} align='center'>
<Text fz={'.875rem'} mt={'1rem'}> <Text fz={'.875rem'} mt={'1rem'}>
Density Density
</Text> </Text>

View File

@@ -58,11 +58,11 @@ function ThemingCard() {
}); });
useEffect(() => { useEffect(() => {
if (theme.name !== current) { if (theme.name === current) {
start();
} else {
setCurrent(theme.name); setCurrent(theme.name);
stop(); stop();
} else {
start();
} }
}, [theme]); }, [theme]);
@@ -215,9 +215,7 @@ function ThemingCard() {
} }
onChange={(_value, option: ComboboxItem) => { onChange={(_value, option: ComboboxItem) => {
setCurrentThemePlaylist(option.value); setCurrentThemePlaylist(option.value);
if (option.value !== 'Time of Day') { if (option.value === 'Time of Day') {
setThemeName('random');
} else {
const themeList = JSON.parse( const themeList = JSON.parse(
themePlaylistData.find((t: ThemePlaylist) => t.name === 'Time of Day') themePlaylistData.find((t: ThemePlaylist) => t.name === 'Time of Day')
.themeList, .themeList,
@@ -228,6 +226,8 @@ function ThemingCard() {
t.id === Number(themeList[Math.floor(new Date().getHours() / 6)]), t.id === Number(themeList[Math.floor(new Date().getHours() / 6)]),
).name, ).name,
); );
} else {
setThemeName('random');
} }
}} }}
/> />

View File

@@ -7,6 +7,7 @@ import {
Grid, Grid,
GridCol, GridCol,
Group, Group,
LoadingOverlay,
Popover, Popover,
Select, Select,
Stack, Stack,
@@ -48,7 +49,11 @@ const ApplicationSettings = () => {
}); });
const appsEndpoint = 'api/applications'; const appsEndpoint = 'api/applications';
const appsOrderEndpoint = 'api/applicationOrder'; const appsOrderEndpoint = 'api/applicationOrder';
const { data: applicationData, isFetching: isAppsFetching } = useQueryAPI({ const {
data: applicationData,
isFetched: applicationDataIsFetched,
isFetching: applicationDataIsFetching,
} = useQueryAPI({
endpoint: appsEndpoint, endpoint: appsEndpoint,
}); });
const [applications, setApplications] = useState<Application[]>([]); const [applications, setApplications] = useState<Application[]>([]);
@@ -88,7 +93,7 @@ const ApplicationSettings = () => {
form.reset(); form.reset();
}; };
useEffect(() => { useEffect(() => {
if (applicationData && !isAppsFetching) { if (applicationData && applicationDataIsFetched) {
setApplications( setApplications(
applicationData applicationData
.filter( .filter(
@@ -103,7 +108,7 @@ const ApplicationSettings = () => {
if (debouncedSearch === searchValue) { if (debouncedSearch === searchValue) {
setIsLoading(false); setIsLoading(false);
} }
}, [applicationData, isAppsFetching, debouncedSearch]); }, [applicationData, applicationDataIsFetched, debouncedSearch]);
useEffect(() => { useEffect(() => {
if (applicationList.length > 0 && applicationList !== applications) { if (applicationList.length > 0 && applicationList !== applications) {
@@ -305,6 +310,16 @@ const ApplicationSettings = () => {
mb={isMobile ? '6rem' : '1rem'} mb={isMobile ? '6rem' : '1rem'}
align='center' align='center'
> >
<LoadingOverlay
visible={applicationDataIsFetching}
zIndex={180}
overlayProps={{ blur: 5 }}
loaderProps={{
color: 'var(--mantine-color-primary-0)',
type: 'bars',
size: 'xl',
}}
/>
{applications.length > 0 ? ( {applications.length > 0 ? (
<Droppable droppableId={'groupList'}> <Droppable droppableId={'groupList'}>
{(provided) => ( {(provided) => (

View File

@@ -1,5 +1,14 @@
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'; import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
import { Box, Button, Card, Group, Stack, Text, TextInput } from '@mantine/core'; import {
Box,
Button,
Card,
Group,
LoadingOverlay,
Stack,
Text,
TextInput,
} from '@mantine/core';
import { isNotEmpty, useForm } from '@mantine/form'; import { isNotEmpty, useForm } from '@mantine/form';
import { useDebouncedValue, useDisclosure } from '@mantine/hooks'; import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
import { AddItemModal, NoItemsFound, SettingsHeader } from 'components'; import { AddItemModal, NoItemsFound, SettingsHeader } from 'components';
@@ -24,7 +33,11 @@ const BookmarkSettings = () => {
}, },
}); });
const bookmarksEndpoint = 'api/bookmarks'; const bookmarksEndpoint = 'api/bookmarks';
const { data: bookmarkData, isFetched: bookmarksIsFetched } = useQueryAPI({ const {
data: bookmarkData,
isFetched: bookmarkDataIsFetched,
isFetching: bookmarkDataIsFetching,
} = useQueryAPI({
endpoint: bookmarksEndpoint, endpoint: bookmarksEndpoint,
}); });
const [bookmarks, setBookmarks] = useState<Bookmark[]>([]); const [bookmarks, setBookmarks] = useState<Bookmark[]>([]);
@@ -68,10 +81,10 @@ const BookmarkSettings = () => {
} }
useEffect(() => { useEffect(() => {
if (bookmarkData && bookmarksIsFetched) { if (bookmarkData && bookmarkDataIsFetched) {
setBookmarks(bookmarkData); setBookmarks(bookmarkData);
} }
}, [bookmarksIsFetched, bookmarkData]); }, [bookmarkDataIsFetched, bookmarkData]);
useEffect(() => { useEffect(() => {
if (groupData && groupsIsFetched) { if (groupData && groupsIsFetched) {
@@ -83,12 +96,11 @@ const BookmarkSettings = () => {
a.name.toLowerCase().includes(debouncedGroupSearch.toLowerCase()) || a.name.toLowerCase().includes(debouncedGroupSearch.toLowerCase()) ||
a.url.toLowerCase().includes(debouncedGroupSearch.toLowerCase()), a.url.toLowerCase().includes(debouncedGroupSearch.toLowerCase()),
) )
.map( .map((a: Bookmark) =>
(a: Bookmark) => groupData.find(
groupData.filter( (b: BookmarkGroup) =>
(b: BookmarkGroup) => b.categoryName.toLowerCase() === a.category.toLowerCase(),
b.categoryName.toLowerCase() === a.category.toLowerCase(), ),
)[0],
), ),
), ),
]; ];
@@ -300,6 +312,16 @@ const BookmarkSettings = () => {
mb={isMobile ? '6rem' : '1rem'} mb={isMobile ? '6rem' : '1rem'}
align='center' align='center'
> >
<LoadingOverlay
visible={bookmarkDataIsFetching}
zIndex={180}
overlayProps={{ blur: 5 }}
loaderProps={{
color: 'var(--mantine-color-primary-0)',
type: 'bars',
size: 'xl',
}}
/>
<Group w={isMobile ? undefined : '80%'} align='start' justify='center'> <Group w={isMobile ? undefined : '80%'} align='start' justify='center'>
{groups.length > 0 ? ( {groups.length > 0 ? (
groups?.map((group: BookmarkGroup) => ( groups?.map((group: BookmarkGroup) => (

View File

@@ -1,4 +1,12 @@
import { Button, Grid, GridCol, Group, Stack, TextInput } from '@mantine/core'; import {
Button,
Grid,
GridCol,
Group,
LoadingOverlay,
Stack,
TextInput,
} from '@mantine/core';
import { isNotEmpty, useForm } from '@mantine/form'; import { isNotEmpty, useForm } from '@mantine/form';
import { useDebouncedValue, useDisclosure } from '@mantine/hooks'; import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
import { AddItemModal, NoItemsFound, SettingsHeader } from 'components'; import { AddItemModal, NoItemsFound, SettingsHeader } from 'components';
@@ -10,7 +18,11 @@ import DebouncedMessage from './DebouncedMessage';
const MessageSettings = () => { const MessageSettings = () => {
const endpoint = 'api/messages'; const endpoint = 'api/messages';
const { data: messageData, isFetched: isMessagesFetched } = useQueryAPI({ const {
data: messageData,
isFetched: messageDataIsFetched,
isFetching: messageDataIsFetching,
} = useQueryAPI({
endpoint: endpoint, endpoint: endpoint,
}); });
const form = useForm({ const form = useForm({
@@ -37,7 +49,7 @@ const MessageSettings = () => {
}; };
useEffect(() => { useEffect(() => {
if (messageData && isMessagesFetched) { if (messageData && messageDataIsFetched) {
setMessages( setMessages(
messageData.filter((m: Message) => messageData.filter((m: Message) =>
m.message.toLowerCase().includes(debouncedSearch.toLowerCase()), m.message.toLowerCase().includes(debouncedSearch.toLowerCase()),
@@ -47,7 +59,7 @@ const MessageSettings = () => {
if (debouncedSearch === searchValue) { if (debouncedSearch === searchValue) {
setIsLoading(false); setIsLoading(false);
} }
}, [messageData, isMessagesFetched, debouncedSearch]); }, [messageData, messageDataIsFetched, debouncedSearch]);
useEffect(() => { useEffect(() => {
if (Object.keys(debounced).length) { if (Object.keys(debounced).length) {
@@ -133,7 +145,18 @@ const MessageSettings = () => {
mt={isMobile ? '10rem' : '6rem'} mt={isMobile ? '10rem' : '6rem'}
mb={isMobile ? '6rem' : undefined} mb={isMobile ? '6rem' : undefined}
align='center' align='center'
pos={'relative'}
> >
<LoadingOverlay
visible={messageDataIsFetching}
zIndex={180}
overlayProps={{ blur: 5 }}
loaderProps={{
color: 'var(--mantine-color-primary-0)',
type: 'bars',
size: 'xl',
}}
/>
<Grid w={'75%'}> <Grid w={'75%'}>
{messages.length > 0 ? ( {messages.length > 0 ? (
messages?.map((message: Message) => ( messages?.map((message: Message) => (

View File

@@ -1,4 +1,4 @@
import { Card, Grid, GridCol, Overlay, Stack, TextInput } from '@mantine/core'; import { Card, Grid, GridCol, LoadingOverlay, Stack, TextInput } from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks'; import { useDebouncedValue } from '@mantine/hooks';
import { SettingsHeader } from 'components'; import { SettingsHeader } from 'components';
import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks'; import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks';
@@ -10,8 +10,8 @@ const PersonalizationSettings = () => {
const endpoint = 'api/personalData'; const endpoint = 'api/personalData';
const { const {
data: personalData, data: personalData,
isFetched: personalIsFetched, isFetched: personalDataIsFetched,
isFetching: personalIsFetching, isFetching: personalDataIsFetching,
} = useQueryAPI({ } = useQueryAPI({
endpoint: endpoint, endpoint: endpoint,
}); });
@@ -22,10 +22,10 @@ const PersonalizationSettings = () => {
const { isMobile, theme } = useCommonHooks(); const { isMobile, theme } = useCommonHooks();
useEffect(() => { useEffect(() => {
if (personalData && personalIsFetched) { if (personalData && personalDataIsFetched) {
setPersonal(personalData[0]); setPersonal(personalData[0]);
} }
}, [personalData, personalIsFetched]); }, [personalData, personalDataIsFetched]);
useEffect(() => { useEffect(() => {
if (Object.keys(debounced).length) { if (Object.keys(debounced).length) {
@@ -66,17 +66,18 @@ const PersonalizationSettings = () => {
isLoading={false} isLoading={false}
setIsLoading={() => {}} setIsLoading={() => {}}
/> />
<Stack align='center' mt={'6rem'}> <Stack align='center' mt={'6rem'} pos={'relative'}>
<LoadingOverlay
visible={personalDataIsFetching}
zIndex={180}
overlayProps={{ blur: 5 }}
loaderProps={{
color: 'var(--mantine-color-primary-0)',
type: 'bars',
size: 'xl',
}}
/>
<Grid w={'75%'}> <Grid w={'75%'}>
{personalIsFetching && (
<Overlay
style={{
borderRadius: '10px',
margin: '-.5rem',
backdropFilter: 'blur(5px) brightness(1.2)',
}}
/>
)}
<GridCol span={isMobile ? 12 : 4}> <GridCol span={isMobile ? 12 : 4}>
<Card className={cardClasses.settingsItem} classNames={cardClasses} pt={0}> <Card className={cardClasses.settingsItem} classNames={cardClasses} pt={0}>
<TextInput <TextInput

View File

@@ -9,7 +9,11 @@ import type { SearchHistoryItem } from 'types';
const SearchHistory = () => { const SearchHistory = () => {
const searchEndpoint = 'api/searchHistory'; const searchEndpoint = 'api/searchHistory';
const { data: historyData, isFetched: historyDataIsFetched } = useQueryAPI({ const {
data: historyData,
isFetched: historyDataIsFetched,
isFetching: historyDataIsFetching,
} = useQueryAPI({
endpoint: searchEndpoint, endpoint: searchEndpoint,
}); });
const [searchHistory, setSearchHistory] = useState<SearchHistoryItem[]>([]); const [searchHistory, setSearchHistory] = useState<SearchHistoryItem[]>([]);
@@ -92,7 +96,7 @@ const SearchHistory = () => {
align='center' align='center'
> >
<Stack h={'calc(100vh - 14rem)'} w={'100%'} p={'1rem'} pt={0} mt={'1rem'} gap={0}> <Stack h={'calc(100vh - 14rem)'} w={'100%'} p={'1rem'} pt={0} mt={'1rem'} gap={0}>
<Stack style={{ overflow: 'scroll', whiteSpace: 'none' }}> <Stack h={'inherit'} style={{ overflow: 'scroll', whiteSpace: 'none' }}>
<DataTable <DataTable
data={searchHistory} data={searchHistory}
columns={SearchHistoryColumns( columns={SearchHistoryColumns(
@@ -106,6 +110,7 @@ const SearchHistory = () => {
filtersEnabled={filtersEnabled} filtersEnabled={filtersEnabled}
globalFilter={globalFilter} globalFilter={globalFilter}
setGlobalFilter={setGlobalFilter} setGlobalFilter={setGlobalFilter}
isRefreshing={historyDataIsFetching}
/> />
</Stack> </Stack>
</Stack> </Stack>

View File

@@ -6,6 +6,7 @@ import {
Grid, Grid,
GridCol, GridCol,
Group, Group,
LoadingOverlay,
Popover, Popover,
Select, Select,
Stack, Stack,
@@ -51,11 +52,15 @@ const SearchProviderSettings = () => {
}, },
}); });
const endpoint = 'api/providers'; const endpoint = 'api/providers';
const { data: providerData, isFetched: providerDataIsFetched } = useQueryAPI({ const {
data: providerData,
isFetched: providerDataIsFetched,
isFetching: providerDataIsFetching,
} = useQueryAPI({
endpoint: endpoint, endpoint: endpoint,
}); });
const [providers, setProviders] = useState<Provider[]>([]); const [providers, setProviders] = useState<Provider[]>([]);
const { isMobile, theme, filter } = useCommonHooks(); const { isMobile, theme, iconFilter } = useCommonHooks();
const { mutateAsync } = useMutationAPI(); const { mutateAsync } = useMutationAPI();
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false); const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
@@ -273,6 +278,16 @@ const SearchProviderSettings = () => {
w={'100%'} w={'100%'}
align='center' align='center'
> >
<LoadingOverlay
visible={providerDataIsFetching}
zIndex={180}
overlayProps={{ blur: 5 }}
loaderProps={{
color: 'var(--mantine-color-primary-0)',
type: 'bars',
size: 'xl',
}}
/>
<Grid columns={18} w={'75%'}> <Grid columns={18} w={'75%'}>
{providers.length > 0 ? ( {providers.length > 0 ? (
providers?.map((provider: Provider) => ( providers?.map((provider: Provider) => (
@@ -291,7 +306,7 @@ const SearchProviderSettings = () => {
<SearchProviderSettingsItem <SearchProviderSettingsItem
provider={provider} provider={provider}
Icon={allIcons[provider.icon || '']} Icon={allIcons[provider.icon || '']}
filter={filter} filter={iconFilter}
/> />
</Box> </Box>
</Popover.Target> </Popover.Target>

View File

@@ -1,4 +1,4 @@
import { Box, Group, SimpleGrid, Stack, Text } from '@mantine/core'; import { Box, Group, LoadingOverlay, SimpleGrid, Stack, Text } from '@mantine/core';
import { useDebouncedValue, useDisclosure } from '@mantine/hooks'; import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
import { NoItemsFound, SettingsHeader } from 'components'; import { NoItemsFound, SettingsHeader } from 'components';
import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks'; import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks';
@@ -15,7 +15,11 @@ export function ThemePlaylists() {
endpoint: themesEndpoint, endpoint: themesEndpoint,
}); });
const themePlaylistsEndpoint = 'api/themePlaylists'; const themePlaylistsEndpoint = 'api/themePlaylists';
const { data: themePlaylistData, isFetched: isThemePlaylistDataFetched } = useQueryAPI({ const {
data: themePlaylistData,
isFetched: themePlaylistDataIsFetched,
isFetching: themePlaylistDataIsFetching,
} = useQueryAPI({
endpoint: themePlaylistsEndpoint, endpoint: themePlaylistsEndpoint,
}); });
const [themePlaylists, setThemePlaylists] = useState<ThemePlaylist[]>([]); const [themePlaylists, setThemePlaylists] = useState<ThemePlaylist[]>([]);
@@ -54,7 +58,7 @@ export function ThemePlaylists() {
}; };
useEffect(() => { useEffect(() => {
if (themePlaylistData && isThemePlaylistDataFetched) { if (themePlaylistData && themePlaylistDataIsFetched) {
setThemePlaylists( setThemePlaylists(
themePlaylistData.filter( themePlaylistData.filter(
(a: ThemePlaylist) => (a: ThemePlaylist) =>
@@ -66,7 +70,7 @@ export function ThemePlaylists() {
if (debouncedSearch === searchValue) { if (debouncedSearch === searchValue) {
setIsLoading(false); setIsLoading(false);
} }
}, [themePlaylistData, isThemePlaylistDataFetched, debouncedSearch]); }, [themePlaylistData, themePlaylistDataIsFetched, debouncedSearch]);
return ( return (
<Stack> <Stack>
@@ -97,6 +101,16 @@ export function ThemePlaylists() {
setIsLoading={setIsLoading} setIsLoading={setIsLoading}
/> />
<Stack mt={isMobile ? '10rem' : '6rem'}> <Stack mt={isMobile ? '10rem' : '6rem'}>
<LoadingOverlay
visible={themePlaylistDataIsFetching}
zIndex={180}
overlayProps={{ blur: 5 }}
loaderProps={{
color: 'var(--mantine-color-primary-0)',
type: 'bars',
size: 'xl',
}}
/>
{allThemeData && ( {allThemeData && (
<> <>
<Group justify='center' align='center' pos={'relative'}> <Group justify='center' align='center' pos={'relative'}>

View File

@@ -60,11 +60,11 @@ export function ThemeSelect({
height: '60px', height: '60px',
color: theme?.primaryColor, color: theme?.primaryColor,
backgroundImage: backgroundImage:
theme?.backgroundImage.toLocaleLowerCase() !== 'none' theme?.backgroundImage.toLocaleLowerCase() === 'none'
? `linear-gradient(${theme?.backgroundColor}33, ${theme?.backgroundColor}33), url(../../api/backgroundImages/thumbs/${theme?.backgroundImage})` ? undefined
: undefined, : `linear-gradient(${theme?.backgroundColor}33, ${theme?.backgroundColor}33), url(../../api/backgroundImages/thumbs/${theme?.backgroundImage})`,
backgroundSize: backgroundSize:
theme?.backgroundImage.toLocaleLowerCase() !== 'none' ? 'cover' : undefined, theme?.backgroundImage.toLocaleLowerCase() === 'none' ? undefined : 'cover',
backgroundColor: backgroundColor:
theme?.backgroundImage.toLocaleLowerCase() === 'none' theme?.backgroundImage.toLocaleLowerCase() === 'none'
? theme?.backgroundColor ? theme?.backgroundColor
@@ -87,13 +87,13 @@ export function ThemeSelect({
h={'60px'} h={'60px'}
style={{ style={{
backgroundImage: backgroundImage:
theme?.backgroundImage.toLocaleLowerCase() !== 'none' theme?.backgroundImage.toLocaleLowerCase() === 'none'
? `linear-gradient(${theme?.backgroundColor}33, ${theme?.backgroundColor}33), url(../../api/backgroundImages/thumbs/${theme?.backgroundImage})` ? undefined
: undefined, : `linear-gradient(${theme?.backgroundColor}33, ${theme?.backgroundColor}33), url(../../api/backgroundImages/thumbs/${theme?.backgroundImage})`,
backgroundSize: backgroundSize:
theme?.backgroundImage.toLocaleLowerCase() !== 'none' theme?.backgroundImage.toLocaleLowerCase() === 'none'
? 'cover' ? undefined
: undefined, : 'cover',
backgroundColor: backgroundColor:
theme?.backgroundImage.toLocaleLowerCase() === 'none' theme?.backgroundImage.toLocaleLowerCase() === 'none'
? theme?.backgroundColor ? theme?.backgroundColor

View File

@@ -1,10 +1,5 @@
import { Box, Button, Group, Overlay, Stack, Text } from '@mantine/core'; import { Box, Button, Group, Overlay, Stack, Text } from '@mantine/core';
import { import { useDebouncedState, useDisclosure, useHover } from '@mantine/hooks';
useDebouncedState,
useDisclosure,
useElementSize,
useHover,
} from '@mantine/hooks';
import { DeleteItemModal } from 'components'; import { DeleteItemModal } from 'components';
import { useCommonHooks } from 'hooks'; import { useCommonHooks } from 'hooks';
import { textClasses } from 'styles'; import { textClasses } from 'styles';
@@ -29,7 +24,6 @@ export function ThemeCard({
enabled?: boolean; enabled?: boolean;
}) { }) {
const { hovered, ref: hoverRef } = useHover(); const { hovered, ref: hoverRef } = useHover();
const { ref, width } = useElementSize();
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false); const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const { currentTheme, setCurrentTheme, setCurrentThemePlaylist, handleDelete } = const { currentTheme, setCurrentTheme, setCurrentThemePlaylist, handleDelete } =
useCommonHooks(); useCommonHooks();
@@ -83,7 +77,7 @@ export function ThemeCard({
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
overflow: 'hidden', overflow: 'hidden',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
maxWidth: width, maxWidth: 275,
}} }}
> >
{cardTheme.name} {cardTheme.name}
@@ -111,19 +105,18 @@ export function ThemeCard({
/> />
</Group> </Group>
<Button <Button
ref={ref}
w={'275px'} w={'275px'}
h={'115px'} h={'115px'}
disabled={currentTheme === 'random'} disabled={currentTheme === 'random'}
style={{ style={{
backgroundImage: backgroundImage:
cardTheme?.backgroundImage.toLocaleLowerCase() !== 'none' cardTheme?.backgroundImage.toLocaleLowerCase() === 'none'
? `linear-gradient(${cardTheme?.backgroundColor}33, ${cardTheme?.backgroundColor}33), url(../../api/backgroundImages/thumbs/${cardTheme?.backgroundImage})` ? undefined
: undefined, : `linear-gradient(${cardTheme?.backgroundColor}33, ${cardTheme?.backgroundColor}33), url(../../api/backgroundImages/thumbs/${cardTheme?.backgroundImage})`,
backgroundSize: backgroundSize:
cardTheme?.backgroundImage.toLocaleLowerCase() !== 'none' cardTheme?.backgroundImage.toLocaleLowerCase() === 'none'
? 'cover' ? undefined
: undefined, : 'cover',
backgroundColor: backgroundColor:
cardTheme?.backgroundImage.toLocaleLowerCase() === 'none' cardTheme?.backgroundImage.toLocaleLowerCase() === 'none'
? cardTheme?.backgroundColor ? cardTheme?.backgroundColor

View File

@@ -147,7 +147,7 @@ export function ThemeManagementPill({
position: 'absolute', position: 'absolute',
right: 3, right: 3,
top: 3, top: 3,
marginRight: !hovered ? 0 : '-2.5rem', marginRight: hovered ? '-2.5rem' : 0,
transition: 'all 0.2s', transition: 'all 0.2s',
}} }}
align='center' align='center'

View File

@@ -13,11 +13,11 @@ export function ThemeMiniCard({ miniTheme }: { miniTheme: Theme | undefined }) {
h={'60px'} h={'60px'}
style={{ style={{
backgroundImage: backgroundImage:
miniTheme?.backgroundImage.toLocaleLowerCase() !== 'none' miniTheme?.backgroundImage.toLocaleLowerCase() === 'none'
? `linear-gradient(${miniTheme?.backgroundColor}33, ${miniTheme?.backgroundColor}33), url(../../api/backgroundImages/thumbs/${miniTheme?.backgroundImage})` ? undefined
: undefined, : `linear-gradient(${miniTheme?.backgroundColor}33, ${miniTheme?.backgroundColor}33), url(../../api/backgroundImages/thumbs/${miniTheme?.backgroundImage})`,
backgroundSize: backgroundSize:
miniTheme?.backgroundImage.toLocaleLowerCase() !== 'none' ? 'cover' : undefined, miniTheme?.backgroundImage.toLocaleLowerCase() === 'none' ? undefined : 'cover',
backgroundColor: backgroundColor:
miniTheme?.backgroundImage.toLocaleLowerCase() === 'none' miniTheme?.backgroundImage.toLocaleLowerCase() === 'none'
? miniTheme?.backgroundColor ? miniTheme?.backgroundColor

View File

@@ -37,7 +37,87 @@ export function NewThemeStarter({
}, []); }, []);
return ( return (
<Stack align='center' p={'2rem'}> <Stack align='center' p={'2rem'}>
{!duplicate ? ( {duplicate ? (
<Stack align='center'>
<Group
w={'100%'}
justify='center'
style={{
backgroundColor:
'color-mix(in srgb, var(--mantine-color-accent-0), var(--mantine-color-background-0))',
padding: '1rem',
borderRadius: '8px',
}}
>
{allThemes?.map((miniTheme: Theme) => {
return (
<Box
{...handlers}
key={miniTheme.id}
onClick={() => {
!pressed && setSelectedTheme(miniTheme);
setPressed(false);
}}
>
<ThemePreviewMiddleware
newThemeList={[miniTheme.id as number]}
miniTheme={miniTheme}
themeList={[]}
selectedPlaylist={undefined}
setSelectedPlaylist={() => {}}
/>
</Box>
);
})}
</Group>
<Group>
<Group
style={{
backgroundColor:
'color-mix(in srgb, var(--mantine-color-primary-0), var(--mantine-color-background-0))',
padding: '1rem',
borderRadius: '8px',
}}
>
<ThemeMiniCard miniTheme={selectedTheme} />
<Button
disabled={selectedTheme.name === 'default'}
onClick={() => {
setSelectedTheme({
...selectedTheme,
name: `${selectedTheme.name} copy`,
});
setIsNew(false);
}}
>
Confirm
</Button>
</Group>
<Button
color={'red'}
styles={{ label: { color: 'black' } }}
onClick={() => {
setDuplicate(false);
setSelectedTheme({
id: undefined,
name: 'Default',
backgroundImage: 'none',
backgroundColor: '#EEEEEE',
searchColor: '#00000033',
primaryColor: '#222222',
accentColor: '#5c5c5c',
borderColor: '#5c5c5c',
randomEnabled: 1,
textShadowsEnabled: 0,
appShadowsEnabled: 0,
});
}}
>
Back
</Button>
</Group>
</Stack>
) : (
<> <>
<Text className={textClasses.settingsSubTitle} classNames={textClasses}> <Text className={textClasses.settingsSubTitle} classNames={textClasses}>
Please choose an option: Please choose an option:
@@ -189,86 +269,6 @@ export function NewThemeStarter({
</Stack> </Stack>
</Group> </Group>
</> </>
) : (
<Stack align='center'>
<Group
w={'100%'}
justify='center'
style={{
backgroundColor:
'color-mix(in srgb, var(--mantine-color-accent-0), var(--mantine-color-background-0))',
padding: '1rem',
borderRadius: '8px',
}}
>
{allThemes?.map((miniTheme: Theme) => {
return (
<Box
{...handlers}
key={miniTheme.id}
onClick={() => {
!pressed && setSelectedTheme(miniTheme);
setPressed(false);
}}
>
<ThemePreviewMiddleware
newThemeList={[miniTheme.id as number]}
miniTheme={miniTheme}
themeList={[]}
selectedPlaylist={undefined}
setSelectedPlaylist={() => {}}
/>
</Box>
);
})}
</Group>
<Group>
<Group
style={{
backgroundColor:
'color-mix(in srgb, var(--mantine-color-primary-0), var(--mantine-color-background-0))',
padding: '1rem',
borderRadius: '8px',
}}
>
<ThemeMiniCard miniTheme={selectedTheme} />
<Button
disabled={selectedTheme.name === 'default'}
onClick={() => {
setSelectedTheme({
...selectedTheme,
name: `${selectedTheme.name} copy`,
});
setIsNew(false);
}}
>
Confirm
</Button>
</Group>
<Button
color={'red'}
styles={{ label: { color: 'black' } }}
onClick={() => {
setDuplicate(false);
setSelectedTheme({
id: undefined,
name: 'Default',
backgroundImage: 'none',
backgroundColor: '#EEEEEE',
searchColor: '#00000033',
primaryColor: '#222222',
accentColor: '#5c5c5c',
borderColor: '#5c5c5c',
randomEnabled: 1,
textShadowsEnabled: 0,
appShadowsEnabled: 0,
});
}}
>
Back
</Button>
</Group>
</Stack>
)} )}
</Stack> </Stack>
); );

View File

@@ -84,7 +84,7 @@ export function ThemeEditor({
? `linear-gradient(${selectedTheme.backgroundColor}88, ${selectedTheme.backgroundColor}88), no-repeat center url(../../api/backgroundImages/thumbs/${item})` ? `linear-gradient(${selectedTheme.backgroundColor}88, ${selectedTheme.backgroundColor}88), no-repeat center url(../../api/backgroundImages/thumbs/${item})`
: `no-repeat center url(../../api/backgroundImages/thumbs/${item})` : `no-repeat center url(../../api/backgroundImages/thumbs/${item})`
: 'repeating-linear-gradient(45deg, #33333366, #33333366 10px, #88888866 10px, #88888866 20px)', : 'repeating-linear-gradient(45deg, #33333366, #33333366 10px, #88888866 10px, #88888866 20px)',
backgroundSize: item !== 'none' ? 'cover' : undefined, backgroundSize: item === 'none' ? undefined : 'cover',
backgroundColor: item === 'none' ? '#00000088' : undefined, backgroundColor: item === 'none' ? '#00000088' : undefined,
paddingLeft: '.5rem', paddingLeft: '.5rem',
alignItems: 'center', alignItems: 'center',
@@ -639,12 +639,12 @@ export function ThemeEditor({
</Tooltip> </Tooltip>
<Button <Button
onClick={() => { onClick={() => {
!duplicate duplicate
? setSelectedTheme({ ...defaultTheme }) ? setSelectedTheme({
: setSelectedTheme({
...defaultTheme, ...defaultTheme,
name: `${defaultTheme.name} copy`, name: `${defaultTheme.name} copy`,
}); })
: setSelectedTheme({ ...defaultTheme });
}} }}
> >
Reset Changes Reset Changes

View File

@@ -10,13 +10,13 @@ export function ThemeModalContent({ selectedTheme }: { selectedTheme: Theme }) {
<Stack <Stack
style={{ style={{
backgroundImage: backgroundImage:
selectedTheme?.backgroundImage.toLocaleLowerCase() !== 'none' selectedTheme?.backgroundImage.toLocaleLowerCase() === 'none'
? `linear-gradient(${selectedTheme?.backgroundColor}33, ${selectedTheme?.backgroundColor}33), url(../../api/backgroundImages/thumbs/${selectedTheme?.backgroundImage})` ? undefined
: undefined, : `linear-gradient(${selectedTheme?.backgroundColor}33, ${selectedTheme?.backgroundColor}33), url(../../api/backgroundImages/thumbs/${selectedTheme?.backgroundImage})`,
backgroundSize: backgroundSize:
selectedTheme?.backgroundImage.toLocaleLowerCase() !== 'none' selectedTheme?.backgroundImage.toLocaleLowerCase() === 'none'
? 'cover' ? undefined
: undefined, : 'cover',
backgroundColor: backgroundColor:
selectedTheme?.backgroundImage.toLocaleLowerCase() === 'none' selectedTheme?.backgroundImage.toLocaleLowerCase() === 'none'
? selectedTheme?.backgroundColor ? selectedTheme?.backgroundColor

View File

@@ -40,7 +40,9 @@ export function ThemePreviewMiddleware({
: undefined, : undefined,
}} }}
onClick={() => { onClick={() => {
if (!pressed) { if (pressed) {
setPressed(false);
} else {
const newSelectedPlaylist = { ...selectedPlaylist }; const newSelectedPlaylist = { ...selectedPlaylist };
if (themeList?.includes(miniTheme.id)) { if (themeList?.includes(miniTheme.id)) {
newThemeList.splice(themeList.indexOf(miniTheme.id), 1); newThemeList.splice(themeList.indexOf(miniTheme.id), 1);
@@ -49,8 +51,6 @@ export function ThemePreviewMiddleware({
} }
newSelectedPlaylist.themeList = JSON.stringify(newThemeList); newSelectedPlaylist.themeList = JSON.stringify(newThemeList);
setSelectedPlaylist(newSelectedPlaylist); setSelectedPlaylist(newSelectedPlaylist);
} else {
setPressed(false);
} }
}} }}
> >

View File

@@ -3,6 +3,7 @@ import {
Button, Button,
Divider, Divider,
Group, Group,
LoadingOverlay,
Overlay, Overlay,
Stack, Stack,
Switch, Switch,
@@ -24,11 +25,19 @@ import { ThemeModal } from './ThemeModal';
export function Themes() { export function Themes() {
const backgroundsEndpoint = 'api/backgrounds'; const backgroundsEndpoint = 'api/backgrounds';
const { data: allThemeData, isFetched: isAllThemeDataFetched } = useQueryAPI({ const {
data: allThemeData,
isFetched: allThemeDataIsFetched,
isFetching: allThemeDataIsFetching,
} = useQueryAPI({
endpoint: 'api/themes', endpoint: 'api/themes',
}); });
const [allThemes, setAllThemes] = useState<Theme[]>([]); const [allThemes, setAllThemes] = useState<Theme[]>([]);
const { data: backgroundData, isFetched: isBackgroundFetched } = useQueryAPI({ const {
data: backgroundData,
isFetched: backgroundDataIsFetched,
isFetching: backgroundDataIsFetching,
} = useQueryAPI({
endpoint: backgroundsEndpoint, endpoint: backgroundsEndpoint,
}); });
const { data: themePlaylistData } = useQueryAPI({ const { data: themePlaylistData } = useQueryAPI({
@@ -76,13 +85,13 @@ export function Themes() {
}; };
useEffect(() => { useEffect(() => {
if (backgroundData && isBackgroundFetched) { if (backgroundData && backgroundDataIsFetched) {
setBackgroundList([ setBackgroundList([
'None', 'None',
...backgroundData.sort((a: string, b: string) => a.localeCompare(b)), ...backgroundData.sort((a: string, b: string) => a.localeCompare(b)),
]); ]);
} }
}, [backgroundData, isBackgroundFetched]); }, [backgroundData, backgroundDataIsFetched]);
useEffect(() => { useEffect(() => {
if (themePlaylistData) { if (themePlaylistData) {
@@ -91,7 +100,7 @@ export function Themes() {
}, [themePlaylistData]); }, [themePlaylistData]);
useEffect(() => { useEffect(() => {
if (allThemeData && isAllThemeDataFetched) { if (allThemeData && allThemeDataIsFetched) {
setAllThemes( setAllThemes(
allThemeData.filter((a: Theme) => allThemeData.filter((a: Theme) =>
a.name.toLowerCase().includes(debouncedSearch.toLowerCase()), a.name.toLowerCase().includes(debouncedSearch.toLowerCase()),
@@ -101,7 +110,7 @@ export function Themes() {
if (debouncedSearch === searchValue) { if (debouncedSearch === searchValue) {
setIsLoading(false); setIsLoading(false);
} }
}, [allThemeData, isAllThemeDataFetched, debouncedSearch]); }, [allThemeData, allThemeDataIsFetched, debouncedSearch]);
useEffect(() => { useEffect(() => {
if (allThemes) { if (allThemes) {
@@ -156,6 +165,16 @@ export function Themes() {
setIsLoading={setIsLoading} setIsLoading={setIsLoading}
/> />
<Stack mt={isMobile ? '11rem' : '7rem'} align='center'> <Stack mt={isMobile ? '11rem' : '7rem'} align='center'>
<LoadingOverlay
visible={allThemeDataIsFetching || backgroundDataIsFetching}
zIndex={180}
overlayProps={{ blur: 5 }}
loaderProps={{
color: 'var(--mantine-color-primary-0)',
type: 'bars',
size: 'xl',
}}
/>
<Group <Group
w={'85%'} w={'85%'}
justify='center' justify='center'
@@ -201,10 +220,10 @@ export function Themes() {
zIndex: zIndex:
currentTheme === 'random' && currentTheme === 'random' &&
(themePlaylists (themePlaylists
.filter( .find(
(t: ThemePlaylist) => (t: ThemePlaylist) =>
t.name.toLowerCase() === currentThemePlaylist.toLowerCase(), t.name.toLowerCase() === currentThemePlaylist.toLowerCase(),
)[0] )
?.themeList.includes(`${cardTheme.id}`) || ?.themeList.includes(`${cardTheme.id}`) ||
cardTheme.name === theme.name) cardTheme.name === theme.name)
? 195 ? 195
@@ -222,10 +241,7 @@ export function Themes() {
</Group> </Group>
)) ))
) : ( ) : (
<Group <Group w={'100%'} style={{ display: 'flex', justifyContent: 'center' }}>
w={'100%'}
style={{ zIndex: 1000, display: 'flex', justifyContent: 'center' }}
>
<NoItemsFound /> <NoItemsFound />
</Group> </Group>
)} )}

View File

@@ -1,4 +1,13 @@
import { Button, Grid, GridCol, Group, Popover, Stack, TextInput } from '@mantine/core'; import {
Button,
Grid,
GridCol,
Group,
LoadingOverlay,
Popover,
Stack,
TextInput,
} from '@mantine/core';
import { isNotEmpty, matches, useForm } from '@mantine/form'; import { isNotEmpty, matches, useForm } from '@mantine/form';
import { useDebouncedValue, useDisclosure } from '@mantine/hooks'; import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
import { AddItemModal, NoItemsFound, SettingsHeader } from 'components'; import { AddItemModal, NoItemsFound, SettingsHeader } from 'components';
@@ -26,7 +35,11 @@ const WolClientSettings = () => {
}, },
}); });
const endpoint = 'api/wolClients'; const endpoint = 'api/wolClients';
const { data: wolClientsData, isFetched: wolClientsIsFetched } = useQueryAPI({ const {
data: wolClientsData,
isFetched: wolClientsIsFetched,
isFetching: wolClientsIsFetching,
} = useQueryAPI({
endpoint: endpoint, endpoint: endpoint,
}); });
const [wolClients, setWolClients] = useState<WolClient[]>([]); const [wolClients, setWolClients] = useState<WolClient[]>([]);
@@ -145,6 +158,16 @@ const WolClientSettings = () => {
w={'100%'} w={'100%'}
align='center' align='center'
> >
<LoadingOverlay
visible={wolClientsIsFetching}
zIndex={180}
overlayProps={{ blur: 5 }}
loaderProps={{
color: 'var(--mantine-color-primary-0)',
type: 'bars',
size: 'xl',
}}
/>
<Grid columns={18} w={'75%'}> <Grid columns={18} w={'75%'}>
{wolClients.length > 0 ? ( {wolClients.length > 0 ? (
wolClients?.map((client: WolClient) => ( wolClients?.map((client: WolClient) => (