feat: extract shop from mp/shop — initial libreshop/shop
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.
This commit is contained in:
Michael Czechowski
2026-04-29 17:48:56 +02:00
commit 44107c0734
134 changed files with 19521 additions and 0 deletions

View File

@@ -0,0 +1,236 @@
<template>
<main class="pt-4 lg:container mx-auto px-4">
<div v-if="displayState === 'loading'">
<div class="flex items-center justify-center h-[60vh]">
<div class="text-center">
<LoadingSpinner />
<p class="text-2xl mt-8 font-semibold">Bestellung wird geladen...</p>
<p class="text-lg">Bitte warte einen Moment, während wir deine Bestellung vorbereiten.</p>
</div>
</div>
</div>
<div v-else-if="displayState === 'main-content'">
<Stepper :step="4" />
<div class="flex gap-12 relative max-w-screen-lg mx-auto my-8">
<div v-if="order && order.id" class="flex flex-col gap-8 w-full">
<ClientOnly>
<ConfettiExplosion :colors="['#2563eb', '#ec4899', '#16a34a']" />
</ClientOnly>
<div>
<Heading :level="1">Vielen Dank für deine Bestellung bei MUELLERPRINTS!</Heading>
<Heading :level="2">Deine Bestellnummer: {{ order.id }}</Heading>
</div>
<p class="text-lg lg:w-2/3">
In Kürze erhälst du von uns eine E-Mail mit allen Einzelheiten zu deiner Bestellung. Du kannst sie auch hier herunterladen, sobald sie erstellt
wurde.
</p>
<div>
<Button id="download-invoice" :is-pending="!hasReachedMaxFailedRequests && (!isReadyToDownload || isDownloadPending)" @click="downloadInvoice">
<span v-if="hasReachedMaxFailedRequests">Bald verfügbar</span>
<span v-else>Rechnung herunterladen</span>
</Button>
<div v-if="hasReachedMaxFailedRequests" class="text-sm mt-3">
Es konnte noch keine Rechnung ermittelt werden. Bitte prüfe deine E-Mails oder kontaktiere uns: order@muellerprints.de
</div>
</div>
<hr />
<div class="flex flex-col gap-8">
<ul class="divide-y divide-gray-300" data-e2e="order-products">
<li v-for="(position, index) in orderProducts" :key="index" class="py-6 gap-6">
<div v-if="position.product" class="flex items-center justify-between">
<img
v-if="position.product?.images?.images"
:src="position.product?.images?.images[0]?.formats?.thumbnail?.url"
:alt="position.product?.name"
class="w-16 lg:w-24 object-cover"
/>
<div v-else class="bg-black opacity-5 w-12 h-12"></div>
<a :href="`/details/${position.product?.slug}`" class="p-2 text-xl font-bold flex-grow">{{ position.product?.name }}</a>
<span
data-e2e="cart-products-item-count"
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"
>
{{ position?.count }}
</span>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
<div v-else-if="displayState === 'terms-warning'">
<div class="bg-yellow-100 border-l-4 border-yellow-500 p-4 mb-8">
<p class="text-yellow-700 font-semibold">
Bitte, akzeptiere die
<a href="/agb" target="_blank" class="underline">AGB</a>
und
<a href="/datenschutz" target="_blank" class="underline">Datenschutzerklärung</a>, um fortzufahren.
</p>
<NuxtLink to="/checkout/1" class="text-yellow-700 hover:underline">Zurück zur Kasse</NuxtLink>
</div>
</div>
<div v-else-if="displayState === 'address-warning'">
<div class="bg-yellow-100 border-l-4 border-yellow-500 p-4 mb-8">
<p class="text-yellow-700 font-semibold">Bitte, gib deine Lieferadresse an, um fortzufahren.</p>
<NuxtLink to="/checkout/2" class="text-yellow-700 hover:underline">Zurück zur Kasse</NuxtLink>
</div>
</div>
<div v-else-if="displayState === 'payment-warning'">
<div class="bg-yellow-100 border-l-4 border-yellow-500 p-4 mb-8">
<p class="text-yellow-700 font-semibold">Bitte, gib eine Zahlungsart an, um fortzufahren.</p>
<NuxtLink to="/checkout/3" class="text-yellow-700 hover:underline">Zurück zur Kasse</NuxtLink>
</div>
</div>
<div v-else>
<div class="flex items-center justify-center h-[60vh]">
<div class="text-center">
<div class="w-20 h-20"></div>
<p class="text-2xl mt-8 font-semibold">Bestellung nicht gefunden...</p>
<p class="text-lg">Leider konnte deine Bestellung nicht unter dieser Adresse gefunden werden.</p>
<p class="text-lg mt-6">
Falls das Problem bestehen bleibt, <a href="/kontakt" class="underline">kontaktiere uns</a> bitte. Schicke deine Bestell-ID bitte an
paperwork@muellerprints.de
</p>
<pre class="mt-3 p-2 rounded-sm bg-gray-100">Bestell-ID: {{ uuid }}</pre>
</div>
</div>
</div>
</main>
</template>
<script setup lang="ts">
import type { Order } from "~/types";
import { trackEvent } from "~/utils/trackEvent";
const route = useRoute();
const uuid = computed(() => route.params.uuid as string);
const shopApi = useShopApi();
const order = ref<Order | null>(null);
const isLoading = ref(true);
const isDownloadPending = ref(false);
const failedDownloadRequests = ref(0);
const hasReachedMaxFailedRequests = ref(false);
const displayState = ref("loading");
const maxFailedRequests = 20;
const orderProducts = computed(() => order.value?.cart ?? []);
const isReadyToDownload = computed(() => !!order.value?.invoice);
onMounted(async () => {
let hasTrackedConversion = false;
const fetchAndSetOrder = async () => {
try {
order.value = await shopApi.getOrder(uuid.value);
updateDisplayState();
if (!order.value?.acceptedTermsAndConditionsAt || !order.value?.invoiceAddress || !order.value?.paymentAuthorised) {
console.error("Unauthorised view");
isLoading.value = false;
return;
}
// Track order confirmation view (only once)
if (!hasTrackedConversion) {
hasTrackedConversion = true;
trackEvent("order-confirmation-viewed", {
orderId: order.value.id,
orderTotal: order.value.total,
itemCount: order.value.cart?.length ?? 0
});
}
if (order.value?.invoice?.url) {
console.log("Invoice fetched successfully");
isLoading.value = false;
return;
}
failedDownloadRequests.value++;
if (failedDownloadRequests.value < maxFailedRequests) {
console.log("Retrying to fetch invoice", failedDownloadRequests.value);
setTimeout(fetchAndSetOrder, 3000);
} else {
hasReachedMaxFailedRequests.value = true;
isLoading.value = false;
}
} catch (e) {
console.error("Error fetching order:", e);
isLoading.value = false;
updateDisplayState();
}
};
await fetchAndSetOrder();
});
function updateDisplayState() {
if (!order.value) {
displayState.value = "loading";
return;
}
if (!order.value.id) {
displayState.value = "not-found-error";
return;
}
if (!order.value.acceptedTermsAndConditionsAt) {
displayState.value = "terms-warning";
return;
}
if (!order.value.invoiceAddress) {
displayState.value = "address-warning";
return;
}
if (!order.value.paymentAuthorised) {
displayState.value = "payment-warning";
return;
}
displayState.value = "main-content";
}
function downloadInvoice() {
if (!isReadyToDownload.value) {
console.error("Invoice not ready to download");
return;
}
try {
isDownloadPending.value = true;
if (order.value.invoice) {
trackEvent("order-invoice-downloaded", {
orderId: order.value.id
});
window.open(order.value.invoice.url, "_blank");
} else {
console.error("Invoice URL not available");
}
} catch (e) {
console.error("Error downloading invoice:", e);
} finally {
isDownloadPending.value = false;
}
}
// SEO
useSeoMeta({
title: "Bestellbestätigung | MUELLERPRINTS"
});
</script>