Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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
3,394 changes: 1,917 additions & 1,477 deletions package-lock.json

Large diffs are not rendered by default.

16 changes: 9 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
"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.2",
Expand All @@ -37,19 +36,22 @@
"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.24.0",
"@typescript-eslint/parser": "^8.24.0",
"babel-plugin-styled-components": "^2.1.4",
"eslint-config-next": "^15.1.7",
"@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",
"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