Compare commits

...

8 Commits

Author SHA1 Message Date
NicoR
7077d97812 Convert deliveryDate input to a Controller to handle type conversions 2025-07-03 12:51:41 -03:00
NicoR
25d5fa415b Reuse OrderThumbnail with draft form state 2025-07-03 10:18:12 -03:00
NicoR
9cd8c407d2 Use React Hook Form with JSON object for draft form state 2025-07-03 10:06:39 -03:00
NicoR
88d393ac91 Keep form's draft state in a JSON object 2025-07-02 15:13:55 -03:00
NicoR
f04b11572d Fix rebase errors 2025-07-02 13:56:56 -03:00
Trisha Lim
3cbd4eae3b fix cotext field 2025-07-02 13:56:56 -03:00
Trisha Lim
63ab6abd4f fix reactivity 2025-07-02 13:56:56 -03:00
Trisha Lim
52243161a3 useForm hook 2025-07-02 13:56:55 -03:00
5 changed files with 184 additions and 8 deletions

View File

@@ -13,7 +13,8 @@
"hash-slash": "workspace:*",
"jazz-tools": "workspace:*",
"react": "19.0.0",
"react-dom": "19.0.0"
"react-dom": "19.0.0",
"react-hook-form": "^7.59.0"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
@@ -30,4 +31,4 @@
"typescript": "5.6.2",
"vite": "^6.3.5"
}
}
}

View File

@@ -1,11 +1,13 @@
import { useCoState } from "jazz-tools/react";
import { LinkToHome } from "./LinkToHome.tsx";
import { OrderForm } from "./OrderForm.tsx";
import { OrderFormWithSaveButton } from "./OrderFormWithSaveButton.tsx";
import { OrderThumbnail } from "./OrderThumbnail.tsx";
import { BubbleTeaOrder } from "./schema.ts";
export function EditOrder(props: { id: string }) {
const order = useCoState(BubbleTeaOrder, props.id);
const order = useCoState(BubbleTeaOrder, props.id, {
resolve: { addOns: true, instructions: true },
});
if (!order) return;
@@ -13,13 +15,17 @@ export function EditOrder(props: { id: string }) {
<>
<LinkToHome />
<OrderThumbnail order={order} />
<div>
<p>Saved order:</p>
<OrderThumbnail order={order} />
</div>
<h1 className="text-lg">
<strong>Edit your bubble tea order 🧋</strong>
</h1>
<OrderForm order={order} />
<OrderFormWithSaveButton order={order} />
</>
);
}

View File

@@ -0,0 +1,155 @@
import { CoPlainText, Loaded } from "jazz-tools";
import { useForm, SubmitHandler, Controller } from "react-hook-form";
import {
BubbleTeaAddOnTypes,
BubbleTeaBaseTeaTypes,
BubbleTeaOrder,
} from "./schema.ts";
import { OrderThumbnail } from "./OrderThumbnail.tsx";
type LoadedBubbleTeaOrder = Loaded<
typeof BubbleTeaOrder,
{ addOns: { $each: true }; instructions: true }
>;
// Would be great to derive this type from the CoValue schema
export type OrderFormData = {
id: string;
baseTea: (typeof BubbleTeaBaseTeaTypes)[number];
addOns: (typeof BubbleTeaAddOnTypes)[number][];
deliveryDate: Date;
withMilk: boolean;
instructions?: string;
};
export function OrderFormWithSaveButton({
order: originalOrder,
}: {
order: LoadedBubbleTeaOrder;
}) {
const defaultValues = originalOrder.toJSON();
// Convert timestamp to Date object
defaultValues.deliveryDate = new Date(defaultValues.deliveryDate);
const {
register,
handleSubmit,
watch,
control,
formState: { errors },
} = useForm<OrderFormData>({
defaultValues,
});
const watchedValues = watch();
const onSubmit: SubmitHandler<OrderFormData> = (data) => {
console.log("submit form", data);
// Apply changes to the original Jazz order
originalOrder.baseTea = data.baseTea;
originalOrder.addOns.applyDiff(data.addOns);
originalOrder.deliveryDate = data.deliveryDate;
originalOrder.withMilk = data.withMilk;
// `applyDiff` requires nested objects to be CoValues as well
const instructions = originalOrder.instructions ?? CoPlainText.create("");
if (data.instructions) {
instructions.applyDiff(data.instructions);
}
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="grid gap-5">
<div>
<p>Unsaved order preview:</p>
<OrderThumbnail order={watchedValues} />
</div>
<div className="flex flex-col gap-2">
<label htmlFor="baseTea">Base tea</label>
<select
{...register("baseTea", {
required: "Please select your preferred base tea",
})}
id="baseTea"
className="dark:bg-transparent"
>
<option value="" disabled>
Please select your preferred base tea
</option>
{BubbleTeaBaseTeaTypes.map((teaType) => (
<option key={teaType} value={teaType}>
{teaType}
</option>
))}
</select>
{errors.baseTea && (
<span className="text-red-500 text-sm">{errors.baseTea.message}</span>
)}
</div>
<fieldset>
<legend className="mb-2">Add-ons</legend>
{BubbleTeaAddOnTypes.map((addOn) => (
<div key={addOn} className="flex items-center gap-2">
<input
type="checkbox"
value={addOn}
{...register("addOns")}
id={addOn}
/>
<label htmlFor={addOn}>{addOn}</label>
</div>
))}
</fieldset>
<div className="flex flex-col gap-2">
<label htmlFor="deliveryDate">Delivery date</label>
<Controller
name="deliveryDate"
control={control}
rules={{ required: "Delivery date is required" }}
render={({ field }) => (
<input
type="date"
id="deliveryDate"
className="dark:bg-transparent"
value={
field.value instanceof Date
? field.value.toISOString().split("T")[0]
: ""
}
onChange={(e) => field.onChange(new Date(e.target.value))}
/>
)}
/>
{errors.deliveryDate && (
<span className="text-red-500 text-sm">
{errors.deliveryDate.message}
</span>
)}
</div>
<div className="flex items-center gap-2">
<input type="checkbox" {...register("withMilk")} id="withMilk" />
<label htmlFor="withMilk">With milk?</label>
</div>
<div className="flex flex-col gap-2">
<label htmlFor="instructions">Special instructions</label>
<textarea
{...register("instructions")}
id="instructions"
className="dark:bg-transparent"
/>
</div>
<button
type="submit"
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Submit
</button>
</form>
);
}

