Skip to content

Commit ea9faa7

Browse files
committed
Add filter and sorting
1 parent 0dd2cc1 commit ea9faa7

File tree

6 files changed

+282
-1
lines changed

6 files changed

+282
-1
lines changed

components/Products/ProductCard.vue

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<template>
2+
<div class="flex flex-col mt-6 sm:w-1/2 md:w-1/3 lg:w-1/4 lg:mr-4">
3+
<NuxtLink
4+
class="text-black cursor-pointer hover:underline"
5+
:to="productLink(product)"
6+
>
7+
<ProductImage :alt="product.name" :src="productImage(product)" />
8+
<div class="flex justify-center pt-3">
9+
<p class="text-2xl font-bold text-center cursor-pointer">
10+
{{ product.name }}
11+
</p>
12+
</div>
13+
</NuxtLink>
14+
<ProductPrice
15+
:product="product"
16+
priceFontSize="normal"
17+
:shouldCenterPrice="true"
18+
/>
19+
</div>
20+
</template>
21+
22+
<script setup>
23+
const props = defineProps({
24+
product: {
25+
type: Object,
26+
required: true,
27+
},
28+
});
29+
30+
const config = useRuntimeConfig();
31+
32+
const productLink = (product) => {
33+
return {
34+
path: "/product/" + product.slug,
35+
query: { id: product.databaseId },
36+
};
37+
};
38+
39+
const productImage = (product) =>
40+
product.image ? product.image.sourceUrl : config.public.placeholderImage;
41+
</script>
42+
43+
<style scoped>
44+
a:hover {
45+
border: none;
46+
}
47+
</style>

components/Products/ProductFilter.vue

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<template>
2+
<div class="w-full md:w-1/4 p-4">
3+
<h3 class="text-lg font-bold mb-2">Product Type</h3>
4+
<div v-for="productType in store.productTypes" :key="productType.id">
5+
<input
6+
type="checkbox"
7+
:id="productType.id"
8+
:checked="productType.checked"
9+
@change="store.toggleProductType(productType.id)"
10+
/>
11+
<label :for="productType.id" class="ml-2">{{ productType.name }}</label>
12+
</div>
13+
14+
<h3 class="text-lg font-bold mt-4 mb-2">Price</h3>
15+
<div class="flex items-center" v-if="store.priceRange">
16+
<input
17+
type="range"
18+
min="0"
19+
max="1000"
20+
:value="store.priceRange[0]"
21+
@input="updatePriceRange($event, 0)"
22+
class="w-full"
23+
/>
24+
<span class="mx-2">{{ store.priceRange[0] }}</span>
25+
<span>-</span>
26+
<input
27+
type="range"
28+
min="0"
29+
max="1000"
30+
:value="store.priceRange[1]"
31+
@input="updatePriceRange($event, 1)"
32+
class="w-full"
33+
/>
34+
<span class="ml-2">{{ store.priceRange[1] }}</span>
35+
</div>
36+
37+
<button
38+
@click="store.resetFilters()"
39+
class="mt-4 bg-gray-200 px-4 py-2 rounded"
40+
>
41+
Reset Filter
42+
</button>
43+
</div>
44+
</template>
45+
46+
<script setup>
47+
import { useProductsStore } from "@/store/useProductsStore";
48+
49+
const store = useProductsStore();
50+
51+
const updatePriceRange = (event, index) => {
52+
const newRange = [...store.priceRange];
53+
newRange[index] = parseInt(event.target.value, 10);
54+
store.setPriceRange(newRange);
55+
};
56+
</script>

components/Products/ProductGrid.vue

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<template>
2+
<div class="w-full md:w-3/4 p-4">
3+
<div class="flex justify-end mb-4">
4+
<ProductSort />
5+
</div>
6+
<div id="product-container" class="flex flex-wrap items-center">
7+
<template v-if="store.loading">
8+
<SpinnerLoading />
9+
</template>
10+
<template v-else-if="store.error">
11+
<p>Error loading products.</p>
12+
</template>
13+
<template v-else>
14+
<ProductCard
15+
v-for="product in store.filteredProducts"
16+
:key="product.id"
17+
:product="product"
18+
/>
19+
</template>
20+
</div>
21+
</div>
22+
</template>
23+
24+
<script setup>
25+
import { useProductsStore } from "@/store/useProductsStore";
26+
27+
const store = useProductsStore();
28+
29+
onMounted(() => {
30+
store.fetchProducts();
31+
});
32+
</script>

