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

104
components/Header.vue Normal file
View File

@@ -0,0 +1,104 @@
<template>
<header class="text-gray-900 z-50" :class="{ 'bg-white bg-opacity-95 absolute shadow-xl top-0 left-0 right-0 z-[99999]': isDarkMode }">
<div class="xl:container mx-auto lg:text-lg">
<div class="flex flex-col lg:flex-row lg:gap-12 relative">
<div class="flex lg:w-1/4 justify-between">
<NuxtLink to="/" class="max-sm:sticky max-sm:top-0 p-6 py-6 lg:py-7 hover:text-red-600" @click="trackEvent('header-logo-clicked')">
<span class="tracking-wide text-3xl font-bebas leading-none block">
<span class="!text-black">MUELLERPRINTS</span>.<br />Paperwork
</span>
</NuxtLink>
<button @click="toggleMobileMenu()" class="p-6 py-6 lg:py-7 lg:hidden" :class="{ 'bg-white': !isDarkMode }" aria-label="Menü öffnen" :aria-expanded="isMobileMenuOpen">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7"></path>
</svg>
</button>
</div>
<div
v-if="route.name"
role="navigation"
class="grow lg:w-3/4 p-6 py-6 lg:py-7 hidden lg:block bg-transparent"
:class="{ '!block bg-white': isMobileMenuOpen, 'text-gray-900': isMobileMenuOpen && isDarkMode }"
>
<nav class="flex justify-end" v-if="isCheckoutRoute" aria-label="Navigation">
<HeaderNavLink v-if="!isPaymentRoute" label="Zurück zum Warenkorb" path="/cart" @click="trackEvent('header-back-to-cart-clicked')" />
</nav>
<nav
class="flex flex-col lg:flex-row justify-between gap-4 lg:gap-28 lg:h-full"
v-else
aria-label="Navigation"
@click="closeMobileMenu()"
>
<div class="flex flex-col lg:flex-row gap-4 lg:gap-2">
<HeaderNavLink
label="Shop"
description="Alle Notizbücher"
path="/notebooks"
@click="trackEvent('header-item-clicked', { label: 'Shop' })"
/>
<HeaderNavLink
v-for="item in sortedCovers"
:key="item.id"
:label="item.label"
:description="item.description"
:path="`/notebooks/${item.slug}`"
@click="trackEvent('header-item-clicked', { label: item.label })"
/>
<HeaderNavLink
label="Über uns"
description="Über MUELLERPRINTS"
path="/about"
@click="trackEvent('header-item-clicked', { label: 'Über uns' })"
/>
</div>
<NuxtLink
to="/cart"
class="px-4 py-0.5 rounded-full font-medium text-lg text-black transition-all duration-200 flex items-center gap-2 hover:bg-gray-900 hover:text-white"
active-class="!bg-gray-900 !text-white"
title="Warenkorb"
@click="trackEvent('header-cart-clicked')"
>
Warenkorb
<span
v-if="productsCount > 0"
class="inline-flex items-center justify-center w-5 h-5 text-xs font-semibold rounded-full bg-gray-900 text-white group-hover:bg-white group-hover:text-gray-900"
:class="{ '!bg-white !text-gray-900': $route.path === '/cart' }"
>
{{ productsCount }}
</span>
</NuxtLink>
</nav>
</div>
</div>
</div>
</header>
</template>
<script setup lang="ts">
import type { CoverNavItem } from "~/types";
const props = defineProps<{
covers: CoverNavItem[];
isDarkMode?: boolean;
}>();
const route = useRoute();
const { productsCount } = useCart();
const isMobileMenuOpen = ref(false);
const sortedCovers = computed(() => [...props.covers].sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0)));
const isCheckoutRoute = computed(() => String(route.name).startsWith("checkout-"));
const isPaymentRoute = computed(() => route.name === "checkout-3");
function toggleMobileMenu() {
isMobileMenuOpen.value = !isMobileMenuOpen.value;
}
function closeMobileMenu() {
isMobileMenuOpen.value = false;
}
</script>