From 057c8885d68eec8856bd63b5d7d217a06ef1b4e5 Mon Sep 17 00:00:00 2001 From: BFlat Date: Thu, 27 Jun 2024 03:43:17 +0700 Subject: [PATCH] new percentage strength slider update with number input (#540) - new custom reusable components: number input and slider - better looking slider UI - strength slider now shows percentage visually, but it actually returns value 0.0-1.0 - users can input numbers from 0-100, arrow buttons increment/decrement the value ![image](https://github.com/storytold/GottaGoFast/assets/46063141/43b915f4-1f75-4784-86cd-fc60a5bf8c0d) --- .../reusable/NumberInput/NumberInput.tsx | 86 ++++++++++++ app/components/reusable/NumberInput/index.ts | 1 + app/components/reusable/Slider/Slider.tsx | 130 ++++++++++++++++++ app/components/reusable/Slider/index.ts | 1 + app/components/reusable/index.ts | 2 + .../itemTabs/StylizeTab/StyleButtons.tsx | 50 ++++--- 6 files changed, 244 insertions(+), 26 deletions(-) create mode 100644 app/components/reusable/NumberInput/NumberInput.tsx create mode 100644 app/components/reusable/NumberInput/index.ts create mode 100644 app/components/reusable/Slider/Slider.tsx create mode 100644 app/components/reusable/Slider/index.ts diff --git a/app/components/reusable/NumberInput/NumberInput.tsx b/app/components/reusable/NumberInput/NumberInput.tsx new file mode 100644 index 0000000000..bbd2011ae2 --- /dev/null +++ b/app/components/reusable/NumberInput/NumberInput.tsx @@ -0,0 +1,86 @@ +import React, { useEffect, useState } from "react"; +import { Input } from "../Input"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faChevronDown, faChevronUp } from "@fortawesome/pro-solid-svg-icons"; +import { twMerge } from "tailwind-merge"; + +interface NumberInputProps { + value: number; + onChange: (value: number) => void; +} + +export const NumberInput: React.FC = ({ + value, + onChange, +}) => { + const [inputValue, setInputValue] = useState(value.toString()); + + useEffect(() => { + setInputValue(Math.round(value * 100) / 100 + ""); + }, [value]); + + const handleChange = (event: React.ChangeEvent) => { + const newValue = event.target.value; + if ( + newValue === "" || + (!isNaN(Number(newValue)) && Number(newValue) <= 100) + ) { + setInputValue(newValue); + const numericValue = parseFloat(newValue); + if (newValue !== "") { + onChange(Math.round(numericValue * 100) / 100); + } + } + }; + + const handleBlur = () => { + if (inputValue === "") { + setInputValue(Math.round(value * 100) / 100 + ""); + } + }; + + const handleIncrement = () => { + const newValue = Math.min(Number(inputValue) + 1, 100); + setInputValue(Math.round(newValue * 100) / 100 + ""); + onChange(Math.round(newValue * 100) / 100); + }; + + const handleDecrement = () => { + const newValue = Math.max(Number(inputValue) - 1, 0); + setInputValue(Math.round(newValue * 100) / 100 + ""); + onChange(Math.round(newValue * 100) / 100); + }; + + const incrementButtonStyle = + "flex h-[15px] w-6 items-center justify-center text-[10px] text-white/80 bg-ui-controls hover:bg-ui-controls-button outline-none focus:outline-none transition-all active:outline-none"; + + return ( +
+ + +
+ + +
+
+ ); +}; diff --git a/app/components/reusable/NumberInput/index.ts b/app/components/reusable/NumberInput/index.ts new file mode 100644 index 0000000000..967c67e426 --- /dev/null +++ b/app/components/reusable/NumberInput/index.ts @@ -0,0 +1 @@ +export * from "./NumberInput"; diff --git a/app/components/reusable/Slider/Slider.tsx b/app/components/reusable/Slider/Slider.tsx new file mode 100644 index 0000000000..adeb63c6a7 --- /dev/null +++ b/app/components/reusable/Slider/Slider.tsx @@ -0,0 +1,130 @@ +import { Transition } from "@headlessui/react"; +import React, { useState, useRef, useEffect, ReactNode } from "react"; +import { twMerge } from "tailwind-merge"; + +interface SliderProps { + min: number; + max: number; + value: number; + onChange: (value: number) => void; + step: number; + className?: string; + showTooltip?: boolean; + tooltipContent?: ReactNode; + suffix?: string; +} + +export const Slider: React.FC = ({ + min, + max, + value, + onChange, + step, + className, + showTooltip, + tooltipContent, + suffix, +}) => { + const [localValue, setLocalValue] = useState(value); + const sliderRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); + const [showHandleTooltip, setShowHandleTooltip] = useState(false); + + useEffect(() => { + setLocalValue(value); + }, [value]); + + const updatePosition = (x: number) => { + if (!sliderRef.current) return; + const rect = sliderRef.current.getBoundingClientRect(); + const fraction = (x - rect.left) / rect.width; + const newValue = fraction * (max - min) + min; + const roundedValue = Math.round(newValue / step) * step; + const clampedValue = Math.min(Math.max(roundedValue, min), max); + setLocalValue(clampedValue); + onChange(clampedValue); + }; + + const handleMouseDown = (e: React.MouseEvent) => { + e.preventDefault(); + setIsDragging(true); + setShowHandleTooltip(true); + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + updatePosition(e.clientX); + }; + + const handleMouseMove = (e: MouseEvent) => { + e.preventDefault(); + updatePosition(e.clientX); + }; + + const handleMouseUp = (e: MouseEvent) => { + e.preventDefault(); + setIsDragging(false); + setShowHandleTooltip(false); + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + + const handleMouseEnter = () => { + if (!isDragging) { + setShowHandleTooltip(true); + } + }; + + const handleMouseLeave = () => { + if (!isDragging) { + setShowHandleTooltip(false); + } + }; + + const percentage = ((localValue - min) / (max - min)) * 100; + const displayValue = + localValue.toFixed(step.toString().split(".")[1]?.length || 0) + + (suffix || ""); + + return ( +
+
+
+ + {showTooltip && showHandleTooltip && ( +
+ {tooltipContent ? tooltipContent : displayValue} +
+ )} +
+
+ ); +}; diff --git a/app/components/reusable/Slider/index.ts b/app/components/reusable/Slider/index.ts new file mode 100644 index 0000000000..a58909197c --- /dev/null +++ b/app/components/reusable/Slider/index.ts @@ -0,0 +1 @@ +export * from "./Slider"; diff --git a/app/components/reusable/index.ts b/app/components/reusable/index.ts index 9ee54ddfcb..ca7752ff27 100644 --- a/app/components/reusable/index.ts +++ b/app/components/reusable/index.ts @@ -14,9 +14,11 @@ export * from "./Input"; //need to decouple hotkey signal export * from "./InputVector"; //need to decouple hotkey signal export * from "./ListDropdown"; export * from "./LoadingBar"; +export * from "./NumberInput"; export * from "./Pagination"; export * from "./Pill"; export * from "./Select"; //Select's onChange type should be generic +export * from "./Slider"; export * from "./Tabs"; export * from "./Textarea"; export * from "./Tooltip"; diff --git a/app/pages/PageEnigma/comps/SidePanelTabs/itemTabs/StylizeTab/StyleButtons.tsx b/app/pages/PageEnigma/comps/SidePanelTabs/itemTabs/StylizeTab/StyleButtons.tsx index 56c295c668..6891d56863 100644 --- a/app/pages/PageEnigma/comps/SidePanelTabs/itemTabs/StylizeTab/StyleButtons.tsx +++ b/app/pages/PageEnigma/comps/SidePanelTabs/itemTabs/StylizeTab/StyleButtons.tsx @@ -1,4 +1,4 @@ -import { Button, Label, PremiumLock } from "~/components"; +import { Button, Label, NumberInput, PremiumLock, Slider } from "~/components"; import { faArrowsRotate, faChevronLeft, @@ -10,6 +10,8 @@ import Queue from "~/pages/PageEnigma/Queue/Queue"; import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames"; import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions"; import { Switch } from "@headlessui/react"; +// import Slider from "rc-slider"; +// import "rc-slider/assets/index.css"; import { faceDetail, @@ -22,8 +24,12 @@ import { export function StyleButtons() { useSignals(); - const sliderChanged = (event: React.ChangeEvent) => { - styleStrength.value = parseFloat(event.target.value); + const sliderChanged = (value: number | number[]) => { + styleStrength.value = (value as number) / 100; + }; + + const handleNumberInputChange = (value: number) => { + styleStrength.value = value / 100; }; const switchPreview = async () => { @@ -202,35 +208,27 @@ export function StyleButtons() {
-
+
(The higher the value the more the style will be applied, the lower the value the closer to source.)
-
-
- {styleStrength.value} -
- + +