import React, { useState, useEffect, useRef } from 'react';// Main App component to demonstrate ItemListModal usageexport default function App() { const [isModalOpen, setIsModalOpen] = useState(false); // Mock invoice items data const mockInvoiceItems = [ { id: 'item-1', name: 'Wireless Mouse', mrp: 25.99, barcodeId: 'WM-001' }, { id: 'item-2', name: 'Mechanical Keyboard', mrp: 79.99, barcodeId: 'MK-002' }, { id: 'item-3', name: 'USB-C Hub', mrp: 34.50, barcodeId: 'UH-003' }, { id: 'item-4', name: 'External SSD 1TB', mrp: 120.00, barcodeId: 'ES-004' }, { id: 'item-5', name: 'Gaming Headset', mrp: 59.99, barcodeId: 'GH-005' }, { id: 'item-6', name: 'Webcam 1080p', mrp: 45.00, barcodeId: 'WC-006' }, { id: 'item-7', name: 'Monitor Stand', mrp: 29.99, barcodeId: 'MS-007' }, { id: 'item-8', name: 'Ergonomic Chair', mrp: 299.99, barcodeId: 'EC-008' }, ]; // Function to handle opening the modal const handleOpenModal = () => setIsModalOpen(true); // Function to handle closing the modal const handleCloseModal = () => setIsModalOpen(false); return ( <div className="min-h-screen bg-gray-100 flex items-center justify-center p-4 font-sans"> <div className="text-center"> <h1 className="text-4xl font-bold text-gray-800 mb-6">Barcode Generator App</h1> <p className="text-lg text-gray-600 mb-8"> Click the button below to open the barcode generation modal. </p> <button onClick={handleOpenModal} className="px-8 py-4 bg-purple-600 text-white font-semibold rounded-xl shadow-lg hover:bg-purple-700 transition-all duration-300 transform hover:scale-105 focus:outline-none focus:ring-4 focus:ring-purple-300" > Open Barcode Generator </button> </div> {/* Render ItemListModal only if isModalOpen is true */} {isModalOpen && ( <ItemListModal invoiceItems={mockInvoiceItems} onClose={handleCloseModal} /> )} </div> );}// ItemListModal Componentfunction ItemListModal({ invoiceItems, onClose }) { const [step, setStep] = useState(1); // Current step: 1 for selection, 2 for customization // selectedItems now stores { isSelected: boolean, numBarcodes: number } for each item const [selectedItems, setSelectedItems] = useState({}); // customizationData stores { showMrp: boolean, showCustomText: boolean, customText: string } for each item const [customizationData, setCustomizationData] = useState({}); // Global settings for Step 2 const [globalShowMrp, setGlobalShowMrp] = useState(true); const [globalShowCustomText, setGlobalShowCustomText] = useState(false); const [globalCustomText, setGlobalCustomText] = useState(''); const [isPrinting, setIsPrinting] = useState(false); // State for printing loading spinner const modalRef = useRef(null); // Ref for the modal content to detect clicks outside // Initialize selectedItems and customizationData on component mount or invoiceItems change useEffect(() => { const initialSelected = {}; const initialCustomization = {}; invoiceItems.forEach(item => { initialSelected[item.id] = { isSelected: false, numBarcodes: 1 }; // Default quantity to 1 initialCustomization[item.id] = { showMrp: true, showCustomText: false, customText: '', }; }); setSelectedItems(initialSelected); setCustomizationData(initialCustomization); }, [invoiceItems]); // Effect to handle clicking outside the modal to close it useEffect(() => { const handleClickOutside = (event) => { if (modalRef.current && !modalRef.current.contains(event.target)) { onClose(); // Call the onClose prop to close the modal } }; document.addEventListener('mousedown', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [onClose]); // Handle individual item selection (checkbox) const handleItemSelect = (itemId) => { setSelectedItems(prev => ({ ...prev, [itemId]: { ...prev[itemId], isSelected: !prev[itemId].isSelected, // Toggle selection }, })); }; // Handle quantity change for an item const handleQuantityChange = (itemId, value) => { const newQuantity = Math.max(1, parseInt(value) || 1); // Ensure quantity is at least 1 setSelectedItems(prev => ({ ...prev, [itemId]: { ...prev[itemId], numBarcodes: newQuantity, }, })); }; // Handle "Select All" checkbox const handleSelectAll = (event) => { const isChecked = event.target.checked; const newSelectedItems = {}; invoiceItems.forEach(item => { newSelectedItems[item.id] = { isSelected: isChecked, numBarcodes: isChecked ? (selectedItems[item.id]?.numBarcodes || 1) : 1, // Keep existing quantity if selecting all, else reset to 1 }; }); setSelectedItems(newSelectedItems); }; // Check if all items are selected const areAllItemsSelected = invoiceItems.every(item => selectedItems[item.id]?.isSelected); // Check if at least one item is selected to enable "Next" button const isAnyItemSelected = Object.values(selectedItems).some(item => item.isSelected); // Handle individual item customization data changes (MRP, Custom Text toggle, Custom Text input) const handleCustomizationChange = (itemId, field, value) => { setCustomizationData(prev => ({ ...prev, [itemId]: { ...prev[itemId], [field]: value, }, })); }; // Handle global "Show MRP" change const handleGlobalMrpChange = (event) => { const isChecked = event.target.checked; setGlobalShowMrp(isChecked); // Apply this setting to all currently selected items setCustomizationData(prev => { const newState = { ...prev }; getTrulySelectedItems().forEach(item => { newState[item.id] = { ...newState[item.id], showMrp: isChecked, }; }); return newState; }); }; // Handle global "Show Custom Text" toggle const handleGlobalCustomTextToggle = (event) => { const isChecked = event.target.checked; setGlobalShowCustomText(isChecked); // Apply this setting to all currently selected items setCustomizationData(prev => { const newState = { ...prev }; getTrulySelectedItems().forEach(item => { newState[item.id] = { ...newState[item.id], showCustomText: isChecked, }; }); return newState; }); }; // Handle global custom text input change const handleGlobalCustomTextInput = (event) => { const text = event.target.value; setGlobalCustomText(text); // Apply this text to all currently selected items setCustomizationData(prev => { const newState = { ...prev }; getTrulySelectedItems().forEach(item => { newState[item.id] = { ...newState[item.id], customText: text, }; }); return newState; }); }; // Filter items that are actually selected for Step 2, combining all necessary data const getTrulySelectedItems = () => { return invoiceItems.filter(item => selectedItems[item.id]?.isSelected) .map(item => ({ ...item, numBarcodes: selectedItems[item.id]?.numBarcodes || 1, // Get quantity from selectedItems customization: customizationData[item.id] || { showMrp: true, showCustomText: false, customText: '' } })); }; // Simulate printing process const handlePrintBarcodes = () => { setIsPrinting(true); // Simulate API call or printing delay setTimeout(() => { setIsPrinting(false); // Optionally, close modal or show success message onClose(); console.log("Printing simulated for:", getTrulySelectedItems().map(item => ({ id: item.id, name: item.name, quantity: item.numBarcodes, // Now quantity is directly on the item object customization: item.customization }))); }, 2000); // 2 second delay }; // Render Step 1: Item Selection const renderStep1 = () => ( <> <h2 className="text-2xl font-bold text-gray-800 mb-6 text-center">Step 1: Select Items and Quantity</h2> <div className="mb-4 flex items-center p-3 bg-gray-50 rounded-xl shadow-sm"> <input type="checkbox" id="selectAll" checked={areAllItemsSelected} onChange={handleSelectAll} className="form-checkbox h-5 w-5 text-purple-600 rounded focus:ring-purple-500 cursor-pointer" /> <label htmlFor="selectAll" className="ml-3 text-lg font-medium text-gray-700 cursor-pointer"> Select All </label> </div> <div className="max-h-80 overflow-y-auto pr-2 custom-scrollbar"> {invoiceItems.map(item => ( <div key={item.id} className="flex flex-col sm:flex-row items-start sm:items-center justify-between p-4 mb-3 bg-white rounded-xl shadow-md hover:shadow-lg transition-shadow duration-200 border border-gray-200" > <div className="flex items-center mb-3 sm:mb-0"> <input type="checkbox" checked={selectedItems[item.id]?.isSelected || false} onChange={() => handleItemSelect(item.id)} className="form-checkbox h-5 w-5 text-purple-600 rounded focus:ring-purple-500 cursor-pointer" /> <label className="ml-4 text-lg font-medium text-gray-800"> {item.name} <span className="text-gray-500 text-sm">(${item.mrp.toFixed(2)})</span> </label> </div> <div className="flex items-center"> <label htmlFor={`numBarcodes-${item.id}`} className="block text-md font-medium text-gray-700 mr-2"> Qty: </label> <input type="number" id={`numBarcodes-${item.id}`} value={selectedItems[item.id]?.numBarcodes || 1} onChange={(e) => handleQuantityChange(item.id, e.target.value)} min="1" disabled={!selectedItems[item.id]?.isSelected} // Disable if not selected className={`w-20 px-3 py-1 border rounded-lg shadow-sm focus:ring-purple-500 focus:border-purple-500 text-md text-center ${ !selectedItems[item.id]?.isSelected ? 'bg-gray-100 cursor-not-allowed' : 'bg-white border-gray-300' }`} /> </div> </div> ))} </div> <div className="mt-8 flex justify-center"> <button onClick={() => setStep(2)} disabled={!isAnyItemSelected} className={`px-8 py-4 font-semibold rounded-xl shadow-lg transition-all duration-300 transform hover:scale-105 focus:outline-none focus:ring-4 focus:ring-purple-300 ${ isAnyItemSelected ? 'bg-purple-600 text-white hover:bg-purple-700' : 'bg-gray-300 text-gray-500 cursor-not-allowed' }`} > Next </button> </div> </> ); // Render Step 2: Customize & Preview Barcodes const renderStep2 = () => ( <> <h2 className="text-2xl font-bold text-gray-800 mb-6 text-center">Step 2: Customize & Preview Barcodes</h2> {getTrulySelectedItems().length === 0 ? ( <p className="text-center text-gray-600 text-lg py-8">No items selected. Please go back to Step 1.</p> ) : ( <> {/* Global Settings Section */} <div className="mb-6 p-5 bg-purple-50 rounded-xl shadow-md border border-purple-200"> <h3 className="text-xl font-bold text-purple-800 mb-4">Global Settings</h3> <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> {/* Global Show MRP Toggle */} <div className="flex items-center"> <input type="checkbox" id="globalShowMrp" checked={globalShowMrp} onChange={handleGlobalMrpChange} className="form-checkbox h-5 w-5 text-purple-600 rounded focus:ring-purple-500 cursor-pointer" /> <label htmlFor="globalShowMrp" className="ml-3 text-md text-gray-700 cursor-pointer"> Show MRP for all items </label> </div> {/* Global Show Custom Text Toggle */} <div className="flex items-center"> <input type="checkbox" id="globalShowCustomText" checked={globalShowCustomText} onChange={handleGlobalCustomTextToggle} className="form-checkbox h-5 w-5 text-purple-600 rounded focus:ring-purple-500 cursor-pointer" /> <label htmlFor="globalShowCustomText" className="ml-3 text-md text-gray-700 cursor-pointer"> Show Custom Text for all items </label> </div> </div> {/* Global Custom Text Input */} {globalShowCustomText && ( <div className="mt-4"> <label htmlFor="globalCustomTextInput" className="block text-md font-medium text-gray-700 mb-2"> Global Custom Text: </label> <input type="text" id="globalCustomTextInput" value={globalCustomText} onChange={handleGlobalCustomTextInput} placeholder="Enter custom text for all barcodes" className="mt-1 block w-full px-4 py-2 border border-gray-300 rounded-lg shadow-sm focus:ring-purple-500 focus:border-purple-500 sm:text-md" /> </div> )} </div> {/* Individual Item Customization */} <div className="max-h-80 overflow-y-auto pr-2 custom-scrollbar"> {/* Reduced max-height for individual items */} <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> {/* Grid for all items */} {getTrulySelectedItems().map(item => ( <div key={item.id} className="p-4 bg-white rounded-xl shadow-md border border-gray-200 flex flex-col justify-between" > <div> <h3 className="text-lg font-semibold text-gray-800 mb-2"> {item.name} <span className="text-gray-500 text-sm">(Qty: {item.numBarcodes})</span> </h3> {/* Customization Options */} <div className="grid grid-cols-1 gap-y-2 mb-3"> {/* Inner grid for toggles */} {/* Show MRP Toggle */} <div className="flex items-center"> <input type="checkbox" id={`showMrp-${item.id}`} checked={item.customization.showMrp || false} onChange={(e) => handleCustomizationChange(item.id, 'showMrp', e.target.checked)} className="form-checkbox h-4 w-4 text-purple-600 rounded focus:ring-purple-500 cursor-pointer" /> <label htmlFor={`showMrp-${item.id}`} className="ml-2 text-sm text-gray-700 cursor-pointer"> Show MRP (${item.mrp.toFixed(2)}) </label> </div> {/* Show Custom Text Toggle */} <div className="flex items-center"> <input type="checkbox" id={`showCustomText-${item.id}`} checked={item.customization.showCustomText || false} onChange={(e) => handleCustomizationChange(item.id, 'showCustomText', e.target.checked)} className="form-checkbox h-4 w-4 text-purple-600 rounded focus:ring-purple-500 cursor-pointer" /> <label htmlFor={`showCustomText-${item.id}`} className="ml-2 text-sm text-gray-700 cursor-pointer"> Show Custom Text </label> </div> </div> {/* Custom Text Input */} {item.customization.showCustomText && ( <div className="mb-3"> <label htmlFor={`customText-${item.id}`} className="block text-sm font-medium text-gray-700 mb-1"> Custom Text: </label> <input type="text" id={`customText-${item.id}`} value={item.customization.customText || ''} onChange={(e) => handleCustomizationChange(item.id, 'customText', e.target.value)} placeholder="Enter custom text" className="block w-full px-3 py-1 border border-gray-300 rounded-lg shadow-sm focus:ring-purple-500 focus:border-purple-500 text-sm" /> </div> )} </div> {/* Barcode Preview */} <div className="mt-auto p-3 bg-purple-50 rounded-lg border border-purple-200 shadow-inner"> <h4 className="text-md font-semibold text-purple-800 mb-2">Preview:</h4> <div className="bg-white p-2 border border-gray-300 rounded-md text-center text-gray-900 font-mono text-xs break-words"> {/* Mock barcode content based on selections */} <div className="font-bold text-sm mb-0.5">{item.name}</div> {item.customization.showMrp && ( <div className="text-xs">MRP: ${item.mrp.toFixed(2)}</div> )} {item.customization.showCustomText && item.customization.customText && ( <div className="text-xs italic">"{item.customization.customText}"</div> )} <div className="text-xs mt-0.5">ID: {item.barcodeId}</div> <div className="text-xs mt-0.5">Qty: {item.numBarcodes}</div> <div className="text-xl font-bold tracking-widest mt-1">||||||||||||||||||</div> {/* Mock barcode graphic */} </div> </div> </div> ))} </div> </div> </> )} <div className="mt-8 flex justify-between items-center"> <button onClick={() => setStep(1)} className="px-6 py-3 bg-gray-200 text-gray-700 font-semibold rounded-xl shadow-md hover:bg-gray-300 transition-all duration-300 focus:outline-none focus:ring-4 focus:ring-gray-300" > Back </button> <button onClick={handlePrintBarcodes} disabled={isPrinting || getTrulySelectedItems().length === 0} className={`px-8 py-4 font-semibold rounded-xl shadow-lg transition-all duration-300 transform hover:scale-105 focus:outline-none focus:ring-4 focus:ring-purple-300 flex items-center justify-center ${ isPrinting || getTrulySelectedItems().length === 0 ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-purple-600 text-white hover:bg-purple-700' }`} > {isPrinting ? ( <> <svg className="animate-spin h-5 w-5 mr-3 text-white" viewBox="0 0 24 24"> <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> </svg> Printing... </> ) : ( 'Print Barcodes' )} </button> </div> </> ); return ( <div className="fixed inset-0 bg-gray-900 bg-opacity-75 flex items-center justify-center p-4 z-50 font-sans"> <style> {` /* Custom scrollbar for better aesthetics */ .custom-scrollbar::-webkit-scrollbar { width: 8px; } .custom-scrollbar::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 10px; } .custom-scrollbar::-webkit-scrollbar-thumb { background: #888; border-radius: 10px; } .custom-scrollbar::-webkit-scrollbar-thumb:hover { background: #555; } `} </style> <div ref={modalRef} className="bg-white rounded-2xl shadow-2xl p-8 w-full max-w-lg mx-auto transform transition-all duration-300 scale-100 opacity-100 sm:max-w-xl md:max-w-2xl lg:max-w-3xl xl:max-w-4xl" > <button onClick={onClose} className="absolute top-4 right-4 text-gray-500 hover:text-gray-700 text-3xl font-semibold focus:outline-none transition-colors duration-200" aria-label="Close modal" > × </button> {step === 1 ? renderStep1() : renderStep2()} </div> </div> );}
I'll help you convert this barcode generator app to use modern Next.js patterns with shadcn/ui components. This is a well-structured multi-step modal with item selection, customization, and preview functionality.
I've converted your barcode generator app to use modern Next.js patterns with shadcn/ui components. Here are the key improvements: