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