Skip to content
Closed

Develop #1437

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3085de6
fix(deps): update dependency framer-motion to v12.4.2
renovate[bot] Feb 11, 2025
4971ab1
fix(deps): update dependency postcss to ^8.5.2
renovate[bot] Feb 11, 2025
642a20e
fix(deps): update nextjs monorepo to ^15.1.7
renovate[bot] Feb 11, 2025
14ec7c4
Delete package-lock.json
w3bdesign Feb 11, 2025
d0e6492
Install Zustand
w3bdesign Feb 11, 2025
47de5b6
Zustand
w3bdesign Feb 11, 2025
69d0e47
Add to cart
w3bdesign Feb 11, 2025
9740708
Update CheckoutForm.component.tsx
w3bdesign Feb 11, 2025
1858b6a
Zustand
w3bdesign Feb 11, 2025
d4d51d1
Refactor
w3bdesign Feb 12, 2025
5bb4648
Refactor
w3bdesign Feb 12, 2025
77bac2c
Fix price bug
w3bdesign Feb 12, 2025
f759759
Delete package-lock.json
w3bdesign Feb 12, 2025
15540db
Merge branch 'master' into 1433-add-zustand-for-replacement-of-usestate
w3bdesign Feb 12, 2025
dd09a6b
Update package-lock.json
w3bdesign Feb 12, 2025
0db0d3e
chore(deps): update typescript-eslint monorepo to ^8.24.0 (#1432)
renovate[bot] Feb 12, 2025
afb0a4e
Merge branch 'master' into 1433-add-zustand-for-replacement-of-usestate
w3bdesign Feb 12, 2025
e412796
Update Cart.component.tsx
w3bdesign Feb 12, 2025
09d54fb
Show persisted cart quantity immediately, will be updated if query re…
w3bdesign Feb 12, 2025
c671726
Update package-lock.json
w3bdesign Feb 12, 2025
5c8d84a
Update cart.ts
w3bdesign Feb 12, 2025
ae4f59c
Update Cart.component.tsx
w3bdesign Feb 12, 2025
4d57cdb
Merge pull request #1434 from w3bdesign/1433-add-zustand-for-replacem…
w3bdesign Feb 12, 2025
ce9fd6e
chore(deps): update dependency eslint-config-next to ^15.1.7
renovate[bot] Feb 12, 2025
daba524
chore(deps): update typescript-eslint monorepo to ^8.24.0
renovate[bot] Feb 12, 2025
8afae40
Merge pull request #1435 from w3bdesign/renovate/typescript-eslint-mo…
w3bdesign Feb 12, 2025
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
3,493 changes: 2,020 additions & 1,473 deletions package-lock.json

Large diffs are not rendered by default.

22 changes: 12 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,32 +24,34 @@
"license": "ISC",
"dependencies": {
"@apollo/client": "^3.12.11",
"@types/react": "^19.0.8",
"algoliasearch": "^4.24.0",
"autoprefixer": "^10.4.20",
"framer-motion": "12.4.1",
"framer-motion": "12.4.2",
"graphql": "^16.10.0",
"lodash": "^4.17.21",
"next": "15.1.6",
"next": "15.1.7",
"nprogress": "^0.2.0",
"postcss": "^8.5.1",
"postcss": "^8.5.2",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-hook-form": "^7.54.2",
"react-instantsearch-dom": "^6.40.4",
"uuid": "^11.0.5"
"uuid": "^11.0.5",
"zustand": "^5.0.3"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
"@types/lodash": "^4.17.15",
"@types/node": "22.13.1",
"@types/node": "^22.13.1",
"@types/nprogress": "^0.2.3",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@types/react-instantsearch-dom": "^6.12.8",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^8.23.0",
"@typescript-eslint/parser": "^8.23.0",
"babel-plugin-styled-components": "^2.1.4",
"eslint-config-next": "^15.1.6",
"@typescript-eslint/eslint-plugin": "^8.24.0",
"@typescript-eslint/parser": "^8.24.0",
"babel-plugin-styled-components": "^2.1.4",
"eslint-config-next": "^15.1.7",
"postcss-preset-env": "^10.1.3",
"prettier": "^3.5.0",
"tailwindcss": "^3.4.17",
Expand Down
136 changes: 47 additions & 89 deletions src/components/Cart/CartContents.component.tsx
Original file line number Diff line number Diff line change
@@ -1,157 +1,123 @@
import { useContext, useEffect } from 'react';
import { ChangeEvent } from 'react';
import { useMutation, useQuery } from '@apollo/client';
import Link from 'next/link';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { v4 as uuidv4 } from 'uuid';

import { CartContext } from '@/stores/CartProvider';
import useCartStore, { RootObject, Product } from '@/stores/cart';
import Button from '@/components/UI/Button.component';
import LoadingSpinner from '../LoadingSpinner/LoadingSpinner.component';

import {
getFormattedCart,
getUpdatedItems,
handleQuantityChange,
IProductRootObject,
} from '@/utils/functions/functions';

import { GET_CART } from '@/utils/gql/GQL_QUERIES';
import { UPDATE_CART } from '@/utils/gql/GQL_MUTATIONS';

const CartContents = () => {
const router = useRouter();
const { setCart } = useContext(CartContext);
const { cart, setCart } = useCartStore();
const isCheckoutPage = router.pathname === '/kasse';

const { data, refetch } = useQuery(GET_CART, {
useQuery(GET_CART, {
notifyOnNetworkStatusChange: true,
onCompleted: () => {
const updatedCart = getFormattedCart(data);
if (!updatedCart && !data.cart.contents.nodes.length) {
localStorage.removeItem('woocommerce-cart');
setCart(null);
return;
onCompleted: (data) => {
// Only update if there's a significant difference to avoid unnecessary re-renders
const updatedCart = getFormattedCart(data) as RootObject | undefined;
if (!cart || cart.totalProductsCount !== updatedCart?.totalProductsCount) {
setCart(updatedCart || null);
}
localStorage.setItem('woocommerce-cart', JSON.stringify(updatedCart));
setCart(updatedCart);
},
});

const [updateCart, { loading: updateCartProcessing }] = useMutation(
UPDATE_CART,
{
onCompleted: () => {
refetch();
setTimeout(() => {
refetch();
}, 3000);
},
},
);

const handleRemoveProductClick = (
cartKey: string,
products: IProductRootObject[],
) => {
if (products?.length) {
const updatedItems = getUpdatedItems(products, 0, cartKey);
updateCart({
variables: {
input: {
clientMutationId: uuidv4(),
items: updatedItems,
},
},
});
}
refetch();
setTimeout(() => {
refetch();
}, 3000);
};

useEffect(() => {
refetch();
}, [refetch]);
const [updateCart] = useMutation(UPDATE_CART);

const cartTotal = data?.cart?.total || '0';
const handleRemoveProductClick = (cartKey: string) => {
// Update local state
useCartStore.getState().removeProduct(cartKey);

const getUnitPrice = (subtotal: string, quantity: number) => {
const numericSubtotal = parseFloat(subtotal.replace(/[^0-9.-]+/g, ''));
return isNaN(numericSubtotal)
? 'N/A'
: (numericSubtotal / quantity).toFixed(2);
// Update remote state in background
updateCart({
variables: {
input: {
clientMutationId: uuidv4(),
items: [{
key: cartKey,
quantity: 0
}],
},
},
});
};

return (
<div className="container mx-auto px-4 py-8">
{data?.cart?.contents?.nodes?.length ? (
{cart?.products?.length ? (
<>
<div className="bg-white rounded-lg p-6 mb-8 md:w-full">
{data.cart.contents.nodes.map((item: IProductRootObject) => (
{cart.products.map((item: Product) => (
<div
key={item.key}
key={item.cartKey}
className="flex items-center border-b border-gray-200 py-4"
>
<div className="flex-shrink-0 w-24 h-24 relative hidden md:block">
<Image
src={
item.product.node.image?.sourceUrl || '/placeholder.png'
}
alt={item.product.node.name}
src={item.image?.sourceUrl || '/placeholder.png'}
alt={item.name}
layout="fill"
objectFit="cover"
className="rounded"
/>
</div>
<div className="flex-grow ml-4">
<h2 className="text-lg font-semibold">
{item.product.node.name}
{item.name}
</h2>
<p className="text-gray-600">
kr {getUnitPrice(item.subtotal, item.quantity)}
kr {item.price}
</p>
</div>
<div className="flex items-center">
<input
type="number"
min="1"
value={item.quantity}
onChange={(event) => {
value={item.qty}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
const newQty = parseInt(event.target.value, 10);
if (isNaN(newQty) || newQty < 1) return;

// Update local state
useCartStore.getState().updateProductQuantity(item.cartKey, newQty);

// Update remote state in background
handleQuantityChange(
event,
item.key,
data.cart.contents.nodes,
updateCart,
updateCartProcessing,
item.cartKey,
newQty,
updateCart
);
}}
className="w-16 px-2 py-1 text-center border border-gray-300 rounded mr-2"
/>
<Button
handleButtonClick={() =>
handleRemoveProductClick(
item.key,
data.cart.contents.nodes,
)
}
handleButtonClick={() => handleRemoveProductClick(item.cartKey)}
variant="secondary"
buttonDisabled={updateCartProcessing}
>
Fjern
</Button>
</div>
<div className="ml-4">
<p className="text-lg font-semibold">{item.subtotal}</p>
<p className="text-lg font-semibold">{item.totalPrice}</p>
</div>
</div>
))}
</div>
<div className="bg-white rounded-lg p-6 md:w-full">
<div className="flex justify-end mb-4">
<span className="font-semibold pr-2">Subtotal:</span>
<span>{cartTotal}</span>
<span>{cart.totalProductsPrice}</span>
</div>
{!isCheckoutPage && (
<div className="flex justify-center mb-4">
Expand All @@ -172,14 +138,6 @@ const CartContents = () => {
</Link>
</div>
)}
{updateCartProcessing && (
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
<div className="bg-white p-4 rounded-lg">
<p className="text-lg mb-2">Oppdaterer handlekurv...</p>
<LoadingSpinner />
</div>
</div>
)}
</div>
);
};
Expand Down
41 changes: 10 additions & 31 deletions src/components/Checkout/CheckoutForm.component.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*eslint complexity: ["error", 20]*/
// Imports
import { useState, useContext, useEffect } from 'react';
import { useState, useEffect } from 'react';
import { useQuery, useMutation, ApolloError } from '@apollo/client';

// Components
Expand All @@ -11,7 +11,7 @@ import LoadingSpinner from '../LoadingSpinner/LoadingSpinner.component';
// GraphQL
import { GET_CART } from '@/utils/gql/GQL_QUERIES';
import { CHECKOUT_MUTATION } from '@/utils/gql/GQL_MUTATIONS';
import { CartContext } from '@/stores/CartProvider';
import useCartStore, { RootObject } from '@/stores/cart';

// Utils
import {
Expand Down Expand Up @@ -51,29 +51,17 @@ export interface ICheckoutData {
}

const CheckoutForm = () => {
const { cart, setCart } = useContext(CartContext);
const { cart, setCart } = useCartStore();
const [orderData, setOrderData] = useState<ICheckoutData | null>(null);
const [requestError, setRequestError] = useState<ApolloError | null>(null);
const [orderCompleted, setorderCompleted] = useState<boolean>(false);

// Get cart data query
const { data, refetch } = useQuery(GET_CART, {
useQuery(GET_CART, {
notifyOnNetworkStatusChange: true,
onCompleted: () => {
// Update cart in the localStorage.
const updatedCart = getFormattedCart(data);

if (!updatedCart && !data.cart.contents.nodes.length) {
localStorage.removeItem('woo-session');
localStorage.removeItem('wooocommerce-cart');
setCart(null);
return;
}

localStorage.setItem('woocommerce-cart', JSON.stringify(updatedCart));

// Update cart data in React Context.
setCart(updatedCart);
onCompleted: (data) => {
const updatedCart = getFormattedCart(data) as RootObject | undefined;
setCart(updatedCart || null);
},
});

Expand All @@ -84,16 +72,14 @@ const CheckoutForm = () => {
variables: {
input: orderData,
},
refetchQueries: [{ query: GET_CART }],
awaitRefetchQueries: true,
onCompleted: () => {
localStorage.removeItem('woo-session');
localStorage.removeItem('wooocommerce-cart');
setorderCompleted(true);
setCart(null);
refetch();
},
onError: (error) => {
setRequestError(error);
refetch();
},
},
);
Expand All @@ -102,15 +88,8 @@ const CheckoutForm = () => {
if (null !== orderData) {
// Perform checkout mutation when the value for orderData changes.
checkout();
setTimeout(() => {
refetch();
}, 2000);
}
}, [checkout, orderData, refetch]);

useEffect(() => {
refetch();
}, [refetch]);
}, [checkout, orderData]);

const handleFormSubmit = (submitData: ICheckoutDataProps) => {
const checkOutData = createCheckoutData(submitData);
Expand Down
Loading
Loading