@m5kdev/web-ui 0.1.0

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.
Files changed (127) hide show
  1. package/LICENSE +621 -0
  2. package/README.md +17 -0
  3. package/package.json +169 -0
  4. package/src/animations/card.motion.ts +9 -0
  5. package/src/components/AvatarUpload.tsx +133 -0
  6. package/src/components/Button.tsx +14 -0
  7. package/src/components/Calendar.css +684 -0
  8. package/src/components/Calendar.tsx +32 -0
  9. package/src/components/CardsSelect.tsx +155 -0
  10. package/src/components/CollapsibleSidebarMenuItem.tsx +57 -0
  11. package/src/components/ColorPicker.tsx +56 -0
  12. package/src/components/CopyButton.tsx +45 -0
  13. package/src/components/CropDialog.tsx +154 -0
  14. package/src/components/DialogProvider.tsx +105 -0
  15. package/src/components/ErrorFallback.tsx +17 -0
  16. package/src/components/FileDropzone.tsx +120 -0
  17. package/src/components/MultiSelectDropdown.tsx +233 -0
  18. package/src/components/Orb.tsx +288 -0
  19. package/src/components/PageAlert.tsx +121 -0
  20. package/src/components/SelectChips.tsx +40 -0
  21. package/src/components/SidebarItem.tsx +26 -0
  22. package/src/components/Steps.tsx +340 -0
  23. package/src/components/TablerIconPicker.tsx +4260 -0
  24. package/src/components/app-header.tsx +40 -0
  25. package/src/components/blur-card.tsx +132 -0
  26. package/src/components/features-section-demo-1.tsx +127 -0
  27. package/src/components/features-section-demo-2.tsx +102 -0
  28. package/src/components/features-section-demo-3.tsx +272 -0
  29. package/src/components/mode-toggle.tsx +31 -0
  30. package/src/components/nav-main.tsx +69 -0
  31. package/src/components/pricing-cards.tsx +133 -0
  32. package/src/components/shared/ButtonCopy.tsx +50 -0
  33. package/src/components/team-switcher.tsx +83 -0
  34. package/src/components/theme-provider.tsx +74 -0
  35. package/src/components/typewriter.tsx +90 -0
  36. package/src/components/ui/alert-dialog.tsx +133 -0
  37. package/src/components/ui/alert.tsx +60 -0
  38. package/src/components/ui/avatar.tsx +47 -0
  39. package/src/components/ui/badge.tsx +33 -0
  40. package/src/components/ui/bento-grid.tsx +54 -0
  41. package/src/components/ui/bento-grid2.tsx +66 -0
  42. package/src/components/ui/breadcrumb.tsx +101 -0
  43. package/src/components/ui/button.tsx +50 -0
  44. package/src/components/ui/card.tsx +55 -0
  45. package/src/components/ui/checkbox.tsx +26 -0
  46. package/src/components/ui/collapsible.tsx +9 -0
  47. package/src/components/ui/dialog.tsx +119 -0
  48. package/src/components/ui/dropdown-menu.tsx +186 -0
  49. package/src/components/ui/floating-navbar.tsx +78 -0
  50. package/src/components/ui/form.tsx +167 -0
  51. package/src/components/ui/image.tsx +55 -0
  52. package/src/components/ui/input.tsx +22 -0
  53. package/src/components/ui/label.tsx +19 -0
  54. package/src/components/ui/pagination.tsx +105 -0
  55. package/src/components/ui/progress.tsx +23 -0
  56. package/src/components/ui/resizable-navbar.tsx +260 -0
  57. package/src/components/ui/segment-control.tsx +143 -0
  58. package/src/components/ui/select.tsx +153 -0
  59. package/src/components/ui/separator.tsx +24 -0
  60. package/src/components/ui/sheet.tsx +121 -0
  61. package/src/components/ui/sidebar.tsx +736 -0
  62. package/src/components/ui/skeleton.tsx +7 -0
  63. package/src/components/ui/slider.tsx +23 -0
  64. package/src/components/ui/sonner.tsx +27 -0
  65. package/src/components/ui/spinner.tsx +45 -0
  66. package/src/components/ui/switch.tsx +27 -0
  67. package/src/components/ui/table.tsx +90 -0
  68. package/src/components/ui/tabs.tsx +52 -0
  69. package/src/components/ui/textarea.tsx +18 -0
  70. package/src/components/ui/timeline.tsx +95 -0
  71. package/src/components/ui/toast.tsx +126 -0
  72. package/src/components/ui/tooltip.tsx +55 -0
  73. package/src/components/ui/typewriter-effect.tsx +181 -0
  74. package/src/hooks/use-mobile.ts +19 -0
  75. package/src/hooks/useDialog.ts +25 -0
  76. package/src/icons/GoogleIcon.tsx +32 -0
  77. package/src/icons/LinkedInIcon.tsx +30 -0
  78. package/src/icons/MicrosoftIcon.tsx +21 -0
  79. package/src/lib/chatwoot.ts +51 -0
  80. package/src/lib/utils.ts +6 -0
  81. package/src/modules/app/components/AppLoader.tsx +9 -0
  82. package/src/modules/app/components/AppShell.tsx +21 -0
  83. package/src/modules/app/components/AppSidebar.tsx +26 -0
  84. package/src/modules/app/components/AppSidebarContent.tsx +73 -0
  85. package/src/modules/app/components/AppSidebarHeader.tsx +57 -0
  86. package/src/modules/app/components/AppSidebarInvites.tsx +32 -0
  87. package/src/modules/app/components/AppSidebarUser.tsx +128 -0
  88. package/src/modules/auth/components/AdminUserManagement.tsx +1136 -0
  89. package/src/modules/auth/components/AdminWaitlist.tsx +358 -0
  90. package/src/modules/auth/components/AuthLayout.tsx +13 -0
  91. package/src/modules/auth/components/AuthProviders.tsx +105 -0
  92. package/src/modules/auth/components/AuthRouter.tsx +29 -0
  93. package/src/modules/auth/components/ClaimAccountRoute.tsx +242 -0
  94. package/src/modules/auth/components/ErrorAuthRoute.tsx +121 -0
  95. package/src/modules/auth/components/ForgotPasswordForm.tsx +58 -0
  96. package/src/modules/auth/components/ForgotPasswordRoute.tsx +27 -0
  97. package/src/modules/auth/components/InviteFriends.tsx +273 -0
  98. package/src/modules/auth/components/LastUsedBadge.tsx +22 -0
  99. package/src/modules/auth/components/LoginForm.tsx +104 -0
  100. package/src/modules/auth/components/LoginRoute.tsx +31 -0
  101. package/src/modules/auth/components/LogoutRoute.tsx +21 -0
  102. package/src/modules/auth/components/OrganizationAcceptInvitationRoute.tsx +161 -0
  103. package/src/modules/auth/components/OrganizationMembersRoute.tsx +730 -0
  104. package/src/modules/auth/components/OrganizationSettingsRoute.tsx +280 -0
  105. package/src/modules/auth/components/OrganizationSwitcher.tsx +148 -0
  106. package/src/modules/auth/components/ProfileRoute.tsx +104 -0
  107. package/src/modules/auth/components/RangeNuqsDatePicker.tsx +365 -0
  108. package/src/modules/auth/components/ResetPasswordForm.tsx +103 -0
  109. package/src/modules/auth/components/ResetPasswordRoute.tsx +27 -0
  110. package/src/modules/auth/components/SignupFormRoute.tsx +189 -0
  111. package/src/modules/auth/components/SignupRoute.tsx +53 -0
  112. package/src/modules/auth/components/UserPreferences.tsx +144 -0
  113. package/src/modules/auth/components/WaitlistCard.tsx +78 -0
  114. package/src/modules/auth/components/WaitlistCodeValidation.tsx +79 -0
  115. package/src/modules/billing/components/BillingBetaPage.tsx +124 -0
  116. package/src/modules/billing/components/BillingInvoicePage.tsx +180 -0
  117. package/src/modules/billing/components/BillingPlanSelect.tsx +14 -0
  118. package/src/modules/billing/components/BillingRouter.tsx +20 -0
  119. package/src/modules/billing/components/BillingSinglePlanSelect.tsx +172 -0
  120. package/src/modules/table/components/ColumnOrderAndVisibility.tsx +127 -0
  121. package/src/modules/table/components/NuqsTable.tsx +396 -0
  122. package/src/modules/table/components/TableFiltering.tsx +520 -0
  123. package/src/modules/table/components/TablePagination.tsx +59 -0
  124. package/src/modules/table/components/table.types.ts +11 -0
  125. package/src/modules/table/filterTransformers.ts +323 -0
  126. package/src/types.ts +4 -0
  127. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,520 @@
