remix-demo
react router (remix) demo
git clone https://9o.is/git/remix-demo.git
new.tsx
(3100B)
1 import type {
2 ActionArgs,
3 LinksFunction,
4 MetaFunction,
5 } from "@remix-run/server-runtime";
6 import { redirect } from "@remix-run/server-runtime";
7 import Form from "~/components/forms/Form";
8 import Input from "~/components/forms/Input";
9 import PrimaryButton from "~/components/forms/PrimaryButton";
10 import foodEntryController from "~/controllers/foodEntry.server";
11 import {
12 foodEntryUserValidator,
13 foodEntryAdminValidator,
14 } from "~/schemas/foodEntry";
15 import { formatDateTime } from "~/utils/date";
16 import { useState } from "react";
17 import { debounce } from "throttle-debounce";
18 import { useUser } from "~/utils/hooks";
19 import { Role } from "@prisma/client";
20 import comboboxStyles from "@reach/combobox/styles.css";
21 import InputCombobox from "~/components/forms/InputCombobox";
22 import Heading from "~/components/Heading";
23
24 export const meta: MetaFunction = () => ({
25 title: "Create Food Entry | Calories Counter",
26 });
27
28 export const links: LinksFunction = () => {
29 return [{ rel: "stylesheet", href: comboboxStyles }];
30 };
31
32 export async function action({ request }: ActionArgs) {
33 const response = await foodEntryController.create(request);
34 if (response.ok) return redirect("..");
35 else return response;
36 }
37
38 export default function FoodEntriesNew() {
39 const user = useUser();
40 const now = formatDateTime(new Date());
41 const searchDebounced = debounce(250, autocompleteSearch);
42 const [foodOptions, setFoodOptions] = useState([]);
43 const [calories, setCalories] = useState(0);
44
45 async function autocompleteSearch(query: string) {
46 if (!query) return;
47
48 const result = await fetch(`/api/foods?query=${encodeURIComponent(query)}`);
49 setFoodOptions(await result.json());
50 }
51
52 async function findFoodByName(name: string) {
53 const result = await fetch(`/api/foods/${name}`);
54 const body = await result.json();
55 setCalories(body.calories);
56 }
57
58 const validator =
59 user.role === Role.ADMIN ? foodEntryAdminValidator : foodEntryUserValidator;
60
61 return (
62 <section>
63 <Heading>Add Food Entry</Heading>
64 <Form replace method="post" validator={validator} autoComplete="off">
65 <InputCombobox
66 label="Food Name"
67 name="name"
68 options={foodOptions}
69 onInputChange={searchDebounced}
70 onSelect={findFoodByName}
71 required
72 />
73 <Input
74 label="Calories"
75 name="calories"
76 type="number"
77 min={0}
78 max={5000}
79 defaultValue={calories || undefined}
80 required
81 />
82 <Input
83 label="Date Consumed"
84 name="consumed"
85 type="datetime-local"
86 defaultValue={now}
87 max={now}
88 required
89 />
90 {user.role === Role.ADMIN ? (
91 <Input
92 label="User ID"
93 name="userId"
94 type="text"
95 defaultValue={user.id}
96 required
97 />
98 ) : null}
99 <PrimaryButton type="submit" className="w-full" data-cy="add-food">
100 Add Food
101 </PrimaryButton>
102 </Form>
103 </section>
104 );
105 }