filter
@@ -2,7 +2,7 @@ import "./back.css";
|
|||||||
|
|
||||||
export default function BackAnimatsiya() {
|
export default function BackAnimatsiya() {
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 w-full h-full flex items-center justify-center pointer-events-none z-0 opacity-100">
|
<div className="fixed inset-0 w-full h-full flex items-center justify-center pointer-events-none z-0 opacity-50">
|
||||||
<svg
|
<svg
|
||||||
id="Layer_2"
|
id="Layer_2"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|||||||
129
components/pages/products/filter.tsx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
"use client";
|
||||||
|
import { useFilter } from "@/lib/filter-zustand";
|
||||||
|
import { Check } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export default function Filter() {
|
||||||
|
const filter = useFilter((state) => state.filter);
|
||||||
|
const toggleFilter = useFilter((state) => state.toggleFilter);
|
||||||
|
const hasData = useFilter((state) => state.hasFilter);
|
||||||
|
|
||||||
|
const [dataExpanded, setDataExpanded] = useState<boolean>(false);
|
||||||
|
const [numberExpanded, setNumberExpanded] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const sectionData = [
|
||||||
|
"SLT-Aqua",
|
||||||
|
"Вварное седло",
|
||||||
|
"Кран шаровый",
|
||||||
|
"Муфты",
|
||||||
|
"Муфты комбинированные",
|
||||||
|
"Муфты переходные",
|
||||||
|
"Тройник комбинированный",
|
||||||
|
"Тройники",
|
||||||
|
"Трубы SDR 6",
|
||||||
|
"Трубы SDR 7,4",
|
||||||
|
"Угол 45",
|
||||||
|
"Угол 90",
|
||||||
|
"Угольник комбинированный",
|
||||||
|
"Фланцы+бурты",
|
||||||
|
];
|
||||||
|
|
||||||
|
const sectionNumber = [
|
||||||
|
"25", '25х1/2"', '25х3/4"', "32", "32x25x25", "32x25x32",
|
||||||
|
'32х1"', '32х1/2"', "32х25", '32х3/4"', "40", "40x25x40",
|
||||||
|
"40x32x40", '40х1 1/4"', '40х1 3/4"', '40х1/2"', "40х25",
|
||||||
|
"40х32", "50", "50x25x50", "50x32x50", "50x40x50",
|
||||||
|
'50х1 1/2"', '50х1/2"', "50х25", "50х32", "50х40", "63",
|
||||||
|
"63x25x63", "63x32x63", "63x40x63", "63x50x63", '63х1/2"',
|
||||||
|
'63х2"', "63х25", "63х32", "63х40", "63х50", "75",
|
||||||
|
"75x25x75", "75x32x75", "75x40x75", "75x50x75", "75x63x75",
|
||||||
|
'75х1/2"', "75х32", "75х40", "75х50", "75х63", "90",
|
||||||
|
"90x40x90", "90x50x90", "90x63x90", "90x75x90", '90х1/2"',
|
||||||
|
"90х32", "90х40", "90х50", "90х63", "90х75", "110",
|
||||||
|
"110x50x110", "110x63x110", "110x75x110", "110x90x110",
|
||||||
|
'110х1/2"', "110х25", "110х32", "110х40", "110х50",
|
||||||
|
"110х63", "110х75", "110х90", "125", "160",
|
||||||
|
];
|
||||||
|
|
||||||
|
// Bo'lim uchun ko'rsatiladigan itemlar
|
||||||
|
const visibleSectionData = dataExpanded ? sectionData : sectionData.slice(0, 5);
|
||||||
|
|
||||||
|
// O'lcham uchun ko'rsatiladigan itemlar
|
||||||
|
const visibleSectionNumber = numberExpanded ? sectionNumber : sectionNumber.slice(0, 10);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3 max-w-70 w-full text-white">
|
||||||
|
{/* Bo'lim filtri */}
|
||||||
|
<div className="bg-gray-500 rounded-lg">
|
||||||
|
<p className="bg-red-500 text-white p-2 font-semibold font-almarai text-lg rounded-t-lg">
|
||||||
|
Bo'lim
|
||||||
|
</p>
|
||||||
|
<div className="space-y-3 p-2">
|
||||||
|
{visibleSectionData.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item}
|
||||||
|
onClick={() => toggleFilter(item)}
|
||||||
|
className="hover:cursor-pointer flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`flex h-5 w-5 items-center justify-center rounded border-2 transition ${
|
||||||
|
hasData(item)
|
||||||
|
? "border-red-600 bg-red-600"
|
||||||
|
: "border-gray-400 bg-transparent"
|
||||||
|
}`}
|
||||||
|
aria-label="Filter checkbox"
|
||||||
|
>
|
||||||
|
{hasData(item) && (
|
||||||
|
<Check className="h-3 w-3 text-white" strokeWidth={3} />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<p>{item}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="p-2 text-lg underline hover:text-red-300 transition"
|
||||||
|
onClick={() => setDataExpanded(!dataExpanded)}
|
||||||
|
>
|
||||||
|
{dataExpanded ? "Yashirish" : "Ko'proq ko'rish"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* O'lcham filtri */}
|
||||||
|
<div className="bg-gray-500 rounded-lg">
|
||||||
|
<p className="bg-red-500 text-white p-2 font-semibold font-almarai text-lg rounded-t-lg">
|
||||||
|
O'lcham
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-2 gap-3 p-2">
|
||||||
|
{visibleSectionNumber.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item}
|
||||||
|
onClick={() => toggleFilter(item)}
|
||||||
|
className="hover:cursor-pointer flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`flex h-5 w-5 items-center justify-center rounded border-2 transition ${
|
||||||
|
hasData(item)
|
||||||
|
? "border-red-600 bg-red-600"
|
||||||
|
: "border-gray-400 bg-transparent"
|
||||||
|
}`}
|
||||||
|
aria-label="Filter checkbox"
|
||||||
|
>
|
||||||
|
{hasData(item) && (
|
||||||
|
<Check className="h-3 w-3 text-white" strokeWidth={3} />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<p>{item}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setNumberExpanded(!numberExpanded)}
|
||||||
|
className="p-2 text-lg underline hover:text-red-300 transition"
|
||||||
|
>
|
||||||
|
{numberExpanded ? "Yashirish" : "Ko'proq ko'rish"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
35
components/pages/products/filterInfo.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useFilter } from "@/lib/filter-zustand";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
|
||||||
|
export default function FilterInfo() {
|
||||||
|
const filtered = useFilter((state) => state.filter);
|
||||||
|
const resetFilter = useFilter((state) => state.resetFilter);
|
||||||
|
const togleFilter = useFilter((state) => state.toggleFilter);
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="fixed bottom-13 left-5 z-10 bg-gray-500 p-3 rounded-lg space-y-3 max-w-70 w-full">
|
||||||
|
<p className="text-white ">Found: 20</p>
|
||||||
|
<div className="flex gap-1 flex-wrap">
|
||||||
|
{filtered &&
|
||||||
|
filtered.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item}
|
||||||
|
className="flex items-center gap-1 p-1 rounded-lg bg-gray-700 text-white text-sm "
|
||||||
|
>
|
||||||
|
<button onClick={() => togleFilter(item)}>
|
||||||
|
<X size={16} />
|
||||||
|
</button>
|
||||||
|
{item}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button onClick={resetFilter} className="text-white underline ">
|
||||||
|
Clear all
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,16 @@
|
|||||||
|
import Filter from "./filter";
|
||||||
|
import FilterInfo from "./filterInfo";
|
||||||
import ProductCard from "./productCard";
|
import ProductCard from "./productCard";
|
||||||
|
|
||||||
export function Products() {
|
export function Products() {
|
||||||
return (
|
return (
|
||||||
<div className="bg-[#1e1d1c] py-20">
|
<div className="bg-[#1e1d1c] py-20">
|
||||||
<div className="max-w-250 mx-auto w-full sm:-mt-50 -mt-30 z-20 relative">
|
<div className="max-w-300 mx-auto w-full z-20 relative">
|
||||||
|
<div className="flex items-start gap-5">
|
||||||
|
{/* filter part */}
|
||||||
|
<Filter/>
|
||||||
|
|
||||||
|
{/* main products */}
|
||||||
<div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
|
<div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
|
||||||
{Array(9)
|
{Array(9)
|
||||||
.fill(null)
|
.fill(null)
|
||||||
@@ -18,6 +25,8 @@ export function Products() {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<FilterInfo/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
33
lib/filter-zustand.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
|
||||||
|
interface FilterZustandTypes {
|
||||||
|
filter: string[];
|
||||||
|
removeFilter: (data: string) => void;
|
||||||
|
toggleFilter: (data: string) => void;
|
||||||
|
resetFilter: () => void;
|
||||||
|
hasFilter: (data: string) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFilter = create<FilterZustandTypes>((set, get) => ({
|
||||||
|
filter: [],
|
||||||
|
|
||||||
|
removeFilter: (data) =>
|
||||||
|
set((state) => ({
|
||||||
|
filter: state.filter.filter((item) => item !== data),
|
||||||
|
})),
|
||||||
|
|
||||||
|
// Toggle: mavjud bo'lsa o'chirish, yo'q bo'lsa qo'shish
|
||||||
|
toggleFilter: (data) =>
|
||||||
|
set((state) => {
|
||||||
|
if (state.filter.includes(data)) {
|
||||||
|
return { filter: state.filter.filter((item) => item !== data) };
|
||||||
|
}
|
||||||
|
return { filter: [...state.filter, data] };
|
||||||
|
}),
|
||||||
|
|
||||||
|
resetFilter: () => set({ filter: [] }),
|
||||||
|
|
||||||
|
hasFilter: (data) => {
|
||||||
|
return get().filter.includes(data);
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -64,7 +64,8 @@
|
|||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"zod": "3.25.76"
|
"zod": "3.25.76",
|
||||||
|
"zustand": "^5.0.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.9",
|
"@tailwindcss/postcss": "^4.1.9",
|
||||||
|
|||||||
27
pnpm-lock.yaml
generated
@@ -176,6 +176,9 @@ importers:
|
|||||||
zod:
|
zod:
|
||||||
specifier: 3.25.76
|
specifier: 3.25.76
|
||||||
version: 3.25.76
|
version: 3.25.76
|
||||||
|
zustand:
|
||||||
|
specifier: ^5.0.10
|
||||||
|
version: 5.0.10(@types/react@19.2.9)(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0))
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@tailwindcss/postcss':
|
'@tailwindcss/postcss':
|
||||||
specifier: ^4.1.9
|
specifier: ^4.1.9
|
||||||
@@ -2006,6 +2009,24 @@ packages:
|
|||||||
zod@3.25.76:
|
zod@3.25.76:
|
||||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||||
|
|
||||||
|
zustand@5.0.10:
|
||||||
|
resolution: {integrity: sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==}
|
||||||
|
engines: {node: '>=12.20.0'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '>=18.0.0'
|
||||||
|
immer: '>=9.0.6'
|
||||||
|
react: '>=18.0.0'
|
||||||
|
use-sync-external-store: '>=1.2.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
immer:
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
use-sync-external-store:
|
||||||
|
optional: true
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@alloc/quick-lru@5.2.0': {}
|
'@alloc/quick-lru@5.2.0': {}
|
||||||
@@ -3687,3 +3708,9 @@ snapshots:
|
|||||||
d3-timer: 3.0.1
|
d3-timer: 3.0.1
|
||||||
|
|
||||||
zod@3.25.76: {}
|
zod@3.25.76: {}
|
||||||
|
|
||||||
|
zustand@5.0.10(@types/react@19.2.9)(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0)):
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.9
|
||||||
|
react: 19.2.0
|
||||||
|
use-sync-external-store: 1.6.0(react@19.2.0)
|
||||||
|
|||||||
BIN
public/images/products/products2.webp
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
public/images/products/products3.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
public/images/products/products4.webp
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
public/images/products/products5.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
public/images/products/products6.webp
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
public/images/products/products7.webp
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
public/images/products/products8.webp
Normal file
|
After Width: | Height: | Size: 1.9 KiB |