Skip to content

Commit 4e6f43c

Browse files
committed
Refactor
1 parent d2fda93 commit 4e6f43c

File tree

3 files changed

+206
-138
lines changed

3 files changed

+206
-138
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { Product } from '@/types/product';
2+
import { useProductFilters } from '@/hooks/useProductFilters';
3+
import ProductCard from './ProductCard.component';
4+
import ProductFilters from './ProductFilters.component';
5+
6+
interface ProductListProps {
7+
products: Product[];
8+
title: string;
9+
}
10+
11+
const ProductList = ({ products, title }: ProductListProps) => {
12+
const {
13+
sortBy,
14+
setSortBy,
15+
selectedSizes,
16+
setSelectedSizes,
17+
selectedColors,
18+
setSelectedColors,
19+
priceRange,
20+
setPriceRange,
21+
productTypes,
22+
toggleProductType,
23+
resetFilters,
24+
filterProducts
25+
} = useProductFilters(products);
26+
27+
const filteredProducts = filterProducts(products);
28+
29+
return (
30+
<div className="flex flex-col md:flex-row gap-8">
31+
<ProductFilters
32+
selectedSizes={selectedSizes}
33+
setSelectedSizes={setSelectedSizes}
34+
selectedColors={selectedColors}
35+
setSelectedColors={setSelectedColors}
36+
priceRange={priceRange}
37+
setPriceRange={setPriceRange}
38+
productTypes={productTypes}
39+
toggleProductType={toggleProductType}
40+
products={products}
41+
resetFilters={resetFilters}
42+
/>
43+
44+
{/* Main Content */}
45+
<div className="flex-1">
46+
<div className="flex justify-between items-center mb-8">
47+
<h1 className="text-2xl font-medium">
48+
{title} <span className="text-gray-500">({filteredProducts.length})</span>
49+
</h1>
50+
51+
<div className="flex items-center gap-4">
52+
<label className="text-sm">Vis produkter:</label>
53+
<select
54+
value={sortBy}
55+
onChange={(e) => setSortBy(e.target.value)}
56+
className="border rounded-md px-3 py-1"
57+
>
58+
<option value="popular">Populær</option>
59+
<option value="price-low">Pris: Lav til Høy</option>
60+
<option value="price-high">Pris: Høy til Lav</option>
61+
<option value="newest">Nyeste</option>
62+
</select>
63+
</div>
64+
</div>
65+
66+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
67+
{filteredProducts.map((product: Product) => (
68+
<ProductCard
69+
key={product.databaseId}
70+
databaseId={product.databaseId}
71+
name={product.name}
72+
price={product.price}
73+
slug={product.slug}
74+
image={product.image}
75+
/>
76+
))}
77+
</div>
78+
</div>
79+
</div>
80+
);
81+
};
82+
83+
export default ProductList;

