first commit

This commit is contained in:
Bivekich
2025-06-26 06:59:59 +03:00
commit d44874775c
450 changed files with 76635 additions and 0 deletions

View File

@ -0,0 +1,168 @@
import React, { useState, useEffect } from "react";
interface FilterDropdownProps {
title?: string; // Делаем необязательным
options: string[];
multi?: boolean;
showAll?: boolean;
defaultOpen?: boolean; // Открыт ли по умолчанию
hasMore?: boolean; // Есть ли еще опции для загрузки
onShowMore?: () => void; // Обработчик "Показать еще"
isMobile?: boolean; // Добавляем флаг для мобильной версии
selectedValues?: string[]; // Выбранные значения
onChange?: (values: string[]) => void; // Обработчик изменений
}
const FilterDropdown: React.FC<FilterDropdownProps> = ({
title,
options,
multi = true,
showAll = false,
defaultOpen = false,
hasMore = false,
onShowMore,
isMobile = false,
selectedValues = [],
onChange
}) => {
const [open, setOpen] = useState(isMobile || defaultOpen); // На мобилке или если defaultOpen - сразу открыт
const [showAllOptions, setShowAllOptions] = useState(false);
const [selected, setSelected] = useState<string[]>(selectedValues);
const visibleOptions = showAll && !showAllOptions ? options.slice(0, 4) : options;
useEffect(() => {
// Сравниваем содержимое массивов, а не ссылки
if (JSON.stringify(selected) !== JSON.stringify(selectedValues)) {
setSelected(selectedValues);
}
}, [selectedValues, selected]);
const handleSelect = (option: string) => {
let newSelected: string[];
if (multi) {
newSelected = selected.includes(option)
? selected.filter(o => o !== option)
: [...selected, option];
} else {
newSelected = selected.includes(option) ? [] : [option];
}
setSelected(newSelected);
// Вызываем колбэк только если значения действительно изменились
if (onChange && JSON.stringify(newSelected) !== JSON.stringify(selected)) {
onChange(newSelected);
}
};
// Мобильная версия - всегда открытый список
if (isMobile) {
return (
<div className="filter-block-mobile">
<div className="dropdown w-dropdown w--open">
<div className="dropdown-toggle w-dropdown-toggle" style={{ cursor: 'default', background: 'none', boxShadow: 'none' }}>
<h4 className="heading-2">{title}</h4>
</div>
<nav className="dropdown-list w-dropdown-list" style={{ display: 'block', position: 'static', boxShadow: 'none', background: 'transparent', padding: 0 }}>
<div className="w-layout-vflex flex-block-17">
{visibleOptions.map(option => (
<div className="div-block-8" key={option} onClick={() => handleSelect(option)}>
<div className={`div-block-7${selected.includes(option) ? " active" : ""}`}>
{selected.includes(option) && (
<svg width="16" height="12" viewBox="0 0 16 12" fill="none">
<path d="M5.33333 12L0 6.89362L1.86667 5.10638L5.33333 8.42553L14.1333 0L16 1.78723L5.33333 12Z" fill="currentColor" />
</svg>
)}
</div>
<div className="text-block-12">{option}</div>
</div>
))}
{((showAll && options.length > 4) || hasMore) && (
<div className="w-layout-vflex flex-block-17">
<div
className="div-block-8"
onClick={() => {
if (hasMore && onShowMore) {
onShowMore();
} else {
setShowAllOptions(!showAllOptions);
}
}}
style={{ cursor: 'pointer' }}
>
<div className="text-block-13">
{hasMore ? "Показать еще" : (showAllOptions ? "Скрыть" : "Показать все")}
</div>
<img
loading="lazy"
src="/images/arrow_drop_down.svg"
alt=""
style={{ marginLeft: 4, transform: showAllOptions ? 'rotate(180deg)' : undefined }}
/>
</div>
</div>
)}
</div>
</nav>
</div>
</div>
);
}
// Десктопная версия - классический dropdown
return (
<div className={`dropdown w-dropdown${open ? " w--open" : ""}`}>
<div className="dropdown-toggle w-dropdown-toggle" onClick={() => setOpen(!open)}>
<h4 className="heading-2">
{title} {selectedValues.length > 0 && `(${selectedValues.length})`}
</h4>
<div className="icon-3 w-icon-dropdown-toggle"></div>
</div>
{open && (
<nav className="dropdown-list w-dropdown-list">
<div className="w-layout-vflex flex-block-17">
{visibleOptions.map(option => (
<div className="div-block-8" key={option} onClick={() => handleSelect(option)}>
<div className={`div-block-7${selected.includes(option) ? " active" : ""}`}>
{selected.includes(option) && (
<svg width="16" height="12" viewBox="0 0 16 12" fill="none">
<path d="M5.33333 12L0 6.89362L1.86667 5.10638L5.33333 8.42553L14.1333 0L16 1.78723L5.33333 12Z" fill="currentColor" />
</svg>
)}
</div>
<div className="text-block-12">{option}</div>
</div>
))}
</div>
{((showAll && options.length > 4) || hasMore) && (
<div className="w-layout-vflex flex-block-17">
<div
className="div-block-8"
onClick={() => {
if (hasMore && onShowMore) {
onShowMore();
} else {
setShowAllOptions(!showAllOptions);
}
}}
style={{ cursor: 'pointer' }}
>
<div className="text-block-13">
{hasMore ? "Показать еще" : (showAllOptions ? "Скрыть" : "Показать все")}
</div>
<img
loading="lazy"
src="/images/arrow_drop_down.svg"
alt=""
style={{ marginLeft: 4, transform: showAllOptions ? 'rotate(180deg)' : undefined }}
/>
</div>
</div>
)}
</nav>
)}
</div>
);
};
export default FilterDropdown;

