[dyad] Implemented a QR code scanner to filter the stock list - wrote 5 file(s), added html5-qrcode package(s)
This commit is contained in:
parent
d72b809813
commit
f9a6bdc346
5 changed files with 118 additions and 10 deletions
|
|
@ -46,6 +46,7 @@
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"embla-carousel-react": "^8.3.0",
|
"embla-carousel-react": "^8.3.0",
|
||||||
|
"html5-qrcode": "^2.3.8",
|
||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
"lucide-react": "^0.462.0",
|
"lucide-react": "^0.462.0",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,9 @@ importers:
|
||||||
embla-carousel-react:
|
embla-carousel-react:
|
||||||
specifier: ^8.3.0
|
specifier: ^8.3.0
|
||||||
version: 8.6.0(react@18.3.1)
|
version: 8.6.0(react@18.3.1)
|
||||||
|
html5-qrcode:
|
||||||
|
specifier: ^2.3.8
|
||||||
|
version: 2.3.8
|
||||||
input-otp:
|
input-otp:
|
||||||
specifier: ^1.2.4
|
specifier: ^1.2.4
|
||||||
version: 1.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 1.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
|
@ -1829,6 +1832,9 @@ packages:
|
||||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
html5-qrcode@2.3.8:
|
||||||
|
resolution: {integrity: sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==}
|
||||||
|
|
||||||
ignore@5.3.2:
|
ignore@5.3.2:
|
||||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
|
|
@ -4052,6 +4058,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.2
|
function-bind: 1.1.2
|
||||||
|
|
||||||
|
html5-qrcode@2.3.8: {}
|
||||||
|
|
||||||
ignore@5.3.2: {}
|
ignore@5.3.2: {}
|
||||||
|
|
||||||
import-fresh@3.3.1:
|
import-fresh@3.3.1:
|
||||||
|
|
|
||||||
74
src/components/stock/QRScannerDialog.tsx
Normal file
74
src/components/stock/QRScannerDialog.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useEffect } from "react"
|
||||||
|
import { Html5QrcodeScanner } from "html5-qrcode"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
|
||||||
|
interface QRScannerDialogProps {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
onScanSuccess: (decodedText: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const QRScannerDialog = ({ open, onOpenChange, onScanSuccess }: QRScannerDialogProps) => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
|
||||||
|
const scanner = new Html5QrcodeScanner(
|
||||||
|
"reader", // The ID of the element to render the scanner in
|
||||||
|
{
|
||||||
|
qrbox: {
|
||||||
|
width: 250,
|
||||||
|
height: 250,
|
||||||
|
},
|
||||||
|
fps: 5,
|
||||||
|
},
|
||||||
|
false // verbose
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSuccess = (decodedText: string) => {
|
||||||
|
scanner.clear();
|
||||||
|
onScanSuccess(decodedText);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleError = (error: any) => {
|
||||||
|
// Don't log common errors like QR code not found
|
||||||
|
if (typeof error !== 'string' || !error.includes('No QR code found')) {
|
||||||
|
console.error("QR Scanner Error:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
scanner.render(handleSuccess, handleError);
|
||||||
|
|
||||||
|
// Cleanup function to stop the scanner when the component unmounts or dialog closes
|
||||||
|
return () => {
|
||||||
|
if (scanner) {
|
||||||
|
scanner.clear().catch(error => {
|
||||||
|
console.error("Failed to clear scanner.", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [open, onScanSuccess]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Scan QR Code</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Point your camera at a QR code to scan it.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div id="reader" className="w-full"></div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QRScannerDialog;
|
||||||
|
|
@ -35,11 +35,15 @@ import {
|
||||||
interface DataTableProps<TData, TValue> {
|
interface DataTableProps<TData, TValue> {
|
||||||
columns: ColumnDef<TData, TValue>[]
|
columns: ColumnDef<TData, TValue>[]
|
||||||
data: TData[]
|
data: TData[]
|
||||||
|
filterValue: string
|
||||||
|
onFilterChange: (value: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataTable<TData, TValue>({
|
export function DataTable<TData, TValue>({
|
||||||
columns,
|
columns,
|
||||||
data,
|
data,
|
||||||
|
filterValue,
|
||||||
|
onFilterChange,
|
||||||
}: DataTableProps<TData, TValue>) {
|
}: DataTableProps<TData, TValue>) {
|
||||||
const [sorting, setSorting] = React.useState<SortingState>([])
|
const [sorting, setSorting] = React.useState<SortingState>([])
|
||||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
|
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
|
||||||
|
|
@ -76,15 +80,18 @@ export function DataTable<TData, TValue>({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Sync external filter value with the table's internal state
|
||||||
|
React.useEffect(() => {
|
||||||
|
table.getColumn("sku")?.setFilterValue(filterValue)
|
||||||
|
}, [filterValue, table])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center py-4">
|
<div className="flex items-center py-4">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Filter by item name..."
|
placeholder="Filter by SKU..."
|
||||||
value={(table.getColumn("name")?.getFilterValue() as string) ?? ""}
|
value={filterValue}
|
||||||
onChange={(event) =>
|
onChange={(event) => onFilterChange(event.target.value)}
|
||||||
table.getColumn("name")?.setFilterValue(event.target.value)
|
|
||||||
}
|
|
||||||
className="max-w-sm"
|
className="max-w-sm"
|
||||||
/>
|
/>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
import { AddItemForm } from "../components/stock/AddItemForm"
|
import { AddItemForm } from "../components/stock/AddItemForm"
|
||||||
|
import QRScannerDialog from "../components/stock/QRScannerDialog"
|
||||||
|
|
||||||
// In a real app, you would fetch this data from an API.
|
// In a real app, you would fetch this data from an API.
|
||||||
const data: StockItem[] = [
|
const data: StockItem[] = [
|
||||||
|
|
@ -88,21 +89,33 @@ const data: StockItem[] = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const StockList = () => {
|
const StockList = () => {
|
||||||
const [open, setOpen] = useState(false);
|
const [addItemOpen, setAddItemOpen] = useState(false);
|
||||||
|
const [scannerOpen, setScannerOpen] = useState(false);
|
||||||
|
const [filterValue, setFilterValue] = useState("");
|
||||||
|
|
||||||
|
const handleScanSuccess = (decodedText: string) => {
|
||||||
|
setFilterValue(decodedText);
|
||||||
|
setScannerOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<QRScannerDialog
|
||||||
|
open={scannerOpen}
|
||||||
|
onOpenChange={setScannerOpen}
|
||||||
|
onScanSuccess={handleScanSuccess}
|
||||||
|
/>
|
||||||
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4 mb-6">
|
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4 mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold">Stock List</h1>
|
<h1 className="text-3xl font-bold">Stock List</h1>
|
||||||
<p className="text-gray-500">Manage your inventory here.</p>
|
<p className="text-gray-500">Manage your inventory here.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Button variant="outline">
|
<Button variant="outline" onClick={() => setScannerOpen(true)}>
|
||||||
<Camera className="mr-2 h-4 w-4" />
|
<Camera className="mr-2 h-4 w-4" />
|
||||||
Scan Barcode
|
Scan Barcode
|
||||||
</Button>
|
</Button>
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={addItemOpen} onOpenChange={setAddItemOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button>
|
<Button>
|
||||||
<PlusCircle className="mr-2 h-4 w-4" />
|
<PlusCircle className="mr-2 h-4 w-4" />
|
||||||
|
|
@ -116,12 +129,17 @@ const StockList = () => {
|
||||||
Fill in the details below to add a new item to your inventory.
|
Fill in the details below to add a new item to your inventory.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<AddItemForm setOpen={setOpen} />
|
<AddItemForm setOpen={setAddItemOpen} />
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DataTable columns={columns} data={data} />
|
<DataTable
|
||||||
|
columns={columns}
|
||||||
|
data={data}
|
||||||
|
filterValue={filterValue}
|
||||||
|
onFilterChange={setFilterValue}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue