Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions examples/astro-stores-demo/src/components/store/PriceRangeSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import React, { useState, useEffect } from 'react';

interface PriceRangeSelectorProps {
min: number;
max: number;
selectedMin: number;
selectedMax: number;
setSelectedMin: (price: number) => void;
setSelectedMax: (price: number) => void;
}

export const PriceRangeSelector: React.FC<PriceRangeSelectorProps> = ({
min,
max,
selectedMin,
selectedMax,
setSelectedMin,
setSelectedMax,
}) => {
// Local state for immediate UI updates while dragging
const [localSelectedMin, setLocalSelectedMin] = useState(selectedMin);
const [localSelectedMax, setLocalSelectedMax] = useState(selectedMax);

// Round functions for display only
const roundMinValue = (value: number): number => Math.floor(value);
const roundMaxValue = (value: number): number => Math.ceil(value);

return (
<div>
<div className="space-y-4">
<h4 className="text-content-primary font-medium mb-4">Price Range</h4>
{/* Price Range Display */}
<div className="flex items-center justify-between text-sm text-content-light">
<span>${String(roundMinValue(min))}</span>
<span>${String(roundMaxValue(max))}</span>
</div>

{/* Dual Range Slider */}
<div className="relative h-6">
<div className="absolute top-2 left-0 right-0 h-2 bg-brand-medium rounded-full">
<div
className="absolute h-2 rounded-full bg-gradient-primary"
style={{
left: `${((localSelectedMin - min) / (max - min)) * 100}%`,
width: `${
((localSelectedMax - min) / (max - min)) * 100 -
((localSelectedMin - min) / (max - min)) * 100
}%`,
}}
/>
</div>

{/* Min Range Input */}
<input
type="range"
min={min}
max={max}
value={localSelectedMin}
onChange={e => setLocalSelectedMin(Number(e.target.value))}
onMouseUp={() => {
setSelectedMin(localSelectedMin);
}}
onTouchEnd={() => {
setSelectedMin(localSelectedMin);
}}
className="absolute top-0 left-0 w-full h-6 bg-transparent appearance-none cursor-pointer range-slider range-slider-min"
style={{
zIndex: localSelectedMin > min + (max - min) * 0.5 ? 2 : 1,
}}
/>

{/* Max Range Input */}
<input
type="range"
min={min}
max={max}
value={localSelectedMax}
onChange={e => setLocalSelectedMax(Number(e.target.value))}
onMouseUp={() => {
setSelectedMax(localSelectedMax);
}}
onTouchEnd={() => {
setSelectedMax(localSelectedMax);
}}
className="absolute top-0 left-0 w-full h-6 bg-transparent appearance-none cursor-pointer range-slider range-slider-max"
style={{
zIndex: localSelectedMax < min + (max - min) * 0.5 ? 2 : 1,
}}
/>
</div>

{/* Manual Price Input */}
<div className="flex items-center gap-4">
<div className="flex-1">
<label className="block text-xs text-content-muted mb-1">Min</label>
<input
type="number"
value={roundMinValue(localSelectedMin)}
onChange={e => {
const value = Number(e.target.value) || min;
setLocalSelectedMin(value);
setSelectedMin(value);
}}
min={min}
max={max}
className="w-full px-3 py-2 bg-surface-primary border border-brand-light rounded-lg text-content-primary text-sm focus:outline-none focus:ring-2 focus:ring-brand-primary"
/>
</div>
<div className="flex-1">
<label className="block text-xs text-content-muted mb-1">Max</label>
<input
type="number"
value={roundMaxValue(localSelectedMax)}
onChange={e => {
const value = Number(e.target.value) || max;
setLocalSelectedMax(value);
setSelectedMax(value);
}}
min={min}
max={max}
className="w-full px-3 py-2 bg-surface-primary border border-brand-light rounded-lg text-content-primary text-sm focus:outline-none focus:ring-2 focus:ring-brand-primary"
/>
</div>
</div>
</div>
</div>
);
};

export default PriceRangeSelector;
118 changes: 17 additions & 101 deletions examples/astro-stores-demo/src/components/store/ProductFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { getStockStatusMessage } from './product-status-enums';
import { ProductListFilters } from '@wix/headless-stores/react';
import type { ProductsListFiltersServiceConfig } from '@wix/headless-stores/services';
import { PriceRangeSelector } from './PriceRangeSelector';