View File

@@ -1,10 +1,11 @@
import { Loaded } from "jazz-tools";
import { BubbleTeaOrder } from "./schema.ts";
import { OrderFormData } from "./OrderFormWithSaveButton.tsx";
export function OrderThumbnail({
order,
}: {
order: Loaded<typeof BubbleTeaOrder>;
order: Loaded<typeof BubbleTeaOrder> | OrderFormData;
}) {
const { id, baseTea, addOns, instructions, deliveryDate, withMilk } = order;
const date = deliveryDate.toLocaleDateString();

15
pnpm-lock.yaml generated
View File

@@ -679,6 +679,9 @@ importers:
react-dom:
specifier: 19.0.0
version: 19.0.0(react@19.0.0)
react-hook-form:
specifier: ^7.59.0
version: 7.59.0(react@19.0.0)
devDependencies:
'@biomejs/biome':
specifier: 1.9.4
@@ -11070,6 +11073,12 @@ packages:
peerDependencies:
react: 19.0.0
react-hook-form@7.59.0:
resolution: {integrity: sha512-kmkek2/8grqarTJExFNjy+RXDIP8yM+QTl3QL6m6Q8b2bih4ltmiXxH7T9n+yXNK477xPh5yZT/6vD8sYGzJTA==}
engines: {node: '>=18.0.0'}
peerDependencies:
react: 19.0.0
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
@@ -18820,7 +18829,7 @@ snapshots:
sirv: 3.0.1
tinyglobby: 0.2.14
tinyrainbow: 2.0.0
vitest: 3.1.3(@types/node@22.15.18)(@vitest/browser@3.1.3)(@vitest/ui@3.1.3)(happy-dom@17.4.4)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.30.1)(msw@2.7.0(@types/node@22.15.18)(typescript@5.6.2))(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1)
vitest: 3.1.3(@types/node@22.15.18)(@vitest/browser@3.1.3)(@vitest/ui@3.1.3)(happy-dom@17.4.4)(jiti@2.4.2)(jsdom@25.0.1)(lightningcss@1.30.1)(msw@2.7.0(@types/node@22.15.18)(typescript@5.8.3))(terser@5.37.0)(tsx@4.19.3)(yaml@2.6.1)
'@vitest/utils@3.1.1':
dependencies:
@@ -24073,6 +24082,10 @@ snapshots:
dependencies:
react: 19.0.0
react-hook-form@7.59.0(react@19.0.0):
dependencies:
react: 19.0.0
react-is@16.13.1: {}
react-is@17.0.2: {}