diff --git a/src/ProductCard.tsx b/src/ProductCard.tsx index 647de8a..0e75e5f 100644 --- a/src/ProductCard.tsx +++ b/src/ProductCard.tsx @@ -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' @@ -13,25 +11,8 @@ type ProductCardProps = { } export default function ProductCard({ product, onAdd, isHighlighted }: ProductCardProps) { - const [averageRating, setAverageRating] = useState(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 ( () const [selectedColor, setSelectedColor] = useState(null) - const [averageRating, setAverageRating] = useState(null) const { loading, error, data } = useQuery(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 @@ -37,33 +50,28 @@ export default function ProductPage({ onAddToCart, isHighlighted }: ProductPageP 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 diff --git a/src/Reviews.tsx b/src/Reviews.tsx index 4b34f58..439e6da 100644 --- a/src/Reviews.tsx +++ b/src/Reviews.tsx @@ -1,14 +1,16 @@ -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 = { @@ -16,67 +18,59 @@ type ReviewsProps = { } export default function Reviews({ productId }: ReviewsProps) { - const [reviews, setReviews] = useState([]) - 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((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 (

Customer Reviews

- {reviews.length > 0 && ( + {loading &&

Loading reviews...

} + + {!loading && reviews.length > 0 && (
- {reviews.map((review) => ( + {reviews.map((review: Review) => ( ))}
@@ -86,7 +80,7 @@ export default function Reviews({ productId }: ReviewsProps) { onSubmit={handleSubmit} isSubmitting={isSubmitting} error={error} - onError={setError} + onError={setFormError} />
) diff --git a/src/components/reviews/ReviewForm.tsx b/src/components/reviews/ReviewForm.tsx index 9febd14..02c81f2 100644 --- a/src/components/reviews/ReviewForm.tsx +++ b/src/components/reviews/ReviewForm.tsx @@ -1,6 +1,7 @@ import { useState } from 'react' import StarRating from './StarRating' import ErrorMessage from '../ErrorMessage' +import { useAuth } from '../../contexts/AuthContext' function generateCaptcha() { const num1 = Math.floor(Math.random() * 10) + 1 @@ -9,13 +10,14 @@ function generateCaptcha() { } type ReviewFormProps = { - onSubmit: (review: { name: string; text: string; rating: number }) => Promise + onSubmit: (review: { name?: string; text: string; rating: number }) => Promise isSubmitting: boolean error: string onError: (error: string) => void } export default function ReviewForm({ onSubmit, isSubmitting, error, onError }: ReviewFormProps) { + const { isAuthenticated } = useAuth() const [name, setName] = useState('') const [text, setText] = useState('') const [rating, setRating] = useState(0) @@ -26,7 +28,8 @@ export default function ReviewForm({ onSubmit, isSubmitting, error, onError }: R e.preventDefault() onError('') - if (!name.trim()) { + // Only require name if user is not authenticated + if (!isAuthenticated && !name.trim()) { onError('Please enter your name') return } @@ -41,22 +44,31 @@ export default function ReviewForm({ onSubmit, isSubmitting, error, onError }: R return } - const answer = parseInt(captchaAnswer, 10) - if (isNaN(answer) || answer !== captcha.answer) { - onError('Incorrect captcha answer. Please try again.') - setCaptcha(generateCaptcha()) - setCaptchaAnswer('') - return + // Only validate captcha if user is not authenticated + if (!isAuthenticated) { + const answer = parseInt(captchaAnswer, 10) + if (isNaN(answer) || answer !== captcha.answer) { + onError('Incorrect captcha answer. Please try again.') + setCaptcha(generateCaptcha()) + setCaptchaAnswer('') + return + } } try { - await onSubmit({ name: name.trim(), text: text.trim(), rating }) + await onSubmit({ + name: isAuthenticated ? undefined : name.trim(), + text: text.trim(), + rating + }) // Reset form on success setName('') setText('') setRating(0) - setCaptchaAnswer('') - setCaptcha(generateCaptcha()) + if (!isAuthenticated) { + setCaptchaAnswer('') + setCaptcha(generateCaptcha()) + } } catch { // Error handling is done by parent component } @@ -69,20 +81,22 @@ export default function ReviewForm({ onSubmit, isSubmitting, error, onError }: R >

Write a review

-
- - setName(e.target.value)} - placeholder="Enter your name" - disabled={isSubmitting} - /> -
+ {!isAuthenticated && ( +
+ + setName(e.target.value)} + placeholder="Enter your name" + disabled={isSubmitting} + /> +
+ )}
@@ -104,20 +118,22 @@ export default function ReviewForm({ onSubmit, isSubmitting, error, onError }: R />
-
- - setCaptchaAnswer(e.target.value)} - placeholder="Enter answer" - disabled={isSubmitting} - /> -
+ {!isAuthenticated && ( +
+ + setCaptchaAnswer(e.target.value)} + placeholder="Enter answer" + disabled={isSubmitting} + /> +
+ )} {error && } diff --git a/src/components/reviews/ReviewItem.tsx b/src/components/reviews/ReviewItem.tsx index 9aff753..13b195e 100644 --- a/src/components/reviews/ReviewItem.tsx +++ b/src/components/reviews/ReviewItem.tsx @@ -3,10 +3,10 @@ import StarRating from './StarRating' type Review = { id: string productId: string - name: string + userName: string text: string rating: number - date: string + createdAt: string } type ReviewItemProps = { @@ -27,11 +27,11 @@ export default function ReviewItem({ review }: ReviewItemProps) {
-

{review.name}

+

{review.userName}

-

{review.text}

diff --git a/src/graphql/queries.ts b/src/graphql/queries.ts index f69745f..8a2b609 100644 --- a/src/graphql/queries.ts +++ b/src/graphql/queries.ts @@ -100,3 +100,35 @@ export const MY_ORDERS = gql` } } ` + +export const GET_REVIEWS = gql` + query GetReviews($productId: ID!) { + reviews(productId: $productId) { + id + productId + text + rating + userName + createdAt + } + } +` + +export const CREATE_REVIEW = gql` + mutation CreateReview($createReviewInput: CreateReviewInput!) { + createReview(createReviewInput: $createReviewInput) { + id + productId + text + rating + userName + createdAt + } + } +` + +export const DELETE_REVIEW = gql` + mutation DeleteReview($id: ID!) { + deleteReview(id: $id) + } +` diff --git a/src/graphql/schema.ts b/src/graphql/schema.ts index 19b3b61..d23eae0 100644 --- a/src/graphql/schema.ts +++ b/src/graphql/schema.ts @@ -36,6 +36,24 @@ export const typeDefs = `#graphql createdAt: String! } + type Review { + id: ID! + productId: ID! + name: String + text: String! + rating: Int! + userName: String! + createdAt: String! + updatedAt: String! + } + + input CreateReviewInput { + productId: ID! + text: String! + rating: Int! + name: String + } + input CheckoutItemInput { productId: ID! quantity: Int! @@ -62,10 +80,13 @@ export const typeDefs = `#graphql product(id: ID!): Product checkout(id: ID!): Checkout checkouts(status: String): [Checkout!]! + reviews(productId: ID!): [Review!]! } type Mutation { createCheckout(input: CheckoutInput!): Checkout! cancelCheckout(id: ID!): Checkout! + createReview(input: CreateReviewInput!): Review! + deleteReview(id: ID!): Boolean! } ` diff --git a/src/utils/reviews.ts b/src/utils/reviews.ts index 4afac5d..372b6bd 100644 --- a/src/utils/reviews.ts +++ b/src/utils/reviews.ts @@ -1,25 +1,18 @@ type Review = { id: string productId: string - name: string + userName: string text: string rating: number - date: string + createdAt: string } -export function getAverageRating(productId: string): number | null { - const stored = localStorage.getItem(`reviews-${productId}`) - if (!stored) return null +export function getAverageRating(reviews: Review[]): number | null { + if (!reviews || reviews.length === 0) return null - try { - const reviews = JSON.parse(stored) as Review[] - const validReviews = reviews.filter((review) => review.rating && review.rating > 0) - if (validReviews.length === 0) return null + const validReviews = reviews.filter((review) => review.rating && review.rating > 0) + if (validReviews.length === 0) return null - const sum = validReviews.reduce((acc, review) => acc + review.rating, 0) - return sum / validReviews.length - } catch { - return null - } + const sum = validReviews.reduce((acc, review) => acc + review.rating, 0) + return sum / validReviews.length } -