New settings pages

This commit is contained in:
Sean Edmunds
2025-03-20 17:09:07 -05:00
parent cecdc84cf6
commit 321d620d3c
28 changed files with 529 additions and 228 deletions

View File

@@ -1,61 +0,0 @@
import type { MRT_ColumnDef } from 'mantine-react-table';
import { useMemo } from 'react';
export function PersonalizationColumns() {
const columns = useMemo<MRT_ColumnDef<any>[]>(
() => [
{
accessorFn: (row: any) => {
if (row.firstName) {
return `${row.firstName}`;
}
return '';
},
id: 'firstName',
header: 'First Name',
size: 50,
Cell: ({ renderedCellValue }) => renderedCellValue,
Header: ({ column }) => column.columnDef.header,
},
{
accessorFn: (row: any) => {
if (row.lastName) {
return `${row.lastName}`;
}
return '';
},
id: 'lastName',
header: 'Last Name',
Cell: ({ renderedCellValue }) => renderedCellValue,
Header: ({ column }) => column.columnDef.header,
},
{
accessorFn: (row: any) => {
if (row.email) {
return `${row.email}`;
}
return '';
},
id: 'email',
header: 'Email',
Cell: ({ renderedCellValue }) => renderedCellValue,
Header: ({ column }) => column.columnDef.header,
},
{
accessorFn: (row: any) => {
if (row.physicalAddress) {
return `${row.physicalAddress}`;
}
return '';
},
id: 'physicalAddress',
header: 'Address',
Cell: ({ renderedCellValue }) => renderedCellValue,
Header: ({ column }) => column.columnDef.header,
},
],
[],
);
return columns;
}

View File

