Back cart with GraphQL storage

This commit is contained in:
Ruslan Bakiev
2026-04-04 09:08:51 +07:00
parent f1ee0850c9
commit 264b88bcee
10 changed files with 644 additions and 68 deletions

View File

@@ -1,14 +1,27 @@
import {
AddProductToCartDocument,
ClearCartDocument,
MyCartDocument,
RemoveCartItemDocument,
SetCartDeliveryAddressDocument,
UpdateCartItemQuantityDocument,
type MyCartQuery,
} from '~/composables/graphql/generated';
import { useGqlClient } from '~/composables/useGqlClient';
type CartParameters = {
width: number;
thickness: number;
color: string;
};
export type ClientCartItem = {
productId: string;
productName: string;
sku: string;
isCustomizable: boolean;
quantity: number;
parameters: {
width: number;
thickness: number;
color: string;
};
parameters: CartParameters;
};
function normalizeQuantity(value: number) {
@@ -19,8 +32,33 @@ function normalizeQuantity(value: number) {
return Math.floor(value);
}
function normalizeParameters(value: unknown): CartParameters {
const source = (value && typeof value === 'object') ? value as Partial<CartParameters> : {};
return {
width: Number(source.width ?? 100),
thickness: Number(source.thickness ?? 50),
color: typeof source.color === 'string' ? source.color : 'прозрачный',
};
}
function mapCartItem(item: MyCartQuery['myCart']['items'][number]): ClientCartItem {
return {
productId: item.productId,
productName: item.productName,
sku: item.sku,
isCustomizable: item.isCustomizable,
quantity: Number(item.quantity),
parameters: normalizeParameters(item.parameters),
};
}
export function useClientCart() {
const client = useGqlClient();
const items = useState<ClientCartItem[]>('client-cart-items', () => []);
const selectedDeliveryAddressId = useState<string>('client-cart-delivery-address-id', () => '');
const initialized = useState<boolean>('client-cart-initialized', () => false);
const loading = useState<boolean>('client-cart-loading', () => false);
const totalPositions = computed(() => items.value.length);
const totalItems = computed(() => items.value.reduce((sum, item) => sum + item.quantity, 0));
@@ -28,94 +66,138 @@ export function useClientCart() {
items.value.reduce((sum, item) => sum + item.quantity * item.parameters.width * item.parameters.thickness, 0),
);
function applyCart(cart: MyCartQuery['myCart']) {
items.value = cart.items.map(mapCartItem);
selectedDeliveryAddressId.value = cart.deliveryAddressId ?? '';
initialized.value = true;
}
async function fetchCart(force = false) {
if (loading.value) {
return;
}
if (initialized.value && !force) {
return;
}
loading.value = true;
try {
const { data } = await client.query({
query: MyCartDocument,
fetchPolicy: 'network-only',
});
if (data?.myCart) {
applyCart(data.myCart);
}
} finally {
loading.value = false;
}
}
function getQuantity(productId: string) {
return items.value.find((item) => item.productId === productId)?.quantity ?? 0;
}
function addProduct(product: {
async function addProduct(product: {
id: string;
name: string;
sku: string;
isCustomizable: boolean;
}) {
const existing = items.value.find((item) => item.productId === product.id);
if (existing) {
existing.quantity += 1;
return;
}
items.value.push({
productId: product.id,
productName: product.name,
sku: product.sku,
isCustomizable: product.isCustomizable,
quantity: 1,
parameters: {
width: 100,
thickness: 50,
color: 'прозрачный',
},
const { data } = await client.mutate({
mutation: AddProductToCartDocument,
variables: { productId: product.id },
fetchPolicy: 'no-cache',
});
if (data?.addProductToCart) {
applyCart(data.addProductToCart);
}
}
function setQuantity(productId: string, quantity: number) {
const existing = items.value.find((item) => item.productId === productId);
if (!existing) {
return;
}
async function setQuantity(productId: string, quantity: number) {
const normalizedQuantity = normalizeQuantity(quantity);
if (normalizedQuantity === 0) {
removeProduct(productId);
return;
}
const { data } = await client.mutate({
mutation: UpdateCartItemQuantityDocument,
variables: {
input: {
productId,
quantity: normalizedQuantity,
},
},
fetchPolicy: 'no-cache',
});
existing.quantity = normalizedQuantity;
if (data?.updateCartItemQuantity) {
applyCart(data.updateCartItemQuantity);
}
}
function incrementQuantity(productId: string) {
const existing = items.value.find((item) => item.productId === productId);
if (!existing) {
return;
}
existing.quantity += 1;
async function incrementQuantity(productId: string) {
await setQuantity(productId, getQuantity(productId) + 1);
}
function decrementQuantity(productId: string) {
const existing = items.value.find((item) => item.productId === productId);
if (!existing) {
return;
}
const normalizedQuantity = normalizeQuantity(existing.quantity - 1);
if (normalizedQuantity === 0) {
removeProduct(productId);
return;
}
existing.quantity = normalizedQuantity;
async function decrementQuantity(productId: string) {
await setQuantity(productId, getQuantity(productId) - 1);
}
function removeProduct(productId: string) {
items.value = items.value.filter((item) => item.productId !== productId);
async function removeProduct(productId: string) {
const { data } = await client.mutate({
mutation: RemoveCartItemDocument,
variables: { productId },
fetchPolicy: 'no-cache',
});
if (data?.removeCartItem) {
applyCart(data.removeCartItem);
}
}
function clearCart() {
items.value = [];
async function setDeliveryAddress(addressId: string | null) {
const { data } = await client.mutate({
mutation: SetCartDeliveryAddressDocument,
variables: { addressId: addressId || null },
fetchPolicy: 'no-cache',
});
if (data?.setCartDeliveryAddress) {
applyCart(data.setCartDeliveryAddress);
}
}
async function clearCart() {
const { data } = await client.mutate({
mutation: ClearCartDocument,
fetchPolicy: 'no-cache',
});
if (data?.clearCart) {
applyCart(data.clearCart);
}
}
if (!initialized.value && !loading.value) {
void fetchCart();
}
return {
items,
loading,
selectedDeliveryAddressId,
totalPositions,
totalItems,
totalVolume,
fetchCart,
addProduct,
setQuantity,
incrementQuantity,
decrementQuantity,
removeProduct,
clearCart,
setDeliveryAddress,
getQuantity,
};
}