src/hooks/useProductFilters.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { useState } from 'react';
2+
import { Product, ProductType } from '@/types/product';
3+
import { getUniqueProductTypes } from '@/utils/functions/productUtils';
4+
5+
export const useProductFilters = (products: Product[]) => {
6+
const [sortBy, setSortBy] = useState('popular');
7+
const [selectedSizes, setSelectedSizes] = useState<string[]>([]);
8+
const [selectedColors, setSelectedColors] = useState<string[]>([]);
9+
const [priceRange, setPriceRange] = useState<[number, number]>([0, 1000]);
10+
const [productTypes, setProductTypes] = useState<ProductType[]>(() =>
11+
products ? getUniqueProductTypes(products) : [],
12+
);
13+
14+
const toggleProductType = (id: string) => {
15+
setProductTypes((prev) =>
16+
prev.map((type) =>
17+
type.id === id ? { ...type, checked: !type.checked } : type,
18+
),
19+
);
20+
};
21+
22+
const resetFilters = () => {
23+
setSelectedSizes([]);
24+
setSelectedColors([]);
25+
setPriceRange([0, 1000]);
26+
setProductTypes((prev) =>
27+
prev.map((type) => ({ ...type, checked: false })),
28+
);
29+
};
30+
31+
const filterProducts = (products: Product[]) => {
32+
const filtered = products?.filter((product: Product) => {
33+
// Filter by price
34+
const productPrice = parseFloat(product.price.replace(/[^0-9.]/g, ''));
35+
const withinPriceRange =
36+
productPrice >= priceRange[0] && productPrice <= priceRange[1];
37+
if (!withinPriceRange) return false;
38+
39+
// Filter by product type
40+
const selectedTypes = productTypes
41+
.filter((t) => t.checked)
42+
.map((t) => t.name.toLowerCase());
43+
if (selectedTypes.length > 0) {
44+
const productCategories =
45+
product.productCategories?.nodes.map((cat) =>
46+
cat.name.toLowerCase(),
47+
) || [];
48+
if (!selectedTypes.some((type) => productCategories.includes(type)))
49+
return false;
50+
}
51+
52+
// Filter by size
53+
if (selectedSizes.length > 0) {
54+
const productSizes =
55+
product.allPaSizes?.nodes.map((node) => node.name) || [];
56+
if (!selectedSizes.some((size) => productSizes.includes(size)))
57+
return false;
58+
}
59+
60+
// Filter by color
61+
if (selectedColors.length > 0) {
62+
const productColors =
63+
product.allPaColors?.nodes.map((node) => node.name) || [];
64+
if (!selectedColors.some((color) => productColors.includes(color)))
65+
return false;
66+
}
67+
68+
return true;
69+
});
70+
71+
// Sort products
72+
return [...(filtered || [])].sort((a, b) => {
73+
const priceA = parseFloat(a.price.replace(/[^0-9.]/g, ''));
74+
const priceB = parseFloat(b.price.replace(/[^0-9.]/g, ''));
75+
76+
switch (sortBy) {
77+
case 'price-low':
78+
return priceA - priceB;
79+
case 'price-high':
80+
return priceB - priceA;
81+
case 'newest':
82+
return b.databaseId - a.databaseId;
83+
default: // 'popular'
84+
return 0;
85+
}
86+
});
87+
};
88+
89+
return {
90+
sortBy,
91+
setSortBy,
92+
selectedSizes,
93+
setSelectedSizes,
94+
selectedColors,
95+
setSelectedColors,
96+
priceRange,
97+
setPriceRange,
98+
productTypes,
99+
toggleProductType,
100+
resetFilters,
101+
filterProducts,
102+
};
103+
};

src/pages/products2.tsx

Lines changed: 20 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,32 @@
1-
import { useState } from 'react';
21
import Head from 'next/head';
32
import Layout from '@/components/Layout/Layout.component';
4-
import ProductCard from '@/components/Product/ProductCard.component';
5-
import ProductFilters from '@/components/Product/ProductFilters.component';
3+
import ProductList from '@/components/Product/ProductList.component';
64
import client from '@/utils/apollo/ApolloClient';
75
import { FETCH_ALL_PRODUCTS_QUERY } from '@/utils/gql/GQL_QUERIES';
86
import type { NextPage, GetStaticProps, InferGetStaticPropsType } from 'next';
9-
10-
import { Product, ProductCategory, ProductType, SizeNode, ColorNode } from '@/types/product';
11-
import { getUniqueProductTypes } from '@/utils/functions/productUtils';
7+
import { Product } from '@/types/product';
128

