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