This repository has no description
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}