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.9 kB 225 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 <Text fw={500} c="gray"> 160 Loading open collections... 161 </Text> 162 <Loader color="gray" /> 163 </Stack> 164 ) : hasCollections ? ( 165 <Stack gap={'xs'}> 166 {/* selected collections */} 167 {hasSelectedCollections && ( 168 <Fragment> 169 <Text fw={600} fz={'sm'} c={'gray'}> 170 Selected Collections ({props.selectedCollections.length} 171 ) 172 </Text> 173 <CollectionSelectorItemList 174 collections={props.selectedCollections} 175 selectedCollections={props.selectedCollections} 176 onChange={handleCollectionChange} 177 /> 178 {unselectedCollections.length > 0 && ( 179 <Divider variant="dashed" my="xs" /> 180 )} 181 </Fragment> 182 )} 183 184 {/* remaining collections */} 185 {unselectedCollections.length > 0 ? ( 186 <CollectionSelectorItemList 187 collections={unselectedCollections} 188 selectedCollections={props.selectedCollections} 189 onChange={handleCollectionChange} 190 /> 191 ) : ( 192 !hasSelectedCollections && ( 193 <Alert 194 color="gray" 195 title="No open collections available" 196 /> 197 ) 198 )} 199 </Stack> 200 ) : ( 201 <Stack align="center" gap="xs"> 202 <Text fz="lg" fw={600} c="gray"> 203 No open collections 204 </Text> 205 </Stack> 206 )} 207 </Stack> 208 </ScrollArea.Autosize> 209 </Stack> 210 </Stack> 211 212 <CreateCollectionDrawer 213 key={search} 214 isOpen={isDrawerOpen} 215 onClose={() => setIsDrawerOpen(false)} 216 initialName={search} 217 initialAccessType={CollectionAccessType.OPEN} 218 onCreate={(collection) => { 219 setSearch(''); 220 handleCollectionChange(true, collection); 221 }} 222 /> 223 </Fragment> 224 ); 225}