@ayasofyazilim/ui 0.0.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 (236) hide show
  1. package/__mocks__/canvas.ts +8 -0
  2. package/components.json +21 -0
  3. package/eslint.config.js +4 -0
  4. package/jest-environment.js +37 -0
  5. package/jest.config.ts +47 -0
  6. package/jest.setup.ts +69 -0
  7. package/package.json +124 -0
  8. package/postcss.config.mjs +6 -0
  9. package/src/aria/index.tsx +1 -0
  10. package/src/aria/number-field.tsx +41 -0
  11. package/src/components/.gitkeep +0 -0
  12. package/src/components/accordion.tsx +66 -0
  13. package/src/components/alert-dialog.tsx +157 -0
  14. package/src/components/alert.tsx +70 -0
  15. package/src/components/aspect-ratio.tsx +11 -0
  16. package/src/components/avatar.tsx +53 -0
  17. package/src/components/badge.tsx +67 -0
  18. package/src/components/breadcrumb.tsx +109 -0
  19. package/src/components/button-group.tsx +83 -0
  20. package/src/components/button.tsx +68 -0
  21. package/src/components/calendar.tsx +219 -0
  22. package/src/components/card.tsx +92 -0
  23. package/src/components/carousel.tsx +241 -0
  24. package/src/components/chart.tsx +363 -0
  25. package/src/components/checkbox.tsx +32 -0
  26. package/src/components/collapsible.tsx +33 -0
  27. package/src/components/command.tsx +184 -0
  28. package/src/components/context-menu.tsx +252 -0
  29. package/src/components/dialog.tsx +144 -0
  30. package/src/components/drawer.tsx +135 -0
  31. package/src/components/dropdown-menu.tsx +258 -0
  32. package/src/components/empty.tsx +100 -0
  33. package/src/components/field.tsx +248 -0
  34. package/src/components/form.tsx +169 -0
  35. package/src/components/hover-card.tsx +44 -0
  36. package/src/components/input-group.tsx +170 -0
  37. package/src/components/input-otp.tsx +77 -0
  38. package/src/components/input.tsx +21 -0
  39. package/src/components/item.tsx +193 -0
  40. package/src/components/kbd.tsx +28 -0
  41. package/src/components/label.tsx +24 -0
  42. package/src/components/menubar.tsx +276 -0
  43. package/src/components/navigation-menu.tsx +168 -0
  44. package/src/components/pagination.tsx +130 -0
  45. package/src/components/popover.tsx +88 -0
  46. package/src/components/progress.tsx +31 -0
  47. package/src/components/radio-group.tsx +45 -0
  48. package/src/components/resizable.tsx +56 -0
  49. package/src/components/scroll-area.tsx +58 -0
  50. package/src/components/select.tsx +189 -0
  51. package/src/components/separator.tsx +28 -0
  52. package/src/components/sheet.tsx +140 -0
  53. package/src/components/sidebar.tsx +862 -0
  54. package/src/components/skeleton.tsx +13 -0
  55. package/src/components/slider.tsx +63 -0
  56. package/src/components/sonner.tsx +40 -0
  57. package/src/components/spinner.tsx +16 -0
  58. package/src/components/stepper.tsx +291 -0
  59. package/src/components/switch.tsx +31 -0
  60. package/src/components/table.tsx +133 -0
  61. package/src/components/tabs.tsx +66 -0
  62. package/src/components/textarea.tsx +18 -0
  63. package/src/components/toggle-group.tsx +83 -0
  64. package/src/components/toggle.tsx +47 -0
  65. package/src/components/tooltip.tsx +66 -0
  66. package/src/custom/action-button.tsx +48 -0
  67. package/src/custom/async-select.tsx +287 -0
  68. package/src/custom/awesome-not-found.tsx +116 -0
  69. package/src/custom/charts/area-chart.tsx +147 -0
  70. package/src/custom/charts/bar-chart.tsx +233 -0
  71. package/src/custom/charts/chart-card.tsx +103 -0
  72. package/src/custom/charts/index.tsx +16 -0
  73. package/src/custom/charts/pie-chart.tsx +168 -0
  74. package/src/custom/charts/radar-chart.tsx +126 -0
  75. package/src/custom/checkbox-tree.tsx +100 -0
  76. package/src/custom/combobox.tsx +296 -0
  77. package/src/custom/confirm-dialog.tsx +102 -0
  78. package/src/custom/country-selector.tsx +204 -0
  79. package/src/custom/date-picker/calendar-rac.tsx +109 -0
  80. package/src/custom/date-picker/datefield-rac.tsx +84 -0
  81. package/src/custom/date-picker/index.tsx +273 -0
  82. package/src/custom/date-picker/types/index.ts +4 -0
  83. package/src/custom/date-picker/utils/index.ts +42 -0
  84. package/src/custom/date-picker-old.tsx +50 -0
  85. package/src/custom/date-tooltip.tsx +98 -0
  86. package/src/custom/document-scanner/consts.ts +5 -0
  87. package/src/custom/document-scanner/corner-adjustment/action-buttons.tsx +33 -0
  88. package/src/custom/document-scanner/corner-adjustment/corner-handle.tsx +43 -0
  89. package/src/custom/document-scanner/corner-adjustment/hooks/use-corner-drag.ts +85 -0
  90. package/src/custom/document-scanner/corner-adjustment/index.tsx +125 -0
  91. package/src/custom/document-scanner/corner-adjustment/types.ts +53 -0
  92. package/src/custom/document-scanner/corner-adjustment/utils/clip-path.ts +22 -0
  93. package/src/custom/document-scanner/corner-adjustment/zoom-magnifier.tsx +115 -0
  94. package/src/custom/document-scanner/hooks/use-document-capture.ts +81 -0
  95. package/src/custom/document-scanner/hooks/use-document-scanner.ts +80 -0
  96. package/src/custom/document-scanner/hooks/use-perspective-crop.ts +38 -0
  97. package/src/custom/document-scanner/index.tsx +255 -0
  98. package/src/custom/document-scanner/lib.ts +407 -0
  99. package/src/custom/document-scanner/types.ts +205 -0
  100. package/src/custom/document-scanner/utils/perspective-correction.ts +139 -0
  101. package/src/custom/document-viewer/controllers.tsx +98 -0
  102. package/src/custom/document-viewer/index.tsx +43 -0
  103. package/src/custom/document-viewer/renderers/image.tsx +37 -0
  104. package/src/custom/document-viewer/renderers/index.tsx +2 -0
  105. package/src/custom/document-viewer/renderers/pdf.tsx +105 -0
  106. package/src/custom/email-input/domains.json +159 -0
  107. package/src/custom/email-input/email.tsx +229 -0
  108. package/src/custom/email-input/index.tsx +4 -0
  109. package/src/custom/email-input/types.ts +104 -0
  110. package/src/custom/file-uploader.tsx +541 -0
  111. package/src/custom/filter-component/fields/async-select.tsx +33 -0
  112. package/src/custom/filter-component/fields/date.tsx +60 -0
  113. package/src/custom/filter-component/fields/multi-select.tsx +30 -0
  114. package/src/custom/filter-component/index.tsx +217 -0
  115. package/src/custom/image-canvas.tsx +260 -0
  116. package/src/custom/json-editor.tsx +22 -0
  117. package/src/custom/master-data-grid/components/dialogs/column-settings-dialog.tsx +100 -0
  118. package/src/custom/master-data-grid/components/dialogs/index.ts +1 -0
  119. package/src/custom/master-data-grid/components/filters/client-filter.tsx +368 -0
  120. package/src/custom/master-data-grid/components/filters/filter-input.tsx +256 -0
  121. package/src/custom/master-data-grid/components/filters/index.ts +3 -0
  122. package/src/custom/master-data-grid/components/filters/inline-column-filter.tsx +233 -0
  123. package/src/custom/master-data-grid/components/filters/multi-filter-dialog.tsx +90 -0
  124. package/src/custom/master-data-grid/components/filters/server-filter.tsx +255 -0
  125. package/src/custom/master-data-grid/components/master-data-grid.tsx +472 -0
  126. package/src/custom/master-data-grid/components/pagination/index.ts +1 -0
  127. package/src/custom/master-data-grid/components/pagination/pagination.tsx +178 -0
  128. package/src/custom/master-data-grid/components/table/cell-renderer.tsx +634 -0
  129. package/src/custom/master-data-grid/components/table/header-cell.tsx +162 -0
  130. package/src/custom/master-data-grid/components/table/index.ts +4 -0
  131. package/src/custom/master-data-grid/components/table/table-body-renderer.tsx +113 -0
  132. package/src/custom/master-data-grid/components/table/virtual-body.tsx +138 -0
  133. package/src/custom/master-data-grid/components/toolbar/index.ts +1 -0
  134. package/src/custom/master-data-grid/components/toolbar/toolbar.tsx +314 -0
  135. package/src/custom/master-data-grid/hooks/index.ts +3 -0
  136. package/src/custom/master-data-grid/hooks/use-columns.tsx +332 -0
  137. package/src/custom/master-data-grid/hooks/use-editing.ts +106 -0
  138. package/src/custom/master-data-grid/hooks/use-table-state-reducer.ts +157 -0
  139. package/src/custom/master-data-grid/hooks/use-table-state.ts +31 -0
  140. package/src/custom/master-data-grid/index.ts +16 -0
  141. package/src/custom/master-data-grid/types.ts +466 -0
  142. package/src/custom/master-data-grid/utils/column-generator.tsx +306 -0
  143. package/src/custom/master-data-grid/utils/export-utils.ts +67 -0
  144. package/src/custom/master-data-grid/utils/filter-fns.ts +290 -0
  145. package/src/custom/master-data-grid/utils/index.ts +8 -0
  146. package/src/custom/master-data-grid/utils/pinning-utils.ts +88 -0
  147. package/src/custom/master-data-grid/utils/translation-utils.ts +42 -0
  148. package/src/custom/multi-select.tsx +432 -0
  149. package/src/custom/password-input.tsx +194 -0
  150. package/src/custom/phone-input.tsx +172 -0
  151. package/src/custom/schema-form/custom/index.tsx +1 -0
  152. package/src/custom/schema-form/custom/label.tsx +53 -0
  153. package/src/custom/schema-form/fields/base-input-field.tsx +82 -0
  154. package/src/custom/schema-form/fields/field.tsx +67 -0
  155. package/src/custom/schema-form/fields/index.tsx +5 -0
  156. package/src/custom/schema-form/fields/object.tsx +12 -0
  157. package/src/custom/schema-form/fields/table-array/array-field-item.tsx +90 -0
  158. package/src/custom/schema-form/fields/table-array/array-field-template.tsx +115 -0
  159. package/src/custom/schema-form/index.tsx +259 -0
  160. package/src/custom/schema-form/templates/description.tsx +20 -0
  161. package/src/custom/schema-form/templates/index.tsx +2 -0
  162. package/src/custom/schema-form/templates/submit.tsx +32 -0
  163. package/src/custom/schema-form/types.ts +64 -0
  164. package/src/custom/schema-form/utils/index.ts +4 -0
  165. package/src/custom/schema-form/utils/schema-dependency.ts +655 -0
  166. package/src/custom/schema-form/utils/schemas.ts +289 -0
  167. package/src/custom/schema-form/utils/validation.ts +23 -0
  168. package/src/custom/schema-form/widgets/boolean.tsx +77 -0
  169. package/src/custom/schema-form/widgets/combobox.tsx +274 -0
  170. package/src/custom/schema-form/widgets/date.tsx +59 -0
  171. package/src/custom/schema-form/widgets/email.tsx +34 -0
  172. package/src/custom/schema-form/widgets/index.tsx +10 -0
  173. package/src/custom/schema-form/widgets/password.tsx +40 -0
  174. package/src/custom/schema-form/widgets/phone.tsx +40 -0
  175. package/src/custom/schema-form/widgets/select.tsx +105 -0
  176. package/src/custom/schema-form/widgets/selectable.tsx +25 -0
  177. package/src/custom/schema-form/widgets/string-array.tsx +296 -0
  178. package/src/custom/schema-form/widgets/url.tsx +56 -0
  179. package/src/custom/section-layout-v2.tsx +212 -0
  180. package/src/custom/select-tabs.tsx +109 -0
  181. package/src/custom/selectable.tsx +316 -0
  182. package/src/custom/stepper.tsx +236 -0
  183. package/src/custom/tab-layout.tsx +213 -0
  184. package/src/custom/tanstack-table/fields/index.tsx +12 -0
  185. package/src/custom/tanstack-table/fields/tanstack-table-action-dialogs.tsx +89 -0
  186. package/src/custom/tanstack-table/fields/tanstack-table-column-header.tsx +66 -0
  187. package/src/custom/tanstack-table/fields/tanstack-table-filter-date.tsx +180 -0
  188. package/src/custom/tanstack-table/fields/tanstack-table-filter-faceted.tsx +158 -0
  189. package/src/custom/tanstack-table/fields/tanstack-table-filter-text.tsx +76 -0
  190. package/src/custom/tanstack-table/fields/tanstack-table-pagination.tsx +136 -0
  191. package/src/custom/tanstack-table/fields/tanstack-table-plain-table.tsx +142 -0
  192. package/src/custom/tanstack-table/fields/tanstack-table-row-actions-confirmation.tsx +77 -0
  193. package/src/custom/tanstack-table/fields/tanstack-table-row-actions-custom-dialog.tsx +87 -0
  194. package/src/custom/tanstack-table/fields/tanstack-table-row-actions.tsx +151 -0
  195. package/src/custom/tanstack-table/fields/tanstack-table-table-actions-custom-dialog.tsx +88 -0
  196. package/src/custom/tanstack-table/fields/tanstack-table-table-actions-schemaform-dialog.tsx +47 -0
  197. package/src/custom/tanstack-table/fields/tanstack-table-toolbar.tsx +143 -0
  198. package/src/custom/tanstack-table/fields/tanstack-table-view-options.tsx +171 -0
  199. package/src/custom/tanstack-table/index.tsx +244 -0
  200. package/src/custom/tanstack-table/types/index.ts +328 -0
  201. package/src/custom/tanstack-table/utils/cell-with-actions.tsx +21 -0
  202. package/src/custom/tanstack-table/utils/column-names.ts +26 -0
  203. package/src/custom/tanstack-table/utils/columns-by-row-data.tsx +312 -0
  204. package/src/custom/tanstack-table/utils/editable-columns-by-row-data.tsx +219 -0
  205. package/src/custom/tanstack-table/utils/faceted-boolean-options.tsx +22 -0
  206. package/src/custom/tanstack-table/utils/index.tsx +10 -0
  207. package/src/custom/tanstack-table/utils/pinning-styles.ts +57 -0
  208. package/src/custom/tanstack-table/utils/table.tsx +83 -0
  209. package/src/custom/tanstack-table/utils/test-conditions.ts +17 -0
  210. package/src/custom/timeline.tsx +208 -0
  211. package/src/custom/tree.tsx +200 -0
  212. package/src/custom/tscanify/browser.ts +66 -0
  213. package/src/custom/tscanify/index.ts +51 -0
  214. package/src/custom/tscanify/tscanify-browser.ts +522 -0
  215. package/src/custom/tscanify/tscanify.ts +262 -0
  216. package/src/custom/tscanify/types.ts +22 -0
  217. package/src/custom/webcam.tsx +737 -0
  218. package/src/hooks/.gitkeep +0 -0
  219. package/src/hooks/use-callback-ref.ts +27 -0
  220. package/src/hooks/use-controllable-state.ts +67 -0
  221. package/src/hooks/use-debounce.ts +19 -0
  222. package/src/hooks/use-is-visible.ts +23 -0
  223. package/src/hooks/use-media-query.ts +21 -0
  224. package/src/hooks/use-mobile.ts +21 -0
  225. package/src/hooks/use-on-window-resize.ts +15 -0
  226. package/src/hooks/use-scroll.tsx +22 -0
  227. package/src/lib/utils.ts +61 -0
  228. package/src/lib/zod.ts +2 -0
  229. package/src/styles/core.css +57 -0
  230. package/src/styles/globals.css +130 -0
  231. package/src/test/email-input.test.tsx +217 -0
  232. package/src/test/password-input.test.tsx +92 -0
  233. package/src/test/select-tabs.test.tsx +302 -0
  234. package/src/test/selectable.test.tsx +1093 -0
  235. package/tsconfig.json +13 -0
  236. package/tsconfig.lint.json +8 -0
