Skip to content

Commit 9e9153d

Browse files
authored
Merge pull request #1403 from w3bdesign/develop
New product page
2 parents 14e293d + e2e3ca1 commit 9e9153d

File tree

10 files changed

+556
-22
lines changed

10 files changed

+556
-22
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,14 @@ The current release has been tested and is confirmed working with the following
9494
- Pretty URLs with builtin Nextjs functionality
9595
- Tailwind 3 for styling
9696
- JSDoc comments
97+
- Product filtering:
98+
- Dynamic color filtering using Tailwind's color system
99+
- Mobile-optimized filter layout
100+
- Accessible form controls with ARIA labels
101+
- Price range slider
102+
- Size and color filters
103+
- Product type categorization
104+
- Sorting options (popularity, price, newest)
97105

98106
## Troubleshooting
99107

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "1.2.2",
44
"private": true,
55
"scripts": {
6-
"dev": "next dev",
6+
"dev": "next dev --turbopack",
77
"build": "next build",
88
"start": "next start",
99
"lint": "next lint",
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import Link from 'next/link';
2+
import Image from 'next/image';
3+
4+
interface ProductCardProps {
5+
databaseId: number;
6+
name: string;
7+
price: string;
8+
slug: string;
9+
image?: {
10+
sourceUrl?: string;
11+
};
12+
}
13+
14+
const ProductCard = ({
15+
databaseId,
16+
name,
17+
price,
18+
slug,
19+
image,
20+
}: ProductCardProps) => {
21+
return (
22+
<div className="group">
23+
<div className="aspect-[3/4] overflow-hidden bg-gray-100 relative">
24+
<Link href={`/produkt/${slug}?id=${databaseId}`}>
25+
{image?.sourceUrl ? (
26+
<Image
27+
src={image.sourceUrl}
28+
alt={name}
29+
fill
30+
className="w-full h-full object-cover object-center transition duration-300 group-hover:scale-105"
31+
priority={databaseId === 1}
32+
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
33+
/>
34+
) : (
35+
<div className="h-full w-full bg-gray-100 flex items-center justify-center">
36+
<span className="text-gray-400">No image</span>
37+
</div>
38+
)}
39+
</Link>
40+
</div>
41+
42+
<Link href={`/produkt/${slug}?id=${databaseId}`}>
43+
<div className="mt-4">
44+
<p className="text-base font-bold text-center cursor-pointer hover:text-gray-600 transition-colors">
45+
{name}
46+
</p>
47+
</div>
48+
</Link>
49+
<div className="mt-2 text-center">
50+
<span className="text-gray-900">{price}</span>
51+
</div>
52+
</div>
53+
);
54+
};
55+
56+
export default ProductCard;
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { Dispatch, SetStateAction } from 'react';
2+
import { Product, ProductType } from '@/types/product';
3+
4+
interface ProductFiltersProps {
5+
selectedSizes: string[];
6+
setSelectedSizes: Dispatch<SetStateAction<string[]>>;
7+
selectedColors: string[];
8+
setSelectedColors: Dispatch<SetStateAction<string[]>>;
9+
priceRange: [number, number];
10+
setPriceRange: Dispatch<SetStateAction<[number, number]>>;
11+
productTypes: ProductType[];
12+
toggleProductType: (id: string) => void;
13+
products: Product[];
14+
resetFilters: () => void;
15+
}
16+
17+
const ProductFilters = ({
18+
selectedSizes,
19+
setSelectedSizes,
20+
selectedColors,
21+
setSelectedColors,
22+
priceRange,
23+
setPriceRange,
24+
productTypes,
25+
toggleProductType,
26+
products,
27+
resetFilters,
28+
}: ProductFiltersProps) => {
29+
// Get unique sizes from all products
30+
const sizes = Array.from(
31+
new Set(
32+
products.flatMap(
33+
(product: Product) =>
34+
product.allPaSizes?.nodes.map(
35+
(node: { name: string }) => node.name,
36+
) || [],
37+
),
38+
),
39+
).sort((a, b) => a.localeCompare(b));
40+
41+
// Get unique colors from all products
42+
const availableColors = products
43+
.flatMap((product: Product) => product.allPaColors?.nodes || [])
44+
.filter((color, index, self) =>
45+
index === self.findIndex((c) => c.slug === color.slug)
46+
)
47+
.sort((a, b) => a.name.localeCompare(b.name));
48+
49+
const colors = availableColors.map((color) => ({
50+
name: color.name,
51+
class: `bg-${color.slug}-500`
52+
}));
53+
54+
const toggleSize = (size: string) => {
55+
setSelectedSizes((prev) =>
56+
prev.includes(size) ? prev.filter((s) => s !== size) : [...prev, size],
57+
);
58+
};
59+
60+
const toggleColor = (color: string) => {
61+
setSelectedColors((prev) =>
62+
prev.includes(color) ? prev.filter((c) => c !== color) : [...prev, color],
63+
);
64+
};
65+
66+
return (
67+
<div className="w-full md:w-64 flex-shrink-0">
68+
<div className="bg-white p-8 sm:p-6 rounded-lg shadow-sm">
69+
<div className="mb-8">
70+
<h3 className="font-semibold mb-4">PRODUKT TYPE</h3>
71+
<div className="space-y-2">
72+
{productTypes.map((type) => (
73+
<label key={type.id} className="flex items-center">
74+
<input
75+
type="checkbox"
76+
className="form-checkbox"
77+
checked={type.checked}
78+
onChange={() => toggleProductType(type.id)}
79+
/>
80+
<span className="ml-2">{type.name}</span>
81+
</label>
82+
))}
83+
</div>
84+
</div>
85+
86+
<div className="mb-8">
87+
<h3 className="font-semibold mb-4">PRIS</h3>
88+
<label htmlFor="price-range" className="sr-only">Pris</label>
89+
<input
90+
id="price-range"
91+
type="range"
92+
min="0"
93+
max="1000"
94+
value={priceRange[1]}
95+
onChange={(e) =>
96+
setPriceRange([priceRange[0], parseInt(e.target.value)])
97+
}
98+
className="w-full"
99+
/>
100+
<div className="flex justify-between mt-2">
101+
<span>kr {priceRange[0]}</span>
102+
<span>kr {priceRange[1]}</span>
103+
</div>
104+
</div>
105+
106+
<div className="mb-8">
107+
<h3 className="font-semibold mb-4">STØRRELSE</h3>
108+
<div className="grid grid-cols-3 gap-2">
109+
{sizes.map((size) => (
110+
<button
111+
key={size}
112+
onClick={() => toggleSize(size)}
113+
className={`px-3 py-1 border rounded ${
114+
selectedSizes.includes(size)
115+
? 'bg-gray-900 text-white'
116+
: 'hover:bg-gray-100'
117+
}`}
118+
>
119+
{size}
120+
</button>
121+
))}
122+
</div>
123+
</div>
124+
125+
<div className="mb-8">
126+
<h3 className="font-semibold mb-4">FARGE</h3>
127+
<div className="grid grid-cols-3 gap-2">
128+
{colors.map((color) => (
129+
<button
130+
key={color.name}
131+
onClick={() => toggleColor(color.name)}
132+
className={`w-8 h-8 rounded-full flex items-center justify-center text-xs ${
133+
color.class
134+
} ${
135+
selectedColors.includes(color.name)
136+
? 'ring-2 ring-offset-2 ring-gray-900'
137+
: ''
138+
}`}
139+
title={color.name}
140+
/>
141+
))}
142+
</div>
143+
</div>
144+
145+
<button
146+
onClick={resetFilters}
147+
className="w-full mt-8 py-2 px-4 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 transition-colors"
148+
>
149+
Resett filter
150+
</button>
151+
</div>
152+
</div>
153+
);
154+
};
155+
156+
export default ProductFilters;
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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 flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-8">
47+
<h1 className="text-xl sm:text-2xl font-medium text-center sm:text-left">
48+
{title} <span className="text-gray-500">({filteredProducts.length})</span>
49+
</h1>
50+
51+
<div className="flex flex-wrap items-center justify-center sm:justify-end gap-2 sm:gap-4">
52+
<label htmlFor="sort-select" className="text-sm font-medium">Sortering:</label>
53+
<select
54+
id="sort-select"
55+
value={sortBy}
56+
onChange={(e) => setSortBy(e.target.value)}
57+
className="min-w-[140px] border rounded-md px-3 py-1.5 text-sm"
58+
>
59+
<option value="popular">Populær</option>
60+
<option value="price-low">Pris: Lav til Høy</option>
61+
<option value="price-high">Pris: Høy til Lav</option>
62+
<option value="newest">Nyeste</option>
63+
</select>
64+
</div>
65+
</div>
66+
67+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
68+
{filteredProducts.map((product: Product) => (
69+
<ProductCard
70+
key={product.databaseId}
71+
databaseId={product.databaseId}
72+
name={product.name}
73+
price={product.price}
74+
slug={product.slug}
75+
image={product.image}
76+
/>
77+
))}
78+
</div>
79+
</div>
80+
</div>
81+
);
82+
};
83+
84+
export default ProductList;

0 commit comments

Comments
 (0)