mirror of
https://github.com/clucraft/PriceGhost.git
synced 2026-05-15 10:52:36 +02:00
Add target price alerts, historical low indicator, bulk actions, and dashboard summary
Features: - Target price alerts: Set a specific price target and get notified when reached - Historical low indicator: Badge showing when current price is at/near all-time low - Bulk actions: Select multiple products to delete at once - Dashboard summary: Shows total products, items at lowest price, at target, biggest drops Backend changes: - Add target_price column to products table - Add target_price notification type with Telegram/Discord support - Include min_price in product queries for historical low detection - Update scheduler to check target price conditions Frontend changes: - Add target price input to ProductDetail notification settings - Show target price badge on product cards - Add "Lowest Price" and "Near Low" badges to product cards - Add bulk selection mode with checkboxes - Add dashboard summary cards at top of product list Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
2acc47c21c
commit
a85e22d8bc
9 changed files with 454 additions and 5 deletions
|
|
@ -7,9 +7,12 @@ interface ProductCardProps {
|
|||
product: Product;
|
||||
onDelete: (id: number) => void;
|
||||
onRefresh: (id: number) => Promise<void>;
|
||||
isSelected?: boolean;
|
||||
onSelect?: (id: number, selected: boolean) => void;
|
||||
showCheckbox?: boolean;
|
||||
}
|
||||
|
||||
export default function ProductCard({ product, onDelete, onRefresh }: ProductCardProps) {
|
||||
export default function ProductCard({ product, onDelete, onRefresh, isSelected, onSelect, showCheckbox }: ProductCardProps) {
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
|
||||
const handleRefresh = async () => {
|
||||
|
|
@ -54,8 +57,14 @@ export default function ProductCard({ product, onDelete, onRefresh }: ProductCar
|
|||
|
||||
const isOutOfStock = product.stock_status === 'out_of_stock';
|
||||
|
||||
// Check if current price is at or near historical low
|
||||
const isHistoricalLow = product.current_price && product.min_price &&
|
||||
product.current_price <= product.min_price;
|
||||
const isNearHistoricalLow = !isHistoricalLow && product.current_price && product.min_price &&
|
||||
product.current_price <= product.min_price * 1.05; // Within 5% of low
|
||||
|
||||
return (
|
||||
<div className={`product-list-item ${isOutOfStock ? 'out-of-stock' : ''}`}>
|
||||
<div className={`product-list-item ${isOutOfStock ? 'out-of-stock' : ''} ${isSelected ? 'selected' : ''}`}>
|
||||
<style>{`
|
||||
.product-list-item {
|
||||
background: var(--surface);
|
||||
|
|
@ -73,6 +82,19 @@ export default function ProductCard({ product, onDelete, onRefresh }: ProductCar
|
|||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.product-list-item.selected {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.product-checkbox {
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
accent-color: var(--primary);
|
||||
}
|
||||
|
||||
.product-thumbnail {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
|
|
@ -199,6 +221,26 @@ export default function ProductCard({ product, onDelete, onRefresh }: ProductCar
|
|||
color: #f87171;
|
||||
}
|
||||
|
||||
.product-stock-badge.historical-low {
|
||||
background: #ecfdf5;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .product-stock-badge.historical-low {
|
||||
background: rgba(5, 150, 105, 0.2);
|
||||
color: #34d399;
|
||||
}
|
||||
|
||||
.product-stock-badge.near-low {
|
||||
background: #fef3c7;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .product-stock-badge.near-low {
|
||||
background: rgba(217, 119, 6, 0.2);
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.product-list-item.out-of-stock {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
|
@ -284,6 +326,15 @@ export default function ProductCard({ product, onDelete, onRefresh }: ProductCar
|
|||
}
|
||||
`}</style>
|
||||
|
||||
{showCheckbox && (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="product-checkbox"
|
||||
checked={isSelected}
|
||||
onChange={(e) => onSelect?.(product.id, e.target.checked)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{product.image_url ? (
|
||||
<img
|
||||
src={product.image_url}
|
||||
|
|
@ -297,7 +348,7 @@ export default function ProductCard({ product, onDelete, onRefresh }: ProductCar
|
|||
<div className="product-info">
|
||||
<h3 className="product-name">{product.name || 'Unknown Product'}</h3>
|
||||
<p className="product-source">{truncateUrl(product.url)}</p>
|
||||
{(product.price_drop_threshold || product.notify_back_in_stock) && (
|
||||
{(product.price_drop_threshold || product.target_price || product.notify_back_in_stock) && (
|
||||
<div className="product-notifications">
|
||||
{product.price_drop_threshold && (
|
||||
<span className="product-notification-badge" title="Price drop alert">
|
||||
|
|
@ -308,6 +359,16 @@ export default function ProductCard({ product, onDelete, onRefresh }: ProductCar
|
|||
${Number(product.price_drop_threshold).toFixed(2)} drop
|
||||
</span>
|
||||
)}
|
||||
{product.target_price && (
|
||||
<span className="product-notification-badge" title="Target price alert">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<circle cx="12" cy="12" r="6" />
|
||||
<circle cx="12" cy="12" r="2" />
|
||||
</svg>
|
||||
Target: ${Number(product.target_price).toFixed(2)}
|
||||
</span>
|
||||
)}
|
||||
{product.notify_back_in_stock && (
|
||||
<span className="product-notification-badge" title="Back in stock alert">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
|
|
@ -336,6 +397,16 @@ export default function ProductCard({ product, onDelete, onRefresh }: ProductCar
|
|||
{formatPriceChange(product.price_change_7d)} (7d)
|
||||
</span>
|
||||
)}
|
||||
{isHistoricalLow && (
|
||||
<span className="product-stock-badge historical-low">
|
||||
Lowest Price
|
||||
</span>
|
||||
)}
|
||||
{isNearHistoricalLow && (
|
||||
<span className="product-stock-badge near-low">
|
||||
Near Low
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue