alpha
Login
or
Join now
nandi.uk
/
semble
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
This repository has no description
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Overview
Issues
Pulls
Pipelines
feat: url search on new card form
author
pdelfan
date
2 months ago
(May 14, 2026, 2:40 PM -0700)
commit
74494aa2
74494aa25f9ce41b1bd756ff01db5ed7ba7fc312
parent
de57e8b7
de57e8b739e2e50d7625475dd63db605e438585e
+361
-245
4 changed files
Expand all
Collapse all
Unified
Split
src
webapp
features
cards
components
addCardDrawer
AddCardForm.tsx
composer
components
Composer.tsx
connections
components
addConnectionDrawer
SourceCardPreview.tsx
UrlSearchInput.tsx
+53
-11
src/webapp/features/cards/components/addCardDrawer/AddCardForm.tsx
View file
Reviewed
···
9
9
Stack,
10
10
Text,
11
11
Textarea,
12
12
-
TextInput,
13
12
ThemeIcon,
14
13
VisuallyHidden,
15
14
Container,
···
19
18
import { notifications } from '@mantine/notifications';
20
19
import useAddCard from '../../lib/mutations/useAddCard';
21
20
import CollectionSelector from '@/features/collections/components/collectionSelector/CollectionSelector';
22
22
-
import { Suspense, useEffect, useState } from 'react';
21
21
+
import { Suspense, useEffect, useRef, useState } from 'react';
23
22
import CollectionSelectorSkeleton from '@/features/collections/components/collectionSelector/Skeleton.CollectionSelector';
24
23
import { useDisclosure } from '@mantine/hooks';
25
24
import { BiCollection } from 'react-icons/bi';
···
33
32
import { FaSeedling } from 'react-icons/fa6';
34
33
import { CardSaveSource } from '@/features/analytics/types';
35
34
import { usePathname } from 'next/navigation';
35
35
+
import UrlSearchInput from '@/features/connections/components/addConnectionDrawer/UrlSearchInput';
36
36
37
37
interface Props {
38
38
onClose: () => void;
···
67
67
pagePath: pathname,
68
68
});
69
69
70
70
+
const rawUrlInput = useRef(props.initialUrl ?? '');
71
71
+
70
72
const form = useForm({
71
73
initialValues: {
72
74
url: props.initialUrl || '',
73
75
note: '',
74
76
collections: selectedCollections,
75
77
},
78
78
+
validateInputOnChange: false,
79
79
+
validateInputOnBlur: true,
80
80
+
validate: {
81
81
+
url: (value) => {
82
82
+
if (!value || value.trim() === '') {
83
83
+
return 'URL is required';
84
84
+
}
85
85
+
try {
86
86
+
new URL(value);
87
87
+
return null;
88
88
+
} catch {
89
89
+
return 'Please enter a valid URL';
90
90
+
}
91
91
+
},
92
92
+
},
76
93
});
77
94
78
95
const MAX_NOTE_LENGTH = 500;
···
80
97
useEffect(() => {
81
98
if (props.initialUrl) {
82
99
form.setValues({ url: props.initialUrl });
100
100
+
rawUrlInput.current = props.initialUrl;
83
101
}
84
102
}, [props.initialUrl]);
85
103
86
104
const handleAddCard = (e: React.FormEvent) => {
87
105
e.preventDefault();
106
106
+
107
107
+
// Auto-confirm a valid URL that was typed/pasted but not explicitly selected
108
108
+
if (!form.values.url && rawUrlInput.current) {
109
109
+
try {
110
110
+
new URL(rawUrlInput.current);
111
111
+
form.setFieldValue('url', rawUrlInput.current);
112
112
+
} catch {
113
113
+
// let validation handle it
114
114
+
}
115
115
+
}
116
116
+
117
117
+
const validation = form.validate();
118
118
+
if (validation.hasErrors) return;
119
119
+
88
120
track('add new card');
89
121
90
122
// Capture values before any state changes
···
109
141
// Close drawer immediately
110
142
props.onClose();
111
143
setSelectedCollections(initialCollections);
144
144
+
rawUrlInput.current = '';
112
145
form.reset();
113
146
114
147
addCard.mutate({ ...cardData, notificationId });
···
118
151
<>
119
152
<form onSubmit={handleAddCard}>
120
153
<Stack gap={'xl'}>
121
121
-
<TextInput
154
154
+
<UrlSearchInput
122
155
id="url"
123
156
label="URL"
124
124
-
type="url"
125
125
-
placeholder="https://www.example.com"
126
126
-
variant="filled"
127
127
-
required
128
128
-
size="md"
129
129
-
leftSection={<IoMdLink size={22} />}
130
130
-
key={form.key('url')}
131
131
-
{...form.getInputProps('url')}
157
157
+
placeholder="Search or paste a link"
158
158
+
value={form.values.url}
159
159
+
error={form.errors.url}
160
160
+
onUrlSelect={(url) => form.setFieldValue('url', url)}
161
161
+
onUrlClear={() => {
162
162
+
rawUrlInput.current = '';
163
163
+
form.setFieldValue('url', '');
164
164
+
}}
165
165
+
onInputChange={(raw) => {
166
166
+
rawUrlInput.current = raw;
167
167
+
}}
168
168
+
inputProps={{
169
169
+
variant: 'filled',
170
170
+
size: 'md',
171
171
+
leftSection: <IoMdLink size={22} />,
172
172
+
'data-autofocus': true,
173
173
+
}}
132
174
/>
133
175
134
176
<Stack gap={0}>
+54
-11
src/webapp/features/composer/components/Composer.tsx
View file
Reviewed
···
18
18
VisuallyHidden,
19
19
Scroller,
20
20
} from '@mantine/core';
21
21
-
import { useState, useEffect, Suspense } from 'react';
21
21
+
import { useState, useEffect, useRef, Suspense } from 'react';
22
22
import { Collection, CollectionAccessType } from '@semble/types';
23
23
import { DEFAULT_OVERLAY_PROPS } from '@/styles/overlays';
24
24
import { useForm } from '@mantine/form';
···
30
30
import { useDisclosure } from '@mantine/hooks';
31
31
import { BiCollection } from 'react-icons/bi';
32
32
import { IoMdCheckmark, IoMdLink } from 'react-icons/io';
33
33
+
import UrlSearchInput from '@/features/connections/components/addConnectionDrawer/UrlSearchInput';
33
34
import { track } from '@vercel/analytics';
34
35
import useMyCollections from '@/features/collections/lib/queries/useMyCollections';
35
36
import { isMarginUri, getMarginUrl } from '@/lib/utils/margin';
···
84
85
pagePath: pathname,
85
86
});
86
87
88
88
+
const rawUrlInput = useRef(props.initialUrl ?? '');
89
89
+
87
90
const cardForm = useForm({
88
91
initialValues: {
89
92
url: props.initialUrl || '',
90
93
note: '',
91
94
collections: selectedCollections,
92
95
},
96
96
+
validateInputOnChange: false,
97
97
+
validateInputOnBlur: true,
98
98
+
validate: {
99
99
+
url: (value) => {
100
100
+
if (!value || value.trim() === '') {
101
101
+
return 'URL is required';
102
102
+
}
103
103
+
try {
104
104
+
new URL(value);
105
105
+
return null;
106
106
+
} catch {
107
107
+
return 'Please enter a valid URL';
108
108
+
}
109
109
+
},
110
110
+
},
93
111
});
94
112
95
113
// Collection form state
···
114
132
useEffect(() => {
115
133
if (props.initialUrl) {
116
134
cardForm.setValues({ url: props.initialUrl });
135
135
+
rawUrlInput.current = props.initialUrl;
117
136
}
118
137
}, [props.initialUrl]);
119
138
···
124
143
collectionForm.reset();
125
144
setSelectedCollections(initialCollections);
126
145
setMode('card');
146
146
+
rawUrlInput.current = '';
127
147
}
128
148
}, [props.isOpen]);
129
149
130
150
const handleAddCard = (e: React.FormEvent) => {
131
151
e.preventDefault();
152
152
+
153
153
+
// Auto-confirm a valid URL that was typed/pasted but not explicitly selected
154
154
+
if (!cardForm.values.url && rawUrlInput.current) {
155
155
+
try {
156
156
+
new URL(rawUrlInput.current);
157
157
+
cardForm.setFieldValue('url', rawUrlInput.current);
158
158
+
} catch {
159
159
+
// let validation handle it
160
160
+
}
161
161
+
}
162
162
+
163
163
+
const validation = cardForm.validate();
164
164
+
if (validation.hasErrors) return;
165
165
+
132
166
track('add new card');
133
167
134
168
// Capture values before any state changes
···
154
188
props.onClose();
155
189
setSelectedCollections(initialCollections);
156
190
window.history.replaceState({}, '', window.location.pathname);
191
191
+
rawUrlInput.current = '';
157
192
cardForm.reset();
158
193
159
194
addCard.mutate({ ...cardData, notificationId });
···
268
303
style={{ flex: 1, display: 'flex', flexDirection: 'column' }}
269
304
>
270
305
<Stack gap={'xl'} style={{ flex: 1 }}>
271
271
-
<TextInput
306
306
+
<UrlSearchInput
272
307
id="url"
273
308
label="URL"
274
274
-
type="url"
275
275
-
placeholder="https://www.example.com"
276
276
-
variant="filled"
277
277
-
required
278
278
-
size="md"
279
279
-
leftSection={<IoMdLink size={22} />}
280
280
-
data-autofocus
281
281
-
key={cardForm.key('url')}
282
282
-
{...cardForm.getInputProps('url')}
309
309
+
placeholder="Search of paste a link"
310
310
+
value={cardForm.values.url}
311
311
+
error={cardForm.errors.url}
312
312
+
onUrlSelect={(url) => cardForm.setFieldValue('url', url)}
313
313
+
onUrlClear={() => {
314
314
+
rawUrlInput.current = '';
315
315
+
cardForm.setFieldValue('url', '');
316
316
+
}}
317
317
+
onInputChange={(raw) => {
318
318
+
rawUrlInput.current = raw;
319
319
+
}}
320
320
+
inputProps={{
321
321
+
variant: 'filled',
322
322
+
size: 'md',
323
323
+
leftSection: <IoMdLink size={22} />,
324
324
+
'data-autofocus': true,
325
325
+
}}
283
326
/>
284
327
285
328
<Stack gap={0}>
+3
-3
src/webapp/features/connections/components/addConnectionDrawer/SourceCardPreview.tsx
View file
Reviewed
···
19
19
return (
20
20
<Card withBorder component="article" p={'xs'} radius={'lg'}>
21
21
<Group gap="xs" wrap="nowrap">
22
22
-
<Skeleton width={45} height={45} radius={'md'} />
22
22
+
<Skeleton width={42} height={42} radius={'md'} />
23
23
<Stack gap={0} style={{ flex: 1 }}>
24
24
<Skeleton height={21.5} width="80%" radius="sm" />
25
25
<Skeleton height={18.5} width="60%" radius="sm" mt={4} />
···
56
56
src={data.metadata.imageUrl}
57
57
alt={`${data.metadata.title} social preview image`}
58
58
radius={'md'}
59
59
-
w={45}
60
60
-
h={45}
59
59
+
w={42}
60
60
+
h={42}
61
61
style={{ flexShrink: 0 }}
62
62
/>
63
63
)}
+251
-220
src/webapp/features/connections/components/addConnectionDrawer/UrlSearchInput.tsx
View file
Reviewed
···
13
13
Skeleton,
14
14
Stack,
15
15
Text,
16
16
+
TextInput,
17
17
+
type TextInputProps,
16
18
ThemeIcon,
17
19
useCombobox,
18
20
VisuallyHidden,
···
61
63
onUrlSelect: (url: string) => void;
62
64
onUrlClear?: () => void;
63
65
onInputChange?: (rawValue: string) => void;
66
66
+
/**
67
67
+
* When provided, the input is rendered as a standard Mantine `TextInput`
68
68
+
* (with a visible label) and these props are spread onto it. Use this to
69
69
+
* customize variant, size, leftSection, etc. When omitted, the input keeps
70
70
+
* the legacy card-wrapped unstyled layout used by the connection form.
71
71
+
*/
72
72
+
inputProps?: Omit<
73
73
+
TextInputProps,
74
74
+
| 'id'
75
75
+
| 'label'
76
76
+
| 'placeholder'
77
77
+
| 'value'
78
78
+
| 'onChange'
79
79
+
| 'onFocus'
80
80
+
| 'onBlur'
81
81
+
| 'error'
82
82
+
| 'required'
83
83
+
> & { [K in `data-${string}`]?: unknown };
64
84
}
65
85
66
86
export default function UrlSearchInput(props: Props) {
···
216
236
217
237
const currentError = searchFilter === 'cards' ? error : collectionSearchError;
218
238
219
219
-
return (
220
220
-
<Card padding="xs" radius="lg" withBorder>
221
221
-
<Stack gap={0}>
222
222
-
<Combobox
223
223
-
shadow="sm"
224
224
-
radius={'md'}
225
225
-
store={combobox}
226
226
-
position="bottom-start"
227
227
-
onOptionSubmit={(url) => {
228
228
-
props.onUrlSelect(url);
229
229
-
setInputValue(url);
230
230
-
if (isValidUrl(url)) {
231
231
-
setConfirmedUrl(url);
232
232
-
// Look up metadata from search results or recent cards
233
233
-
const searchMatch = urls.find((u) => u.url === url);
234
234
-
const recentMatch = recentCardsList.find((c) => c.url === url);
235
235
-
setConfirmedMetadata(
236
236
-
searchMatch?.metadata ?? recentMatch?.cardContent,
237
237
-
);
238
238
-
}
239
239
-
combobox.closeDropdown();
240
240
-
}}
241
241
-
>
242
242
-
<Combobox.Target>
243
243
-
<Input
244
244
-
id={props.id}
245
245
-
component="input"
246
246
-
type="text"
247
247
-
py={2.5}
248
248
-
placeholder={props.placeholder}
249
249
-
value={inputValue}
250
250
-
onChange={(e) => {
251
251
-
const val = e.currentTarget.value;
252
252
-
setInputValue(val);
253
253
-
props.onInputChange?.(val);
254
254
-
combobox.openDropdown();
255
255
-
}}
256
256
-
onFocus={() => {
257
257
-
setIsInputFocused(true);
258
258
-
combobox.openDropdown();
259
259
-
}}
260
260
-
onBlur={() => {
261
261
-
setIsInputFocused(false);
262
262
-
}}
263
263
-
rightSection={null}
264
264
-
variant="unstyled"
265
265
-
size="md"
266
266
-
required
267
267
-
error={props.error}
268
268
-
/>
269
269
-
</Combobox.Target>
239
239
+
const isPlainLayout = !!props.inputProps;
240
240
+
241
241
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
242
242
+
const val = e.currentTarget.value;
243
243
+
setInputValue(val);
244
244
+
props.onInputChange?.(val);
245
245
+
combobox.openDropdown();
246
246
+
};
247
247
+
248
248
+
const handleFocus = () => {
249
249
+
setIsInputFocused(true);
250
250
+
combobox.openDropdown();
251
251
+
};
252
252
+
253
253
+
const handleBlur = () => {
254
254
+
setIsInputFocused(false);
255
255
+
};
270
256
271
271
-
<Combobox.Dropdown
272
272
-
hidden={
273
273
-
(inputValue.trim().length === 0 &&
274
274
-
debounced.trim().length === 0 &&
275
275
-
!(
276
276
-
isInputFocused &&
277
277
-
(isLoadingRecentCards || recentCardsList.length > 0)
278
278
-
)) ||
279
279
-
(inputValue.trim().length === 0 && debounced.trim().length > 0)
280
280
-
}
257
257
+
const inputElement = isPlainLayout ? (
258
258
+
<TextInput
259
259
+
{...props.inputProps}
260
260
+
id={props.id}
261
261
+
label={props.label}
262
262
+
placeholder={props.placeholder}
263
263
+
value={inputValue}
264
264
+
onChange={handleInputChange}
265
265
+
onFocus={handleFocus}
266
266
+
onBlur={handleBlur}
267
267
+
error={props.error}
268
268
+
required
269
269
+
/>
270
270
+
) : (
271
271
+
<Input
272
272
+
id={props.id}
273
273
+
component="input"
274
274
+
type="text"
275
275
+
py={2.5}
276
276
+
placeholder={props.placeholder}
277
277
+
value={inputValue}
278
278
+
onChange={handleInputChange}
279
279
+
onFocus={handleFocus}
280
280
+
onBlur={handleBlur}
281
281
+
rightSection={null}
282
282
+
variant="unstyled"
283
283
+
size="md"
284
284
+
required
285
285
+
error={props.error}
286
286
+
/>
287
287
+
);
288
288
+
289
289
+
const comboboxBlock = (
290
290
+
<Combobox
291
291
+
shadow="sm"
292
292
+
radius={'md'}
293
293
+
store={combobox}
294
294
+
position="bottom-start"
295
295
+
onOptionSubmit={(url) => {
296
296
+
props.onUrlSelect(url);
297
297
+
setInputValue(url);
298
298
+
if (isValidUrl(url)) {
299
299
+
setConfirmedUrl(url);
300
300
+
// Look up metadata from search results or recent cards
301
301
+
const searchMatch = urls.find((u) => u.url === url);
302
302
+
const recentMatch = recentCardsList.find((c) => c.url === url);
303
303
+
setConfirmedMetadata(
304
304
+
searchMatch?.metadata ?? recentMatch?.cardContent,
305
305
+
);
306
306
+
}
307
307
+
combobox.closeDropdown();
308
308
+
}}
309
309
+
>
310
310
+
<Combobox.Target>{inputElement}</Combobox.Target>
311
311
+
312
312
+
<Combobox.Dropdown
313
313
+
hidden={
314
314
+
(inputValue.trim().length === 0 &&
315
315
+
debounced.trim().length === 0 &&
316
316
+
!(
317
317
+
isInputFocused &&
318
318
+
(isLoadingRecentCards || recentCardsList.length > 0)
319
319
+
)) ||
320
320
+
(inputValue.trim().length === 0 && debounced.trim().length > 0)
321
321
+
}
322
322
+
>
323
323
+
<Combobox.Options>
324
324
+
<ScrollArea.Autosize
325
325
+
type="scroll"
326
326
+
mah={{ base: 150, xs: 300 }}
327
327
+
offsetScrollbars={'present'}
281
328
>
282
282
-
<Combobox.Options>
283
283
-
<ScrollArea.Autosize
284
284
-
type="scroll"
285
285
-
mah={{ base: 150, xs: 300 }}
286
286
-
offsetScrollbars={'present'}
287
287
-
>
288
288
-
{debounced.trim().length === 0 ? (
329
329
+
{debounced.trim().length === 0 ? (
330
330
+
<Fragment>
331
331
+
<Text size="sm" fw={500} c="dimmed" py="xs" px={5}>
332
332
+
Recent cards
333
333
+
</Text>
334
334
+
{isLoadingRecentCards ? (
335
335
+
<Stack gap={5} p={5}>
336
336
+
{Array.from({ length: 5 }).map((_, i) => (
337
337
+
<Group key={i} gap="xs" align="center" wrap="nowrap">
338
338
+
<Skeleton height={35} width={35} radius="sm" />
339
339
+
<Stack gap={4} style={{ flex: 1 }}>
340
340
+
<Skeleton height={12} width="70%" radius="xl" />
341
341
+
<Skeleton height={10} width="40%" radius="xl" />
342
342
+
</Stack>
343
343
+
</Group>
344
344
+
))}
345
345
+
</Stack>
346
346
+
) : (
347
347
+
recentCardsList.map((card) => (
348
348
+
<Combobox.Option key={card.url} value={card.url} p={5}>
349
349
+
<Group gap={'xs'} align="center" wrap="nowrap">
350
350
+
{card.cardContent.imageUrl && (
351
351
+
<Image
352
352
+
src={card.cardContent.imageUrl}
353
353
+
alt={card.cardContent.title || 'URL thumbnail'}
354
354
+
w={35}
355
355
+
h={35}
356
356
+
radius="sm"
357
357
+
fit="cover"
358
358
+
/>
359
359
+
)}
360
360
+
<Stack gap={0}>
361
361
+
<Text fw={500} c={'bright'} lineClamp={1} size="sm">
362
362
+
{card.cardContent.title || card.url}
363
363
+
</Text>
364
364
+
<Text c={'gray'} lineClamp={1} size="xs">
365
365
+
{getDomain(card.url)}
366
366
+
</Text>
367
367
+
</Stack>
368
368
+
</Group>
369
369
+
</Combobox.Option>
370
370
+
))
371
371
+
)}
372
372
+
</Fragment>
373
373
+
) : (
374
374
+
<Fragment>
375
375
+
{currentError && (
376
376
+
<Combobox.Empty>Could not search</Combobox.Empty>
377
377
+
)}
378
378
+
{!currentError && inputValue.trim() && (
379
379
+
<Fragment>
380
380
+
<Combobox.Option value={inputValue}>
381
381
+
<Group gap="xs" wrap="nowrap" p={0}>
382
382
+
<ThemeIcon
383
383
+
radius={'xl'}
384
384
+
size={'sm'}
385
385
+
variant="light"
386
386
+
color="gray"
387
387
+
>
388
388
+
<BiPlus />
389
389
+
</ThemeIcon>
390
390
+
<Stack gap={0} style={{ flex: 1 }}>
391
391
+
<Text size="sm" fw={600} c={'bright'}>
392
392
+
Add this link
393
393
+
</Text>
394
394
+
<Text size="xs" c="dimmed" lineClamp={1}>
395
395
+
{inputValue}
396
396
+
</Text>
397
397
+
</Stack>
398
398
+
</Group>
399
399
+
</Combobox.Option>
400
400
+
401
401
+
{(hasSearchResults || true) && (
402
402
+
<Fragment>
403
403
+
<Text size="sm" fw={500} c="dimmed" py="xs" px={5}>
404
404
+
Search results
405
405
+
</Text>
406
406
+
<SegmentedControl
407
407
+
value={searchFilter}
408
408
+
onChange={(value) => {
409
409
+
setSearchFilter(value as SearchFilter);
410
410
+
combobox.resetSelectedOption();
411
411
+
}}
412
412
+
data={[
413
413
+
{ label: 'Cards', value: 'cards' },
414
414
+
{
415
415
+
label: 'Collections',
416
416
+
value: 'collections',
417
417
+
},
418
418
+
]}
419
419
+
size="xs"
420
420
+
mb="xs"
421
421
+
/>
422
422
+
</Fragment>
423
423
+
)}
424
424
+
</Fragment>
425
425
+
)}
426
426
+
{searchFilter === 'cards' && (
289
427
<Fragment>
290
290
-
<Text size="sm" fw={500} c="dimmed" py="xs" px={5}>
291
291
-
Recent cards
292
292
-
</Text>
293
293
-
{isLoadingRecentCards ? (
428
428
+
{isFetching ? (
294
429
<Stack gap={5} p={5}>
295
295
-
{Array.from({ length: 5 }).map((_, i) => (
430
430
+
{Array.from({ length: 3 }).map((_, i) => (
296
431
<Group key={i} gap="xs" align="center" wrap="nowrap">
297
432
<Skeleton height={35} width={35} radius="sm" />
298
433
<Stack gap={4} style={{ flex: 1 }}>
···
302
437
</Group>
303
438
))}
304
439
</Stack>
440
440
+
) : cardOptions.length > 0 ? (
441
441
+
<Fragment>{cardOptions}</Fragment>
305
442
) : (
306
306
-
recentCardsList.map((card) => (
307
307
-
<Combobox.Option key={card.url} value={card.url} p={5}>
308
308
-
<Group gap={'xs'} align="center" wrap="nowrap">
309
309
-
{card.cardContent.imageUrl && (
310
310
-
<Image
311
311
-
src={card.cardContent.imageUrl}
312
312
-
alt={card.cardContent.title || 'URL thumbnail'}
313
313
-
w={35}
314
314
-
h={35}
315
315
-
radius="sm"
316
316
-
fit="cover"
317
317
-
/>
318
318
-
)}
319
319
-
<Stack gap={0}>
320
320
-
<Text
321
321
-
fw={500}
322
322
-
c={'bright'}
323
323
-
lineClamp={1}
324
324
-
size="sm"
325
325
-
>
326
326
-
{card.cardContent.title || card.url}
327
327
-
</Text>
328
328
-
<Text c={'gray'} lineClamp={1} size="xs">
329
329
-
{getDomain(card.url)}
330
330
-
</Text>
331
331
-
</Stack>
332
332
-
</Group>
333
333
-
</Combobox.Option>
334
334
-
))
443
443
+
debounced.trim().length > 0 && (
444
444
+
<Combobox.Empty>No cards found</Combobox.Empty>
445
445
+
)
335
446
)}
336
447
</Fragment>
337
337
-
) : (
448
448
+
)}
449
449
+
{searchFilter === 'collections' && (
338
450
<Fragment>
339
339
-
{currentError && (
340
340
-
<Combobox.Empty>Could not search</Combobox.Empty>
341
341
-
)}
342
342
-
{!currentError && inputValue.trim() && (
343
343
-
<Fragment>
344
344
-
<Combobox.Option value={inputValue}>
345
345
-
<Group gap="xs" wrap="nowrap" p={0}>
346
346
-
<ThemeIcon
347
347
-
radius={'xl'}
348
348
-
size={'sm'}
349
349
-
variant="light"
350
350
-
color="gray"
351
351
-
>
352
352
-
<BiPlus />
353
353
-
</ThemeIcon>
354
354
-
<Stack gap={0} style={{ flex: 1 }}>
355
355
-
<Text size="sm" fw={600} c={'bright'}>
356
356
-
Add this link
357
357
-
</Text>
358
358
-
<Text size="xs" c="dimmed" lineClamp={1}>
359
359
-
{inputValue}
360
360
-
</Text>
361
361
-
</Stack>
451
451
+
{isCollectionSearchFetching ? (
452
452
+
<Stack gap={5} p={5}>
453
453
+
{Array.from({ length: 5 }).map((_, i) => (
454
454
+
<Group
455
455
+
key={i}
456
456
+
gap="xs"
457
457
+
align="center"
458
458
+
justify="space-between"
459
459
+
wrap="nowrap"
460
460
+
>
461
461
+
<Skeleton height={20} width="70%" radius="xl" />
462
462
+
<Skeleton height={26} width={26} radius="md" />
362
463
</Group>
363
363
-
</Combobox.Option>
364
364
-
365
365
-
{(hasSearchResults || true) && (
366
366
-
<Fragment>
367
367
-
<Text size="sm" fw={500} c="dimmed" py="xs" px={5}>
368
368
-
Search results
369
369
-
</Text>
370
370
-
<SegmentedControl
371
371
-
value={searchFilter}
372
372
-
onChange={(value) => {
373
373
-
setSearchFilter(value as SearchFilter);
374
374
-
combobox.resetSelectedOption();
375
375
-
}}
376
376
-
data={[
377
377
-
{ label: 'Cards', value: 'cards' },
378
378
-
{
379
379
-
label: 'Collections',
380
380
-
value: 'collections',
381
381
-
},
382
382
-
]}
383
383
-
size="xs"
384
384
-
mb="xs"
385
385
-
/>
386
386
-
</Fragment>
387
387
-
)}
388
388
-
</Fragment>
389
389
-
)}
390
390
-
{searchFilter === 'cards' && (
391
391
-
<Fragment>
392
392
-
{isFetching ? (
393
393
-
<Stack gap={5} p={5}>
394
394
-
{Array.from({ length: 3 }).map((_, i) => (
395
395
-
<Group
396
396
-
key={i}
397
397
-
gap="xs"
398
398
-
align="center"
399
399
-
wrap="nowrap"
400
400
-
>
401
401
-
<Skeleton height={35} width={35} radius="sm" />
402
402
-
<Stack gap={4} style={{ flex: 1 }}>
403
403
-
<Skeleton
404
404
-
height={12}
405
405
-
width="70%"
406
406
-
radius="xl"
407
407
-
/>
408
408
-
<Skeleton
409
409
-
height={10}
410
410
-
width="40%"
411
411
-
radius="xl"
412
412
-
/>
413
413
-
</Stack>
414
414
-
</Group>
415
415
-
))}
416
416
-
</Stack>
417
417
-
) : cardOptions.length > 0 ? (
418
418
-
<Fragment>{cardOptions}</Fragment>
419
419
-
) : (
420
420
-
debounced.trim().length > 0 && (
421
421
-
<Combobox.Empty>No cards found</Combobox.Empty>
422
422
-
)
423
423
-
)}
424
424
-
</Fragment>
425
425
-
)}
426
426
-
{searchFilter === 'collections' && (
427
427
-
<Fragment>
428
428
-
{isCollectionSearchFetching ? (
429
429
-
<Stack gap={5} p={5}>
430
430
-
{Array.from({ length: 5 }).map((_, i) => (
431
431
-
<Group
432
432
-
key={i}
433
433
-
gap="xs"
434
434
-
align="center"
435
435
-
justify="space-between"
436
436
-
wrap="nowrap"
437
437
-
>
438
438
-
<Skeleton height={20} width="70%" radius="xl" />
439
439
-
<Skeleton height={26} width={26} radius="md" />
440
440
-
</Group>
441
441
-
))}
442
442
-
</Stack>
443
443
-
) : collectionOptions.filter(Boolean).length > 0 ? (
444
444
-
<Fragment>{collectionOptions}</Fragment>
445
445
-
) : (
446
446
-
debounced.trim().length > 0 && (
447
447
-
<Combobox.Empty>
448
448
-
No collections found
449
449
-
</Combobox.Empty>
450
450
-
)
451
451
-
)}
452
452
-
</Fragment>
464
464
+
))}
465
465
+
</Stack>
466
466
+
) : collectionOptions.filter(Boolean).length > 0 ? (
467
467
+
<Fragment>{collectionOptions}</Fragment>
468
468
+
) : (
469
469
+
debounced.trim().length > 0 && (
470
470
+
<Combobox.Empty>No collections found</Combobox.Empty>
471
471
+
)
453
472
)}
454
473
</Fragment>
455
474
)}
456
456
-
</ScrollArea.Autosize>
457
457
-
</Combobox.Options>
458
458
-
</Combobox.Dropdown>
459
459
-
</Combobox>
475
475
+
</Fragment>
476
476
+
)}
477
477
+
</ScrollArea.Autosize>
478
478
+
</Combobox.Options>
479
479
+
</Combobox.Dropdown>
480
480
+
</Combobox>
481
481
+
);
482
482
+
483
483
+
if (isPlainLayout) {
484
484
+
return comboboxBlock;
485
485
+
}
486
486
+
487
487
+
return (
488
488
+
<Card padding="xs" radius="lg" withBorder>
489
489
+
<Stack gap={0}>
490
490
+
{comboboxBlock}
460
491
<VisuallyHidden>
461
492
<Input.Label htmlFor={props.id} required>
462
493
{props.label}