From 79c2cd699e0cb151b7c6e00b06a0a976d7f872eb Mon Sep 17 00:00:00 2001 From: Martin M Date: Tue, 20 Jun 2023 17:08:35 -0300 Subject: [PATCH] Wishlist functionality mods (#167) * Product detail wishlist functionality * update product detail wishlist logic * wishlist on product list page functionality * Update wishlist logic to global store * Check if user is authenticated * Tooltip please log in to use this functionality * Wishlist functionality * Style enhancements * Change metadata title * wishlist enhancements * wishlist page enhancements * remove unused component * refactor wishlist on products listing * remove unused actions * remove unused store * refactor wishlist check for product * fix for non logged users * possible cache fix * wishlist enhancements * switch wishlist to client side rendering * remove unused code * switch wishlist to client side rendering * fixed loading wishlist bug * fixed loading wishlist bug * fixed wishlist page * Add isAuthenticated prop to ProductHighlight --------- Co-authored-by: Felipe Cabrera Co-authored-by: JoaquinEtchegaray --- app/_components/Globals/ProductCard.tsx | 35 +++++---- app/_components/Globals/ProductList.tsx | 55 +++++++++----- app/_components/Globals/ProductWishlist.tsx | 57 +++++++++++++++ app/_components/Globals/Spinner.tsx | 5 +- app/_components/Globals/Tooltip.tsx | 12 +++- app/_components/Home/ProductHighlight.tsx | 2 +- app/_components/Navbar/MobileMenu.tsx | 2 + app/_components/Navbar/UserButtons.tsx | 14 +++- app/_data/partials.json | 1 + app/_hooks/useFetch.ts | 43 +++++++++++ app/_hooks/useStore.ts | 11 ++- app/_layouts/AccountLayout/AccountLayout.tsx | 1 + app/_lib/Store.ts | 4 +- app/_lib/SwellAPI.ts | 21 ++++-- app/account/wishlist/page.tsx | 57 +++++++++++++++ app/api/wishlist/[productId]/route.ts | 15 ++++ app/api/wishlist/route.ts | 8 +++ .../_components/ProductInfo/AddToCart.tsx | 20 +++--- .../_components/ProductInfo/ProductInfo.tsx | 8 ++- .../{[slug] => }/_actions/wishlist.ts | 0 app/products/_components/Wishlist.tsx | 72 +++++++++++++++++++ app/products/page.tsx | 8 ++- package-lock.json | 30 ++++---- package.json | 4 +- 24 files changed, 405 insertions(+), 80 deletions(-) create mode 100644 app/_components/Globals/ProductWishlist.tsx create mode 100644 app/_hooks/useFetch.ts create mode 100644 app/account/wishlist/page.tsx create mode 100644 app/api/wishlist/[productId]/route.ts create mode 100644 app/api/wishlist/route.ts rename app/products/{[slug] => }/_actions/wishlist.ts (100%) create mode 100644 app/products/_components/Wishlist.tsx diff --git a/app/_components/Globals/ProductCard.tsx b/app/_components/Globals/ProductCard.tsx index 1e656803..6bc8f4f4 100644 --- a/app/_components/Globals/ProductCard.tsx +++ b/app/_components/Globals/ProductCard.tsx @@ -4,19 +4,21 @@ import Image from 'next/image'; import Link from 'next/link'; import { useState } from 'react'; -import { FaRegHeart } from 'react-icons/fa'; +import ProductWishlist from './ProductWishlist'; import Button from '~/_components/Button'; -import Tooltip from '~/_components/Globals/Tooltip'; import { formatCurrency } from '~/_utils/numbers'; interface Props { product: Product; + isAuthenticated?: boolean; + isWishlistCard?: boolean; } -const ProductCard = ({ product }: Props) => { +const ProductCard = ({ product, isAuthenticated, isWishlistCard }: Props) => { const [isHovered, setIsHovered] = useState(false); + const image = product.images?.[0] || { src: '', alt: 'Not Found' }; return ( @@ -29,18 +31,21 @@ const ProductCard = ({ product }: Props) => { }`} >
-
- -
- -
-
-
-
+ {!isWishlistCard && ( +
+ +
+ )} + +
{image.alt} diff --git a/app/_components/Globals/ProductList.tsx b/app/_components/Globals/ProductList.tsx index 77c57306..42a22190 100644 --- a/app/_components/Globals/ProductList.tsx +++ b/app/_components/Globals/ProductList.tsx @@ -1,29 +1,50 @@ +'use client'; + +import { useEffect } from 'react'; + import ProductCard from './ProductCard'; +import useFetch from '~/_hooks/useFetch'; +import { useWishlistState } from '~/_hooks/useStore'; + import Container from '~/_layouts/Container'; interface Props { relatedProducts?: boolean; threeColumns?: boolean; products?: Product[]; + isAuthenticated?: boolean; } -const ProductList = ({ relatedProducts, threeColumns, products }: Props) => ( - -
- {products?.map((product, i) => ( - - ))} -
-
-); +const ProductList = ({ relatedProducts, threeColumns, products, isAuthenticated }: Props) => { + const { setWishlist } = useWishlistState(); + + /** Get user wishlist */ + const wishlistUrl = isAuthenticated ? '/api/wishlist/' : null; + const { data } = useFetch<{ wishlist: string[] }>(wishlistUrl); + + /** Once wishlist is retrieved, set it to the store */ + useEffect(() => { + if (data) setWishlist(data.wishlist); + }, [data, setWishlist]); + + return ( + +
+ {products?.map((product, i) => ( + + ))} +
+
+ ); +}; export default ProductList; diff --git a/app/_components/Globals/ProductWishlist.tsx b/app/_components/Globals/ProductWishlist.tsx new file mode 100644 index 00000000..b58f3648 --- /dev/null +++ b/app/_components/Globals/ProductWishlist.tsx @@ -0,0 +1,57 @@ +import { useState } from 'react'; +import { FaRegHeart } from 'react-icons/fa'; + +import { Spinner } from '~/_components/Globals/Spinner'; +import Tooltip from '~/_components/Globals/Tooltip'; +import { useWishlistState } from '~/_hooks/useStore'; + +type Props = { + product: Product; + isAuthenticated?: boolean; + isHovered?: boolean; +}; + +export default function ProductWishlist({ product, isAuthenticated, isHovered }: Props) { + const { wishlist, setWishlist } = useWishlistState(); + + const [isWishlistLoading, setIsWishlistLoading] = useState(false); + + /***************************************************************************** + * Toggle product from wishlist + ****************************************************************************/ + const handleToggleWishlist = async () => { + setIsWishlistLoading(true); + const wishlistReq = await fetch(`/api/wishlist/${product.id}`, { method: 'PUT' }); + const { wishlist } = (await wishlistReq.json()) as { wishlist: string[] }; + setWishlist(wishlist); + setIsWishlistLoading(false); + }; + + if (isWishlistLoading) { + return ( + + + + ); + } + + return ( + + + + ); +} diff --git a/app/_components/Globals/Spinner.tsx b/app/_components/Globals/Spinner.tsx index 3faef89c..ac37ada9 100644 --- a/app/_components/Globals/Spinner.tsx +++ b/app/_components/Globals/Spinner.tsx @@ -1,11 +1,12 @@ interface SpinnerProp { position?: 'center' | 'left' | 'right'; size?: 4 | 5 | 6 | 8 | 10; + className?: string; } -export const Spinner = ({ position = 'center', size = 10 }: SpinnerProp) => { +export const Spinner = ({ position = 'center', size = 10, className }: SpinnerProp) => { return ( -
+
{ +const Tooltip = ({ children, content, className }: Props) => { return ( - + {children} ); diff --git a/app/_components/Home/ProductHighlight.tsx b/app/_components/Home/ProductHighlight.tsx index ce164fca..dc2778fe 100644 --- a/app/_components/Home/ProductHighlight.tsx +++ b/app/_components/Home/ProductHighlight.tsx @@ -15,7 +15,7 @@ const ProductHighlight = ({ products, title }: ProductHighlightProps) => {
{title}
- +
- = { + data: T | null; + loading: boolean; + error: Error | null; +}; + +const useFetch = (url: string | null): State => { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchResource = async () => { + setLoading(true); + setError(null); + + try { + if (!url) return; + + const response = await fetch(url); + if (!response.ok) throw new Error('An error occurred while fetching data.'); + + const result = (await response.json()) as T; + + setData(result); + } catch (e) { + setError(e instanceof Error ? e : new Error('An unexpected error occurred.')); + } finally { + setLoading(false); + } + }; + + fetchResource().catch((e) => + setError(e instanceof Error ? e : new Error('An unexpected error occurred.')) + ); + }, [url]); + + return { data, loading, error }; +}; + +export default useFetch; diff --git a/app/_hooks/useStore.ts b/app/_hooks/useStore.ts index 0e2ba759..6e8b968b 100644 --- a/app/_hooks/useStore.ts +++ b/app/_hooks/useStore.ts @@ -15,7 +15,6 @@ export function useGlobalState() { /***************************************************************************** * Define and declare global state to be used on products ****************************************************************************/ - type StateProduct = { chosenOptions: { [key: string]: string }; }; @@ -40,6 +39,16 @@ export function useProductState(): { return { productState, updateProductProp }; } +/***************************************************************************** + * Global state used for wishlist + ****************************************************************************/ +const stateWishlist = atom(null); + +export function useWishlistState() { + const [wishlist, setWishlist] = useAtom(stateWishlist); + return { wishlist, setWishlist }; +} + /***************************************************************************** * Global state used for UI ****************************************************************************/ diff --git a/app/_layouts/AccountLayout/AccountLayout.tsx b/app/_layouts/AccountLayout/AccountLayout.tsx index 5087488b..498ac764 100644 --- a/app/_layouts/AccountLayout/AccountLayout.tsx +++ b/app/_layouts/AccountLayout/AccountLayout.tsx @@ -33,6 +33,7 @@ const AccountLayout = ({ account, children }: Props) => { pathname="/account/payments" label="Payment methods" /> +
diff --git a/app/_lib/Store.ts b/app/_lib/Store.ts index 7e69ecd9..b281dda4 100644 --- a/app/_lib/Store.ts +++ b/app/_lib/Store.ts @@ -104,7 +104,7 @@ class Store { salePrice: product.sale_price || null, sku: product.sku || null, images: this.transformImages(product), - categories: product.category_index.id + categories: product.category_index?.id }; } @@ -137,7 +137,7 @@ class Store { * Convert SwellProduct variants to a Product variants format ****************************************************************************/ transformProductVariants(product: SwellProduct) { - return product.variants.results.map((variant) => ({ + return product.variants?.results?.map((variant) => ({ name: variant.name, active: variant.active, value_ids: variant.option_value_ids diff --git a/app/_lib/SwellAPI.ts b/app/_lib/SwellAPI.ts index 539bade7..5ce789ce 100644 --- a/app/_lib/SwellAPI.ts +++ b/app/_lib/SwellAPI.ts @@ -2,6 +2,8 @@ import { cookies } from 'next/headers'; import { redirect } from 'next/navigation'; import 'server-only'; +import Store from './Store'; + type RequestBody = { query: string; }; @@ -162,6 +164,7 @@ export const getUserInfo = async () => { const cards = await getCards(); return { + authenticated: true, user: user.account, orders: user.orders.results || [], addresses: addresses || [], @@ -204,9 +207,9 @@ const getCards = async () => { /***************************************************************************** * Get logged user wishlist products ids ****************************************************************************/ -const getWishlistIds = async (): Promise => { - const { content } = (await makeRequest('/api/account')) as WishlistBody; - return content.wishlist_ids; +export const getWishlistIds = async (): Promise => { + const response = (await makeRequest('/api/account')) as WishlistBody; + return response?.content?.wishlist_ids || []; }; /***************************************************************************** @@ -221,9 +224,13 @@ export const getWishlist = async (): Promise => { const { results } = (await makeRequest( `/api/products?where[id][$in]=${productsIds.join(',')}` - )) as { results: Product[] }; + )) as { results: SwellProduct[] }; + + const formattedWishlist: Product[] = results.map((product) => { + return Store.tranformProduct(product); + }); - return results; + return formattedWishlist; }; /***************************************************************************** @@ -241,9 +248,9 @@ export const toggleWishlist = async (productId: string): Promise => { const { id, content } = (await makeRequest('/api/account')) as WishlistBody; // Add or remove product depending on if it's already in the wishlist - const wishlistIds = content.wishlist_ids.includes(productId) + const wishlistIds = content?.wishlist_ids.includes(productId) ? content.wishlist_ids.filter((id) => id !== productId) - : [...content.wishlist_ids, productId]; + : [...(content?.wishlist_ids || []), productId]; // Overwrite wishlist with new list of products const wishlist = (await makeAdminRequest(`/accounts/${id}`, 'PUT', { diff --git a/app/account/wishlist/page.tsx b/app/account/wishlist/page.tsx new file mode 100644 index 00000000..812f9f5d --- /dev/null +++ b/app/account/wishlist/page.tsx @@ -0,0 +1,57 @@ +import Link from 'next/link'; +import { BiShoppingBag } from 'react-icons/bi'; + +import Button from '~/_components/Button'; +import ProductCard from '~/_components/Globals/ProductCard'; + +import AccountLayout from '~/_layouts/AccountLayout'; + +import { getUserInfo, getWishlist } from '~/_lib/SwellAPI'; + +export const metadata = { + title: 'SquareOne - Wishlist', + description: 'Sit excepteur proident est commodo laboris consectetur ea tempor officia.' +}; + +export default async function Addresses() { + const { authenticated, user } = await getUserInfo(); + const wishlist = await getWishlist(); + + return ( + +

Wishlist

+ + {wishlist.length === 0 && ( + <> +

+ You don't have any product on your wishlist yet. +

+ +
+ } + /> + + + )} + + {wishlist.length > 0 && ( +
+ {wishlist?.map((product, i) => ( + + ))} +
+ )} + + ); +} diff --git a/app/api/wishlist/[productId]/route.ts b/app/api/wishlist/[productId]/route.ts new file mode 100644 index 00000000..c441d709 --- /dev/null +++ b/app/api/wishlist/[productId]/route.ts @@ -0,0 +1,15 @@ +import { NextResponse } from 'next/server'; + +import { isProductInWishlist, toggleWishlist } from '~/_lib/SwellAPI'; + +export async function GET(request: Request, { params }: { params: { productId: string } }) { + const productId = params.productId; + const inWishlist = await isProductInWishlist(productId); + return NextResponse.json({ status: inWishlist }); +} + +export async function PUT(request: Request, { params }: { params: { productId: string } }) { + const productId = params.productId; + const wishlist = await toggleWishlist(productId); + return NextResponse.json({ wishlist }); +} diff --git a/app/api/wishlist/route.ts b/app/api/wishlist/route.ts new file mode 100644 index 00000000..b70acd65 --- /dev/null +++ b/app/api/wishlist/route.ts @@ -0,0 +1,8 @@ +import { NextResponse } from 'next/server'; + +import { getWishlistIds } from '~/_lib/SwellAPI'; + +export async function GET() { + const wishlist = await getWishlistIds(); + return NextResponse.json({ wishlist }); +} diff --git a/app/products/[slug]/_components/ProductInfo/AddToCart.tsx b/app/products/[slug]/_components/ProductInfo/AddToCart.tsx index 2bb6a8ab..1205f2f0 100644 --- a/app/products/[slug]/_components/ProductInfo/AddToCart.tsx +++ b/app/products/[slug]/_components/ProductInfo/AddToCart.tsx @@ -1,20 +1,21 @@ 'use client'; import React, { useEffect, useState } from 'react'; -import { AiOutlineHeart } from 'react-icons/ai'; import { IoIosArrowDown, IoIosArrowUp } from 'react-icons/io'; import 'react-toastify/dist/ReactToastify.css'; import { Spinner } from '~/_components/Globals/Spinner'; -import Tooltip from '~/_components/Globals/Tooltip'; import { useStore, useProductState, useGlobalState } from '~/_hooks/useStore'; import swell from '~/_lib/SwellJS'; import { notifyFailure, notifySuccess } from '~/_utils/toastifies'; +import Wishlist from '~/products/_components/Wishlist'; + interface ProductProp { product: Product; + isAuthenticated: boolean; } interface AddProductProps { @@ -23,14 +24,15 @@ interface AddProductProps { toastifyMessage: string; } -const AddToCart = ({ product }: ProductProp) => { - const [productAmount, setProductAmount] = useState(1); - const [pleaseSelectAllOptions, setPleaseSelectAllOptions] = useState(''); - const [isLoading, setIsLoading] = useState(false); +const AddToCart = ({ product, isAuthenticated }: ProductProp) => { const { state } = useStore(); const { productState } = useProductState(); const { setCart } = useGlobalState(); + const [productAmount, setProductAmount] = useState(1); + const [pleaseSelectAllOptions, setPleaseSelectAllOptions] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const { chosenOptions } = productState; useEffect(() => { @@ -125,11 +127,7 @@ const AddToCart = ({ product }: ProductProp) => { 'UNAVAILABLE' )} - - - +
{pleaseSelectAllOptions && ( diff --git a/app/products/[slug]/_components/ProductInfo/ProductInfo.tsx b/app/products/[slug]/_components/ProductInfo/ProductInfo.tsx index a66c04c3..5fd32923 100644 --- a/app/products/[slug]/_components/ProductInfo/ProductInfo.tsx +++ b/app/products/[slug]/_components/ProductInfo/ProductInfo.tsx @@ -6,19 +6,23 @@ import ProductRating from './ProductRating'; import ProductSocialMedia from './ProductSocialMedia'; import ProductTitle from './ProductTitle'; +import { isAuthenticated } from '~/_lib/SwellAPI'; + interface ProductProp { product: Product; categories: Category[]; } -const ProductInfo = ({ product }: ProductProp) => { +const ProductInfo = async ({ product }: ProductProp) => { + const auth = await isAuthenticated(); + return (
- +
diff --git a/app/products/[slug]/_actions/wishlist.ts b/app/products/_actions/wishlist.ts similarity index 100% rename from app/products/[slug]/_actions/wishlist.ts rename to app/products/_actions/wishlist.ts diff --git a/app/products/_components/Wishlist.tsx b/app/products/_components/Wishlist.tsx new file mode 100644 index 00000000..62bea2d5 --- /dev/null +++ b/app/products/_components/Wishlist.tsx @@ -0,0 +1,72 @@ +import { useEffect, useState } from 'react'; +import { AiOutlineHeart } from 'react-icons/ai'; + +import { Spinner } from '~/_components/Globals/Spinner'; +import Tooltip from '~/_components/Globals/Tooltip'; + +import useFetch from '~/_hooks/useFetch'; + +type Props = { + isAuthenticated: boolean; + productId: string; +}; + +export default function Wishlist({ isAuthenticated, productId }: Props) { + const [isWishlistLoading, setIsWishlistLoading] = useState(false); + const [inWishlist, setInWishlist] = useState(null); + + /***************************************************************************** + * Retrieve wishlist status + ****************************************************************************/ + const wishlistUrl = isAuthenticated ? `/api/wishlist/${productId}` : null; + const { data, loading } = useFetch<{ status: boolean }>(wishlistUrl); + + /***************************************************************************** + * Define "wishlisted" status + ****************************************************************************/ + useEffect(() => { + setInWishlist(data?.status ?? null); + }, [data]); + + /***************************************************************************** + * Toggle product from wishlist + ****************************************************************************/ + const handleToggleWishlist = async () => { + setIsWishlistLoading(true); + const wishlistReq = await fetch(`/api/wishlist/${productId}`, { method: 'PUT' }); + const { wishlist } = (await wishlistReq.json()) as { wishlist: string[] }; + setInWishlist(wishlist.includes(productId)); + setIsWishlistLoading(false); + }; + + /** Wait for wishlist status to be retrieved */ + if (loading) { + return null; + } + + /** Show spinner while toggling from wishlist */ + if (isWishlistLoading) { + return ( +
+ +
+ ); + } + + /** Show wishlist indicator & toggle button */ + return ( + + + + ); +} diff --git a/app/products/page.tsx b/app/products/page.tsx index 566ef195..fea90af9 100644 --- a/app/products/page.tsx +++ b/app/products/page.tsx @@ -9,6 +9,7 @@ import Pagination from '~/_components/Pagination'; import keywords from '~/_data/keywords.json'; import Store from '~/_lib/Store'; +import { isAuthenticated } from '~/_lib/SwellAPI'; const { NEXT_PUBLIC_BASE_URL } = process.env; @@ -38,6 +39,8 @@ const getData = async (searchParams: FilterParams) => { }; const Products = async ({ searchParams }: { searchParams: FilterParams }) => { + const auth = await isAuthenticated(); + const { categories, products, pagination } = await getData(searchParams); const filterKeys = Object.keys(searchParams).filter((key) => key !== 'page' && key !== 'sort'); @@ -50,10 +53,13 @@ const Products = async ({ searchParams }: { searchParams: FilterParams }) => { {filterKeys && filterKeys.length > 0 && ( )} - + + + {pagination.pages.length > 0 && ( )} + ) : ( diff --git a/package-lock.json b/package-lock.json index 556ae646..abaac69a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ }, "devDependencies": { "@types/node": "18.0.0", - "@types/react": "17.0.41", + "@types/react": "^18.2.11", "@types/react-image-gallery": "^1.0.5", "@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/parser": "^5.4.0", @@ -44,7 +44,7 @@ "postcss": "^8.4.5", "prettier": "^2.4.1", "tailwindcss": "^3.3.2", - "typescript": "^5.0.4" + "typescript": "^5.1.3" } }, "node_modules/@alloc/quick-lru": { @@ -691,9 +691,9 @@ "dev": true }, "node_modules/@types/react": { - "version": "17.0.41", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.41.tgz", - "integrity": "sha512-chYZ9ogWUodyC7VUTRBfblysKLjnohhFY9bGLwvnUFFy48+vB9DikmB3lW0qTFmBcKSzmdglcvkHK71IioOlDA==", + "version": "18.2.11", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.11.tgz", + "integrity": "sha512-+hsJr9hmwyDecSMQAmX7drgbDpyE+EgSF6t7+5QEBAn1tQK7kl1vWZ4iRf6SjQ8lk7dyEULxUmZOIpN0W5baZA==", "dev": true, "dependencies": { "@types/prop-types": "*", @@ -4693,16 +4693,16 @@ } }, "node_modules/typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.3.tgz", + "integrity": "sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=12.20" + "node": ">=14.17" } }, "node_modules/unbox-primitive": { @@ -5275,9 +5275,9 @@ "dev": true }, "@types/react": { - "version": "17.0.41", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.41.tgz", - "integrity": "sha512-chYZ9ogWUodyC7VUTRBfblysKLjnohhFY9bGLwvnUFFy48+vB9DikmB3lW0qTFmBcKSzmdglcvkHK71IioOlDA==", + "version": "18.2.11", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.11.tgz", + "integrity": "sha512-+hsJr9hmwyDecSMQAmX7drgbDpyE+EgSF6t7+5QEBAn1tQK7kl1vWZ4iRf6SjQ8lk7dyEULxUmZOIpN0W5baZA==", "dev": true, "requires": { "@types/prop-types": "*", @@ -7979,9 +7979,9 @@ "dev": true }, "typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.3.tgz", + "integrity": "sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==", "dev": true }, "unbox-primitive": { diff --git a/package.json b/package.json index 1dc24c54..7ae23d91 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ }, "devDependencies": { "@types/node": "18.0.0", - "@types/react": "17.0.41", + "@types/react": "^18.2.11", "@types/react-image-gallery": "^1.0.5", "@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/parser": "^5.4.0", @@ -46,6 +46,6 @@ "postcss": "^8.4.5", "prettier": "^2.4.1", "tailwindcss": "^3.3.2", - "typescript": "^5.0.4" + "typescript": "^5.1.3" } }