@star-insure/sdk 3.0.2 → 3.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.
- package/dist/components/common/Button.d.ts +2 -1
- package/dist/components/filter/Action.d.ts +3 -0
- package/dist/components/filter/Back.d.ts +4 -0
- package/dist/components/filter/Dropdown.d.ts +12 -0
- package/dist/components/filter/FilterItem.d.ts +6 -0
- package/dist/components/filter/FilterOptions.d.ts +5 -0
- package/dist/components/filter/PageHeader.d.ts +14 -0
- package/dist/components/filter/SearchBar.d.ts +7 -0
- package/dist/components/filter/index.d.ts +1 -0
- package/dist/lib/page.d.ts +19 -0
- package/dist/sdk.cjs.development.js +4 -2
- package/dist/sdk.cjs.development.js.map +1 -1
- package/dist/sdk.cjs.production.min.js +1 -1
- package/dist/sdk.cjs.production.min.js.map +1 -1
- package/dist/sdk.esm.js +4 -2
- package/dist/sdk.esm.js.map +1 -1
- package/dist/types/misc/index.d.ts +27 -0
- package/package.json +4 -1
- package/src/components/common/Button.tsx +5 -2
- package/src/components/filter/Action.tsx +30 -0
- package/src/components/filter/Back.tsx +35 -0
- package/src/components/filter/Dropdown.tsx +51 -0
- package/src/components/filter/FilterItem.tsx +318 -0
- package/src/components/filter/FilterOptions.tsx +21 -0
- package/src/components/filter/PageHeader.tsx +176 -0
- package/src/components/filter/SearchBar.tsx +105 -0
- package/src/components/filter/index.ts +1 -0
- package/src/lib/page.tsx +24 -0
- package/src/types/misc/index.ts +33 -0
|
@@ -11,6 +11,7 @@ interface Props {
|
|
|
11
11
|
children: React.ReactNode;
|
|
12
12
|
disabled?: boolean;
|
|
13
13
|
as?: 'link' | 'button' | 'a';
|
|
14
|
+
small?: boolean;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export default function Button({
|
|
@@ -23,9 +24,11 @@ export default function Button({
|
|
|
23
24
|
status = 'default',
|
|
24
25
|
disabled = false,
|
|
25
26
|
as,
|
|
27
|
+
small = false,
|
|
26
28
|
}: Props) {
|
|
27
|
-
const baseClasses =
|
|
28
|
-
'font-
|
|
29
|
+
const baseClasses = small
|
|
30
|
+
? 'font-bold text-sm inline-flex items-center gap-3 justify-center text-white text-center px-3 py-1 rounded min-w-[80px] transition-all hover:opacity-75'
|
|
31
|
+
: 'font-black inline-flex items-center gap-3 justify-center text-white text-center px-5 py-2 rounded-md min-w-[120px] transition-all hover:opacity-75';
|
|
29
32
|
|
|
30
33
|
const statusClass =
|
|
31
34
|
(status === 'primary' && 'bg-teal') ||
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Link } from "@inertiajs/react";
|
|
2
|
+
import { TPageHeaderAction } from "../../types";
|
|
3
|
+
import React from "react";
|
|
4
|
+
|
|
5
|
+
export default function Action({ title, href, as = 'Link', target = '_self', type, onClick = () => {} }: TPageHeaderAction) {
|
|
6
|
+
const className =
|
|
7
|
+
'bg-white rounded-full font-bold px-3 py-1.5 text-xs whitespace-nowrap hover:bg-gray-100 hover:border-gray-300 transition-colors border border-gray-200';
|
|
8
|
+
|
|
9
|
+
if (as === 'Link' && href) {
|
|
10
|
+
return (
|
|
11
|
+
<Link className={className} href={href}>
|
|
12
|
+
{title}
|
|
13
|
+
</Link>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (as === 'a' && href) {
|
|
18
|
+
return (
|
|
19
|
+
<a className={className} target={target} href={href}>
|
|
20
|
+
{title}
|
|
21
|
+
</a>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<button className={className} type={type} onClick={() => onClick()}>
|
|
27
|
+
{title}
|
|
28
|
+
</button>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Link } from "@inertiajs/react";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { HiArrowLeft } from "react-icons/hi2";
|
|
4
|
+
import { usePage } from "../../lib/page";
|
|
5
|
+
|
|
6
|
+
export function BackButton({ back }: { back?: string | boolean }) {
|
|
7
|
+
const [backUrl, setBackUrl] = React.useState<string | undefined>(typeof back === 'string' ? back : undefined);
|
|
8
|
+
const { breadcrumbs } = usePage().props;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Set the back URL on mount
|
|
12
|
+
*/
|
|
13
|
+
React.useEffect(() => {
|
|
14
|
+
if (typeof window !== 'undefined') {
|
|
15
|
+
// Set back button URL
|
|
16
|
+
if (back && typeof back === 'boolean') {
|
|
17
|
+
// If we haven't provided a path as a prop, use the last breadcrumb that's not the current one
|
|
18
|
+
const crumb = breadcrumbs.slice(breadcrumbs.length - 2, breadcrumbs.length - 1);
|
|
19
|
+
if (crumb && crumb.length === 1) {
|
|
20
|
+
// Strip off everything but the path so the <Link> component works
|
|
21
|
+
const url = crumb[0].url;
|
|
22
|
+
if (url) {
|
|
23
|
+
setBackUrl(new URL(url).pathname);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}, [breadcrumbs]);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Link href={backUrl || '/'} className="hover:text-teal transition-all p-1 -mr-1">
|
|
32
|
+
<HiArrowLeft className="h-5 w-5 stroke-[1.25]" />
|
|
33
|
+
</Link>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import { createHost, createSlot } from 'create-slots';
|
|
4
|
+
import { useClickOutside } from "../../lib";
|
|
5
|
+
|
|
6
|
+
const DropdownTitle = createSlot('button');
|
|
7
|
+
const DropdownContent = createSlot('form');
|
|
8
|
+
|
|
9
|
+
type Props = {
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
onClose: Function;
|
|
12
|
+
active: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function Dropdown({ children, onClose, active }: Props) {
|
|
16
|
+
const ref = React.useRef<HTMLDivElement | null>(null);
|
|
17
|
+
const innerRef = React.useRef<HTMLDivElement | null>(null);
|
|
18
|
+
|
|
19
|
+
useClickOutside(ref, () => onClose(false));
|
|
20
|
+
|
|
21
|
+
React.useEffect(() => {
|
|
22
|
+
const current = ref?.current;
|
|
23
|
+
const innerCurrent = innerRef.current;
|
|
24
|
+
|
|
25
|
+
current?.classList.toggle('active', active)
|
|
26
|
+
|
|
27
|
+
if (!current || !innerCurrent) return;
|
|
28
|
+
|
|
29
|
+
const pos = current?.getBoundingClientRect();
|
|
30
|
+
innerCurrent.style.setProperty('left', `${pos?.left}px`)
|
|
31
|
+
}, [active, ref.current, innerRef.current]);
|
|
32
|
+
|
|
33
|
+
return createHost(children, (Slots) => {
|
|
34
|
+
return (
|
|
35
|
+
<div ref={ref} className="static inline-block">
|
|
36
|
+
{Slots.get(DropdownTitle)}
|
|
37
|
+
|
|
38
|
+
{active && (
|
|
39
|
+
<div ref={innerRef} className="absolute children z-10 flex flex-col">
|
|
40
|
+
{Slots.get(DropdownContent)}
|
|
41
|
+
</div>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
Dropdown.Title = DropdownTitle;
|
|
49
|
+
Dropdown.Content = DropdownContent;
|
|
50
|
+
|
|
51
|
+
export default Dropdown;
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { router } from "@inertiajs/react";
|
|
2
|
+
import { format, subYears } from "date-fns";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { FilterOption, FilterValue } from "../../types";
|
|
5
|
+
import Select from 'react-select';
|
|
6
|
+
import { HiChevronDown } from "react-icons/hi2";
|
|
7
|
+
import Dropdown from "./Dropdown";
|
|
8
|
+
import cn from 'classnames';
|
|
9
|
+
import { Button } from "../common";
|
|
10
|
+
|
|
11
|
+
export function FilterItem({ filter }: { filter: FilterOption, path?: string }) {
|
|
12
|
+
const [isOpen, setOpen] = React.useState<boolean>(false);
|
|
13
|
+
const [selected, setSelected] = React.useState<string[]>([]);
|
|
14
|
+
|
|
15
|
+
const [selectedOptions, setSelectedOptions] = React.useState<FilterValue[]>([]); // Filter type: select
|
|
16
|
+
const hasFilters = selected.length > 0;
|
|
17
|
+
|
|
18
|
+
// Populate values on load
|
|
19
|
+
React.useEffect(() => {
|
|
20
|
+
if (typeof window !== 'undefined') {
|
|
21
|
+
const search = new URLSearchParams(window.location.search);
|
|
22
|
+
|
|
23
|
+
if (filter.type === 'date') {
|
|
24
|
+
const fromDate = search.get(`${filter.name}[from]`);
|
|
25
|
+
const toDate = search.get(`${filter.name}[to]`);
|
|
26
|
+
if (fromDate && toDate) {
|
|
27
|
+
setSelected([fromDate, toDate]);
|
|
28
|
+
}
|
|
29
|
+
} else if (filter.type === 'greaterThan') {
|
|
30
|
+
const selectedFromUrl = search.getAll(`${filter.name}-GTE`);
|
|
31
|
+
if (selectedFromUrl) {
|
|
32
|
+
setSelected(selectedFromUrl);
|
|
33
|
+
}
|
|
34
|
+
} else if (filter.type === 'scope') {
|
|
35
|
+
const selectedFromUrl = search.getAll(`scope${filter.name}`);
|
|
36
|
+
if (selectedFromUrl) {
|
|
37
|
+
setSelected(selectedFromUrl);
|
|
38
|
+
}
|
|
39
|
+
} else if (filter.type === 'select') {
|
|
40
|
+
const selectedFromUrl = search.getAll(`${filter.name}[]`);
|
|
41
|
+
if (selectedFromUrl) {
|
|
42
|
+
setSelected(selectedFromUrl);
|
|
43
|
+
const selectedOptions = filter.options && filter.options.filter(item => selectedFromUrl.includes(item.value.toString()));
|
|
44
|
+
selectedOptions && setSelectedOptions(selectedOptions);
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
const selectedFromUrl = search.getAll(`${filter.name}[]`);
|
|
48
|
+
if (selectedFromUrl) {
|
|
49
|
+
setSelected(selectedFromUrl);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
function handleInput(e: React.SyntheticEvent<HTMLInputElement>) {
|
|
56
|
+
const { value, checked } = e.currentTarget;
|
|
57
|
+
|
|
58
|
+
if (checked) {
|
|
59
|
+
setSelected((curr) => [...curr, value]);
|
|
60
|
+
} else {
|
|
61
|
+
setSelected(selected.filter((f) => f !== value));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function handleDateSelect(e: React.SyntheticEvent<HTMLInputElement>) {
|
|
66
|
+
const { name, value } = e.currentTarget;
|
|
67
|
+
|
|
68
|
+
// First value in "selected" will be the "from", second will be the "to"
|
|
69
|
+
if (name.includes('from')) {
|
|
70
|
+
return setSelected([value]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (name.includes('to')) {
|
|
74
|
+
// Make sure we have a "from" value first
|
|
75
|
+
if (selected.length === 0) {
|
|
76
|
+
// We'll default to a year ago if nothing entered
|
|
77
|
+
return setSelected([format(subYears(new Date(), 1), 'yyyy-MM-dd'), value]);
|
|
78
|
+
}
|
|
79
|
+
return setSelected([selected[0], value]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return setSelected([]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Handle Select for FilterType: Select
|
|
87
|
+
*/
|
|
88
|
+
const handleSelectedOptions = (options: FilterValue[] | any) => {
|
|
89
|
+
setSelectedOptions(options);
|
|
90
|
+
const selectedValues = options.map((option : FilterValue) => option.value.toString());
|
|
91
|
+
|
|
92
|
+
if (selectedValues.length > 0) {
|
|
93
|
+
setSelected(selectedValues);
|
|
94
|
+
} else {
|
|
95
|
+
setSelected([]);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* For Filter Select
|
|
101
|
+
* Removes the Clear Button
|
|
102
|
+
*/
|
|
103
|
+
function NullComponent() {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function handleSelect(e: React.SyntheticEvent<HTMLSelectElement>) {
|
|
108
|
+
const { value } = e.currentTarget;
|
|
109
|
+
|
|
110
|
+
if (value) {
|
|
111
|
+
setSelected([value]);
|
|
112
|
+
} else {
|
|
113
|
+
setSelected([]);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function handleApply(e: React.FormEvent) {
|
|
118
|
+
e.preventDefault();
|
|
119
|
+
|
|
120
|
+
const search = new URLSearchParams(window.location.search);
|
|
121
|
+
|
|
122
|
+
// Reset the page in the query
|
|
123
|
+
search.set('page', '1');
|
|
124
|
+
|
|
125
|
+
if (filter.type === 'date') {
|
|
126
|
+
const [from, to] = selected;
|
|
127
|
+
|
|
128
|
+
search.set(`${filter.name}[from]`, from ?? format(new Date(), 'yyyy-MM-dd'));
|
|
129
|
+
search.set(`${filter.name}[to]`, to ?? format(new Date(), 'yyyy-MM-dd'));
|
|
130
|
+
} else if (filter.type === 'greaterThan') {
|
|
131
|
+
search.delete(`${filter.name}-GTE`);
|
|
132
|
+
|
|
133
|
+
if (selected.length > 0) {
|
|
134
|
+
search.set(`${filter.name}-GTE`, selected[0]);
|
|
135
|
+
}
|
|
136
|
+
} else if (filter.type === 'scope') {
|
|
137
|
+
search.delete(`${filter.name}`);
|
|
138
|
+
if (selected.length > 0) {
|
|
139
|
+
search.set(`scope${filter.name}`, selected[0]);
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
// Clear this filter first
|
|
143
|
+
search.delete(`${filter.name}[]`);
|
|
144
|
+
|
|
145
|
+
// Apply the filters to the query string
|
|
146
|
+
selected.forEach((selectedValue, i) => {
|
|
147
|
+
// Fall back to option equality filters
|
|
148
|
+
search.append(`${filter.name}[]`, selectedValue);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Fetch new data
|
|
153
|
+
router.get(`${window.location.pathname}?${search.toString()}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function handleClear() {
|
|
157
|
+
const search = new URLSearchParams(window.location.search);
|
|
158
|
+
|
|
159
|
+
// Reset the page in the query
|
|
160
|
+
search.set('page', '1');
|
|
161
|
+
|
|
162
|
+
// Clear this filter
|
|
163
|
+
if (filter.type === 'date') {
|
|
164
|
+
search.delete(`${filter.name}[from]`);
|
|
165
|
+
search.delete(`${filter.name}[to]`);
|
|
166
|
+
} else if (filter.type === 'greaterThan') {
|
|
167
|
+
search.delete(`${filter.name}-GTE`);
|
|
168
|
+
} else if (filter.type === 'scope') {
|
|
169
|
+
search.delete(`scope${filter.name}`);
|
|
170
|
+
} else {
|
|
171
|
+
search.delete(`${filter.name}[]`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Fetch new data
|
|
175
|
+
router.get(`${window.location.pathname}?${search.toString()}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function handleClick(force?: boolean) {
|
|
179
|
+
if (typeof force === 'boolean') {
|
|
180
|
+
return setOpen(force);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
setOpen(!isOpen);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<Dropdown onClose={() => setOpen(!open)} active={isOpen}>
|
|
188
|
+
<Dropdown.Title
|
|
189
|
+
onClick={() => handleClick()}
|
|
190
|
+
className={cn(
|
|
191
|
+
'flex rounded-2xl border hover:border-teal bg-gray-600 min-w-[90px] px-2 py-1 items-center justify-between shrink-0 gap-2 text-xs text-white hover:bg-teal transition-colors',
|
|
192
|
+
{
|
|
193
|
+
'!bg-teal border-teal': hasFilters,
|
|
194
|
+
}
|
|
195
|
+
)}
|
|
196
|
+
>
|
|
197
|
+
<p className="whitespace-nowrap">{filter.label}</p>
|
|
198
|
+
<HiChevronDown />
|
|
199
|
+
</Dropdown.Title>
|
|
200
|
+
|
|
201
|
+
<Dropdown.Content
|
|
202
|
+
className={`mt-2 flex max-h-[350px] min-w-[200px] flex-col gap-2 rounded-md border border-gray-300 bg-white p-4 shadow-lg ${ filter.type && filter.type === 'select' ? '' : 'overflow-y-scroll'}`}
|
|
203
|
+
onSubmit={handleApply}
|
|
204
|
+
>
|
|
205
|
+
<div className="flex flex-col items-start gap-1">
|
|
206
|
+
{(!filter.type || filter.type === 'options') &&
|
|
207
|
+
filter.options?.map((option, i) => (
|
|
208
|
+
<div className="checkbox text-sm " key={`${option.label}-${i}`}>
|
|
209
|
+
<input
|
|
210
|
+
type="checkbox"
|
|
211
|
+
name={filter.name}
|
|
212
|
+
id={option.label}
|
|
213
|
+
value={option.value.toString()}
|
|
214
|
+
checked={selected.includes(option.value.toString())}
|
|
215
|
+
onChange={handleInput}
|
|
216
|
+
/>
|
|
217
|
+
<label className="whitespace-nowrap" htmlFor={option.label}>
|
|
218
|
+
{option.label}
|
|
219
|
+
</label>
|
|
220
|
+
</div>
|
|
221
|
+
))}
|
|
222
|
+
{filter.type === 'greaterThan' && filter.options && (
|
|
223
|
+
<label className="mb-2 w-full">
|
|
224
|
+
<div className="text-sm">Greater than or equal to</div>
|
|
225
|
+
<select
|
|
226
|
+
name={filter.name}
|
|
227
|
+
id={filter.label}
|
|
228
|
+
value={selected[0]}
|
|
229
|
+
onChange={handleSelect}
|
|
230
|
+
className="w-full"
|
|
231
|
+
>
|
|
232
|
+
<option value="">Select option</option>
|
|
233
|
+
{filter.options?.map((option) => (
|
|
234
|
+
<option key={option.value} value={option.value}>
|
|
235
|
+
{option.label}
|
|
236
|
+
</option>
|
|
237
|
+
))}
|
|
238
|
+
</select>
|
|
239
|
+
</label>
|
|
240
|
+
)}
|
|
241
|
+
{filter.type === 'scope' && filter.options && (
|
|
242
|
+
<label className="mb-2 w-full">
|
|
243
|
+
<select
|
|
244
|
+
name={filter.name}
|
|
245
|
+
id={filter.label}
|
|
246
|
+
value={selected[0]}
|
|
247
|
+
onChange={handleSelect}
|
|
248
|
+
className="w-full"
|
|
249
|
+
>
|
|
250
|
+
<option value="">Select option</option>
|
|
251
|
+
{filter.options?.map((option) => (
|
|
252
|
+
<option key={option.value} value={option.value}>
|
|
253
|
+
{option.label}
|
|
254
|
+
</option>
|
|
255
|
+
))}
|
|
256
|
+
</select>
|
|
257
|
+
</label>
|
|
258
|
+
)}
|
|
259
|
+
{filter.type === 'date' && (
|
|
260
|
+
<div className="mb-2 flex flex-col gap-4">
|
|
261
|
+
<label className="text-xs">
|
|
262
|
+
From
|
|
263
|
+
<input
|
|
264
|
+
type="date"
|
|
265
|
+
name={`${filter.name}[from]`}
|
|
266
|
+
id={`${filter.name}[from]`}
|
|
267
|
+
onChange={handleDateSelect}
|
|
268
|
+
value={selected[0] ?? ''}
|
|
269
|
+
/>
|
|
270
|
+
</label>
|
|
271
|
+
<label className="text-xs">
|
|
272
|
+
To
|
|
273
|
+
<input
|
|
274
|
+
type="date"
|
|
275
|
+
name={`${filter.name}[to]`}
|
|
276
|
+
id={`${filter.name}[to]`}
|
|
277
|
+
onChange={handleDateSelect}
|
|
278
|
+
value={selected[1] ?? ''}
|
|
279
|
+
/>
|
|
280
|
+
</label>
|
|
281
|
+
</div>
|
|
282
|
+
)}
|
|
283
|
+
{filter.type === 'select' && filter.options && (
|
|
284
|
+
<div className="w-full">
|
|
285
|
+
<Select
|
|
286
|
+
isMulti
|
|
287
|
+
options={filter.options}
|
|
288
|
+
className="basic-multi-select text-sm w-64 !transition-none"
|
|
289
|
+
classNamePrefix="select"
|
|
290
|
+
onChange={handleSelectedOptions}
|
|
291
|
+
components={{
|
|
292
|
+
ClearIndicator: NullComponent, // Hide the ClearIndicator (X button) -> exits filter when clicked
|
|
293
|
+
}}
|
|
294
|
+
value={selectedOptions}
|
|
295
|
+
theme={(theme) => ({
|
|
296
|
+
...theme,
|
|
297
|
+
colors: {
|
|
298
|
+
...theme.colors,
|
|
299
|
+
primary25: 'rgb(111, 199, 182)',
|
|
300
|
+
primary: 'rgb(111, 199, 182)',
|
|
301
|
+
},
|
|
302
|
+
})}
|
|
303
|
+
/>
|
|
304
|
+
</div>
|
|
305
|
+
)}
|
|
306
|
+
</div>
|
|
307
|
+
<div className="flex items-center gap-2">
|
|
308
|
+
<Button type="button" className="!min-w-[0px] flex-grow !px-2 text-sm !transition-none" onClick={handleClear}>
|
|
309
|
+
Clear
|
|
310
|
+
</Button>
|
|
311
|
+
<Button type="submit" status="primary" className="!min-w-[0px] flex-grow !px-2 text-sm !transition-none">
|
|
312
|
+
Apply
|
|
313
|
+
</Button>
|
|
314
|
+
</div>
|
|
315
|
+
</Dropdown.Content>
|
|
316
|
+
</Dropdown>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { FilterOption } from "../../types";
|
|
3
|
+
import Button from "../common/Button"
|
|
4
|
+
import { FilterItem } from "./FilterItem";
|
|
5
|
+
import { router } from "@inertiajs/react";
|
|
6
|
+
|
|
7
|
+
export function FilterOptions({ filterOptions }: { filterOptions: FilterOption[] }) {
|
|
8
|
+
function handleClear() {
|
|
9
|
+
router.get(`${window.location.pathname}`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className="flex items-center">
|
|
14
|
+
{filterOptions.map(filter => (
|
|
15
|
+
<FilterItem filter={filter} key={filter.name} />
|
|
16
|
+
))}
|
|
17
|
+
<Button onClick={handleClear}>Clear filters</Button>
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import cn from 'classnames';
|
|
3
|
+
import { FilterOption, TPageHeaderAction } from '../../types';
|
|
4
|
+
import { BackButton } from './Back';
|
|
5
|
+
import SearchBar from './SearchBar';
|
|
6
|
+
import Action from './Action';
|
|
7
|
+
import { FilterItem } from './FilterItem';
|
|
8
|
+
import { HiChevronLeft, HiChevronRight, HiXMark } from 'react-icons/hi2';
|
|
9
|
+
import { router } from '@inertiajs/core';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
title: string;
|
|
13
|
+
search?: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
innerClassName?: string;
|
|
16
|
+
back?: boolean | string;
|
|
17
|
+
actions?: TPageHeaderAction[];
|
|
18
|
+
backText?: string;
|
|
19
|
+
filterOptions?: FilterOption[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default function PageHeader({
|
|
23
|
+
title,
|
|
24
|
+
search,
|
|
25
|
+
className = '',
|
|
26
|
+
innerClassName = '',
|
|
27
|
+
back = true,
|
|
28
|
+
actions = [],
|
|
29
|
+
filterOptions = [],
|
|
30
|
+
}: Props) {
|
|
31
|
+
const [isSearchActive, setSearchActive] = React.useState<boolean>(false);
|
|
32
|
+
const scrollContainerRef = React.useRef<HTMLDivElement | null>(null);
|
|
33
|
+
|
|
34
|
+
const [overScroll, setOverScroll] = React.useState<boolean>(false);
|
|
35
|
+
|
|
36
|
+
React.useEffect(() => {
|
|
37
|
+
const current = scrollContainerRef.current;
|
|
38
|
+
if (!current) return;
|
|
39
|
+
|
|
40
|
+
const listener = function() {
|
|
41
|
+
const activeDropdown = current.querySelector('.active');
|
|
42
|
+
if (!activeDropdown) return;
|
|
43
|
+
|
|
44
|
+
const childrenContainer = activeDropdown.querySelector('.children');
|
|
45
|
+
if (!childrenContainer) return;
|
|
46
|
+
|
|
47
|
+
const pos = activeDropdown.getBoundingClientRect();
|
|
48
|
+
|
|
49
|
+
const posChild = childrenContainer.getBoundingClientRect();
|
|
50
|
+
const posContainer = current.getBoundingClientRect();
|
|
51
|
+
|
|
52
|
+
// if the dropdown content moves outside the scroll container to the left, make it disappear
|
|
53
|
+
if (posChild.left <= posContainer.left - 50) {
|
|
54
|
+
// @ts-ignore
|
|
55
|
+
childrenContainer.style.visibility = 'hidden';
|
|
56
|
+
} else if (posChild.left >= posContainer.right - 50) {
|
|
57
|
+
// @ts-ignore
|
|
58
|
+
childrenContainer.style.visibility = 'hidden';
|
|
59
|
+
} else {
|
|
60
|
+
// @ts-ignore
|
|
61
|
+
childrenContainer.style.visibility = 'visible';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// @ts-ignore
|
|
65
|
+
childrenContainer.style.left = `${pos.left}px`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const mouseListener = function(e: MouseEvent) {
|
|
69
|
+
const pos = current.getBoundingClientRect();
|
|
70
|
+
if (e.clientX > pos.left && e.clientX < pos.right && e.clientY > pos.top && e.clientY < pos.bottom) {
|
|
71
|
+
setOverScroll(true);
|
|
72
|
+
} else {
|
|
73
|
+
setOverScroll(false);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
current.addEventListener('scroll', listener);
|
|
78
|
+
document.addEventListener('mousemove', mouseListener)
|
|
79
|
+
|
|
80
|
+
return () => {
|
|
81
|
+
current.removeEventListener('scroll', listener);
|
|
82
|
+
document.removeEventListener('mousemove', mouseListener)
|
|
83
|
+
}
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
React.useEffect(() => {
|
|
87
|
+
const scrollListener = function(e: WheelEvent) {
|
|
88
|
+
if (overScroll) {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
scrollContainerRef.current?.scrollBy({ left: e.deltaY, behavior: 'auto' });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
document.addEventListener('wheel', scrollListener, { passive: false });
|
|
95
|
+
|
|
96
|
+
return () => {
|
|
97
|
+
document.removeEventListener('wheel', scrollListener);
|
|
98
|
+
}
|
|
99
|
+
}, [overScroll])
|
|
100
|
+
|
|
101
|
+
function handleClear() {
|
|
102
|
+
router.get(`${window.location.pathname}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const [hasScroll, setHasScroll] = React.useState<boolean>(false);
|
|
106
|
+
|
|
107
|
+
const checkScroll = React.useCallback(() => {
|
|
108
|
+
const current = scrollContainerRef.current;
|
|
109
|
+
if (!current) return;
|
|
110
|
+
|
|
111
|
+
const pos = current.getBoundingClientRect();
|
|
112
|
+
|
|
113
|
+
setHasScroll(current.scrollWidth > (pos.width + 11));
|
|
114
|
+
}, [scrollContainerRef.current]);
|
|
115
|
+
|
|
116
|
+
React.useEffect(() => {
|
|
117
|
+
checkScroll();
|
|
118
|
+
|
|
119
|
+
window.addEventListener('resize', checkScroll)
|
|
120
|
+
|
|
121
|
+
return () => {
|
|
122
|
+
window.removeEventListener('resize', checkScroll);
|
|
123
|
+
}
|
|
124
|
+
}, [checkScroll]);
|
|
125
|
+
|
|
126
|
+
function clickRight() {
|
|
127
|
+
scrollContainerRef.current?.scrollBy({ left: 100, behavior: 'auto' });
|
|
128
|
+
checkScroll();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function clickLeft() {
|
|
132
|
+
scrollContainerRef.current?.scrollBy({ left: -100, behavior: 'auto' });
|
|
133
|
+
checkScroll();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<section className={cn('col-span-full max-w-full rounded-lg bg-white p-2 shadow', className)}>
|
|
138
|
+
<div className={cn('grid grid-cols-[auto_1fr_auto] min-h-[60px] rounded gap-4 bg-gray-100 p-3', innerClassName)}>
|
|
139
|
+
<div className="mr-auto flex items-center gap-4">
|
|
140
|
+
{back && <BackButton back={back} />}
|
|
141
|
+
|
|
142
|
+
{search && <SearchBar search={search} active={isSearchActive} onActive={setSearchActive} placeholder={`Search ${title}...`} />}
|
|
143
|
+
|
|
144
|
+
{!isSearchActive && <h1 className="text-base font-bold">{title}</h1>}
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<div className="flex ml-auto items-center max-w-sm md:max-w-md lg:max-w-lg xl:max-w-2xl gap-2 h-full min-w-0">
|
|
148
|
+
{filterOptions.length > 0 && (
|
|
149
|
+
<>
|
|
150
|
+
<button onClick={handleClear} className="p-1.5 hover:text-red-500"><HiXMark className="w-5 h-5 stroke-[1.25]" /></button>
|
|
151
|
+
|
|
152
|
+
{hasScroll && <HiChevronLeft onClick={clickLeft} className="w-5 h-5 text-gray-400 stroke-[1.25]" />}
|
|
153
|
+
<div ref={scrollContainerRef} className="flex items-center h-full w-full gap-3 overflow-x-scroll hide-scroll">
|
|
154
|
+
{filterOptions.map(filter => (
|
|
155
|
+
<FilterItem key={filter.name} filter={filter} />
|
|
156
|
+
))}
|
|
157
|
+
</div>
|
|
158
|
+
{hasScroll && <HiChevronRight onClick={clickRight} className="w-5 h-5 text-gray-400 stroke-[1.25]" />}
|
|
159
|
+
</>
|
|
160
|
+
)}
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
{actions.length > 0 && (
|
|
164
|
+
<div className="flex items-center gap-3">
|
|
165
|
+
{filterOptions.length > 0 && <div className="w-[1px] h-full bg-gray-300" />}
|
|
166
|
+
<nav className="flex items-center gap-2">
|
|
167
|
+
{actions.map((action) => (
|
|
168
|
+
<Action key={`${action.title}-${action.as}-${action.href}`} {...action} />
|
|
169
|
+
))}
|
|
170
|
+
</nav>
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
</section>
|
|
175
|
+
);
|
|
176
|
+
}
|