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'] });
endpoint === 'api/categoryOrder' &&
queryClient.refetchQueries({ queryKey: ['GET', 'api/categories'] });
endpoint === 'api/applicationOrder' &&
queryClient.refetchQueries({ queryKey: ['GET', 'api/applicationOrder'] });
},
onSuccess: (data, { method, endpoint }) => {
queryClient.refetchQueries({
@@ -141,6 +143,8 @@ export function useMutationAPI() {
queryClient.refetchQueries({ queryKey: ['GET', 'api/links'] });
endpoint === 'api/categoryOrder' &&
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',
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]>({
key: 'bookmarkFilter',
defaultValue: ['All'],
@@ -114,6 +122,10 @@ export function useCommonHooks() {
setThemeName,
appFilter,
setAppFilter,
appSort,
setAppSort,
bookmarkSort,
setBookmarkSort,
bookmarkFilter,
setBookmarkFilter,
currentTheme,

View File

@@ -9,17 +9,31 @@ function Applications({ contentType }: any) {
const { data: apps, isFetching: isAppsFetching } = useQueryAPI({
endpoint: 'api/apps',
});
const { isMobile } = useCommonHooks();
const { isMobile, appSort } = useCommonHooks();
const [applicationData, setApplicationData] = useState<Application[]>([]);
useEffect(() => {
if (apps && !isAppsFetching) {
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 (
<>
{applicationData.length > 0 && contentType.length > 0 && (
@@ -39,11 +53,13 @@ function Applications({ contentType }: any) {
gridTemplateRows: '64px',
}}
>
{applicationData?.map((application: any) => {
if (
{applicationData
?.filter(
(application: Application) =>
contentType.includes(application.filterName) ||
contentType.includes('All')
contentType.includes('All'),
)
.map((application: any) => {
return (
<AppItem
key={application.name}

View File

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

View File

@@ -53,6 +53,10 @@ function SettingsButton() {
theme,
appFilter,
setAppFilter,
appSort,
setAppSort,
bookmarkSort,
setBookmarkSort,
bookmarkFilter,
setBookmarkFilter,
isMobile,
@@ -330,6 +334,57 @@ function SettingsButton() {
/>
</Group>
</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>
</Menu.Dropdown>
</Menu>

View File

@@ -1,14 +1,30 @@
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 { 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 [hover, setHover] = useState<boolean>(false);
const { isMobile } = useCommonHooks();
const { isMobile, theme } = useCommonHooks();
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
component='a'
onMouseOver={() => {
@@ -69,6 +85,7 @@ function AppSettingsItem({ url, icon, name }: any) {
</Text>
</Stack>
</Box>
</>
);
}

View File

@@ -1,3 +1,4 @@
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
import {
ActionIcon,
Box,
@@ -11,6 +12,7 @@ import {
Modal,
Popover,
Stack,
Switch,
Text,
TextInput,
} from '@mantine/core';
@@ -36,19 +38,50 @@ const ApplicationSettings = () => {
endpoint: endpoint,
});
const [applicationData, setApplicationData] = useState<Application[]>([]);
const [applicationList, setApplicationList] = useState<Application[]>([]);
const { theme, isMobile, modalStyle } = useCommonHooks();
const [newApplication, setNewApplication] = useState<Application>({
id: undefined,
id: null,
name: '',
url: '',
icon: '',
baseUrl: '',
filterName: '',
index: 0,
enabled: 1,
});
const { mutateAsync } = useMutationAPI();
const [searchValue, setSearchValue] = useState('');
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);
useEffect(() => {
@@ -58,11 +91,21 @@ const ApplicationSettings = () => {
.filter((a: Application) =>
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]);
useEffect(() => {
if (applicationList.length > 0 && applicationList !== applicationData) {
mutateAsync({
method: 'PATCH',
endpoint: 'api/applicationOrder',
body: applicationList,
});
}
}, [applicationList]);
return (
<Stack w={'100%'}>
<Modal.Root
@@ -266,7 +309,104 @@ const ApplicationSettings = () => {
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>
{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'>
<Grid columns={18} w={'75%'}>
{applicationData?.map((application: Application) => (
@@ -282,6 +422,7 @@ const ApplicationSettings = () => {
url={application.url}
icon={application.icon}
name={application.name}
enabled={application.enabled}
/>
</Box>
</Popover.Target>
@@ -293,6 +434,7 @@ const ApplicationSettings = () => {
))}
</Grid>
</Stack>
)}
</Stack>
);
};

View File

@@ -5,6 +5,7 @@ import {
Group,
Modal,
Stack,
Switch,
Text,
TextInput,
Tooltip,
@@ -36,7 +37,6 @@ function DebouncedApplication({ application }: { application: Application }) {
setApp(update);
}
}, [update]);
return (
<Card className={cardClasses.settingsItem} classNames={cardClasses}>
<Modal.Root
@@ -114,6 +114,27 @@ function DebouncedApplication({ application }: { application: Application }) {
<IconTrash />
</ActionIcon>
</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'>
<TextInput
label={'Name'}

View File

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

View File

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