App and sorting updates

This commit is contained in:
Sean Edmunds
2025-04-01 13:59:18 -05:00
parent b741903649
commit c36ca07893
10 changed files with 390 additions and 109 deletions

View File

@@ -132,6 +132,8 @@ export function useMutationAPI() {
queryClient.refetchQueries({ queryKey: ['GET', 'api/links'] }); queryClient.refetchQueries({ queryKey: ['GET', 'api/links'] });
endpoint === 'api/categoryOrder' && endpoint === 'api/categoryOrder' &&
queryClient.refetchQueries({ queryKey: ['GET', 'api/categories'] }); queryClient.refetchQueries({ queryKey: ['GET', 'api/categories'] });
endpoint === 'api/applicationOrder' &&
queryClient.refetchQueries({ queryKey: ['GET', 'api/applicationOrder'] });
}, },
onSuccess: (data, { method, endpoint }) => { onSuccess: (data, { method, endpoint }) => {
queryClient.refetchQueries({ queryClient.refetchQueries({
@@ -141,6 +143,8 @@ export function useMutationAPI() {
queryClient.refetchQueries({ queryKey: ['GET', 'api/links'] }); queryClient.refetchQueries({ queryKey: ['GET', 'api/links'] });
endpoint === 'api/categoryOrder' && endpoint === 'api/categoryOrder' &&
queryClient.refetchQueries({ queryKey: ['GET', 'api/categories'] }); queryClient.refetchQueries({ queryKey: ['GET', 'api/categories'] });
endpoint === 'api/applicationOrder' &&
queryClient.refetchQueries({ queryKey: ['GET', 'api/applicationOrder'] });
}, },
}); });
} }

View File

