import { QueryClient, QueryClientProvider, useMutation, useQuery } from "@tanstack/react-query"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
import { toast } from "sonner"
import { Button } from "./components/ui/button"
import { Input } from "./components/ui/input"
import { Label } from "./components/ui/label"
import { Skeleton } from "./components/ui/skeleton"
import { Toaster } from "./components/ui/sonner"
import { fetchProduct, saveProduct } from "./api"
import { Form } from "lucide-react"
const queryClient = new QueryClient()
const numberField = (label) =>
z.preprocess(
(value) => (value === "" ? undefined : Number(value)),
z.number({ error: (issue) => (issue.input === undefined ? "Please enter the " + label.toLowerCase() : "Please enter a number") }).int("Please enter a whole number"),
)
const toNumber = (value) => (value === "" ? "" : Number(value))
const schema = z.object({
name: z.string().min(1, "Please enter a product name"),
price: numberField("Price").pipe(z.number().min(1, "Must be at least $1")),
stock: numberField("Stock").pipe(z.number().min(0, "Must be 0 or more")),
description: z.string().max(200, "Please use 200 characters or fewer"),
})
function Field({ id, label, error, children }) {
return (
<div className="flex flex-col gap-2">
<Label htmlFor={id}>{label}</Label>
{children}
{error && <p className="text-sm text-destructive">{error}</p>}
</div>
)
}
function FormSkeleton() {
return (
<div className="flex flex-col gap-5">
<Skeleton className="h-6 w-40" />
{[1, 2, 3].map((n) => (
<div key={n} className="flex flex-col gap-2">
<Skeleton className="h-4 w-16" />
<Skeleton className="h-9 w-full" />
</div>
))}
<Skeleton className="h-24 w-full" />
</div>
)
}
function EditProduct() {
const { data, isPending } = useQuery({ queryKey: ["product"], queryFn: fetchProduct })
const { register, handleSubmit, reset, formState: { errors, isDirty } } = useForm({
resolver: zodResolver(schema),
values: data,
})
const mutation = useMutation({
mutationFn: saveProduct,
onSuccess: (saved) => {
queryClient.setQueryData(["product"], saved)
reset(saved)
toast.success("Saved")
},
})
if (isPending) {
return <FormSkeleton />
}
return (
<form onSubmit={handleSubmit((values) => mutation.mutate(values))} className="flex flex-col gap-5">
<div className="flex items-center gap-3">
<h1 className="text-lg font-semibold">Edit product</h1>
{}
{isDirty && <span className="rounded-full bg-amber-100 px-2 py-0.5 text-xs text-amber-700">Unsaved changes</span>}
</div>
<Field id="name" label="Product name" error={errors.name?.message}>
<Input id="name" {...register("name")} />
</Field>
<div className="grid grid-cols-2 gap-4">
<Field id="price" label="Price ($)" error={errors.price?.message}>
<Input id="price" inputMode="numeric" {...register("price", { setValueAs: toNumber })} />
</Field>
<Field id="stock" label="Stock" error={errors.stock?.message}>
<Input id="stock" inputMode="numeric" {...register("stock", { setValueAs: toNumber })} />
</Field>
</div>
<Field id="description" label="Description" error={errors.description?.message}>
<textarea
id="description"
rows={3}
className="w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50"
{...register("description")}
/>
</Field>
<div className="flex justify-end gap-2 border-t pt-4">
{}
{}
<Button type="button" variant="outline" disabled={!isDirty || mutation.isPending} onClick={() => reset()}>
Discard changes
</Button>
{}
<Button type="submit" disabled={!isDirty || mutation.isPending}>
{}
{mutation.isPending ? "Saving…" : "Save"}
</Button>
</div>
</form >
)
}
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<div className="mx-auto my-6 w-full max-w-xl rounded-xl border bg-card p-6 text-card-foreground">
<EditProduct />
</div>
<Toaster position="bottom-right" />
</QueryClientProvider>
)
}