This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

semble / src / webapp / features / collections / components / collectionSelector / CollectionSelector.tsx
7.6 kB 234 lines
1'use client'; 2 3import { 4 ScrollArea, 5 Stack, 6 TextInput, 7 Text, 8 Alert, 9 Loader, 10 CloseButton, 11 Button, 12 Group, 13 Divider, 14 FocusTrap, 15} from '@mantine/core'; 16import { Fragment, useState } from 'react'; 17import { useDebouncedValue } from '@mantine/hooks'; 18import useMyCollections from '../../lib/queries/useMyCollections'; 19import useCollectionSearch from '../../lib/queries/useCollectionSearch'; 20import CollectionSelectorItemList from '../collectionSelectorItemList/CollectionSelectorItemList'; 21import CreateCollectionDrawer from '@/features/collections/components/createCollectionDrawer/CreateCollectionDrawer'; 22import CollectionSelectorError from './Error.CollectionSelector'; 23import { FiPlus } from 'react-icons/fi'; 24import { IoSearch } from 'react-icons/io5'; 25 26interface Props { 27 isOpen: boolean; 28 onClose: () => void; 29 onCancel: () => void; 30 onSave: (e: React.FormEvent) => void; 31 isSaving?: boolean; 32 selectedCollections: SelectableCollectionItem[]; 33 onSelectedCollectionsChange: ( 34 collectionIds: SelectableCollectionItem[], 35 ) => void; 36} 37 38export default function CollectionSelector(props: Props) { 39 const { data, error } = useMyCollections(); 40 const [search, setSearch] = useState<string>(''); 41 const [debouncedSearch] = useDebouncedValue(search, 200); 42 const searchedCollections = useCollectionSearch({ query: debouncedSearch }); 43 const [isDrawerOpen, setIsDrawerOpen] = useState(false); 44 45 const handleCollectionChange = ( 46 checked: boolean, 47 item: SelectableCollectionItem, 48 ) => { 49 if (checked) { 50 if (!props.selectedCollections.some((col) => col.id === item.id)) { 51 props.onSelectedCollectionsChange([...props.selectedCollections, item]); 52 } 53 } else { 54 props.onSelectedCollectionsChange( 55 props.selectedCollections.filter((col) => col.id !== item.id), 56 ); 57 } 58 }; 59 60 if (error) { 61 return <CollectionSelectorError />; 62 } 63 64 const allCollections = 65 data?.pages.flatMap((page) => page.collections ?? []) ?? []; 66 67 const hasCollections = allCollections.length > 0; 68 const hasSelectedCollections = props.selectedCollections.length > 0; 69 70 // filter out selected from all to avoid duplication 71 const unselectedCollections = allCollections.filter( 72 (c) => !props.selectedCollections.some((sel) => sel.id === c.id), 73 ); 74 75 return ( 76 <Fragment> 77 <FocusTrap.InitialFocus /> 78 <Stack gap="xl"> 79 <Stack> 80 <TextInput 81 placeholder="Search for collections" 82 value={search} 83 onChange={(e) => setSearch(e.currentTarget.value)} 84 size="md" 85 variant="filled" 86 id="search" 87 leftSection={<IoSearch size={22} />} 88 rightSection={ 89 <CloseButton 90 aria-label="Clear input" 91 onClick={() => setSearch('')} 92 style={{ display: search ? undefined : 'none' }} 93 /> 94 } 95 /> 96 97 <ScrollArea.Autosize mah={215} type="auto"> 98 <Stack gap="xs"> 99 {search ? ( 100 <> 101 <Button 102 variant="light" 103 size="md" 104 color="grape" 105 radius="lg" 106 leftSection={<FiPlus size={22} />} 107 onClick={() => setIsDrawerOpen(true)} 108 > 109 Create new collection "{search}" 110 </Button> 111 112 {searchedCollections.isPending && ( 113 <Stack align="center"> 114 <Text fw={500} c="gray"> 115 Searching collections... 116 </Text> 117 <Loader color="gray" /> 118 </Stack> 119 )} 120 121 {searchedCollections.data && 122 (searchedCollections.data.collections.length === 0 ? ( 123 <Alert 124 color="gray" 125 title={`No results found for "${search}"`} 126 /> 127 ) : ( 128 <CollectionSelectorItemList 129 collections={searchedCollections.data.collections} 130 selectedCollections={props.selectedCollections} 131 onChange={handleCollectionChange} 132 /> 133 ))} 134 </> 135 ) : hasCollections ? ( 136 <> 137 <Button 138 variant="light" 139 size="md" 140 color="grape" 141 radius="lg" 142 leftSection={<FiPlus size={22} />} 143 onClick={() => setIsDrawerOpen(true)} 144 > 145 Create new collection 146 </Button> 147 148 {/* selected collections */} 149 {hasSelectedCollections && ( 150 <Fragment> 151 <Text fw={600} fz={'sm'} c={'gray'}> 152 Selected Collections ({props.selectedCollections.length} 153 ) 154 </Text> 155 <CollectionSelectorItemList 156 collections={props.selectedCollections} 157 selectedCollections={props.selectedCollections} 158 onChange={handleCollectionChange} 159 /> 160 {unselectedCollections.length > 0 && <Divider my="xs" />} 161 </Fragment> 162 )} 163 164 {/* remaining collections */} 165 {unselectedCollections.length > 0 ? ( 166 <CollectionSelectorItemList 167 collections={unselectedCollections} 168 selectedCollections={props.selectedCollections} 169 onChange={handleCollectionChange} 170 /> 171 ) : ( 172 !hasSelectedCollections && ( 173 <Alert color="gray" title="No collections available" /> 174 ) 175 )} 176 </> 177 ) : ( 178 <Stack align="center" gap="xs"> 179 <Text fz="lg" fw={600} c="gray"> 180 No collections 181 </Text> 182 <Button 183 onClick={() => setIsDrawerOpen(true)} 184 variant="light" 185 color="gray" 186 rightSection={<FiPlus size={22} />} 187 > 188 Create a collection 189 </Button> 190 </Stack> 191 )} 192 </Stack> 193 </ScrollArea.Autosize> 194 </Stack> 195 196 {/* Action Buttons */} 197 <Group justify="space-between" gap="xs" grow> 198 <Button 199 variant="light" 200 color="gray" 201 size="md" 202 onClick={() => props.onCancel()} 203 > 204 Cancel 205 </Button> 206 207 <Button 208 size="md" 209 loading={props.isSaving} 210 onClick={(e) => { 211 props.onSave(e); 212 }} 213 > 214 Save 215 </Button> 216 </Group> 217 </Stack> 218 219 <CreateCollectionDrawer 220 key={search} 221 isOpen={isDrawerOpen} 222 onClose={() => setIsDrawerOpen(false)} 223 initialName={search} 224 onCreate={(newCollection) => { 225 props.onSelectedCollectionsChange([ 226 ...props.selectedCollections, 227 newCollection, 228 ]); 229 setSearch(''); 230 }} 231 /> 232 </Fragment> 233 ); 234}