Skip to content

Commit

Permalink
feat: add React Query to product/category data hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
jwinr committed Sep 20, 2024
1 parent 975d6c4 commit de2f60c
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 99 deletions.
27 changes: 10 additions & 17 deletions src/app/products/[slug]/[variantSku]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use client'

import { useEffect, useState } from 'react'
import styled from 'styled-components'
import Breadcrumb from '@/components/Elements/Breadcrumb'
import { useParams } from 'next/navigation'
Expand All @@ -14,11 +13,12 @@ import AddToCartButton from '@/components/Shopping/AddToCartButton'
import useProductData from 'src/hooks/useProductData'
import ProductThumbnails from '@/components/Products/ProductThumbnails'
import { Product } from '@/types/product'
import { useState } from 'react'

const PageWrapper = styled.div`
display: flex;
flex-direction: column;
padding: 25px 15px 75px 15px; // Leave some room for the breadcrumb component on mobile view
padding: 25px 15px 75px 15px;
@media (min-width: 768px) {
padding: 45px 75px 75px 75px;
Expand Down Expand Up @@ -52,59 +52,52 @@ const AddCartWrapper = styled.div`

function ProductDetails() {
const { slug, variantSku } = useParams()
const { product, categoryName, categorySlug } = useProductData(
const { product, categoryName, categorySlug, isLoading } = useProductData(
slug as string,
variantSku as string
)
const { deliveryDate, dayOfWeek, returnDate } = ShippingInfo()
const isMobileView = useMobileView()
const [loading, setLoading] = useState<boolean>(true)
const [selectedSizeVariantId, setSelectedSizeVariantId] = useState<
string | undefined
>()
const [hoveredImage, setHoveredImage] = useState<number>(0)

useEffect(() => {
if (product) {
setLoading(false)
}
}, [product])

const handleThumbnailHover = (index: number) => {
setHoveredImage(index)
}

return (
<div>
<Breadcrumb
loading={loading}
loading={isLoading}
categoryName={categoryName}
categorySlug={categorySlug}
/>
<PageWrapper>
<MainSection>
<ProductImageGallery
loading={loading}
loading={isLoading}
product={product as Product}
isMobileView={isMobileView}
hoveredImage={hoveredImage}
/>
<InfoCardWrapper>
<ProductInfo
loading={loading}
loading={isLoading}
product={product as Product}
deliveryDate={deliveryDate}
dayOfWeek={dayOfWeek}
returnDate={returnDate}
/>
<ProductAttributes
product={product as Product}
loading={loading}
loading={isLoading}
onSizeVariantSelected={setSelectedSizeVariantId}
/>
<AddCartWrapper>
<AddToCartButton
loading={loading}
loading={isLoading}
sizeVariantId={Number(selectedSizeVariantId) || 0}
quantity={1}
productName={product?.name || ''}
Expand All @@ -114,7 +107,7 @@ function ProductDetails() {
</MainSection>
{!isMobileView && (
<ProductThumbnails
loading={loading}
loading={isLoading}
images={
(product && 'images' in product ? product.images : []) as {
image_url: string
Expand All @@ -125,7 +118,7 @@ function ProductDetails() {
onThumbnailHover={handleThumbnailHover}
/>
)}
<ProductOverview loading={loading} product={product as Product} />
<ProductOverview loading={isLoading} product={product as Product} />
</PageWrapper>
</div>
)
Expand Down
2 changes: 1 addition & 1 deletion src/components/Products/ProductAttributes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const LoaderAttributes = styled.div`
display: flex;
flex-direction: column;
width: 100%;
padding-bottom: 25%;
min-height: 100px;
border-radius: 8px;
background-color: #d6d6d6;
animation: loadingAnimation 2s ease-in-out infinite;
Expand Down
8 changes: 2 additions & 6 deletions src/components/Products/ProductOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,7 @@ const LoaderDescription = styled.div`
min-height: 250px;
width: 75%;
background-color: #d6d6d6;
animation:
enter 0.3s forwards,
loadingAnimation 2s ease-in-out infinite;
animation: loadingAnimation 2s ease-in-out infinite;
animation-fill-mode: forwards;
@media (max-width: 768px) {
Expand All @@ -98,9 +96,7 @@ const LoaderFeatures = styled.div`
min-height: 250px;
width: 75%;
background-color: #d6d6d6;
animation:
enter 0.3s forwards,
loadingAnimation 2s ease-in-out infinite;
animation: loadingAnimation 2s ease-in-out infinite;
animation-fill-mode: forwards;
@media (max-width: 768px) {
Expand Down
94 changes: 54 additions & 40 deletions src/hooks/useCategoryData.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { useState, useEffect } from 'react'
import { Attribute } from '@/types/product'

interface Filter {
Expand Down Expand Up @@ -36,61 +37,74 @@ interface CategoryData {
}

interface UseCategoryDataReturn {
categoryData: CategoryData | null
categoryData: CategoryData | undefined
loading: boolean
filteredItems: Product[]
setFilteredItems: React.Dispatch<React.SetStateAction<Product[]>>
setLoading: React.Dispatch<React.SetStateAction<boolean>>
}

const fetchCategoryData = async (
slug: string,
currentPage: number,
filters: Filter | null
): Promise<CategoryData> => {
const filterQuery = filters
? `&filters=${encodeURIComponent(JSON.stringify(filters))}`
: ''

const response = await fetch(
`/api/categories/${slug}?page=${currentPage}${filterQuery}`,
{
headers: {
'x-api-key': process.env.NEXT_PUBLIC_API_KEY || '',
},
}
)

if (!response.ok) {
if (response.status === 404) {
throw new Error('Category not found')
}
throw new Error('An unexpected error occurred.')
}

return response.json()
}

const useCategoryData = (
slug: string | undefined,
currentPage: number,
filters: Filter | null
): UseCategoryDataReturn => {
const [categoryData, setCategoryData] = useState<CategoryData | null>(null)
const [loading, setLoading] = useState<boolean>(true)
const [filteredItems, setFilteredItems] = useState<Product[]>([])

const {
data: categoryData,
error,
isLoading,
} = useQuery<CategoryData>({
queryKey: ['categoryData', slug, currentPage, filters],
queryFn: () => fetchCategoryData(slug as string, currentPage, filters),
enabled: !!slug,
})

// Update filteredItems once the categoryData is available
useEffect(() => {
const fetchCategoryData = async () => {
const filterQuery = filters
? `&filters=${encodeURIComponent(JSON.stringify(filters))}`
: ''

try {
const response = await fetch(
`/api/categories/${slug}?page=${currentPage}${filterQuery}`,
{
headers: {
'x-api-key': process.env.NEXT_PUBLIC_API_KEY || '',
},
}
)

if (response.ok) {
const data: CategoryData = (await response.json()) as CategoryData
setCategoryData(data)

setFilteredItems(data.products)
} else if (response.status === 404) {
throw new Error('Category not found')
} else {
throw new Error('An unexpected error occurred.')
}
} catch (error) {
console.error('Error fetching category data:', error)
} finally {
setTimeout(() => {
setLoading(false)
}, 750)
}
if (categoryData) {
setFilteredItems(categoryData.products)
}
}, [categoryData])

void fetchCategoryData()
}, [slug, currentPage, filters])
if (error) {
console.error('Error fetching category data:', error)
}

return { categoryData, loading, filteredItems, setFilteredItems, setLoading }
return {
categoryData,
loading: isLoading,
filteredItems,
setFilteredItems,
}
}

export default useCategoryData
77 changes: 42 additions & 35 deletions src/hooks/useProductData.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'
import { useQuery } from '@tanstack/react-query'

interface Product {
id: string
Expand All @@ -8,45 +8,52 @@ interface Product {
}

interface UseProductDataReturn {
product: Product | null
categoryName: string | null
categorySlug: string | null
product: Product | undefined
categoryName: string | undefined
categorySlug: string | undefined
}

const fetchProductDetails = async (
slug: string,
variantSku: string
): Promise<Product> => {
const response = await fetch(`/api/products/${slug}/${variantSku}`, {
headers: {
'x-api-key': process.env.NEXT_PUBLIC_API_KEY || '',
},
})

if (!response.ok) {
throw new Error('Network response was not ok')
}

return response.json()
}

const useProductData = (
slug: string,
variantSku: string
): UseProductDataReturn => {
const [product, setProduct] = useState<Product | null>(null)
const [categoryName, setCategoryName] = useState<string | null>(null)
const [categorySlug, setCategorySlug] = useState<string | null>(null)

useEffect(() => {
const fetchProductDetails = async () => {
try {
const response = await fetch(`/api/products/${slug}/${variantSku}`, {
headers: {
'x-api-key': process.env.NEXT_PUBLIC_API_KEY || '',
},
})

if (response.ok) {
const data: Product = (await response.json()) as Product
setProduct(data)
setCategoryName(data.category_name)
setCategorySlug(data.category_slug)
}
} catch (error) {
console.error('Error:', error)
}
}

if (slug) {
void fetchProductDetails()
}
}, [slug, variantSku])

return { product, categoryName, categorySlug }
): UseProductDataReturn & { isLoading: boolean; error: Error | null } => {
const {
data: product,
error,
isLoading,
} = useQuery<Product>({
queryKey: ['productData', slug, variantSku],
queryFn: () => fetchProductDetails(slug, variantSku),
})

if (error) {
console.error('Error fetching product data:', error)
}

return {
product,
categoryName: product?.category_name,
categorySlug: product?.category_slug,
isLoading,
error,
}
}

export default useProductData

0 comments on commit de2f60c

Please sign in to comment.