Files
TriggerFrontend/src/pages/settings/components/Bookmarks/Bookmarks.tsx
2026-01-09 12:45:40 -06:00

341 lines
9.4 KiB
TypeScript

import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
import {
Box,
Button,
Card,
Group,
LoadingOverlay,
Stack,
Text,
TextInput,
} from '@mantine/core';
import { isNotEmpty, useForm } from '@mantine/form';
import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
import { AddItemModal, NoItemsFound, SettingsHeader } from 'components';
import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks';
import { useEffect, useState } from 'react';
import { buttonClasses, textInputClasses } from 'styles';
import type { Bookmark, BookmarkGroup } from 'types';
import DebouncedGroupSettingsItem from './DebouncedGroupSettingsItem';
const BookmarkSettings = () => {
const defaultGroup = {
id: null,
categoryName: '',
categoryIndex: 0,
filterName: '',
};
const form = useForm({
initialValues: defaultGroup,
validate: {
categoryName: isNotEmpty('Category name is required'),
filterName: isNotEmpty('Filter name is required'),
},
});
const bookmarksEndpoint = 'api/bookmarks';
const { data: bookmarkData, isFetching: bookmarkDataIsFetching } = useQueryAPI({
endpoint: bookmarksEndpoint,
});
const bookmarks: Bookmark[] = bookmarkData || [];
const categoriesEndpoint = 'api/bookmarkCategories';
const { data: groupData, isFetched: groupsIsFetched } = useQueryAPI({
endpoint: categoriesEndpoint,
});
const [dragging, setDragging] = useState(false);
const [groups, setGroups] = useState<BookmarkGroup[]>([]);
const [groupList, setGroupList] = useState<BookmarkGroup[]>([]);
const { isMobile, reorder, theme } = useCommonHooks();
const { mutateAsync } = useMutationAPI();
const [searchValue, setSearchValue] = useState<string | number>('');
const [debouncedGroupSearch] = useDebouncedValue(searchValue, 500);
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const [isLoading, setIsLoading] = useState(false);
const foundBookmarkGroups: BookmarkGroup[] = [
...new Set(
bookmarks
.filter(
(a: Bookmark) =>
a.name
.toLowerCase()
.includes((debouncedGroupSearch as string).toLowerCase()) ||
a.url.toLowerCase().includes((debouncedGroupSearch as string).toLowerCase()),
)
.map((a: Bookmark) =>
groupData.find(
(b: BookmarkGroup) =>
b.categoryName.toLowerCase() === a.category.toLowerCase(),
),
),
),
];
const foundGroups: BookmarkGroup[] = groupData.filter((a: BookmarkGroup) =>
a.categoryName.toLowerCase().includes((debouncedGroupSearch as string).toLowerCase()),
);
const handleClose = () => {
closeModal();
form.reset();
};
function onDragEnd(result: any) {
if (!result.destination) {
return;
}
if (result.destination.index === result.source.index) {
return;
}
const items = reorder(groups, result.source.index, result.destination.index);
setGroupList(
items.map((item: any, index: number) => {
return {
...item,
index: index,
};
}),
);
}
useEffect(() => {
if (groupData && groupsIsFetched) {
const groupList = [...new Set([...foundBookmarkGroups, ...foundGroups])].sort(
(a: BookmarkGroup, b: BookmarkGroup) =>
a.categoryIndex > b.categoryIndex ? 1 : -1,
);
setGroups(groupList);
}
if (debouncedGroupSearch === searchValue) {
setIsLoading(false);
}
}, [groupData, groupsIsFetched, debouncedGroupSearch]);
useEffect(() => {
if (groupList.length > 0 && groupList !== groups) {
mutateAsync({
method: 'PATCH',
endpoint: 'api/bookmarkCategoryOrder',
body: groupList,
});
}
}, [groupList]);
return (
<Stack>
<AddItemModal
modalOpened={modalOpened}
handleClose={handleClose}
title={'Add a new Bookmark Group'}
body={
<form
onSubmit={form.onSubmit((values) => {
mutateAsync({
method: 'POST',
endpoint: categoriesEndpoint,
body: {
...values,
categoryIndex: groups.length,
},
});
handleClose();
})}
>
<Stack gap={0} w={'100%'} align='center'>
<Group w={'100%'} justify='center'>
<TextInput
key={form.key('categoryName')}
{...form.getInputProps('categoryName')}
label={'Category Name'}
placeholder='eg. Shopping'
className={textInputClasses.settingsItem}
classNames={textInputClasses}
withAsterisk
styles={
theme.textShadowsEnabled
? {
label: { boxShadow: '2px 2px var(--mantine-color-border-0)' },
}
: undefined
}
/>
<TextInput
key={form.key('filterName')}
{...form.getInputProps('filterName')}
label={'Filter Name'}
placeholder='eg. Personal'
className={textInputClasses.settingsItem}
classNames={textInputClasses}
withAsterisk
styles={
theme.textShadowsEnabled
? {
label: { boxShadow: '2px 2px var(--mantine-color-border-0)' },
}
: undefined
}
/>
</Group>
<Group mt={'1rem'}>
<Button
type='submit'
className={buttonClasses.saveButton}
classNames={buttonClasses}
>
Save
</Button>
<Button
className={buttonClasses.cancelButton}
classNames={buttonClasses}
onClick={handleClose}
>
Cancel
</Button>
</Group>
</Stack>
</form>
}
/>
<SettingsHeader
setDragging={groups.length > 0 ? setDragging : undefined}
openModal={openModal}
title={'Bookmarks'}
tooltip={{
title: 'Bookmarks are the links that will be displayed on the homepage.',
highlightList: [
'Bookmark',
'Bookmarks',
'Group',
'Groups',
'Link',
'Links',
'Category',
'Categories',
],
description:
'Bookmarks can only belong to a single category, and will be displayed under that category on the homepage.',
}}
searchValue={searchValue}
setSearchValue={setSearchValue}
searchPlaceholder={'Search Bookmarks...'}
isLoading={isLoading}
setIsLoading={setIsLoading}
/>
{dragging ? (
<DragDropContext onDragEnd={onDragEnd}>
<Stack
mt={isMobile ? '10rem' : '6rem'}
mb={isMobile ? '6rem' : '1rem'}
align='center'
>
<Droppable droppableId={'groupList'}>
{(provided) => (
<Stack
w={'75%'}
align='center'
justify='center'
ref={provided.innerRef}
{...provided.droppableProps}
>
{groups?.map((group: BookmarkGroup) => (
<Draggable
key={group.id}
draggableId={`${group.id}`}
index={Number(group.categoryIndex)}
>
{(provided) => (
<Box
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<Card
w={'400px'}
style={{
backgroundColor: 'var(--mantine-color-search-0)',
border: '1px solid var(--mantine-color-border-0)',
borderRadius: '8px',
}}
>
<Group gap={0} align='center' justify='space-between'>
<Stack gap={0}>
<Text c={'var(--mantine-color-accent-0)'} fz={'1rem'}>
Name
</Text>
<Text
p={'.25rem'}
style={{
borderRadius: '4px',
paddingLeft: '.5rem',
color: 'var(--mantine-color-primary-0)',
backgroundColor: 'var(--mantine-color-search-0)',
}}
>
{group.categoryName}
</Text>
</Stack>
<Stack gap={0}>
<Text c={'var(--mantine-color-accent-0)'} fz={'1rem'}>
Filter Name
</Text>
<Text
p={'.25rem'}
style={{
borderRadius: '4px',
paddingLeft: '.5rem',
color: 'var(--mantine-color-primary-0)',
backgroundColor: 'var(--mantine-color-search-0)',
}}
>
{group.filterName}
</Text>
</Stack>
</Group>
</Card>
</Box>
)}
</Draggable>
))}
{provided.placeholder}
</Stack>
)}
</Droppable>
</Stack>
</DragDropContext>
) : (
<Stack
mt={isMobile ? '10rem' : '6rem'}
mb={isMobile ? '6rem' : '1rem'}
align='center'
>
<LoadingOverlay
visible={bookmarkDataIsFetching}
zIndex={180}
overlayProps={{ blur: 5 }}
loaderProps={{
color: 'var(--mantine-color-primary-0)',
type: 'bars',
size: 'xl',
}}
/>
<Group w={isMobile ? undefined : '80%'} align='start' justify='center'>
{groups.length > 0 ? (
groups?.map((group: BookmarkGroup) => (
<DebouncedGroupSettingsItem
key={group.id}
bookmarkGroup={group}
bookmarks={bookmarks
.filter((a: Bookmark) => a.category === group.categoryName)
.sort((a: Bookmark, b: Bookmark) => (a.index > b.index ? 1 : -1))}
searchTerm={searchValue}
/>
))
) : (
<NoItemsFound />
)}
</Group>
</Stack>
)}
</Stack>
);
};
export default BookmarkSettings;