From 287c2f1bd80c29bf58562e6a1c3e133f729c82ea Mon Sep 17 00:00:00 2001 From: Karla Oshikawa Date: Thu, 5 Dec 2024 02:44:58 +0900 Subject: [PATCH] fix: range filter --- components/search/FilterRange.tsx | 294 ++++++++++++------------------ components/search/Filters.tsx | 65 +++---- fresh.gen.ts | 2 + islands/RangeFilter.tsx | 142 +++++++++++++++ tailwind.css | 210 ++++++++++++++++++++- 5 files changed, 493 insertions(+), 220 deletions(-) create mode 100644 islands/RangeFilter.tsx diff --git a/components/search/FilterRange.tsx b/components/search/FilterRange.tsx index e607dbf..b33ca2b 100644 --- a/components/search/FilterRange.tsx +++ b/components/search/FilterRange.tsx @@ -1,195 +1,125 @@ -import { useEffect, useId, useRef } from "preact/hooks"; -import { RefObject } from "preact"; -import { useSignal } from "@preact/signals"; -import { formatPrice } from "site/sdk/format.ts"; - -function useDebounce( - // deno-lint-ignore no-explicit-any - func: (...args: any[]) => void, - timeout = 300, - // deno-lint-ignore no-explicit-any -): (...args: any[]) => void { - let timer: ReturnType; - return (...args) => { - clearTimeout(timer); - timer = setTimeout(() => { - func(...args); - }, timeout); - }; +import Avatar from "$store/components/ui/Avatar.tsx"; +import { formatPrice } from "$store/sdk/format.ts"; +import type { + Filter, + FilterToggle, + FilterToggleValue, + ProductListingPage, +} from "apps/commerce/types.ts"; +import { parseRange } from "apps/commerce/utils/filters.ts"; +import ClearFilters from "site/islands/ClearFilters.tsx"; +import RangeFilter from "site/islands/RangeFilter.tsx"; + +interface Props { + filters: ProductListingPage["filters"]; + /** @description O valor minimo esta definido em R$0,00 se quiser mudar o valor preencher o campo abaixo */ + min?: number; + /** @description O valor maximo esta definido em R$10.000,00 se quiser mudar o valor preencher o campo abaixo */ + max?: number; } -const thumbsize = 14; +const isToggle = (filter: Filter): filter is FilterToggle => + filter["@type"] === "FilterToggle"; -interface FilterRangeProps { - min: number; - max: number; - currentUrlFilterPrice?: string; - currentMaxFacet?: string; - currentMinFacet?: string; +function ValueItem({ url, selected, label, quantity }: FilterToggleValue) { + console.log(url); + return ( + <> + {quantity > 0 && ( + +
+ {label} + {quantity > 0 && ( + ({quantity}) + )} + + )} + + ); } -function applyFilterPrice( - { min, max, currentUrlFilterPrice }: FilterRangeProps, -) { - const searchParams = new URLSearchParams(currentUrlFilterPrice); - console.log("searchParams", searchParams); - searchParams.set("filter.price", `${min}:${max}`); - const newUrl = `${window.location.pathname}?${searchParams.toString()}`; +function FilterValues({ key, values }: FilterToggle) { + const flexDirection = + key === "tamanho" || key === "cor" ? "flex-row" : "flex-col"; - globalThis.location.href = newUrl; + return ( + <> +
    + {values.map((item) => { + const { url, selected, value } = item; + + if (key === "cor" || key === "tamanho") { + return ( + + + + ); + } + + if (key === "price") { + const range = parseRange(item.value); + + return ( + range && ( + <> + + + ) + ); + } + + return ; + })} +
+ + ); } -const debouncedApplyFilterPrice = useDebounce((arg) => applyFilterPrice(arg)); - -function FilterRange( - { - min: minValue, - max: maxValue, - currentUrlFilterPrice = "", - currentMinFacet, - currentMaxFacet, - }: FilterRangeProps, -) { - const id = useId(); - const slider: RefObject = useRef(null); - const min: RefObject = useRef(null); - const max: RefObject = useRef(null); - const rangemin = useSignal(Number(currentMinFacet)); - const rangemax = useSignal(Number(currentMaxFacet)); - - const avgvalueprimary = (rangemin.value + rangemax.value) / 2; - const dataValue = useSignal({ - min: minValue, - max: maxValue, - rangewitdh: 0, - }); - - function draw(splitvalue: number) { - if ( - min.current && - max.current && - slider.current && - !!dataValue.value.rangewitdh - ) { - min.current.setAttribute("max", `${splitvalue}`); - max.current.setAttribute("min", `${splitvalue}`); - - // Set css - min.current.style.width = `${ - Math.floor( - thumbsize + - ((splitvalue - minValue) / - (maxValue - minValue)) * - (dataValue.value.rangewitdh - 2 * thumbsize), - ) - }px`; - max.current.style.width = `${ - Math.floor( - thumbsize + - ((maxValue - splitvalue) / - (maxValue - minValue)) * - (dataValue.value.rangewitdh - 2 * thumbsize), - ) - }px`; - - min.current.style.left = "0px"; - max.current.style.left = min.current.style.width; - - slider.current.style.height = `${min.current.offsetHeight}px`; - - if (Number(max.current.value) > maxValue) { - max.current.setAttribute("data-value", `${dataValue.value.max}`); - } - - rangemin.value = Number(min.current.getAttribute("data-value")); - rangemax.value = Number(max.current.getAttribute("data-value")); - } - } - - function update(props: FilterRangeProps): void { - if (min.current && max.current) { - const minvalue = props.min; - const maxvalue = props.max; - - min.current.setAttribute("data-value", `${minvalue}`); - max.current.setAttribute("data-value", `${maxvalue}`); - - const avgvalue = (minvalue + maxvalue) / 2; - draw(Math.round(avgvalue)); - } - } - - function handleInput(props: FilterRangeProps) { - update(props); - debouncedApplyFilterPrice({ - min: rangemin.value, - max: rangemax.value, - currentUrlFilterPrice, - }); - } - - useEffect(() => { - if (slider.current) { - dataValue.value.rangewitdh = slider.current.offsetWidth; - draw(Math.round(avgvalueprimary)); - } - }, []); - +function Filters({ filters, min, max }: Props) { return ( -
-
- - ) => - handleInput({ - min: Math.round(Number(ev.currentTarget.value)), - max: rangemax.value, - })} - value={rangemin.value} - /> - - ) => - handleInput({ - max: Math.round(Number(ev.currentTarget.value)), - min: rangemin.value, - })} - value={Math.round(rangemax.value)} - /> +
    +
    + +
    + SELECIONADOS +
    +
    + {filters + .filter(isToggle) + .map((filter) => + filter.values.map( + (item) => item.selected && + ) + )} + +
    -
    - - {formatPrice(rangemin.value, "BRL")} - - - - - {formatPrice(rangemax.value, "BRL")} - -
    -