@@ -1,15 +1,18 @@
import { Divider, Group, Modal, Stack } from '@mantine/core';
import { Divider, Modal, Stack } from '@mantine/core';
import { useCommonHooks } from 'hooks';
import { modalClasses } from 'styles';
import HintItem from './HintItem';
import HelpSection from './HelpSection';
export function HelpModal({
setSearchValue,
setProviderLabel,
setProviderURL,
providerModalOpened,
closeProviderModal,
applicationData,
providerData,
}: any) {
const { theme, setSearchValue, setProviderLabel, setProviderURL } = useCommonHooks();
const { theme } = useCommonHooks();
return (
<Modal.Root
size={'60%'}
@@ -17,10 +20,10 @@ export function HelpModal({
classNames={modalClasses}
opened={providerModalOpened}
onClose={() => {
closeProviderModal();
setSearchValue('');
setProviderLabel('Google Search');
setProviderURL('https://www.google.com/search?q=');
closeProviderModal();
}}
style={{
'--color-text-pri': theme.primaryColor,
@@ -41,67 +44,25 @@ export function HelpModal({
</Modal.Header>
<Modal.Body pb={'2rem'}>
<Stack gap={0} w={'100%'} align='center'>
<h3
style={{
zIndex: 0,
padding: '8px',
marginTop: 0,
marginBottom: -32,
borderRadius: '8px',
color: theme.backgroundColor,
backgroundColor: theme.borderColor,
}}
>
Search Providers
</h3>
<Divider py={8} color={theme.accentColor} />
<Group
style={{
width: '100%',
backgroundColor: theme.searchColor,
borderRadius: '8px',
padding: '24px',
justifyItems: 'center',
}}
>
{providerData?.map((provider: any) => (
<HintItem
key={provider.prefix}
command={provider.prefix}
item={provider.name}
<HelpSection
name={'Search Providers'}
items={providerData.map((provider: any) => {
return {
name: provider.name,
command: provider.prefix,
};
})}
/>
))}
</Group>
<h3
style={{
zIndex: 0,
padding: '8px',
marginBottom: -32,
borderRadius: '8px',
color: theme.backgroundColor,
backgroundColor: theme.borderColor,
}}
>
Available Applications
</h3>
<Divider py={8} color={theme.accentColor} />
<Group
style={{
width: '100%',
backgroundColor: theme.searchColor,
borderRadius: '8px',
padding: '32px',
justifyItems: 'center',
}}
>
{applicationData?.map((application: any) => (
<HintItem
key={application.id}
command={`!${application.baseUrl.substr(1).toLowerCase()}`}
item={application.url}
<Divider my={8} color={theme.accentColor} />
<HelpSection
name={'Available Applications'}
items={applicationData.map((application: any) => {
return {
name: application.url,
command: `!${application.baseUrl.substring(1).toLowerCase()}`,
};
})}
/>
))}
</Group>
</Stack>
</Modal.Body>
</Modal.Content>

View File

@@ -0,0 +1,42 @@
import { Divider, Group } from '@mantine/core';
import { useCommonHooks } from 'hooks';
import HintItem from './HintItem';
function HelpSection({ name, items }: any) {
const { theme } = useCommonHooks();
return (
<>
<h3
style={{
zIndex: 0,
padding: '8px',
marginTop: 0,
marginBottom: -32,
borderRadius: '8px',
color: theme.backgroundColor,
backgroundColor: theme.borderColor,
}}
>
{name}
</h3>
<Divider py={8} color={theme.accentColor} />
<Group
style={{
width: '100%',
backgroundColor: theme.searchColor,
borderRadius: '8px',
padding: '24px',
justifyItems: 'center',
}}
>
{items
?.sort((a: any, b: any) => (a.command > b.command ? 1 : -1))
.map((item: any) => (
<HintItem key={item.command} command={item.command} name={item.name} />
))}
</Group>
</>
);
}
export default HelpSection;

View File

@@ -1,6 +1,6 @@
import { Group, Text } from '@mantine/core';
function HintItem({ command, item }: any) {
function HintItem({ command, name }: any) {
return (
<Group
gap={0}
@@ -31,7 +31,7 @@ function HintItem({ command, item }: any) {
borderRadius: '10px',
}}
>
{item}
{name}
</Text>
</Group>
);

View File

@@ -0,0 +1,120 @@
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 DebouncedMessage({ label, message, setMessage, handleDelete }: 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 message?</Text>
<Text
style={{
fontWeight: 'bold',
fontSize: '20px',
padding: '8px',
margin: '8px',
borderRadius: '10px',
backgroundColor: 'var(--color-text-acc)',
color: 'var(--color-background)',
}}
>{`"${message.message}"`}</Text>
<Text c='red'>This action cannot be undone!</Text>
<Group pos={'absolute'} bottom={8}>
<Button
color={'red'}
styles={{ label: { color: 'black' } }}
onClick={() => {
handleDelete(message);
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>
<TextInput
label={label}
className={textInputClasses.settingsItem}
pt={16}
classNames={textInputClasses}
defaultValue={message.message}
onChange={(event) => {
setMessage({
...message,
message: event.target.value,
});
}}
/>
</Card>
);
}
export default DebouncedMessage;

View File

@@ -7,6 +7,5 @@ export { ApplicationColumns } from './DataTable/columns/ApplicationColumns';
export { BookmarkColumns } from './DataTable/columns/BookmarkColumns';
export { CategoryColumns } from './DataTable/columns/CategoryColumns';
export { MessagesColumns } from './DataTable/columns/MessagesColumns';
export { PersonalizationColumns } from './DataTable/columns/PersonalizationColumns';
export { SearchProviderColumns } from './DataTable/columns/SearchProviderColumns';
export { WolClientColumns } from './DataTable/columns/WolClientColumns';

View File

@@ -240,8 +240,12 @@ export default function Search({
const optionsFilter: OptionsFilter = ({ options, search }) => {
const splittedSearch = search.toLowerCase().trim().split(' ');
if (splittedSearch[0] === '!') {
return apps.map((app: any) => {
if (splittedSearch[0].startsWith('!')) {
return apps
.filter((app: any) =>
app.name.toLowerCase().includes(splittedSearch[0].substring(1).toLowerCase()),
)
.map((app: any) => {
return {
label: app.name,
value: app.baseUrl,
@@ -469,6 +473,9 @@ export default function Search({
onKeyUp={handleKeyUp}
/>
<HelpModal
setSearchValue={setSearchValue}
setProviderLabel={setProviderLabel}
setProviderURL={setProviderURL}
providerModalOpened={providerModalOpened}
applicationData={applicationData}
providerData={providerData}

View File

@@ -13,15 +13,15 @@ import {
} from 'react-icons/fi';
import { useNavigate, useParams } from 'react-router-dom';
import { actionIconClasses, tabClasses, textClasses } from 'styles';
import Applications from './components/Applications';
import Bookmarks from './components/Bookmarks';
import Categories from './components/Categories';
import Messages from './components/Messages';
import Personalization from './components/Personalization';
import SearchProviders from './components/SearchProviders';
import ThemePlaylists from './components/ThemePlaylists';
import Themes from './components/Themes';
import WolClients from './components/WolClients';
import Applications from './components/Applications/Applications';
import Bookmarks from './components/Bookmarks/Bookmarks';
import Categories from './components/Categories/Categories';
import Messages from './components/Messages/Messages';
import Personalization from './components/Personalization/Personalization';
import SearchProviders from './components/Search/SearchProviders';
import ThemePlaylists from './components/Theming/ThemePlaylists';
import Themes from './components/Theming/Themes';
import WolClients from './components/WoL/WolClients';
export function Settings() {
const { isMobile } = useCommonHooks();

View File

@@ -2,7 +2,7 @@ import { Stack, Text } from '@mantine/core';
import { CategoryColumns, DataTable } from 'components';
import { useQueryAPI } from 'hooks';
import { useEffect, useState } from 'react';
import { textClasses } from '../../../styles';
import { textClasses } from '../../../../styles';
const CategorySettings = () => {
const endpoint = 'api/categories';
const {

View File

@@ -1,40 +0,0 @@
import { Stack, Text } from '@mantine/core';
import { DataTable, MessagesColumns } from 'components';
import { useQueryAPI } from 'hooks';
import { useEffect, useState } from 'react';
import { textClasses } from 'styles';
const MessageSettings = () => {
const endpoint = 'api/messages';
const {
data: messageData,
isFetching: isMessagesFetching,
isFetched: isMessagesFetched,
isLoading: isMessagesLoading,
} = useQueryAPI({
endpoint: 'api/messages',
});
const [messages, setMessages] = useState<any>([]);
useEffect(() => {
if (messageData && isMessagesFetched) {
setMessages(messageData);
}
}, [messageData, isMessagesFetched]);
return (
<Stack>
<Text className={textClasses.settingsSubTitle} classNames={textClasses}>
Messages
</Text>
<DataTable
tableName='Messages'
columns={MessagesColumns()}
tableData={messages}
isFetching={isMessagesFetching}
isLoading={isMessagesLoading}
endpoint={endpoint}
/>
</Stack>
);
};
export default MessageSettings;

View File

@@ -0,0 +1,154 @@
import {
Button,
Divider,
Grid,
GridCol,
Group,
Modal,
Stack,
Text,
TextInput,
} from '@mantine/core';
import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
import DebouncedMessage from 'components/Shared/DebouncedMessage';
import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks';
import { useEffect, useState } from 'react';
import { FiPlus } from 'react-icons/fi';
import { buttonClasses, modalClasses, textClasses, textInputClasses } from 'styles';
const MessageSettings = () => {
const endpoint = 'api/messages';
const {
data: messageData,
isFetching: isMessagesFetching,
isFetched: isMessagesFetched,
isLoading: isMessagesLoading,
} = useQueryAPI({
endpoint: 'api/messages',
});
const { theme } = useCommonHooks();
const { mutateAsync } = useMutationAPI();
const [messages, setMessages] = useState<any>([]);
const [newMessage, setNewMessage] = useState('');
const [newMessageError, setNewMessageError] = useState<string>('');
const [message, setMessage] = useState({});
const [debounced] = useDebouncedValue(message, 1000);
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const handleDelete = (message: any) => {
mutateAsync({ method: 'DELETE', endpoint: endpoint, body: message });
};
useEffect(() => {
if (messageData && isMessagesFetched) {
setMessages(messageData);
}
}, [messageData, isMessagesFetched]);
useEffect(() => {
if (Object.keys(debounced).length) {
mutateAsync({ method: 'PATCH', endpoint: endpoint, body: debounced });
}
}, [debounced]);
return (
<Stack>
<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%'} align='center'>
<TextInput
autoFocus
w={'75%'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
onChange={(event) => {
setNewMessage(event.target.value);
}}
error={newMessageError}
/>
<Divider color='black' my='sm' />
<Group pos={'absolute'} bottom={8}>
<Button
color={'green'}
styles={{ label: { color: 'black' } }}
onClick={() => {
if (newMessage) {
mutateAsync({
method: 'POST',
endpoint: endpoint,
body: { message: newMessage },
});
closeModal();
} else {
setNewMessageError('Please enter a message');
}
}}
>
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}>
Messages
</Text>
<Button
mt={'1.5rem'}
className={buttonClasses.tableButton}
classNames={buttonClasses}
onClick={openModal}
>
<FiPlus />
</Button>
</Group>
<Grid>
{messages?.map((message: any) => (
<GridCol span={6} key={message.id}>
<DebouncedMessage
label={`ID# ${message.id}`}
message={message}
setMessage={setMessage}
handleDelete={handleDelete}
/>
</GridCol>
))}
</Grid>
</Stack>
);
};
export default MessageSettings;

View File

@@ -1,40 +0,0 @@
import { Text } from '@mantine/core';
import { DataTable, PersonalizationColumns } from 'components';
import { useQueryAPI } from 'hooks';
import { useEffect, useState } from 'react';
import { textClasses } from 'styles';
const PersonalizationSettings = () => {
const endpoint = 'api/personalData';
const {
data: personalData,
isFetched: personalIsFetched,
isFetching: personalIsFetching,
isLoading: personalIsLoading,
} = useQueryAPI({
endpoint: endpoint,
});
const [personal, setPersonal] = useState<any>([]);
useEffect(() => {
if (personalData && personalIsFetched) {
setPersonal(personalData);
}
}, [personalData, personalIsFetched]);
return (
<>
<Text className={textClasses.settingsSubTitle} classNames={textClasses}>
Personalization
</Text>
<DataTable
tableName='Personalization'
columns={PersonalizationColumns()}
tableData={personal}
isFetching={personalIsFetching}
isLoading={personalIsLoading}
endpoint={endpoint}
/>
</>
);
};
export default PersonalizationSettings;

View File

@@ -0,0 +1,128 @@
import { Card, Grid, GridCol, Overlay, Stack, Text, TextInput } from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks';
import { useMutationAPI, useQueryAPI } from 'hooks';
import { useEffect, useState } from 'react';
import { cardClasses, textClasses, textInputClasses } from 'styles';
const PersonalizationSettings = () => {
const endpoint = 'api/personalData';
const {
data: personalData,
isFetched: personalIsFetched,
isFetching: personalIsFetching,
} = useQueryAPI({
endpoint: endpoint,
});
const [personal, setPersonal] = useState<any>({});
const [update, setUpdate] = useState({});
const [debounced] = useDebouncedValue(update, 1000);
const { mutateAsync: editRow } = useMutationAPI();
useEffect(() => {
if (personalData && personalIsFetched) {
setPersonal(personalData[0]);
}
}, [personalData, personalIsFetched]);
useEffect(() => {
if (Object.keys(debounced).length) {
setPersonal(update);
editRow({ method: 'PATCH', endpoint: endpoint, body: debounced });
setUpdate({});
}
}, [debounced]);
useEffect(() => {
if (update !== personal && Object.keys(update).length) {
setPersonal(update);
}
}, [update]);
return (
<>
<Text className={textClasses.settingsSubTitle} classNames={textClasses}>
Personalization
</Text>
<Stack w={'100%'} align='center'>
<Grid w={'75%'} pos={'relative'}>
{personalIsFetching && (
<Overlay
style={{
borderRadius: '10px',
margin: '-8px',
backdropFilter: 'blur(10px) brightness(1.2)',
}}
/>
)}
<GridCol span={4}>
<Card className={cardClasses.settingsItem} classNames={cardClasses}>
<TextInput
label={'First Name'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
defaultValue={personal?.firstName}
onChange={(event) => {
const updatedPersonal = {
...personal,
firstName: event.currentTarget.value,
};
setUpdate(updatedPersonal);
}}
/>
</Card>
</GridCol>
<GridCol span={4}>
<Card className={cardClasses.settingsItem} classNames={cardClasses}>
<TextInput
label={'Last Name'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
defaultValue={personal?.lastName}
onChange={(event) => {
const updatedPersonal = {
...personal,
lastName: event.currentTarget.value,
};
setUpdate(updatedPersonal);
}}
/>
</Card>
</GridCol>
<GridCol span={4}>
<Card className={cardClasses.settingsItem} classNames={cardClasses}>
<TextInput
label={'Email'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
defaultValue={personal?.email}
onChange={(event) => {
const updatedPersonal = {
...personal,
email: event.currentTarget.value,
};
setUpdate(updatedPersonal);
}}
/>
</Card>
</GridCol>
<GridCol span={12}>
<Card className={cardClasses.settingsItem} classNames={cardClasses}>
<TextInput
label={'Physical Address'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
defaultValue={personal?.physicalAddress}
onChange={(event) => {
const updatedPersonal = {
...personal,
physicalAddress: event.currentTarget.value,
};
setUpdate(updatedPersonal);
}}
/>
</Card>
</GridCol>
</Grid>
</Stack>
</>
);
};
export default PersonalizationSettings;

View File

@@ -11,7 +11,7 @@ function ThemeCard({
createTheme,
allThemes,
}: any) {
const { data: themePlaylists, isFetched: isThemePlaylistsFetched } = useQueryAPI({
const { data: themePlaylists } = useQueryAPI({
endpoint: 'api/themePlaylists',
});
const [hover, setHover] = useState(false);

View File

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

View File

@@ -20,6 +20,28 @@
}
}
.settingsItem {
&.root {
.wrapper {
display: flex;
justify-content: center;
.input {
width: 100%;
border: none;
background-color: var(--color-search-background);
color: var(--color-text-pri);
font-size: 18px;
}
.section {
color: var(--color-border);
width: auto;
padding-left: .5rem;
opacity: 1;
}
}
}
}
.search {
&.root {
.wrapper {

View File

@@ -1,5 +1,6 @@
import actionIconClasses from './ActionIcon.module.css';
import buttonClasses from './Button.module.css';
import cardClasses from './Card.module.css';
import menuClasses from './Menu.module.css';
import modalClasses from './Modal.module.css';
import multiSelectClasses from './MultiSelect.module.css';
@@ -13,6 +14,7 @@ import textInputClasses from './TextInput.module.css';
export {
actionIconClasses,
cardClasses,
buttonClasses,
menuClasses,
modalClasses,