feat: extract shop from mp/shop — initial libreshop/shop
Some checks failed
Build and publish / build (push) Failing after 19s
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:
98
components/ProductCard.vue
Normal file
98
components/ProductCard.vue
Normal 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>
|
||||
Reference in New Issue
Block a user