From 2f6dbc7e85615345e063256efc31fee89c5c73b5 Mon Sep 17 00:00:00 2001 From: Bikram Mondal Date: Wed, 8 Jul 2026 00:07:56 +0530 Subject: [PATCH] creating-a-draft-pt-for-issue-18 --- .../add-item-modal.module.css | 120 ++++++++++++++++ .../inventory-management/add-item-modal.tsx | 132 +++++++++++++++++- .../inventory-table.module.css | 30 ++++ .../inventory-management/inventory-table.tsx | 18 ++- app/routes.ts | 1 + app/routes/api.inventory.upload-image.ts | 78 +++++++++++ app/routes/inventory-management.tsx | 4 + .../migration.sql | 3 + 8 files changed, 383 insertions(+), 3 deletions(-) create mode 100644 app/routes/api.inventory.upload-image.ts create mode 100644 prisma/migrations/20260708000048_add_image_url_to_inventory_item/migration.sql diff --git a/app/blocks/inventory-management/add-item-modal.module.css b/app/blocks/inventory-management/add-item-modal.module.css index d4ed1e0..c4ba372 100644 --- a/app/blocks/inventory-management/add-item-modal.module.css +++ b/app/blocks/inventory-management/add-item-modal.module.css @@ -97,3 +97,123 @@ color: var(--color-text-subtle); line-height: 1.4; } + +/* ── Image Upload ─────────────────────────────────────────── */ + +/* Drag-and-drop / click-to-upload button */ +.imageUploadBtn { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--space-2); + width: 100%; + padding: var(--space-5) var(--space-4); + border-radius: var(--radius-md); + border: 1.5px dashed var(--color-border); + background: var(--color-bg-surface); + color: var(--color-text-muted); + font-size: 13px; + cursor: pointer; + transition: border-color var(--transition-base), background var(--transition-base), color var(--transition-base); + text-align: center; +} + +.imageUploadBtn:hover { + border-color: var(--color-primary); + background: rgba(0, 255, 136, 0.05); + color: var(--color-text); +} + +.imageUploadHint { + font-size: 11px; + color: var(--color-text-subtle); + margin-top: var(--space-1); +} + +.imageUploadIcon { + color: var(--color-primary); + opacity: 0.7; +} + +/* Preview wrapper */ +.imagePreviewWrapper { + position: relative; + width: 100%; + border-radius: var(--radius-md); + overflow: hidden; + border: 1px solid var(--color-border); + background: var(--color-bg-surface); + max-height: 180px; +} + +.imagePreview { + display: block; + width: 100%; + max-height: 180px; + object-fit: cover; +} + +/* Overlay appears on hover */ +.imagePreviewOverlay { + position: absolute; + inset: 0; + display: flex; + align-items: flex-end; + justify-content: space-between; + padding: var(--space-2) var(--space-3); + background: linear-gradient(to top, rgba(0, 0, 0, 0.55) 0%, transparent 60%); + opacity: 0; + transition: opacity var(--transition-base); +} + +.imagePreviewWrapper:hover .imagePreviewOverlay { + opacity: 1; +} + +/* Uploading badge */ +.imageUploadingBadge { + display: inline-flex; + align-items: center; + gap: var(--space-1); + padding: 3px 8px; + border-radius: var(--radius-full); + font-size: 11px; + font-weight: 700; + background: rgba(0, 0, 0, 0.55); + color: #fff; + backdrop-filter: blur(4px); +} + +/* Uploaded / success badge */ +.imageUploadedBadge { + display: inline-flex; + align-items: center; + gap: var(--space-1); + padding: 3px 8px; + border-radius: var(--radius-full); + font-size: 11px; + font-weight: 700; + background: var(--color-success-bg); + color: var(--color-success); +} + +/* Remove button */ +.imageRemoveBtn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 5px; + border-radius: var(--radius-full); + border: none; + background: rgba(220, 38, 38, 0.85); + color: #fff; + cursor: pointer; + transition: background var(--transition-fast), transform var(--transition-fast); +} + +.imageRemoveBtn:hover { + background: var(--color-danger); + transform: scale(1.1); +} + diff --git a/app/blocks/inventory-management/add-item-modal.tsx b/app/blocks/inventory-management/add-item-modal.tsx index eb292e6..3ee9cf7 100644 --- a/app/blocks/inventory-management/add-item-modal.tsx +++ b/app/blocks/inventory-management/add-item-modal.tsx @@ -1,6 +1,14 @@ import { useState, useRef } from "react"; import { Form } from "react-router"; -import { IconX, IconScan, IconLoader2, IconCheck } from "@tabler/icons-react"; +import { + IconX, + IconScan, + IconLoader2, + IconCheck, + IconPhoto, + IconTrash, + IconUpload, +} from "@tabler/icons-react"; import { toast } from "sonner"; import styles from "./add-item-modal.module.css"; @@ -12,6 +20,7 @@ interface Props { } type ScanState = "idle" | "scanning" | "success"; +type UploadState = "idle" | "uploading" | "done" | "error"; const steps = ["Basic Info", "Purchase Details", "Marketplace"]; @@ -25,6 +34,12 @@ export function AddItemModal({ className, onClose, item, isDuplicate = false }: item?.purchasePrice ? String(Number(item.purchasePrice)) : "", ); + // Image upload state + const [imageUrl, setImageUrl] = useState(item?.imageUrl ?? ""); + const [imagePreview, setImagePreview] = useState(item?.imageUrl ?? ""); + const [uploadState, setUploadState] = useState("idle"); + const imageInputRef = useRef(null); + const [scanState, setScanState] = useState("idle"); const fileInputRef = useRef(null); @@ -86,6 +101,55 @@ export function AddItemModal({ className, onClose, item, isDuplicate = false }: } }; + /** Handle item image selection and upload to Supabase Storage */ + const handleImageSelect = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + // Reset input value so the same file can be re-selected if needed + e.target.value = ""; + + // Show local preview immediately + const objectUrl = URL.createObjectURL(file); + setImagePreview(objectUrl); + setUploadState("uploading"); + + try { + const fd = new FormData(); + fd.append("image", file); + + const res = await fetch("/api/inventory/upload-image", { + method: "POST", + body: fd, + }); + + const data = await res.json(); + + if (!data.ok) { + throw new Error(data.error ?? "Upload failed."); + } + + setImageUrl(data.url); + setUploadState("done"); + toast.success("Image uploaded successfully."); + } catch (err: unknown) { + const message = err instanceof Error ? err.message : "Image upload failed."; + toast.error(message); + setImageUrl(""); + setImagePreview(""); + setUploadState("error"); + } + }; + + const handleRemoveImage = () => { + setImageUrl(""); + setImagePreview(""); + setUploadState("idle"); + if (imageInputRef.current) { + imageInputRef.current.value = ""; + } + }; + const scanButtonLabel = scanState === "scanning" ? "Scanning…" : scanState === "success" ? "Extracted!" : "Scan Receipt / Invoice"; @@ -110,6 +174,8 @@ export function AddItemModal({ className, onClose, item, isDuplicate = false }:
{item && !isDuplicate && } + {/* Hidden field to carry the uploaded image URL to the server */} +
{step === 0 && ( <> @@ -152,6 +218,68 @@ export function AddItemModal({ className, onClose, item, isDuplicate = false }: AI will auto-fill SKU, name & price from your image
+ {/* ── Image Upload ── */} +
+ + + {imagePreview ? ( +
+ Item preview +
+ {uploadState === "uploading" && ( +
+ + Uploading… +
+ )} + {uploadState === "done" && ( +
+ + Uploaded +
+ )} + +
+
+ ) : ( + + )} +
+ {/* ── Basic Info Fields ── */}