1
+ import {
2
+ DatePicker,
3
+ DateRangePicker,
4
+ type DateValue,
5
+ Input,
6
+ Select,
7
+ SelectItem,
8
+ type SharedSelection,
9
+ } from "@heroui/react";
10
+ import { getLocalTimeZone, today } from "@internationalized/date";
11
+ import type { QueryFilters } from "@m5kdev/commons/modules/schemas/query.schema";
12
+ import type {
13
+ ColumnDataType,
14
+ ComponentForFilterMethod,
15
+ FilterMethod,
16
+ FilterMethods,
17
+ } from "@m5kdev/commons/modules/table/filter.types";
18
+ import { PlusIcon, XIcon } from "lucide-react";
19
+ import type { ReactNode } from "react";
20
+ import { useCallback, useEffect, useMemo, useState } from "react";
21
+ import { Button } from "#components/ui/button";
22
+ import {
23
+ type FilterValue,
24
+ type HeroUIFilter,
25
+ transformFiltersFromHeroUI,
26
+ transformFiltersToHeroUI,
27
+ } from "../filterTransformers";
28
+
29
+ type ComponentRenderer = (
30
+ value: FilterValue,
31
+ onChange: (value: FilterValue) => void,
32
+ options?: { label: string; value: string }[]
33
+ ) => ReactNode;
34
+
35
+ const componentForFilterMethod: Record<ComponentForFilterMethod, ComponentRenderer> = {
36
+ text: (value, onChange) => (
37
+ <Input
38
+ size="sm"
39
+ aria-label="Select Value"
40
+ className="flex-1 min-w-0 text-sm"
41
+ value={(value as string) ?? ""}
42
+ onChange={(e) => onChange(e.target.value)}
43
+ />
44
+ ),
45
+ number: (value, onChange) => (
46
+ <Input
47
+ size="sm"
48
+ aria-label="Select Value"
49
+ type="number"
50
+ className="flex-1 min-w-0 text-sm"
51
+ value={(value as number | null)?.toString() ?? ""}
52
+ onChange={(e) => onChange(Number.parseFloat(e.target.value) || 0)}
53
+ />
54
+ ),
55
+ date: (value, onChange) => (
56
+ <DatePicker
57
+ size="sm"
58
+ aria-label="Select Value"
59
+ className="flex-1 min-w-0 text-sm"
60
+ value={(value as any) ?? undefined}
61
+ onChange={(date) => date && onChange(date as FilterValue)}
62
+ maxValue={today(getLocalTimeZone()) as unknown as DateValue}
63
+ />
64
+ ),
65
+ range: (value, onChange) => (
66
+ <DateRangePicker
67
+ size="sm"
68
+ aria-label="Select Value"
69
+ className="flex-1 min-w-0 text-sm"
70
+ value={(value as any) ?? undefined}
71
+ onChange={(range) => range && onChange(range as FilterValue)}
72
+ maxValue={today(getLocalTimeZone()) as unknown as DateValue}
73
+ />
74
+ ),
75
+ radio: (value, onChange) => (
76
+ <Select
77
+ size="sm"
78
+ aria-label="Select Value"
79
+ className="flex-1 min-w-0 text-sm"
80
+ selectedKeys={(value as boolean | null) ? ["true"] : ["false"]}
81
+ onSelectionChange={(keys) => onChange(keys.currentKey === "true")}
82
+ >
83
+ <SelectItem key="true" className="text-sm">
84
+ True
85
+ </SelectItem>
86
+ <SelectItem key="false" className="text-sm">
87
+ False
88
+ </SelectItem>
89
+ </Select>
90
+ ),
91
+ select: (value, onChange, options = []) => (
92
+ <Select
93
+ size="sm"
94
+ aria-label="Select Value"
95
+ className="flex-1 min-w-0 text-sm"
96
+ selectedKeys={(value as SharedSelection) ?? new Set()}
97
+ onSelectionChange={(keys) => keys && onChange(keys as FilterValue)}
98
+ >
99
+ {options.map((option) => (
100
+ <SelectItem key={option.value} className="text-sm">
101
+ {option.label}
102
+ </SelectItem>
103
+ ))}
104
+ </Select>
105
+ ),
106
+ multiSelect: (value, onChange, options = []) => (
107
+ <Select
108
+ size="sm"
109
+ aria-label="Select Value"
110
+ selectionMode="multiple"
111
+ className="flex-1 min-w-0 text-sm"
112
+ selectedKeys={value ? new Set(value as SharedSelection) : new Set()}
113
+ onSelectionChange={(keys) => onChange(keys as FilterValue)}
114
+ >
115
+ {options.map((option) => (
116
+ <SelectItem key={option.value} className="text-sm">
117
+ {option.label}
118
+ </SelectItem>
119
+ ))}
120
+ </Select>
121
+ ),
122
+ };
123
+
124
+ const defaultFilterMethods: FilterMethods = {
125
+ string: [
126
+ { value: "contains", label: "Contains", component: "text" },
127
+ { value: "equals", label: "Equals", component: "text" },
128
+ { value: "starts_with", label: "Starts With", component: "text" },
129
+ { value: "ends_with", label: "Ends With", component: "text" },
130
+ ],
131
+ number: [
132
+ { value: "equals", label: "Equals", component: "number" },
133
+ { value: "greater_than", label: "Greater Than", component: "number" },
134
+ { value: "less_than", label: "Less Than", component: "number" },
135
+ ],
136
+ date: [
137
+ { value: "on", label: "On", component: "date" },
138
+ { value: "between", label: "Between", component: "range" },
139
+ { value: "before", label: "Before", component: "date" },
140
+ { value: "after", label: "After", component: "date" },
141
+ { value: "intersect", label: "During", component: "range" },
142
+ ],
143
+ boolean: [{ value: "equals", label: "Equals", component: "radio" }],
144
+ enum: [
145
+ { value: "oneOf", label: "One Of", component: "multiSelect" },
146
+ { value: "equals", label: "Equals", component: "select" },
147
+ ],
148
+ };
149
+
150
+ const SINGLE_FILTER_KEY = "single-filter";
151
+
152
+ type TableFilteringProps = {
153
+ columns: {
154
+ id: string;
155
+ label: string;
156
+ type?: ColumnDataType | null;
157
+ options?: { label: string; value: string }[] | null;
158
+ endColumnId?: string | null;
159
+ periodStartColumnId?: string | null;
160
+ periodEndColumnId?: string | null;
161
+ }[];
162
+ onFiltersChange: (filters: QueryFilters) => void;
163
+ filters: QueryFilters;
164
+ onClose?: () => void;
165
+ singleFilter?: boolean;
166
+ filterMethods?: Partial<FilterMethods>;
167
+ };
168
+
169
+ const mergeFilterMethods = (overrides?: Partial<FilterMethods>): FilterMethods => ({
170
+ string:
171
+ overrides?.string && overrides.string.length > 0
172
+ ? overrides.string
173
+ : defaultFilterMethods.string,
174
+ number:
175
+ overrides?.number && overrides.number.length > 0
176
+ ? overrides.number
177
+ : defaultFilterMethods.number,
178
+ date: overrides?.date && overrides.date.length > 0 ? overrides.date : defaultFilterMethods.date,
179
+ boolean:
180
+ overrides?.boolean && overrides.boolean.length > 0
181
+ ? overrides.boolean
182
+ : defaultFilterMethods.boolean,
183
+ enum: overrides?.enum && overrides.enum.length > 0 ? overrides.enum : defaultFilterMethods.enum,
184
+ });
185
+
186
+ export const TableFiltering = ({
187
+ columns,
188
+ onFiltersChange,
189
+ filters: initialFilters = [],
190
+ onClose,
191
+ singleFilter = false,
192
+ filterMethods,
193
+ }: TableFilteringProps) => {
194
+ const [filters, setFilters] = useState<Record<string, HeroUIFilter>>({});
195
+ const effectiveFilterMethods = useMemo(() => mergeFilterMethods(filterMethods), [filterMethods]);
196
+
197
+ const createEmptyFilter = useCallback(
198
+ (): HeroUIFilter => ({
199
+ columnId: "",
200
+ type: null,
201
+ value: null,
202
+ method: null,
203
+ options: null,
204
+ endColumnId: null,
205
+ periodStartColumnId: null,
206
+ periodEndColumnId: null,
207
+ }),
208
+ []
209
+ );
210
+
211
+ const addFilter = useCallback(() => {
212
+ setFilters((prev) => {
213
+ if (!singleFilter) {
214
+ const filterId = crypto.randomUUID();
215
+ return {
216
+ ...prev,
217
+ [filterId]: createEmptyFilter(),
218
+ };
219
+ }
220
+
221
+ // single filter mode
222
+ return Object.keys(prev).length > 0 ? prev : { [SINGLE_FILTER_KEY]: createEmptyFilter() };
223
+ });
224
+ }, [createEmptyFilter, singleFilter]);
225
+
226
+ const removeFilter = useCallback(
227
+ (filterId: string) => {
228
+ setFilters((prev) => {
229
+ if (!singleFilter) {
230
+ const { [filterId]: _, ...rest } = prev;
231
+ return rest;
232
+ }
233
+
234
+ // single filter mode resets the lone filter
235
+ return { [SINGLE_FILTER_KEY]: createEmptyFilter() };
236
+ });
237
+ },
238
+ [createEmptyFilter, singleFilter]
239
+ );
240
+
241
+ const columnsMap = useMemo(
242
+ () => new Map(columns.map((column) => [column.id, column])),
243
+ [columns]
244
+ );
245
+
246
+ useEffect(() => {
247
+ // Transform initialFilters from FiltersToApply format to HeroUIFilter format
248
+ const transformedFilters = transformFiltersToHeroUI(
249
+ initialFilters,
250
+ columnsMap,
251
+ effectiveFilterMethods
252
+ );
253
+
254
+ if (!singleFilter) {
255
+ setFilters(Object.fromEntries(transformedFilters.map((filter) => [filter.columnId, filter])));
256
+ return;
257
+ }
258
+
259
+ const firstFilter = transformedFilters[0];
260
+ setFilters({
261
+ [SINGLE_FILTER_KEY]: firstFilter ?? createEmptyFilter(),
262
+ });
263
+ }, [createEmptyFilter, initialFilters, singleFilter, columnsMap, effectiveFilterMethods]);
264
+
265
+ const selectColumn = useCallback(
266
+ (filterId: string, columnId: string) => {
267
+ setFilters((prev) => {
268
+ const oldFilter = prev[filterId];
269
+ if (!oldFilter) return prev;
270
+ const column = columnsMap.get(columnId);
271
+ if (!column) return prev;
272
+
273
+ // If Period column, auto-set intersect method
274
+ const isPeriodColumn = columnId.endsWith("__period");
275
+ const intersectMethod = isPeriodColumn
276
+ ? (effectiveFilterMethods.date.find((m) => m.value === "intersect") ?? null)
277
+ : null;
278
+
279
+ return {
280
+ ...prev,
281
+ [filterId]: {
282
+ ...oldFilter,
283
+ columnId,
284
+ type: column.type ?? null,
285
+ options: column.options ?? null,
286
+ endColumnId: column.endColumnId ?? null,
287
+ periodStartColumnId: column.periodStartColumnId ?? null,
288
+ periodEndColumnId: column.periodEndColumnId ?? null,
289
+ method: intersectMethod,
290
+ value: null,
291
+ },
292
+ };
293
+ });
294
+ },
295
+ [columnsMap, effectiveFilterMethods]
296
+ );
297
+
298
+ const selectMethod = useCallback((filterId: string, method: FilterMethod) => {
299
+ setFilters((prev) => {
300
+ const oldFilter = prev[filterId];
301
+ if (!oldFilter) return prev;
302
+ return {
303
+ ...prev,
304
+ [filterId]: { ...oldFilter, method, value: null },
305
+ };
306
+ });
307
+ }, []);
308
+
309
+ const selectValue = useCallback((filterId: string, value: FilterValue) => {
310
+ setFilters((prev) => {
311
+ const oldFilter = prev[filterId];
312
+ if (!oldFilter) return prev;
313
+ return {
314
+ ...prev,
315
+ [filterId]: { ...oldFilter, value },
316
+ };
317
+ });
318
+ }, []);
319
+
320
+ const filterEntries = useMemo(() => Object.entries(filters), [filters]);
321
+
322
+ const availableColumnsMap = useMemo(() => {
323
+ const map = new Map<string, typeof columns>();
324
+ filterEntries.forEach(([filterId]) => {
325
+ const columnsUsedByOtherFilters = new Set(
326
+ filterEntries
327
+ .filter(([id]) => id !== filterId)
328
+ .map(([_, f]) => f.columnId)
329
+ .filter((id) => id !== "")
330
+ );
331
+ map.set(
332
+ filterId,
333
+ columns.filter((column) => !columnsUsedByOtherFilters.has(column.id))
334
+ );
335
+ });
336
+ return map;
337
+ }, [filterEntries, columns]);
338
+
339
+ const applyFilters = useCallback(() => {
340
+ const heroUIFilters = Object.values(filters);
341
+ const filtersToApply = transformFiltersFromHeroUI(heroUIFilters);
342
+ onFiltersChange(filtersToApply);
343
+ onClose?.();
344
+ }, [filters, onFiltersChange, onClose]);
345
+
346
+ return (
347
+ <div className="flex flex-col gap-2 p-1 min-w-[600px]">
348
+ {filterEntries.map(([filterId, filter]) => {
349
+ const availableColumns = availableColumnsMap.get(filterId) ?? columns;
350
+ return (
351
+ <TableFilteringItem
352
+ key={filterId}
353
+ id={filterId}
354
+ filter={filter}
355
+ columns={availableColumns}
356
+ selectColumn={selectColumn}
357
+ selectMethod={selectMethod}
358
+ removeFilter={removeFilter}
359
+ selectValue={selectValue}
360
+ filterMethods={effectiveFilterMethods}
361
+ />
362
+ );
363
+ })}
364
+ {!singleFilter && (
365
+ <Button variant="outline" size="sm" onClick={addFilter}>
366
+ <PlusIcon className="h-4 w-4" />
367
+ Add Filter
368
+ </Button>
369
+ )}
370
+ <Button onClick={applyFilters}>{singleFilter ? "Apply Filter" : "Apply Filters"}</Button>
371
+ </div>
372
+ );
373
+ };
374
+
375
+ const TableFilteringItem = ({
376
+ id,
377
+ filter,
378
+ columns,
379
+ selectColumn,
380
+ selectMethod,
381
+ selectValue,
382
+ removeFilter,
383
+ filterMethods,
384
+ }: {
385
+ id: string;
386
+ filter: HeroUIFilter;
387
+ columns: {
388
+ id: string;
389
+ label: string;
390
+ type?: ColumnDataType | null;
391
+ options?: { label: string; value: string }[] | null;
392
+ endColumnId?: string | null;
393
+ periodStartColumnId?: string | null;
394
+ periodEndColumnId?: string | null;
395
+ }[];
396
+ selectColumn: (filterId: string, columnId: string) => void;
397
+ selectMethod: (filterId: string, method: FilterMethod) => void;
398
+ selectValue: (filterId: string, value: FilterValue) => void;
399
+ removeFilter: (filterId: string) => void;
400
+ filterMethods: FilterMethods;
401
+ }) => {
402
+ const handleColumnChange = useCallback(
403
+ (keys: any) => {
404
+ const columnId = String(keys.currentKey);
405
+ selectColumn(id, columnId);
406
+ },
407
+ [id, selectColumn]
408
+ );
409
+
410
+ const methodsForType = useMemo(() => {
411
+ if (!filter.type) return [];
412
+
413
+ // Period columns only support intersect method
414
+ const isPeriodColumn = filter.columnId.endsWith("__period");
415
+ if (isPeriodColumn) {
416
+ const intersectMethod = filterMethods.date.find((m) => m.value === "intersect");
417
+ return intersectMethod ? [intersectMethod] : [];
418
+ }
419
+
420
+ const baseMethods = filterMethods[filter.type as ColumnDataType] ?? [];
421
+ const emptyMethods: FilterMethod[] = [
422
+ { value: "isEmpty", label: "Is Empty", component: null },
423
+ { value: "isNotEmpty", label: "Is Not Empty", component: null },
424
+ ];
425
+
426
+ if (filter.type !== "boolean" && filter.type !== "date") {
427
+ return [...baseMethods, ...emptyMethods];
428
+ }
429
+
430
+ return baseMethods;
431
+ }, [filter.type, filter.columnId, filter.method?.value, filterMethods]);
432
+
433
+ const handleMethodChange = useCallback(
434
+ (keys: any) => {
435
+ if (!filter.type) return;
436
+ // Use methodsForType instead of filterMethods to include dynamically added methods
437
+ const method = methodsForType.find(
438
+ (currentMethod: FilterMethod) => currentMethod.value === String(keys.currentKey)
439
+ );
440
+ if (method) {
441
+ selectMethod(id, method);
442
+ }
443
+ },
444
+ [id, filter.type, selectMethod, methodsForType]
445
+ );
446
+
447
+ const handleValueChange = useCallback(
448
+ (value: FilterValue) => {
449
+ selectValue(id, value);
450
+ },
451
+ [id, selectValue]
452
+ );
453
+
454
+ const methodSelect = useMemo(() => {
455
+ if (!filter.type) return null;
456
+ return (
457
+ <Select
458
+ size="sm"
459
+ aria-label="Select Method"
460
+ className="w-40 flex-shrink-0 text-sm"
461
+ selectedKeys={filter.method?.value ? [filter.method.value] : []}
462
+ onSelectionChange={handleMethodChange}
463
+ popoverProps={{
464
+ className: "w-auto min-w-max",
465
+ }}
466
+ >
467
+ {methodsForType.map((method: FilterMethod) => (
468
+ <SelectItem key={method.value} className="text-sm">
469
+ {method.label}
470
+ </SelectItem>
471
+ ))}
472
+ </Select>
473
+ );
474
+ }, [filter.type, filter.method?.value, methodsForType, handleMethodChange]);
475
+
476
+ const filterValueComponent = useMemo(() => {
477
+ if (!filter.method?.component) {
478
+ return <div className="flex-1 min-w-0" />;
479
+ }
480
+ const component = filter.method.component as ComponentForFilterMethod;
481
+ const ComponentFn =
482
+ componentForFilterMethod[component as keyof typeof componentForFilterMethod];
483
+ if (!ComponentFn) return <div className="flex-1 min-w-0" />;
484
+ return ComponentFn(filter.value as any, handleValueChange, filter.options ?? []);
485
+ }, [filter.method?.component, filter.value, filter.options, handleValueChange]);
486
+
487
+ const columnSelectItems = useMemo(
488
+ () =>
489
+ columns.map((column) => (
490
+ <SelectItem key={column.id} className="text-sm">
491
+ {String(column.label)}
492
+ </SelectItem>
493
+ )),
494
+ [columns]
495
+ );
496
+
497
+ return (
498
+ <div className="flex items-center gap-2 w-full">
499
+ <div className="flex flex-1 items-center gap-2 min-w-0">
500
+ <Select
501
+ size="sm"
502
+ aria-label="Select Column"
503
+ className="w-40 flex-shrink-0 text-sm"
504
+ selectedKeys={filter.columnId ? [filter.columnId] : []}
505
+ onSelectionChange={handleColumnChange}
506
+ popoverProps={{
507
+ className: "w-auto min-w-max",
508
+ }}
509
+ >
510
+ {columnSelectItems}
511
+ </Select>
512
+ {methodSelect}
513
+ {filterValueComponent}
514
+ </div>
515
+ <Button variant="outline" size="sm" onClick={() => removeFilter(id)}>
516
+ <XIcon className="h-4 w-4" />
517
+ </Button>
518
+ </div>
519
+ );
520
+ };
@@ -0,0 +1,59 @@
1
+ import type { TableProps } from "@m5kdev/frontend/modules/table/hooks/useNuqsTable";
2
+ import { Input } from "#components/ui/input";
3
+ import {
4
+ Pagination,
5
+ PaginationContent,
6
+ PaginationNext,
7
+ PaginationPrevious,
8
+ } from "#components/ui/pagination";
9
+
10
+ interface TablePaginationProps {
11
+ pageCount: number;
12
+ page: TableProps["page"];
13
+ limit: TableProps["limit"];
14
+ setPagination: TableProps["setPagination"];
15
+ }
16
+
17
+ export const TablePagination = ({
18
+ pageCount,
19
+ page = 1,
20
+ limit = 10,
21
+ setPagination,
22
+ }: TablePaginationProps) => {
23
+ const isFirstPage = page === 1;
24
+ const isLastPage = page >= pageCount;
25
+ return (
26
+ <Pagination>
27
+ <PaginationContent>
28
+ <PaginationPrevious
29
+ isActive={!isFirstPage}
30
+ onClick={() => {
31
+ if (!isFirstPage) {
32
+ setPagination?.({ pageIndex: page - 2, pageSize: limit });
33
+ }
34
+ }}
35
+ />
36
+ <Input
37
+ type="number"
38
+ value={page}
39
+ min={1}
40
+ max={pageCount}
41
+ onChange={(e) => {
42
+ const newPage = e.target.valueAsNumber;
43
+ if (newPage >= 1 && newPage <= pageCount) {
44
+ setPagination?.({ pageIndex: newPage - 1, pageSize: limit });
45
+ }
46
+ }}
47
+ />
48
+ <PaginationNext
49
+ isActive={!isLastPage}
50
+ onClick={() => {
51
+ if (!isLastPage) {
52
+ setPagination?.({ pageIndex: page, pageSize: limit });
53
+ }
54
+ }}
55
+ />
56
+ </PaginationContent>
57
+ </Pagination>
58
+ );
59
+ };
@@ -0,0 +1,11 @@
1
+ import type { ColumnDataType as CommonColumnDataType } from "@m5kdev/commons/modules/table/filter.types";
2
+
3
+ export type ColumnDataType = CommonColumnDataType;
4
+
5
+ export type ColumnItem = {
6
+ id: string;
7
+ label: string;
8
+ visibility: boolean;
9
+ options?: { label: string; value: string }[];
10
+ type?: ColumnDataType;
11
+ };