Product listing got slight tweaks

This commit is contained in:
Thastertyn 2025-04-20 22:04:57 +02:00
parent c60ec969d5
commit 2fe5dbcb21
4 changed files with 54 additions and 29 deletions

View File

@ -28,7 +28,7 @@ interface ProductDialogProps {
showDeleteButton?: boolean;
}
export default function ProductDialog({
export function ProductDialog({
open,
onOpenChange,
form,
@ -53,6 +53,7 @@ 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">
<div className="flex flex-col">
<Label htmlFor="name" className="mb-1">
Product Name
</Label>
@ -66,17 +67,24 @@ export default function ProductDialog({
{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>

View File

@ -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">

View File

@ -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()
});

View File

@ -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} />