@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.cjs +1179 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +574 -0
- package/dist/index.d.ts +574 -0
- package/dist/index.js +1150 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
- package/src/filters/BooleanFilter.tsx +46 -0
- package/src/filters/Filter.tsx +44 -0
- package/src/filters/MultiSelectFilter.tsx +46 -0
- package/src/filters/RangeSliderFilter.tsx +250 -0
- package/src/filters/index.ts +4 -0
- package/src/helpers/index.ts +5 -0
- package/src/helpers/usePagination.ts +210 -0
- package/src/helpers/useSorting.ts +185 -0
- package/src/index.ts +4 -0
- package/src/selectors/ChainSelector.tsx +72 -0
- package/src/selectors/ChainsSelector.tsx +76 -0
- package/src/selectors/OpportunitiesSelector.tsx +79 -0
- package/src/selectors/OpportunitySelector.tsx +75 -0
- package/src/selectors/ProductSelector.tsx +69 -0
- package/src/selectors/ProductsSelector.tsx +73 -0
- package/src/selectors/TokenSelector.tsx +85 -0
- package/src/selectors/TokensSelector.tsx +89 -0
- package/src/selectors/index.ts +11 -0
- package/src/wrappers/ActiveFilterBadges.tsx +139 -0
- package/src/wrappers/FiltersGrid.tsx +69 -0
- package/src/wrappers/FiltersPopover.tsx +113 -0
- package/src/wrappers/FiltersWrapper.tsx +159 -0
- package/src/wrappers/README.md +272 -0
- package/src/wrappers/index.ts +8 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useQueryState, parseAsInteger } from "nuqs";
|
|
4
|
+
import { useMemo, useCallback } from "react";
|
|
5
|
+
import type { PaginationState } from "@tanstack/react-table";
|
|
6
|
+
|
|
7
|
+
export interface UsePaginationOptions {
|
|
8
|
+
/** Default page size (default: 10) */
|
|
9
|
+
defaultPageSize?: number;
|
|
10
|
+
/** Default page index (default: 0) */
|
|
11
|
+
defaultPageIndex?: number;
|
|
12
|
+
/** Query param key for page index (default: 'page') */
|
|
13
|
+
pageIndexKey?: string;
|
|
14
|
+
/** Query param key for page size (default: 'pageSize') */
|
|
15
|
+
pageSizeKey?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface UsePaginationReturn {
|
|
19
|
+
/** Current pagination state for TanStack Table */
|
|
20
|
+
pagination: PaginationState;
|
|
21
|
+
/** Function to update pagination state */
|
|
22
|
+
setPagination: (updater: PaginationState | ((prev: PaginationState) => PaginationState)) => void;
|
|
23
|
+
/** Current page index (0-based) */
|
|
24
|
+
pageIndex: number;
|
|
25
|
+
/** Current page size */
|
|
26
|
+
pageSize: number;
|
|
27
|
+
/** Set page index directly */
|
|
28
|
+
setPageIndex: (pageIndex: number) => void;
|
|
29
|
+
/** Set page size directly */
|
|
30
|
+
setPageSize: (pageSize: number) => void;
|
|
31
|
+
/** Go to next page */
|
|
32
|
+
nextPage: (totalPages?: number) => void;
|
|
33
|
+
/** Go to previous page */
|
|
34
|
+
previousPage: () => void;
|
|
35
|
+
/** Go to first page */
|
|
36
|
+
firstPage: () => void;
|
|
37
|
+
/** Go to last page */
|
|
38
|
+
lastPage: (totalPages: number) => void;
|
|
39
|
+
/** Reset pagination to defaults */
|
|
40
|
+
reset: () => void;
|
|
41
|
+
/** Check if can go to next page */
|
|
42
|
+
canNextPage: (totalPages?: number) => boolean;
|
|
43
|
+
/** Check if can go to previous page */
|
|
44
|
+
canPreviousPage: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Hook for managing server-side pagination state using nuqs.
|
|
49
|
+
*
|
|
50
|
+
* This hook synchronizes pagination state with URL query parameters,
|
|
51
|
+
* making it perfect for server-side pagination where the URL should
|
|
52
|
+
* reflect the current page and page size.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```tsx
|
|
56
|
+
* function MyTable() {
|
|
57
|
+
* const {
|
|
58
|
+
* pagination,
|
|
59
|
+
* setPagination,
|
|
60
|
+
* pageIndex,
|
|
61
|
+
* pageSize,
|
|
62
|
+
* setPageIndex,
|
|
63
|
+
* setPageSize
|
|
64
|
+
* } = usePagination({
|
|
65
|
+
* defaultPageSize: 20,
|
|
66
|
+
* pageIndexKey: 'page',
|
|
67
|
+
* pageSizeKey: 'limit'
|
|
68
|
+
* });
|
|
69
|
+
*
|
|
70
|
+
* // Use with TanStack Table
|
|
71
|
+
* const table = useReactTable({
|
|
72
|
+
* data,
|
|
73
|
+
* columns,
|
|
74
|
+
* manualPagination: true,
|
|
75
|
+
* pagination,
|
|
76
|
+
* onPaginationChange: setPagination,
|
|
77
|
+
* pageCount: Math.ceil(totalCount / pageSize),
|
|
78
|
+
* });
|
|
79
|
+
*
|
|
80
|
+
* return <DataTable table={table} />;
|
|
81
|
+
* }
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export function usePagination(options: UsePaginationOptions = {}): UsePaginationReturn {
|
|
85
|
+
const {
|
|
86
|
+
defaultPageSize = 10,
|
|
87
|
+
defaultPageIndex = 0,
|
|
88
|
+
pageIndexKey = "page",
|
|
89
|
+
pageSizeKey = "pageSize",
|
|
90
|
+
} = options;
|
|
91
|
+
|
|
92
|
+
// Use nuqs to manage query state
|
|
93
|
+
const [rawPageIndex, setRawPageIndex] = useQueryState(
|
|
94
|
+
pageIndexKey,
|
|
95
|
+
parseAsInteger.withDefault(defaultPageIndex)
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const [rawPageSize, setRawPageSize] = useQueryState(
|
|
99
|
+
pageSizeKey,
|
|
100
|
+
parseAsInteger.withDefault(defaultPageSize)
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// Ensure page index is never negative
|
|
104
|
+
const pageIndex = useMemo(() => Math.max(0, rawPageIndex), [rawPageIndex]);
|
|
105
|
+
|
|
106
|
+
// Use page size directly from query state
|
|
107
|
+
const pageSize = rawPageSize;
|
|
108
|
+
|
|
109
|
+
// Create pagination state for TanStack Table
|
|
110
|
+
const pagination = useMemo<PaginationState>(
|
|
111
|
+
() => ({
|
|
112
|
+
pageIndex,
|
|
113
|
+
pageSize,
|
|
114
|
+
}),
|
|
115
|
+
[pageIndex, pageSize]
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// Set pagination function that handles both object and function updates
|
|
119
|
+
const setPagination = useCallback(
|
|
120
|
+
(updater: PaginationState | ((prev: PaginationState) => PaginationState)) => {
|
|
121
|
+
const newPagination = typeof updater === "function" ? updater(pagination) : updater;
|
|
122
|
+
|
|
123
|
+
if (newPagination.pageIndex !== pageIndex) {
|
|
124
|
+
setRawPageIndex(Math.max(0, newPagination.pageIndex));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (newPagination.pageSize !== pageSize) {
|
|
128
|
+
setRawPageSize(newPagination.pageSize);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
[pagination, pageIndex, pageSize, setRawPageIndex, setRawPageSize]
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// Individual setters
|
|
135
|
+
const setPageIndex = useCallback(
|
|
136
|
+
(newPageIndex: number) => {
|
|
137
|
+
setRawPageIndex(Math.max(0, newPageIndex));
|
|
138
|
+
},
|
|
139
|
+
[setRawPageIndex]
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const setPageSize = useCallback(
|
|
143
|
+
(newPageSize: number) => {
|
|
144
|
+
setRawPageSize(newPageSize);
|
|
145
|
+
// Reset to first page when page size changes
|
|
146
|
+
setRawPageIndex(0);
|
|
147
|
+
},
|
|
148
|
+
[setRawPageSize, setRawPageIndex]
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// Navigation functions
|
|
152
|
+
const nextPage = useCallback(
|
|
153
|
+
(totalPages?: number) => {
|
|
154
|
+
const nextPageIndex = pageIndex + 1;
|
|
155
|
+
if (!totalPages || nextPageIndex < totalPages) {
|
|
156
|
+
setPageIndex(nextPageIndex);
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
[pageIndex, setPageIndex]
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const previousPage = useCallback(() => {
|
|
163
|
+
if (pageIndex > 0) {
|
|
164
|
+
setPageIndex(pageIndex - 1);
|
|
165
|
+
}
|
|
166
|
+
}, [pageIndex, setPageIndex]);
|
|
167
|
+
|
|
168
|
+
const firstPage = useCallback(() => {
|
|
169
|
+
setPageIndex(0);
|
|
170
|
+
}, [setPageIndex]);
|
|
171
|
+
|
|
172
|
+
const lastPage = useCallback(
|
|
173
|
+
(totalPages: number) => {
|
|
174
|
+
setPageIndex(Math.max(0, totalPages - 1));
|
|
175
|
+
},
|
|
176
|
+
[setPageIndex]
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// Reset function
|
|
180
|
+
const reset = useCallback(() => {
|
|
181
|
+
setRawPageIndex(defaultPageIndex);
|
|
182
|
+
setRawPageSize(defaultPageSize);
|
|
183
|
+
}, [setRawPageIndex, setRawPageSize, defaultPageIndex, defaultPageSize]);
|
|
184
|
+
|
|
185
|
+
// Navigation state
|
|
186
|
+
const canPreviousPage = pageIndex > 0;
|
|
187
|
+
const canNextPage = useCallback(
|
|
188
|
+
(totalPages?: number) => {
|
|
189
|
+
if (!totalPages) return true; // Assume we can go next if total pages unknown
|
|
190
|
+
return pageIndex < totalPages - 1;
|
|
191
|
+
},
|
|
192
|
+
[pageIndex]
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
pagination,
|
|
197
|
+
setPagination,
|
|
198
|
+
pageIndex,
|
|
199
|
+
pageSize,
|
|
200
|
+
setPageIndex,
|
|
201
|
+
setPageSize,
|
|
202
|
+
nextPage,
|
|
203
|
+
previousPage,
|
|
204
|
+
firstPage,
|
|
205
|
+
lastPage,
|
|
206
|
+
reset,
|
|
207
|
+
canNextPage,
|
|
208
|
+
canPreviousPage,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useQueryState, parseAsString } from "nuqs";
|
|
4
|
+
import { useMemo, useCallback } from "react";
|
|
5
|
+
import type { SortingState } from "@tanstack/react-table";
|
|
6
|
+
|
|
7
|
+
export interface UseSortingOptions {
|
|
8
|
+
/** Default sort column (default: undefined - no sorting) */
|
|
9
|
+
defaultSortBy?: string;
|
|
10
|
+
/** Default sort order (default: 'asc') */
|
|
11
|
+
defaultSortOrder?: "asc" | "desc";
|
|
12
|
+
/** Query param key for sort column (default: 'sortBy') */
|
|
13
|
+
sortByKey?: string;
|
|
14
|
+
/** Query param key for sort order (default: 'sortOrder') */
|
|
15
|
+
sortOrderKey?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface UseSortingReturn {
|
|
19
|
+
/** Current sorting state for TanStack Table */
|
|
20
|
+
sorting: SortingState;
|
|
21
|
+
/** Function to update sorting state */
|
|
22
|
+
setSorting: (updater: SortingState | ((prev: SortingState) => SortingState)) => void;
|
|
23
|
+
/** Current sort column (undefined if no sorting) */
|
|
24
|
+
sortBy: string | undefined;
|
|
25
|
+
/** Current sort order */
|
|
26
|
+
sortOrder: "asc" | "desc";
|
|
27
|
+
/** Set sort column and order directly */
|
|
28
|
+
setSort: (column: string, order?: "asc" | "desc") => void;
|
|
29
|
+
/** Clear all sorting */
|
|
30
|
+
clearSort: () => void;
|
|
31
|
+
/** Toggle sort order for a column */
|
|
32
|
+
toggleSort: (column: string) => void;
|
|
33
|
+
/** Check if a column is currently sorted */
|
|
34
|
+
isSorted: (column: string) => boolean;
|
|
35
|
+
/** Get sort order for a column */
|
|
36
|
+
getSortOrder: (column: string) => "asc" | "desc" | undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Hook for managing server-side sorting state using nuqs.
|
|
41
|
+
*
|
|
42
|
+
* This hook synchronizes sorting state with URL query parameters,
|
|
43
|
+
* making it perfect for server-side sorting where the URL should
|
|
44
|
+
* reflect the current sort state.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```tsx
|
|
48
|
+
* function MyTable() {
|
|
49
|
+
* const {
|
|
50
|
+
* sorting,
|
|
51
|
+
* setSorting,
|
|
52
|
+
* sortBy,
|
|
53
|
+
* sortOrder,
|
|
54
|
+
* setSort,
|
|
55
|
+
* toggleSort
|
|
56
|
+
* } = useSorting({
|
|
57
|
+
* defaultSortBy: 'name',
|
|
58
|
+
* defaultSortOrder: 'asc'
|
|
59
|
+
* });
|
|
60
|
+
*
|
|
61
|
+
* // Use with TanStack Table
|
|
62
|
+
* const table = useReactTable({
|
|
63
|
+
* data,
|
|
64
|
+
* columns,
|
|
65
|
+
* manualSorting: true,
|
|
66
|
+
* sorting,
|
|
67
|
+
* onSortingChange: setSorting,
|
|
68
|
+
* });
|
|
69
|
+
*
|
|
70
|
+
* return <DataTable table={table} />;
|
|
71
|
+
* }
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export function useSorting(options: UseSortingOptions = {}): UseSortingReturn {
|
|
75
|
+
const {
|
|
76
|
+
defaultSortBy,
|
|
77
|
+
defaultSortOrder = "asc",
|
|
78
|
+
sortByKey = "sortBy",
|
|
79
|
+
sortOrderKey = "sortOrder",
|
|
80
|
+
} = options;
|
|
81
|
+
|
|
82
|
+
// Use nuqs to manage query state
|
|
83
|
+
const [sortBy, setSortBy] = useQueryState(
|
|
84
|
+
sortByKey,
|
|
85
|
+
parseAsString.withDefault(defaultSortBy || "")
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const [sortOrder, setSortOrder] = useQueryState(
|
|
89
|
+
sortOrderKey,
|
|
90
|
+
parseAsString.withDefault(defaultSortOrder)
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// Ensure sort order is valid
|
|
94
|
+
const validSortOrder = useMemo(() => {
|
|
95
|
+
return sortOrder === "desc" ? "desc" : "asc";
|
|
96
|
+
}, [sortOrder]);
|
|
97
|
+
|
|
98
|
+
// Create sorting state for TanStack Table
|
|
99
|
+
const sorting = useMemo<SortingState>(() => {
|
|
100
|
+
if (!sortBy || sortBy.trim() === "") {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
return [
|
|
104
|
+
{
|
|
105
|
+
id: sortBy,
|
|
106
|
+
desc: validSortOrder === "desc",
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
}, [sortBy, validSortOrder]);
|
|
110
|
+
|
|
111
|
+
// Set sorting function that handles both object and function updates
|
|
112
|
+
const setSorting = useCallback(
|
|
113
|
+
(updater: SortingState | ((prev: SortingState) => SortingState)) => {
|
|
114
|
+
const newSorting = typeof updater === "function" ? updater(sorting) : updater;
|
|
115
|
+
|
|
116
|
+
if (newSorting.length === 0) {
|
|
117
|
+
// Clear sorting
|
|
118
|
+
setSortBy("");
|
|
119
|
+
} else {
|
|
120
|
+
// Set first sort (TanStack Table typically only has one sort at a time)
|
|
121
|
+
const firstSort = newSorting[0];
|
|
122
|
+
setSortBy(firstSort.id);
|
|
123
|
+
setSortOrder(firstSort.desc ? "desc" : "asc");
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
[sorting, setSortBy, setSortOrder]
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// Set sort column and order directly
|
|
130
|
+
const setSort = useCallback(
|
|
131
|
+
(column: string, order: "asc" | "desc" = "asc") => {
|
|
132
|
+
setSortBy(column);
|
|
133
|
+
setSortOrder(order);
|
|
134
|
+
},
|
|
135
|
+
[setSortBy, setSortOrder]
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// Clear all sorting
|
|
139
|
+
const clearSort = useCallback(() => {
|
|
140
|
+
setSortBy(null);
|
|
141
|
+
}, [setSortBy]);
|
|
142
|
+
|
|
143
|
+
// Toggle sort order for a column
|
|
144
|
+
const toggleSort = useCallback(
|
|
145
|
+
(column: string) => {
|
|
146
|
+
if (sortBy === column) {
|
|
147
|
+
// Same column, toggle order
|
|
148
|
+
setSortOrder(validSortOrder === "asc" ? "desc" : "asc");
|
|
149
|
+
} else {
|
|
150
|
+
// Different column, set new column with asc
|
|
151
|
+
setSortBy(column);
|
|
152
|
+
setSortOrder("asc");
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
[sortBy, validSortOrder, setSortBy, setSortOrder]
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// Check if a column is currently sorted
|
|
159
|
+
const isSorted = useCallback(
|
|
160
|
+
(column: string) => {
|
|
161
|
+
return sortBy === column;
|
|
162
|
+
},
|
|
163
|
+
[sortBy]
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// Get sort order for a column
|
|
167
|
+
const getSortOrder = useCallback(
|
|
168
|
+
(column: string) => {
|
|
169
|
+
return sortBy === column ? validSortOrder : undefined;
|
|
170
|
+
},
|
|
171
|
+
[sortBy, validSortOrder]
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
sorting,
|
|
176
|
+
setSorting,
|
|
177
|
+
sortBy: sortBy || undefined,
|
|
178
|
+
sortOrder: validSortOrder,
|
|
179
|
+
setSort,
|
|
180
|
+
clearSort,
|
|
181
|
+
toggleSort,
|
|
182
|
+
isSorted,
|
|
183
|
+
getSortOrder,
|
|
184
|
+
};
|
|
185
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { Combobox } from "@turtleclub/ui";
|
|
5
|
+
import { useSupportedChains } from "@turtleclub/hooks";
|
|
6
|
+
|
|
7
|
+
interface ChainSelectorProps {
|
|
8
|
+
/** Selected chain ID */
|
|
9
|
+
value: string;
|
|
10
|
+
/** Callback when selection changes */
|
|
11
|
+
onValueChange: (value: string) => void;
|
|
12
|
+
/** Placeholder text */
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
/** Whether the selector is disabled */
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
/** Custom className */
|
|
17
|
+
className?: string;
|
|
18
|
+
/** Whether to close on select */
|
|
19
|
+
closeOnSelect?: boolean;
|
|
20
|
+
/** Whether to show search */
|
|
21
|
+
searchable?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ChainSelector({
|
|
25
|
+
value,
|
|
26
|
+
onValueChange,
|
|
27
|
+
placeholder = "Select chain",
|
|
28
|
+
disabled = false,
|
|
29
|
+
className,
|
|
30
|
+
closeOnSelect = true,
|
|
31
|
+
searchable = true,
|
|
32
|
+
}: ChainSelectorProps) {
|
|
33
|
+
const { chains, isLoading } = useSupportedChains({
|
|
34
|
+
page: 1,
|
|
35
|
+
limit: 100, // High limit to fetch all chains
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const chainOptions = useMemo(() => {
|
|
39
|
+
if (!chains || chains.length === 0) return [];
|
|
40
|
+
|
|
41
|
+
const filteredChains = chains.filter((chain) => chain.status === "active");
|
|
42
|
+
|
|
43
|
+
return filteredChains.map((chain) => ({
|
|
44
|
+
label: chain.name,
|
|
45
|
+
value: chain?.id ?? "",
|
|
46
|
+
icon: chain.logoUrl
|
|
47
|
+
? () => (
|
|
48
|
+
<img
|
|
49
|
+
src={chain.logoUrl}
|
|
50
|
+
alt={chain.name}
|
|
51
|
+
width={16}
|
|
52
|
+
height={16}
|
|
53
|
+
className="size-4 rounded-full"
|
|
54
|
+
/>
|
|
55
|
+
)
|
|
56
|
+
: undefined,
|
|
57
|
+
}));
|
|
58
|
+
}, [chains]);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<Combobox
|
|
62
|
+
searchable={searchable}
|
|
63
|
+
options={chainOptions}
|
|
64
|
+
value={value}
|
|
65
|
+
onValueChange={onValueChange}
|
|
66
|
+
disabled={disabled || isLoading}
|
|
67
|
+
placeholder={isLoading ? "Loading chains..." : placeholder}
|
|
68
|
+
closeOnSelect={closeOnSelect}
|
|
69
|
+
className={className}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { MultiSelect } from "@turtleclub/ui";
|
|
5
|
+
import { useSupportedChains } from "@turtleclub/hooks";
|
|
6
|
+
|
|
7
|
+
interface ChainSelectorProps {
|
|
8
|
+
/** Selected chain IDs */
|
|
9
|
+
value: string[];
|
|
10
|
+
/** Callback when selection changes */
|
|
11
|
+
onValueChange: (values: string[]) => void;
|
|
12
|
+
/** Placeholder text */
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
/** Whether the selector is disabled */
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
/** Custom className */
|
|
17
|
+
className?: string;
|
|
18
|
+
/** Maximum count to show in trigger */
|
|
19
|
+
maxCount?: number;
|
|
20
|
+
/** Whether to close on select */
|
|
21
|
+
closeOnSelect?: boolean;
|
|
22
|
+
/** Whether to show search */
|
|
23
|
+
searchable?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function ChainsSelector({
|
|
27
|
+
value,
|
|
28
|
+
onValueChange,
|
|
29
|
+
placeholder = "Select chains",
|
|
30
|
+
disabled = false,
|
|
31
|
+
className,
|
|
32
|
+
maxCount = 1,
|
|
33
|
+
closeOnSelect = true,
|
|
34
|
+
searchable = true,
|
|
35
|
+
}: ChainSelectorProps) {
|
|
36
|
+
const { chains, isLoading } = useSupportedChains({
|
|
37
|
+
page: 1,
|
|
38
|
+
limit: 100, // High limit to fetch all chains
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const chainOptions = useMemo(() => {
|
|
42
|
+
if (!chains || chains.length === 0) return [];
|
|
43
|
+
|
|
44
|
+
const filteredChains = chains.filter((chain) => chain.status === "active");
|
|
45
|
+
|
|
46
|
+
return filteredChains.map((chain) => ({
|
|
47
|
+
label: chain.name,
|
|
48
|
+
value: chain?.id ?? "",
|
|
49
|
+
icon: chain.logoUrl
|
|
50
|
+
? () => (
|
|
51
|
+
<img
|
|
52
|
+
src={chain.logoUrl}
|
|
53
|
+
alt={chain.name}
|
|
54
|
+
width={16}
|
|
55
|
+
height={16}
|
|
56
|
+
className="size-4 rounded-full"
|
|
57
|
+
/>
|
|
58
|
+
)
|
|
59
|
+
: undefined,
|
|
60
|
+
}));
|
|
61
|
+
}, [chains]);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<MultiSelect
|
|
65
|
+
searchable={searchable}
|
|
66
|
+
options={chainOptions}
|
|
67
|
+
value={value}
|
|
68
|
+
onValueChange={onValueChange}
|
|
69
|
+
disabled={disabled || isLoading}
|
|
70
|
+
placeholder={isLoading ? "Loading chains..." : placeholder}
|
|
71
|
+
closeOnSelect={closeOnSelect}
|
|
72
|
+
maxCount={maxCount}
|
|
73
|
+
className={className}
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { MultiSelect } from "@turtleclub/ui";
|
|
5
|
+
import { useOpportunities } from "@turtleclub/hooks";
|
|
6
|
+
|
|
7
|
+
interface OpportunitySelectorProps {
|
|
8
|
+
/** Selected opportunity IDs */
|
|
9
|
+
value: string[];
|
|
10
|
+
/** Callback when selection changes */
|
|
11
|
+
onValueChange: (values: string[]) => void;
|
|
12
|
+
/** Placeholder text */
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
/** Whether the selector is disabled */
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
/** Custom className */
|
|
17
|
+
className?: string;
|
|
18
|
+
/** Maximum count to show in trigger */
|
|
19
|
+
maxCount?: number;
|
|
20
|
+
/** Whether to close on select */
|
|
21
|
+
closeOnSelect?: boolean;
|
|
22
|
+
/** Whether to show search */
|
|
23
|
+
searchable?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function OpportunitiesSelector({
|
|
27
|
+
value,
|
|
28
|
+
onValueChange,
|
|
29
|
+
placeholder = "Select opportunities",
|
|
30
|
+
disabled = false,
|
|
31
|
+
className,
|
|
32
|
+
maxCount = 1,
|
|
33
|
+
closeOnSelect = true,
|
|
34
|
+
searchable = true,
|
|
35
|
+
}: OpportunitySelectorProps) {
|
|
36
|
+
const { data: opportunitiesData, isLoading } = useOpportunities();
|
|
37
|
+
|
|
38
|
+
const opportunities = useMemo(
|
|
39
|
+
() => opportunitiesData?.opportunities ?? [],
|
|
40
|
+
[opportunitiesData?.opportunities]
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const opportunityOptions = useMemo(() => {
|
|
44
|
+
if (!opportunities || opportunities.length === 0) return [];
|
|
45
|
+
|
|
46
|
+
const filteredOpportunities = opportunities.filter((opp) => opp.status === "active");
|
|
47
|
+
|
|
48
|
+
return filteredOpportunities.map((opportunity) => ({
|
|
49
|
+
label: opportunity.name || opportunity.shortName,
|
|
50
|
+
value: opportunity.id!,
|
|
51
|
+
description: opportunity.shortName !== opportunity.name ? opportunity.shortName : undefined,
|
|
52
|
+
icon: opportunity.depositTokens?.[0]?.logoUrl
|
|
53
|
+
? () => (
|
|
54
|
+
<img
|
|
55
|
+
src={opportunity.depositTokens[0].logoUrl}
|
|
56
|
+
alt={opportunity.name}
|
|
57
|
+
width={16}
|
|
58
|
+
height={16}
|
|
59
|
+
className="size-4 rounded-full"
|
|
60
|
+
/>
|
|
61
|
+
)
|
|
62
|
+
: undefined,
|
|
63
|
+
}));
|
|
64
|
+
}, [opportunities]);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<MultiSelect
|
|
68
|
+
searchable={searchable}
|
|
69
|
+
options={opportunityOptions}
|
|
70
|
+
value={value}
|
|
71
|
+
onValueChange={onValueChange}
|
|
72
|
+
disabled={disabled || isLoading}
|
|
73
|
+
placeholder={isLoading ? "Loading opportunities..." : placeholder}
|
|
74
|
+
closeOnSelect={closeOnSelect}
|
|
75
|
+
maxCount={maxCount}
|
|
76
|
+
className={className}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { Combobox } from "@turtleclub/ui";
|
|
5
|
+
import { useOpportunities } from "@turtleclub/hooks";
|
|
6
|
+
|
|
7
|
+
interface OpportunitySelectorProps {
|
|
8
|
+
/** Selected opportunity ID */
|
|
9
|
+
value: string;
|
|
10
|
+
/** Callback when selection changes */
|
|
11
|
+
onValueChange: (value: string) => void;
|
|
12
|
+
/** Placeholder text */
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
/** Whether the selector is disabled */
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
/** Custom className */
|
|
17
|
+
className?: string;
|
|
18
|
+
/** Whether to close on select */
|
|
19
|
+
closeOnSelect?: boolean;
|
|
20
|
+
/** Whether to show search */
|
|
21
|
+
searchable?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function OpportunitySelector({
|
|
25
|
+
value,
|
|
26
|
+
onValueChange,
|
|
27
|
+
placeholder = "Select opportunity",
|
|
28
|
+
disabled = false,
|
|
29
|
+
className,
|
|
30
|
+
closeOnSelect = true,
|
|
31
|
+
searchable = true,
|
|
32
|
+
}: OpportunitySelectorProps) {
|
|
33
|
+
const { data: opportunitiesData, isLoading } = useOpportunities();
|
|
34
|
+
|
|
35
|
+
const opportunities = useMemo(
|
|
36
|
+
() => opportunitiesData?.opportunities ?? [],
|
|
37
|
+
[opportunitiesData?.opportunities]
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const opportunityOptions = useMemo(() => {
|
|
41
|
+
if (!opportunities || opportunities.length === 0) return [];
|
|
42
|
+
|
|
43
|
+
const filteredOpportunities = opportunities.filter((opp) => opp.status === "active");
|
|
44
|
+
|
|
45
|
+
return filteredOpportunities.map((opportunity) => ({
|
|
46
|
+
label: opportunity.name || opportunity.shortName,
|
|
47
|
+
value: opportunity.id!,
|
|
48
|
+
description: opportunity.shortName !== opportunity.name ? opportunity.shortName : undefined,
|
|
49
|
+
icon: opportunity.depositTokens?.[0]?.logoUrl
|
|
50
|
+
? () => (
|
|
51
|
+
<img
|
|
52
|
+
src={opportunity.depositTokens[0].logoUrl}
|
|
53
|
+
alt={opportunity.name}
|
|
54
|
+
width={16}
|
|
55
|
+
height={16}
|
|
56
|
+
className="size-4 rounded-full"
|
|
57
|
+
/>
|
|
58
|
+
)
|
|
59
|
+
: undefined,
|
|
60
|
+
}));
|
|
61
|
+
}, [opportunities]);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Combobox
|
|
65
|
+
searchable={searchable}
|
|
66
|
+
options={opportunityOptions}
|
|
67
|
+
value={value}
|
|
68
|
+
onValueChange={onValueChange}
|
|
69
|
+
disabled={disabled || isLoading}
|
|
70
|
+
placeholder={isLoading ? "Loading opportunities..." : placeholder}
|
|
71
|
+
closeOnSelect={closeOnSelect}
|
|
72
|
+
className={className}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
}
|