From d09850d84edde2cccaf9b64f78c5c1c021a01a3b Mon Sep 17 00:00:00 2001 From: clucraft Date: Thu, 22 Jan 2026 08:08:15 -0500 Subject: [PATCH] Add progress bar and countdown timer to product cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Show time remaining until next refresh (e.g., "12m 45s") - Animated progress bar at bottom of card (blue → cyan → green gradient) - Glowing edge effect on the progress bar leading point - Pulse animation when progress reaches 100% Co-Authored-By: Claude Opus 4.5 --- frontend/src/components/ProductCard.tsx | 121 +++++++++++++++++++++++- 1 file changed, 120 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/ProductCard.tsx b/frontend/src/components/ProductCard.tsx index 494d3b6..28f21f2 100644 --- a/frontend/src/components/ProductCard.tsx +++ b/frontend/src/components/ProductCard.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { Link } from 'react-router-dom'; import { Product } from '../api/client'; import Sparkline from './Sparkline'; @@ -14,6 +14,57 @@ interface ProductCardProps { export default function ProductCard({ product, onDelete, onRefresh, isSelected, onSelect, showCheckbox }: ProductCardProps) { const [isRefreshing, setIsRefreshing] = useState(false); + const [progress, setProgress] = useState(0); + const [timeRemaining, setTimeRemaining] = useState(''); + const [isComplete, setIsComplete] = useState(false); + + // Calculate progress and time remaining + useEffect(() => { + const calculateProgress = () => { + if (!product.last_checked) { + setProgress(100); + setTimeRemaining('Soon'); + return; + } + + const lastChecked = new Date(product.last_checked).getTime(); + const intervalMs = product.refresh_interval * 1000; + const nextCheck = lastChecked + intervalMs; + const now = Date.now(); + const elapsed = now - lastChecked; + const remaining = nextCheck - now; + + const progressPercent = Math.min((elapsed / intervalMs) * 100, 100); + setProgress(progressPercent); + + // Trigger complete animation when reaching 100% + if (progressPercent >= 100 && !isComplete) { + setIsComplete(true); + setTimeout(() => setIsComplete(false), 1500); + } + + // Format time remaining + if (remaining <= 0) { + setTimeRemaining('Soon'); + } else { + const seconds = Math.floor(remaining / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + + if (hours > 0) { + setTimeRemaining(`${hours}h ${minutes % 60}m`); + } else if (minutes > 0) { + setTimeRemaining(`${minutes}m ${seconds % 60}s`); + } else { + setTimeRemaining(`${seconds}s`); + } + } + }; + + calculateProgress(); + const interval = setInterval(calculateProgress, 1000); + return () => clearInterval(interval); + }, [product.last_checked, product.refresh_interval, isComplete]); const handleRefresh = async () => { setIsRefreshing(true); @@ -67,14 +118,17 @@ export default function ProductCard({ product, onDelete, onRefresh, isSelected,
{showCheckbox && ( @@ -460,6 +571,14 @@ export default function ProductCard({ product, onDelete, onRefresh, isSelected,
+ +
+
+
+ {timeRemaining}
); }