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,98 @@
<template>
<a
:href="product.slug ? `/details/${product.slug}` : undefined"
class="cursor-pointer w-full gap-2 flex flex-col items-stretch group hover:scale-105 transition-all z-100"
@click="handleProductClick"
>
<div
v-if="variant === 'neutral'"
class="p-12 flex items-center justify-center rounded-lg relative"
>
<div class="absolute bottom-5 drop-shadow-sm text-gray-800 text-md">{{ product.pattern?.name }}</div>
<img
v-if="product.images?.images"
:src="product.images?.images[0].formats.small.url"
:alt="product.name"
class="lg:w-full object-cover max-h-64 lg:max-h-fit" loading="lazy"
/>
<div v-else class="bg-black opacity-5 shadow-sm lg:w-full object-cover min-h-48 lg:max-h-fit"></div>
</div>
<Background
v-else
:coverId="product.cover?.id"
:patternId="product.pattern?.id"
:shade="200"
:gradient="true"
:class="[...shadowColors]"
class="p-12 flex items-center justify-center rounded-lg shadow-none group-hover:shadow-2xl transition-all duration-300 relative"
>
<div class="absolute bottom-5 drop-shadow-sm text-gray-800 text-md">{{ product.pattern?.name }}</div>
<img
v-if="product.images?.images"
:src="product.images?.images[0].formats.small.url"
:alt="product.name"
class="lg:w-full object-cover max-h-64 lg:max-h-fit" loading="lazy"
/>
<div v-else class="bg-black opacity-5 shadow-sm lg:w-full object-cover min-h-48 lg:max-h-fit"></div>
</Background>
<div class="flex flex-col justify-between">
<!-- Product Options Pills -->
<div v-if="hasProductOptions" class="pt-4 pb-1 flex flex-wrap gap-1">
<span v-if="product.cover?.name" class="text-xs px-2 py-1 bg-gray-100 rounded-full text-gray-600">{{ product.cover.name }}</span>
<span v-if="product.ruling?.name" class="text-xs px-2 py-1 bg-gray-100 rounded-full text-gray-600">{{ product.ruling.name }}</span>
<span v-if="product.pages?.name" class="text-xs px-2 py-1 bg-gray-100 rounded-full text-gray-600">{{ product.pages.name }}</span>
</div>
<!-- Price -->
<div v-if="product.totalProductPrice" class="px-2 py-2">
<p class="text-gray-600 text-xs text-left">ab {{ numberFormatter(product.totalProductPrice, "€") }}</p>
</div>
</div>
</a>
</template>
<script setup lang="ts">
import { randomTailwindColor } from "~/utils/randomTailwindColor";
import { numberFormatter } from "~/utils/numberFormatter";
import { trackEvent } from "~/utils/trackEvent";
import type { Product } from "~/types";
const props = withDefaults(defineProps<{
product: Product;
variant?: "default" | "neutral";
}>(), {
variant: "default",
});
const shadowColors = computed(() => {
return (
props.product?.pattern && [
randomTailwindColor(props.product.pattern.id, "group-hover:shadow", 200, "/60")
]
);
});
const hasProductOptions = computed(() => {
return props.product?.cover || props.product?.ruling || props.product?.pages;
});
function handlePersonalizedProductClick() {
const subject = `Anfrage für personalisiertes Produkt: ${props.product.cover?.name}`;
const body = `Hallo,\n\nich interessiere mich für ein personalisiertes Produkt: ${props.product.cover?.name}\n\nBitte kontaktieren Sie mich für weitere Details.\n\nVielen Dank!`;
window.location.href = `mailto:paperwork@muellerprints.de?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
}
function handleProductClick() {
trackEvent("product-card-clicked", {
productId: props.product.id,
productSlug: props.product.slug,
productName: props.product.name,
price: props.product.totalProductPrice
});
if (props.product.slug) {
navigateTo(`/details/${props.product.slug}`);
} else {
handlePersonalizedProductClick();
}
}
</script>