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(
contentType.includes(application.filterName) || (application: Application) =>
contentType.includes('All') contentType.includes(application.filterName) ||
contentType.includes('All'),
) )
.map((application: any) => {
return ( return (
<AppItem <AppItem
key={application.name} key={application.name}
@@ -53,7 +69,7 @@ function Applications({ contentType }: any) {
category={application.category} category={application.category}
/> />
); );
})} })}
</Stack> </Stack>
</Stack> </Stack>
)} )}

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(
contentType.includes(category.filterName) || (category: BookmarkGroup) =>
contentType.includes('All') contentType.includes(category.filterName) ||
) { contentType.includes('All'),
)
.map((category: BookmarkGroup) => {
return ( return (
<LinkGroups <LinkGroups
key={category.categoryName} key={category.categoryName}
@@ -70,8 +84,7 @@ 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,74 +1,91 @@
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 (
<Box <>
component='a' {enabled === 0 && (
onMouseOver={() => { <Tooltip label={<Text c={theme.primaryColor}>This app is disabled</Text>}>
setTextDecoration('underline'); <ActionIcon
setHover(true); className={actionIconClasses.settingsTooltip}
}} classNames={actionIconClasses}
onMouseOut={() => { pos={'absolute'}
setTextDecoration('none'); size={'xl'}
setHover(false); variant='subtle'
}} style={{ zIndex: 1 }}
className={boxClasses.appItemBox} >
> <FiAlertCircle color='red' />
{hover && ( </ActionIcon>
<svg </Tooltip>
style={{ position: 'absolute', borderRadius: '6px' }}
height='100%'
width='100%'
xmlns='http://www.w3.org/2000/svg'
>
<rect rx='16' ry='16' height='100%' width='100%' strokeLinejoin='round' />
</svg>
)} )}
<Stack <Box
align={isMobile ? 'center' : undefined} component='a'
justify={isMobile ? 'center' : undefined} onMouseOver={() => {
style={{ setTextDecoration('underline');
height: '128px', setHover(true);
width: isMobile ? '100%' : undefined,
marginRight: isMobile ? '0' : '1em',
marginLeft: isMobile ? '0' : '2em',
paddingTop: isMobile ? '0' : '32px',
}} }}
> onMouseOut={() => {
<Icon icon={`mdi:${icon}`} height={64} width={64} /> setTextDecoration('none');
</Stack> setHover(false);
<Stack
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
flex: 1,
overflow: 'hidden',
gap: 0,
}} }}
className={boxClasses.appItemBox}
> >
<Text fz={22} className={textClasses.appName} classNames={textClasses}> {hover && (
{name} <svg
</Text> style={{ position: 'absolute', borderRadius: '6px' }}
<Text height='100%'
fz={16} width='100%'
className={textClasses.appUrl} xmlns='http://www.w3.org/2000/svg'
classNames={textClasses} >
<rect rx='16' ry='16' height='100%' width='100%' strokeLinejoin='round' />
</svg>
)}
<Stack
align={isMobile ? 'center' : undefined}
justify={isMobile ? 'center' : undefined}
style={{ style={{
textDecoration: textDecoration, height: '128px',
width: isMobile ? '100%' : undefined,
marginRight: isMobile ? '0' : '1em',
marginLeft: isMobile ? '0' : '2em',
paddingTop: isMobile ? '0' : '32px',
}} }}
> >
{url} <Icon icon={`mdi:${icon}`} height={64} width={64} />
</Text> </Stack>
</Stack> <Stack
</Box> style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
flex: 1,
overflow: 'hidden',
gap: 0,
}}
>
<Text fz={22} className={textClasses.appName} classNames={textClasses}>
{name}
</Text>
<Text
fz={16}
className={textClasses.appUrl}
classNames={textClasses}
style={{
textDecoration: textDecoration,
}}
>
{url}
</Text>
</Stack>
</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,33 +309,132 @@ 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>
<Stack w={'100%'} align='center'> {dragging ? (
<Grid columns={18} w={'75%'}> <DragDropContext onDragEnd={onDragEnd}>
{applicationData?.map((application: Application) => ( <Stack w={'100%'} align='center'>
<GridCol span={6} key={application.id}> <Droppable droppableId={'groupList'}>
<Popover withArrow withOverlay withinPortal={false}> {(provided) => (
<Popover.Target> <Stack
<Box w={'75%'}
onClick={(e) => { align='center'
e.stopPropagation(); justify='center'
}} ref={provided.innerRef}
> {...provided.droppableProps}
<AppSettingsItem >
url={application.url} {applicationData?.map((app: Application) => (
icon={application.icon} <Draggable
name={application.name} key={app.id}
/> draggableId={`${app.id}`}
</Box> index={Number(app.index)}
</Popover.Target> >
<Popover.Dropdown> {(provided) => (
<DebouncedApplication application={application} /> <Box
</Popover.Dropdown> ref={provided.innerRef}
</Popover> {...provided.draggableProps}
</GridCol> {...provided.dragHandleProps}
))} >
</Grid> <Card
</Stack> 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'>
<Grid columns={18} w={'75%'}>
{applicationData?.map((application: Application) => (
<GridCol span={6} key={application.id}>
<Popover withArrow withOverlay withinPortal={false}>
<Popover.Target>
<Box
onClick={(e) => {
e.stopPropagation();
}}
>
<AppSettingsItem
url={application.url}
icon={application.icon}
name={application.name}
enabled={application.enabled}
/>
</Box>
</Popover.Target>
<Popover.Dropdown>
<DebouncedApplication application={application} />
</Popover.Dropdown>
</Popover>
</GridCol>
))}
</Grid>
</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 {