@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.
@@ -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,4 @@
1
+ export * from "./filters";
2
+ export * from "./wrappers";
3
+ export * from "./helpers";
4
+ export * from "./selectors";
@@ -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
+ }