/
customers.tsx
109 lines (101 loc) · 3.33 KB
/
customers.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import type { LoaderArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useFetcher } from "@remix-run/react";
import clsx from "clsx";
import { useCombobox } from "downshift";
import { useId, useState } from "react";
import invariant from "tiny-invariant";
import { LabelText } from "~/components";
import { searchCustomers } from "~/models/customer.server";
import { requireUser } from "~/session.server";
type CustomerSearchResult = {
customers: Awaited<ReturnType<typeof searchCustomers>>;
};
export async function loader({ request }: LoaderArgs) {
await requireUser(request);
const url = new URL(request.url);
const query = url.searchParams.get("query");
invariant(typeof query === "string", "query is required");
return json<CustomerSearchResult>({
customers: await searchCustomers(query),
});
}
type Customer = CustomerSearchResult["customers"][number];
export function CustomerCombobox({ error }: { error?: string | null }) {
const customerFetcher = useFetcher();
const id = useId();
const customers =
(customerFetcher.data as CustomerSearchResult | null)?.customers ?? [];
const [selectedCustomer, setSelectedCustomer] = useState<
Customer | null | undefined
>(null);
const cb = useCombobox<Customer>({
id,
onSelectedItemChange: ({ selectedItem }) => {
setSelectedCustomer(selectedItem);
},
items: customers,
itemToString: (item) => (item ? item.name : ""),
onInputValueChange: (changes) => {
if (!changes.inputValue) return;
customerFetcher.submit(
{ query: changes.inputValue },
{ method: "get", action: "/resources/customers" },
);
},
});
const displayMenu = cb.isOpen && customers.length > 0;
return (
<div className="relative">
<input
name="customerId"
type="hidden"
value={selectedCustomer?.id ?? ""}
/>
<div className="flex flex-wrap items-center gap-1">
<label {...cb.getLabelProps()}>
<LabelText>Customer</LabelText>
</label>
{error ? (
<em id="customer-error" className="text-d-p-xs text-red-600">
{error}
</em>
) : null}
</div>
<div {...cb.getComboboxProps()}>
<input
{...cb.getInputProps({
className: clsx("text-lg w-full border border-gray-500 px-2 py-1", {
"rounded-t rounded-b-0": displayMenu,
rounded: !displayMenu,
}),
"aria-invalid": Boolean(error) || undefined,
"aria-errormessage": error ? "customer-error" : undefined,
})}
/>
</div>
<ul
{...cb.getMenuProps({
className: clsx(
"absolute z-10 bg-white shadow-lg rounded-b w-full border border-t-0 border-gray-500 max-h-[180px] overflow-scroll",
{ hidden: !displayMenu },
),
})}
>
{cb.isOpen
? customers.map((customer, index) => (
<li
className={clsx("cursor-pointer py-1 px-2", {
"bg-green-200": cb.highlightedIndex === index,
})}
key={customer.id}
{...cb.getItemProps({ item: customer, index })}
>
{customer.name} ({customer.email})
</li>
))
: null}
</ul>
</div>
);
}