cyb/src/components/Slider/Slider.tsx

import cx from 'classnames';
import SliderComponent, { SliderProps as RcSliderProps } from 'rc-slider';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import imgSwap from 'src/image/exchange-arrows.svg';
import 'rc-slider/assets/index.css';
import styles from './Slider.module.scss';
import './styles.override.css';
import DenomArr from '../denom/denomArr';
import FormatNumberTokens from '../FormatNumberTokens/FormatNumberTokens';

function SpetionLabel({ value }: { value: number }) {
  let position = '';

  if (value <= 5) {
    position = 'left';
  } else if (value === 10) {
    position = 'center';
  } else {
    position = 'right';
  }
  return (
    // eslint-disable-next-line jsx-a11y/label-has-associated-control
    <label className={styles.trackLabel}>
      <div className={styles.trackMarkBgBlur} />
      <div className={cx(styles.trackMarkGradient, styles[`trackMarkGradient${position}`])} />

      <div
        className={cx(styles.trackMarkLabel, {
          [styles.trackMarkLabelColorBlue]: value === 100,
        })}
      >
        {value < 100 ? `${value}%` : 'Max'}
      </div>
    </label>
  );
}

function SphereValue({ angle, children }: { angle: number; children: React.ReactNode }) {
  return (
    <div className={cx(styles.debtAmountPosToken)} style={{ transform: `rotate(${angle}deg)` }}>
      <div
        className={styles.debtAmountPosTokenObj}
        style={{ transform: `rotate(${angle * -1}deg)` }}
      >
        {children}
      </div>
    </div>
  );
}

const SphereValueMemo = React.memo(SphereValue);

function SphereValueWithToken({
  tokenName,
  angle,
  price,
}: {
  tokenName: string;
  angle: number;
  price?: number;
}) {
  return (
    <SphereValueMemo angle={angle}>
      <DenomArr denomValue={tokenName} onlyImg tooltipStatusImg={false} />
      {price && (
        <div className={styles.imgValue}>
          <FormatNumberTokens value={price} />
        </div>
      )}
    </SphereValueMemo>
  );
}

type TokenPair = {
  tokenA: string;
  tokenB: string;
  priceA: number;
  priceB: number;
};

const angleDeg = 135;

const scaleMin = 1;
const scaleMax = 101;
const minlVal = Math.log(scaleMin);
const maxlVal = Math.log(scaleMax);

const scale = (maxlVal - minlVal) / scaleMax;

const scalePercents = [1, 2, 5, 10, 20, 50, 100];

function roundToOneDecimalPlace(num: number) {
  return Math.round(num * 10) / 10;
}
const positionToPercents = (position: number) =>
  position === 0 ? 0 : roundToOneDecimalPlace(Math.exp(minlVal + scale * position)) - 1;

const percentsToPosition = (percents: number) => (Math.log(percents + 1) - minlVal) / scale;

const scaleMarks = Object.fromEntries(
  scalePercents.map((percents) => [
    percentsToPosition(percents),
    <SpetionLabel key={`slider_mark_${percents}`} value={percents} />,
  ])
);

export type SliderProps = {
  onChange: (value: number) => void;
  onSwapClick?: () => void;
  valuePercents: number;
  disabled?: boolean;
  tokenPair?: TokenPair;
  text?: string | React.ReactNode;
};

function Slider({ onChange, onSwapClick, valuePercents, disabled, tokenPair, text }: SliderProps) {
  const [valueSilder, setValueSilder] = useState(0);
  const [currentPercents, setCurrentPercent] = useState(0);
  const [draggingMode, setDraggingMode] = useState(false);
  const [isHadleFocused, setIsHandleFocused] = useState(false);
  const draggingDetectorTimer = useRef(undefined);

  const finishDragging = useCallback(() => {
    clearTimeout(draggingDetectorTimer.current);
    if (!draggingMode) {
      onSwapClick?.();
    }

    setDraggingMode(false);
  }, [draggingMode, onSwapClick]);

  const startDragging = () => {
    draggingDetectorTimer.current = setTimeout(() => {
      clearTimeout(draggingDetectorTimer.current);
      setDraggingMode(true);
    }, 300);
  };

  useEffect(() => {
    const percents = valuePercents;

    if (percents > 100) {
      setCurrentPercent(100);
      setValueSilder(scaleMax);
      return;
    }
    if (percents <= 0) {
      setValueSilder(scaleMin);
      setCurrentPercent(0);
      return;
    }

    setCurrentPercent(percents);

    const position = percentsToPosition(percents);

    setValueSilder(position < 0 ? 0 : position);
  }, [valuePercents]);

  const onSliderChange = (position: number) => {
    // Hack to avoid glitch when click on handle
    if (isHadleFocused && !draggingMode) {
      return;
    }
    setDraggingMode(true);
    setValueSilder(position);
    requestAnimationFrame(() => {
      const value = positionToPercents(position);
      setCurrentPercent(value);
      onChange?.(value);
    });
  };

  const renderCustomHandle: RcSliderProps['handle'] = useCallback(
    ({ value }) => {
      const percents =
        currentPercents < 2 ? currentPercents.toFixed(1) : Math.round(currentPercents || 0);
      return (
        // eslint-disable-next-line jsx-a11y/no-static-element-interactions
        <div
          className={styles.debtAmountPos}
          style={{
            left: `${value - scaleMin}%`,
          }}
          onMouseEnter={() => setIsHandleFocused(true)}
          onMouseLeave={() => setIsHandleFocused(false)}
          onMouseDown={() => startDragging()}
          onTouchStart={() => startDragging()}
          onMouseUp={() => finishDragging()}
          onTouchEnd={() => finishDragging()}
        >
          <SphereValueMemo angle={90}>
            <div>{percents}%</div>
          </SphereValueMemo>
          {tokenPair && (
            <>
              <SphereValueWithToken
                tokenName={tokenPair.tokenA}
                angle={angleDeg}
                price={tokenPair.priceA}
              />
              <SphereValueWithToken
                tokenName={tokenPair.tokenB}
                angle={-angleDeg}
                price={tokenPair.priceB}
              />
            </>
          )}
          <button type="button" className={styles.buttonIcon}>
            <img src={imgSwap} alt="swap" draggable="false" />
          </button>
        </div>
      );
    },
    [tokenPair, currentPercents, finishDragging, startDragging]
  );

  return (
    <div className={styles.formWrapper}>
      <div className={styles.debtAmountSlider}>
        <div style={{ width: '100%', padding: '0 25px' }}>
          <SliderComponent
            disabled={disabled}
            value={valueSilder}
            min={scaleMin}
            max={scaleMax}
            step={1}
            handle={renderCustomHandle}
            onChange={(pos) => onSliderChange(pos)}
            marks={scaleMarks}
            trackStyle={{ backgroundColor: '#C5C5C5', height: '2px' }}
            railStyle={{ backgroundColor: '#C5C5C5', height: '2px' }}
            dotStyle={{
              background: 'none',
              border: 'none',
            }}
          />
          {text && (
            <div className={cx(styles.text, valueSilder > 80 && styles.textLeft)}>{text}</div>
          )}
        </div>
      </div>
    </div>
  );
}

export default Slider;

Synonyms

bostrom.network/src/components/ui/slider.tsx
pussy-ts/src/components/Slider/Slider.tsx
cyb/src/containers/mint/components/Slider/Slider.tsx

Neighbours