View File

@ -0,0 +1,279 @@
import React, { useRef, useState, useLayoutEffect, useEffect } from "react";
interface FilterRangeProps {
title: string;
min?: number;
max?: number;
isMobile?: boolean; // Добавляем флаг для мобильной версии
value?: [number, number] | null; // Текущее значение диапазона
onChange?: (value: [number, number]) => void;
}
const DEFAULT_MIN = 1;
const DEFAULT_MAX = 32000;
const clamp = (v: number, min: number, max: number) => Math.max(min, Math.min(v, max));
const FilterRange: React.FC<FilterRangeProps> = ({ title, min = DEFAULT_MIN, max = DEFAULT_MAX, isMobile = false, value = null, onChange }) => {
const [from, setFrom] = useState(value ? value[0] : min);
const [to, setTo] = useState(value ? value[1] : max);
const [dragging, setDragging] = useState<null | "from" | "to">(null);
const [trackWidth, setTrackWidth] = useState(0);
const [open, setOpen] = useState(true);
const trackRef = useRef<HTMLDivElement>(null);
// Обновляем локальное состояние при изменении внешнего значения
useEffect(() => {
if (value) {
setFrom(value[0]);
setTo(value[1]);
} else {
setFrom(min);
setTo(max);
}
}, [value, min, max]);
// Обновляем ширину полосы при монтировании и ресайзе
useLayoutEffect(() => {
const updateWidth = () => {
if (trackRef.current) setTrackWidth(trackRef.current.offsetWidth);
};
updateWidth();
window.addEventListener("resize", updateWidth);
return () => window.removeEventListener("resize", updateWidth);
}, []);
// Перевод значения в px и обратно
const valueToPx = (value: number) => trackWidth ? ((value - min) / (max - min)) * trackWidth : 0;
const pxToValue = (px: number) => trackWidth ? Math.round((px / trackWidth) * (max - min) + min) : min;
// Drag logic
const onMouseDown = (type: "from" | "to") => (e: React.MouseEvent) => {
setDragging(type);
e.preventDefault();
};
useEffect(() => {
if (!dragging) return;
const onMove = (e: MouseEvent) => {
if (!trackRef.current) return;
const rect = trackRef.current.getBoundingClientRect();
let x = e.clientX - rect.left;
x = clamp(x, 0, trackWidth);
const value = clamp(pxToValue(x), min, max);
if (dragging === "from") {
setFrom(v => clamp(Math.min(value, to), min, to));
} else {
setTo(v => clamp(Math.max(value, from), from, max));
}
};
const onUp = () => {
setDragging(null);
if (onChange) {
onChange([from, to]);
}
};
window.addEventListener("mousemove", onMove);
window.addEventListener("mouseup", onUp);
return () => {
window.removeEventListener("mousemove", onMove);
window.removeEventListener("mouseup", onUp);
};
}, [dragging, from, to, min, max, trackWidth, onChange]);
// Input handlers
const handleFromInput = (e: React.ChangeEvent<HTMLInputElement>) => {
let v = Number(e.target.value.replace(/\D/g, ""));
if (isNaN(v)) v = min;
setFrom(clamp(Math.min(v, to), min, to));
};
const handleToInput = (e: React.ChangeEvent<HTMLInputElement>) => {
let v = Number(e.target.value.replace(/\D/g, ""));
if (isNaN(v)) v = max;
setTo(clamp(Math.max(v, from), from, max));
};
const handleInputBlur = () => {
if (onChange) {
onChange([from, to]);
}
};
// px позиции для точек
const pxFrom = valueToPx(from);
const pxTo = valueToPx(to);
// Мобильная версия - без dropdown
if (isMobile) {
return (
<div className="filter-block-mobile">
<div className="dropdown w-dropdown w--open">
<div className="dropdown-toggle w-dropdown-toggle" style={{ cursor: 'default', background: 'none', boxShadow: 'none' }}>
<h4 className="heading-2">{title}</h4>
</div>
<nav className="dropdown-list w-dropdown-list" style={{ display: 'block', position: 'static', boxShadow: 'none', background: 'transparent', padding: 0 }}>
<div className="form-block-2">
<form className="form-2" onSubmit={e => e.preventDefault()} style={{ display: 'flex', gap: 12, marginBottom: 16 }}>
<div className="div-block-5" style={{ position: 'relative', flex: 1 }}>
<label htmlFor="from" className="field-label" style={{ position: 'absolute', left: 3, top: '50%', transform: 'translateY(-50%)', color: '#888', fontSize: 15, pointerEvents: 'none' }}>от</label>
<input
className="text-field-2 w-input"
maxLength={6}
name="from"
placeholder={String(min)}
type="text"
id="from"
value={from}
onChange={handleFromInput}
onBlur={handleInputBlur}
style={{ padding: '8px 10px 8px 36px', fontSize: 16, width: '100%' }}
/>
</div>
<div className="div-block-5" style={{ position: 'relative', flex: 1 }}>
<label htmlFor="to" className="field-label" style={{ position: 'absolute', left: 3, top: '50%', transform: 'translateY(-50%)', color: '#888', fontSize: 15, pointerEvents: 'none' }}>до</label>
<input
className="text-field-2 w-input"
maxLength={6}
name="to"
placeholder={String(max)}
type="text"
id="to"
value={to}
onChange={handleToInput}
onBlur={handleInputBlur}
style={{ padding: '8px 10px 8px 36px', fontSize: 16, width: '100%' }}
/>
</div>
</form>
</div>
<div className="div-block-6" style={{ position: 'relative', height: 32, marginTop: 12 }} ref={trackRef}>
<div className="track" style={{ position: 'absolute', top: 14, left: 0, right: 0, height: 4, borderRadius: 2 }}></div>
<div
className="track fill"
style={{
position: 'absolute',
top: 14,
left: pxFrom,
width: pxTo - pxFrom - 20,
height: 4,
borderRadius: 2,
zIndex: 2,
}}
></div>
<div
className="start"
style={{
position: 'absolute',
top: 6,
left: pxFrom ,
zIndex: 3,
cursor: 'pointer'
}}
onMouseDown={onMouseDown("from")}
></div>
<div
className="start end"
style={{
position: 'absolute',
top: 6,
left: pxTo - 20,
zIndex: 3,
cursor: 'pointer'
}}
onMouseDown={onMouseDown("to")}
></div>
</div>
<div className="range-values" style={{ display: 'flex', justifyContent: 'space-between', marginTop: 8, fontSize: 14, color: '#888' }}>
<span>{min}</span>
<span>{max}</span>
</div>
</nav>
</div>
</div>
);
}
// Десктопная версия - с dropdown
return (
<div className={`dropdown w-dropdown${open ? " w--open" : ""}`}>
<div className="dropdown-toggle w-dropdown-toggle" onClick={() => setOpen(o => !o)} tabIndex={0} aria-expanded={open} aria-label={`Фильтр диапазона ${title}`}>
<h4 className="heading-2">{title}</h4>
<div className="icon-3 w-icon-dropdown-toggle"></div>
</div>
{open && (
<nav className="dropdown-list w-dropdown-list">
<div className="form-block-2">
<form className="form-2" onSubmit={e => e.preventDefault()}>
<div className="div-block-5">
<label htmlFor="from" className="field-label">от</label>
<input
className="text-field-2 w-input"
maxLength={6}
name="from"
placeholder={String(min)}
type="text"
id="from"
value={from}
onChange={handleFromInput}
onBlur={handleInputBlur}
/>
</div>
<div className="div-block-5">
<label htmlFor="to" className="field-label">до</label>
<input
className="text-field-2 w-input"
maxLength={6}
name="to"
placeholder={String(max)}
type="text"
id="to"
value={to}
onChange={handleToInput}
onBlur={handleInputBlur}
/>
</div>
</form>
</div>
<div className="div-block-6" style={{ position: "relative", height: 32, marginTop: 12 }} ref={trackRef}>
<div className="track" style={{ position: "absolute", top: 14, left: 0, right: 0, height: 4, borderRadius: 2 }}></div>
<div
className="track fill"
style={{
position: "absolute",
top: 14,
left: pxFrom,
width: pxTo - pxFrom,
height: 4,
borderRadius: 2,
zIndex: 2,
}}
></div>
<div
className="start"
style={{
position: "absolute",
top: 6,
left: pxFrom - 8,
zIndex: 3,
cursor: "pointer"
}}
onMouseDown={onMouseDown("from")}
></div>
<div
className="start end"
style={{
position: "absolute",
top: 6,
left: pxTo - 8,
zIndex: 3,
cursor: "pointer"
}}
onMouseDown={onMouseDown("to")}
></div>
</div>
</nav>
)}
</div>
);
};
export default FilterRange;