Product listing got slight tweaks
This commit is contained in:
parent
c60ec969d5
commit
2fe5dbcb21
@ -28,7 +28,7 @@ interface ProductDialogProps {
|
||||
showDeleteButton?: boolean;
|
||||
}
|
||||
|
||||
export default function ProductDialog({
|
||||
export function ProductDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
form,
|
||||
@ -53,30 +53,38 @@ export default function ProductDialog({
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
{/* Name field */}
|
||||
<div className="flex flex-col justify-between">
|
||||
<Label htmlFor="name" className="mb-1">
|
||||
Product Name
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
{...form.register("name")}
|
||||
placeholder="Enter product name"
|
||||
/>
|
||||
{form.formState.errors.name && (
|
||||
<p className="text-sm text-red-500">
|
||||
{form.formState.errors.name.message}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col">
|
||||
<Label htmlFor="name" className="mb-1">
|
||||
Product Name
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
{...form.register("name")}
|
||||
placeholder="Enter product name"
|
||||
/>
|
||||
{form.formState.errors.name && (
|
||||
<p className="text-sm text-red-500">
|
||||
{form.formState.errors.name.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<Label htmlFor="price" className="mb-1">
|
||||
Price
|
||||
</Label>
|
||||
<Input
|
||||
id="price"
|
||||
type="number"
|
||||
{...form.register("price", { valueAsNumber: true })}
|
||||
{...form.register("price", {
|
||||
valueAsNumber: true,
|
||||
setValueAs: (v) => parseFloat(v).toFixed(2)
|
||||
})}
|
||||
placeholder="0.00"
|
||||
/>
|
||||
{form.formState.errors.price && (
|
||||
<p className="text-sm text-red-500">
|
||||
{form.formState.errors.price.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
@ -93,27 +101,31 @@ export default function ProductDialog({
|
||||
</div>
|
||||
|
||||
{/* Image upload & preview */}
|
||||
<div className="flex flex-col items-center">
|
||||
<Label htmlFor="image">Image</Label>
|
||||
<div className="mt-2 flex flex-col items-center gap-3">
|
||||
<Label htmlFor="image" className="text-sm font-medium">
|
||||
Image
|
||||
</Label>
|
||||
|
||||
{imagePreview ? (
|
||||
<img
|
||||
src={imagePreview}
|
||||
alt="Preview"
|
||||
className="mb-2 h-48 w-48 rounded-lg border object-cover"
|
||||
className="h-32 w-32 rounded-lg border object-cover"
|
||||
/>
|
||||
) : (
|
||||
<Placeholder className="h-48 w-48 fill-gray-100 dark:fill-slate-900 text-slate-900 dark:text-zinc-500" />
|
||||
|
||||
<Placeholder className="h-32 w-32 fill-gray-100 text-slate-900 dark:fill-slate-900 dark:text-zinc-500" />
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground select-none">
|
||||
|
||||
<p className="select-none text-center text-xs text-muted-foreground">
|
||||
JPG or PNG only. Max size: 2MB.
|
||||
</p>
|
||||
|
||||
<Input
|
||||
id="image"
|
||||
type="file"
|
||||
accept="image/png, image/jpeg"
|
||||
onChange={onImageUpload}
|
||||
className="pt-1"
|
||||
className="w-full max-w-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -8,10 +8,12 @@ import {
|
||||
} from "@/components/ui/card";
|
||||
|
||||
export function ProductList({
|
||||
currency,
|
||||
products,
|
||||
isLoading,
|
||||
onClick
|
||||
}: {
|
||||
currency: string,
|
||||
products: ProductWithDetails[];
|
||||
isLoading: boolean;
|
||||
onClick: (id: number) => void;
|
||||
@ -37,12 +39,15 @@ export function ProductList({
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<Placeholder className="h-full w-full object-cover fill-gray-100 dark:fill-slate-900 text-slate-900 dark:text-zinc-500" />
|
||||
<Placeholder className="h-full w-full fill-gray-100 object-cover text-slate-900 dark:fill-slate-900 dark:text-zinc-500" />
|
||||
)}
|
||||
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-background/90 to-background/0 p-2">
|
||||
<div className="absolute bottom-0 left-0 right-0 flex items-center justify-between bg-gradient-to-t from-background/90 to-background/0 p-2 text-xs">
|
||||
<span className="rounded bg-secondary px-2 py-1 text-[10px] font-medium">
|
||||
{product.stock_quantity} in stock
|
||||
</span>
|
||||
<span className="rounded bg-background/70 px-2 py-1 font-semibold">
|
||||
{product.price.toFixed(2)} {currency}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<CardHeader className="p-3 pb-2">
|
||||
|
@ -8,7 +8,7 @@ import { z } from "zod";
|
||||
export const schema = z.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
description: z.string().min(1, "Description is required"),
|
||||
price: z.coerce.number().min(0),
|
||||
price: z.coerce.number().min(0.01, "Price must be greater than 0.01").step(0.01),
|
||||
stock_quantity: z.coerce.number().int().min(0),
|
||||
image_data: z.string().optional()
|
||||
});
|
||||
|
@ -28,6 +28,7 @@ import { useProductForm } from "./hooks/use-product-form";
|
||||
import { ProductList } from "./components/product-list";
|
||||
import { ProductDialog } from "./components/product-dialog";
|
||||
import { toast } from "@/hooks/useToast";
|
||||
import { useShop } from "@/hooks/useShop";
|
||||
|
||||
export default function Products() {
|
||||
const [sort, setSort] = useState<"ascending" | "descending">("ascending");
|
||||
@ -38,6 +39,7 @@ export default function Products() {
|
||||
number | undefined
|
||||
>(undefined);
|
||||
|
||||
const { shop } = useShop();
|
||||
const { data: products = [], isLoading } = useProducts();
|
||||
const { product, createProduct, updateProduct, deleteProduct } =
|
||||
useProduct(selectedProductId);
|
||||
@ -52,7 +54,7 @@ export default function Products() {
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
const handleCreateSubmit = form.handleSubmit((data) => {
|
||||
const handleCreateClicked = () => {
|
||||
form.reset({
|
||||
name: "",
|
||||
description: "",
|
||||
@ -60,6 +62,11 @@ export default function Products() {
|
||||
stock_quantity: 0,
|
||||
image_data: ""
|
||||
});
|
||||
setSelectedProductId(undefined);
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleCreateSubmit = form.handleSubmit((data) => {
|
||||
createProduct.mutate(data);
|
||||
setDialogOpen(false);
|
||||
toast({ title: `${data.name} created`, variant: "default" });
|
||||
@ -144,6 +151,7 @@ export default function Products() {
|
||||
<Separator className="shadow" />
|
||||
|
||||
<ProductList
|
||||
currency={shop?.currency ?? ""}
|
||||
products={filteredProducts}
|
||||
isLoading={isLoading}
|
||||
onClick={(id) => {
|
||||
@ -153,7 +161,7 @@ export default function Products() {
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={() => setDialogOpen(true)}
|
||||
onClick={() => handleCreateClicked()}
|
||||
className="absolute bottom-0 z-10 mb-4 size-14 rounded-full p-0 shadow-lg hover:brightness-110"
|
||||
variant="default">
|
||||
<IconPlus size={28} />
|
||||
|
Loading…
x
Reference in New Issue
Block a user