Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 2 additions & 21 deletions src/ProductCard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import { type Product } from './data/products'
import { getAverageRating } from './utils/reviews'
import ProductImage from './components/product/ProductImage'
import ProductMeta from './components/product/ProductMeta'
import AddToCartButton from './components/product/AddToCartButton'
Expand All @@ -13,25 +11,8 @@ type ProductCardProps = {
}

export default function ProductCard({ product, onAdd, isHighlighted }: ProductCardProps) {
const [averageRating, setAverageRating] = useState<number | null>(null)

useEffect(() => {
const loadRating = () => {
const rating = getAverageRating(product.id)
setAverageRating(rating)
}
loadRating()

const handleReviewAdded = () => {
const newRating = getAverageRating(product.id)
setAverageRating(newRating)
}

window.addEventListener('reviewAdded', handleReviewAdded)
return () => {
window.removeEventListener('reviewAdded', handleReviewAdded)
}
}, [product.id])
// Use product.rating from database (which should be kept up to date)
const averageRating = product.rating || null

return (
<Link
Expand Down
50 changes: 29 additions & 21 deletions src/ProductPage.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useState, useEffect } from 'react'
import { useState, useEffect, useMemo } from 'react'
import { useParams } from 'react-router-dom'
import { useQuery } from '@apollo/client/react'
import Reviews from './Reviews'
import { getAverageRating } from './utils/reviews'
import { GET_PRODUCT } from './graphql/queries'
import { GET_PRODUCT, GET_REVIEWS } from './graphql/queries'
import type { Product } from './data/products'
import LoadingState from './components/LoadingState'
import EmptyState from './components/EmptyState'
Expand All @@ -23,47 +23,55 @@ type ProductQueryResult = {
export default function ProductPage({ onAddToCart, isHighlighted }: ProductPageProps) {
const { id } = useParams<{ id: string }>()
const [selectedColor, setSelectedColor] = useState<string | null>(null)
const [averageRating, setAverageRating] = useState<number | null>(null)

const { loading, error, data } = useQuery<ProductQueryResult>(GET_PRODUCT, {
variables: { id: id || '' },
skip: !id,
})

const { data: reviewsData, refetch: refetchReviews } = useQuery<{
reviews: Array<{
id: string
productId: string
userName: string
text: string
rating: number
createdAt: string
}>
}>(GET_REVIEWS, {
variables: { productId: id || '' },
skip: !id,
})

const product = data?.product || null

// Scroll to top when product page loads or product changes
useEffect(() => {
window.scrollTo({ top: 0, behavior: 'smooth' })
}, [id])

useEffect(() => {
if (!product) return
const loadRating = () => {
const rating = getAverageRating(product.id)
setAverageRating(rating)
// Calculate average rating from reviews
const averageRating = useMemo(() => {
if (reviewsData?.reviews) {
return getAverageRating(reviewsData.reviews)
}
loadRating()
}, [product])
return null
}, [reviewsData])

// Listen for storage changes to update rating when new reviews are added
// Listen for review added event to refetch reviews
useEffect(() => {
if (!product) return
if (!id) return

const handleStorageChange = () => {
const rating = getAverageRating(product.id)
setAverageRating(rating)
const handleReviewAdded = () => {
refetchReviews()
}

window.addEventListener('storage', handleStorageChange)
// Also listen for custom event from Reviews component
window.addEventListener('reviewAdded', handleStorageChange)
window.addEventListener('reviewAdded', handleReviewAdded)

return () => {
window.removeEventListener('storage', handleStorageChange)
window.removeEventListener('reviewAdded', handleStorageChange)
window.removeEventListener('reviewAdded', handleReviewAdded)
}
}, [product])
}, [id, refetchReviews])

if (loading) {
return <LoadingState />
Expand Down
102 changes: 48 additions & 54 deletions src/Reviews.tsx
Original file line number Diff line number Diff line change
@@ -1,82 +1,76 @@
import { useState, useEffect } from 'react'
import { useState } from 'react'
import { useQuery, useMutation } from '@apollo/client/react'
import ReviewItem from './components/reviews/ReviewItem'
import ReviewForm from './components/reviews/ReviewForm'
import { GET_REVIEWS, CREATE_REVIEW } from './graphql/queries'

type Review = {
id: string
productId: string
name: string
userName: string
text: string
rating: number
date: string
createdAt: string
}

type ReviewsProps = {
productId: string
}

export default function Reviews({ productId }: ReviewsProps) {
const [reviews, setReviews] = useState<Review[]>([])
const [error, setError] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)

useEffect(() => {
const loadReviews = () => {
const stored = localStorage.getItem(`reviews-${productId}`)
if (stored) {
try {
const parsed = JSON.parse(stored) as Review[]
// Filter out old reviews without ratings
const validReviews = parsed.filter((review) => review.rating && review.rating > 0)
setReviews(validReviews)
// Update localStorage to remove old reviews
if (validReviews.length !== parsed.length) {
localStorage.setItem(`reviews-${productId}`, JSON.stringify(validReviews))
}
} catch {
// Invalid JSON, ignore
}
}
}
loadReviews()
}, [productId])

const handleSubmit = async (review: { name: string; text: string; rating: number }) => {
setIsSubmitting(true)
setError('')

// Simulate a brief delay for better UX
return new Promise<void>((resolve) => {
setTimeout(() => {
const newReview: Review = {
id: Date.now().toString(),
productId,
name: review.name,
text: review.text,
rating: review.rating,
date: new Date().toISOString(),
}

const updatedReviews = [newReview, ...reviews]
setReviews(updatedReviews)
localStorage.setItem(`reviews-${productId}`, JSON.stringify(updatedReviews))
const [formError, setFormError] = useState('')
const {
data,
loading,
error: queryError,
refetch,
} = useQuery<{ reviews: Review[] }>(GET_REVIEWS, {
variables: { productId },
})

const [createReview, { loading: isSubmitting, error: mutationError }] = useMutation(
CREATE_REVIEW,
{
onCompleted: () => {
setFormError('')
refetch()
// Dispatch custom event to notify other components
window.dispatchEvent(new CustomEvent('reviewAdded', { detail: { productId } }))
},
},
)

const reviews = data?.reviews || []
const error = formError || mutationError?.message || queryError?.message || ''

setIsSubmitting(false)
resolve()
}, 300)
})
const handleSubmit = async (review: { name?: string; text: string; rating: number }) => {
setFormError('')
try {
await createReview({
variables: {
createReviewInput: {
productId,
text: review.text,
rating: review.rating,
name: review.name,
},
},
})
} catch (err) {
// Error is handled by mutation error
console.error('Failed to create review:', err)
}
}

return (
<section className="mt-16 pt-12 border-t border-slate-200">
<h2 className="text-3xl m-0 mb-8">Customer Reviews</h2>

{reviews.length > 0 && (
{loading && <p className="text-slate-600">Loading reviews...</p>}

{!loading && reviews.length > 0 && (
<div className="flex flex-col gap-6 mb-12">
{reviews.map((review) => (
{reviews.map((review: Review) => (
<ReviewItem key={review.id} review={review} />
))}
</div>
Expand All @@ -86,7 +80,7 @@ export default function Reviews({ productId }: ReviewsProps) {
onSubmit={handleSubmit}
isSubmitting={isSubmitting}
error={error}
onError={setError}
onError={setFormError}
/>
</section>
)
Expand Down
Loading