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; showDeleteButton?: boolean;
} }
export default function ProductDialog({ export function ProductDialog({
open, open,
onOpenChange, onOpenChange,
form, form,
@ -53,30 +53,38 @@ export default function ProductDialog({
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2"> <div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
{/* Name field */} {/* Name field */}
<div className="flex flex-col justify-between"> <div className="flex flex-col justify-between">
<Label htmlFor="name" className="mb-1"> <div className="flex flex-col">
Product Name <Label htmlFor="name" className="mb-1">
</Label> Product Name
<Input </Label>
id="name" <Input
{...form.register("name")} id="name"
placeholder="Enter product name" {...form.register("name")}
/> placeholder="Enter product name"
{form.formState.errors.name && ( />
<p className="text-sm text-red-500"> {form.formState.errors.name && (
{form.formState.errors.name.message} <p className="text-sm text-red-500">
</p> {form.formState.errors.name.message}
)} </p>
)}
</div>
<div className="flex flex-col"> <div className="flex flex-col">
<Label htmlFor="price" className="mb-1"> <Label htmlFor="price" className="mb-1">
Price Price
</Label> </Label>
<Input <Input
id="price" id="price"
type="number" {...form.register("price", {
{...form.register("price", { valueAsNumber: true })} valueAsNumber: true,
setValueAs: (v) => parseFloat(v).toFixed(2)
})}
placeholder="0.00" placeholder="0.00"
/> />
{form.formState.errors.price && (
<p className="text-sm text-red-500">
{form.formState.errors.price.message}
</p>
)}
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
@ -93,27 +101,31 @@ export default function ProductDialog({
</div> </div>
{/* Image upload & preview */} {/* Image upload & preview */}
<div className="flex flex-col items-center"> <div className="mt-2 flex flex-col items-center gap-3">
<Label htmlFor="image">Image</Label> <Label htmlFor="image" className="text-sm font-medium">
Image
</Label>
{imagePreview ? ( {imagePreview ? (
<img <img
src={imagePreview} src={imagePreview}
alt="Preview" 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. JPG or PNG only. Max size: 2MB.
</p> </p>
<Input <Input
id="image" id="image"
type="file" type="file"
accept="image/png, image/jpeg" accept="image/png, image/jpeg"
onChange={onImageUpload} onChange={onImageUpload}
className="pt-1" className="w-full max-w-xs"
/> />
</div> </div>

View File

@ -8,10 +8,12 @@ import {
} from "@/components/ui/card"; } from "@/components/ui/card";
export function ProductList({ export function ProductList({
currency,
products, products,
isLoading, isLoading,
onClick onClick
}: { }: {
currency: string,
products: ProductWithDetails[]; products: ProductWithDetails[];
isLoading: boolean; isLoading: boolean;
onClick: (id: number) => void; onClick: (id: number) => void;
@ -37,12 +39,15 @@ export function ProductList({
className="h-full w-full object-cover" 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"> <span className="rounded bg-secondary px-2 py-1 text-[10px] font-medium">
{product.stock_quantity} in stock {product.stock_quantity} in stock
</span> </span>
<span className="rounded bg-background/70 px-2 py-1 font-semibold">
{product.price.toFixed(2)} {currency}
</span>
</div> </div>
</div> </div>
<CardHeader className="p-3 pb-2"> <CardHeader className="p-3 pb-2">

View File

@ -8,7 +8,7 @@ import { z } from "zod";
export const schema = z.object({ export const schema = z.object({
name: z.string().min(1, "Name is required"), name: z.string().min(1, "Name is required"),
description: z.string().min(1, "Description 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), stock_quantity: z.coerce.number().int().min(0),
image_data: z.string().optional() 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 { ProductList } from "./components/product-list";
import { ProductDialog } from "./components/product-dialog"; import { ProductDialog } from "./components/product-dialog";
import { toast } from "@/hooks/useToast"; import { toast } from "@/hooks/useToast";
import { useShop } from "@/hooks/useShop";
export default function Products() { export default function Products() {
const [sort, setSort] = useState<"ascending" | "descending">("ascending"); const [sort, setSort] = useState<"ascending" | "descending">("ascending");
@ -38,6 +39,7 @@ export default function Products() {
number | undefined number | undefined
>(undefined); >(undefined);
const { shop } = useShop();
const { data: products = [], isLoading } = useProducts(); const { data: products = [], isLoading } = useProducts();
const { product, createProduct, updateProduct, deleteProduct } = const { product, createProduct, updateProduct, deleteProduct } =
useProduct(selectedProductId); useProduct(selectedProductId);
@ -52,7 +54,7 @@ export default function Products() {
reader.readAsDataURL(file); reader.readAsDataURL(file);
}; };
const handleCreateSubmit = form.handleSubmit((data) => { const handleCreateClicked = () => {
form.reset({ form.reset({
name: "", name: "",
description: "", description: "",
@ -60,6 +62,11 @@ export default function Products() {
stock_quantity: 0, stock_quantity: 0,
image_data: "" image_data: ""
}); });
setSelectedProductId(undefined);
setDialogOpen(true);
};
const handleCreateSubmit = form.handleSubmit((data) => {
createProduct.mutate(data); createProduct.mutate(data);
setDialogOpen(false); setDialogOpen(false);
toast({ title: `${data.name} created`, variant: "default" }); toast({ title: `${data.name} created`, variant: "default" });
@ -144,6 +151,7 @@ export default function Products() {
<Separator className="shadow" /> <Separator className="shadow" />
<ProductList <ProductList
currency={shop?.currency ?? ""}
products={filteredProducts} products={filteredProducts}
isLoading={isLoading} isLoading={isLoading}
onClick={(id) => { onClick={(id) => {
@ -153,7 +161,7 @@ export default function Products() {
/> />
<Button <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" className="absolute bottom-0 z-10 mb-4 size-14 rounded-full p-0 shadow-lg hover:brightness-110"
variant="default"> variant="default">
<IconPlus size={28} /> <IconPlus size={28} />