interface ProductFiltersProps {
className?: string;
Expand Down Expand Up @@ -85,107 +86,22 @@ export const ProductFilters: React.FC<ProductFiltersProps> = ({

<div className={`space-y-6 ${isExpanded ? 'block' : 'hidden lg:block'}`}>
<ProductListFilters.PriceRange>
{({ minPrice, maxPrice, setMinPrice, setMaxPrice }) => (
<div
className={`space-y-6 ${isExpanded ? 'block' : 'hidden lg:block'}`}
>
<div>
<h4 className="text-content-primary font-medium mb-4">
Price Range
</h4>
<div className="space-y-4">
{/* Price Range Display */}
<div className="flex items-center justify-between text-sm text-content-light">
<span>${String(minPrice)}</span>
<span>${String(maxPrice)}</span>
</div>

{/* Dual Range Slider */}
<div className="relative h-6">
<div className="absolute top-2 left-0 right-0 h-2 bg-brand-medium rounded-full">
<div
className="absolute h-2 rounded-full bg-gradient-primary"
style={{
left: `${
((minPrice - minPrice) / (maxPrice - minPrice)) *
100
}%`,
width: `${
((maxPrice - minPrice) / (maxPrice - minPrice)) *
100
}%`,
}}
/>
</div>

{/* Min Range Input */}

<input
type="range"
min={minPrice}
max={maxPrice}
value={minPrice}
onChange={e => setMinPrice(Number(e.target.value))}
className="absolute top-0 left-0 w-full h-6 bg-transparent appearance-none cursor-pointer range-slider range-slider-min"
style={{
zIndex:
minPrice > minPrice + (maxPrice - minPrice) * 0.5
? 2
: 1,
}}
/>

{/* Max Range Input */}
<input
type="range"
min={minPrice}
max={maxPrice}
value={maxPrice}
onChange={e => setMaxPrice(Number(e.target.value))}
className="absolute top-0 left-0 w-full h-6 bg-transparent appearance-none cursor-pointer range-slider range-slider-max"
style={{
zIndex:
maxPrice < minPrice + (maxPrice - minPrice) * 0.5
? 2
: 1,
}}
/>
</div>

{/* Manual Price Input */}
<div className="flex items-center gap-4">
<div className="flex-1">
<label className="block text-xs text-content-muted mb-1">
Min
</label>

<input
type="number"
value={minPrice}
onChange={e => {
setMinPrice(Number(e.target.value));
}}
className="w-full px-3 py-2 bg-surface-primary border border-brand-light rounded-lg text-content-primary text-sm focus:outline-none focus:ring-2 focus:ring-brand-primary"
/>
</div>
<div className="flex-1">
<label className="block text-xs text-content-muted mb-1">
Max
</label>

<input
type="number"
value={maxPrice}
onChange={e => {
setMaxPrice(Number(e.target.value));
}}
className="w-full px-3 py-2 bg-surface-primary border border-brand-light rounded-lg text-content-primary text-sm focus:outline-none focus:ring-2 focus:ring-brand-primary"
/>
</div>
</div>
</div>
</div>
</div>
{({
availableMinPrice,
availableMaxPrice,
selectedMinPrice,
selectedMaxPrice,
setSelectedMinPrice,
setSelectedMaxPrice,
}) => (
<PriceRangeSelector
min={availableMinPrice}
max={availableMaxPrice}
selectedMin={selectedMinPrice}
selectedMax={selectedMaxPrice}
setSelectedMin={setSelectedMinPrice}
setSelectedMax={setSelectedMaxPrice}
/>
)}
</ProductListFilters.PriceRange>

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"scripts": {
"build:all": "turbo run build",
"build:headless-components": "yarn build:all --filter=\"./packages/headless-components/*\"",
"clean": "find . -name 'node_modules' -type d -prune -exec rm -rf '{}' + && rm -rf .astro ./dist .turbo",
"astro": "astro",
"tsc": "tsc",
"syncpack": "syncpack"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,13 +195,17 @@ export interface PriceRangeProps {
*/
export interface PriceRangeRenderProps {
/** Current minimum price filter value */
minPrice: number;
selectedMinPrice: number;
/** Current maximum price filter value */
maxPrice: number;
selectedMaxPrice: number;
/** Catalog minimum price */
availableMinPrice: number;
/** Catalog maximum price */
availableMaxPrice: number;
/** Function to update the minimum price filter */
setMinPrice: (minPrice: number) => void;
setSelectedMinPrice: (minPrice: number) => void;
/** Function to update the maximum price filter */
setMaxPrice: (maxPrice: number) => void;
setSelectedMaxPrice: (maxPrice: number) => void;
}

/**
Expand Down Expand Up @@ -242,13 +246,22 @@ export interface PriceRangeRenderProps {
*/
export function PriceRange(props: PriceRangeProps) {
const service = useService(ProductsListFiltersServiceDefinition);
const minPrice = service.minPrice.get();
const maxPrice = service.maxPrice.get();
const setMinPrice = service.setMinPrice;
const setMaxPrice = service.setMaxPrice;
const selectedMinPrice = service.selectedMinPrice.get();
const availableMinPrice = service.availableMinPrice.get();
const selectedMaxPrice = service.selectedMaxPrice.get();
const availableMaxPrice = service.availableMaxPrice.get();
const setSelectedMinPrice = service.setSelectedMinPrice;
const setSelectedMaxPrice = service.setSelectedMaxPrice;

return typeof props.children === "function"
? props.children({ minPrice, maxPrice, setMinPrice, setMaxPrice })
? props.children({
availableMinPrice,
selectedMinPrice,
selectedMaxPrice,
availableMaxPrice,
setSelectedMinPrice,
setSelectedMaxPrice,
})
: props.children;
}

Expand Down
Loading
Loading