Files
flyer-crawler.projectium.com/components/WatchedItemsList.tsx

186 lines
9.4 KiB
TypeScript

import React, { useState, useMemo } from 'react';
import type { MasterGroceryItem } from '../types';
import { EyeIcon } from './icons/EyeIcon';
import { LoadingSpinner } from './LoadingSpinner';
import { SortAscIcon } from './icons/SortAscIcon';
import { SortDescIcon } from './icons/SortDescIcon';
import { CATEGORIES } from '../types';
import { Session } from '@supabase/supabase-js';
import { TrashIcon } from './icons/TrashIcon';
import { UserIcon } from './icons/UserIcon';
import { PlusCircleIcon } from './icons/PlusCircleIcon';
interface WatchedItemsListProps {
items: MasterGroceryItem[];
onAddItem: (itemName: string, category: string) => Promise<void>;
onRemoveItem: (masterItemId: number) => Promise<void>;
session: Session | null;
activeListId: number | null;
onAddItemToList: (masterItemId: number) => void;
}
export const WatchedItemsList: React.FC<WatchedItemsListProps> = ({ items, onAddItem, onRemoveItem, session, activeListId, onAddItemToList }) => {
const [newItemName, setNewItemName] = useState('');
const [newCategory, setNewCategory] = useState('');
const [isAdding, setIsAdding] = useState(false);
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
const [categoryFilter, setCategoryFilter] = useState('all');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!newItemName.trim() || !newCategory) return;
setIsAdding(true);
try {
await onAddItem(newItemName, newCategory);
setNewItemName('');
setNewCategory('');
} catch (error) {
// Error is handled in the parent component
console.error(error);
} finally {
setIsAdding(false);
}
};
const handleSortToggle = () => {
setSortOrder(prev => (prev === 'asc' ? 'desc' : 'asc'));
};
const availableCategories = useMemo(() => {
const cats = new Set(items.map(i => i.category_name).filter((c): c is string => !!c));
return Array.from(cats).sort();
}, [items]);
const sortedAndFilteredItems = useMemo(() => {
const filteredItems = categoryFilter === 'all'
? items
: items.filter(item => item.category_name === categoryFilter);
return [...filteredItems].sort((a, b) => {
if (sortOrder === 'asc') {
return a.name.localeCompare(b.name);
} else {
return b.name.localeCompare(a.name);
}
});
}, [items, sortOrder, categoryFilter]);
if (!session) {
return (
<div className="bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 p-4 text-center">
<div className="flex flex-col items-center justify-center h-full min-h-[150px]">
<UserIcon className="w-10 h-10 text-gray-400 mb-3" />
<h4 className="font-semibold text-gray-700 dark:text-gray-300">Personalize Your Deals</h4>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
Please log in to create and manage your personal watchlist.
</p>
</div>
</div>
);
}
return (
<div className="bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<div className="flex justify-between items-center mb-3">
<h3 className="text-lg font-bold text-gray-800 dark:text-white flex items-center">
<EyeIcon className="w-6 h-6 mr-2 text-brand-primary" />
Your Watched Items
</h3>
<div className="flex items-center space-x-2">
{items.length > 0 && (
<select
value={categoryFilter}
onChange={(e) => setCategoryFilter(e.target.value)}
className="block w-full pl-3 pr-8 py-1.5 text-xs bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-brand-primary focus:border-brand-primary"
aria-label="Filter by category"
>
<option value="all">All Categories</option>
{availableCategories.map(cat => <option key={cat} value={cat}>{cat}</option>)}
</select>
)}
{items.length > 1 && (
<button
onClick={handleSortToggle}
className="p-1.5 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-500 dark:text-gray-400 transition-colors"
aria-label={`Sort items ${sortOrder === 'asc' ? 'descending' : 'ascending'}`}
title={`Sort ${sortOrder === 'asc' ? 'Z-A' : 'A-Z'}`}
>
{sortOrder === 'asc' ?
<SortAscIcon className="w-5 h-5" /> :
<SortDescIcon className="w-5 h-5" />
}
</button>
)}
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-2 mb-4">
<input
type="text"
value={newItemName}
onChange={(e) => setNewItemName(e.target.value)}
placeholder="Add item (e.g., Avocados)"
className="flex-grow block w-full px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-brand-primary focus:border-brand-primary sm:text-sm"
disabled={isAdding}
/>
<div className="grid grid-cols-3 gap-2">
<select
value={newCategory}
onChange={(e) => setNewCategory(e.target.value)}
required
className="col-span-2 block w-full px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-brand-primary focus:border-brand-primary sm:text-sm"
disabled={isAdding}
>
<option value="" disabled>Select a category</option>
{CATEGORIES.map(cat => <option key={cat} value={cat}>{cat}</option>)}
</select>
<button
type="submit"
disabled={isAdding || !newItemName.trim() || !newCategory}
className="col-span-1 bg-brand-secondary hover:bg-brand-dark disabled:bg-gray-400 disabled:cursor-not-allowed text-white font-bold py-2 px-3 rounded-lg transition-colors duration-300 flex items-center justify-center"
>
{isAdding ? <div className="w-5 h-5"><LoadingSpinner /></div> : 'Add'}
</button>
</div>
</form>
{sortedAndFilteredItems.length > 0 ? (
<ul className="space-y-2 max-h-60 overflow-y-auto">
{sortedAndFilteredItems.map(item => (
<li key={item.id} className="group text-sm bg-gray-50 dark:bg-gray-800 p-2 rounded text-gray-700 dark:text-gray-300 flex justify-between items-center">
<div className="flex-grow">
<span>{item.name}</span>
<span className="text-xs text-gray-500 dark:text-gray-400 italic ml-2">{item.category_name}</span>
</div>
<div className="flex items-center flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={() => onAddItemToList(item.id)}
disabled={!activeListId}
className="p-1 text-gray-400 hover:text-brand-primary disabled:text-gray-300 disabled:cursor-not-allowed"
title={activeListId ? `Add ${item.name} to list` : 'Select a shopping list first'}
>
<PlusCircleIcon className="w-4 h-4" />
</button>
<button
onClick={() => onRemoveItem(item.id)}
className="text-red-500 hover:text-red-700 dark:hover:text-red-400 p-1"
aria-label={`Remove ${item.name}`}
title={`Remove ${item.name}`}
>
<TrashIcon className="w-4 h-4" />
</button>
</div>
</li>
))}
</ul>
) : (
<p className="p-4 text-center text-sm text-gray-500 dark:text-gray-400">
{categoryFilter === 'all'
? 'Your watchlist is empty. Add items above to start tracking prices.'
: `No watched items in the "${categoryFilter}" category.`}
</p>
)}
</div>
);
};