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