diff --git a/components/Cart/CartContents.vue b/components/Cart/CartContents.vue index b1679adf..cdea8f31 100644 --- a/components/Cart/CartContents.vue +++ b/components/Cart/CartContents.vue @@ -1,18 +1,24 @@ @@ -21,56 +27,43 @@ * Vue.js component for handling the logic of removing a product from the cart and updating the cart state. * * @module CartContents - * @param {Object} props - Object containing the component's properties. - * @param {Boolean} props.showCheckoutButton - Determines whether the checkout button should be shown or not. * @returns {Object} The Vue.js component object. */ -import GET_CART_QUERY from "@/apollo/queries/GET_CART_QUERY.gql"; -import UPDATE_CART_MUTATION from "@/apollo/mutations/UPDATE_CART_MUTATION.gql"; - +import { computed, ref, onMounted } from 'vue'; import { useCart } from "@/store/useCart"; +import CommonButton from '@/components/common/CommonButton.vue'; const cart = useCart(); +const isLoading = ref(true); +const error = ref(null); -defineProps({ - showCheckoutButton: { type: Boolean, required: false, default: false }, -}); - -const { data } = await useAsyncQuery(GET_CART_QUERY); +const cartItems = computed(() => cart.cart); /** * Handles the removal of a product. * - * @param {Object} product - The product to be removed. + * @param {string} key - The key of the product to be removed. */ -const handleRemoveProduct = (product) => { - const updatedItems = []; - - const { key } = product; - - updatedItems.push({ - key, - quantity: 0, - }); - - const variables = { - input: { - items: updatedItems, - }, - }; - - cart.removeProductFromCart(product); - - const { mutate, onDone, onError } = useMutation(UPDATE_CART_MUTATION, { - variables, - }); - - mutate(variables); - - onDone(() => { - document.location = "/cart"; - }); +const handleRemoveProduct = async (key) => { + try { + await cart.removeProductFromCart(key); + } catch (error) { + console.error('Error removing product from cart:', error); + // Optionally, you can add a user-friendly notification here + // without exposing the error details + } }; + +onMounted(async () => { + try { + await cart.refetch(); + } catch (err) { + console.error('Error fetching cart:', err); + error.value = err; + } finally { + isLoading.value = false; + } +}); \ No newline at end of file diff --git a/nuxt.config.js b/nuxt.config.js index 6294f2a7..c72c6e52 100644 --- a/nuxt.config.js +++ b/nuxt.config.js @@ -13,7 +13,7 @@ export default defineNuxtConfig({ "nuxt-icon", "@nuxt/image", ], - plugins: ["~/plugins/apollo"], + plugins: ["~/plugins/apollo", "~/plugins/cartUpdater"], runtimeConfig: { public: { graphqlURL: process.env.PUBLIC_GRAPHQL_URL, diff --git a/plugins/apollo.js b/plugins/apollo.js index cc616acf..a89631c6 100644 --- a/plugins/apollo.js +++ b/plugins/apollo.js @@ -57,12 +57,29 @@ export default defineNuxtPlugin((nuxtApp) => { ); // Cache implementation - const cache = new InMemoryCache(); + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + cart: { + merge(existing, incoming) { + return incoming; + }, + }, + }, + }, + }, + }); // Create the apollo client const apolloClient = new ApolloClient({ link: middleware.concat(afterware.concat(httpLink)), cache, + defaultOptions: { + watchQuery: { + fetchPolicy: 'cache-and-network', + }, + }, }); provideApolloClient(apolloClient); diff --git a/plugins/cartUpdater.js b/plugins/cartUpdater.js new file mode 100644 index 00000000..53e00956 --- /dev/null +++ b/plugins/cartUpdater.js @@ -0,0 +1,16 @@ +import { useCart } from '@/store/useCart'; + +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.hook('app:created', () => { + const cart = useCart(); + + // Refetch cart data on initial load + cart.refetch(); + + // Refetch cart data on route change + nuxtApp.$router.beforeEach((to, from, next) => { + cart.refetch(); + next(); + }); + }); +}); \ No newline at end of file diff --git a/store/useCart.js b/store/useCart.js index e47e2ee0..99a91071 100644 --- a/store/useCart.js +++ b/store/useCart.js @@ -1,103 +1,139 @@ import { defineStore } from "pinia"; - +import { useQuery, useMutation } from "@vue/apollo-composable"; +import { computed, ref, watch } from "vue"; import ADD_TO_CART_MUTATION from "@/apollo/mutations/ADD_TO_CART_MUTATION.gql"; +import UPDATE_CART_MUTATION from "@/apollo/mutations/UPDATE_CART_MUTATION.gql"; +import GET_CART_QUERY from "@/apollo/queries/GET_CART_QUERY.gql"; -/** - * Manages a shopping cart store using the Pinia library. - * - * @typedef {Object} CartState - * @property {Array} cart - The array representing the cart. - * @property {boolean} loading - The loading status of the cart. - * @property {string|null} error - The error message, if any. - */ +export const useCart = defineStore("cartState", () => { + const cart = ref([]); + const loading = ref(false); + const error = ref(null); + const cartTotals = ref({}); -const state = { - cart: [], - loading: false, - error: null, -}; + const { result: cartResult, loading: cartLoading, refetch: refetchCart } = useQuery(GET_CART_QUERY, null, { fetchPolicy: 'network-only' }); -export const useCart = defineStore("cartState", { - state: () => state, - actions: { - async addToCart(product) { - this.loading = true; - try { - const { mutate } = useMutation(ADD_TO_CART_MUTATION); - const response = await mutate({ - input: { - productId: product.databaseId, - quantity: 1, - }, - }); + watch(cartResult, (newCartResult) => { + if (newCartResult && newCartResult.cart) { + updateCartState(newCartResult.cart); + } + }); - if (response.data && response.data.addToCart) { - this.loading = false; - const newCartItem = response.data.addToCart.cartItem; - const foundProductInCartIndex = this.cart.findIndex( - (cartProduct) => newCartItem.product.node.slug === cartProduct.slug - ); + const updateCartState = (newCart) => { + cart.value = newCart.contents.nodes.map(item => ({ + key: item.key, + product: item.product.node, + variation: item.variation ? item.variation.node : null, + quantity: item.quantity, + total: item.total, + subtotal: item.subtotal, + subtotalTax: item.subtotalTax + })); - if (foundProductInCartIndex > -1) { - this.cart[foundProductInCartIndex].quantity += 1; - } else { - // We need to construct a cart item that matches the expected structure in `this.cart` - const productCopy = { - ...newCartItem.product.node, - quantity: newCartItem.quantity, - price: newCartItem.total, // Assuming 'total' is the price for one item - slug: newCartItem.product.node.slug, - }; + cartTotals.value = { + subtotal: newCart.subtotal, + subtotalTax: newCart.subtotalTax, + shippingTax: newCart.shippingTax, + shippingTotal: newCart.shippingTotal, + total: newCart.total, + totalTax: newCart.totalTax, + feeTax: newCart.feeTax, + feeTotal: newCart.feeTotal, + discountTax: newCart.discountTax, + discountTotal: newCart.discountTotal + }; + }; - this.cart.push(productCopy); - } - } else { - // Handle the case where the mutation does not return the expected data - this.error = "Did not receive expected cart data from the server."; - } - } catch (error) { - this.error = error.message || "An error occurred while adding to cart."; - } finally { - this.loading = false; - } - }, + const addToCart = async (product, quantity = 1) => { + loading.value = true; + error.value = null; + try { + const { mutate } = useMutation(ADD_TO_CART_MUTATION); + await mutate({ + input: { + productId: product.databaseId, + quantity: quantity, + }, + }); + await refetchCart(); + } catch (err) { + console.error("Error adding to cart:", err); + } finally { + loading.value = false; + } + }; - removeProductFromCart(product) { - this.cart.splice(this.cart.indexOf(product), 1); - }, - clearCart() { - this.cart.length = 0; - }, - }, - getters: { - getCartQuantity() { - if (!this.cart) { - console.error("Cart is undefined"); - return 0; - } - return this.cart.reduce((total, product) => total + product.quantity, 0); - }, + const updateCartItemQuantity = async (key, quantity) => { + loading.value = true; + error.value = null; + try { + const { mutate } = useMutation(UPDATE_CART_MUTATION); + await mutate({ + input: { + items: [{ key, quantity }], + }, + }); + await refetchCart(); + } catch (err) { + console.error("Error updating cart:", err); + await refetchCart(); + } finally { + loading.value = false; + } + }; - getCartTotal() { - const currencySymbol = useRuntimeConfig().public.currencySymbol || "kr"; + const removeProductFromCart = async (key) => { + loading.value = true; + error.value = null; + try { + await updateCartItemQuantity(key, 0); + } catch (err) { + console.error("Error removing product from cart:", err); + } finally { + loading.value = false; + await refetchCart(); + } + }; - const total = this.cart.reduce((total, product) => { - // Assuming product.price is a string that includes the currency symbol - const numericPrice = product.price - .replace(currencySymbol, "") - .replace(/[^0-9.]+/g, ""); + const clearCart = async () => { + loading.value = true; + error.value = null; + try { + for (const item of cart.value) { + await removeProductFromCart(item.key); + } + } catch (err) { + console.error("Error clearing cart:", err); + } finally { + loading.value = false; + await refetchCart(); + } + }; - // Convert the cleaned string to a floating-point number - const price = parseFloat(numericPrice); + const cartQuantity = computed(() => { + return cart.value.reduce((total, item) => total + item.quantity, 0); + }); - const productTotal = price * product.quantity; + const cartSubtotal = computed(() => { + return cartTotals.value.subtotal || '0'; + }); - return total + productTotal; - }, 0); + const cartTotal = computed(() => { + return cartTotals.value.total || '0'; + }); - // Format the total with the currency symbol and return it - return `${currencySymbol} ${total.toFixed(2)}`; - }, - }, - persist: true, + return { + cart, + loading, + error, + cartTotals, + addToCart, + updateCartItemQuantity, + removeProductFromCart, + clearCart, + cartQuantity, + cartSubtotal, + cartTotal, + refetch: refetchCart, + }; });