@snapdragonsnursery/react-components 1.1.8 → 1.1.10
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/package.json +8 -3
- package/src/ChildSearchModal.jsx +17 -7
- package/src/ChildSearchPage.jsx +803 -0
- package/src/ChildSearchPageDemo.jsx +78 -0
- package/src/components/ui/button.jsx +51 -0
- package/src/components/ui/input.jsx +22 -0
- package/src/components/ui/pagination.jsx +95 -0
- package/src/components/ui/select.jsx +34 -0
- package/src/components/ui/table.jsx +95 -0
- package/src/index.js +2 -0
- package/src/lib/utils.js +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@snapdragonsnursery/react-components",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.10",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"keywords": [],
|
|
32
32
|
"author": "",
|
|
33
33
|
"license": "ISC",
|
|
34
|
-
"type": "
|
|
34
|
+
"type": "module",
|
|
35
35
|
"bugs": {
|
|
36
36
|
"url": "https://github.com/Snapdragons-Nursery/react-components/issues"
|
|
37
37
|
},
|
|
@@ -39,8 +39,13 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@headlessui/react": "^2.2.4",
|
|
41
41
|
"@popperjs/core": "^2.11.8",
|
|
42
|
+
"@tanstack/react-table": "^8.21.3",
|
|
43
|
+
"class-variance-authority": "^0.7.1",
|
|
44
|
+
"clsx": "^2.1.1",
|
|
45
|
+
"lucide-react": "^0.526.0",
|
|
42
46
|
"react": "^18.3.1",
|
|
43
|
-
"react-dom": "^18.3.1"
|
|
47
|
+
"react-dom": "^18.3.1",
|
|
48
|
+
"tailwind-merge": "^3.3.1"
|
|
44
49
|
},
|
|
45
50
|
"peerDependencies": {
|
|
46
51
|
"@azure/msal-react": ">=1.0.0",
|
package/src/ChildSearchModal.jsx
CHANGED
|
@@ -98,14 +98,22 @@ const ChildSearchModal = ({
|
|
|
98
98
|
if (isOpen && modalRef.current) {
|
|
99
99
|
const modal = modalRef.current;
|
|
100
100
|
const rect = modal.getBoundingClientRect();
|
|
101
|
+
const viewportHeight = window.innerHeight;
|
|
102
|
+
const expectedHeight = viewportHeight * 0.9;
|
|
103
|
+
const expectedContentHeight = expectedHeight - 300;
|
|
104
|
+
|
|
101
105
|
console.log('🔍 Modal Debug:', {
|
|
102
106
|
modalHeight: rect.height,
|
|
103
107
|
modalTop: rect.top,
|
|
104
108
|
modalBottom: rect.bottom,
|
|
105
|
-
viewportHeight:
|
|
109
|
+
viewportHeight: viewportHeight,
|
|
110
|
+
expectedHeight: expectedHeight,
|
|
111
|
+
expectedContentHeight: expectedContentHeight,
|
|
106
112
|
childrenCount: children.length,
|
|
107
113
|
loading,
|
|
108
|
-
error
|
|
114
|
+
error,
|
|
115
|
+
modalStyle: modal.style,
|
|
116
|
+
modalComputedStyle: window.getComputedStyle(modal)
|
|
109
117
|
});
|
|
110
118
|
|
|
111
119
|
// Check if modal is overflowing
|
|
@@ -342,12 +350,13 @@ const ChildSearchModal = ({
|
|
|
342
350
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4 overflow-hidden">
|
|
343
351
|
<div
|
|
344
352
|
ref={modalRef}
|
|
345
|
-
className={`bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-4xl w-full
|
|
353
|
+
className={`bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-4xl w-full flex flex-col overflow-hidden ${className}`}
|
|
346
354
|
style={{
|
|
347
355
|
maxHeight: '90vh',
|
|
348
356
|
height: '90vh',
|
|
349
357
|
display: 'flex',
|
|
350
|
-
flexDirection: 'column'
|
|
358
|
+
flexDirection: 'column',
|
|
359
|
+
position: 'relative'
|
|
351
360
|
}}
|
|
352
361
|
>
|
|
353
362
|
{/* Header */}
|
|
@@ -648,10 +657,11 @@ const ChildSearchModal = ({
|
|
|
648
657
|
<div
|
|
649
658
|
className="overflow-hidden"
|
|
650
659
|
style={{
|
|
651
|
-
height: 'calc(90vh -
|
|
652
|
-
maxHeight: 'calc(90vh -
|
|
660
|
+
height: 'calc(90vh - 300px)',
|
|
661
|
+
maxHeight: 'calc(90vh - 300px)',
|
|
653
662
|
overflow: 'hidden',
|
|
654
|
-
flex: 'none'
|
|
663
|
+
flex: 'none',
|
|
664
|
+
position: 'relative'
|
|
655
665
|
}}
|
|
656
666
|
ref={(el) => {
|
|
657
667
|
if (el) {
|
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
// Full page Child Search component with TanStack table and shadcn styling
|
|
2
|
+
// Provides a comprehensive search interface for children with advanced filtering and pagination
|
|
3
|
+
|
|
4
|
+
import React, { useState, useEffect, useCallback } from "react";
|
|
5
|
+
import { useMsal } from "@azure/msal-react";
|
|
6
|
+
import {
|
|
7
|
+
useReactTable,
|
|
8
|
+
getCoreRowModel,
|
|
9
|
+
getSortedRowModel,
|
|
10
|
+
flexRender,
|
|
11
|
+
createColumnHelper,
|
|
12
|
+
} from "@tanstack/react-table";
|
|
13
|
+
import { trackEvent } from "./telemetry";
|
|
14
|
+
import { Button } from "./components/ui/button";
|
|
15
|
+
import { Input } from "./components/ui/input";
|
|
16
|
+
import { Select, SelectOption } from "./components/ui/select";
|
|
17
|
+
import {
|
|
18
|
+
Table,
|
|
19
|
+
TableBody,
|
|
20
|
+
TableCell,
|
|
21
|
+
TableHead,
|
|
22
|
+
TableHeader,
|
|
23
|
+
TableRow,
|
|
24
|
+
} from "./components/ui/table";
|
|
25
|
+
import {
|
|
26
|
+
Pagination,
|
|
27
|
+
PaginationContent,
|
|
28
|
+
PaginationEllipsis,
|
|
29
|
+
PaginationItem,
|
|
30
|
+
PaginationLink,
|
|
31
|
+
PaginationNext,
|
|
32
|
+
PaginationPrevious,
|
|
33
|
+
} from "./components/ui/pagination";
|
|
34
|
+
import {
|
|
35
|
+
Search,
|
|
36
|
+
Filter,
|
|
37
|
+
ChevronDown,
|
|
38
|
+
ChevronUp,
|
|
39
|
+
User,
|
|
40
|
+
MapPin,
|
|
41
|
+
Calendar,
|
|
42
|
+
Hash,
|
|
43
|
+
CheckCircle,
|
|
44
|
+
XCircle,
|
|
45
|
+
} from "lucide-react";
|
|
46
|
+
import { cn } from "./lib/utils";
|
|
47
|
+
|
|
48
|
+
const ChildSearchPage = ({
|
|
49
|
+
title = "Search Children",
|
|
50
|
+
siteId = null,
|
|
51
|
+
siteIds = null,
|
|
52
|
+
sites = null,
|
|
53
|
+
activeOnly = true,
|
|
54
|
+
status = null,
|
|
55
|
+
dobFrom = null,
|
|
56
|
+
dobTo = null,
|
|
57
|
+
ageFrom = null,
|
|
58
|
+
ageTo = null,
|
|
59
|
+
sortBy = "last_name",
|
|
60
|
+
sortOrder = "asc",
|
|
61
|
+
applicationContext = "child-search",
|
|
62
|
+
bypassPermissions = false,
|
|
63
|
+
onSelect = null, // Optional callback for when a child is selected
|
|
64
|
+
multiSelect = false,
|
|
65
|
+
maxSelections = null,
|
|
66
|
+
selectedChildren = [],
|
|
67
|
+
}) => {
|
|
68
|
+
const [searchTerm, setSearchTerm] = useState("");
|
|
69
|
+
const [children, setChildren] = useState([]);
|
|
70
|
+
const [loading, setLoading] = useState(false);
|
|
71
|
+
const [error, setError] = useState(null);
|
|
72
|
+
const [pagination, setPagination] = useState({
|
|
73
|
+
page: 1,
|
|
74
|
+
pageSize: 20,
|
|
75
|
+
totalCount: 0,
|
|
76
|
+
totalPages: 0,
|
|
77
|
+
hasNextPage: false,
|
|
78
|
+
hasPreviousPage: false,
|
|
79
|
+
});
|
|
80
|
+
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
|
|
81
|
+
const [isAdvancedFiltersOpen, setIsAdvancedFiltersOpen] = useState(false);
|
|
82
|
+
const [advancedFilters, setAdvancedFilters] = useState({
|
|
83
|
+
status: status || (activeOnly ? "active" : "all"),
|
|
84
|
+
selectedSiteId: siteId || "",
|
|
85
|
+
dobFrom: dobFrom || "",
|
|
86
|
+
dobTo: dobTo || "",
|
|
87
|
+
ageFrom: ageFrom || "",
|
|
88
|
+
ageTo: ageTo || "",
|
|
89
|
+
sortBy: sortBy,
|
|
90
|
+
sortOrder: sortOrder,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// State for multi-select mode
|
|
94
|
+
const [selectedChildrenState, setSelectedChildrenState] = useState(
|
|
95
|
+
selectedChildren || []
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const { instance, accounts } = useMsal();
|
|
99
|
+
|
|
100
|
+
// Column helper for TanStack table
|
|
101
|
+
const columnHelper = createColumnHelper();
|
|
102
|
+
|
|
103
|
+
// Define table columns
|
|
104
|
+
const columns = [
|
|
105
|
+
// Checkbox column for multi-select
|
|
106
|
+
...(multiSelect
|
|
107
|
+
? [
|
|
108
|
+
columnHelper.display({
|
|
109
|
+
id: "select",
|
|
110
|
+
header: ({ table }) => (
|
|
111
|
+
<input
|
|
112
|
+
type="checkbox"
|
|
113
|
+
checked={table.getIsAllPageRowsSelected()}
|
|
114
|
+
onChange={table.getToggleAllPageRowsSelectedHandler()}
|
|
115
|
+
className="rounded border-gray-300"
|
|
116
|
+
/>
|
|
117
|
+
),
|
|
118
|
+
cell: ({ row }) => (
|
|
119
|
+
<input
|
|
120
|
+
type="checkbox"
|
|
121
|
+
checked={row.getIsSelected()}
|
|
122
|
+
onChange={row.getToggleSelectedHandler()}
|
|
123
|
+
className="rounded border-gray-300"
|
|
124
|
+
/>
|
|
125
|
+
),
|
|
126
|
+
size: 50,
|
|
127
|
+
}),
|
|
128
|
+
]
|
|
129
|
+
: []),
|
|
130
|
+
// Name column
|
|
131
|
+
columnHelper.accessor("full_name", {
|
|
132
|
+
header: "Name",
|
|
133
|
+
cell: ({ row }) => (
|
|
134
|
+
<div className="flex items-center space-x-2">
|
|
135
|
+
<User className="h-4 w-4 text-gray-400" />
|
|
136
|
+
<div>
|
|
137
|
+
<div className="font-medium">{row.original.full_name}</div>
|
|
138
|
+
<div className="text-sm text-gray-500">
|
|
139
|
+
ID: {row.original.child_id}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
),
|
|
144
|
+
}),
|
|
145
|
+
// Site column
|
|
146
|
+
columnHelper.accessor("site_name", {
|
|
147
|
+
header: "Site",
|
|
148
|
+
cell: ({ row }) => (
|
|
149
|
+
<div className="flex items-center space-x-2">
|
|
150
|
+
<MapPin className="h-4 w-4 text-gray-400" />
|
|
151
|
+
<span>{row.original.site_name}</span>
|
|
152
|
+
</div>
|
|
153
|
+
),
|
|
154
|
+
}),
|
|
155
|
+
// Date of Birth column
|
|
156
|
+
columnHelper.accessor("date_of_birth", {
|
|
157
|
+
header: "Date of Birth",
|
|
158
|
+
cell: ({ row }) => (
|
|
159
|
+
<div className="flex items-center space-x-2">
|
|
160
|
+
<Calendar className="h-4 w-4 text-gray-400" />
|
|
161
|
+
<span>
|
|
162
|
+
{row.original.date_of_birth
|
|
163
|
+
? new Date(row.original.date_of_birth).toLocaleDateString("en-GB")
|
|
164
|
+
: "N/A"}
|
|
165
|
+
</span>
|
|
166
|
+
</div>
|
|
167
|
+
),
|
|
168
|
+
}),
|
|
169
|
+
// Age column
|
|
170
|
+
columnHelper.accessor("age_years", {
|
|
171
|
+
header: "Age",
|
|
172
|
+
cell: ({ row }) => {
|
|
173
|
+
const { age_years, age_months } = row.original;
|
|
174
|
+
const ageText =
|
|
175
|
+
age_years || age_months
|
|
176
|
+
? `${age_years || 0}y ${age_months || 0}m`
|
|
177
|
+
: "N/A";
|
|
178
|
+
return (
|
|
179
|
+
<div className="flex items-center space-x-2">
|
|
180
|
+
<Hash className="h-4 w-4 text-gray-400" />
|
|
181
|
+
<span>{ageText}</span>
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
},
|
|
185
|
+
}),
|
|
186
|
+
// Status column
|
|
187
|
+
columnHelper.accessor("is_active", {
|
|
188
|
+
header: "Status",
|
|
189
|
+
cell: ({ row }) => (
|
|
190
|
+
<div className="flex items-center space-x-2">
|
|
191
|
+
{row.original.is_active ? (
|
|
192
|
+
<CheckCircle className="h-4 w-4 text-green-500" />
|
|
193
|
+
) : (
|
|
194
|
+
<XCircle className="h-4 w-4 text-red-500" />
|
|
195
|
+
)}
|
|
196
|
+
<span
|
|
197
|
+
className={cn(
|
|
198
|
+
"px-2 py-1 text-xs rounded-full",
|
|
199
|
+
row.original.is_active
|
|
200
|
+
? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
|
|
201
|
+
: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
|
|
202
|
+
)}
|
|
203
|
+
>
|
|
204
|
+
{row.original.is_active ? "Active" : "Inactive"}
|
|
205
|
+
</span>
|
|
206
|
+
</div>
|
|
207
|
+
),
|
|
208
|
+
}),
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
// Create table instance
|
|
212
|
+
const table = useReactTable({
|
|
213
|
+
data: children,
|
|
214
|
+
columns,
|
|
215
|
+
getCoreRowModel: getCoreRowModel(),
|
|
216
|
+
getSortedRowModel: getSortedRowModel(),
|
|
217
|
+
state: {
|
|
218
|
+
rowSelection: selectedChildrenState.reduce((acc, child, index) => {
|
|
219
|
+
acc[index] = true;
|
|
220
|
+
return acc;
|
|
221
|
+
}, {}),
|
|
222
|
+
},
|
|
223
|
+
onRowSelectionChange: (updater) => {
|
|
224
|
+
const newSelection =
|
|
225
|
+
typeof updater === "function" ? updater({}) : updater;
|
|
226
|
+
const selectedRows = Object.keys(newSelection).map(
|
|
227
|
+
(index) => children[parseInt(index)]
|
|
228
|
+
);
|
|
229
|
+
setSelectedChildrenState(selectedRows);
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Debounce search term
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
const timer = setTimeout(() => {
|
|
236
|
+
setDebouncedSearchTerm(searchTerm);
|
|
237
|
+
setPagination((prev) => ({ ...prev, page: 1 }));
|
|
238
|
+
}, 300);
|
|
239
|
+
|
|
240
|
+
return () => clearTimeout(timer);
|
|
241
|
+
}, [searchTerm]);
|
|
242
|
+
|
|
243
|
+
// Search children
|
|
244
|
+
const searchChildren = useCallback(async () => {
|
|
245
|
+
if (!instance || !accounts[0]) {
|
|
246
|
+
setError("Authentication required");
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
setLoading(true);
|
|
251
|
+
setError(null);
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const functionKey = import.meta.env.VITE_COMMON_API_FUNCTION_KEY || "";
|
|
255
|
+
|
|
256
|
+
const params = new URLSearchParams({
|
|
257
|
+
entra_id: accounts[0].localAccountId,
|
|
258
|
+
search_term: debouncedSearchTerm,
|
|
259
|
+
page: pagination.page,
|
|
260
|
+
page_size: pagination.pageSize,
|
|
261
|
+
application_context: applicationContext,
|
|
262
|
+
bypass_permissions: bypassPermissions.toString(),
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Handle site filtering
|
|
266
|
+
if (siteIds && siteIds.length > 0) {
|
|
267
|
+
params.append("site_ids", siteIds.join(","));
|
|
268
|
+
} else if (advancedFilters.selectedSiteId) {
|
|
269
|
+
params.append("site_id", advancedFilters.selectedSiteId.toString());
|
|
270
|
+
} else if (siteId) {
|
|
271
|
+
params.append("site_id", siteId.toString());
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Handle status filtering
|
|
275
|
+
if (advancedFilters.status && advancedFilters.status !== "all") {
|
|
276
|
+
params.append("status", advancedFilters.status);
|
|
277
|
+
} else if (activeOnly) {
|
|
278
|
+
params.append("active_only", "true");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Add date of birth filters
|
|
282
|
+
if (advancedFilters.dobFrom) {
|
|
283
|
+
params.append("dob_from", advancedFilters.dobFrom);
|
|
284
|
+
}
|
|
285
|
+
if (advancedFilters.dobTo) {
|
|
286
|
+
params.append("dob_to", advancedFilters.dobTo);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Add age filters
|
|
290
|
+
if (advancedFilters.ageFrom) {
|
|
291
|
+
params.append("age_from", advancedFilters.ageFrom);
|
|
292
|
+
}
|
|
293
|
+
if (advancedFilters.ageTo) {
|
|
294
|
+
params.append("age_to", advancedFilters.ageTo);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Add sorting
|
|
298
|
+
params.append("sort_by", advancedFilters.sortBy);
|
|
299
|
+
params.append("sort_order", advancedFilters.sortOrder);
|
|
300
|
+
|
|
301
|
+
const apiResponse = await fetch(
|
|
302
|
+
`${
|
|
303
|
+
import.meta.env.VITE_COMMON_API_BASE_URL ||
|
|
304
|
+
"https://snaps-common-api.azurewebsites.net"
|
|
305
|
+
}/api/search-children?${params}`,
|
|
306
|
+
{
|
|
307
|
+
headers: {
|
|
308
|
+
"Content-Type": "application/json",
|
|
309
|
+
"x-functions-key": functionKey,
|
|
310
|
+
},
|
|
311
|
+
}
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
if (!apiResponse.ok) {
|
|
315
|
+
throw new Error(`API request failed: ${apiResponse.status}`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const data = await apiResponse.json();
|
|
319
|
+
|
|
320
|
+
if (data.success) {
|
|
321
|
+
setChildren(data.data.children);
|
|
322
|
+
setPagination(data.data.pagination);
|
|
323
|
+
trackEvent("child_search_success", {
|
|
324
|
+
searchTerm: debouncedSearchTerm,
|
|
325
|
+
resultCount: data.data.children.length,
|
|
326
|
+
totalCount: data.data.pagination.totalCount,
|
|
327
|
+
});
|
|
328
|
+
} else {
|
|
329
|
+
throw new Error(data.error || "Search failed");
|
|
330
|
+
}
|
|
331
|
+
} catch (err) {
|
|
332
|
+
console.error("Error searching children:", err);
|
|
333
|
+
setError(err.message);
|
|
334
|
+
trackEvent("child_search_error", {
|
|
335
|
+
error: err.message,
|
|
336
|
+
searchTerm: debouncedSearchTerm,
|
|
337
|
+
});
|
|
338
|
+
} finally {
|
|
339
|
+
setLoading(false);
|
|
340
|
+
}
|
|
341
|
+
}, [
|
|
342
|
+
instance,
|
|
343
|
+
accounts,
|
|
344
|
+
debouncedSearchTerm,
|
|
345
|
+
pagination.page,
|
|
346
|
+
pagination.pageSize,
|
|
347
|
+
siteId,
|
|
348
|
+
siteIds,
|
|
349
|
+
activeOnly,
|
|
350
|
+
advancedFilters,
|
|
351
|
+
applicationContext,
|
|
352
|
+
bypassPermissions,
|
|
353
|
+
]);
|
|
354
|
+
|
|
355
|
+
// Search when debounced term changes
|
|
356
|
+
useEffect(() => {
|
|
357
|
+
searchChildren();
|
|
358
|
+
}, [debouncedSearchTerm, pagination.page, searchChildren]);
|
|
359
|
+
|
|
360
|
+
const handlePageChange = (newPage) => {
|
|
361
|
+
setPagination((prev) => ({ ...prev, page: newPage }));
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const handleChildSelect = (child) => {
|
|
365
|
+
if (onSelect) {
|
|
366
|
+
if (multiSelect) {
|
|
367
|
+
const isAlreadySelected = selectedChildrenState.some(
|
|
368
|
+
(selected) => selected.child_id === child.child_id
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
if (isAlreadySelected) {
|
|
372
|
+
const updatedSelection = selectedChildrenState.filter(
|
|
373
|
+
(selected) => selected.child_id !== child.child_id
|
|
374
|
+
);
|
|
375
|
+
setSelectedChildrenState(updatedSelection);
|
|
376
|
+
onSelect(updatedSelection);
|
|
377
|
+
} else {
|
|
378
|
+
if (maxSelections && selectedChildrenState.length >= maxSelections) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const updatedSelection = [...selectedChildrenState, child];
|
|
382
|
+
setSelectedChildrenState(updatedSelection);
|
|
383
|
+
onSelect(updatedSelection);
|
|
384
|
+
}
|
|
385
|
+
} else {
|
|
386
|
+
onSelect(child);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const clearFilters = () => {
|
|
392
|
+
setAdvancedFilters({
|
|
393
|
+
status: activeOnly ? "active" : "all",
|
|
394
|
+
selectedSiteId: "",
|
|
395
|
+
dobFrom: "",
|
|
396
|
+
dobTo: "",
|
|
397
|
+
ageFrom: "",
|
|
398
|
+
ageTo: "",
|
|
399
|
+
sortBy: "last_name",
|
|
400
|
+
sortOrder: "asc",
|
|
401
|
+
});
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
return (
|
|
405
|
+
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
|
406
|
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
407
|
+
{/* Header */}
|
|
408
|
+
<div className="mb-8">
|
|
409
|
+
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
|
410
|
+
{title}
|
|
411
|
+
</h1>
|
|
412
|
+
{multiSelect && (
|
|
413
|
+
<div className="mt-2 flex items-center space-x-2">
|
|
414
|
+
<span className="px-3 py-1 text-sm bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 rounded-full">
|
|
415
|
+
{selectedChildrenState.length} selected
|
|
416
|
+
{maxSelections && ` / ${maxSelections}`}
|
|
417
|
+
</span>
|
|
418
|
+
</div>
|
|
419
|
+
)}
|
|
420
|
+
</div>
|
|
421
|
+
|
|
422
|
+
{/* Search and Filters */}
|
|
423
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 mb-6">
|
|
424
|
+
<div className="p-6">
|
|
425
|
+
{/* Search Input */}
|
|
426
|
+
<div className="relative mb-4">
|
|
427
|
+
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
|
428
|
+
<Input
|
|
429
|
+
type="text"
|
|
430
|
+
placeholder="Search by name or child ID..."
|
|
431
|
+
value={searchTerm}
|
|
432
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
433
|
+
className="pl-10"
|
|
434
|
+
/>
|
|
435
|
+
</div>
|
|
436
|
+
|
|
437
|
+
{/* Advanced Filters Toggle */}
|
|
438
|
+
<div className="flex items-center justify-between">
|
|
439
|
+
<button
|
|
440
|
+
onClick={() => setIsAdvancedFiltersOpen(!isAdvancedFiltersOpen)}
|
|
441
|
+
className="flex items-center space-x-2 text-sm text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300"
|
|
442
|
+
>
|
|
443
|
+
<Filter className="h-4 w-4" />
|
|
444
|
+
<span>Advanced Filters</span>
|
|
445
|
+
{isAdvancedFiltersOpen ? (
|
|
446
|
+
<ChevronUp className="h-4 w-4" />
|
|
447
|
+
) : (
|
|
448
|
+
<ChevronDown className="h-4 w-4" />
|
|
449
|
+
)}
|
|
450
|
+
</button>
|
|
451
|
+
<button
|
|
452
|
+
onClick={clearFilters}
|
|
453
|
+
className="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200"
|
|
454
|
+
>
|
|
455
|
+
Clear Filters
|
|
456
|
+
</button>
|
|
457
|
+
</div>
|
|
458
|
+
|
|
459
|
+
{/* Advanced Filters */}
|
|
460
|
+
{isAdvancedFiltersOpen && (
|
|
461
|
+
<div className="mt-4 p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
462
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
463
|
+
{/* Status Filter */}
|
|
464
|
+
<div>
|
|
465
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
466
|
+
Status
|
|
467
|
+
</label>
|
|
468
|
+
<Select
|
|
469
|
+
value={advancedFilters.status}
|
|
470
|
+
onChange={(e) =>
|
|
471
|
+
setAdvancedFilters((prev) => ({
|
|
472
|
+
...prev,
|
|
473
|
+
status: e.target.value,
|
|
474
|
+
}))
|
|
475
|
+
}
|
|
476
|
+
>
|
|
477
|
+
<SelectOption value="all">All Children</SelectOption>
|
|
478
|
+
<SelectOption value="active">Active Only</SelectOption>
|
|
479
|
+
<SelectOption value="inactive">
|
|
480
|
+
Inactive Only
|
|
481
|
+
</SelectOption>
|
|
482
|
+
</Select>
|
|
483
|
+
</div>
|
|
484
|
+
|
|
485
|
+
{/* Site Filter */}
|
|
486
|
+
{sites && sites.length > 0 && (
|
|
487
|
+
<div>
|
|
488
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
489
|
+
Site
|
|
490
|
+
</label>
|
|
491
|
+
<Select
|
|
492
|
+
value={advancedFilters.selectedSiteId}
|
|
493
|
+
onChange={(e) =>
|
|
494
|
+
setAdvancedFilters((prev) => ({
|
|
495
|
+
...prev,
|
|
496
|
+
selectedSiteId: e.target.value,
|
|
497
|
+
}))
|
|
498
|
+
}
|
|
499
|
+
>
|
|
500
|
+
<SelectOption value="">All Sites</SelectOption>
|
|
501
|
+
{sites.map((site) => (
|
|
502
|
+
<SelectOption key={site.site_id} value={site.site_id}>
|
|
503
|
+
{site.site_name}
|
|
504
|
+
</SelectOption>
|
|
505
|
+
))}
|
|
506
|
+
</Select>
|
|
507
|
+
</div>
|
|
508
|
+
)}
|
|
509
|
+
|
|
510
|
+
{/* Date of Birth Range */}
|
|
511
|
+
<div>
|
|
512
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
513
|
+
Date of Birth From
|
|
514
|
+
</label>
|
|
515
|
+
<Input
|
|
516
|
+
type="date"
|
|
517
|
+
value={advancedFilters.dobFrom}
|
|
518
|
+
onChange={(e) =>
|
|
519
|
+
setAdvancedFilters((prev) => ({
|
|
520
|
+
...prev,
|
|
521
|
+
dobFrom: e.target.value,
|
|
522
|
+
}))
|
|
523
|
+
}
|
|
524
|
+
/>
|
|
525
|
+
</div>
|
|
526
|
+
|
|
527
|
+
<div>
|
|
528
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
529
|
+
Date of Birth To
|
|
530
|
+
</label>
|
|
531
|
+
<Input
|
|
532
|
+
type="date"
|
|
533
|
+
value={advancedFilters.dobTo}
|
|
534
|
+
onChange={(e) =>
|
|
535
|
+
setAdvancedFilters((prev) => ({
|
|
536
|
+
...prev,
|
|
537
|
+
dobTo: e.target.value,
|
|
538
|
+
}))
|
|
539
|
+
}
|
|
540
|
+
/>
|
|
541
|
+
</div>
|
|
542
|
+
|
|
543
|
+
{/* Age Range */}
|
|
544
|
+
<div>
|
|
545
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
546
|
+
Age From (months)
|
|
547
|
+
</label>
|
|
548
|
+
<Input
|
|
549
|
+
type="number"
|
|
550
|
+
min="0"
|
|
551
|
+
value={advancedFilters.ageFrom}
|
|
552
|
+
onChange={(e) =>
|
|
553
|
+
setAdvancedFilters((prev) => ({
|
|
554
|
+
...prev,
|
|
555
|
+
ageFrom: e.target.value,
|
|
556
|
+
}))
|
|
557
|
+
}
|
|
558
|
+
placeholder="0"
|
|
559
|
+
/>
|
|
560
|
+
</div>
|
|
561
|
+
|
|
562
|
+
<div>
|
|
563
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
564
|
+
Age To (months)
|
|
565
|
+
</label>
|
|
566
|
+
<Input
|
|
567
|
+
type="number"
|
|
568
|
+
min="0"
|
|
569
|
+
value={advancedFilters.ageTo}
|
|
570
|
+
onChange={(e) =>
|
|
571
|
+
setAdvancedFilters((prev) => ({
|
|
572
|
+
...prev,
|
|
573
|
+
ageTo: e.target.value,
|
|
574
|
+
}))
|
|
575
|
+
}
|
|
576
|
+
placeholder="60"
|
|
577
|
+
/>
|
|
578
|
+
</div>
|
|
579
|
+
|
|
580
|
+
{/* Sort Options */}
|
|
581
|
+
<div>
|
|
582
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
583
|
+
Sort By
|
|
584
|
+
</label>
|
|
585
|
+
<Select
|
|
586
|
+
value={advancedFilters.sortBy}
|
|
587
|
+
onChange={(e) =>
|
|
588
|
+
setAdvancedFilters((prev) => ({
|
|
589
|
+
...prev,
|
|
590
|
+
sortBy: e.target.value,
|
|
591
|
+
}))
|
|
592
|
+
}
|
|
593
|
+
>
|
|
594
|
+
<SelectOption value="last_name">Last Name</SelectOption>
|
|
595
|
+
<SelectOption value="first_name">First Name</SelectOption>
|
|
596
|
+
<SelectOption value="full_name">Full Name</SelectOption>
|
|
597
|
+
<SelectOption value="date_of_birth">
|
|
598
|
+
Date of Birth
|
|
599
|
+
</SelectOption>
|
|
600
|
+
<SelectOption value="site_name">Site Name</SelectOption>
|
|
601
|
+
</Select>
|
|
602
|
+
</div>
|
|
603
|
+
|
|
604
|
+
<div>
|
|
605
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
606
|
+
Sort Order
|
|
607
|
+
</label>
|
|
608
|
+
<Select
|
|
609
|
+
value={advancedFilters.sortOrder}
|
|
610
|
+
onChange={(e) =>
|
|
611
|
+
setAdvancedFilters((prev) => ({
|
|
612
|
+
...prev,
|
|
613
|
+
sortOrder: e.target.value,
|
|
614
|
+
}))
|
|
615
|
+
}
|
|
616
|
+
>
|
|
617
|
+
<SelectOption value="asc">Ascending</SelectOption>
|
|
618
|
+
<SelectOption value="desc">Descending</SelectOption>
|
|
619
|
+
</Select>
|
|
620
|
+
</div>
|
|
621
|
+
</div>
|
|
622
|
+
</div>
|
|
623
|
+
)}
|
|
624
|
+
</div>
|
|
625
|
+
</div>
|
|
626
|
+
|
|
627
|
+
{/* Results */}
|
|
628
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
|
629
|
+
{loading && (
|
|
630
|
+
<div className="flex items-center justify-center p-8">
|
|
631
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
|
|
632
|
+
</div>
|
|
633
|
+
)}
|
|
634
|
+
|
|
635
|
+
{error && (
|
|
636
|
+
<div className="p-6 text-center">
|
|
637
|
+
<div className="text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20 p-4 rounded-lg">
|
|
638
|
+
{error}
|
|
639
|
+
</div>
|
|
640
|
+
</div>
|
|
641
|
+
)}
|
|
642
|
+
|
|
643
|
+
{!loading && !error && (
|
|
644
|
+
<>
|
|
645
|
+
{/* Results Header */}
|
|
646
|
+
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
|
647
|
+
<div className="flex items-center justify-between">
|
|
648
|
+
<div className="text-sm text-gray-600 dark:text-gray-400">
|
|
649
|
+
{children.length === 0
|
|
650
|
+
? "No children found"
|
|
651
|
+
: `Showing ${
|
|
652
|
+
(pagination.page - 1) * pagination.pageSize + 1
|
|
653
|
+
} to ${Math.min(
|
|
654
|
+
pagination.page * pagination.pageSize,
|
|
655
|
+
pagination.totalCount
|
|
656
|
+
)} of ${pagination.totalCount} children`}
|
|
657
|
+
</div>
|
|
658
|
+
</div>
|
|
659
|
+
</div>
|
|
660
|
+
|
|
661
|
+
{/* Table */}
|
|
662
|
+
{children.length > 0 ? (
|
|
663
|
+
<div className="overflow-x-auto">
|
|
664
|
+
<Table>
|
|
665
|
+
<TableHeader>
|
|
666
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
667
|
+
<TableRow key={headerGroup.id}>
|
|
668
|
+
{headerGroup.headers.map((header) => (
|
|
669
|
+
<TableHead key={header.id}>
|
|
670
|
+
{header.isPlaceholder
|
|
671
|
+
? null
|
|
672
|
+
: flexRender(
|
|
673
|
+
header.column.columnDef.header,
|
|
674
|
+
header.getContext()
|
|
675
|
+
)}
|
|
676
|
+
</TableHead>
|
|
677
|
+
))}
|
|
678
|
+
</TableRow>
|
|
679
|
+
))}
|
|
680
|
+
</TableHeader>
|
|
681
|
+
<TableBody>
|
|
682
|
+
{table.getRowModel().rows?.length ? (
|
|
683
|
+
table.getRowModel().rows.map((row) => (
|
|
684
|
+
<TableRow
|
|
685
|
+
key={row.id}
|
|
686
|
+
data-state={row.getIsSelected() && "selected"}
|
|
687
|
+
onClick={() => handleChildSelect(row.original)}
|
|
688
|
+
className="cursor-pointer"
|
|
689
|
+
>
|
|
690
|
+
{row.getVisibleCells().map((cell) => (
|
|
691
|
+
<TableCell key={cell.id}>
|
|
692
|
+
{flexRender(
|
|
693
|
+
cell.column.columnDef.cell,
|
|
694
|
+
cell.getContext()
|
|
695
|
+
)}
|
|
696
|
+
</TableCell>
|
|
697
|
+
))}
|
|
698
|
+
</TableRow>
|
|
699
|
+
))
|
|
700
|
+
) : (
|
|
701
|
+
<TableRow>
|
|
702
|
+
<TableCell
|
|
703
|
+
colSpan={columns.length}
|
|
704
|
+
className="h-24 text-center"
|
|
705
|
+
>
|
|
706
|
+
{debouncedSearchTerm
|
|
707
|
+
? "No children found matching your search."
|
|
708
|
+
: "Start typing to search for children."}
|
|
709
|
+
</TableCell>
|
|
710
|
+
</TableRow>
|
|
711
|
+
)}
|
|
712
|
+
</TableBody>
|
|
713
|
+
</Table>
|
|
714
|
+
</div>
|
|
715
|
+
) : (
|
|
716
|
+
<div className="p-6 text-center text-gray-500 dark:text-gray-400">
|
|
717
|
+
{debouncedSearchTerm
|
|
718
|
+
? "No children found matching your search."
|
|
719
|
+
: "Start typing to search for children."}
|
|
720
|
+
</div>
|
|
721
|
+
)}
|
|
722
|
+
|
|
723
|
+
{/* Pagination */}
|
|
724
|
+
{pagination.totalPages > 1 && (
|
|
725
|
+
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700">
|
|
726
|
+
<Pagination>
|
|
727
|
+
<PaginationContent>
|
|
728
|
+
<PaginationItem>
|
|
729
|
+
<PaginationPrevious
|
|
730
|
+
onClick={() => handlePageChange(pagination.page - 1)}
|
|
731
|
+
className={cn(
|
|
732
|
+
!pagination.hasPreviousPage &&
|
|
733
|
+
"pointer-events-none opacity-50"
|
|
734
|
+
)}
|
|
735
|
+
/>
|
|
736
|
+
</PaginationItem>
|
|
737
|
+
|
|
738
|
+
{/* Page numbers */}
|
|
739
|
+
{Array.from(
|
|
740
|
+
{ length: pagination.totalPages },
|
|
741
|
+
(_, i) => i + 1
|
|
742
|
+
)
|
|
743
|
+
.filter((page) => {
|
|
744
|
+
const current = pagination.page;
|
|
745
|
+
const total = pagination.totalPages;
|
|
746
|
+
return (
|
|
747
|
+
page === 1 ||
|
|
748
|
+
page === total ||
|
|
749
|
+
(page >= current - 1 && page <= current + 1)
|
|
750
|
+
);
|
|
751
|
+
})
|
|
752
|
+
.map((page, index, array) => {
|
|
753
|
+
if (index > 0 && array[index - 1] !== page - 1) {
|
|
754
|
+
return (
|
|
755
|
+
<React.Fragment key={`ellipsis-${page}`}>
|
|
756
|
+
<PaginationItem>
|
|
757
|
+
<PaginationEllipsis />
|
|
758
|
+
</PaginationItem>
|
|
759
|
+
<PaginationItem>
|
|
760
|
+
<PaginationLink
|
|
761
|
+
onClick={() => handlePageChange(page)}
|
|
762
|
+
isActive={page === pagination.page}
|
|
763
|
+
>
|
|
764
|
+
{page}
|
|
765
|
+
</PaginationLink>
|
|
766
|
+
</PaginationItem>
|
|
767
|
+
</React.Fragment>
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
return (
|
|
771
|
+
<PaginationItem key={page}>
|
|
772
|
+
<PaginationLink
|
|
773
|
+
onClick={() => handlePageChange(page)}
|
|
774
|
+
isActive={page === pagination.page}
|
|
775
|
+
>
|
|
776
|
+
{page}
|
|
777
|
+
</PaginationLink>
|
|
778
|
+
</PaginationItem>
|
|
779
|
+
);
|
|
780
|
+
})}
|
|
781
|
+
|
|
782
|
+
<PaginationItem>
|
|
783
|
+
<PaginationNext
|
|
784
|
+
onClick={() => handlePageChange(pagination.page + 1)}
|
|
785
|
+
className={cn(
|
|
786
|
+
!pagination.hasNextPage &&
|
|
787
|
+
"pointer-events-none opacity-50"
|
|
788
|
+
)}
|
|
789
|
+
/>
|
|
790
|
+
</PaginationItem>
|
|
791
|
+
</PaginationContent>
|
|
792
|
+
</Pagination>
|
|
793
|
+
</div>
|
|
794
|
+
)}
|
|
795
|
+
</>
|
|
796
|
+
)}
|
|
797
|
+
</div>
|
|
798
|
+
</div>
|
|
799
|
+
</div>
|
|
800
|
+
);
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
export default ChildSearchPage;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Demo component for ChildSearchPage
|
|
2
|
+
// Shows how to use the new full page child search component
|
|
3
|
+
|
|
4
|
+
import React, { useState } from "react";
|
|
5
|
+
import { ChildSearchPage } from "./index";
|
|
6
|
+
|
|
7
|
+
const ChildSearchPageDemo = () => {
|
|
8
|
+
const [selectedChild, setSelectedChild] = useState(null);
|
|
9
|
+
const [selectedChildren, setSelectedChildren] = useState([]);
|
|
10
|
+
|
|
11
|
+
// Mock sites data for demo
|
|
12
|
+
const mockSites = [
|
|
13
|
+
{ site_id: 1, site_name: "Sunshine Nursery" },
|
|
14
|
+
{ site_id: 2, site_name: "Rainbow Daycare" },
|
|
15
|
+
{ site_id: 3, site_name: "Little Stars Preschool" },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const handleSingleSelect = (child) => {
|
|
19
|
+
setSelectedChild(child);
|
|
20
|
+
console.log("Selected child:", child);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const handleMultiSelect = (children) => {
|
|
24
|
+
setSelectedChildren(children);
|
|
25
|
+
console.log("Selected children:", children);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
|
30
|
+
{/* Demo Controls */}
|
|
31
|
+
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-4">
|
|
32
|
+
<div className="max-w-7xl mx-auto">
|
|
33
|
+
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
|
34
|
+
ChildSearchPage Demo
|
|
35
|
+
</h2>
|
|
36
|
+
|
|
37
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
38
|
+
<div>
|
|
39
|
+
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
40
|
+
Single Select Mode
|
|
41
|
+
</h3>
|
|
42
|
+
{selectedChild && (
|
|
43
|
+
<div className="text-sm text-gray-600 dark:text-gray-400">
|
|
44
|
+
Selected: {selectedChild.full_name} (ID:{" "}
|
|
45
|
+
{selectedChild.child_id})
|
|
46
|
+
</div>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div>
|
|
51
|
+
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
52
|
+
Multi Select Mode
|
|
53
|
+
</h3>
|
|
54
|
+
{selectedChildren.length > 0 && (
|
|
55
|
+
<div className="text-sm text-gray-600 dark:text-gray-400">
|
|
56
|
+
Selected: {selectedChildren.length} children
|
|
57
|
+
</div>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
{/* ChildSearchPage Component */}
|
|
65
|
+
<ChildSearchPage
|
|
66
|
+
title="Search Children - Demo"
|
|
67
|
+
sites={mockSites}
|
|
68
|
+
activeOnly={true}
|
|
69
|
+
applicationContext="demo"
|
|
70
|
+
onSelect={handleSingleSelect}
|
|
71
|
+
multiSelect={false}
|
|
72
|
+
showAdvancedFilters={true}
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export default ChildSearchPageDemo;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// shadcn-style Button component with variants
|
|
2
|
+
// Provides consistent button styling across the application
|
|
3
|
+
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { cva } from "class-variance-authority";
|
|
6
|
+
import { cn } from "../../lib/utils";
|
|
7
|
+
|
|
8
|
+
const buttonVariants = cva(
|
|
9
|
+
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
10
|
+
{
|
|
11
|
+
variants: {
|
|
12
|
+
variant: {
|
|
13
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
14
|
+
destructive:
|
|
15
|
+
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
16
|
+
outline:
|
|
17
|
+
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
18
|
+
secondary:
|
|
19
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
20
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
21
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
22
|
+
},
|
|
23
|
+
size: {
|
|
24
|
+
default: "h-10 px-4 py-2",
|
|
25
|
+
sm: "h-9 rounded-md px-3",
|
|
26
|
+
lg: "h-11 rounded-md px-8",
|
|
27
|
+
icon: "h-10 w-10",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
defaultVariants: {
|
|
31
|
+
variant: "default",
|
|
32
|
+
size: "default",
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const Button = React.forwardRef(
|
|
38
|
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
39
|
+
const Comp = asChild ? "span" : "button";
|
|
40
|
+
return (
|
|
41
|
+
<Comp
|
|
42
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
43
|
+
ref={ref}
|
|
44
|
+
{...props}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
Button.displayName = "Button";
|
|
50
|
+
|
|
51
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// shadcn-style Input component
|
|
2
|
+
// Provides consistent input styling across the application
|
|
3
|
+
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
|
|
7
|
+
const Input = React.forwardRef(({ className, type, ...props }, ref) => {
|
|
8
|
+
return (
|
|
9
|
+
<input
|
|
10
|
+
type={type}
|
|
11
|
+
className={cn(
|
|
12
|
+
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
13
|
+
className
|
|
14
|
+
)}
|
|
15
|
+
ref={ref}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
Input.displayName = "Input";
|
|
21
|
+
|
|
22
|
+
export { Input };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// shadcn-style Pagination component
|
|
2
|
+
// Provides consistent pagination styling for tables
|
|
3
|
+
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
import { Button } from "./button";
|
|
7
|
+
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
|
|
8
|
+
|
|
9
|
+
const Pagination = ({ className, ...props }) => (
|
|
10
|
+
<nav
|
|
11
|
+
role="navigation"
|
|
12
|
+
aria-label="pagination"
|
|
13
|
+
className={cn("mx-auto flex w-full justify-center", className)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
Pagination.displayName = "Pagination";
|
|
18
|
+
|
|
19
|
+
const PaginationContent = React.forwardRef(({ className, ...props }, ref) => (
|
|
20
|
+
<ul
|
|
21
|
+
ref={ref}
|
|
22
|
+
className={cn("flex flex-row items-center gap-1", className)}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
));
|
|
26
|
+
PaginationContent.displayName = "PaginationContent";
|
|
27
|
+
|
|
28
|
+
const PaginationItem = React.forwardRef(({ className, ...props }, ref) => (
|
|
29
|
+
<li ref={ref} className={cn("", className)} {...props} />
|
|
30
|
+
));
|
|
31
|
+
PaginationItem.displayName = "PaginationItem";
|
|
32
|
+
|
|
33
|
+
const PaginationLink = React.forwardRef(
|
|
34
|
+
({ className, isActive, size = "icon", ...props }, ref) => (
|
|
35
|
+
<Button
|
|
36
|
+
aria-current={isActive ? "page" : undefined}
|
|
37
|
+
variant={isActive ? "outline" : "ghost"}
|
|
38
|
+
size={size}
|
|
39
|
+
ref={ref}
|
|
40
|
+
className={cn("", className)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
)
|
|
44
|
+
);
|
|
45
|
+
PaginationLink.displayName = "PaginationLink";
|
|
46
|
+
|
|
47
|
+
const PaginationPrevious = React.forwardRef(({ className, ...props }, ref) => (
|
|
48
|
+
<PaginationLink
|
|
49
|
+
aria-label="Go to previous page"
|
|
50
|
+
size="default"
|
|
51
|
+
ref={ref}
|
|
52
|
+
className={cn("gap-1 pl-2.5", className)}
|
|
53
|
+
{...props}
|
|
54
|
+
>
|
|
55
|
+
<ChevronLeft className="h-4 w-4" />
|
|
56
|
+
<span>Previous</span>
|
|
57
|
+
</PaginationLink>
|
|
58
|
+
));
|
|
59
|
+
PaginationPrevious.displayName = "PaginationPrevious";
|
|
60
|
+
|
|
61
|
+
const PaginationNext = React.forwardRef(({ className, ...props }, ref) => (
|
|
62
|
+
<PaginationLink
|
|
63
|
+
aria-label="Go to next page"
|
|
64
|
+
size="default"
|
|
65
|
+
ref={ref}
|
|
66
|
+
className={cn("gap-1 pr-2.5", className)}
|
|
67
|
+
{...props}
|
|
68
|
+
>
|
|
69
|
+
<span>Next</span>
|
|
70
|
+
<ChevronRight className="h-4 w-4" />
|
|
71
|
+
</PaginationLink>
|
|
72
|
+
));
|
|
73
|
+
PaginationNext.displayName = "PaginationNext";
|
|
74
|
+
|
|
75
|
+
const PaginationEllipsis = ({ className, ...props }) => (
|
|
76
|
+
<span
|
|
77
|
+
aria-hidden
|
|
78
|
+
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
|
79
|
+
{...props}
|
|
80
|
+
>
|
|
81
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
82
|
+
<span className="sr-only">More pages</span>
|
|
83
|
+
</span>
|
|
84
|
+
);
|
|
85
|
+
PaginationEllipsis.displayName = "PaginationEllipsis";
|
|
86
|
+
|
|
87
|
+
export {
|
|
88
|
+
Pagination,
|
|
89
|
+
PaginationContent,
|
|
90
|
+
PaginationEllipsis,
|
|
91
|
+
PaginationItem,
|
|
92
|
+
PaginationLink,
|
|
93
|
+
PaginationNext,
|
|
94
|
+
PaginationPrevious,
|
|
95
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// shadcn-style Select component
|
|
2
|
+
// Provides consistent select styling across the application
|
|
3
|
+
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
|
|
7
|
+
const Select = React.forwardRef(({ className, children, ...props }, ref) => {
|
|
8
|
+
return (
|
|
9
|
+
<select
|
|
10
|
+
className={cn(
|
|
11
|
+
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
12
|
+
className
|
|
13
|
+
)}
|
|
14
|
+
ref={ref}
|
|
15
|
+
{...props}
|
|
16
|
+
>
|
|
17
|
+
{children}
|
|
18
|
+
</select>
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
Select.displayName = "Select";
|
|
22
|
+
|
|
23
|
+
const SelectOption = React.forwardRef(
|
|
24
|
+
({ className, children, ...props }, ref) => {
|
|
25
|
+
return (
|
|
26
|
+
<option className={cn("", className)} ref={ref} {...props}>
|
|
27
|
+
{children}
|
|
28
|
+
</option>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
SelectOption.displayName = "SelectOption";
|
|
33
|
+
|
|
34
|
+
export { Select, SelectOption };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// shadcn-style Table components
|
|
2
|
+
// Provides consistent table styling for TanStack table integration
|
|
3
|
+
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
|
|
7
|
+
const Table = React.forwardRef(({ className, ...props }, ref) => (
|
|
8
|
+
<div className="relative w-full overflow-auto">
|
|
9
|
+
<table
|
|
10
|
+
ref={ref}
|
|
11
|
+
className={cn("w-full caption-bottom text-sm", className)}
|
|
12
|
+
{...props}
|
|
13
|
+
/>
|
|
14
|
+
</div>
|
|
15
|
+
));
|
|
16
|
+
Table.displayName = "Table";
|
|
17
|
+
|
|
18
|
+
const TableHeader = React.forwardRef(({ className, ...props }, ref) => (
|
|
19
|
+
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
|
20
|
+
));
|
|
21
|
+
TableHeader.displayName = "TableHeader";
|
|
22
|
+
|
|
23
|
+
const TableBody = React.forwardRef(({ className, ...props }, ref) => (
|
|
24
|
+
<tbody
|
|
25
|
+
ref={ref}
|
|
26
|
+
className={cn("[&_tr:last-child]:border-0", className)}
|
|
27
|
+
{...props}
|
|
28
|
+
/>
|
|
29
|
+
));
|
|
30
|
+
TableBody.displayName = "TableBody";
|
|
31
|
+
|
|
32
|
+
const TableFooter = React.forwardRef(({ className, ...props }, ref) => (
|
|
33
|
+
<tfoot
|
|
34
|
+
ref={ref}
|
|
35
|
+
className={cn(
|
|
36
|
+
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
|
37
|
+
className
|
|
38
|
+
)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
));
|
|
42
|
+
TableFooter.displayName = "TableFooter";
|
|
43
|
+
|
|
44
|
+
const TableRow = React.forwardRef(({ className, ...props }, ref) => (
|
|
45
|
+
<tr
|
|
46
|
+
ref={ref}
|
|
47
|
+
className={cn(
|
|
48
|
+
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
|
49
|
+
className
|
|
50
|
+
)}
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
53
|
+
));
|
|
54
|
+
TableRow.displayName = "TableRow";
|
|
55
|
+
|
|
56
|
+
const TableHead = React.forwardRef(({ className, ...props }, ref) => (
|
|
57
|
+
<th
|
|
58
|
+
ref={ref}
|
|
59
|
+
className={cn(
|
|
60
|
+
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
|
61
|
+
className
|
|
62
|
+
)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
));
|
|
66
|
+
TableHead.displayName = "TableHead";
|
|
67
|
+
|
|
68
|
+
const TableCell = React.forwardRef(({ className, ...props }, ref) => (
|
|
69
|
+
<td
|
|
70
|
+
ref={ref}
|
|
71
|
+
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
|
72
|
+
{...props}
|
|
73
|
+
/>
|
|
74
|
+
));
|
|
75
|
+
TableCell.displayName = "TableCell";
|
|
76
|
+
|
|
77
|
+
const TableCaption = React.forwardRef(({ className, ...props }, ref) => (
|
|
78
|
+
<caption
|
|
79
|
+
ref={ref}
|
|
80
|
+
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
|
81
|
+
{...props}
|
|
82
|
+
/>
|
|
83
|
+
));
|
|
84
|
+
TableCaption.displayName = "TableCaption";
|
|
85
|
+
|
|
86
|
+
export {
|
|
87
|
+
Table,
|
|
88
|
+
TableHeader,
|
|
89
|
+
TableBody,
|
|
90
|
+
TableFooter,
|
|
91
|
+
TableHead,
|
|
92
|
+
TableRow,
|
|
93
|
+
TableCell,
|
|
94
|
+
TableCaption,
|
|
95
|
+
};
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { default as AuthButtons } from "./AuthButtons";
|
|
2
2
|
export { default as ThemeToggle } from "./ThemeToggle";
|
|
3
3
|
export { default as ChildSearchModal } from "./ChildSearchModal";
|
|
4
|
+
export { default as ChildSearchPage } from "./ChildSearchPage";
|
|
5
|
+
export { default as ChildSearchPageDemo } from "./ChildSearchPageDemo";
|
|
4
6
|
export { default as ThemeToggleTest } from "./ThemeToggleTest";
|
|
5
7
|
export { default as LandingPage } from "./LandingPage";
|
|
6
8
|
export { configureTelemetry } from "./telemetry";
|
package/src/lib/utils.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Utility functions for shadcn styling
|
|
2
|
+
// This file provides the cn function for combining class names with proper Tailwind CSS merging
|
|
3
|
+
|
|
4
|
+
import { clsx } from "clsx";
|
|
5
|
+
import { twMerge } from "tailwind-merge";
|
|
6
|
+
|
|
7
|
+
export function cn(...inputs) {
|
|
8
|
+
return twMerge(clsx(inputs));
|
|
9
|
+
}
|