mirror of
https://github.com/clucraft/PriceGhost.git
synced 2026-05-15 10:52:36 +02:00
Add progress bar and countdown timer to product cards
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
e8123b27dc
commit
d09850d84e
1 changed files with 120 additions and 1 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Product } from '../api/client';
|
import { Product } from '../api/client';
|
||||||
import Sparkline from './Sparkline';
|
import Sparkline from './Sparkline';
|
||||||
|
|
@ -14,6 +14,57 @@ interface ProductCardProps {
|
||||||
|
|
||||||
export default function ProductCard({ product, onDelete, onRefresh, isSelected, onSelect, showCheckbox }: ProductCardProps) {
|
export default function ProductCard({ product, onDelete, onRefresh, isSelected, onSelect, showCheckbox }: ProductCardProps) {
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
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 () => {
|
const handleRefresh = async () => {
|
||||||
setIsRefreshing(true);
|
setIsRefreshing(true);
|
||||||
|
|
@ -67,14 +118,17 @@ export default function ProductCard({ product, onDelete, onRefresh, isSelected,
|
||||||
<div className={`product-list-item ${isOutOfStock ? 'out-of-stock' : ''} ${isSelected ? 'selected' : ''}`}>
|
<div className={`product-list-item ${isOutOfStock ? 'out-of-stock' : ''} ${isSelected ? 'selected' : ''}`}>
|
||||||
<style>{`
|
<style>{`
|
||||||
.product-list-item {
|
.product-list-item {
|
||||||
|
position: relative;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
padding-bottom: 1.25rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
transition: box-shadow 0.2s, transform 0.2s;
|
transition: box-shadow 0.2s, transform 0.2s;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-list-item:hover {
|
.product-list-item:hover {
|
||||||
|
|
@ -324,6 +378,63 @@ export default function ProductCard({ product, onDelete, onRefresh, isSelected,
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-progress-container {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: var(--border);
|
||||||
|
border-radius: 0 0 0.75rem 0.75rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #3b82f6, #06b6d4, #10b981);
|
||||||
|
border-radius: 0 0 0 0.75rem;
|
||||||
|
transition: width 0.3s ease-out;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-progress-bar::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: -2px;
|
||||||
|
bottom: -2px;
|
||||||
|
width: 20px;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.6));
|
||||||
|
filter: blur(3px);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-progress-bar.complete {
|
||||||
|
animation: progress-pulse 0.75s ease-in-out 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes progress-pulse {
|
||||||
|
0%, 100% {
|
||||||
|
box-shadow: 0 0 5px rgba(16, 185, 129, 0.5);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 15px rgba(16, 185, 129, 0.8), 0 0 30px rgba(16, 185, 129, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-time-remaining {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 8px;
|
||||||
|
right: 8px;
|
||||||
|
font-size: 0.625rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
background: var(--surface);
|
||||||
|
padding: 0.125rem 0.375rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
|
|
||||||
{showCheckbox && (
|
{showCheckbox && (
|
||||||
|
|
@ -460,6 +571,14 @@ export default function ProductCard({ product, onDelete, onRefresh, isSelected,
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="product-progress-container">
|
||||||
|
<div
|
||||||
|
className={`product-progress-bar ${isComplete ? 'complete' : ''}`}
|
||||||
|
style={{ width: `${progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="product-time-remaining">{timeRemaining}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue