diff --git a/app/_components/Globals/Cart.tsx b/app/_components/Globals/Cart.tsx index dcf9453d..a979ce96 100644 --- a/app/_components/Globals/Cart.tsx +++ b/app/_components/Globals/Cart.tsx @@ -88,7 +88,7 @@ const Cart = ({ isCartOpen, setIsCartOpen }: Props) => { {/* products sections */}
- {cart?.items.length === 0 || cart === null ? ( + {cart?.items?.length === 0 || cart === null ? (

There are no items in your cart yet!

) : ( <> diff --git a/app/_components/Globals/Spinner.tsx b/app/_components/Globals/Spinner.tsx index ac37ada9..af8ce726 100644 --- a/app/_components/Globals/Spinner.tsx +++ b/app/_components/Globals/Spinner.tsx @@ -9,7 +9,7 @@ export const Spinner = ({ position = 'center', size = 10, className }: SpinnerPr
({ + chosenOptions: {}, + chosenOptionsId: [], + chosenVariant: { variantId: '', variantActive: true, variantLabel: '', variantStock: 0 } }); export function useProductState(): { productState: StateProduct; updateProductProp: (property: string, value: unknown) => void; + updateProductState: (newState: StateProduct) => void; } { const [productState, setProductState] = useAtom(stateProduct); @@ -36,7 +46,11 @@ export function useProductState(): { }); }; - return { productState, updateProductProp }; + const updateProductState = (newState: StateProduct) => { + setProductState(newState); + }; + + return { productState, updateProductProp, updateProductState }; } /***************************************************************************** diff --git a/app/_lib/Store.ts b/app/_lib/Store.ts index 5873b5a1..a5398ce0 100644 --- a/app/_lib/Store.ts +++ b/app/_lib/Store.ts @@ -105,7 +105,8 @@ class Store { sku: product.sku || null, images: this.transformImages(product), categories: product.category_index?.id, - stock: product.stock_level + stock: product.stock_level, + reviewRating: product.review_rating }; } @@ -127,7 +128,7 @@ class Store { * Convert Swell product options to generic format ****************************************************************************/ transformProductOptions(product: SwellProduct) { - return product.options.map((option) => ({ + return product.options?.map((option) => ({ label: option.name, active: option.active, values: option.values @@ -139,9 +140,11 @@ class Store { ****************************************************************************/ transformProductVariants(product: SwellProduct) { return product.variants?.results?.map((variant) => ({ + id: variant.id, name: variant.name, active: variant.active, - value_ids: variant.option_value_ids + value_ids: variant.option_value_ids, + stock_variant: variant.stock_level })); } @@ -177,6 +180,59 @@ class Store { limit }; } + + async getReviews(reviewInfo: { productId: string; limit?: number; page?: number }) { + const { productId, limit = 10, page } = reviewInfo; + const reviews = await swell.get('/products:reviews', { + parent_id: productId, + limit, + page + }); + return reviews; + } + + async postReview(reviewInfo: { + userId: string; + comments: string; + productId: string; + rating: number; + title: string; + }) { + const { userId, comments, productId, rating, title } = reviewInfo; + + const reviews = await swell.post('/products:reviews', { + account_id: userId, + comments: comments, + parent_id: productId, + rating: rating, + title: title, + approved: true + }); + return reviews; + } + + async deleteReview(reviewId: string) { + const review = await swell.delete(`/products:reviews/${reviewId}`); + return review; + } + + async editReview( + reviewId: string, + reviewInfo: { + comments: string; + rating: number; + title: string; + } + ) { + const { comments, rating, title } = reviewInfo; + + const review = await swell.put(`/products:reviews/${reviewId}`, { + comments: comments, + rating: rating, + title: title + }); + return review; + } } export default new Store(); diff --git a/app/_lib/SwellAPI.ts b/app/_lib/SwellAPI.ts index 5ce789ce..d99041ec 100644 --- a/app/_lib/SwellAPI.ts +++ b/app/_lib/SwellAPI.ts @@ -1,5 +1,4 @@ import { cookies } from 'next/headers'; -import { redirect } from 'next/navigation'; import 'server-only'; import Store from './Store'; @@ -104,6 +103,7 @@ export const getLoggedUser = async (): Promise => { name } product { + id name images { file { @@ -157,7 +157,7 @@ export const getUserInfo = async () => { const user = await getLoggedUser(); if (!user?.session.accountId) { - return redirect('/account/login'); + return { authenticated: false, user: null, userId: null, orders: [], addresses: [], cards: [] }; } const addresses = await getAddresses(); @@ -166,6 +166,7 @@ export const getUserInfo = async () => { return { authenticated: true, user: user.account, + userId: user.session.accountId, orders: user.orders.results || [], addresses: addresses || [], cards: cards || [] diff --git a/app/_types/Generic/Generic.d.ts b/app/_types/Generic/Generic.d.ts index b0b09efd..077e33aa 100644 --- a/app/_types/Generic/Generic.d.ts +++ b/app/_types/Generic/Generic.d.ts @@ -1,5 +1,5 @@ interface Pagination { - total: number; + total?: number; pages: number[]; current: number; limit: number; diff --git a/app/_types/Generic/Products.d.ts b/app/_types/Generic/Products.d.ts index 8b57310f..29604244 100644 --- a/app/_types/Generic/Products.d.ts +++ b/app/_types/Generic/Products.d.ts @@ -15,6 +15,7 @@ interface Product { sku?: string | null; categories?: string[]; stock?: number; + reviewRating?: number; } interface ProductImage { @@ -29,9 +30,12 @@ interface ProductOption { } interface Variant { + id: string; name: string; active: boolean; - value_ids: string[]; + value_ids: Record | string[]; + stock_variant: number; + variantActive: boolean; } interface GenericProductsList { diff --git a/app/_types/Generic/Reviews.d.ts b/app/_types/Generic/Reviews.d.ts new file mode 100644 index 00000000..9b053ab3 --- /dev/null +++ b/app/_types/Generic/Reviews.d.ts @@ -0,0 +1,21 @@ +type Reviews = { + count: number; + page_count: number; + page: number; + results: Review[]; + pages: { + [key: string]: { start: number; end: number }; + }; +}; + +interface Review { + account_id: string; + comments: string; + parent_id: string; + rating: number; + title: string; + name: string; + date_created: string; + approved: boolean; + id: string; +} diff --git a/app/_types/Swell/Product.d.ts b/app/_types/Swell/Product.d.ts index 546dbac4..62b258eb 100644 --- a/app/_types/Swell/Product.d.ts +++ b/app/_types/Swell/Product.d.ts @@ -34,6 +34,7 @@ interface SwellProduct { sale_price?: number | null; sku?: string; category_index: CategoryIndex; + review_rating: number; id: string; } @@ -103,4 +104,5 @@ interface SwellVariant { date_updated: string; sku?: string; id: string; + stock_level: number; } diff --git a/app/_types/Swell/SwellAPI.d.ts b/app/_types/Swell/SwellAPI.d.ts index 4f901ce7..6d22032d 100644 --- a/app/_types/Swell/SwellAPI.d.ts +++ b/app/_types/Swell/SwellAPI.d.ts @@ -37,6 +37,7 @@ interface SwellAPI_Order { name: string; }; product: { + id: string; name: string; price: number; images: { diff --git a/app/_types/Swell/swell-js_index.d.ts b/app/_types/Swell/swell-js_index.d.ts index ae401255..9039d0cc 100644 --- a/app/_types/Swell/swell-js_index.d.ts +++ b/app/_types/Swell/swell-js_index.d.ts @@ -104,6 +104,7 @@ declare module 'swell-js' { variant: { name: string; id: string }; taxTotal: number; product: Product; + variant_id: string; } export interface CartItemSnakeCase { @@ -120,6 +121,7 @@ declare module 'swell-js' { variant: { name: string; id: string }; tax_total: number; product: Product; + variant_id: string; } export type CartItem = CartItemCamelCase | CartItemSnakeCase; diff --git a/app/account/(anon)/create-account/_components/RegisterForm.tsx b/app/account/(anon)/create-account/_components/RegisterForm.tsx index 74a15ce4..58bceac7 100644 --- a/app/account/(anon)/create-account/_components/RegisterForm.tsx +++ b/app/account/(anon)/create-account/_components/RegisterForm.tsx @@ -1,7 +1,6 @@ 'use client'; import Link from 'next/link'; -import { useRouter } from 'next/navigation'; import { useState } from 'react'; import { useForm, SubmitHandler } from 'react-hook-form'; import { AiOutlineEye, AiOutlineEyeInvisible } from 'react-icons/ai'; diff --git a/app/account/(anon)/reset-password/_components/ResetPasswordForm.tsx b/app/account/(anon)/reset-password/_components/ResetPasswordForm.tsx index c1d235a1..61a5ad4d 100644 --- a/app/account/(anon)/reset-password/_components/ResetPasswordForm.tsx +++ b/app/account/(anon)/reset-password/_components/ResetPasswordForm.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useRouter, useSearchParams } from 'next/navigation'; +import { useSearchParams } from 'next/navigation'; import { useRef, useState } from 'react'; import { SubmitHandler, useForm } from 'react-hook-form'; import { AiOutlineEye, AiOutlineEyeInvisible } from 'react-icons/ai'; diff --git a/app/account/(user)/addresses/_components/AddressCard.tsx b/app/account/(user)/addresses/_components/AddressCard.tsx index fd8e66ee..380d9eef 100644 --- a/app/account/(user)/addresses/_components/AddressCard.tsx +++ b/app/account/(user)/addresses/_components/AddressCard.tsx @@ -28,7 +28,7 @@ const AddressCard = ({ address }: Props) => {
{ setOpenModal={setOpenModal} />
diff --git a/app/account/(user)/layout.tsx b/app/account/(user)/layout.tsx index e70250b2..c52f082e 100644 --- a/app/account/(user)/layout.tsx +++ b/app/account/(user)/layout.tsx @@ -1,3 +1,5 @@ +import { redirect } from 'next/navigation'; + import EditProfileModal from './_components/EditProfileModal'; import AccountLink from './_components/Link'; import LogOutModal from './_components/LogOutModal'; @@ -9,6 +11,11 @@ import { getUserInfo } from '~/_lib/SwellAPI'; export default async function Layout({ children }: { children: React.ReactNode }) { const { user: account } = await getUserInfo(); + // Prevent access to account pages if user is not logged in + if (!account) { + return redirect('/account/login'); + } + return (
diff --git a/app/account/(user)/payments/_components/PaymentsCard.tsx b/app/account/(user)/payments/_components/PaymentsCard.tsx index 4a33496a..fbef5a0f 100644 --- a/app/account/(user)/payments/_components/PaymentsCard.tsx +++ b/app/account/(user)/payments/_components/PaymentsCard.tsx @@ -37,7 +37,7 @@ const PaymentCard = ({ card, defaultCard }: PaymentCardProps) => {
{defaultCard &&

Default

}
{ const [productAmount, setProductAmount] = useState(1); const [pleaseSelectAllOptions, setPleaseSelectAllOptions] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); + const [isDisabled, setIsDisabled] = useState(false); + const { state } = useStore(); const { productState } = useProductState(); - const { setCart } = useGlobalState(); + const { cart, setCart } = useGlobalState(); + + const { chosenOptions, chosenVariant } = productState; + + const selectedStock = + // if there is variant information, we use the variant stock, otherwise we use the general product stock + product.variants && product.variants?.length > 0 ? chosenVariant?.variantStock : product.stock; - const { chosenOptions } = productState; + const productHasVariant = product.variants && product.variants?.length > 0; useEffect(() => { product.options?.length === Object.keys(chosenOptions).length; product.options?.length === Object.keys(chosenOptions).length && setPleaseSelectAllOptions(''); + + setProductAmount(1); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [chosenOptions, product.options?.length]); + useEffect(() => { + setIsDisabled(productAmount === selectedStock); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [product, productAmount, setProductAmount]); + const addProduct = async ({ product, quantity, toastifyMessage }: AddProductProps) => { // Turn on spinner while waiting setIsSubmitting(true); - // Add product to cart on Swell await swell.cart .addItem({ @@ -75,6 +92,26 @@ const AddToCart = ({ product, isAuthenticated }: ProductProp) => { }; const handleAddToCart = () => { + // to check if the product (with no variant) is in the cart + const productInCart = cart && cart?.items?.find((item) => item.product_id.includes(product.id)); + // to check if the variant is in the cart + const variantInCart = + cart && + cart?.items?.find((item) => + item.variant_id?.includes(chosenVariant.variantId ? chosenVariant.variantId : '') + ); + + // to check the stock of the product in the cart + if (productInCart && product.stock && productInCart?.quantity + productAmount > product.stock) { + return notifyFailure("The amount selected exceeds the stock available. We're sorry."); + } + + const variantStock = chosenVariant.variantStock ?? 0; + + if (variantInCart && variantInCart?.quantity + productAmount > variantStock) { + return notifyFailure("The amount selected exceeds the stock available. We're sorry."); + } + !isSubmitting && void addProduct({ product: product, @@ -85,7 +122,7 @@ const AddToCart = ({ product, isAuthenticated }: ProductProp) => { const buttonLabel = () => { if (product.stock === 0) { - return 'COMING SOON!'; + return 'OUT OF STOCK'; } if (isSubmitting) { return ; @@ -96,6 +133,15 @@ const AddToCart = ({ product, isAuthenticated }: ProductProp) => { return 'UNAVAILABLE'; }; + const handleProductAmount = () => { + if (selectedStock) { + if (productAmount < selectedStock) { + setProductAmount(productAmount + 1); + } + // if there is no stock information, there is no limit + } else setProductAmount(productAmount + 1); + }; + return ( <>
@@ -110,14 +156,32 @@ const AddToCart = ({ product, isAuthenticated }: ProductProp) => { />
@@ -128,8 +192,12 @@ const AddToCart = ({ product, isAuthenticated }: ProductProp) => { onClick={() => handleAddToCart()} label={buttonLabel()} variant="fill" + disabled={ + product.stock === 0 || (productHasVariant && !state.isVariantActive) || isSubmitting + } className={`font-bold py-3 px-5 md:min-w-[240px] ${ - state.isVariantActive || (product.options?.length === 0 && product.stock !== 0) + (state.isVariantActive && productHasVariant) || + (product.options?.length === 0 && product?.stock !== 0) ? 'bg-black font-quicksand border text-white duration-200 cursor-pointer hover:bg-white hover:text-black' : 'bg-gray-medium text-white font-quicksand border border-gray-medium' }`} diff --git a/app/products/[slug]/_components/ProductInfo/ProductInfo.tsx b/app/products/[slug]/_components/ProductInfo/ProductInfo.tsx index 73f03388..fdfad905 100644 --- a/app/products/[slug]/_components/ProductInfo/ProductInfo.tsx +++ b/app/products/[slug]/_components/ProductInfo/ProductInfo.tsx @@ -16,12 +16,13 @@ interface ProductProp { const ProductInfo = async ({ product }: ProductProp) => { const auth = await isAuthenticated(); + const productHasVariant = product.variants && product.variants?.length > 0; return (
- - + {!productHasVariant && } + diff --git a/app/products/[slug]/_components/ProductInfo/ProductOptions.tsx b/app/products/[slug]/_components/ProductInfo/ProductOptions.tsx index 2125f2ea..dd697b85 100644 --- a/app/products/[slug]/_components/ProductInfo/ProductOptions.tsx +++ b/app/products/[slug]/_components/ProductInfo/ProductOptions.tsx @@ -2,64 +2,140 @@ import { useEffect, useState } from 'react'; -import { useStore, useProductState } from '~/_hooks/useStore'; +import ProductStock from './ProductStock'; + +import { useProductState } from '~/_hooks/useStore'; interface ProductProp { product: Product; } +interface Option { + label: string; + active: boolean; + values: Array<{ name: string; id: string }>; +} + +interface OptionState { + currentOptionValue: { name: string; id: string } | null; + currentOption: Option | null; + selectedIds: Record; +} + const ProductOptions = ({ product }: ProductProp) => { - const [selectedIds, setSelectedIds] = useState({}); - const { state, updateState } = useStore(); - const { productState, updateProductProp } = useProductState(); + const { productState, updateProductState } = useProductState(); + const [optionState, setOptionState] = useState({ + currentOptionValue: null, + currentOption: null, + selectedIds: {} + }); - // Save only the variants with active states - const activeProductVariants = product.variants?.filter((variant) => variant.active); + function findMatchingObject(variants: Variant[], valueIdsObject: Record) { + for (let i = 0; i < variants.length; i++) { + const item = variants[i]; + const valueIds = Object.values(item.value_ids); + if (objectContainsAllValues(valueIds, valueIdsObject)) { + return item; + } + } + return null; // Return null if no matching object is found + } - // Transform the selected products, from object into an array, to compare them with the set of active ids. - const availableProductsId = Object.entries(selectedIds).map(([, value]) => value); + function objectContainsAllValues(valuesArray: string[], valueIdsObject: Record) { + for (const key in valueIdsObject) { + if (!valuesArray.includes(valueIdsObject[key])) { + return false; + } + } + return true; + } // Find the first available (active) variant option and select it by default useEffect(() => { const firstActiveVariant = product.variants?.reverse().find((variant) => variant.active); const firstActiveLabel = firstActiveVariant?.name.split(', '); - let initialOptions = {}; + const initialOptions: Record = {}; - product.options?.map((option, i) => { - initialOptions = { - ...initialOptions, - [option.label]: firstActiveLabel ? firstActiveLabel[i] : '' - }; + product.options?.forEach((option, i) => { + initialOptions[option.label] = firstActiveLabel ? firstActiveLabel[i] : ''; + }); - setSelectedIds((prev) => ({ ...prev, [option.label]: firstActiveVariant?.value_ids[i] })); + // Update selectedIds directly from the product prop + const selectedIds: Record = {}; + product.options?.forEach((option, i) => { + if (option.active && firstActiveVariant) { + const valueIds = firstActiveVariant.value_ids; + if (typeof valueIds === 'object' && !Array.isArray(valueIds)) { + selectedIds[option.label] = valueIds[i]; + } + } }); - updateProductProp('chosenOptions', initialOptions); + setOptionState((prevState) => ({ + ...prevState, + selectedIds + })); + + const variantSelectedbyOptionsId = + product.variants && findMatchingObject(product.variants, selectedIds); + + updateProductState({ + ...productState, + chosenOptions: initialOptions, + chosenOptionsId: Object.values(selectedIds), + chosenVariant: { + variantLabel: variantSelectedbyOptionsId?.name, + variantId: variantSelectedbyOptionsId?.id, + variantActive: variantSelectedbyOptionsId?.active, + variantStock: variantSelectedbyOptionsId?.stock_variant + } + }); }, [product]); - // Declare useEffect to 'listen' for variant selections + const handleClick = (value: { name: string; id: string }, option: Option) => { + setOptionState((prevState) => ({ + ...prevState, + selectedIds: { + ...prevState.selectedIds, + [option.label]: value?.id + }, + currentOptionValue: value, + currentOption: option + })); + }; + useEffect(() => { - // When the options have an item clicked we ask if the selected ids are the same as in the active variant - if (availableProductsId.length === product.options?.length) { - // Returns true or false if the selected ids are the same in the active variant - const selectedIdsameAsActiveVariants = activeProductVariants?.map((variant) => { - return variant.value_ids.every((id) => { - return availableProductsId?.includes(id); - }); - }); - // Set global state accordingly if its active or not - if (selectedIdsameAsActiveVariants?.includes(true)) { - updateState({ ...state, isVariantActive: true }); - } else { - updateState({ ...state, isVariantActive: false }); + if (!optionState.currentOption || !optionState.currentOptionValue) return; + + const variantSelectedbyOptionsId = + product.variants && findMatchingObject(product.variants, optionState.selectedIds); + + updateProductState({ + ...productState, + chosenOptions: { + ...productState.chosenOptions, + [optionState.currentOption.label]: optionState.currentOptionValue.name + }, + chosenOptionsId: Object.values({ + ...productState.chosenOptions, + [optionState.currentOption.label]: optionState.currentOptionValue.name + }), + chosenVariant: { + ...productState.chosenVariant, + variantLabel: variantSelectedbyOptionsId?.name, + variantId: variantSelectedbyOptionsId?.id, + variantActive: variantSelectedbyOptionsId?.active, + variantStock: variantSelectedbyOptionsId?.stock_variant } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedIds]); + }); + }, [optionState.selectedIds]); return (
+ {productState.chosenVariant.variantActive && ( + + )} {product.options?.map((option, index) => { return (
@@ -67,38 +143,28 @@ const ProductOptions = ({ product }: ProductProp) => { <>

{option.label}:

    - {option.values.map((value, index) => { + {option.values.map((value) => { return ( <> {option.label === 'Color' ? (
  • { - updateProductProp('chosenOptions', { - ...productState.chosenOptions, - [option.label]: value.name - }); - setSelectedIds({ ...selectedIds, [option.label]: value.id }); - }} + onClick={() => handleClick(value, option)} >
  • ) : (
  • { - updateProductProp('chosenOptions', { - ...productState.chosenOptions, - [option.label]: value.name - }); - setSelectedIds({ ...selectedIds, [option.label]: value.id }); + handleClick(value, option); }} > {value.name} diff --git a/app/products/[slug]/_components/ProductInfo/ProductRating.tsx b/app/products/[slug]/_components/ProductInfo/ProductRating.tsx index df46b2fd..d8e49c92 100644 --- a/app/products/[slug]/_components/ProductInfo/ProductRating.tsx +++ b/app/products/[slug]/_components/ProductInfo/ProductRating.tsx @@ -1,21 +1,20 @@ 'use client'; -import Rater from 'react-rater'; -import 'react-rater/lib/react-rater.css'; +import { Rating } from 'react-simple-star-rating'; -import Tooltip from '~/_components/Globals/Tooltip'; - -type Rating = { - rating?: number; -}; - -const ProductRating = ({ rating }: Rating) => { +const ProductRating = ({ rating }: { rating: number }) => { return ( - -
    - -
    -
    +
    + + ({rating || 0} stars) +
    ); }; diff --git a/app/products/[slug]/_components/ProductInfo/ProductStock.tsx b/app/products/[slug]/_components/ProductInfo/ProductStock.tsx index d71f3468..8bef1d83 100644 --- a/app/products/[slug]/_components/ProductInfo/ProductStock.tsx +++ b/app/products/[slug]/_components/ProductInfo/ProductStock.tsx @@ -6,7 +6,9 @@ function ProductStock({ stock }: StockProp) { return ( <> {stock && stock > 0 && stock <= 5 ? ( -

    {`Only ${stock} item${stock > 1 ? 's' : ''} left!`}

    + {`Only ${stock} item${ + stock > 1 ? 's' : '' + } left!`} ) : ( '' )} diff --git a/app/products/[slug]/_components/ProductReview.tsx b/app/products/[slug]/_components/ProductReview.tsx deleted file mode 100644 index 682cee0d..00000000 --- a/app/products/[slug]/_components/ProductReview.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import ProductRatings from './ProductReview/ProductRatings'; - -const ProductReview = () => { - return ( -
    -
    - - Reviews - -
    -
    - -
    -
    - ); -}; - -export default ProductReview; diff --git a/app/products/[slug]/_components/ProductReview/ProductRatings.tsx b/app/products/[slug]/_components/ProductReview/ProductRatings.tsx deleted file mode 100644 index 8930c571..00000000 --- a/app/products/[slug]/_components/ProductReview/ProductRatings.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import Rating from './Rating'; -import WriteAReview from './WriteAReview'; - -const ProductRatings = () => { - return ( -
    - - - - - -
    - ); -}; - -export default ProductRatings; diff --git a/app/products/[slug]/_components/ProductReview/ProductReviews.tsx b/app/products/[slug]/_components/ProductReview/ProductReviews.tsx new file mode 100644 index 00000000..b5dd08c1 --- /dev/null +++ b/app/products/[slug]/_components/ProductReview/ProductReviews.tsx @@ -0,0 +1,133 @@ +import { UseFormSetValue } from 'react-hook-form'; + +import { Rating } from 'react-simple-star-rating'; + +import EditIcon from 'public/img/icons/EditIcon'; + +import TrashIcon from 'public/img/icons/TrashIcon'; +import { Spinner } from '~/_components/Globals/Spinner'; +import Pagination from '~/_components/Pagination'; + +type Inputs = { + title: string; + comments: string; + rating: string; + reviewId: string; +}; + +interface Props { + allReviews: null | Reviews; + userId: string | null; + setIsEditing: (arg0: boolean) => void; + isDeleteReviewLoading: boolean; + setValue: UseFormSetValue; + handleDelete: (reviewId: string) => Promise; + setRating: (arg0: number) => void; + query: { + page: number; + }; +} + +const ProductReviews = ({ + allReviews, + userId, + handleDelete, + isDeleteReviewLoading, + setIsEditing, + setValue, + setRating, + query +}: Props) => { + const editReview = (review: Review) => { + setIsEditing(true); + setValue('title', review.title); + setValue('comments', review.comments); + setValue('rating', review.rating.toString()); + setRating(review.rating); + setValue('reviewId', review.id); + const formReview = document?.getElementById('form-review'); + if (formReview) { + formReview.scrollIntoView({ behavior: 'smooth' }); + } + }; + + const pageArray = (value: number) => { + return Array.from({ length: value }, (_, index) => index + 1); + }; + + const paginationLimit = + allReviews && allReviews.pages && allReviews.pages[1] + ? allReviews.pages[1].end - allReviews.pages[1].start + 1 + : 0; + + const paginationObject = { + total: allReviews?.count, + pages: pageArray(allReviews?.page_count || 0), + current: Number(query.page) || 1, + limit: paginationLimit + }; + return ( + <> + {allReviews?.results.map((review) => ( +
    +
    +
    +
    {review.name}
    + {new Date(review.date_created).toISOString().split('T')[0]} +
    + +
    +
    +

    {review.title}

    + {review.comments} +
    +
    + {userId === review.account_id && ( + + )} + {userId === review.account_id && ( + + )} +
    +
    +
    +
    + ))} + {allReviews && allReviews.page_count > 1 && ( + + )} + + ); +}; + +export default ProductReviews; diff --git a/app/products/[slug]/_components/ProductReview/Rating.tsx b/app/products/[slug]/_components/ProductReview/Rating.tsx deleted file mode 100644 index 83706004..00000000 --- a/app/products/[slug]/_components/ProductReview/Rating.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import Image from 'next/image'; -import Rater from 'react-rater'; - -const Rating = () => { - return ( -
    -
    - Rating -
    -
    -
    -
    Name
    -
    - -
    -
    -
    - Lorem ipsum dolor sit amet, consectetur adipisicing elit. Doloribus nesciunt minus rem - consectetur natus nobis doloremque numquam laboriosam in ipsa, nulla dolorum nostrum iste - neque odio nemo, dolor laudantium modi nam impedit amet labore. Voluptates ullam adipisci - nesciunt temporibus tempore. -
    -
    -
    - ); -}; - -export default Rating; diff --git a/app/products/[slug]/_components/ProductReview/WriteAReview.tsx b/app/products/[slug]/_components/ProductReview/WriteAReview.tsx index 41b1cd73..78a0c28b 100644 --- a/app/products/[slug]/_components/ProductReview/WriteAReview.tsx +++ b/app/products/[slug]/_components/ProductReview/WriteAReview.tsx @@ -1,45 +1,110 @@ import React from 'react'; -import Rater from 'react-rater'; +import { + FieldErrorsImpl, + SubmitHandler, + UseFormHandleSubmit, + UseFormRegister, + UseFormSetValue +} from 'react-hook-form'; +import { Rating } from 'react-simple-star-rating'; + +import Button from '~/_components/Button'; +import { Spinner } from '~/_components/Globals/Spinner'; + +type Inputs = { + title: string; + comments: string; + rating: string; + reviewId: string; +}; +interface Props { + register: UseFormRegister; + onSubmit: SubmitHandler; + handleSubmit: UseFormHandleSubmit; + setValue: UseFormSetValue; + errors: Partial< + FieldErrorsImpl<{ title: string; comments: string; rating: string; reviewId: string }> + >; + isPostReviewLoading: boolean; + isEditing: boolean; + rating: number; + setRating: (rate: number) => void; +} + +const WriteAReview = ({ + register, + onSubmit, + handleSubmit, + errors, + isPostReviewLoading, + isEditing, + setValue, + rating, + setRating +}: Props) => { + const handleRating = (rate: number) => { + setValue('rating', rate.toString(), { shouldValidate: true }); + setRating(rate); + }; -const WriteAReview = () => { return ( -
    +
    { + void handleSubmit(onSubmit)(e); + }} + id="form-review" + className="scroll-mt-40" + >
    -

    Write a review

    -

    - Your email address will not be published. Required fields are marked * -

    +

    Write a review:

    -
    +

    Rating:

    - +
    -
    + {errors.rating &&

    {errors.rating.message}

    } +
    + + + {errors.title &&

    {errors.title.message}

    } +
    +

    Review*

    -