Files
TriggerFrontend/src/pages/settings/components/Bookmarks/DebouncedGroupSettingsItem.tsx
Sean Edmunds c8d4922ca4 Style updates
2025-10-14 16:17:10 -05:00

445 lines
12 KiB
TypeScript

import {
DragDropContext,
Draggable,
type DraggableProvided,
Droppable,
type DroppableProvided,
} from '@hello-pangea/dnd';
import {
ActionIcon,
Box,
Button,
Card,
Group,
Popover,
Stack,
Text,
TextInput,
} from '@mantine/core';
import { isNotEmpty, useForm } from '@mantine/form';
import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
import { DeleteItemModal, ManagementPill } from 'components';
import { useCommonHooks, useMutationAPI } from 'hooks';
import { useEffect, useState } from 'react';
import { FaChevronLeft } from 'react-icons/fa';
import { FiPlus } from 'react-icons/fi';
import {
buttonClasses,
cardClasses,
popoverClasses,
textClasses,
textInputClasses,
} from 'styles';
import type { Bookmark, BookmarkGroup } from 'types';
import BookmarkSettingsItem from './BookmarkSettingsItem';
import DebouncedBookmarkItem from './DebouncedBookmarkItem';
function DebouncedGroupSettingsItem({
bookmarkGroup,
bookmarks,
searchTerm,
}: {
bookmarkGroup: BookmarkGroup;
bookmarks: Bookmark[];
searchTerm: string;
}) {
const { isMobile, handleDelete, reorder, theme } = useCommonHooks();
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const [popoverOpened, setPopoverOpened] = useState(false);
const { mutateAsync } = useMutationAPI();
const [group, setGroup] = useState<BookmarkGroup>(bookmarkGroup);
const [update, setUpdate] = useState<any>({});
const [debounced] = useDebouncedValue(update, 1000);
const [newBookmark, setNewBookmark] = useState<Bookmark>({
id: undefined,
name: '',
url: '',
category: group.categoryName,
index: 0,
enabled: 1,
});
const form = useForm({
initialValues: newBookmark,
validate: {
name: isNotEmpty('Name is required'),
url: isNotEmpty('URL is required'),
},
});
const [bookmarkList, setBookmarkList] = useState<Bookmark[]>(bookmarks);
const [isOpen, setIsOpen] = useState(false);
function onDragEnd(result: any) {
if (!result.destination) {
return;
}
if (result.destination.index === result.source.index) {
return;
}
const items = reorder(bookmarkList, result.source.index, result.destination.index);
setBookmarkList(
items.map((item: any, index: number) => {
return {
...item,
index: index,
};
}),
);
}
useEffect(() => {
if (Object.keys(debounced).length) {
mutateAsync({
method: 'PATCH',
endpoint: 'api/bookmarkCategories',
body: debounced,
});
setUpdate({});
}
}, [debounced]);
useEffect(() => {
if (update !== group && Object.keys(update).length) {
setGroup(update);
}
}, [update]);
useEffect(() => {
if (bookmarkList && bookmarkList !== bookmarks) {
mutateAsync({ method: 'PATCH', endpoint: 'api/bookmarkOrder', body: bookmarkList });
}
}, [bookmarkList]);
return (
<Card w={'400px'} className={cardClasses.settingsItem} classNames={cardClasses}>
<DeleteItemModal
modalOpened={modalOpened}
handleClose={closeModal}
handleDelete={() => {
bookmarks.length > 0 && handleDelete(bookmarks, 'api/bookmarks');
handleDelete(group, 'api/bookmarkCategories');
}}
title='Delete message'
body={
<Stack gap={0} w={'100%'} h={'175px'} align='center'>
<Text>Are you sure you want to delete this bookmark group?</Text>
<Text
className={textClasses.deleteItem}
classNames={textClasses}
>{`"${group.categoryName}"`}</Text>
<Text style={{ fontWeight: 'bold', textTransform: 'uppercase' }}>
This will also delete all bookmarks in this group!
</Text>
<Text c='red'>This action cannot be undone!</Text>
</Stack>
}
/>
<Card.Section
p={'1rem'}
pt={0}
bg={'var(--mantine-color-search-0)'}
c={'var(--mantine-color-background-0)'}
mt={'-1.5rem'}
>
<Stack gap={0}>
<Group w={'100%'} gap={8} align='end' justify='center' pos={'relative'}>
<TextInput
label={'Name'}
w={isMobile ? undefined : '56%'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
defaultValue={group.categoryName}
onChange={(event) => {
setUpdate({
...group,
categoryName: event.target.value,
});
}}
styles={
theme.textShadowsEnabled
? {
label: { boxShadow: '2px 2px var(--mantine-color-border-0)' },
}
: undefined
}
/>
<TextInput
label={'Filter Name'}
w={isMobile ? undefined : '28%'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
defaultValue={group.filterName}
onChange={(event) => {
setUpdate({
...group,
filterName: event.target.value,
});
}}
styles={
theme.textShadowsEnabled
? {
label: { boxShadow: '2px 2px var(--mantine-color-border-0)' },
}
: undefined
}
/>
<Text
style={{
width: '30px',
height: '30px',
borderRadius: '100%',
backgroundColor: 'var(--mantine-color-accent-0)',
fontSize: '1.25rem',
textAlign: 'center',
textJustify: 'auto',
marginBottom: '.5rem',
marginLeft: '.5rem',
}}
>
{bookmarks.length}
</Text>
</Group>
</Stack>
</Card.Section>
<Card.Section mt={'1rem'} p={'1rem'}>
<Stack pos={'relative'}>
<Group
style={{
position: 'absolute',
width: '100%',
right: '6rem',
top: '-3.25rem',
}}
>
<Box
style={{
position: 'absolute',
right: '1rem',
top: '.5rem',
}}
>
<ManagementPill
enabled={group.enabled}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setUpdate({
...group,
enabled: event.target.checked ? 1 : 0,
});
}}
openModal={openModal}
/>
</Box>
</Group>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId={`${bookmarkGroup.id}`}>
{(provided: DroppableProvided) => (
<Stack
pos={'relative'}
ref={provided.innerRef}
{...provided.droppableProps}
>
{bookmarks.map((bookmark: Bookmark) => (
<Draggable
key={bookmark.id}
draggableId={`${bookmark.id}`}
index={bookmark.index}
>
{(provided: DraggableProvided) => (
<Box
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<Popover
withArrow
withOverlay
trapFocus
closeOnClickOutside={!isOpen}
classNames={popoverClasses}
middlewares={{ shift: { padding: 20 } }}
position='bottom'
>
<Popover.Target>
<Box pos={'relative'}>
{(bookmark.name.toLowerCase().includes(searchTerm) ||
bookmark.url.toLowerCase().includes(searchTerm)) &&
searchTerm && (
<ActionIcon
variant='subtle'
style={{
position: 'absolute',
top: 6,
left: -12,
color: 'red',
backgroundColor: 'transparent',
}}
>
<FaChevronLeft
style={{ transform: 'rotate(180deg)' }}
/>
</ActionIcon>
)}
<BookmarkSettingsItem bookmark={bookmark} />
</Box>
</Popover.Target>
<Popover.Dropdown w={isMobile ? '90%' : undefined}>
<DebouncedBookmarkItem
bookmarkItem={bookmark}
setIsOpen={setIsOpen}
/>
</Popover.Dropdown>
</Popover>
</Box>
)}
</Draggable>
))}
{provided.placeholder}
</Stack>
)}
</Droppable>
</DragDropContext>
</Stack>
</Card.Section>
<Card.Section
p={'.5rem'}
bg={'var(--mantine-color-search-0)'}
c={'var(--mantine-color-background-0)'}
>
<Group align='center' justify='center'>
<Popover
withArrow
withOverlay
trapFocus
position='bottom'
opened={popoverOpened}
onChange={setPopoverOpened}
closeOnEscape
classNames={popoverClasses}
>
<Popover.Target>
<Button
className={buttonClasses.newItemButton}
classNames={buttonClasses}
w={'min-content'}
onClick={() => {
setPopoverOpened((o) => !o);
setNewBookmark({
...newBookmark,
category: group.categoryName,
});
}}
>
<FiPlus /> Bookmark
</Button>
</Popover.Target>
<Popover.Dropdown w={isMobile ? '90%' : undefined}>
<Card className={cardClasses.settingsItem} classNames={cardClasses}>
<form
onSubmit={form.onSubmit((values) => {
mutateAsync({
method: 'POST',
endpoint: 'api/bookmarks',
body: { ...values, index: bookmarks.length },
});
setPopoverOpened(false);
form.reset();
})}
>
<Stack w={'100%'} display={'flex'} pos={'relative'}>
<Group w={'100%'} justify='center'>
<TextInput
key={form.key('name')}
{...form.getInputProps('name')}
label={'Name'}
placeholder='eg. Google'
className={textInputClasses.settingsItem}
pt={'1rem'}
classNames={textInputClasses}
withAsterisk
styles={
theme.textShadowsEnabled
? {
label: {
boxShadow: '2px 2px var(--mantine-color-border-0)',
},
}
: undefined
}
/>
<TextInput
key={form.key('category')}
{...form.getInputProps('category')}
label={'Category'}
className={textInputClasses.settingsItem}
pt={'1rem'}
classNames={textInputClasses}
disabled
defaultValue={group.categoryName}
styles={
theme.textShadowsEnabled
? {
label: {
boxShadow: '2px 2px var(--mantine-color-border-0)',
},
}
: undefined
}
/>
</Group>
<Group w={'100%'} justify='center' mb={60}>
<TextInput
key={form.key('url')}
{...form.getInputProps('url')}
label={'URL'}
placeholder='eg. https://google.com'
className={textInputClasses.settingsItem}
w={'100%'}
pt={'1rem'}
classNames={textInputClasses}
withAsterisk
styles={
theme.textShadowsEnabled
? {
label: {
boxShadow: '2px 2px var(--mantine-color-border-0)',
},
}
: undefined
}
/>
</Group>
<Group
w={'100%'}
pos={'absolute'}
bottom={8}
align='center'
justify='center'
>
<Button type='submit' color={'green'}>
Save
</Button>
<Button
color={'red'}
styles={{ label: { color: 'black' } }}
onClick={() => {
setPopoverOpened(false);
form.reset();
}}
>
Cancel
</Button>
</Group>
</Stack>
</form>
</Card>
</Popover.Dropdown>
</Popover>
</Group>
</Card.Section>
</Card>
);
}
export default DebouncedGroupSettingsItem;