Files
shop/pages/cart.vue
Michael Czechowski 44107c0734
Some checks failed
Build and publish / build (push) Failing after 19s
feat: extract shop from mp/shop — initial libreshop/shop
Source moved verbatim from mp/shop/ on 2026-04-29; mp was the first
concrete adapter consuming the libreshop toolkit. Builds and publishes
git.librete.ch/libreshop/shop on every main / v* push via the standard
.gitea/workflows/build.yml shared across libreshop components.
2026-04-29 17:48:56 +02:00

181 lines
6.4 KiB
Vue

<template>
<main class="pt-4 pb-20 lg:container lg:max-w-screen-lg lg:mx-auto px-6">
<div class="relative w-full">
<!-- Loading state -->
<div v-if="isLoading" class="flex items-center justify-center h-[40vh]">
<div class="text-center">
<LoadingSpinner />
<p class="text-xl mt-4">Warenkorb wird geladen...</p>
</div>
</div>
<!-- Cart has products -->
<div v-else-if="cartProducts.length > 0" class="flex flex-col gap-8 w-full" data-e2e="cart">
<Heading :level="2" html-tag="h1" classes="text-center">
Im Warenkorb gesamt: <span class="text-nowrap">{{ numberFormatter(cart.total.value) }} </span>
</Heading>
<div class="grid place-content-center">
<Button href="/checkout" @click="handleCheckoutClickTop">Jetzt bezahlen</Button>
</div>
<hr />
<div class="flex flex-col gap-8">
<ul class="divide-y divide-gray-300" data-e2e="cart-products">
<li v-for="(position, index) in cartProducts" :key="index" class="py-6 gap-6 flex items-center justify-between">
<a v-if="position.product.images?.images" :href="`/details/${position.product.slug}`">
<img :src="position.product.images.images[0].formats.thumbnail.url" :alt="position.product.name" class="w-24 object-cover" />
</a>
<div v-else class="bg-black opacity-5 w-12 h-12"></div>
<div class="flex flex-col gap-3 lg:gap-6 lg:flex-row lg:items-center justify-between flex-grow">
<a :href="`/details/${position.product.slug}`" class="text-xl font-bold flex-grow">{{ position.product.name }}</a>
<div class="flex items-center justify-between">
<select
@change="changeCountCart(position, parseInt(($event.target as HTMLSelectElement).value))"
class="block appearance-none w-16 text-center bg-white border border-gray-300 hover:border-gray-500 px-4 py-2 rounded shadow leading-tight focus:outline-none focus:border-indigo-500 focus:shadow-outline"
>
<option v-for="count in 10" :key="count" :selected="position.count === count" :value="count">
{{ count }}
</option>
</select>
<div class="flex flex-col gap-2 flex-end w-24">
<span class="text-xl font-bold text-right text-nowrap">{{ numberFormatter(position.product.totalProductPrice * position.count) }} </span>
<button @click="removeFromCart(position.product.id, position.count)" class="text-gray-500 hover:text-gray-900 hover:underline text-right">
Entfernen
</button>
</div>
</div>
</div>
</li>
</ul>
<hr />
<div>
<div class="flex flex-col lg:flex-row justify-between mb-2">
<span class="text-2xl font-bold">Deine Gesamtsumme</span>
<div class="flex flex-col lg:flex-end">
<span class="text-2xl font-bold lg:text-right text-nowrap">{{ totalFormatted }} </span>
<span class="text-gray-600 text-sm lg:text-right">
Enthält MwSt. in Höhe von {{ VATFormatted }} {{ deliveryFormattedOrEmpty ? `inkl. ${deliveryFormattedOrEmpty}` : `zzgl.` }} Versandkosten
</span>
</div>
</div>
</div>
<div class="grid lg:place-content-end">
<Button href="/checkout" classes="w-full" @click="handleCheckoutClickBottom">Jetzt bezahlen</Button>
</div>
</div>
</div>
<!-- Cart is empty -->
<div v-else class="w-full">
<Heading :level="2" html-tag="h1" classes="text-center">Dein Warenkorb ist leer.</Heading>
<div class="grid place-content-center">
<NuxtLink
to="/notebooks"
class="inline-flex items-center justify-center px-8 py-3 rounded-full font-medium transition-all duration-200 bg-gray-900 text-white hover:bg-gray-700"
@click="handleContinueShopping"
>
Entdecke unsere Produkte
</NuxtLink>
</div>
</div>
</div>
</main>
</template>
<script setup lang="ts">
import { numberFormatter } from "~/utils/numberFormatter";
import { trackEvent } from "~/utils/trackEvent";
import type { CartProduct } from "~/types";
const shopApi = useShopApi();
const cart = useCart();
// Ensure cart is initialized on mount
onMounted(async () => {
await cart.ensureReady();
// Track cart view after cart is loaded
trackEvent("cart-viewed", {
itemCount: cart.productsCount.value,
cartValue: cart.total.value
});
});
// Local computed for cleaner template access
const isLoading = computed(() => !cart.isInitialized.value);
const cartProducts = computed(() => cart.products.value || ([] as CartProduct[]));
const totalFormatted = computed(() => numberFormatter(cart.total.value));
const VATFormatted = computed(() => numberFormatter(cart.VAT.value));
const deliveryFormattedOrEmpty = computed(() => (cart.delivery.value ? numberFormatter(cart.delivery.value, "€ ") : ""));
async function removeFromCart(productId: number, count = 1) {
try {
const product = cartProducts.value.find((p) => p.product.id === productId);
const cartAfterUpdate = await shopApi.removeProductFromCart(cart.uuid.value, productId, count);
cart.overwrite(cartAfterUpdate);
trackEvent("cart-product-removed", {
productId,
productSlug: product?.product.slug,
quantity: count,
cartValue: cart.total.value
});
} catch (error) {
console.error(`Could not remove product ${productId} from cart:`, error);
}
}
async function addToCart(productId: number, count = 1) {
try {
const cartAfterUpdate = await shopApi.addProductToCart(cart.uuid.value, productId, count);
cart.overwrite(cartAfterUpdate);
} catch (error) {
console.error(`Could not add product ${productId} to cart:`, error);
}
}
async function changeCountCart(position: CartProduct, count: number) {
const oldCount = position.count;
if (count > position.count) {
await addToCart(position.product.id, count - position.count);
} else if (count < position.count) {
await removeFromCart(position.product.id, position.count - count);
}
trackEvent("cart-quantity-changed", {
productId: position.product.id,
oldQuantity: oldCount,
newQuantity: count
});
}
function handleCheckoutClickTop() {
trackEvent("cart-cta-top-clicked", {
itemCount: cart.productsCount.value,
cartValue: cart.total.value
});
}
function handleCheckoutClickBottom() {
trackEvent("cart-cta-bottom-clicked", {
itemCount: cart.productsCount.value,
cartValue: cart.total.value
});
}
function handleContinueShopping() {
trackEvent("cart-empty-continue-shopping");
}
// SEO
useSeoMeta({
title: "Warenkorb | MUELLERPRINTS",
description: "Ihr Warenkorb bei MUELLERPRINTS"
});
</script>