Settings updates

This commit is contained in:
2025-03-20 22:54:13 -05:00
parent 321d620d3c
commit 27a9db82f0
8 changed files with 574 additions and 52 deletions

View File

@@ -4,15 +4,14 @@ import { useCommonHooks } from 'hooks';
import { useState } from 'react'; import { useState } from 'react';
import { stackClasses, textClasses } from 'styles'; import { stackClasses, textClasses } from 'styles';
function AppItem({ url, icon, name }: any) { function AppItem({ url, icon, name, linkDisabled }: any) {
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 { isMobile } = useCommonHooks(); const { isMobile } = useCommonHooks();
return ( return (
<Box <Box
component='a' component='a'
href={`https://${url}`} href={linkDisabled ? undefined : `https://${url}`}
onMouseOver={() => { onMouseOver={() => {
setTextDecoration('underline'); setTextDecoration('underline');
setHover(true); setHover(true);

View File

@@ -1,39 +1,296 @@
import { Stack, Text } from '@mantine/core'; import {
import { ApplicationColumns, DataTable } from 'components'; ActionIcon,
import { useQueryAPI } from 'hooks'; Box,
Button,
Divider,
Grid,
GridCol,
Group,
Highlight,
Modal,
Popover,
Stack,
Text,
TextInput,
} from '@mantine/core';
import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks';
import AppItem from 'pages/homepage/components/Applications/AppItem';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { textClasses } from 'styles'; import { FiHelpCircle, FiPlus } from 'react-icons/fi';
import {
actionIconClasses,
buttonClasses,
modalClasses,
textClasses,
textInputClasses,
} from 'styles';
import DebouncedApplication from './DebouncedApplication';
const ApplicationSettings = () => { const ApplicationSettings = () => {
const endpoint = 'api/apps'; const endpoint = 'api/apps';
const { const { data: apps, isFetching: isAppsFetching } = useQueryAPI({
data: apps,
isFetching: isAppsFetching,
isLoading: isAppsLoading,
} = useQueryAPI({
endpoint: endpoint, endpoint: endpoint,
}); });
const [applicationData, setApplicationData] = useState<any>([]); const [applicationData, setApplicationData] = useState<any>([]);
const { theme } = useCommonHooks();
const [application, setApplication] = useState<any>({});
const [update, setUpdate] = useState({});
const [debounced] = useDebouncedValue(update, 1000);
const [newApplication, setNewApplication] = useState<any>({
name: '',
url: '',
icon: '',
baseUrl: '',
filterName: '',
});
const { mutateAsync } = useMutationAPI();
const [searchValue, setSearchValue] = useState('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const handleDelete = (application: any) => {
mutateAsync({ method: 'DELETE', endpoint: endpoint, body: application });
};
useEffect(() => { useEffect(() => {
if (apps && !isAppsFetching) { if (apps && !isAppsFetching) {
setApplicationData(apps); setApplicationData(
apps.filter((a: any) =>
a.name.toLowerCase().includes(debouncedSearch.toLowerCase()),
),
);
} }
}, [apps, isAppsFetching]); }, [apps, isAppsFetching, debouncedSearch]);
useEffect(() => {
if (Object.keys(debounced).length) {
mutateAsync({ method: 'PATCH', endpoint: endpoint, body: debounced });
}
}, [debounced]);
useEffect(() => {
if (update !== application && Object.keys(update).length > 0) {
setApplication(update);
}
}, [update]);
return ( return (
<Stack> <Stack w={'100%'}>
<Text className={textClasses.settingsSubTitle} classNames={textClasses}> <Modal.Root
Applications size={'60%'}
</Text> className={modalClasses.helpModal}
<DataTable classNames={modalClasses}
tableName='Applications' opened={modalOpened}
columns={ApplicationColumns()} onClose={() => {
tableData={applicationData} closeModal();
isFetching={isAppsFetching} }}
isLoading={isAppsLoading} style={{
endpoint={endpoint} '--color-text-pri': theme.primaryColor,
/> '--color-text-acc': theme.accentColor,
'--color-background': theme.backgroundColor,
'--color-border': theme.borderColor,
'--color-search-background': theme.searchColor,
'--image-background': `url(${theme.backgroundImage})`,
}}
closeOnEscape
closeOnClickOutside
>
<Modal.Overlay />
<Modal.Content>
<Modal.Header>
<Modal.Title>Add a new application</Modal.Title>
<Modal.CloseButton />
</Modal.Header>
<Modal.Body pb={'2rem'}>
<Stack gap={0} w={'100%'} align='center'>
<TextInput
label='Name'
autoFocus
w={'75%'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
onChange={(event) => {
setNewApplication({
...newApplication,
name: event.target.value,
});
}}
/>
<TextInput
label='URL'
autoFocus
w={'75%'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
onChange={(event) => {
setNewApplication({
...newApplication,
url: event.target.value,
});
}}
/>
<TextInput
label='Icon'
autoFocus
w={'75%'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
onChange={(event) => {
setNewApplication({
...newApplication,
icon: event.target.value,
});
}}
/>
<TextInput
label='Base URL'
autoFocus
w={'75%'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
onChange={(event) => {
setNewApplication({
...newApplication,
baseUrl: event.target.value,
});
}}
/>
<TextInput
label='Filter Name'
autoFocus
w={'75%'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
onChange={(event) => {
setNewApplication({
...newApplication,
filterName: event.target.value,
});
}}
/>
<Divider color='black' my='sm' />
<Group pos={'absolute'} bottom={8}>
<Button
color={'green'}
styles={{ label: { color: 'black' } }}
onClick={() => {
if (newApplication) {
mutateAsync({
method: 'POST',
endpoint: endpoint,
body: newApplication,
});
closeModal();
}
}}
>
Save
</Button>
<Button
color={'red'}
styles={{ label: { color: 'black' } }}
onClick={() => {
closeModal();
}}
>
Cancel
</Button>
</Group>
</Stack>
</Modal.Body>
</Modal.Content>
</Modal.Root>
<Group align='center'>
<Text className={textClasses.settingsSubTitle} classNames={textClasses}>
Applications
</Text>
<Popover withArrow withOverlay withinPortal={false}>
<Popover.Target>
<ActionIcon
className={actionIconClasses.settingsTooltip}
classNames={actionIconClasses}
>
<FiHelpCircle />
</ActionIcon>
</Popover.Target>
<Popover.Dropdown
style={{
color: theme.primaryColor,
backgroundColor: theme.backgroundColor,
}}
>
<Stack>
<Text className={textClasses.settingsTooltip} classNames={textClasses}>
{'Applications are the apps that will be displayed in the homepage.'}
</Text>
<Highlight
ta={'center'}
highlight={''}
className={textClasses.settingsTooltip}
classNames={textClasses}
highlightStyles={{
backgroundImage:
'linear-gradient(45deg, var(--color-text-acc), var(--color-text-pri))',
fontWeight: 700,
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
}}
>
{''}
</Highlight>
</Stack>
</Popover.Dropdown>
</Popover>
<Button
className={buttonClasses.tableButton}
classNames={buttonClasses}
onClick={openModal}
>
<FiPlus />
</Button>
<TextInput
className={textInputClasses.settingsItem}
classNames={textInputClasses}
autoFocus
placeholder='Search...'
value={searchValue}
onInput={(event: any) => {
setSearchValue(event.target.value);
}}
/>
</Group>
<Stack w={'100%'} align='center'>
<Grid columns={18} w={'75%'}>
{applicationData?.map((application: any) => (
<GridCol span={3} key={application.id}>
<Popover withArrow withOverlay withinPortal={false}>
<Popover.Target>
<Box
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
>
<AppItem
url={application.url}
icon={application.icon}
name={application.name}
linkDisabled
/>
</Box>
</Popover.Target>
<Popover.Dropdown>
<DebouncedApplication
application={application}
handleDelete={handleDelete}
setApplication={setUpdate}
/>
</Popover.Dropdown>
</Popover>
</GridCol>
))}
</Grid>
</Stack>
</Stack> </Stack>
); );
}; };

View File

@@ -0,0 +1,179 @@
import {
ActionIcon,
Button,
Card,
Group,
Modal,
Stack,
Text,
TextInput,
Tooltip,
} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { IconTrash } from '@tabler/icons-react';
import { useCommonHooks } from 'hooks';
import { cardClasses, modalClasses, textInputClasses } from 'styles';
function DebouncedApplication({ application, handleDelete, setApplication }: any) {
const { theme } = useCommonHooks();
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
return (
<Card className={cardClasses.settingsItem} classNames={cardClasses}>
<Modal.Root
size={'60%'}
className={modalClasses.helpModal}
classNames={modalClasses}
opened={modalOpened}
onClose={() => {
closeModal();
}}
style={{
'--color-text-pri': theme.primaryColor,
'--color-text-acc': theme.accentColor,
'--color-background': theme.backgroundColor,
'--color-border': theme.borderColor,
'--color-search-background': theme.searchColor,
'--image-background': `url(${theme.backgroundImage})`,
}}
closeOnEscape
closeOnClickOutside
>
<Modal.Overlay />
<Modal.Content>
<Modal.Header>
<Modal.Title>Add a new message</Modal.Title>
<Modal.CloseButton />
</Modal.Header>
<Modal.Body pb={'2rem'}>
<Stack gap={0} w={'100%'} h={'150px'} align='center'>
<Text>Are you sure you want to delete this application?</Text>
<Text
style={{
fontWeight: 'bold',
fontSize: '20px',
padding: '8px',
margin: '8px',
borderRadius: '10px',
backgroundColor: 'var(--color-text-acc)',
color: 'var(--color-background)',
}}
>{`"${application.name}"`}</Text>
<Text c='red'>This action cannot be undone!</Text>
<Group pos={'absolute'} bottom={8}>
<Button
color={'red'}
styles={{ label: { color: 'black' } }}
onClick={() => {
handleDelete(application);
closeModal();
}}
>
Delete
</Button>
<Button
color={'grey'}
styles={{ label: { color: 'black' } }}
onClick={() => {
closeModal();
}}
>
Cancel
</Button>
</Group>
</Stack>
</Modal.Body>
</Modal.Content>
</Modal.Root>
<Tooltip
color={theme.primaryColor}
bd={`solid 1px ${theme.borderColor}`}
label={'Delete'}
>
<ActionIcon
pos={'absolute'}
right={16}
color='red'
onClick={() => {
openModal();
}}
>
<IconTrash />
</ActionIcon>
</Tooltip>
<Group w={'100%'} justify='center'>
<TextInput
label={'Name'}
className={textInputClasses.settingsItem}
pt={16}
classNames={textInputClasses}
defaultValue={application.name}
onChange={(event) => {
setApplication({
...application,
name: event.target.value,
});
}}
/>
<TextInput
label={'Base URL'}
className={textInputClasses.settingsItem}
pt={16}
classNames={textInputClasses}
defaultValue={application.baseUrl}
onChange={(event) => {
setApplication({
...application,
baseUrl: event.target.value,
});
}}
/>
</Group>
<Group w={'100%'} justify='center'>
<TextInput
label={'URL'}
className={textInputClasses.settingsItem}
w={'100%'}
pt={16}
classNames={textInputClasses}
defaultValue={application.url}
onChange={(event) => {
setApplication({
...application,
url: event.target.value,
});
}}
/>
</Group>
<Group w={'100%'} justify='center'>
<TextInput
label={'Icon'}
className={textInputClasses.settingsItem}
pt={16}
classNames={textInputClasses}
defaultValue={application.icon}
onChange={(event) => {
setApplication({
...application,
icon: event.target.value,
});
}}
/>
<TextInput
label={'Filter'}
className={textInputClasses.settingsItem}
pt={16}
classNames={textInputClasses}
defaultValue={application.filterName}
onChange={(event) => {
setApplication({
...application,
filterName: event.target.value,
});
}}
/>
</Group>
</Card>
);
}
export default DebouncedApplication;

View File

@@ -1,20 +1,29 @@
import { import {
ActionIcon,
Button, Button,
Divider, Divider,
Grid, Grid,
GridCol, GridCol,
Group, Group,
Highlight,
Modal, Modal,
Popover,
Stack, Stack,
Text, Text,
TextInput, TextInput,
} from '@mantine/core'; } from '@mantine/core';
import { useDebouncedValue, useDisclosure } from '@mantine/hooks'; import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
import DebouncedMessage from 'components/Shared/DebouncedMessage';
import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks'; import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks';
import DebouncedMessage from 'pages/settings/components/Messages/DebouncedMessage';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { FiPlus } from 'react-icons/fi'; import { FiHelpCircle, FiPlus } from 'react-icons/fi';
import { buttonClasses, modalClasses, textClasses, textInputClasses } from 'styles'; import {
actionIconClasses,
buttonClasses,
modalClasses,
textClasses,
textInputClasses,
} from 'styles';
const MessageSettings = () => { const MessageSettings = () => {
const endpoint = 'api/messages'; const endpoint = 'api/messages';
const { const {
@@ -32,6 +41,8 @@ const MessageSettings = () => {
const [newMessageError, setNewMessageError] = useState<string>(''); const [newMessageError, setNewMessageError] = useState<string>('');
const [message, setMessage] = useState({}); const [message, setMessage] = useState({});
const [debounced] = useDebouncedValue(message, 1000); const [debounced] = useDebouncedValue(message, 1000);
const [searchValue, setSearchValue] = useState('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false); const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const handleDelete = (message: any) => { const handleDelete = (message: any) => {
@@ -40,9 +51,13 @@ const MessageSettings = () => {
useEffect(() => { useEffect(() => {
if (messageData && isMessagesFetched) { if (messageData && isMessagesFetched) {
setMessages(messageData); setMessages(
messageData.filter((m: any) =>
m.message.toLowerCase().includes(debouncedSearch.toLowerCase()),
),
);
} }
}, [messageData, isMessagesFetched]); }, [messageData, isMessagesFetched, debouncedSearch]);
useEffect(() => { useEffect(() => {
if (Object.keys(debounced).length) { if (Object.keys(debounced).length) {
@@ -51,7 +66,7 @@ const MessageSettings = () => {
}, [debounced]); }, [debounced]);
return ( return (
<Stack> <Stack w={'100%'}>
<Modal.Root <Modal.Root
size={'60%'} size={'60%'}
className={modalClasses.helpModal} className={modalClasses.helpModal}
@@ -127,27 +142,77 @@ const MessageSettings = () => {
<Text className={textClasses.settingsSubTitle} classNames={textClasses}> <Text className={textClasses.settingsSubTitle} classNames={textClasses}>
Messages Messages
</Text> </Text>
<Popover withArrow withOverlay withinPortal={false}>
<Popover.Target>
<ActionIcon
className={actionIconClasses.settingsTooltip}
classNames={actionIconClasses}
>
<FiHelpCircle />
</ActionIcon>
</Popover.Target>
<Popover.Dropdown
style={{
color: theme.primaryColor,
backgroundColor: theme.backgroundColor,
}}
>
<Stack>
<Text className={textClasses.settingsTooltip} classNames={textClasses}>
{'Messages may contain any unicode text.'}
</Text>
<Highlight
ta={'center'}
highlight={['$firstName', '$lastName', '$^firstName', '$^lastName']}
className={textClasses.settingsTooltip}
classNames={textClasses}
highlightStyles={{
backgroundImage:
'linear-gradient(45deg, var(--color-text-acc), var(--color-text-pri))',
fontWeight: 700,
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
}}
>
{
'Built-in variables include $firstName and $lastName. To force the text to upper case, use a carat (^), ie, $^firstName.'
}
</Highlight>
</Stack>
</Popover.Dropdown>
</Popover>
<Button <Button
mt={'1.5rem'}
className={buttonClasses.tableButton} className={buttonClasses.tableButton}
classNames={buttonClasses} classNames={buttonClasses}
onClick={openModal} onClick={openModal}
> >
<FiPlus /> <FiPlus />
</Button> </Button>
<TextInput
className={textInputClasses.settingsItem}
classNames={textInputClasses}
autoFocus
placeholder='Search...'
value={searchValue}
onInput={(event: any) => {
setSearchValue(event.target.value);
}}
/>
</Group> </Group>
<Grid> <Stack w={'100%'} align='center'>
{messages?.map((message: any) => ( <Grid w={'75%'}>
<GridCol span={6} key={message.id}> {messages?.map((message: any) => (
<DebouncedMessage <GridCol span={6} key={message.id}>
label={`ID# ${message.id}`} <DebouncedMessage
message={message} label={`ID# ${message.id}`}
setMessage={setMessage} message={message}
handleDelete={handleDelete} setMessage={setMessage}
/> handleDelete={handleDelete}
</GridCol> />
))} </GridCol>
</Grid> ))}
</Grid>
</Stack>
</Stack> </Stack>
); );
}; };

View File

@@ -73,6 +73,29 @@
} }
} }
.settingsTooltip {
&.root {
transition: all .1s ease;
width: 30px;
height: 30px;
border-radius: 100%;
border: none;
background-color: transparent;
color: var(--color-text-pri);
.icon {
font-size: 24px;
}
.loader {
display: none;
}
:hover {
background-color: var(--color-text-pri) !important;
color: var(--color-text-acc) !important;
transition: all .1s ease;
}
}
}
.selectOptionActions { .selectOptionActions {
&.root { &.root {
background-color: transparent !important; background-color: transparent !important;

View File

@@ -1,7 +1,7 @@
.settingsItem { .settingsItem {
&.root { &.root {
color: var(--color-text-pri); color: var(--color-text-pri);
background-color: transparent; background-color: transparent;
backdrop-filter: blur(10px) brightness(.75) !important; backdrop-filter: blur(10px) brightness(0.75) !important;
} }
} }

View File

@@ -106,7 +106,6 @@
&.root { &.root {
font-size: 24px; font-size: 24px;
font-weight: bold; font-weight: bold;
padding-top: 1.5rem;
color: var(--color-text-pri); color: var(--color-text-pri);
} }
} }