139
const Products2: NextPage = ({
1410
products,
1511
loading,
1612
}: InferGetStaticPropsType<typeof getStaticProps>) => {
17-
const [sortBy, setSortBy] = useState('popular');
18-
const [selectedSizes, setSelectedSizes] = useState<string[]>([]);
19-
const [selectedColors, setSelectedColors] = useState<string[]>([]);
20-
const [priceRange, setPriceRange] = useState<[number, number]>([0, 1000]);
21-
const [productTypes, setProductTypes] = useState<ProductType[]>(() =>
22-
products ? getUniqueProductTypes(products) : []
23-
);
24-
25-
const toggleProductType = (id: string) => {
26-
setProductTypes(prev => prev.map(type =>
27-
type.id === id ? { ...type, checked: !type.checked } : type
28-
));
29-
};
30-
31-
const resetFilters = () => {
32-
setSelectedSizes([]);
33-
setSelectedColors([]);
34-
setPriceRange([0, 1000]);
35-
setProductTypes(prev => prev.map(type => ({ ...type, checked: false })));
36-
};
37-
38-
// Filter products based on selected filters
39-
const filteredProducts = products?.filter((product: Product) => {
40-
// Filter by price
41-
const productPrice = parseFloat(product.price.replace(/[^0-9.]/g, ''));
42-
const withinPriceRange = productPrice >= priceRange[0] && productPrice <= priceRange[1];
43-
if (!withinPriceRange) return false;
44-
45-
// Filter by product type
46-
const selectedTypes = productTypes.filter(t => t.checked).map(t => t.name.toLowerCase());
47-
if (selectedTypes.length > 0) {
48-
const productCategories = product.productCategories?.nodes.map((cat: ProductCategory) => cat.name.toLowerCase()) || [];
49-
if (!selectedTypes.some(type => productCategories.includes(type))) return false;
50-
}
51-
52-
// Filter by size
53-
if (selectedSizes.length > 0) {
54-
const productSizes = product.allPaSizes?.nodes.map((node: SizeNode) => node.name) || [];
55-
if (!selectedSizes.some(size => productSizes.includes(size))) return false;
56-
}
57-
58-
// Filter by color
59-
if (selectedColors.length > 0) {
60-
const productColors = product.allPaColors?.nodes.map((node: ColorNode) => node.name) || [];
61-
if (!selectedColors.some(color => productColors.includes(color))) return false;
62-
}
63-
64-
return true;
65-
});
66-
67-
// Sort products
68-
const sortedProducts = [...(filteredProducts || [])].sort((a, b) => {
69-
const priceA = parseFloat(a.price.replace(/[^0-9.]/g, ''));
70-
const priceB = parseFloat(b.price.replace(/[^0-9.]/g, ''));
71-
72-
switch (sortBy) {
73-
case 'price-low':
74-
return priceA - priceB;
75-
case 'price-high':
76-
return priceB - priceA;
77-
case 'newest':
78-
return b.databaseId - a.databaseId;
79-
default: // 'popular'
80-
return 0;
81-
}
82-
});
83-
84-
if (loading) return (
85-
<Layout title="Produkter">
86-
<div className="flex justify-center items-center min-h-screen">
87-
<div className="animate-spin rounded-full h-32 w-32 border-t-2 border-b-2 border-gray-900"></div>
88-
</div>
89-
</Layout>
90-
);
91-
92-
if (!products) return (
93-
<Layout title="Produkter">
94-
<div className="flex justify-center items-center min-h-screen">
95-
<p className="text-red-500">Ingen produkter funnet</p>
96-
</div>
97-
</Layout>
98-
);
13+
if (loading)
14+
return (
15+
<Layout title="Produkter">
16+
<div className="flex justify-center items-center min-h-screen">
17+
<div className="animate-spin rounded-full h-32 w-32 border-t-2 border-b-2 border-gray-900"></div>
18+
</div>
19+
</Layout>
20+
);
21+
22+
if (!products)
23+
return (
24+
<Layout title="Produkter">
25+
<div className="flex justify-center items-center min-h-screen">
26+
<p className="text-red-500">Ingen produkter funnet</p>
27+
</div>
28+
</Layout>
29+
);
9930

10031
return (
10132
<Layout title="Produkter">
@@ -104,56 +35,7 @@ const Products2: NextPage = ({
10435
</Head>
10536

10637
<div className="container mx-auto px-4 py-8">
107-
<div className="flex flex-col md:flex-row gap-8">
108-
<ProductFilters
109-
selectedSizes={selectedSizes}
110-
setSelectedSizes={setSelectedSizes}
111-
selectedColors={selectedColors}
112-
setSelectedColors={setSelectedColors}
113-
priceRange={priceRange}
114-
setPriceRange={setPriceRange}
115-
productTypes={productTypes}
116-
toggleProductType={toggleProductType}
117-
products={products}
118-
resetFilters={resetFilters}
119-
/>
120-
121-
{/* Main Content */}
122-
<div className="flex-1">
123-
<div className="flex justify-between items-center mb-8">
124-
<h1 className="text-2xl font-medium">
125-
Herreklær <span className="text-gray-500">({sortedProducts.length})</span>
126-
</h1>
127-
128-
<div className="flex items-center gap-4">
129-
<label className="text-sm">Vis produkter:</label>
130-
<select
131-
value={sortBy}
132-
onChange={(e) => setSortBy(e.target.value)}
133-
className="border rounded-md px-3 py-1"
134-
>
135-
<option value="popular">Populær</option>
136-
<option value="price-low">Pris: Lav til Høy</option>
137-
<option value="price-high">Pris: Høy til Lav</option>
138-
<option value="newest">Nyeste</option>
139-
</select>
140-
</div>
141-
</div>
142-
143-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
144-
{sortedProducts.map((product: Product) => (
145-
<ProductCard
146-
key={product.databaseId}
147-
databaseId={product.databaseId}
148-
name={product.name}
149-
price={product.price}
150-
slug={product.slug}
151-
image={product.image}
152-
/>
153-
))}
154-
</div>
155-
</div>
156-
</div>
38+
<ProductList products={products} title="Herreklær" />
15739
</div>
15840
</Layout>
15941
);

0 commit comments

Comments
 (0)