add mobile support
This commit is contained in:
parent
eb3ae05425
commit
87033d7f9a
@ -10,78 +10,72 @@ import ImportExportModal from './components/ImportExportModal';
|
||||
|
||||
function Navigation({ onImportExportClick }: { onImportExportClick: () => void }) {
|
||||
const location = useLocation();
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
|
||||
const isActive = (path: string) => {
|
||||
return location.pathname === path;
|
||||
};
|
||||
|
||||
const navLinks = [
|
||||
{ path: '/', label: 'Dashboard' },
|
||||
{ path: '/shopping-events', label: 'Shopping Events' },
|
||||
{ path: '/shops', label: 'Shops' },
|
||||
{ path: '/products', label: 'Products' },
|
||||
{ path: '/brands', label: 'Brands' },
|
||||
{ path: '/categories', label: 'Categories' }
|
||||
];
|
||||
|
||||
const NavLink = ({ path, label, mobile = false }: { path: string; label: string; mobile?: boolean }) => (
|
||||
<Link
|
||||
to={path}
|
||||
onClick={() => mobile && setIsMobileMenuOpen(false)}
|
||||
className={`${mobile ? 'block px-3 py-2 text-base font-medium' : 'inline-flex items-center px-1 pt-1 text-sm font-medium'} ${
|
||||
isActive(path)
|
||||
? mobile
|
||||
? 'text-blue-600 bg-blue-50 border-l-4 border-blue-600'
|
||||
: 'text-white border-b-2 border-white'
|
||||
: mobile
|
||||
? 'text-gray-600 hover:text-blue-600 hover:bg-gray-50'
|
||||
: 'text-blue-100 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
);
|
||||
|
||||
return (
|
||||
<nav className="bg-blue-600 shadow-lg">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between h-16">
|
||||
<div className="flex space-x-8">
|
||||
<Link
|
||||
to="/"
|
||||
className={`inline-flex items-center px-1 pt-1 text-sm font-medium ${
|
||||
isActive('/')
|
||||
? 'text-white border-b-2 border-white'
|
||||
: 'text-blue-100 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
to="/shopping-events"
|
||||
className={`inline-flex items-center px-1 pt-1 text-sm font-medium ${
|
||||
isActive('/shopping-events')
|
||||
? 'text-white border-b-2 border-white'
|
||||
: 'text-blue-100 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Shopping Events
|
||||
</Link>
|
||||
<Link
|
||||
to="/shops"
|
||||
className={`inline-flex items-center px-1 pt-1 text-sm font-medium ${
|
||||
isActive('/shops')
|
||||
? 'text-white border-b-2 border-white'
|
||||
: 'text-blue-100 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Shops
|
||||
</Link>
|
||||
<Link
|
||||
to="/products"
|
||||
className={`inline-flex items-center px-1 pt-1 text-sm font-medium ${
|
||||
isActive('/products')
|
||||
? 'text-white border-b-2 border-white'
|
||||
: 'text-blue-100 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Products
|
||||
</Link>
|
||||
<Link
|
||||
to="/brands"
|
||||
className={`inline-flex items-center px-1 pt-1 text-sm font-medium ${
|
||||
isActive('/brands')
|
||||
? 'text-white border-b-2 border-white'
|
||||
: 'text-blue-100 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Brands
|
||||
</Link>
|
||||
<Link
|
||||
to="/categories"
|
||||
className={`inline-flex items-center px-1 pt-1 text-sm font-medium ${
|
||||
isActive('/categories')
|
||||
? 'text-white border-b-2 border-white'
|
||||
: 'text-blue-100 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Categories
|
||||
</Link>
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden md:flex space-x-8">
|
||||
{navLinks.map(({ path, label }) => (
|
||||
<NavLink key={path} path={path} label={label} />
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
|
||||
{/* Mobile menu button */}
|
||||
<div className="md:hidden flex items-center">
|
||||
<button
|
||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||
className="inline-flex items-center justify-center p-2 rounded-md text-blue-100 hover:text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span className="sr-only">Open main menu</span>
|
||||
{!isMobileMenuOpen ? (
|
||||
<svg className="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Import/Export Button */}
|
||||
<div className="hidden sm:flex items-center">
|
||||
<button
|
||||
onClick={onImportExportClick}
|
||||
className="inline-flex items-center px-3 py-2 text-sm font-medium text-blue-100 hover:text-white hover:bg-blue-700 rounded-md transition-colors"
|
||||
@ -89,10 +83,21 @@ function Navigation({ onImportExportClick }: { onImportExportClick: () => void }
|
||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10" />
|
||||
</svg>
|
||||
Import / Export
|
||||
<span className="hidden sm:inline">Import / Export</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu */}
|
||||
{isMobileMenuOpen && (
|
||||
<div className="md:hidden">
|
||||
<div className="px-2 pt-2 pb-3 space-y-1 bg-white border-t border-blue-500">
|
||||
{navLinks.map(({ path, label }) => (
|
||||
<NavLink key={path} path={path} label={label} mobile />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
@ -111,8 +116,8 @@ function App() {
|
||||
<Router>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Navigation onImportExportClick={() => setShowImportExportModal(true)} />
|
||||
<main className="py-10">
|
||||
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<main className="py-6 md:py-10">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/shopping-events" element={<ShoppingEventList />} />
|
||||
|
||||
@ -274,15 +274,15 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div className="relative top-10 mx-auto p-5 border w-full max-w-4xl shadow-lg rounded-md bg-white">
|
||||
<div className="relative min-h-screen md:min-h-0 md:top-10 mx-auto p-4 md:p-5 w-full md:max-w-4xl md:shadow-lg md:rounded-md bg-white">
|
||||
<div className="mt-3">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-medium text-gray-900">
|
||||
<h3 className="text-lg md:text-xl font-medium text-gray-900">
|
||||
{isEditMode ? 'Edit Shopping Event' : 'Add New Shopping Event'}
|
||||
</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
className="text-gray-400 hover:text-gray-600 p-2"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
@ -302,7 +302,7 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Shop and Date Selection */}
|
||||
<div className="flex space-x-4">
|
||||
<div className="flex flex-col md:flex-row md:space-x-4 space-y-4 md:space-y-0">
|
||||
<div className="flex-1">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Shop
|
||||
@ -310,7 +310,7 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
<select
|
||||
value={formData.shop_id}
|
||||
onChange={(e) => setFormData({...formData, shop_id: parseInt(e.target.value)})}
|
||||
className="w-full h-10 border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="w-full h-12 md:h-10 border border-gray-300 rounded-md px-3 py-2 text-base md:text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
>
|
||||
<option value={0}>Select a shop</option>
|
||||
@ -321,7 +321,7 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="w-48">
|
||||
<div className="md:w-48">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Date
|
||||
</label>
|
||||
@ -329,7 +329,7 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
type="date"
|
||||
value={formData.date}
|
||||
onChange={(e) => setFormData({...formData, date: e.target.value})}
|
||||
className="w-full h-10 border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="w-full h-12 md:h-10 border border-gray-300 rounded-md px-3 py-2 text-base md:text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@ -340,7 +340,99 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Add Products
|
||||
</label>
|
||||
<div className="flex space-x-2 mb-4">
|
||||
|
||||
{/* Mobile Product Form - Stacked */}
|
||||
<div className="md:hidden space-y-4 mb-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Product
|
||||
</label>
|
||||
<select
|
||||
value={newProductItem.product_id}
|
||||
onChange={(e) => setNewProductItem({...newProductItem, product_id: parseInt(e.target.value)})}
|
||||
className="w-full h-12 border border-gray-300 rounded-md px-3 py-2 text-base focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value={0}>Select a product</option>
|
||||
{Object.entries(
|
||||
getFilteredProducts().reduce((groups, product) => {
|
||||
const category = product.category.name;
|
||||
if (!groups[category]) {
|
||||
groups[category] = [];
|
||||
}
|
||||
groups[category].push(product);
|
||||
return groups;
|
||||
}, {} as Record<string, typeof products>)
|
||||
)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([category, categoryProducts]) => (
|
||||
<optgroup key={category} label={category}>
|
||||
{categoryProducts
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map(product => (
|
||||
<option key={product.id} value={product.id}>
|
||||
{product.name}{product.organic ? ' 🌱' : ''}{product.weight ? ` ${product.weight}${product.weight_unit}` : product.weight_unit}{product.brand ? ` (${product.brand.name})` : ''}
|
||||
</option>
|
||||
))
|
||||
}
|
||||
</optgroup>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Amount
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
placeholder="1"
|
||||
value={newProductItem.amount}
|
||||
onChange={(e) => setNewProductItem({...newProductItem, amount: parseFloat(e.target.value)})}
|
||||
className="w-full h-12 border border-gray-300 rounded-md px-3 py-2 text-base focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Price ($)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
placeholder="0.00"
|
||||
value={newProductItem.price}
|
||||
onChange={(e) => setNewProductItem({...newProductItem, price: parseFloat(e.target.value)})}
|
||||
className="w-full h-12 border border-gray-300 rounded-md px-3 py-2 text-base focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={newProductItem.discount}
|
||||
onChange={(e) => setNewProductItem({...newProductItem, discount: e.target.checked})}
|
||||
className="h-5 w-5 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-700">Discount</span>
|
||||
</label>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={addProductToEvent}
|
||||
className="px-6 py-3 bg-green-500 hover:bg-green-700 text-white rounded-md font-medium text-base"
|
||||
>
|
||||
Add Product
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop Product Form - Horizontal */}
|
||||
<div className="hidden md:flex space-x-2 mb-4">
|
||||
<div className="flex-1">
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
Product
|
||||
@ -375,14 +467,6 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
</optgroup>
|
||||
))}
|
||||
</select>
|
||||
{formData.shop_id > 0 && (
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{shopBrands.length === 0
|
||||
? `Showing all ${products.length} products (no brand restrictions for this shop)`
|
||||
: `Showing ${getFilteredProducts().length} of ${products.length} products (filtered by shop's available brands)`
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-24">
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
@ -439,9 +523,18 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{formData.shop_id > 0 && (
|
||||
<p className="text-xs text-gray-500 mb-4">
|
||||
{shopBrands.length === 0
|
||||
? `Showing all ${products.length} products (no brand restrictions for this shop)`
|
||||
: `Showing ${getFilteredProducts().length} of ${products.length} products (filtered by shop's available brands)`
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Selected Products List */}
|
||||
{selectedProducts.length > 0 && (
|
||||
<div className="bg-gray-50 rounded-md p-4 max-h-40 overflow-y-auto">
|
||||
<div className="bg-gray-50 rounded-md p-4 max-h-40 md:max-h-48 overflow-y-auto">
|
||||
<h4 className="font-medium text-gray-700 mb-2">Selected Items:</h4>
|
||||
{Object.entries(
|
||||
selectedProducts.reduce((groups, item, index) => {
|
||||
@ -461,28 +554,28 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
{category}
|
||||
</div>
|
||||
{categoryItems.map((item) => (
|
||||
<div key={item.index} className="flex justify-between items-center py-2 pl-2">
|
||||
<div className="flex-1">
|
||||
<div key={item.index} className="flex flex-col md:flex-row md:justify-between md:items-center py-2 pl-2 space-y-2 md:space-y-0">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm text-gray-900">
|
||||
{getProductName(item.product_id)}
|
||||
<span className="text-xs text-gray-600 ml-2">
|
||||
{item.amount} × ${item.price.toFixed(2)} = ${(item.amount * item.price).toFixed(2)}
|
||||
{item.discount && <span className="ml-2 text-green-600 font-medium">🏷️</span>}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-600">
|
||||
{item.amount} × ${item.price.toFixed(2)} = ${(item.amount * item.price).toFixed(2)}
|
||||
{item.discount && <span className="ml-2 text-green-600 font-medium">🏷️</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<div className="flex space-x-2 md:flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => editProductFromEvent(item.index)}
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
className="flex-1 md:flex-none px-3 py-1 text-blue-500 hover:text-blue-700 border border-blue-300 hover:bg-blue-50 rounded text-sm"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeProductFromEvent(item.index)}
|
||||
className="text-red-500 hover:text-red-700"
|
||||
className="flex-1 md:flex-none px-3 py-1 text-red-500 hover:text-red-700 border border-red-300 hover:bg-red-50 rounded text-sm"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
@ -497,24 +590,19 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
|
||||
{/* Total Amount */}
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Total Amount
|
||||
Total Amount ($)
|
||||
</label>
|
||||
<label className="flex items-center space-x-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoCalculate}
|
||||
onChange={(e) => setAutoCalculate(e.target.checked)}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<span className="text-xs text-gray-600">Auto-calculate</span>
|
||||
</label>
|
||||
<div className="flex items-center space-x-2">
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoCalculate}
|
||||
onChange={(e) => setAutoCalculate(e.target.checked)}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-blue-600"></div>
|
||||
</label>
|
||||
<span className={`text-xs px-2 py-1 rounded ${autoCalculate ? 'text-green-600 bg-green-50' : 'text-gray-500 bg-gray-100'}`}>
|
||||
Auto-calculated
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
@ -522,52 +610,48 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
min="0"
|
||||
placeholder="0.00"
|
||||
value={formData.total_amount || ''}
|
||||
onChange={(e) => {
|
||||
setFormData({...formData, total_amount: e.target.value ? parseFloat(e.target.value) : undefined});
|
||||
setAutoCalculate(false); // Disable auto-calculation when manually editing
|
||||
}}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 text-right"
|
||||
onChange={(e) => setFormData({...formData, total_amount: e.target.value ? parseFloat(e.target.value) : undefined})}
|
||||
disabled={autoCalculate}
|
||||
className={`w-full h-12 md:h-10 border border-gray-300 rounded-md px-3 py-2 text-base md:text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||
autoCalculate ? 'bg-gray-100 cursor-not-allowed' : ''
|
||||
}`}
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{autoCalculate
|
||||
? "This field is automatically calculated from your selected items. You can manually edit it if needed."
|
||||
: "Auto-calculation is disabled. You can manually enter the total amount or enable auto-calculation above."
|
||||
}
|
||||
</p>
|
||||
{autoCalculate && selectedProducts.length > 0 && (
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Automatically calculated from selected products: ${calculateTotal(selectedProducts).toFixed(2)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Notes */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Notes (optional)
|
||||
Notes (Optional)
|
||||
</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
value={formData.notes}
|
||||
onChange={(e) => setFormData({...formData, notes: e.target.value})}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Any additional notes about this purchase..."
|
||||
rows={3}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2 text-base md:text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Add any notes about this shopping event..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<div className="flex justify-end space-x-3 pt-4">
|
||||
<div className="flex flex-col md:flex-row md:justify-end space-y-3 md:space-y-0 md:space-x-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md"
|
||||
className="w-full md:w-auto px-6 py-3 md:py-2 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 font-medium text-base md:text-sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || formData.shop_id === 0 || selectedProducts.length === 0}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="w-full md:w-auto px-6 py-3 md:py-2 bg-blue-500 hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed text-white rounded-md font-medium text-base md:text-sm"
|
||||
>
|
||||
{loading
|
||||
? (isEditMode ? 'Updating...' : 'Creating...')
|
||||
: (isEditMode ? 'Update Shopping Event' : 'Create Shopping Event')
|
||||
}
|
||||
{loading ? 'Saving...' : (isEditMode ? 'Update Event' : 'Create Event')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -146,11 +146,11 @@ const BrandList: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Brands</h1>
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center space-y-4 sm:space-y-0">
|
||||
<h1 className="text-xl md:text-2xl font-bold text-gray-900">Brands</h1>
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
||||
className="w-full sm:w-auto bg-blue-500 hover:bg-blue-700 text-white font-bold py-3 sm:py-2 px-4 rounded text-base sm:text-sm"
|
||||
>
|
||||
Add New Brand
|
||||
</button>
|
||||
@ -172,73 +172,114 @@ const BrandList: React.FC = () => {
|
||||
<p className="mt-1 text-sm text-gray-500">Get started by adding your first brand.</p>
|
||||
</div>
|
||||
) : (
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('name')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
{getSortIcon('name')}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('created_at')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Created
|
||||
{getSortIcon('created_at')}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('updated_at')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Updated
|
||||
{getSortIcon('updated_at')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
<>
|
||||
{/* Desktop Table */}
|
||||
<div className="hidden md:block">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('name')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
{getSortIcon('name')}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('created_at')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Created
|
||||
{getSortIcon('created_at')}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('updated_at')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Updated
|
||||
{getSortIcon('updated_at')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{sortedBrands.map((brand) => (
|
||||
<tr key={brand.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{brand.name}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{new Date(brand.created_at).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{brand.updated_at ? new Date(brand.updated_at).toLocaleDateString() : '-'}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<button
|
||||
onClick={() => handleEditBrand(brand)}
|
||||
className="text-indigo-600 hover:text-indigo-900 mr-3"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteBrand(brand)}
|
||||
className="text-red-600 hover:text-red-900"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Mobile Card Layout */}
|
||||
<div className="md:hidden">
|
||||
{sortedBrands.map((brand) => (
|
||||
<tr key={brand.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{brand.name}
|
||||
<div key={brand.id} className="border-b border-gray-200 p-4 last:border-b-0">
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-medium text-gray-900 truncate">{brand.name}</h3>
|
||||
<p className="text-sm text-gray-500">Created: {new Date(brand.created_at).toLocaleDateString()}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{new Date(brand.created_at).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{brand.updated_at ? new Date(brand.updated_at).toLocaleDateString() : '-'}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div className="text-right flex-shrink-0 ml-4">
|
||||
{brand.updated_at && (
|
||||
<p className="text-xs text-gray-500">
|
||||
Updated: {new Date(brand.updated_at).toLocaleDateString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-4">
|
||||
<button
|
||||
onClick={() => handleEditBrand(brand)}
|
||||
className="text-indigo-600 hover:text-indigo-900 mr-3"
|
||||
className="flex-1 text-center py-2 px-4 border border-indigo-300 text-indigo-600 hover:bg-indigo-50 rounded-md text-sm font-medium"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteBrand(brand)}
|
||||
className="text-red-600 hover:text-red-900"
|
||||
className="flex-1 text-center py-2 px-4 border border-red-300 text-red-600 hover:bg-red-50 rounded-md text-sm font-medium"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@ -31,8 +31,8 @@ const Dashboard: React.FC = () => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
|
||||
<p className="text-gray-600">Welcome to your product tracker!</p>
|
||||
<h1 className="text-xl md:text-2xl font-bold text-gray-900">Dashboard</h1>
|
||||
<p className="text-base md:text-gray-600">Welcome to your product tracker!</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
@ -96,21 +96,21 @@ const Dashboard: React.FC = () => {
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<div className="px-4 md:px-6 py-4 border-b border-gray-200">
|
||||
<h2 className="text-lg font-medium text-gray-900">Quick Actions</h2>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<div className="p-4 md:p-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<button
|
||||
onClick={() => navigate('/shopping-events?add=true')}
|
||||
className="flex items-center p-4 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
className="flex items-center p-4 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors text-left"
|
||||
>
|
||||
<div className="p-2 bg-blue-100 rounded-md mr-3">
|
||||
<div className="p-2 bg-blue-100 rounded-md mr-3 flex-shrink-0">
|
||||
<svg className="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div className="min-w-0">
|
||||
<p className="font-medium text-gray-900">Add New Event</p>
|
||||
<p className="text-sm text-gray-600">Record a new shopping event</p>
|
||||
</div>
|
||||
@ -118,14 +118,14 @@ const Dashboard: React.FC = () => {
|
||||
|
||||
<button
|
||||
onClick={() => navigate('/products?add=true')}
|
||||
className="flex items-center p-4 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
className="flex items-center p-4 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors text-left"
|
||||
>
|
||||
<div className="p-2 bg-green-100 rounded-md mr-3">
|
||||
<div className="p-2 bg-green-100 rounded-md mr-3 flex-shrink-0">
|
||||
<svg className="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div className="min-w-0">
|
||||
<p className="font-medium text-gray-900">Add Product</p>
|
||||
<p className="text-sm text-gray-600">Add a new product item</p>
|
||||
</div>
|
||||
@ -133,14 +133,14 @@ const Dashboard: React.FC = () => {
|
||||
|
||||
<button
|
||||
onClick={() => navigate('/shops?add=true')}
|
||||
className="flex items-center p-4 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
className="flex items-center p-4 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors text-left"
|
||||
>
|
||||
<div className="p-2 bg-purple-100 rounded-md mr-3">
|
||||
<div className="p-2 bg-purple-100 rounded-md mr-3 flex-shrink-0">
|
||||
<svg className="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div className="min-w-0">
|
||||
<p className="font-medium text-gray-900">Add Shop</p>
|
||||
<p className="text-sm text-gray-600">Register a new shop</p>
|
||||
</div>
|
||||
@ -151,10 +151,10 @@ const Dashboard: React.FC = () => {
|
||||
|
||||
{/* Recent Activity */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<div className="px-4 md:px-6 py-4 border-b border-gray-200">
|
||||
<h2 className="text-lg font-medium text-gray-900">Recent Shopping Events</h2>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<div className="p-4 md:p-6">
|
||||
{loading ? (
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
|
||||
@ -172,10 +172,10 @@ const Dashboard: React.FC = () => {
|
||||
{recentEvents.map((event) => (
|
||||
<div key={event.id} className="border border-gray-200 rounded-lg p-4 hover:bg-gray-50 transition-colors">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h4 className="font-medium text-gray-900">{event.shop.name}</h4>
|
||||
<span className="text-sm text-gray-500">•</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:space-x-2">
|
||||
<h4 className="font-medium text-gray-900 truncate">{event.shop.name}</h4>
|
||||
<span className="hidden sm:inline text-sm text-gray-500">•</span>
|
||||
<span className="text-sm text-gray-500">{event.shop.city}</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
@ -187,7 +187,7 @@ const Dashboard: React.FC = () => {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-right flex-shrink-0 ml-4">
|
||||
{event.total_amount && (
|
||||
<p className="font-semibold text-green-600">
|
||||
${event.total_amount.toFixed(2)}
|
||||
|
||||
@ -137,11 +137,11 @@ const GroceryCategoryList: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Grocery Categories</h1>
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center space-y-4 sm:space-y-0">
|
||||
<h1 className="text-xl md:text-2xl font-bold text-gray-900">Grocery Categories</h1>
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
||||
className="w-full sm:w-auto bg-blue-500 hover:bg-blue-700 text-white font-bold py-3 sm:py-2 px-4 rounded text-base sm:text-sm"
|
||||
>
|
||||
Add New Category
|
||||
</button>
|
||||
@ -167,61 +167,95 @@ const GroceryCategoryList: React.FC = () => {
|
||||
<p className="mt-1 text-sm text-gray-500">Get started by adding your first category.</p>
|
||||
</div>
|
||||
) : (
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('name')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
{getSortIcon('name')}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('created_at')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Created
|
||||
{getSortIcon('created_at')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
<>
|
||||
{/* Desktop Table */}
|
||||
<div className="hidden md:block">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('name')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
{getSortIcon('name')}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('created_at')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Created
|
||||
{getSortIcon('created_at')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{sortedCategories.map((category) => (
|
||||
<tr key={category.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{category.name}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{new Date(category.created_at).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<button
|
||||
onClick={() => handleEdit(category)}
|
||||
className="text-indigo-600 hover:text-indigo-900 mr-3"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(category)}
|
||||
className="text-red-600 hover:text-red-900"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Mobile Card Layout */}
|
||||
<div className="md:hidden">
|
||||
{sortedCategories.map((category) => (
|
||||
<tr key={category.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{category.name}
|
||||
<div key={category.id} className="border-b border-gray-200 p-4 last:border-b-0">
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-medium text-gray-900 truncate">{category.name}</h3>
|
||||
<p className="text-sm text-gray-500">Created: {new Date(category.created_at).toLocaleDateString()}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{new Date(category.created_at).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<button
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-4">
|
||||
<button
|
||||
onClick={() => handleEdit(category)}
|
||||
className="text-indigo-600 hover:text-indigo-900 mr-3"
|
||||
className="flex-1 text-center py-2 px-4 border border-indigo-300 text-indigo-600 hover:bg-indigo-50 rounded-md text-sm font-medium"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
onClick={() => handleDelete(category)}
|
||||
className="text-red-600 hover:text-red-900"
|
||||
className="flex-1 text-center py-2 px-4 border border-red-300 text-red-600 hover:bg-red-50 rounded-md text-sm font-medium"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@ -171,14 +171,14 @@ const ProductList: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Products</h1>
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center space-y-4 sm:space-y-0">
|
||||
<h1 className="text-xl md:text-2xl font-bold text-gray-900">Products</h1>
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingProduct(null);
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
||||
className="w-full sm:w-auto bg-blue-500 hover:bg-blue-700 text-white font-bold py-3 sm:py-2 px-4 rounded text-base sm:text-sm"
|
||||
>
|
||||
Add New Product
|
||||
</button>
|
||||
@ -200,85 +200,134 @@ const ProductList: React.FC = () => {
|
||||
<p className="mt-1 text-sm text-gray-500">Get started by adding your first product item.</p>
|
||||
</div>
|
||||
) : (
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('name')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
{getSortIcon('name')}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('category')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Category
|
||||
{getSortIcon('category')}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('brand')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Brand
|
||||
{getSortIcon('brand')}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('weight')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Weight
|
||||
{getSortIcon('weight')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
<>
|
||||
{/* Desktop Table */}
|
||||
<div className="hidden md:block">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('name')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
{getSortIcon('name')}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('category')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Category
|
||||
{getSortIcon('category')}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('brand')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Brand
|
||||
{getSortIcon('brand')}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('weight')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Weight
|
||||
{getSortIcon('weight')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{sortedProducts.map((product) => (
|
||||
<tr key={product.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{product.name} {product.organic ? '🌱' : ''}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{product.category.name}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{product.brand ? product.brand.name : '-'}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{product.weight ? `${product.weight}${product.weight_unit}` : '-'}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<button
|
||||
onClick={() => handleEdit(product)}
|
||||
className="text-indigo-600 hover:text-indigo-900 mr-3"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(product)}
|
||||
className="text-red-600 hover:text-red-900"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Mobile Card Layout */}
|
||||
<div className="md:hidden">
|
||||
{sortedProducts.map((product) => (
|
||||
<tr key={product.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{product.name} {product.organic ? '🌱' : ''}
|
||||
<div key={product.id} className="border-b border-gray-200 p-4 last:border-b-0">
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-medium text-gray-900 truncate">
|
||||
{product.name} {product.organic ? '🌱' : ''}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500">{product.category.name}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{product.category.name}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{product.brand ? product.brand.name : '-'}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{product.weight ? `${product.weight}${product.weight_unit}` : '-'}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
{product.weight && (
|
||||
<div className="text-right flex-shrink-0 ml-4">
|
||||
<p className="text-sm text-gray-600">{product.weight}{product.weight_unit}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{product.brand && (
|
||||
<div className="mb-3">
|
||||
<p className="text-sm text-gray-600">
|
||||
<span className="font-medium">Brand:</span> {product.brand.name}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex space-x-4">
|
||||
<button
|
||||
onClick={() => handleEdit(product)}
|
||||
className="text-indigo-600 hover:text-indigo-900 mr-3"
|
||||
className="flex-1 text-center py-2 px-4 border border-indigo-300 text-indigo-600 hover:bg-indigo-50 rounded-md text-sm font-medium"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(product)}
|
||||
className="text-red-600 hover:text-red-900"
|
||||
className="flex-1 text-center py-2 px-4 border border-red-300 text-red-600 hover:bg-red-50 rounded-md text-sm font-medium"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@ -241,11 +241,11 @@ const ShopList: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Shops</h1>
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center space-y-4 sm:space-y-0">
|
||||
<h1 className="text-xl md:text-2xl font-bold text-gray-900">Shops</h1>
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
||||
className="w-full sm:w-auto bg-blue-500 hover:bg-blue-700 text-white font-bold py-3 sm:py-2 px-4 rounded text-base sm:text-sm"
|
||||
>
|
||||
Add New Shop
|
||||
</button>
|
||||
@ -267,110 +267,176 @@ const ShopList: React.FC = () => {
|
||||
<p className="mt-1 text-sm text-gray-500">Get started by adding your first shop.</p>
|
||||
</div>
|
||||
) : (
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('name')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
{getSortIcon('name')}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('city')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
City
|
||||
{getSortIcon('city')}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('address')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Address
|
||||
{getSortIcon('address')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Brands
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('created_at')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Created
|
||||
{getSortIcon('created_at')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
<>
|
||||
{/* Desktop Table */}
|
||||
<div className="hidden md:block">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('name')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
{getSortIcon('name')}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('city')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
City
|
||||
{getSortIcon('city')}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('address')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Address
|
||||
{getSortIcon('address')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Brands
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('created_at')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Created
|
||||
{getSortIcon('created_at')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{sortedShops.map((shop) => (
|
||||
<tr key={shop.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{shop.name}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{shop.city}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{shop.address || '-'}
|
||||
</td>
|
||||
<td
|
||||
className={`brands-cell px-6 py-4 whitespace-nowrap text-sm ${
|
||||
(shopBrands[shop.id]?.length || 0) > 0
|
||||
? 'text-blue-600 hover:text-blue-800 cursor-pointer hover:bg-blue-50'
|
||||
: 'text-gray-900'
|
||||
}`}
|
||||
onMouseEnter={(e) => handleBrandsHover(shop, e)}
|
||||
onMouseLeave={handleBrandsLeave}
|
||||
onClick={(e) => handleBrandsClick(shop, e)}
|
||||
title={(shopBrands[shop.id]?.length || 0) > 0 ? 'Click to view brands' : ''}
|
||||
>
|
||||
{(shopBrands[shop.id]?.length || 0) > 0 ? (
|
||||
<>
|
||||
{(shopBrands[shop.id]?.length || 0)} brand{(shopBrands[shop.id]?.length || 0) !== 1 ? 's' : ''}
|
||||
<svg className="inline-block w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{new Date(shop.created_at).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<button
|
||||
onClick={() => handleEditShop(shop)}
|
||||
className="text-indigo-600 hover:text-indigo-900 mr-3"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteShop(shop)}
|
||||
className="text-red-600 hover:text-red-900"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Mobile Card Layout */}
|
||||
<div className="md:hidden">
|
||||
{sortedShops.map((shop) => (
|
||||
<tr key={shop.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{shop.name}
|
||||
<div key={shop.id} className="border-b border-gray-200 p-4 last:border-b-0">
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-medium text-gray-900 truncate">{shop.name}</h3>
|
||||
<p className="text-sm text-gray-500">{shop.city}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{shop.city}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{shop.address || '-'}
|
||||
</td>
|
||||
<td
|
||||
className={`brands-cell px-6 py-4 whitespace-nowrap text-sm ${
|
||||
(shopBrands[shop.id]?.length || 0) > 0
|
||||
? 'text-blue-600 hover:text-blue-800 cursor-pointer hover:bg-blue-50'
|
||||
: 'text-gray-900'
|
||||
}`}
|
||||
onMouseEnter={(e) => handleBrandsHover(shop, e)}
|
||||
onMouseLeave={handleBrandsLeave}
|
||||
onClick={(e) => handleBrandsClick(shop, e)}
|
||||
title={(shopBrands[shop.id]?.length || 0) > 0 ? 'Click to view brands' : ''}
|
||||
>
|
||||
{(shopBrands[shop.id]?.length || 0) > 0 ? (
|
||||
<>
|
||||
{(shopBrands[shop.id]?.length || 0)} brand{(shopBrands[shop.id]?.length || 0) !== 1 ? 's' : ''}
|
||||
<svg className="inline-block w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{new Date(shop.created_at).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div className="text-right flex-shrink-0 ml-4">
|
||||
<p className="text-xs text-gray-500">{new Date(shop.created_at).toLocaleDateString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{shop.address && (
|
||||
<div className="mb-3">
|
||||
<p className="text-sm text-gray-600">{shop.address}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<button
|
||||
onClick={(e) => handleBrandsClick(shop, e)}
|
||||
className={`text-sm ${
|
||||
(shopBrands[shop.id]?.length || 0) > 0
|
||||
? 'text-blue-600 hover:text-blue-800'
|
||||
: 'text-gray-600'
|
||||
}`}
|
||||
disabled={(shopBrands[shop.id]?.length || 0) === 0}
|
||||
>
|
||||
{(shopBrands[shop.id]?.length || 0) > 0 ? (
|
||||
<>
|
||||
{(shopBrands[shop.id]?.length || 0)} brand{(shopBrands[shop.id]?.length || 0) !== 1 ? 's' : ''}
|
||||
<svg className="inline-block w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</>
|
||||
) : (
|
||||
'No brands'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-4">
|
||||
<button
|
||||
onClick={() => handleEditShop(shop)}
|
||||
className="text-indigo-600 hover:text-indigo-900 mr-3"
|
||||
className="flex-1 text-center py-2 px-4 border border-indigo-300 text-indigo-600 hover:bg-indigo-50 rounded-md text-sm font-medium"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteShop(shop)}
|
||||
className="text-red-600 hover:text-red-900"
|
||||
className="flex-1 text-center py-2 px-4 border border-red-300 text-red-600 hover:bg-red-50 rounded-md text-sm font-medium"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -395,21 +461,30 @@ const ShopList: React.FC = () => {
|
||||
<div
|
||||
className="brands-popup fixed z-50 bg-white border border-gray-200 rounded-lg shadow-lg p-4 max-w-sm"
|
||||
style={{
|
||||
left: `${popupPosition.x}px`,
|
||||
top: `${popupPosition.y}px`,
|
||||
maxHeight: '200px',
|
||||
left: window.innerWidth < 768 ? '50%' : `${popupPosition.x}px`,
|
||||
top: window.innerWidth < 768 ? '50%' : `${popupPosition.y}px`,
|
||||
transform: window.innerWidth < 768 ? 'translate(-50%, -50%)' : 'none',
|
||||
maxHeight: '300px',
|
||||
overflowY: 'auto'
|
||||
}}
|
||||
onMouseEnter={() => setShowBrandsPopup(true)}
|
||||
onMouseLeave={handleBrandsLeave}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium text-gray-900 mb-2">Available Brands:</h4>
|
||||
{shopBrands[hoveredShop.id]?.map((brandInShop, index) => (
|
||||
<div key={index} className="border-b border-gray-100 pb-1 last:border-b-0">
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{brandInShop.brand.name}
|
||||
</div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h4 className="font-medium text-gray-900">Available Brands</h4>
|
||||
<button
|
||||
onClick={() => setShowBrandsPopup(false)}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{(shopBrands[hoveredShop.id] || []).map((brandInShop, index) => (
|
||||
<div key={index} className="text-sm text-gray-700">
|
||||
{brandInShop.brand.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -256,11 +256,11 @@ const ShoppingEventList: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Shopping Events</h1>
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center space-y-4 sm:space-y-0">
|
||||
<h1 className="text-xl md:text-2xl font-bold text-gray-900">Shopping Events</h1>
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
||||
className="w-full sm:w-auto bg-blue-500 hover:bg-blue-700 text-white font-bold py-3 sm:py-2 px-4 rounded text-base sm:text-sm"
|
||||
>
|
||||
Add New Event
|
||||
</button>
|
||||
@ -282,188 +282,244 @@ const ShoppingEventList: React.FC = () => {
|
||||
<p className="mt-1 text-sm text-gray-500">Get started by recording your first purchase.</p>
|
||||
</div>
|
||||
) : (
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('shop')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Shop
|
||||
{getSortIcon('shop')}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('date')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Date
|
||||
{getSortIcon('date')}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('items')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Items
|
||||
{getSortIcon('items')}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('total')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Total
|
||||
{getSortIcon('total')}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('notes')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Notes
|
||||
{getSortIcon('notes')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
<>
|
||||
{/* Desktop Table */}
|
||||
<div className="hidden md:block">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('shop')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Shop
|
||||
{getSortIcon('shop')}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('date')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Date
|
||||
{getSortIcon('date')}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('items')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Items
|
||||
{getSortIcon('items')}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('total')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Total
|
||||
{getSortIcon('total')}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('notes')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Notes
|
||||
{getSortIcon('notes')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{sortedEvents.map((event) => (
|
||||
<tr key={event.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-gray-900">{event.shop.name}</div>
|
||||
<div className="text-xs text-gray-500">{event.shop.city}</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{new Date(event.date).toLocaleDateString()}
|
||||
</td>
|
||||
<td
|
||||
className={`items-cell px-6 py-4 whitespace-nowrap text-sm ${
|
||||
event.products.length > 0
|
||||
? 'text-blue-600 hover:text-blue-800 cursor-pointer hover:bg-blue-50'
|
||||
: 'text-gray-900'
|
||||
}`}
|
||||
onMouseEnter={(e) => handleItemsHover(event, e)}
|
||||
onMouseLeave={handleItemsLeave}
|
||||
onClick={(e) => handleItemsClick(event, e)}
|
||||
title={event.products.length > 0 ? 'Click to view items' : ''}
|
||||
>
|
||||
{event.products.length} item{event.products.length !== 1 ? 's' : ''}
|
||||
{event.products.length > 0 && (
|
||||
<svg className="inline-block w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{event.total_amount ? (
|
||||
<span className="text-sm font-semibold text-green-600">
|
||||
${event.total_amount.toFixed(2)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-gray-500">-</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{event.notes ? (
|
||||
<span className="truncate max-w-xs block" title={event.notes}>
|
||||
{event.notes.length > 30 ? `${event.notes.substring(0, 30)}...` : event.notes}
|
||||
</span>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<button
|
||||
onClick={() => handleEdit(event)}
|
||||
className="text-indigo-600 hover:text-indigo-900 mr-3"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(event)}
|
||||
className="text-red-600 hover:text-red-900"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Mobile Card Layout */}
|
||||
<div className="md:hidden">
|
||||
{sortedEvents.map((event) => (
|
||||
<tr key={event.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-gray-900">{event.shop.name}</div>
|
||||
<div className="text-xs text-gray-500">{event.shop.city}</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{new Date(event.date).toLocaleDateString()}
|
||||
</td>
|
||||
<td
|
||||
className={`items-cell px-6 py-4 whitespace-nowrap text-sm ${
|
||||
event.products.length > 0
|
||||
? 'text-blue-600 hover:text-blue-800 cursor-pointer hover:bg-blue-50'
|
||||
: 'text-gray-900'
|
||||
}`}
|
||||
onMouseEnter={(e) => handleItemsHover(event, e)}
|
||||
onMouseLeave={handleItemsLeave}
|
||||
onClick={(e) => handleItemsClick(event, e)}
|
||||
title={event.products.length > 0 ? 'Click to view items' : ''}
|
||||
>
|
||||
{event.products.length} item{event.products.length !== 1 ? 's' : ''}
|
||||
{event.products.length > 0 && (
|
||||
<svg className="inline-block w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<div key={event.id} className="border-b border-gray-200 p-4 last:border-b-0">
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-medium text-gray-900 truncate">{event.shop.name}</h3>
|
||||
<p className="text-sm text-gray-500">{event.shop.city}</p>
|
||||
</div>
|
||||
<div className="text-right flex-shrink-0 ml-4">
|
||||
<p className="text-sm text-gray-600">{new Date(event.date).toLocaleDateString()}</p>
|
||||
{event.total_amount && (
|
||||
<p className="font-semibold text-green-600 mt-1">
|
||||
${event.total_amount.toFixed(2)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<button
|
||||
onClick={(e) => handleItemsClick(event, e)}
|
||||
className={`text-sm ${
|
||||
event.products.length > 0
|
||||
? 'text-blue-600 hover:text-blue-800'
|
||||
: 'text-gray-600'
|
||||
}`}
|
||||
disabled={event.products.length === 0}
|
||||
>
|
||||
{event.products.length} item{event.products.length !== 1 ? 's' : ''}
|
||||
{event.products.length > 0 && (
|
||||
<svg className="inline-block w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{event.notes && (
|
||||
<p className="text-sm text-gray-500 truncate max-w-xs" title={event.notes}>
|
||||
{event.notes.length > 20 ? `${event.notes.substring(0, 20)}...` : event.notes}
|
||||
</p>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{event.total_amount ? (
|
||||
<span className="text-sm font-semibold text-green-600">
|
||||
${event.total_amount.toFixed(2)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-gray-500">-</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{event.notes ? (
|
||||
<span className="truncate max-w-xs block" title={event.notes}>
|
||||
{event.notes.length > 30 ? `${event.notes.substring(0, 30)}...` : event.notes}
|
||||
</span>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-4">
|
||||
<button
|
||||
onClick={() => handleEdit(event)}
|
||||
className="text-indigo-600 hover:text-indigo-900 mr-3"
|
||||
className="flex-1 text-center py-2 px-4 border border-indigo-300 text-indigo-600 hover:bg-indigo-50 rounded-md text-sm font-medium"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(event)}
|
||||
className="text-red-600 hover:text-red-900"
|
||||
className="flex-1 text-center py-2 px-4 border border-red-300 text-red-600 hover:bg-red-50 rounded-md text-sm font-medium"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Items Popup */}
|
||||
{showItemsPopup && hoveredEvent && (
|
||||
<div
|
||||
className="items-popup fixed bg-white border rounded-lg shadow-lg p-4 z-50 max-w-md max-h-64 overflow-y-auto"
|
||||
style={{
|
||||
left: window.innerWidth < 768 ? '50%' : `${popupPosition.x}px`,
|
||||
top: window.innerWidth < 768 ? '50%' : `${popupPosition.y}px`,
|
||||
transform: window.innerWidth < 768 ? 'translate(-50%, -50%)' : 'none'
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h4 className="font-medium text-gray-900">Items Purchased</h4>
|
||||
<button
|
||||
onClick={() => setShowItemsPopup(false)}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{hoveredEvent.products.map((product, index) => (
|
||||
<div key={index} className="text-sm">
|
||||
<div className="font-medium text-gray-900">{product.name}</div>
|
||||
<div className="text-gray-600">
|
||||
{product.amount} × ${product.price.toFixed(2)} = ${(product.amount * product.price).toFixed(2)}
|
||||
{product.discount && <span className="ml-2 text-green-600">🏷️</span>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modals */}
|
||||
<ConfirmDeleteModal
|
||||
isOpen={!!deletingEvent}
|
||||
onClose={handleCloseDeleteModal}
|
||||
onConfirm={confirmDelete}
|
||||
title="Delete Shopping Event"
|
||||
message={`Are you sure you want to delete the shopping event at ${deletingEvent?.shop.name}? This action cannot be undone.`}
|
||||
isLoading={deleteLoading}
|
||||
/>
|
||||
|
||||
<AddShoppingEventModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={handleCloseModal}
|
||||
onEventAdded={handleEventAdded}
|
||||
editEvent={editingEvent}
|
||||
/>
|
||||
|
||||
<ConfirmDeleteModal
|
||||
isOpen={!!deletingEvent}
|
||||
onClose={handleCloseDeleteModal}
|
||||
onConfirm={confirmDelete}
|
||||
title="Delete Shopping Event"
|
||||
message={`Are you sure you want to delete this shopping event from ${deletingEvent?.shop.name}? This action cannot be undone.`}
|
||||
isLoading={deleteLoading}
|
||||
/>
|
||||
|
||||
{/* Items Popup */}
|
||||
{showItemsPopup && hoveredEvent && hoveredEvent.products.length > 0 && (
|
||||
<div
|
||||
className="items-popup fixed z-50 bg-white border border-gray-200 rounded-lg shadow-lg p-4 max-w-md"
|
||||
style={{
|
||||
left: `${popupPosition.x + 10}px`,
|
||||
top: `${popupPosition.y - 10}px`,
|
||||
maxHeight: '300px',
|
||||
overflowY: 'auto'
|
||||
}}
|
||||
onMouseEnter={() => setShowItemsPopup(true)}
|
||||
onMouseLeave={handleItemsLeave}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
{Object.entries(
|
||||
hoveredEvent.products.reduce((groups, product) => {
|
||||
const category = product.category?.name || 'Unknown';
|
||||
if (!groups[category]) {
|
||||
groups[category] = [];
|
||||
}
|
||||
groups[category].push(product);
|
||||
return groups;
|
||||
}, {} as Record<string, typeof hoveredEvent.products>)
|
||||
)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([category, categoryProducts]) => (
|
||||
<div key={category} className="mb-3 last:mb-0">
|
||||
<div className="text-xs font-semibold text-gray-600 uppercase tracking-wide mb-1 border-b border-gray-300 pb-1">
|
||||
{category}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{categoryProducts.map((product, index) => (
|
||||
<div key={index} className="text-sm text-gray-900 pl-2">
|
||||
{product.name} {product.organic ? '🌱' : ''} {product.weight ? `${product.weight}${product.weight_unit}` : product.weight_unit}
|
||||
<span className="text-xs text-gray-600 ml-2">
|
||||
{product.amount} × ${product.price.toFixed(2)} = ${(product.amount * product.price).toFixed(2)}
|
||||
{product.discount && <span className="ml-1 text-green-600">🏷️</span>}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
26
frontend/src/hooks/useIsMobile.ts
Normal file
26
frontend/src/hooks/useIsMobile.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export const useIsMobile = (breakpoint: number = 768): boolean => {
|
||||
const [isMobile, setIsMobile] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkIsMobile = () => {
|
||||
setIsMobile(window.innerWidth < breakpoint);
|
||||
};
|
||||
|
||||
// Check on mount
|
||||
checkIsMobile();
|
||||
|
||||
// Add event listener
|
||||
window.addEventListener('resize', checkIsMobile);
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
window.removeEventListener('resize', checkIsMobile);
|
||||
};
|
||||
}, [breakpoint]);
|
||||
|
||||
return isMobile;
|
||||
};
|
||||
|
||||
export default useIsMobile;
|
||||
Loading…
x
Reference in New Issue
Block a user