@@ -0,0 +1,233 @@
1
+ import type { Column } from "@tanstack/react-table";
2
+ import { ChevronDown } from "lucide-react";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import {
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ DropdownMenuTrigger,
9
+ } from "../../../../components/dropdown-menu";
10
+ import type {
11
+ ColumnFilter,
12
+ ColumnMeta,
13
+ FilterOperator,
14
+ MasterDataGridResources,
15
+ } from "../../types";
16
+ import {
17
+ getFilterOperators,
18
+ validateFilterValue,
19
+ } from "../../utils/filter-fns";
20
+ import { getTranslations } from "../../utils/translation-utils";
21
+ import { FilterInput } from "./filter-input";
22
+
23
+ interface InlineColumnFilterProps<TData> {
24
+ column: Column<TData, unknown>;
25
+ t?: MasterDataGridResources;
26
+ }
27
+
28
+ export function InlineColumnFilter<TData>({
29
+ column,
30
+ t,
31
+ }: InlineColumnFilterProps<TData>) {
32
+ const meta = column.columnDef.meta as ColumnMeta | undefined;
33
+ const schemaProperty = meta?.schemaProperty;
34
+ const filterOperators = meta?.filterOperators;
35
+
36
+ const availableOperators = useMemo<FilterOperator[]>(() => {
37
+ return (
38
+ filterOperators ||
39
+ (schemaProperty
40
+ ? getFilterOperators(schemaProperty.type, schemaProperty.format)
41
+ : ["contains"])
42
+ );
43
+ }, [filterOperators, schemaProperty?.type, schemaProperty?.format]);
44
+
45
+ const currentFilter = column.getFilterValue() as ColumnFilter | undefined;
46
+ const [operator, setOperator] = useState<FilterOperator>(
47
+ currentFilter?.operator || availableOperators[0] || "contains"
48
+ );
49
+ const [value, setValue] = useState<string>(
50
+ String(currentFilter?.value || "")
51
+ );
52
+ const [value2, setValue2] = useState<string>(
53
+ String(currentFilter?.value2 || "")
54
+ );
55
+ const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
56
+
57
+ const isRangeOperator = operator === "between" || operator === "inRange";
58
+
59
+ useEffect(() => {
60
+ return () => {
61
+ if (debounceTimerRef.current) {
62
+ clearTimeout(debounceTimerRef.current);
63
+ }
64
+ };
65
+ }, []);
66
+
67
+ const handleValueChange = useCallback(
68
+ (newValue: string) => {
69
+ setValue(newValue);
70
+
71
+ if (debounceTimerRef.current) {
72
+ clearTimeout(debounceTimerRef.current);
73
+ }
74
+
75
+ debounceTimerRef.current = setTimeout(() => {
76
+ if (!newValue && !isRangeOperator) {
77
+ column.setFilterValue(undefined);
78
+ return;
79
+ }
80
+
81
+ if (isRangeOperator) {
82
+ if (
83
+ newValue &&
84
+ value2 &&
85
+ validateFilterValue(operator, newValue, value2)
86
+ ) {
87
+ const filter: ColumnFilter = {
88
+ id: column.id,
89
+ operator,
90
+ value: newValue,
91
+ value2,
92
+ };
93
+ column.setFilterValue(filter);
94
+ }
95
+ } else if (validateFilterValue(operator, newValue)) {
96
+ const filter: ColumnFilter = {
97
+ id: column.id,
98
+ operator,
99
+ value: newValue,
100
+ };
101
+ column.setFilterValue(filter);
102
+ }
103
+ }, 300);
104
+ },
105
+ [column, operator, value2, isRangeOperator]
106
+ );
107
+
108
+ const handleValue2Change = useCallback(
109
+ (newValue2: string) => {
110
+ setValue2(newValue2);
111
+ if (debounceTimerRef.current) {
112
+ clearTimeout(debounceTimerRef.current);
113
+ }
114
+ debounceTimerRef.current = setTimeout(() => {
115
+ if (
116
+ value &&
117
+ newValue2 &&
118
+ validateFilterValue(operator, value, newValue2)
119
+ ) {
120
+ const filter: ColumnFilter = {
121
+ id: column.id,
122
+ operator,
123
+ value,
124
+ value2: newValue2,
125
+ };
126
+ column.setFilterValue(filter);
127
+ }
128
+ }, 300);
129
+ },
130
+ [column, operator, value]
131
+ );
132
+
133
+ const handleSliderChange = (values: number[]) => {
134
+ const [min, max] = values;
135
+ setValue(String(min));
136
+ setValue2(String(max));
137
+
138
+ const filter: ColumnFilter = {
139
+ id: column.id,
140
+ operator,
141
+ value: String(min),
142
+ value2: String(max),
143
+ };
144
+ column.setFilterValue(filter);
145
+ };
146
+
147
+ const handleClear = () => {
148
+ setValue("");
149
+ setValue2("");
150
+ column.setFilterValue(undefined);
151
+ };
152
+
153
+ const handleOperatorChange = (newOperator: FilterOperator) => {
154
+ setOperator(newOperator);
155
+
156
+ const isNewRangeOperator =
157
+ newOperator === "between" || newOperator === "inRange";
158
+ const needsNoInputNew =
159
+ newOperator === "isEmpty" || newOperator === "isNotEmpty";
160
+
161
+ if (needsNoInputNew) {
162
+ const filter: ColumnFilter = {
163
+ id: column.id,
164
+ operator: newOperator,
165
+ value: "",
166
+ };
167
+ column.setFilterValue(filter);
168
+ } else if (isNewRangeOperator) {
169
+ if (value && value2 && validateFilterValue(newOperator, value, value2)) {
170
+ const filter: ColumnFilter = {
171
+ id: column.id,
172
+ operator: newOperator,
173
+ value,
174
+ value2,
175
+ };
176
+ column.setFilterValue(filter);
177
+ } else {
178
+ column.setFilterValue(undefined);
179
+ }
180
+ } else {
181
+ if (value && validateFilterValue(newOperator, value)) {
182
+ const filter: ColumnFilter = {
183
+ id: column.id,
184
+ operator: newOperator,
185
+ value,
186
+ };
187
+ column.setFilterValue(filter);
188
+ } else {
189
+ column.setFilterValue(undefined);
190
+ }
191
+ }
192
+ };
193
+ const showOperatorSelect = availableOperators.length > 1;
194
+
195
+ return (
196
+ <div className="space-y-2">
197
+ {showOperatorSelect && (
198
+ <DropdownMenu>
199
+ <DropdownMenuTrigger asChild>
200
+ <button className="w-full px-2 py-1.5 text-xs font-medium hover:bg-accent rounded-md flex items-center justify-between">
201
+ {getTranslations(`filter.operator.${operator}`, t)}
202
+ <ChevronDown className="size-3" />
203
+ </button>
204
+ </DropdownMenuTrigger>
205
+ <DropdownMenuContent align="start">
206
+ {availableOperators.map((op) => (
207
+ <DropdownMenuItem
208
+ key={op}
209
+ onClick={() => handleOperatorChange(op)}
210
+ className="text-xs"
211
+ >
212
+ {getTranslations(`filter.operator.${op}`, t)}
213
+ </DropdownMenuItem>
214
+ ))}
215
+ </DropdownMenuContent>
216
+ </DropdownMenu>
217
+ )}
218
+
219
+ <FilterInput
220
+ operator={operator}
221
+ value={value}
222
+ value2={value2}
223
+ columnMeta={meta}
224
+ onValueChange={handleValueChange}
225
+ onValue2Change={handleValue2Change}
226
+ onSliderChange={handleSliderChange}
227
+ onClear={handleClear}
228
+ t={t}
229
+ variant="inline"
230
+ />
231
+ </div>
232
+ );
233
+ }
@@ -0,0 +1,90 @@
1
+ import {
2
+ Tabs,
3
+ TabsContent,
4
+ TabsList,
5
+ TabsTrigger,
6
+ } from "@repo/ayasofyazilim-ui/components/tabs";
7
+ import type { Table } from "@tanstack/react-table";
8
+
9
+ import { useIsMobile } from "@repo/ayasofyazilim-ui/hooks/use-mobile";
10
+ import { useState } from "react";
11
+ import {
12
+ Drawer,
13
+ DrawerContent,
14
+ DrawerHeader,
15
+ DrawerTitle,
16
+ DrawerTrigger,
17
+ } from "../../../../components/drawer";
18
+ import {
19
+ Popover,
20
+ PopoverContent,
21
+ PopoverTrigger,
22
+ } from "../../../../components/popover";
23
+ import type { MasterDataGridConfig } from "../../types";
24
+ import { getTranslations } from "../../utils/translation-utils";
25
+ import { ClientFilterContent } from "./client-filter";
26
+ import { ServerFilterContent } from "./server-filter";
27
+
28
+ export interface BaseMultiFilterDialogProps<TData> {
29
+ table: Table<TData>;
30
+ config: MasterDataGridConfig<TData>;
31
+ }
32
+ interface MultiFilterDialogProps<TData>
33
+ extends BaseMultiFilterDialogProps<TData> {
34
+ children: React.ReactNode;
35
+ }
36
+
37
+ export function MultiFilterDialog<TData>({
38
+ table,
39
+ config,
40
+ children,
41
+ }: MultiFilterDialogProps<TData>) {
42
+ const { t } = config;
43
+ const isMobile = useIsMobile();
44
+ const [open, setOpen] = useState(false);
45
+
46
+ const filterContent = (
47
+ <Tabs defaultValue="client">
48
+ {config.serverFilters && (
49
+ <TabsList>
50
+ <TabsTrigger value="client">{t?.["toolbar.client"]}</TabsTrigger>
51
+ <TabsTrigger value="server">{t?.["toolbar.server"]}</TabsTrigger>
52
+ </TabsList>
53
+ )}
54
+ <TabsContent value="client" className="w-full">
55
+ <ClientFilterContent setOpen={setOpen} table={table} config={config} />
56
+ </TabsContent>
57
+ <TabsContent
58
+ value="server"
59
+ className="w-full min-w-lg max-w-lg [&>fieldset]:p-0"
60
+ >
61
+ <ServerFilterContent table={table} config={config} />
62
+ </TabsContent>
63
+ </Tabs>
64
+ );
65
+
66
+ if (isMobile) {
67
+ return (
68
+ <Drawer open={open} onOpenChange={setOpen}>
69
+ <DrawerTrigger asChild>{children}</DrawerTrigger>
70
+ <DrawerContent>
71
+ <DrawerHeader>
72
+ <DrawerTitle>{getTranslations("filter.title", t)}</DrawerTitle>
73
+ </DrawerHeader>
74
+ <div className="px-4 pb-4 max-h-[70vh] overflow-y-auto">
75
+ {filterContent}
76
+ </div>
77
+ </DrawerContent>
78
+ </Drawer>
79
+ );
80
+ }
81
+
82
+ return (
83
+ <Popover open={open} onOpenChange={setOpen}>
84
+ <PopoverTrigger asChild>{children}</PopoverTrigger>
85
+ <PopoverContent className="w-auto max-w-3xl" align="end">
86
+ {filterContent}
87
+ </PopoverContent>
88
+ </Popover>
89
+ );
90
+ }
@@ -0,0 +1,255 @@
1
+ "use client";
2
+
3
+ import { Button } from "@repo/ayasofyazilim-ui/components/button";
4
+ import {
5
+ Field,
6
+ FieldError,
7
+ FieldGroup,
8
+ FieldLabel,
9
+ FieldSet,
10
+ } from "@repo/ayasofyazilim-ui/components/field";
11
+ import { Selectable } from "@repo/ayasofyazilim-ui/custom/selectable";
12
+ import { Loader2, RotateCcw, Search, XCircle } from "lucide-react";
13
+ import { usePathname, useRouter, useSearchParams } from "next/navigation";
14
+ import { useCallback, useState, useTransition } from "react";
15
+ import { ServerFilterConfig } from "../../types";
16
+ import { BaseMultiFilterDialogProps } from "./multi-filter-dialog";
17
+ import {
18
+ InputGroup,
19
+ InputGroupAddon,
20
+ InputGroupButton,
21
+ InputGroupInput,
22
+ } from "@repo/ayasofyazilim-ui/components/input-group";
23
+ import {
24
+ ScrollArea,
25
+ ScrollBar,
26
+ } from "@repo/ayasofyazilim-ui/components/scroll-area";
27
+ import { getTranslations } from "../../utils";
28
+
29
+ type FilterValue = string | number | boolean | string[] | undefined;
30
+
31
+ export function ServerFilterContent<TData>({
32
+ config,
33
+ }: BaseMultiFilterDialogProps<TData>) {
34
+ const router = useRouter();
35
+ const pathname = usePathname();
36
+ const searchParams = useSearchParams();
37
+ const [isPending, startTransition] = useTransition();
38
+
39
+ const { serverFilters } = config;
40
+ if (!serverFilters) return null;
41
+
42
+ const [resetCount, setResetCount] = useState(0);
43
+
44
+ const [localValues, setLocalValues] = useState<Record<string, FilterValue>>(
45
+ () => {
46
+ const initial: Record<string, FilterValue> = {};
47
+ serverFilters.forEach((filter) => {
48
+ const val = searchParams.get(filter.key);
49
+ if (filter.type === "array") {
50
+ initial[filter.key] = searchParams.getAll(filter.key);
51
+ } else if (filter.type === "boolean") {
52
+ initial[filter.key] =
53
+ val === "true" ? true : val === "false" ? false : undefined;
54
+ } else if (filter.type === "number") {
55
+ initial[filter.key] = val ? Number(val) : undefined;
56
+ } else {
57
+ initial[filter.key] = val || undefined;
58
+ }
59
+ });
60
+ return initial;
61
+ }
62
+ );
63
+
64
+ const [errors, setErrors] = useState<Record<string, string>>({});
65
+
66
+ const onValueChange = useCallback(
67
+ (filter: ServerFilterConfig, rawValue: FilterValue) => {
68
+ let processedValue: FilterValue = rawValue;
69
+ if (
70
+ rawValue === "" ||
71
+ (Array.isArray(rawValue) && rawValue.length === 0)
72
+ ) {
73
+ processedValue = undefined;
74
+ }
75
+
76
+ if (filter.validator) {
77
+ const result = filter.validator.safeParse(processedValue);
78
+ setErrors((prev) => ({
79
+ ...prev,
80
+ [filter.key]: result.success
81
+ ? ""
82
+ : result.error.issues[0]?.message || "Hata",
83
+ }));
84
+ }
85
+
86
+ setLocalValues((prev) => ({ ...prev, [filter.key]: processedValue }));
87
+ },
88
+ []
89
+ );
90
+
91
+ const handleApply = () => {
92
+ const params = new URLSearchParams(searchParams.toString());
93
+ let hasValidationError = false;
94
+
95
+ serverFilters.forEach((filter) => {
96
+ const val = localValues[filter.key];
97
+ params.delete(filter.key);
98
+ const isEmpty =
99
+ val === undefined ||
100
+ val === null ||
101
+ val === "" ||
102
+ (Array.isArray(val) && val.length === 0);
103
+
104
+ if (!isEmpty) {
105
+ if (filter.validator) {
106
+ const result = filter.validator.safeParse(val);
107
+ if (!result.success) {
108
+ hasValidationError = true;
109
+ setErrors((prev) => ({
110
+ ...prev,
111
+ [filter.key]: result.error.issues[0]?.message || "Hata",
112
+ }));
113
+ return;
114
+ }
115
+ }
116
+ if (Array.isArray(val)) {
117
+ val.forEach((v) => params.append(filter.key, String(v)));
118
+ } else {
119
+ params.set(filter.key, String(val));
120
+ }
121
+ } else {
122
+ setErrors((prev) => ({ ...prev, [filter.key]: "" }));
123
+ }
124
+ });
125
+ if (hasValidationError) return;
126
+ params.delete("page");
127
+ startTransition(() => {
128
+ router.push(`${pathname}?${params.toString()}`, { scroll: false });
129
+ });
130
+ };
131
+
132
+ const handleReset = () => {
133
+ const initial: Record<string, FilterValue> = {};
134
+ serverFilters.forEach((f) => {
135
+ initial[f.key] = f.type === "array" ? [] : undefined;
136
+ });
137
+ setLocalValues(initial);
138
+ setErrors({});
139
+ setResetCount((prev) => prev + 1);
140
+ startTransition(() => router.push(pathname, { scroll: false }));
141
+ };
142
+
143
+ const clearSingleFilter = useCallback((filter: ServerFilterConfig) => {
144
+ const emptyValue =
145
+ filter.type === "array" ? [] : filter.type === "boolean" ? undefined : "";
146
+ setLocalValues((prev) => ({ ...prev, [filter.key]: emptyValue }));
147
+ setErrors((prev) => ({ ...prev, [filter.key]: "" }));
148
+ if (["select", "array", "boolean"].includes(filter.type)) {
149
+ setResetCount((prev) => prev + 1);
150
+ }
151
+ }, []);
152
+
153
+ return (
154
+ <FieldSet className="p-2">
155
+ <ScrollArea className="pr-4">
156
+ <ScrollBar />
157
+ <FieldGroup className={"gap-3 max-h-80"}>
158
+ {serverFilters.map((filter) => {
159
+ const value = localValues[filter.key];
160
+
161
+ const isSelectable =
162
+ filter.type === "select" ||
163
+ filter.type === "array" ||
164
+ filter.type === "boolean";
165
+ if (isSelectable) {
166
+ return (
167
+ <Field key={filter.key} className="gap-1">
168
+ <FieldLabel htmlFor={filter.key}>{filter.label}</FieldLabel>
169
+ <Selectable
170
+ id={filter.key}
171
+ key={`${filter.key}-${resetCount}`}
172
+ singular={filter.type !== "array"}
173
+ options={filter.options}
174
+ defaultValue={filter.options.filter((opt) =>
175
+ Array.isArray(value)
176
+ ? value.includes(opt.value)
177
+ : String(value) === opt.value
178
+ )}
179
+ getKey={(opt) => opt.value}
180
+ getLabel={(opt) => opt.label}
181
+ onChange={(selected) => {
182
+ const values = selected.map((s) => s.value);
183
+ onValueChange(
184
+ filter,
185
+ filter.type === "array"
186
+ ? values
187
+ : values[0] || undefined
188
+ );
189
+ }}
190
+ searchPlaceholderText={filter.placeholder}
191
+ makeAChoiceText={filter.placeholder}
192
+ />
193
+ {errors[filter.key] && (
194
+ <FieldError>{errors[filter.key]}</FieldError>
195
+ )}
196
+ </Field>
197
+ );
198
+ }
199
+ return (
200
+ <Field key={filter.key} className="gap-1">
201
+ <FieldLabel htmlFor={filter.key}>{filter.label}</FieldLabel>
202
+ <InputGroup>
203
+ <InputGroupInput
204
+ id={filter.key}
205
+ type={filter.type === "number" ? "number" : "text"}
206
+ value={
207
+ typeof value === "boolean" || typeof value === "object"
208
+ ? ""
209
+ : value ?? ""
210
+ }
211
+ placeholder={filter.placeholder}
212
+ onKeyDown={(e) => e.key === "Enter" && handleApply()}
213
+ onChange={(e) => onValueChange(filter, e.target.value)}
214
+ className={errors[filter.key] ? "border-destructive" : ""}
215
+ />
216
+ {localValues[filter.key] && (
217
+ <InputGroupAddon align="inline-end">
218
+ <InputGroupButton
219
+ onClick={() => clearSingleFilter(filter)}
220
+ >
221
+ <XCircle />
222
+ </InputGroupButton>
223
+ </InputGroupAddon>
224
+ )}
225
+ </InputGroup>
226
+ {errors[filter.key] && (
227
+ <FieldError>{errors[filter.key]}</FieldError>
228
+ )}
229
+ </Field>
230
+ );
231
+ })}
232
+ </FieldGroup>
233
+ </ScrollArea>
234
+ <Field orientation="horizontal">
235
+ <Button
236
+ type="button"
237
+ variant="ghost"
238
+ onClick={handleReset}
239
+ disabled={isPending}
240
+ >
241
+ <RotateCcw className="mr-2 h-4 w-4" />{" "}
242
+ {getTranslations("filter.clear", config.t)}
243
+ </Button>
244
+ <Button type="button" onClick={handleApply} disabled={isPending}>
245
+ {isPending ? (
246
+ <Loader2 className="animate-spin" />
247
+ ) : (
248
+ <Search className="mr-2 h-4 w-4" />
249
+ )}
250
+ {getTranslations("filter.apply", config.t)}
251
+ </Button>
252
+ </Field>
253
+ </FieldSet>
254
+ );
255
+ }