← back to Watches
src/components/RefreshButton.jsx
179 lines
import React, { useState, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
/**
* RefreshButton - Manual refresh button with loading state
* @param {function} onRefresh - Callback function to trigger refresh (should return a Promise)
* @param {boolean} disabled - External disabled state
* @param {string} className - Additional CSS classes
* @param {string} size - Button size: 'sm', 'md', 'lg'
*/
export default function RefreshButton({
onRefresh,
disabled = false,
className = '',
size = 'md'
}) {
const [isLoading, setIsLoading] = useState(false);
const [showSuccess, setShowSuccess] = useState(false);
const [showError, setShowError] = useState(false);
const sizeClasses = {
sm: 'w-6 h-6 p-1',
md: 'w-8 h-8 p-1.5',
lg: 'w-10 h-10 p-2'
};
const iconSizes = {
sm: 'w-4 h-4',
md: 'w-5 h-5',
lg: 'w-6 h-6'
};
const handleClick = useCallback(async () => {
if (isLoading || disabled) return;
setIsLoading(true);
setShowSuccess(false);
setShowError(false);
try {
if (onRefresh) {
await onRefresh();
}
setShowSuccess(true);
setTimeout(() => setShowSuccess(false), 2000);
} catch (error) {
console.error('Refresh failed:', error);
setShowError(true);
setTimeout(() => setShowError(false), 3000);
} finally {
setIsLoading(false);
}
}, [onRefresh, isLoading, disabled]);
const isDisabled = isLoading || disabled;
return (
<div className={`relative inline-flex items-center ${className}`}>
<motion.button
onClick={handleClick}
disabled={isDisabled}
className={`
${sizeClasses[size]}
rounded-full
bg-gray-800/50 hover:bg-gray-700/50
border border-gray-600/50 hover:border-amber-500/50
text-gray-300 hover:text-amber-400
transition-all duration-200
disabled:opacity-50 disabled:cursor-not-allowed
focus:outline-none focus:ring-2 focus:ring-amber-500/50
backdrop-blur-sm
`}
whileHover={{ scale: isDisabled ? 1 : 1.05 }}
whileTap={{ scale: isDisabled ? 1 : 0.95 }}
title="Refresh market data"
>
<AnimatePresence mode="wait">
{isLoading ? (
<motion.svg
key="loading"
className={`${iconSizes[size]} animate-spin`}
initial={{ opacity: 0, rotate: 0 }}
animate={{ opacity: 1, rotate: 360 }}
exit={{ opacity: 0 }}
transition={{ rotate: { repeat: Infinity, duration: 1, ease: 'linear' } }}
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</motion.svg>
) : showSuccess ? (
<motion.svg
key="success"
className={`${iconSizes[size]} text-green-400`}
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0 }}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</motion.svg>
) : showError ? (
<motion.svg
key="error"
className={`${iconSizes[size]} text-red-400`}
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0 }}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</motion.svg>
) : (
<motion.svg
key="refresh"
className={iconSizes[size]}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</motion.svg>
)}
</AnimatePresence>
</motion.button>
{/* Toast notifications */}
<AnimatePresence>
{showSuccess && (
<motion.div
initial={{ opacity: 0, x: 10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 10 }}
className="absolute left-full ml-2 px-2 py-1 bg-green-900/80 text-green-300 text-xs rounded whitespace-nowrap"
>
Updated!
</motion.div>
)}
{showError && (
<motion.div
initial={{ opacity: 0, x: 10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 10 }}
className="absolute left-full ml-2 px-2 py-1 bg-red-900/80 text-red-300 text-xs rounded whitespace-nowrap"
>
Refresh failed
</motion.div>
)}
</AnimatePresence>
</div>
);
}