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.
183 lines
5.9 KiB
Vue
183 lines
5.9 KiB
Vue
<template>
|
|
<main>
|
|
<!-- Hero Section -->
|
|
<section class="bg-gray-900 text-white py-16 lg:py-24">
|
|
<div class="container mx-auto px-6 text-center">
|
|
<p class="text-sm uppercase tracking-widest opacity-60 mb-4">{{ coverData?.copyText?.format }}</p>
|
|
<h1 class="text-4xl lg:text-5xl font-bebas mb-4">{{ coverData?.name }}</h1>
|
|
<p v-if="coverData?.copyText?.details" class="text-lg opacity-80 max-w-2xl mx-auto" v-html="coverData.copyText.details" />
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Category Navigation -->
|
|
<section class="py-8 border-b border-gray-200">
|
|
<div class="container mx-auto px-6">
|
|
<div class="flex flex-wrap justify-center gap-3">
|
|
<NuxtLink
|
|
to="/notebooks"
|
|
class="px-4 py-2 rounded-full text-sm font-medium transition-colors bg-gray-100 text-gray-700 hover:bg-gray-200"
|
|
>
|
|
Alle
|
|
</NuxtLink>
|
|
<NuxtLink
|
|
v-for="c in covers"
|
|
:key="c.id"
|
|
:to="`/notebooks/${c.slug}`"
|
|
class="px-4 py-2 rounded-full text-sm font-medium transition-colors"
|
|
:class="c.slug === cover ? 'bg-gray-900 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'"
|
|
>
|
|
{{ c.name }}
|
|
</NuxtLink>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Products Grid -->
|
|
<section class="py-12 lg:py-16">
|
|
<div class="container mx-auto px-6">
|
|
<!-- Section Header -->
|
|
<div class="flex justify-between items-center mb-8">
|
|
<div>
|
|
<h2 class="text-xl font-semibold">{{ pagination?.total ?? 0 }} Notizbücher</h2>
|
|
<p class="text-sm text-gray-500">Seite {{ currentPage }} von {{ pagination?.pageCount ?? 1 }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div v-if="pending" class="text-center py-12">
|
|
<LoadingSpinner />
|
|
<p class="mt-4 text-gray-500">Produkte werden geladen...</p>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div v-else-if="error" class="text-center py-12 text-red-600">
|
|
<p>Fehler beim Laden der Produkte</p>
|
|
</div>
|
|
|
|
<!-- Products Grid -->
|
|
<div v-else class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6">
|
|
<ProductCard v-for="(item, i) in products" :key="i" :product="item" />
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
<div v-if="pagination && pagination.pageCount > 1" class="mt-12 flex justify-center gap-2">
|
|
<NuxtLink
|
|
v-for="pageNum in pagination.pageCount"
|
|
:key="pageNum"
|
|
:to="pageNum === 1 ? `/notebooks/${cover}` : `/notebooks/${cover}?page=${pageNum}`"
|
|
class="px-4 py-2 rounded-full text-sm font-medium transition-colors"
|
|
:class="pageNum === currentPage ? 'bg-gray-900 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'"
|
|
>
|
|
{{ pageNum }}
|
|
</NuxtLink>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Footer CTA -->
|
|
<section class="py-16 bg-gray-50 border-t border-gray-200">
|
|
<div class="container mx-auto px-6 text-center">
|
|
<h3 class="text-2xl font-bold mb-3">Fragen zu unseren Produkten?</h3>
|
|
<p class="text-gray-600 max-w-xl mx-auto mb-6">
|
|
Jedes Notizbuch wird in unserer Stuttgarter Werkstatt von Hand gebunden.
|
|
</p>
|
|
<NuxtLink
|
|
to="/about"
|
|
class="inline-block px-6 py-3 rounded-full font-medium bg-gray-900 text-white hover:bg-gray-700 transition-colors"
|
|
>
|
|
Mehr über uns
|
|
</NuxtLink>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { slugify } from "~/utils/slugify";
|
|
import { trackEvent } from "~/utils/trackEvent";
|
|
|
|
const route = useRoute();
|
|
const cover = computed(() => route.params.cover as string);
|
|
const currentPage = computed(() => parseInt(route.query.page as string) || 1);
|
|
|
|
const shopApi = useShopApi();
|
|
|
|
// Fetch cover data - key must be dynamic to avoid stale cache on navigation
|
|
const { data: coverResponse } = await useAsyncData(
|
|
() => `cover-${cover.value}`,
|
|
() => shopApi.getProductCoverById(cover.value),
|
|
{ watch: [cover] }
|
|
);
|
|
const coverData = computed(() => coverResponse.value?.data?.attributes);
|
|
|
|
// Fetch products
|
|
const { data, pending, error } = await useAsyncData(
|
|
() => `products-cover-${cover.value}-page-${currentPage.value}`,
|
|
() => shopApi.getCheapestProducts(cover.value, currentPage.value, 24),
|
|
{ watch: [cover, currentPage] }
|
|
);
|
|
|
|
// Fetch all covers for category navigation
|
|
const { data: coversData } = await useAsyncData("covers-nav", async () => {
|
|
try {
|
|
const { data } = await shopApi.getProductCovers();
|
|
return data
|
|
.map((c: any) => ({
|
|
id: c.id,
|
|
name: c.attributes?.name,
|
|
slug: slugify(c.attributes?.name ?? ""),
|
|
sort: c.attributes?.sort ?? 0
|
|
}))
|
|
.sort((a: any, b: any) => a.sort - b.sort);
|
|
} catch {
|
|
return [];
|
|
}
|
|
});
|
|
|
|
const covers = computed(() => coversData.value ?? []);
|
|
const products = computed(() => data.value?.data ?? []);
|
|
const pagination = computed(() => data.value?.meta?.pagination);
|
|
|
|
// Strip HTML tags from CMS rich text for use in meta tags
|
|
function stripHtml(html: string): string {
|
|
return html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
|
|
}
|
|
|
|
// SEO
|
|
useSeoMeta({
|
|
title: () => `${coverData.value?.name ?? "Notizbücher"} | MUELLERPRINTS`,
|
|
description: () => stripHtml(coverData.value?.copyText?.details ?? "Entdecken Sie unsere handgefertigten Notizbücher aus Stuttgart."),
|
|
ogTitle: () => `${coverData.value?.name ?? "Notizbücher"} | MUELLERPRINTS`,
|
|
ogDescription: () => stripHtml(coverData.value?.copyText?.details ?? "Entdecken Sie unsere handgefertigten Notizbücher aus Stuttgart.")
|
|
});
|
|
|
|
// Track category view on mount
|
|
onMounted(() => {
|
|
trackEvent("category-viewed", {
|
|
category: cover.value,
|
|
page: currentPage.value,
|
|
totalProducts: pagination.value?.total ?? 0
|
|
});
|
|
});
|
|
|
|
// Track pagination clicks
|
|
watch(currentPage, (newPage, oldPage) => {
|
|
if (newPage !== oldPage) {
|
|
trackEvent("category-pagination-clicked", {
|
|
category: cover.value,
|
|
page: newPage
|
|
});
|
|
}
|
|
});
|
|
|
|
// Track category filter changes
|
|
watch(cover, (newCover, oldCover) => {
|
|
if (newCover !== oldCover) {
|
|
trackEvent("category-filter-applied", {
|
|
from: oldCover,
|
|
to: newCover
|
|
});
|
|
}
|
|
});
|
|
</script>
|