import variables from 'config/variables'; import { useState, useCallback, memo, useMemo } from 'react'; import { toast } from 'react-toastify'; import ReactSlider from 'react-slider'; import { MdRefresh, MdEdit } from 'react-icons/md'; import EventBus from 'utils/eventbus'; import clsx from 'clsx'; // Style definitions split into logical groups for better readability const buttonStyles = { base: 'inline-flex items-center text-sm px-3 py-1.5 rounded-md font-medium', colors: 'bg-neutral-200 hover:bg-neutral-300 active:bg-neutral-400', darkMode: 'dark:bg-white/10 dark:hover:bg-white/15 dark:active:bg-white/20', text: 'text-neutral-800 dark:text-white', transitions: 'transition-colors duration-200', }; const inputStyles = { base: 'w-20 text-sm px-3 py-1.5 rounded-md font-medium text-center', colors: 'bg-neutral-200 hover:bg-neutral-300 focus:bg-neutral-300', darkMode: 'dark:bg-white/10 dark:hover:bg-white/15 dark:focus:bg-white/15', text: 'text-neutral-800 dark:text-white', focus: 'focus:outline-none focus:ring-2 focus:ring-neutral-400 dark:focus:ring-white/40', transitions: 'transition-colors duration-200', }; const thumbStyles = { base: [ 'w-5 h-5 rounded-full cursor-pointer', 'absolute top-1/2 transform -translate-y-1/2', 'flex items-center justify-center', ], colors: 'bg-gradient-to-br from-neutral-50 to-neutral-200 dark:from-white dark:to-neutral-100', interactions: [ 'hover:from-neutral-100 hover:to-neutral-300', 'dark:hover:from-white dark:hover:to-neutral-200', 'focus:outline-none focus:ring-2 focus:ring-neutral-400 dark:focus:ring-white/40', 'active:scale-95', ], effects: 'shadow-lg shadow-black/10 dark:shadow-black/25 transition-all duration-200', tooltip: [ 'before:content-[attr(aria-valuenow)]', 'before:absolute before:top-[-28px]', 'before:text-xs before:bg-neutral-800 dark:before:bg-black/90', 'before:text-white before:px-2 before:py-1 before:rounded-md', 'before:opacity-0 hover:before:opacity-100', 'before:transition-all before:duration-200 before:whitespace-nowrap', 'before:shadow-lg', ], }; const markStyles = { base: [ 'h-3 w-1.5 rounded-full cursor-pointer select-none', 'absolute top-1/2 transform -translate-y-1/2', ], colors: 'bg-neutral-400 dark:bg-white/30', hover: 'hover:bg-neutral-600 dark:hover:bg-white/50 hover:scale-110', transitions: 'hover:transition-transform hover:duration-200', label: [ 'after:content-[attr(data-value)]', 'after:absolute after:top-5', 'after:text-xs after:opacity-85', 'after:transform after:-translate-x-1/2', 'after:whitespace-nowrap after:pointer-events-none', 'after:font-medium', 'after:text-neutral-700 dark:after:text-white/85', ], }; const MULTIPLIER_MARKS = { 10: '0.1x', 100: '1x', 200: '2x', 400: '4x', }; // Memoized value display component const ValueDisplay = memo(({ value, display, onChange }) => { const [isEditing, setIsEditing] = useState(false); const [inputValue, setInputValue] = useState(value); const handleBlur = () => { setIsEditing(false); const newValue = Number(inputValue); if (!isNaN(newValue)) { onChange(newValue); } else { setInputValue(value); } }; const handleKeyDown = (e) => { if (e.key === 'Enter') { handleBlur(); } else if (e.key === 'Escape') { setIsEditing(false); setInputValue(value); } }; if (isEditing) { return ( setInputValue(e.target.value)} onBlur={handleBlur} onKeyDown={handleKeyDown} className={clsx( inputStyles.base, inputStyles.colors, inputStyles.darkMode, inputStyles.text, inputStyles.focus, inputStyles.transitions, )} autoFocus /> ); } return (
{value} {display}
); }); // Memoized reset button component const ResetButton = memo(({ onClick, defaultValue }) => ( )); function SliderComponent(props) { const [value, setValue] = useState(() => { try { const stored = localStorage.getItem(props.name); return stored ? Number(stored) : Number(props.default); } catch (e) { return Number(props.default); } }); const handleChange = useCallback( (newValue) => { if (typeof newValue !== 'number') return; const clampedValue = Math.min(Math.max(newValue, props.min), props.max); // Update state immediately for smooth UI setValue(clampedValue); // Batch heavy operations in requestAnimationFrame requestAnimationFrame(() => { try { localStorage.setItem(props.name, clampedValue); if (props.element && !document.querySelector(props.element)) { const reminderElement = document.querySelector('.reminder-info'); if (reminderElement) { reminderElement.style.display = 'flex'; localStorage.setItem('showReminder', true); return; } } EventBus.emit('refresh', props.category); } catch (e) { console.error('Error updating slider:', e); } }); }, [props.min, props.max, props.name, props.element, props.category], ); const resetItem = useCallback(() => { handleChange(Number(props.default)); toast(variables.getMessage('toasts.reset')); }, [props.default, handleChange]); const handleMarkClick = useCallback( (markValue) => { handleChange(markValue); }, [handleChange], ); // Memoized render functions to prevent unnecessary re-renders const renderThumb = useMemo( () => (thumbProps, state) => (
), [props.display], ); const renderMark = useMemo( () => (markProps) => { if (!MULTIPLIER_MARKS[markProps.key]) return null; return (
{ e.preventDefault(); e.stopPropagation(); handleMarkClick(markProps.key); }} onMouseDown={(e) => e.preventDefault()} data-value={MULTIPLIER_MARKS[markProps.key]} role="button" tabIndex={0} aria-label={`Set zoom to ${MULTIPLIER_MARKS[markProps.key]}`} className={`${markProps.className} cursor-pointer`} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleMarkClick(markProps.key); } }} /> ); }, [handleMarkClick], ); return (
{props.title}
); } // Export memoized version for better performance when parent components re-render export const Slider = memo(SliderComponent); export default Slider;