@turtleclub/core 0.1.0-beta.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1150 @@
1
+ // src/filters/RangeSliderFilter.tsx
2
+ import { useEffect, useState } from "react";
3
+ import {
4
+ Slider,
5
+ Label,
6
+ Input,
7
+ Popover,
8
+ PopoverTrigger,
9
+ PopoverContent,
10
+ buttonVariants,
11
+ cn
12
+ } from "@turtleclub/ui";
13
+ import { useQueryState, parseAsInteger } from "nuqs";
14
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
15
+ var defaultFormatter = (value) => {
16
+ return value.toLocaleString();
17
+ };
18
+ function RangeSliderFilter({
19
+ minQueryKey = "min",
20
+ maxQueryKey = "max",
21
+ min = 0,
22
+ max = 100,
23
+ step = 1,
24
+ disabled = false,
25
+ label = "Range",
26
+ showValues = true,
27
+ formatValue = defaultFormatter,
28
+ className = "",
29
+ usePopover = false
30
+ }) {
31
+ const [minValue, setMinValue] = useQueryState(minQueryKey, parseAsInteger.withDefault(min));
32
+ const [maxValue, setMaxValue] = useQueryState(maxQueryKey, parseAsInteger.withDefault(max));
33
+ const [localRange, setLocalRange] = useState([minValue, maxValue]);
34
+ const [minInputValue, setMinInputValue] = useState(minValue.toString());
35
+ const [maxInputValue, setMaxInputValue] = useState(maxValue.toString());
36
+ useEffect(() => {
37
+ setLocalRange([minValue, maxValue]);
38
+ setMinInputValue(minValue.toString());
39
+ setMaxInputValue(maxValue.toString());
40
+ }, [minValue, maxValue]);
41
+ const handleRangeChange = (values) => {
42
+ setLocalRange(values);
43
+ };
44
+ const handleRangeCommit = (values) => {
45
+ const [newMin, newMax] = values;
46
+ if (newMin !== minValue) {
47
+ setMinValue(newMin === min ? null : newMin);
48
+ }
49
+ if (newMax !== maxValue) {
50
+ setMaxValue(newMax === max ? null : newMax);
51
+ }
52
+ };
53
+ const handleMinInputChange = (e) => {
54
+ setMinInputValue(e.target.value);
55
+ };
56
+ const handleMaxInputChange = (e) => {
57
+ setMaxInputValue(e.target.value);
58
+ };
59
+ const handleMinInputBlur = () => {
60
+ const numValue = parseInt(minInputValue, 10);
61
+ if (!isNaN(numValue)) {
62
+ const clampedValue = Math.max(min, Math.min(numValue, localRange[1]));
63
+ setLocalRange([clampedValue, localRange[1]]);
64
+ handleRangeCommit([clampedValue, localRange[1]]);
65
+ setMinInputValue(clampedValue.toString());
66
+ } else {
67
+ setMinInputValue(localRange[0].toString());
68
+ }
69
+ };
70
+ const handleMaxInputBlur = () => {
71
+ const numValue = parseInt(maxInputValue, 10);
72
+ if (!isNaN(numValue)) {
73
+ const clampedValue = Math.min(max, Math.max(numValue, localRange[0]));
74
+ setLocalRange([localRange[0], clampedValue]);
75
+ handleRangeCommit([localRange[0], clampedValue]);
76
+ setMaxInputValue(clampedValue.toString());
77
+ } else {
78
+ setMaxInputValue(localRange[1].toString());
79
+ }
80
+ };
81
+ const handleMinInputKeyDown = (e) => {
82
+ if (e.key === "Enter") {
83
+ handleMinInputBlur();
84
+ }
85
+ };
86
+ const handleMaxInputKeyDown = (e) => {
87
+ if (e.key === "Enter") {
88
+ handleMaxInputBlur();
89
+ }
90
+ };
91
+ const isDefaultRange = localRange[0] === min && localRange[1] === max;
92
+ const filterContent = /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
93
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
94
+ /* @__PURE__ */ jsx(Label, { className: "text-sm font-medium", children: label }),
95
+ showValues && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 text-xs text-muted-foreground", children: [
96
+ /* @__PURE__ */ jsx("span", { children: formatValue(localRange[0]) }),
97
+ /* @__PURE__ */ jsx("span", { children: "-" }),
98
+ /* @__PURE__ */ jsx("span", { children: formatValue(localRange[1]) })
99
+ ] })
100
+ ] }),
101
+ /* @__PURE__ */ jsx("div", { className: "px-2", children: /* @__PURE__ */ jsx(
102
+ Slider,
103
+ {
104
+ value: localRange,
105
+ onValueChange: handleRangeChange,
106
+ onValueCommit: handleRangeCommit,
107
+ min,
108
+ max,
109
+ step,
110
+ disabled,
111
+ className: "w-full"
112
+ }
113
+ ) }),
114
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-between gap-2", children: [
115
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1 flex-1", children: [
116
+ /* @__PURE__ */ jsx(Label, { htmlFor: "min-input", className: "text-xs text-muted-foreground", children: "Min" }),
117
+ /* @__PURE__ */ jsx(
118
+ Input,
119
+ {
120
+ id: "min-input",
121
+ type: "number",
122
+ value: minInputValue,
123
+ onChange: handleMinInputChange,
124
+ onBlur: handleMinInputBlur,
125
+ onKeyDown: handleMinInputKeyDown,
126
+ min,
127
+ max: localRange[1],
128
+ step,
129
+ disabled,
130
+ className: "h-7 text-xs [appearance:textfield] [&::-webkit-outer-spin-button]:m-0 [&::-webkit-outer-spin-button]:[-webkit-appearance:none] [&::-webkit-inner-spin-button]:m-0 [&::-webkit-inner-spin-button]:[-webkit-appearance:none]"
131
+ }
132
+ )
133
+ ] }),
134
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1 flex-1", children: [
135
+ /* @__PURE__ */ jsx(Label, { htmlFor: "max-input", className: "text-xs text-muted-foreground text-right", children: "Max" }),
136
+ /* @__PURE__ */ jsx(
137
+ Input,
138
+ {
139
+ id: "max-input",
140
+ type: "number",
141
+ value: maxInputValue,
142
+ onChange: handleMaxInputChange,
143
+ onBlur: handleMaxInputBlur,
144
+ onKeyDown: handleMaxInputKeyDown,
145
+ min: localRange[0],
146
+ max,
147
+ step,
148
+ disabled,
149
+ className: "h-7 text-xs [appearance:textfield] [&::-webkit-outer-spin-button]:m-0 [&::-webkit-outer-spin-button]:[-webkit-appearance:none] [&::-webkit-inner-spin-button]:m-0 [&::-webkit-inner-spin-button]:[-webkit-appearance:none]"
150
+ }
151
+ )
152
+ ] })
153
+ ] }),
154
+ !isDefaultRange && /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx(
155
+ "button",
156
+ {
157
+ onClick: () => {
158
+ setLocalRange([min, max]);
159
+ setMinInputValue(min.toString());
160
+ setMaxInputValue(max.toString());
161
+ handleRangeCommit([min, max]);
162
+ },
163
+ className: "text-xs text-muted-foreground hover:text-foreground transition-colors",
164
+ disabled,
165
+ children: "Reset"
166
+ }
167
+ ) })
168
+ ] });
169
+ if (usePopover) {
170
+ return /* @__PURE__ */ jsxs(Popover, { children: [
171
+ /* @__PURE__ */ jsxs(
172
+ PopoverTrigger,
173
+ {
174
+ className: cn(
175
+ buttonVariants({
176
+ variant: "default",
177
+ size: "default",
178
+ border: "bordered"
179
+ }),
180
+ "!bg-neutral-alpha-2",
181
+ className
182
+ ),
183
+ disabled,
184
+ children: [
185
+ /* @__PURE__ */ jsxs("span", { className: "font-medium", children: [
186
+ label,
187
+ ":"
188
+ ] }),
189
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: showValues && /* @__PURE__ */ jsxs(Fragment, { children: [
190
+ "[",
191
+ formatValue(localRange[0]),
192
+ " - ",
193
+ formatValue(localRange[1]),
194
+ "]"
195
+ ] }) })
196
+ ]
197
+ }
198
+ ),
199
+ /* @__PURE__ */ jsx(PopoverContent, { className: "min-w-96", children: filterContent })
200
+ ] });
201
+ }
202
+ return /* @__PURE__ */ jsx("div", { className, children: filterContent });
203
+ }
204
+
205
+ // src/filters/BooleanFilter.tsx
206
+ import { Switch, Label as Label2 } from "@turtleclub/ui";
207
+ import { useQueryState as useQueryState2, parseAsBoolean } from "nuqs";
208
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
209
+ function BooleanFilter({
210
+ queryKey = "enabled",
211
+ defaultValue = false,
212
+ disabled = false,
213
+ label,
214
+ description,
215
+ className = ""
216
+ }) {
217
+ const [value, setValue] = useQueryState2(queryKey, parseAsBoolean.withDefault(defaultValue));
218
+ const handleToggle = (checked) => {
219
+ setValue(checked === defaultValue ? null : checked);
220
+ };
221
+ return /* @__PURE__ */ jsxs2("div", { className: `flex items-center justify-between space-x-2 ${className}`, children: [
222
+ /* @__PURE__ */ jsxs2("div", { className: "space-y-0.5", children: [
223
+ /* @__PURE__ */ jsx2(Label2, { className: "text-sm font-medium", children: label }),
224
+ description && /* @__PURE__ */ jsx2("p", { className: "text-xs text-muted-foreground", children: description })
225
+ ] }),
226
+ /* @__PURE__ */ jsx2(Switch, { checked: value, onCheckedChange: handleToggle, disabled })
227
+ ] });
228
+ }
229
+
230
+ // src/filters/Filter.tsx
231
+ import { useQueryState as useQueryState3, parseAsString } from "nuqs";
232
+ import { Fragment as Fragment2, jsx as jsx3 } from "react/jsx-runtime";
233
+ function Filter({ queryKey, children, onValueChange }) {
234
+ const [value, setValue] = useQueryState3(queryKey, parseAsString.withDefault(""));
235
+ const handleValueChange = (newValue) => {
236
+ setValue(newValue || null);
237
+ onValueChange?.(newValue);
238
+ };
239
+ return /* @__PURE__ */ jsx3(Fragment2, { children: children({ value, onValueChange: handleValueChange }) });
240
+ }
241
+
242
+ // src/filters/MultiSelectFilter.tsx
243
+ import { useQueryState as useQueryState4, parseAsArrayOf, parseAsString as parseAsString2 } from "nuqs";
244
+ import { Fragment as Fragment3, jsx as jsx4 } from "react/jsx-runtime";
245
+ function MultiSelectFilter({ queryKey, children, onValueChange }) {
246
+ const [value, setValue] = useQueryState4(queryKey, parseAsArrayOf(parseAsString2).withDefault([]));
247
+ const handleValueChange = (newValue) => {
248
+ setValue(newValue.length > 0 ? newValue : null);
249
+ onValueChange?.(newValue);
250
+ };
251
+ return /* @__PURE__ */ jsx4(Fragment3, { children: children({ value, onValueChange: handleValueChange }) });
252
+ }
253
+
254
+ // src/wrappers/FiltersGrid.tsx
255
+ import { cn as cn2 } from "@turtleclub/ui";
256
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
257
+ function FiltersGrid({
258
+ filters,
259
+ columns = 3,
260
+ className,
261
+ sectionClassName
262
+ }) {
263
+ const enabledFilters = filters.filter((filter) => filter.enabled);
264
+ if (enabledFilters.length === 0) {
265
+ return null;
266
+ }
267
+ const sections = enabledFilters.reduce(
268
+ (acc, filter) => {
269
+ const sectionName = filter.section || "default";
270
+ if (!acc[sectionName]) {
271
+ acc[sectionName] = [];
272
+ }
273
+ acc[sectionName].push(filter);
274
+ return acc;
275
+ },
276
+ {}
277
+ );
278
+ const sectionNames = Object.keys(sections);
279
+ return /* @__PURE__ */ jsx5("div", { className: cn2("space-y-4", className), children: sectionNames.map((sectionName, sectionIndex) => /* @__PURE__ */ jsxs3("div", { children: [
280
+ sectionIndex > 0 && /* @__PURE__ */ jsx5("div", { className: "border-t border-border mb-4" }),
281
+ /* @__PURE__ */ jsx5(
282
+ "div",
283
+ {
284
+ className: cn2("grid gap-2", sectionClassName),
285
+ style: {
286
+ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`
287
+ },
288
+ children: sections[sectionName].map((filter) => /* @__PURE__ */ jsx5("div", { className: "flex flex-col gap-2", children: filter.component }, filter.key))
289
+ }
290
+ )
291
+ ] }, sectionName)) });
292
+ }
293
+
294
+ // src/wrappers/FiltersPopover.tsx
295
+ import {
296
+ Popover as Popover2,
297
+ PopoverContent as PopoverContent2,
298
+ PopoverTrigger as PopoverTrigger2,
299
+ buttonVariants as buttonVariants2,
300
+ cn as cn3
301
+ } from "@turtleclub/ui";
302
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
303
+ function FiltersPopover({
304
+ filters,
305
+ triggerLabel = "Filters",
306
+ columns = 2,
307
+ className,
308
+ popoverClassName,
309
+ sectionClassName,
310
+ align = "start",
311
+ onClearAll
312
+ }) {
313
+ const enabledFilters = filters.filter((filter) => filter.enabled);
314
+ if (enabledFilters.length === 0) {
315
+ return null;
316
+ }
317
+ const sections = enabledFilters.reduce(
318
+ (acc, filter) => {
319
+ const sectionName = filter.section || "default";
320
+ if (!acc[sectionName]) {
321
+ acc[sectionName] = [];
322
+ }
323
+ acc[sectionName].push(filter);
324
+ return acc;
325
+ },
326
+ {}
327
+ );
328
+ const sectionNames = Object.keys(sections);
329
+ return /* @__PURE__ */ jsxs4(Popover2, { children: [
330
+ /* @__PURE__ */ jsx6(
331
+ PopoverTrigger2,
332
+ {
333
+ className: cn3(
334
+ buttonVariants2({
335
+ variant: "default",
336
+ size: "default",
337
+ border: "bordered"
338
+ }),
339
+ "!bg-neutral-alpha-2",
340
+ className
341
+ ),
342
+ children: triggerLabel
343
+ }
344
+ ),
345
+ /* @__PURE__ */ jsx6(
346
+ PopoverContent2,
347
+ {
348
+ className: cn3("w-auto min-w-[400px] max-w-[600px]", popoverClassName),
349
+ align,
350
+ children: /* @__PURE__ */ jsxs4("div", { className: "space-y-4", children: [
351
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-center justify-between", children: [
352
+ /* @__PURE__ */ jsx6("h4", { className: "font-medium text-sm", children: "Filters" }),
353
+ onClearAll && /* @__PURE__ */ jsx6(
354
+ "button",
355
+ {
356
+ onClick: onClearAll,
357
+ className: "text-xs text-muted-foreground hover:text-foreground transition-colors",
358
+ children: "Reset all"
359
+ }
360
+ )
361
+ ] }),
362
+ /* @__PURE__ */ jsx6("div", { className: "space-y-4", children: sectionNames.map((sectionName, sectionIndex) => /* @__PURE__ */ jsxs4("div", { children: [
363
+ sectionIndex > 0 && /* @__PURE__ */ jsx6("div", { className: "border-t border-border mb-4" }),
364
+ /* @__PURE__ */ jsx6(
365
+ "div",
366
+ {
367
+ className: cn3("grid gap-2", sectionClassName),
368
+ style: {
369
+ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`
370
+ },
371
+ children: sections[sectionName].map((filter) => /* @__PURE__ */ jsx6("div", { className: "flex flex-col gap-2", children: filter.component }, filter.key))
372
+ }
373
+ )
374
+ ] }, sectionName)) })
375
+ ] })
376
+ }
377
+ )
378
+ ] });
379
+ }
380
+
381
+ // src/wrappers/ActiveFilterBadges.tsx
382
+ import { useMemo } from "react";
383
+ import { Badge as Badge2, Button, cn as cn4 } from "@turtleclub/ui";
384
+ import { X } from "lucide-react";
385
+ import { Fragment as Fragment4, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
386
+ function ActiveFilterBadges({
387
+ filters,
388
+ className,
389
+ clearAllLabel = "Clear All",
390
+ showClearAll = true,
391
+ renderBadge,
392
+ onClearFilter,
393
+ onClearAll
394
+ }) {
395
+ const activeFilters = useMemo(() => {
396
+ return filters.filter((filter) => filter.isActive && filter.value);
397
+ }, [filters]);
398
+ const clearAll = () => {
399
+ const allQueryKeys = activeFilters.map((filter) => filter.queryKeys);
400
+ onClearAll(allQueryKeys);
401
+ };
402
+ if (activeFilters.length === 0) {
403
+ return null;
404
+ }
405
+ return /* @__PURE__ */ jsxs5("div", { className: cn4("flex flex-wrap items-center gap-2", className), children: [
406
+ activeFilters.map((filter) => {
407
+ return /* @__PURE__ */ jsx7(
408
+ FilterBadge,
409
+ {
410
+ filter,
411
+ renderBadge,
412
+ onClearFilter
413
+ },
414
+ filter.key
415
+ );
416
+ }),
417
+ showClearAll && activeFilters.length > 1 && /* @__PURE__ */ jsx7(Button, { variant: "ghost", size: "sm", onClick: clearAll, className: "h-7 px-2 text-xs", children: clearAllLabel })
418
+ ] });
419
+ }
420
+ function FilterBadge({ filter, renderBadge, onClearFilter }) {
421
+ const clearFilter = () => {
422
+ onClearFilter(filter.queryKeys);
423
+ };
424
+ if (renderBadge) {
425
+ return /* @__PURE__ */ jsx7(Fragment4, { children: renderBadge(filter, clearFilter) });
426
+ }
427
+ return /* @__PURE__ */ jsxs5(Badge2, { className: "flex items-center gap-1 pr-1 border-border border", children: [
428
+ /* @__PURE__ */ jsxs5("span", { className: "text-xs", children: [
429
+ filter.label,
430
+ ": ",
431
+ filter.value
432
+ ] }),
433
+ /* @__PURE__ */ jsx7(
434
+ "button",
435
+ {
436
+ onClick: clearFilter,
437
+ className: "ml-1 rounded-full p-0.5 hover:bg-black/10 focus:bg-black/10 focus:outline-none",
438
+ "aria-label": `Clear ${filter.label} filter`,
439
+ children: /* @__PURE__ */ jsx7(X, { className: "h-3 w-3" })
440
+ }
441
+ )
442
+ ] });
443
+ }
444
+
445
+ // src/wrappers/FiltersWrapper.tsx
446
+ import { cn as cn5 } from "@turtleclub/ui";
447
+ import { Fragment as Fragment5, jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
448
+ function FiltersWrapper({
449
+ filters,
450
+ activeFilters,
451
+ layout,
452
+ className,
453
+ badgesClassName,
454
+ slotClassName,
455
+ leftSlot,
456
+ rightSlot,
457
+ columns = 3,
458
+ triggerLabel = "Filters",
459
+ popoverColumns = 2,
460
+ popoverClassName,
461
+ popoverContentClassName,
462
+ align = "start",
463
+ clearAllLabel = "Clear All",
464
+ showClearAll = true,
465
+ renderBadge,
466
+ showActiveBadges = true,
467
+ onClearFilter,
468
+ onClearAll
469
+ }) {
470
+ const hasActiveFilters = activeFilters.some((filter) => filter.isActive && filter.value);
471
+ return /* @__PURE__ */ jsxs6("div", { className: cn5("space-y-3", className), children: [
472
+ /* @__PURE__ */ jsxs6("div", { className: cn5("flex items-center gap-2", slotClassName), children: [
473
+ leftSlot,
474
+ /* @__PURE__ */ jsx8(Fragment5, { children: layout === "grid" ? /* @__PURE__ */ jsx8(FiltersGrid, { filters, columns, className: popoverClassName }) : /* @__PURE__ */ jsx8(
475
+ FiltersPopover,
476
+ {
477
+ filters,
478
+ triggerLabel,
479
+ columns: popoverColumns,
480
+ className: popoverClassName,
481
+ popoverClassName: popoverContentClassName,
482
+ align,
483
+ onClearAll: () => {
484
+ const allQueryKeys = activeFilters.map((filter) => filter.queryKeys).filter((keys) => keys !== void 0);
485
+ onClearAll(allQueryKeys);
486
+ }
487
+ }
488
+ ) }),
489
+ rightSlot
490
+ ] }),
491
+ showActiveBadges && hasActiveFilters && /* @__PURE__ */ jsx8(
492
+ ActiveFilterBadges,
493
+ {
494
+ filters: activeFilters,
495
+ className: badgesClassName,
496
+ clearAllLabel,
497
+ showClearAll,
498
+ renderBadge,
499
+ onClearFilter,
500
+ onClearAll
501
+ }
502
+ )
503
+ ] });
504
+ }
505
+
506
+ // src/helpers/usePagination.ts
507
+ import { useQueryState as useQueryState5, parseAsInteger as parseAsInteger2 } from "nuqs";
508
+ import { useMemo as useMemo2, useCallback } from "react";
509
+ function usePagination(options = {}) {
510
+ const {
511
+ defaultPageSize = 10,
512
+ defaultPageIndex = 0,
513
+ pageIndexKey = "page",
514
+ pageSizeKey = "pageSize"
515
+ } = options;
516
+ const [rawPageIndex, setRawPageIndex] = useQueryState5(
517
+ pageIndexKey,
518
+ parseAsInteger2.withDefault(defaultPageIndex)
519
+ );
520
+ const [rawPageSize, setRawPageSize] = useQueryState5(
521
+ pageSizeKey,
522
+ parseAsInteger2.withDefault(defaultPageSize)
523
+ );
524
+ const pageIndex = useMemo2(() => Math.max(0, rawPageIndex), [rawPageIndex]);
525
+ const pageSize = rawPageSize;
526
+ const pagination = useMemo2(
527
+ () => ({
528
+ pageIndex,
529
+ pageSize
530
+ }),
531
+ [pageIndex, pageSize]
532
+ );
533
+ const setPagination = useCallback(
534
+ (updater) => {
535
+ const newPagination = typeof updater === "function" ? updater(pagination) : updater;
536
+ if (newPagination.pageIndex !== pageIndex) {
537
+ setRawPageIndex(Math.max(0, newPagination.pageIndex));
538
+ }
539
+ if (newPagination.pageSize !== pageSize) {
540
+ setRawPageSize(newPagination.pageSize);
541
+ }
542
+ },
543
+ [pagination, pageIndex, pageSize, setRawPageIndex, setRawPageSize]
544
+ );
545
+ const setPageIndex = useCallback(
546
+ (newPageIndex) => {
547
+ setRawPageIndex(Math.max(0, newPageIndex));
548
+ },
549
+ [setRawPageIndex]
550
+ );
551
+ const setPageSize = useCallback(
552
+ (newPageSize) => {
553
+ setRawPageSize(newPageSize);
554
+ setRawPageIndex(0);
555
+ },
556
+ [setRawPageSize, setRawPageIndex]
557
+ );
558
+ const nextPage = useCallback(
559
+ (totalPages) => {
560
+ const nextPageIndex = pageIndex + 1;
561
+ if (!totalPages || nextPageIndex < totalPages) {
562
+ setPageIndex(nextPageIndex);
563
+ }
564
+ },
565
+ [pageIndex, setPageIndex]
566
+ );
567
+ const previousPage = useCallback(() => {
568
+ if (pageIndex > 0) {
569
+ setPageIndex(pageIndex - 1);
570
+ }
571
+ }, [pageIndex, setPageIndex]);
572
+ const firstPage = useCallback(() => {
573
+ setPageIndex(0);
574
+ }, [setPageIndex]);
575
+ const lastPage = useCallback(
576
+ (totalPages) => {
577
+ setPageIndex(Math.max(0, totalPages - 1));
578
+ },
579
+ [setPageIndex]
580
+ );
581
+ const reset = useCallback(() => {
582
+ setRawPageIndex(defaultPageIndex);
583
+ setRawPageSize(defaultPageSize);
584
+ }, [setRawPageIndex, setRawPageSize, defaultPageIndex, defaultPageSize]);
585
+ const canPreviousPage = pageIndex > 0;
586
+ const canNextPage = useCallback(
587
+ (totalPages) => {
588
+ if (!totalPages) return true;
589
+ return pageIndex < totalPages - 1;
590
+ },
591
+ [pageIndex]
592
+ );
593
+ return {
594
+ pagination,
595
+ setPagination,
596
+ pageIndex,
597
+ pageSize,
598
+ setPageIndex,
599
+ setPageSize,
600
+ nextPage,
601
+ previousPage,
602
+ firstPage,
603
+ lastPage,
604
+ reset,
605
+ canNextPage,
606
+ canPreviousPage
607
+ };
608
+ }
609
+
610
+ // src/helpers/useSorting.ts
611
+ import { useQueryState as useQueryState6, parseAsString as parseAsString3 } from "nuqs";
612
+ import { useMemo as useMemo3, useCallback as useCallback2 } from "react";
613
+ function useSorting(options = {}) {
614
+ const {
615
+ defaultSortBy,
616
+ defaultSortOrder = "asc",
617
+ sortByKey = "sortBy",
618
+ sortOrderKey = "sortOrder"
619
+ } = options;
620
+ const [sortBy, setSortBy] = useQueryState6(
621
+ sortByKey,
622
+ parseAsString3.withDefault(defaultSortBy || "")
623
+ );
624
+ const [sortOrder, setSortOrder] = useQueryState6(
625
+ sortOrderKey,
626
+ parseAsString3.withDefault(defaultSortOrder)
627
+ );
628
+ const validSortOrder = useMemo3(() => {
629
+ return sortOrder === "desc" ? "desc" : "asc";
630
+ }, [sortOrder]);
631
+ const sorting = useMemo3(() => {
632
+ if (!sortBy || sortBy.trim() === "") {
633
+ return [];
634
+ }
635
+ return [
636
+ {
637
+ id: sortBy,
638
+ desc: validSortOrder === "desc"
639
+ }
640
+ ];
641
+ }, [sortBy, validSortOrder]);
642
+ const setSorting = useCallback2(
643
+ (updater) => {
644
+ const newSorting = typeof updater === "function" ? updater(sorting) : updater;
645
+ if (newSorting.length === 0) {
646
+ setSortBy("");
647
+ } else {
648
+ const firstSort = newSorting[0];
649
+ setSortBy(firstSort.id);
650
+ setSortOrder(firstSort.desc ? "desc" : "asc");
651
+ }
652
+ },
653
+ [sorting, setSortBy, setSortOrder]
654
+ );
655
+ const setSort = useCallback2(
656
+ (column, order = "asc") => {
657
+ setSortBy(column);
658
+ setSortOrder(order);
659
+ },
660
+ [setSortBy, setSortOrder]
661
+ );
662
+ const clearSort = useCallback2(() => {
663
+ setSortBy(null);
664
+ }, [setSortBy]);
665
+ const toggleSort = useCallback2(
666
+ (column) => {
667
+ if (sortBy === column) {
668
+ setSortOrder(validSortOrder === "asc" ? "desc" : "asc");
669
+ } else {
670
+ setSortBy(column);
671
+ setSortOrder("asc");
672
+ }
673
+ },
674
+ [sortBy, validSortOrder, setSortBy, setSortOrder]
675
+ );
676
+ const isSorted = useCallback2(
677
+ (column) => {
678
+ return sortBy === column;
679
+ },
680
+ [sortBy]
681
+ );
682
+ const getSortOrder = useCallback2(
683
+ (column) => {
684
+ return sortBy === column ? validSortOrder : void 0;
685
+ },
686
+ [sortBy, validSortOrder]
687
+ );
688
+ return {
689
+ sorting,
690
+ setSorting,
691
+ sortBy: sortBy || void 0,
692
+ sortOrder: validSortOrder,
693
+ setSort,
694
+ clearSort,
695
+ toggleSort,
696
+ isSorted,
697
+ getSortOrder
698
+ };
699
+ }
700
+
701
+ // src/selectors/ChainsSelector.tsx
702
+ import { useMemo as useMemo4 } from "react";
703
+ import { MultiSelect } from "@turtleclub/ui";
704
+ import { useSupportedChains } from "@turtleclub/hooks";
705
+ import { jsx as jsx9 } from "react/jsx-runtime";
706
+ function ChainsSelector({
707
+ value,
708
+ onValueChange,
709
+ placeholder = "Select chains",
710
+ disabled = false,
711
+ className,
712
+ maxCount = 1,
713
+ closeOnSelect = true,
714
+ searchable = true
715
+ }) {
716
+ const { chains, isLoading } = useSupportedChains({
717
+ page: 1,
718
+ limit: 100
719
+ // High limit to fetch all chains
720
+ });
721
+ const chainOptions = useMemo4(() => {
722
+ if (!chains || chains.length === 0) return [];
723
+ const filteredChains = chains.filter((chain) => chain.status === "active");
724
+ return filteredChains.map((chain) => ({
725
+ label: chain.name,
726
+ value: chain?.id ?? "",
727
+ icon: chain.logoUrl ? () => /* @__PURE__ */ jsx9(
728
+ "img",
729
+ {
730
+ src: chain.logoUrl,
731
+ alt: chain.name,
732
+ width: 16,
733
+ height: 16,
734
+ className: "size-4 rounded-full"
735
+ }
736
+ ) : void 0
737
+ }));
738
+ }, [chains]);
739
+ return /* @__PURE__ */ jsx9(
740
+ MultiSelect,
741
+ {
742
+ searchable,
743
+ options: chainOptions,
744
+ value,
745
+ onValueChange,
746
+ disabled: disabled || isLoading,
747
+ placeholder: isLoading ? "Loading chains..." : placeholder,
748
+ closeOnSelect,
749
+ maxCount,
750
+ className
751
+ }
752
+ );
753
+ }
754
+
755
+ // src/selectors/TokensSelector.tsx
756
+ import { useEffect as useEffect2, useMemo as useMemo5 } from "react";
757
+ import { MultiSelect as MultiSelect2 } from "@turtleclub/ui";
758
+ import { useSupportedTokens } from "@turtleclub/hooks";
759
+ import { jsx as jsx10 } from "react/jsx-runtime";
760
+ function TokensSelector({
761
+ value,
762
+ onValueChange,
763
+ chainId,
764
+ placeholder = "Select tokens",
765
+ disabled = false,
766
+ className,
767
+ maxCount = 1,
768
+ closeOnSelect = true,
769
+ searchable = true
770
+ }) {
771
+ const isChainSelected = !!chainId && chainId.trim() !== "";
772
+ const { tokens, isLoading } = useSupportedTokens({
773
+ chainId: isChainSelected ? chainId : "",
774
+ limit: 9e3,
775
+ enabled: isChainSelected
776
+ });
777
+ const tokenOptions = useMemo5(() => {
778
+ if (!tokens) return [];
779
+ return tokens.map((token) => ({
780
+ label: `${token.symbol} - ${token.name}`,
781
+ value: token?.id || "",
782
+ icon: token.logoUrl ? () => /* @__PURE__ */ jsx10(
783
+ "img",
784
+ {
785
+ src: token.logoUrl,
786
+ alt: token.name,
787
+ width: 16,
788
+ height: 16,
789
+ className: "size-4 rounded-full"
790
+ }
791
+ ) : void 0
792
+ }));
793
+ }, [tokens]);
794
+ useEffect2(() => {
795
+ if (!isChainSelected && value.length > 0) {
796
+ onValueChange([]);
797
+ }
798
+ }, [isChainSelected, value, onValueChange]);
799
+ return /* @__PURE__ */ jsx10(
800
+ MultiSelect2,
801
+ {
802
+ searchable,
803
+ options: tokenOptions,
804
+ value,
805
+ onValueChange,
806
+ disabled: disabled || isLoading || !isChainSelected,
807
+ placeholder: !isChainSelected ? "Select a chain first" : isLoading ? "Loading tokens..." : placeholder,
808
+ closeOnSelect,
809
+ maxCount,
810
+ className
811
+ }
812
+ );
813
+ }
814
+
815
+ // src/selectors/ProductsSelector.tsx
816
+ import { useMemo as useMemo6 } from "react";
817
+ import { MultiSelect as MultiSelect3 } from "@turtleclub/ui";
818
+ import { useProducts } from "@turtleclub/hooks";
819
+ import { jsx as jsx11 } from "react/jsx-runtime";
820
+ function ProductsSelector({
821
+ value,
822
+ onValueChange,
823
+ placeholder = "Select products",
824
+ disabled = false,
825
+ className,
826
+ maxCount = 1,
827
+ closeOnSelect = true,
828
+ searchable = true
829
+ }) {
830
+ const { data: productsData, isLoading } = useProducts({});
831
+ const products = useMemo6(() => productsData?.products ?? [], [productsData?.products]);
832
+ const productOptions = useMemo6(() => {
833
+ if (!products || products.length === 0) return [];
834
+ return products.map((product) => ({
835
+ label: product.name,
836
+ value: product.id,
837
+ icon: product.logoUrl ? () => /* @__PURE__ */ jsx11(
838
+ "img",
839
+ {
840
+ src: product.logoUrl,
841
+ alt: product.name,
842
+ width: 16,
843
+ height: 16,
844
+ className: "size-4 rounded-full"
845
+ }
846
+ ) : void 0
847
+ }));
848
+ }, [products]);
849
+ return /* @__PURE__ */ jsx11(
850
+ MultiSelect3,
851
+ {
852
+ searchable,
853
+ options: productOptions,
854
+ value,
855
+ onValueChange,
856
+ disabled: disabled || isLoading,
857
+ placeholder: isLoading ? "Loading products..." : placeholder,
858
+ closeOnSelect,
859
+ maxCount,
860
+ className
861
+ }
862
+ );
863
+ }
864
+
865
+ // src/selectors/OpportunitiesSelector.tsx
866
+ import { useMemo as useMemo7 } from "react";
867
+ import { MultiSelect as MultiSelect4 } from "@turtleclub/ui";
868
+ import { useOpportunities } from "@turtleclub/hooks";
869
+ import { jsx as jsx12 } from "react/jsx-runtime";
870
+ function OpportunitiesSelector({
871
+ value,
872
+ onValueChange,
873
+ placeholder = "Select opportunities",
874
+ disabled = false,
875
+ className,
876
+ maxCount = 1,
877
+ closeOnSelect = true,
878
+ searchable = true
879
+ }) {
880
+ const { data: opportunitiesData, isLoading } = useOpportunities();
881
+ const opportunities = useMemo7(
882
+ () => opportunitiesData?.opportunities ?? [],
883
+ [opportunitiesData?.opportunities]
884
+ );
885
+ const opportunityOptions = useMemo7(() => {
886
+ if (!opportunities || opportunities.length === 0) return [];
887
+ const filteredOpportunities = opportunities.filter((opp) => opp.status === "active");
888
+ return filteredOpportunities.map((opportunity) => ({
889
+ label: opportunity.name || opportunity.shortName,
890
+ value: opportunity.id,
891
+ description: opportunity.shortName !== opportunity.name ? opportunity.shortName : void 0,
892
+ icon: opportunity.depositTokens?.[0]?.logoUrl ? () => /* @__PURE__ */ jsx12(
893
+ "img",
894
+ {
895
+ src: opportunity.depositTokens[0].logoUrl,
896
+ alt: opportunity.name,
897
+ width: 16,
898
+ height: 16,
899
+ className: "size-4 rounded-full"
900
+ }
901
+ ) : void 0
902
+ }));
903
+ }, [opportunities]);
904
+ return /* @__PURE__ */ jsx12(
905
+ MultiSelect4,
906
+ {
907
+ searchable,
908
+ options: opportunityOptions,
909
+ value,
910
+ onValueChange,
911
+ disabled: disabled || isLoading,
912
+ placeholder: isLoading ? "Loading opportunities..." : placeholder,
913
+ closeOnSelect,
914
+ maxCount,
915
+ className
916
+ }
917
+ );
918
+ }
919
+
920
+ // src/selectors/ChainSelector.tsx
921
+ import { useMemo as useMemo8 } from "react";
922
+ import { Combobox } from "@turtleclub/ui";
923
+ import { useSupportedChains as useSupportedChains2 } from "@turtleclub/hooks";
924
+ import { jsx as jsx13 } from "react/jsx-runtime";
925
+ function ChainSelector({
926
+ value,
927
+ onValueChange,
928
+ placeholder = "Select chain",
929
+ disabled = false,
930
+ className,
931
+ closeOnSelect = true,
932
+ searchable = true
933
+ }) {
934
+ const { chains, isLoading } = useSupportedChains2({
935
+ page: 1,
936
+ limit: 100
937
+ // High limit to fetch all chains
938
+ });
939
+ const chainOptions = useMemo8(() => {
940
+ if (!chains || chains.length === 0) return [];
941
+ const filteredChains = chains.filter((chain) => chain.status === "active");
942
+ return filteredChains.map((chain) => ({
943
+ label: chain.name,
944
+ value: chain?.id ?? "",
945
+ icon: chain.logoUrl ? () => /* @__PURE__ */ jsx13(
946
+ "img",
947
+ {
948
+ src: chain.logoUrl,
949
+ alt: chain.name,
950
+ width: 16,
951
+ height: 16,
952
+ className: "size-4 rounded-full"
953
+ }
954
+ ) : void 0
955
+ }));
956
+ }, [chains]);
957
+ return /* @__PURE__ */ jsx13(
958
+ Combobox,
959
+ {
960
+ searchable,
961
+ options: chainOptions,
962
+ value,
963
+ onValueChange,
964
+ disabled: disabled || isLoading,
965
+ placeholder: isLoading ? "Loading chains..." : placeholder,
966
+ closeOnSelect,
967
+ className
968
+ }
969
+ );
970
+ }
971
+
972
+ // src/selectors/TokenSelector.tsx
973
+ import { useEffect as useEffect3, useMemo as useMemo9 } from "react";
974
+ import { Combobox as Combobox2 } from "@turtleclub/ui";
975
+ import { useSupportedTokens as useSupportedTokens2 } from "@turtleclub/hooks";
976
+ import { jsx as jsx14 } from "react/jsx-runtime";
977
+ function TokenSelector({
978
+ value,
979
+ onValueChange,
980
+ chainId,
981
+ placeholder = "Select token",
982
+ disabled = false,
983
+ className,
984
+ closeOnSelect = true,
985
+ searchable = true
986
+ }) {
987
+ const isChainSelected = !!chainId && chainId.trim() !== "";
988
+ const { tokens, isLoading } = useSupportedTokens2({
989
+ chainId: isChainSelected ? chainId : "",
990
+ limit: 9e3,
991
+ enabled: isChainSelected
992
+ });
993
+ const tokenOptions = useMemo9(() => {
994
+ if (!tokens) return [];
995
+ return tokens.map((token) => ({
996
+ label: `${token.symbol} - ${token.name}`,
997
+ value: token?.id || "",
998
+ icon: token.logoUrl ? () => /* @__PURE__ */ jsx14(
999
+ "img",
1000
+ {
1001
+ src: token.logoUrl,
1002
+ alt: token.name,
1003
+ width: 16,
1004
+ height: 16,
1005
+ className: "size-4 rounded-full"
1006
+ }
1007
+ ) : void 0
1008
+ }));
1009
+ }, [tokens]);
1010
+ useEffect3(() => {
1011
+ if (!isChainSelected && value) {
1012
+ onValueChange("");
1013
+ }
1014
+ }, [isChainSelected, value, onValueChange]);
1015
+ return /* @__PURE__ */ jsx14(
1016
+ Combobox2,
1017
+ {
1018
+ searchable,
1019
+ options: tokenOptions,
1020
+ value,
1021
+ onValueChange,
1022
+ disabled: disabled || isLoading || !isChainSelected,
1023
+ placeholder: !isChainSelected ? "Select a chain first" : isLoading ? "Loading tokens..." : placeholder,
1024
+ closeOnSelect,
1025
+ className
1026
+ }
1027
+ );
1028
+ }
1029
+
1030
+ // src/selectors/ProductSelector.tsx
1031
+ import { useMemo as useMemo10 } from "react";
1032
+ import { Combobox as Combobox3 } from "@turtleclub/ui";
1033
+ import { useProducts as useProducts2 } from "@turtleclub/hooks";
1034
+ import { jsx as jsx15 } from "react/jsx-runtime";
1035
+ function ProductSelector({
1036
+ value,
1037
+ onValueChange,
1038
+ placeholder = "Select product",
1039
+ disabled = false,
1040
+ className,
1041
+ closeOnSelect = true,
1042
+ searchable = true
1043
+ }) {
1044
+ const { data: productsData, isLoading } = useProducts2({});
1045
+ const products = useMemo10(() => productsData?.products ?? [], [productsData?.products]);
1046
+ const productOptions = useMemo10(() => {
1047
+ if (!products || products.length === 0) return [];
1048
+ return products.map((product) => ({
1049
+ label: product.name,
1050
+ value: product.id,
1051
+ icon: product.logoUrl ? () => /* @__PURE__ */ jsx15(
1052
+ "img",
1053
+ {
1054
+ src: product.logoUrl,
1055
+ alt: product.name,
1056
+ width: 16,
1057
+ height: 16,
1058
+ className: "size-4 rounded-full"
1059
+ }
1060
+ ) : void 0
1061
+ }));
1062
+ }, [products]);
1063
+ return /* @__PURE__ */ jsx15(
1064
+ Combobox3,
1065
+ {
1066
+ searchable,
1067
+ options: productOptions,
1068
+ value,
1069
+ onValueChange,
1070
+ disabled: disabled || isLoading,
1071
+ placeholder: isLoading ? "Loading products..." : placeholder,
1072
+ closeOnSelect,
1073
+ className
1074
+ }
1075
+ );
1076
+ }
1077
+
1078
+ // src/selectors/OpportunitySelector.tsx
1079
+ import { useMemo as useMemo11 } from "react";
1080
+ import { Combobox as Combobox4 } from "@turtleclub/ui";
1081
+ import { useOpportunities as useOpportunities2 } from "@turtleclub/hooks";
1082
+ import { jsx as jsx16 } from "react/jsx-runtime";
1083
+ function OpportunitySelector({
1084
+ value,
1085
+ onValueChange,
1086
+ placeholder = "Select opportunity",
1087
+ disabled = false,
1088
+ className,
1089
+ closeOnSelect = true,
1090
+ searchable = true
1091
+ }) {
1092
+ const { data: opportunitiesData, isLoading } = useOpportunities2();
1093
+ const opportunities = useMemo11(
1094
+ () => opportunitiesData?.opportunities ?? [],
1095
+ [opportunitiesData?.opportunities]
1096
+ );
1097
+ const opportunityOptions = useMemo11(() => {
1098
+ if (!opportunities || opportunities.length === 0) return [];
1099
+ const filteredOpportunities = opportunities.filter((opp) => opp.status === "active");
1100
+ return filteredOpportunities.map((opportunity) => ({
1101
+ label: opportunity.name || opportunity.shortName,
1102
+ value: opportunity.id,
1103
+ description: opportunity.shortName !== opportunity.name ? opportunity.shortName : void 0,
1104
+ icon: opportunity.depositTokens?.[0]?.logoUrl ? () => /* @__PURE__ */ jsx16(
1105
+ "img",
1106
+ {
1107
+ src: opportunity.depositTokens[0].logoUrl,
1108
+ alt: opportunity.name,
1109
+ width: 16,
1110
+ height: 16,
1111
+ className: "size-4 rounded-full"
1112
+ }
1113
+ ) : void 0
1114
+ }));
1115
+ }, [opportunities]);
1116
+ return /* @__PURE__ */ jsx16(
1117
+ Combobox4,
1118
+ {
1119
+ searchable,
1120
+ options: opportunityOptions,
1121
+ value,
1122
+ onValueChange,
1123
+ disabled: disabled || isLoading,
1124
+ placeholder: isLoading ? "Loading opportunities..." : placeholder,
1125
+ closeOnSelect,
1126
+ className
1127
+ }
1128
+ );
1129
+ }
1130
+ export {
1131
+ ActiveFilterBadges,
1132
+ BooleanFilter,
1133
+ ChainSelector,
1134
+ ChainsSelector,
1135
+ Filter,
1136
+ FiltersGrid,
1137
+ FiltersPopover,
1138
+ FiltersWrapper,
1139
+ MultiSelectFilter,
1140
+ OpportunitiesSelector,
1141
+ OpportunitySelector,
1142
+ ProductSelector,
1143
+ ProductsSelector,
1144
+ RangeSliderFilter,
1145
+ TokenSelector,
1146
+ TokensSelector,
1147
+ usePagination,
1148
+ useSorting
1149
+ };
1150
+ //# sourceMappingURL=index.js.map