components/Products/ProductSort.vue

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<template>
2+
<div class="flex items-center">
3+
<label for="sort-by" class="mr-2">Sort by:</label>
4+
<select
5+
id="sort-by"
6+
:value="store.sortBy"
7+
@change="store.setSortBy($event.target.value)"
8+
class="p-2 border rounded"
9+
>
10+
<option value="popularity">Popularity</option>
11+
<option value="price-asc">Price: Low to High</option>
12+
<option value="price-desc">Price: High to Low</option>
13+
<option value="newest">Newest</option>
14+
</select>
15+
</div>
16+
</template>
17+
18+
<script setup>
19+
import { useProductsStore } from "@/store/useProductsStore";
20+
21+
const store = useProductsStore();
22+
</script>

pages/products.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
<template>
2-
<ProductsShowAll />
2+
<div class="container mx-auto">
3+
<div class="flex flex-wrap">
4+
<ProductFilter />
5+
<ProductGrid />
6+
</div>
7+
</div>
38
</template>
49

510
<script setup>

store/useProductsStore.js

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { defineStore } from "pinia";
2+
import FETCH_ALL_PRODUCTS_QUERY from "@/apollo/queries/FETCH_ALL_PRODUCTS_QUERY.gql";
3+
4+
export const useProductsStore = defineStore("products", {
5+
state: () => ({
6+
products: [],
7+
loading: false,
8+
error: null,
9+
sortBy: "popularity",
10+
selectedSizes: [],
11+
selectedColors: [],
12+
priceRange: [0, 1000],
13+
productTypes: [],
14+
}),
15+
16+
getters: {
17+
filteredProducts(state) {
18+
let products = [...state.products];
19+
20+
// Filter by product type
21+
const selectedTypes = state.productTypes
22+
.filter((t) => t.checked)
23+
.map((t) => t.name.toLowerCase());
24+
25+
if (selectedTypes.length > 0) {
26+
products = products.filter((product) => {
27+
const productCategories =
28+
product.productCategories?.nodes.map((cat) =>
29+
cat.name.toLowerCase()
30+
) || [];
31+
return selectedTypes.some((type) =>
32+
productCategories.includes(type)
33+
);
34+
});
35+
}
36+
37+
// Filter by price
38+
products = products.filter((product) => {
39+
if (!product.price) return false;
40+
const price = parseFloat(product.price.replace(/[^0-9.-]+/g, ""));
41+
return price >= state.priceRange[0] && price <= state.priceRange[1];
42+
});
43+
44+
// Sort products
45+
return [...products].sort((a, b) => {
46+
const priceA = parseFloat(a.price.replace(/[^0-9.-]+/g, ""));
47+
const priceB = parseFloat(b.price.replace(/[^0-9.-]+/g, ""));
48+
49+
switch (state.sortBy) {
50+
case "price-asc":
51+
return priceA - priceB;
52+
case "price-desc":
53+
return priceB - priceA;
54+
case "newest":
55+
return b.databaseId - a.databaseId;
56+
default: // 'popularity'
57+
return 0;
58+
}
59+
});
60+
},
61+
getUniqueProductTypes(state) {
62+
const productTypes = state.products.flatMap(p => p.productCategories.nodes);
63+
const unique = [];
64+
const map = new Map();
65+
for (const item of productTypes) {
66+
if(!map.has(item.id)){
67+
map.set(item.id, true);
68+
unique.push({
69+
id: item.id,
70+
name: item.name,
71+
checked: false
72+
});
73+
}
74+
}
75+
return unique;
76+
}
77+
},
78+
79+
actions: {
80+
async fetchProducts() {
81+
this.loading = true;
82+
this.error = null;
83+
try {
84+
const { data, error } = await useAsyncQuery(FETCH_ALL_PRODUCTS_QUERY);
85+
86+
if (error.value) {
87+
throw new Error(error.value);
88+
}
89+
90+
if (data.value && data.value.products && data.value.products.nodes) {
91+
this.products = data.value.products.nodes;
92+
this.productTypes = this.getUniqueProductTypes;
93+
}
94+
} catch (e) {
95+
this.error = e.message;
96+
} finally {
97+
this.loading = false;
98+
}
99+
},
100+
setSortBy(sortBy) {
101+
this.sortBy = sortBy;
102+
},
103+
setPriceRange(range) {
104+
this.priceRange = range;
105+
},
106+
toggleProductType(id) {
107+
const productType = this.productTypes.find(pt => pt.id === id);
108+
if (productType) {
109+
productType.checked = !productType.checked;
110+
}
111+
},
112+
resetFilters() {
113+
this.selectedSizes = [];
114+
this.selectedColors = [];
115+
this.priceRange = [0, 1000];
116+
this.productTypes.forEach(pt => pt.checked = false);
117+
}
118+
},
119+
});

0 commit comments

Comments
 (0)