@@ -16,6 +16,14 @@ export function useCommonHooks() {
key: 'appFilter', key: 'appFilter',
defaultValue: ['All'], defaultValue: ['All'],
}); });
const [appSort, setAppSort] = useLocalStorage<string>({
key: 'appSort',
defaultValue: 'A-Z',
});
const [bookmarkSort, setBookmarkSort] = useLocalStorage<string>({
key: 'bookmarkSort',
defaultValue: 'A-Z',
});
const [bookmarkFilter, setBookmarkFilter] = useLocalStorage<[any]>({ const [bookmarkFilter, setBookmarkFilter] = useLocalStorage<[any]>({
key: 'bookmarkFilter', key: 'bookmarkFilter',
defaultValue: ['All'], defaultValue: ['All'],
@@ -114,6 +122,10 @@ export function useCommonHooks() {
setThemeName, setThemeName,
appFilter, appFilter,
setAppFilter, setAppFilter,
appSort,
setAppSort,
bookmarkSort,
setBookmarkSort,
bookmarkFilter, bookmarkFilter,
setBookmarkFilter, setBookmarkFilter,
currentTheme, currentTheme,

View File

@@ -9,17 +9,31 @@ function Applications({ contentType }: any) {
const { data: apps, isFetching: isAppsFetching } = useQueryAPI({ const { data: apps, isFetching: isAppsFetching } = useQueryAPI({
endpoint: 'api/apps', endpoint: 'api/apps',
}); });
const { isMobile } = useCommonHooks(); const { isMobile, appSort } = useCommonHooks();
const [applicationData, setApplicationData] = useState<Application[]>([]); const [applicationData, setApplicationData] = useState<Application[]>([]);
useEffect(() => { useEffect(() => {
if (apps && !isAppsFetching) { if (apps && !isAppsFetching) {
setApplicationData( setApplicationData(
apps.sort((a: Application, b: Application) => (a.name > b.name ? 1 : -1)), apps
.filter((a: Application) => a.enabled === 1)
.sort((a: any, b: any) => {
switch (appSort) {
case 'A>Z':
return a.name > b.name ? 1 : -1;
case 'Z>A':
return a.name < b.name ? 1 : -1;
case 'Newest':
return a.id < b.id ? 1 : -1;
case 'Oldest':
return a.id > b.id ? 1 : -1;
case 'Custom':
return a.index > b.index ? 1 : -1;
}
}),
); );
} }
}, [apps, isAppsFetching]); }, [apps, isAppsFetching, appSort]);
return ( return (
<> <>
{applicationData.length > 0 && contentType.length > 0 && ( {applicationData.length > 0 && contentType.length > 0 && (
@@ -39,11 +53,13 @@ function Applications({ contentType }: any) {
gridTemplateRows: '64px', gridTemplateRows: '64px',
}} }}
> >
{applicationData?.map((application: any) => { {applicationData
if ( ?.filter(
(application: Application) =>
contentType.includes(application.filterName) || contentType.includes(application.filterName) ||
contentType.includes('All') contentType.includes('All'),
) )
.map((application: any) => {
return ( return (
<AppItem <AppItem
key={application.name} key={application.name}

View File

@@ -12,7 +12,7 @@ function Links({ contentType }: any) {
const { data: groupData, isFetched: groupsIsFetched } = useQueryAPI({ const { data: groupData, isFetched: groupsIsFetched } = useQueryAPI({
endpoint: 'api/categories', endpoint: 'api/categories',
}); });
const { isMobile } = useCommonHooks(); const { isMobile, bookmarkSort } = useCommonHooks();
const [bookmarkData, setBookmarkData] = useState<Bookmark[]>([]); const [bookmarkData, setBookmarkData] = useState<Bookmark[]>([]);
const [groups, setGroups] = useState<BookmarkGroup[]>([]); const [groups, setGroups] = useState<BookmarkGroup[]>([]);
@@ -28,12 +28,23 @@ function Links({ contentType }: any) {
return category; return category;
}); });
setGroups( setGroups(
groupList.sort((a: BookmarkGroup, b: BookmarkGroup) => groupList.sort((a: any, b: any) => {
a.categoryIndex > b.categoryIndex ? 1 : -1, switch (bookmarkSort) {
), case 'A>Z':
return a.categoryName > b.categoryName ? 1 : -1;
case 'Z>A':
return a.categoryName < b.categoryName ? 1 : -1;
case 'Newest':
return a.id < b.id ? 1 : -1;
case 'Oldest':
return a.id > b.id ? 1 : -1;
case 'Custom':
return a.index > b.index ? 1 : -1;
}
}),
); );
} }
}, [groupData, groupsIsFetched]); }, [groupData, groupsIsFetched, bookmarkSort]);
return ( return (
<> <>
@@ -55,11 +66,14 @@ function Links({ contentType }: any) {
gridTemplateRows: 'auto', gridTemplateRows: 'auto',
}} }}
> >
{groups.map((category: BookmarkGroup) => { {groups
if ( ?.filter(
(category: BookmarkGroup) =>
contentType.includes(category.filterName) || contentType.includes(category.filterName) ||
contentType.includes('All') contentType.includes('All'),
) { )
.map((category: BookmarkGroup) => {
return ( return (
<LinkGroups <LinkGroups
key={category.categoryName} key={category.categoryName}
@@ -70,7 +84,6 @@ function Links({ contentType }: any) {
)} )}
/> />
); );
}
})} })}
</Stack> </Stack>
</Stack> </Stack>

View File

@@ -53,6 +53,10 @@ function SettingsButton() {
theme, theme,
appFilter, appFilter,
setAppFilter, setAppFilter,
appSort,
setAppSort,
bookmarkSort,
setBookmarkSort,
bookmarkFilter, bookmarkFilter,
setBookmarkFilter, setBookmarkFilter,
isMobile, isMobile,
@@ -330,6 +334,57 @@ function SettingsButton() {
/> />
</Group> </Group>
</Stack> </Stack>
<Menu.Divider
w={'85%'}
m={'.75rem'}
style={{ borderColor: theme.accentColor }}
/>
<Stack
gap={0}
align='center'
w={'85%'}
h={'100px'}
className={stackClasses.menuStack}
classNames={stackClasses}
>
<Menu.Label>Sorting</Menu.Label>
<Group justify='space-between' px={'.5rem'}>
<Select
label={'Apps'}
placeholder='Sort Applications...'
value={appSort}
data={['Custom', 'A>Z', 'Z>A', 'Newest', 'Oldest']}
w={'45%'}
classNames={{
input: selectClasses.menuSelectInput,
label: selectClasses.menuSelectLabel,
options: selectClasses.menuSelectOptions,
option: selectClasses.menuSelectOption,
}}
comboboxProps={{ withinPortal: false }}
onChange={(_value, option: any) => {
setAppSort(option.value);
}}
/>
<Select
label={'Bookmarks'}
placeholder='Sort Bookmarks...'
value={bookmarkSort}
data={['Custom', 'A>Z', 'Z>A', 'Newest', 'Oldest']}
w={'45%'}
classNames={{
input: selectClasses.menuSelectInput,
label: selectClasses.menuSelectLabel,
options: selectClasses.menuSelectOptions,
option: selectClasses.menuSelectOption,
}}
comboboxProps={{ withinPortal: false }}
onChange={(_value, option: any) => {
setBookmarkSort(option.value);
}}
/>
</Group>
</Stack>
</Stack> </Stack>
</Menu.Dropdown> </Menu.Dropdown>
</Menu> </Menu>

View File

@@ -1,14 +1,30 @@
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { Box, Stack, Text } from '@mantine/core'; import { ActionIcon, Box, Stack, Text, Tooltip } from '@mantine/core';
import { useCommonHooks } from 'hooks'; import { useCommonHooks } from 'hooks';
import { useState } from 'react'; import { useState } from 'react';
import { boxClasses, textClasses } from 'styles'; import { FiAlertCircle } from 'react-icons/fi';
import { actionIconClasses, boxClasses, textClasses } from 'styles';
function AppSettingsItem({ url, icon, name }: any) { function AppSettingsItem({ url, icon, name, enabled }: 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, theme } = useCommonHooks();
return ( return (
<>
{enabled === 0 && (
<Tooltip label={<Text c={theme.primaryColor}>This app is disabled</Text>}>
<ActionIcon
className={actionIconClasses.settingsTooltip}
classNames={actionIconClasses}
pos={'absolute'}
size={'xl'}
variant='subtle'
style={{ zIndex: 1 }}
>
<FiAlertCircle color='red' />
</ActionIcon>
</Tooltip>
)}
<Box <Box
component='a' component='a'
onMouseOver={() => { onMouseOver={() => {
@@ -69,6 +85,7 @@ function AppSettingsItem({ url, icon, name }: any) {
</Text> </Text>
</Stack> </Stack>
</Box> </Box>
</>
); );
} }

View File

@@ -1,3 +1,4 @@
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
import { import {
ActionIcon, ActionIcon,
Box, Box,
@@ -11,6 +12,7 @@ import {
Modal, Modal,
Popover, Popover,
Stack, Stack,
Switch,
Text, Text,
TextInput, TextInput,
} from '@mantine/core'; } from '@mantine/core';
@@ -36,19 +38,50 @@ const ApplicationSettings = () => {
endpoint: endpoint, endpoint: endpoint,
}); });
const [applicationData, setApplicationData] = useState<Application[]>([]); const [applicationData, setApplicationData] = useState<Application[]>([]);
const [applicationList, setApplicationList] = useState<Application[]>([]);
const { theme, isMobile, modalStyle } = useCommonHooks(); const { theme, isMobile, modalStyle } = useCommonHooks();
const [newApplication, setNewApplication] = useState<Application>({ const [newApplication, setNewApplication] = useState<Application>({
id: undefined, id: null,
name: '', name: '',
url: '', url: '',
icon: '', icon: '',
baseUrl: '', baseUrl: '',
filterName: '', filterName: '',
index: 0, index: 0,
enabled: 1,
}); });
const { mutateAsync } = useMutationAPI(); const { mutateAsync } = useMutationAPI();
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500); const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const [dragging, setDragging] = useState(false);
const reorder = (list: any, startIndex: any, endIndex: any) => {
const result = Array.from(list);
const [removed]: any = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};
function onDragEnd(result: any) {
if (!result.destination) {
return;
}
if (result.destination.index === result.source.index) {
return;
}
const items = reorder(applicationData, result.source.index, result.destination.index);
setApplicationList(
items.map((item: any, index: number) => {
return {
...item,
index: index,
};
}),
);
}
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false); const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
useEffect(() => { useEffect(() => {
@@ -58,11 +91,21 @@ const ApplicationSettings = () => {
.filter((a: Application) => .filter((a: Application) =>
a.name.toLowerCase().includes(debouncedSearch.toLowerCase()), a.name.toLowerCase().includes(debouncedSearch.toLowerCase()),
) )
.sort((a: Application, b: Application) => (a.name > b.name ? 1 : -1)), .sort((a: Application, b: Application) => (a.index > b.index ? 1 : -1)),
); );
} }
}, [apps, isAppsFetching, debouncedSearch]); }, [apps, isAppsFetching, debouncedSearch]);
useEffect(() => {
if (applicationList.length > 0 && applicationList !== applicationData) {
mutateAsync({
method: 'PATCH',
endpoint: 'api/applicationOrder',
body: applicationList,
});
}
}, [applicationList]);
return ( return (
<Stack w={'100%'}> <Stack w={'100%'}>
<Modal.Root <Modal.Root
@@ -266,7 +309,104 @@ const ApplicationSettings = () => {
setSearchValue(event.target.value); setSearchValue(event.target.value);
}} }}
/> />
<Switch
label={<Text c={theme.primaryColor}>Reorder</Text>}
labelPosition='left'
pos={'absolute'}
right={isMobile ? '2rem' : '1rem'}
top={isMobile ? '10rem' : undefined}
onChange={(e) => {
setDragging(e.target.checked);
}}
color={theme.primaryColor}
styles={{
label: {
color: theme.primaryColor,
top: isMobile ? -22 : 0,
left: isMobile ? -8 : 0,
position: isMobile ? 'absolute' : undefined,
},
}}
/>
</Group> </Group>
{dragging ? (
<DragDropContext onDragEnd={onDragEnd}>
<Stack w={'100%'} align='center'>
<Droppable droppableId={'groupList'}>
{(provided) => (
<Stack
w={'75%'}
align='center'
justify='center'
ref={provided.innerRef}
{...provided.droppableProps}
>
{applicationData?.map((app: Application) => (
<Draggable
key={app.id}
draggableId={`${app.id}`}
index={Number(app.index)}
>
{(provided) => (
<Box
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<Card
w={'400px'}
style={{
backgroundColor: theme.searchColor,
border: '1px solid var(--color-border)',
borderRadius: '8px',
}}
>
<Group gap={0} align='center' justify='space-between'>
<Stack gap={0}>
<Text c={theme.accentColor} fz={14}>
Name
</Text>
<Text
p={4}
style={{
borderRadius: '4px',
paddingLeft: '8px',
color: theme.primaryColor,
backgroundColor: theme.searchColor,
}}
>
{app.name}
</Text>
</Stack>
<Stack gap={0}>
<Text c={theme.accentColor} fz={14}>
Filter Name
</Text>
<Text
p={4}
style={{
borderRadius: '4px',
paddingLeft: '8px',
color: theme.primaryColor,
backgroundColor: theme.searchColor,
}}
>
{app.filterName}
</Text>
</Stack>
</Group>
</Card>
</Box>
)}
</Draggable>
))}
{provided.placeholder}
</Stack>
)}
</Droppable>
</Stack>
</DragDropContext>
) : (
<Stack w={'100%'} align='center'> <Stack w={'100%'} align='center'>
<Grid columns={18} w={'75%'}> <Grid columns={18} w={'75%'}>
{applicationData?.map((application: Application) => ( {applicationData?.map((application: Application) => (
@@ -282,6 +422,7 @@ const ApplicationSettings = () => {
url={application.url} url={application.url}
icon={application.icon} icon={application.icon}
name={application.name} name={application.name}
enabled={application.enabled}
/> />
</Box> </Box>
</Popover.Target> </Popover.Target>
@@ -293,6 +434,7 @@ const ApplicationSettings = () => {
))} ))}
</Grid> </Grid>
</Stack> </Stack>
)}
</Stack> </Stack>
); );
}; };

View File

@@ -5,6 +5,7 @@ import {
Group, Group,
Modal, Modal,
Stack, Stack,
Switch,
Text, Text,
TextInput, TextInput,
Tooltip, Tooltip,
@@ -36,7 +37,6 @@ function DebouncedApplication({ application }: { application: Application }) {
setApp(update); setApp(update);
} }
}, [update]); }, [update]);
return ( return (
<Card className={cardClasses.settingsItem} classNames={cardClasses}> <Card className={cardClasses.settingsItem} classNames={cardClasses}>
<Modal.Root <Modal.Root
@@ -114,6 +114,27 @@ function DebouncedApplication({ application }: { application: Application }) {
<IconTrash /> <IconTrash />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Tooltip
color={theme.primaryColor}
bd={`solid 1px ${theme.borderColor}`}
label={'Enabled'}
>
<Switch
label={<Text c={theme.primaryColor}>Enabled</Text>}
labelPosition='left'
pos={'absolute'}
left={16}
defaultChecked={app.enabled === 1}
defaultValue={app.enabled}
onChange={(event) => {
setUpdate({
...app,
enabled: event.target.checked ? 1 : 0,
});
}}
color={theme.primaryColor}
/>
</Tooltip>
<Group w={'100%'} justify='center'> <Group w={'100%'} justify='center'>
<TextInput <TextInput
label={'Name'} label={'Name'}

View File

@@ -50,7 +50,7 @@
.inputField { .inputField {
color: var(--color-text-acc) !important; color: var(--color-text-acc) !important;
&::placeholder { &::placeholder {
color: var(--color-text-acc) !important; color: var(--color-text-pri) !important;
} }
} }

View File

@@ -1,11 +1,12 @@
export interface Application { export interface Application {
id: number | undefined; id: number | null;
name: string; name: string;
url: string; url: string;
icon: string; icon: string;
baseUrl: string; baseUrl: string;
filterName: string; filterName: string;
index: number; index: number;
enabled: number;
} }
export interface AppCategory { export interface AppCategory {