+ + {filters.filter(isToggle).map((filter) => + filter.label !== "Preço" ? ( +
+ +
+ {filter.label} +
+
+ +
+
+ ) : null + )} + + + ); } -export default FilterRange; +export default Filters; diff --git a/components/search/Filters.tsx b/components/search/Filters.tsx index 92893ea..b33ca2b 100644 --- a/components/search/Filters.tsx +++ b/components/search/Filters.tsx @@ -8,18 +8,21 @@ import type { } from "apps/commerce/types.ts"; import { parseRange } from "apps/commerce/utils/filters.ts"; import ClearFilters from "site/islands/ClearFilters.tsx"; -// import FilterRange from "site/components/search/FilterRange.tsx"; +import RangeFilter from "site/islands/RangeFilter.tsx"; interface Props { filters: ProductListingPage["filters"]; + /** @description O valor minimo esta definido em R$0,00 se quiser mudar o valor preencher o campo abaixo */ + min?: number; + /** @description O valor maximo esta definido em R$10.000,00 se quiser mudar o valor preencher o campo abaixo */ + max?: number; } const isToggle = (filter: Filter): filter is FilterToggle => filter["@type"] === "FilterToggle"; -function ValueItem( - { url, selected, label, quantity }: FilterToggleValue, -) { +function ValueItem({ url, selected, label, quantity }: FilterToggleValue) { + console.log(url); return ( <> {quantity > 0 && ( @@ -36,9 +39,8 @@ function ValueItem( } function FilterValues({ key, values }: FilterToggle) { - const flexDirection = key === "tamanho" || key === "cor" - ? "flex-row" - : "flex-col"; + const flexDirection = + key === "tamanho" || key === "cor" ? "flex-row" : "flex-col"; return ( <> @@ -60,15 +62,17 @@ function FilterValues({ key, values }: FilterToggle) { if (key === "price") { const range = parseRange(item.value); - return range && ( - <> - - + return ( + range && ( + <> + + + ) ); } @@ -79,7 +83,7 @@ function FilterValues({ key, values }: FilterToggle) { ); } -function Filters({ filters }: Props) { +function Filters({ filters, min, max }: Props) { return (
    @@ -88,19 +92,19 @@ function Filters({ filters }: Props) { SELECIONADOS
    - {filters.filter(isToggle) - .map((filter) => ( - filter.values.map((item) => ( - item.selected && - )) - ))} + {filters + .filter(isToggle) + .map((filter) => + filter.values.map( + (item) => item.selected && + ) + )}
- {filters - .filter(isToggle) - .map((filter) => ( + {filters.filter(isToggle).map((filter) => + filter.label !== "Preço" ? (
@@ -110,11 +114,10 @@ function Filters({ filters }: Props) {
- //
  • - // {filter.label} - // - //
  • - ))} + ) : null + )} + + ); } diff --git a/fresh.gen.ts b/fresh.gen.ts index 6e5cda3..6a8f3bf 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -26,6 +26,7 @@ import * as $Newsletter from "./islands/Newsletter.tsx"; import * as $OutOfStock from "./islands/OutOfStock.tsx"; import * as $ProductImageZoom from "./islands/ProductImageZoom.tsx"; import * as $ProductReview from "./islands/ProductReview.tsx"; +import * as $RangeFilter from "./islands/RangeFilter.tsx"; import * as $SearchControls from "./islands/SearchControls.tsx"; import * as $Shipping from "./islands/Shipping.tsx"; import * as $ShippingSimulation from "./islands/ShippingSimulation.tsx"; @@ -64,6 +65,7 @@ const manifest = { "./islands/OutOfStock.tsx": $OutOfStock, "./islands/ProductImageZoom.tsx": $ProductImageZoom, "./islands/ProductReview.tsx": $ProductReview, + "./islands/RangeFilter.tsx": $RangeFilter, "./islands/SearchControls.tsx": $SearchControls, "./islands/Shipping.tsx": $Shipping, "./islands/ShippingSimulation.tsx": $ShippingSimulation, diff --git a/islands/RangeFilter.tsx b/islands/RangeFilter.tsx new file mode 100644 index 0000000..f041f6c --- /dev/null +++ b/islands/RangeFilter.tsx @@ -0,0 +1,142 @@ +import { useState, useEffect, useRef } from "preact/hooks"; + +export interface Props { + /** @description O valor minimo esta definido em R$0,00 se quiser mudar o valor preencher o campo abaixo */ + min?: number; + /** @description O valor maximo esta definido em R$10.000,00 se quiser mudar o valor preencher o campo abaixo */ + max?: number; + /** @hide step */ + step?: number; +} + +const RangeFilter = ({ min = 0, max = 10000, step = 1 }: Props) => { + const [rangeSliderMin, setRangeSliderMin] = useState(min); + const [rangeSliderMax, setRangeSliderMax] = useState(max); + + const rangeSliderRef = useRef(null); + + useEffect(() => { + if (rangeSliderRef.current) { + const rangeSlider = rangeSliderRef.current; + const updateSlider = () => { + const percentageMin = rangeSliderMin / max; + const percentageMax = rangeSliderMax / max; + + rangeSlider.querySelector( + ".range-slider-val-left" + )!.style.width = `${percentageMin * 100}%`; + rangeSlider.querySelector( + ".range-slider-val-right" + )!.style.width = `${(1 - percentageMax) * 100}%`; + rangeSlider.querySelector( + ".range-slider-val-range" + )!.style.left = `${percentageMin * 100}%`; + rangeSlider.querySelector( + ".range-slider-val-range" + )!.style.right = `${(1 - percentageMax) * 100}%`; + rangeSlider.querySelector( + ".range-slider-handle-left" + )!.style.left = `${percentageMin * 100}%`; + rangeSlider.querySelector( + ".range-slider-handle-right" + )!.style.left = `${percentageMax * 100}%`; + rangeSlider.querySelector( + ".range-slider-tooltip-left .range-slider-tooltip-text" + )!.innerText = rangeSliderMin.toString(); + rangeSlider.querySelector( + ".range-slider-tooltip-right .range-slider-tooltip-text" + )!.innerText = rangeSliderMax.toString(); + }; + + updateSlider(); + + const handleLeftInputChange = (e: Event) => { + const target = e.target as HTMLInputElement; + const newValue = Math.min( + parseInt(target.value, 10), + rangeSliderMax - step + ); + setRangeSliderMin(newValue); + }; + + const handleRightInputChange = (e: Event) => { + const target = e.target as HTMLInputElement; + const newValue = Math.max( + parseInt(target.value, 10), + rangeSliderMin + step + ); + setRangeSliderMax(newValue); + }; + + rangeSlider + .querySelector(".range-slider-input-left")! + .addEventListener("input", handleLeftInputChange); + rangeSlider + .querySelector(".range-slider-input-right")! + .addEventListener("input", handleRightInputChange); + + return () => { + rangeSlider + .querySelector(".range-slider-input-left")! + .removeEventListener("input", handleLeftInputChange); + rangeSlider + .querySelector(".range-slider-input-right")! + .removeEventListener("input", handleRightInputChange); + }; + } + }, [rangeSliderMin, rangeSliderMax, max, step]); + + return ( +
    +

    + Preço +

    +
    +
    +
    +
    +
    + + +
    + +
    +
    + +
    +
    + + +
    +
    +

    {`R$${rangeSliderMin},00`}

    +

    {`R$${rangeSliderMax},00`}

    +
    + + + filtrar por preço + +
    + ); +}; + +export default RangeFilter; diff --git a/tailwind.css b/tailwind.css index 221f3e0..2b9f2b5 100644 --- a/tailwind.css +++ b/tailwind.css @@ -3,7 +3,6 @@ @tailwind utilities; @layer base { - /* Allow changing font family via CMS */ html { font-family: var(--font-family); @@ -14,8 +13,8 @@ } /** Remove default styles from input[type=number] */ - input[type=number]::-webkit-inner-spin-button, - input[type=number]::-webkit-outer-spin-button { + input[type="number"]::-webkit-inner-spin-button, + input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; } @@ -29,15 +28,212 @@ } .group:disabled .group-disabled\:animate-progress { - animation: progress-frame ease normal + animation: progress-frame ease normal; } @keyframes progress-frame { 0% { - --dot-progress: 0% + --dot-progress: 0%; } to { - --dot-progress: 100% + --dot-progress: 100%; } -} \ No newline at end of file +} + +:root { + --ColorPrimaryDk: #164195; + --ColorPrimaryLt: #164195; +} + +.range-slider { + --range-slider-common-height: 12px; + --range-slider-handle-width: 20px; + --range-slider-handle-height: 20px; + + position: relative; + display: flex; + align-items: center; + width: 99%; + height: 100%; +} + +.range-slider > div { + position: absolute; + display: flex; + align-items: center; + left: 13px; + right: 15px; + height: var(--range-slider-common-height); +} + +.range-slider > div > .range-slider-val-left, +.range-slider > div > .range-slider-val-right, +.range-slider > div > .range-slider-val-range { + height: 10px; +} + +.range-slider > div > .range-slider-val-left { + position: absolute; + left: 0; + border-radius: 10px; + background-color: #ccc; + margin: 0 7px; +} + +.range-slider > div > .range-slider-val-right { + position: absolute; + right: 0; + margin: 0 7px; + border-radius: 10px; + background-color: #ccc; +} + +.range-slider > div > .range-slider-val-range { + position: absolute; + left: 0; + top: -2px; + height: 14px; + border-radius: 14px; + background-color: var(--ColorPrimaryLt); +} + +.range-slider > div > .range-slider-handle { + z-index: 1; + position: absolute; + top: -5px; + margin-left: -11px; + width: var(--range-slider-handle-width); + height: var(--range-slider-handle-height); + border-radius: 25%; + background-color: #fff; + text-align: left; + + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.4); + outline: none; + cursor: pointer; +} + +div.range-slider > input[type="range"]::-ms-thumb { + width: var(--range-slider-handle-width); + height: var(--range-slider-handle-height); + border: 0 none; + border-radius: 0px; + background: red; + + pointer-events: all; +} + +div.range-slider > input[type="range"]::-moz-range-thumb { + width: var(--range-slider-handle-width); + height: var(--range-slider-handle-height); + border: 0 none; + border-radius: 0px; + background: red; + + pointer-events: all; +} + +div.range-slider > input[type="range"]::-webkit-slider-thumb { + width: var(--range-slider-handle-width); + height: var(--range-slider-handle-height); + border: 0 none; + border-radius: 0px; + background: red; + + pointer-events: all; + -webkit-appearance: none; +} + +div.range-slider > input[type="range"]::-ms-fill-lower { + background: transparent; + border: 0 none; +} + +div.range-slider > input[type="range"]::-ms-fill-upper { + background: transparent; + border: 0 none; +} + +.range-slider > input[type="range"] { + z-index: 1; + position: absolute; + width: 100%; + height: var(--range-slider-common-height); + + opacity: 0; + filter: alpha(opacity=0); + cursor: pointer; + pointer-events: none; + -webkit-appearance: none; +} + +div.range-slider > input[type="range"]::-ms-track { + background: transparent; + color: transparent; + + -webkit-appearance: none; +} + +div.range-slider > input[type="range"]::-moz-range-track { + background: transparent; + color: transparent; + + -moz-appearance: none; +} + +div.range-slider > input[type="range"]:focus::-webkit-slider-runnable-track { + background: transparent; + border: transparent; +} + +div.range-slider > input[type="range"]:focus { + outline: none; +} + +div.range-slider > input[type="range"]::-ms-tooltip { + display: none; +} + +.range-slider > div > .range-slider-tooltip { + z-index: 2; + position: absolute; + top: -42px; + margin-left: -12px; + width: 28px; + height: 28px; + border-radius: 28px; + background-color: var(--ColorPrimaryLt); + color: #fff; + text-align: center; + + opacity: 0; +} + +.range-slider > div > .range-slider-tooltip:after { + content: ""; + + position: absolute; + left: 0; + top: 19px; + border-left: 14px solid transparent; + border-right: 14px solid transparent; + border-top-width: 16px; + border-top-style: solid; + border-top-color: var(--ColorPrimaryLt); + border-radius: 16px; + opacity: 0; +} + +.range-slider > div > .range-slider-tooltip > span { + font-size: 12px; + font-weight: 700; + line-height: 28px; +} + +/* .range-slider:hover > div > .range-slider-tooltip { + opacity: 1; +} */ +.range-slider-tooltip-text { + opacity: 0; +}