Some checks failed
Build and publish / build (push) Failing after 19s
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.
181 lines
6.4 KiB
Vue
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>
|