Product listing got slight tweaks
This commit is contained in:
parent
c60ec969d5
commit
2fe5dbcb21
@ -28,7 +28,7 @@ interface ProductDialogProps {
|
|||||||
showDeleteButton?: boolean;
|
showDeleteButton?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProductDialog({
|
export function ProductDialog({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
form,
|
form,
|
||||||
@ -53,6 +53,7 @@ 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">
|
||||||
|
<div className="flex flex-col">
|
||||||
<Label htmlFor="name" className="mb-1">
|
<Label htmlFor="name" className="mb-1">
|
||||||
Product Name
|
Product Name
|
||||||
</Label>
|
</Label>
|
||||||
@ -66,17 +67,24 @@ export default function ProductDialog({
|
|||||||
{form.formState.errors.name.message}
|
{form.formState.errors.name.message}
|
||||||
</p>
|
</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>
|
||||||
|
|
||||||
|
@ -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">
|
||||||
|
@ -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()
|
||||||
});
|
});
|
||||||
|
@ -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} />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user