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:
289
composables/useCart.ts
Normal file
289
composables/useCart.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
import type { Cart, Order, CartProduct } from "~/types";
|
||||
import { trackEvent } from "~/utils/trackEvent";
|
||||
|
||||
const CART_STORAGE_KEY = "shop:cart";
|
||||
const MAX_RETRY_ATTEMPTS = 3;
|
||||
const RETRY_DELAYS = [1000, 2000, 4000]; // Exponential backoff: 1s, 2s, 4s
|
||||
|
||||
// Module-level promise for deduplication (not reactive, just a lock)
|
||||
let initPromise: Promise<void> | null = null;
|
||||
|
||||
export function useCart() {
|
||||
const shopApi = useShopApi();
|
||||
|
||||
// Use useState for proper Nuxt SSR/client state management
|
||||
const isInitialized = useState<boolean>("cart:initialized", () => false);
|
||||
const isInitializing = useState<boolean>("cart:initializing", () => false);
|
||||
const hasError = useState<boolean>("cart:hasError", () => false);
|
||||
const retryCount = useState<number>("cart:retryCount", () => 0);
|
||||
|
||||
const cart = useState<Cart>("cart", () => ({
|
||||
uuid: "",
|
||||
products: [],
|
||||
productsCount: 0,
|
||||
total: 0,
|
||||
subtotal: 0,
|
||||
VAT: 0,
|
||||
delivery: 0,
|
||||
deliveryMethod: null,
|
||||
emailAddress: "",
|
||||
invoiceAddress: "",
|
||||
deliveryAddress: "",
|
||||
invoiceAddressStructured: null,
|
||||
deliveryAddressStructured: null,
|
||||
acceptedTermsAndConditionsAt: false
|
||||
}));
|
||||
|
||||
function calculateCount(products: CartProduct[] | null) {
|
||||
return products ? products.reduce((sum, { count }) => sum + count, 0) : 0;
|
||||
}
|
||||
|
||||
function overwrite(order: Order) {
|
||||
cart.value.products = (order.cart as CartProduct[] | null) ?? [];
|
||||
cart.value.productsCount = calculateCount(cart.value.products);
|
||||
cart.value.total = order.total ?? 0;
|
||||
cart.value.subtotal = order.subtotal ?? 0;
|
||||
cart.value.VAT = order.VAT ?? 0;
|
||||
cart.value.delivery = order.delivery?.price ?? 0;
|
||||
cart.value.deliveryMethod = order.delivery?.id ?? null;
|
||||
cart.value.uuid = order.uuid ?? "";
|
||||
cart.value.emailAddress = order.email ?? "";
|
||||
cart.value.invoiceAddress = order.invoiceAddress ?? "";
|
||||
cart.value.deliveryAddress = order.deliveryAddress ?? "";
|
||||
cart.value.invoiceAddressStructured = order.invoiceAddressStructured ?? null;
|
||||
cart.value.deliveryAddressStructured = order.deliveryAddressStructured ?? null;
|
||||
cart.value.acceptedTermsAndConditionsAt = Boolean(order.acceptedTermsAndConditionsAt);
|
||||
|
||||
// Persist to localStorage (client-only)
|
||||
if (import.meta.client) {
|
||||
localStorage.setItem(CART_STORAGE_KEY, order.uuid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleep utility for retry delays
|
||||
*/
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset cart to empty state
|
||||
*/
|
||||
function resetCart() {
|
||||
cart.value = {
|
||||
uuid: "",
|
||||
products: [],
|
||||
productsCount: 0,
|
||||
total: 0,
|
||||
subtotal: 0,
|
||||
VAT: 0,
|
||||
delivery: 0,
|
||||
deliveryMethod: null,
|
||||
emailAddress: "",
|
||||
invoiceAddress: "",
|
||||
deliveryAddress: "",
|
||||
invoiceAddressStructured: null,
|
||||
deliveryAddressStructured: null,
|
||||
acceptedTermsAndConditionsAt: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to fetch cart with retry logic
|
||||
*/
|
||||
async function fetchWithRetry(attempt = 0): Promise<Order | null> {
|
||||
const storedUuid = localStorage.getItem(CART_STORAGE_KEY);
|
||||
|
||||
try {
|
||||
let order: Order | null = null;
|
||||
|
||||
if (storedUuid) {
|
||||
try {
|
||||
order = await shopApi.getOrder(storedUuid);
|
||||
// If order is paid, we need a fresh cart
|
||||
if (order?.paymentAuthorised) {
|
||||
order = null;
|
||||
}
|
||||
} catch {
|
||||
// Order not found or error - will create new one
|
||||
order = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Create new order if we don't have a valid one
|
||||
if (!order) {
|
||||
order = await shopApi.createOrder();
|
||||
}
|
||||
|
||||
return order;
|
||||
} catch (error) {
|
||||
// Track retry attempt
|
||||
if (attempt > 0) {
|
||||
trackEvent("cart-fetch-retry", { attempt, maxAttempts: MAX_RETRY_ATTEMPTS });
|
||||
}
|
||||
|
||||
// Check if we should retry
|
||||
if (attempt < MAX_RETRY_ATTEMPTS - 1) {
|
||||
const delay = RETRY_DELAYS[attempt] || RETRY_DELAYS[RETRY_DELAYS.length - 1];
|
||||
console.warn(`Cart fetch attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
|
||||
await sleep(delay);
|
||||
return fetchWithRetry(attempt + 1);
|
||||
}
|
||||
|
||||
// All retries exhausted
|
||||
trackEvent("cart-fetch-failed", { attempts: attempt + 1 });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetch() {
|
||||
// Only run on client
|
||||
if (!import.meta.client) return;
|
||||
|
||||
hasError.value = false;
|
||||
retryCount.value = 0;
|
||||
|
||||
try {
|
||||
const order = await fetchWithRetry();
|
||||
if (order) {
|
||||
overwrite(order);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching/creating cart after retries:", error);
|
||||
localStorage.removeItem(CART_STORAGE_KEY);
|
||||
hasError.value = true;
|
||||
resetCart();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry cart initialization after an error
|
||||
*/
|
||||
async function retry() {
|
||||
if (!import.meta.client) return;
|
||||
|
||||
hasError.value = false;
|
||||
isInitialized.value = false;
|
||||
retryCount.value += 1;
|
||||
|
||||
trackEvent("cart-manual-retry", { retryCount: retryCount.value });
|
||||
|
||||
await initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize cart - ensures cart is fetched exactly once.
|
||||
* Safe to call from multiple components; subsequent calls return the same promise.
|
||||
*/
|
||||
async function initialize() {
|
||||
// Only run on client
|
||||
if (!import.meta.client) return;
|
||||
|
||||
// Already initialized
|
||||
if (isInitialized.value) return;
|
||||
|
||||
// Already initializing - wait for existing promise
|
||||
if (isInitializing.value && initPromise) {
|
||||
return initPromise;
|
||||
}
|
||||
|
||||
// Start initialization
|
||||
isInitializing.value = true;
|
||||
initPromise = fetch().finally(() => {
|
||||
isInitialized.value = true;
|
||||
isInitializing.value = false;
|
||||
});
|
||||
|
||||
return initPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for cart to be ready. Use this in components that need the cart.
|
||||
*/
|
||||
async function ensureReady() {
|
||||
if (!import.meta.client) return;
|
||||
|
||||
if (!isInitialized.value) {
|
||||
await initialize();
|
||||
}
|
||||
}
|
||||
|
||||
async function addProduct(productId: number, count = 1) {
|
||||
// Ensure cart is initialized
|
||||
if (!cart.value.uuid) {
|
||||
await fetch();
|
||||
}
|
||||
|
||||
// Verify we have a valid uuid before making the API call
|
||||
if (!cart.value.uuid) {
|
||||
throw new Error("Failed to initialize cart - no order UUID available");
|
||||
}
|
||||
|
||||
const order = await shopApi.addProductToCart(cart.value.uuid, productId, count);
|
||||
overwrite(order);
|
||||
}
|
||||
|
||||
async function removeProduct(productId: number, count = 1) {
|
||||
const order = await shopApi.removeProductFromCart(cart.value.uuid, productId, count);
|
||||
overwrite(order);
|
||||
}
|
||||
|
||||
async function update(data: Partial<Order>) {
|
||||
const order = await shopApi.updateOrder(cart.value.uuid, data);
|
||||
overwrite(order);
|
||||
}
|
||||
|
||||
async function checkout() {
|
||||
return await shopApi.checkoutOrder(cart.value.uuid);
|
||||
}
|
||||
|
||||
// Expose individual computed refs for easier access in components
|
||||
const uuid = computed(() => cart.value.uuid);
|
||||
const products = computed(() => cart.value.products);
|
||||
const productsCount = computed(() => cart.value.productsCount);
|
||||
const total = computed(() => cart.value.total);
|
||||
const subtotal = computed(() => cart.value.subtotal);
|
||||
const VAT = computed(() => cart.value.VAT);
|
||||
const delivery = computed(() => cart.value.delivery);
|
||||
const deliveryMethod = computed(() => cart.value.deliveryMethod);
|
||||
const emailAddress = computed(() => cart.value.emailAddress);
|
||||
const invoiceAddress = computed(() => cart.value.invoiceAddress);
|
||||
const deliveryAddress = computed(() => cart.value.deliveryAddress);
|
||||
const invoiceAddressStructured = computed(() => cart.value.invoiceAddressStructured);
|
||||
const deliveryAddressStructured = computed(() => cart.value.deliveryAddressStructured);
|
||||
const acceptedTermsAndConditionsAt = computed(() => cart.value.acceptedTermsAndConditionsAt);
|
||||
|
||||
return {
|
||||
// State
|
||||
isInitialized,
|
||||
isInitializing,
|
||||
hasError,
|
||||
retryCount,
|
||||
// Individual computed refs for easy template access
|
||||
uuid,
|
||||
products,
|
||||
productsCount,
|
||||
total,
|
||||
subtotal,
|
||||
VAT,
|
||||
delivery,
|
||||
deliveryMethod,
|
||||
emailAddress,
|
||||
invoiceAddress,
|
||||
deliveryAddress,
|
||||
invoiceAddressStructured,
|
||||
deliveryAddressStructured,
|
||||
acceptedTermsAndConditionsAt,
|
||||
// Methods
|
||||
initialize,
|
||||
ensureReady,
|
||||
fetch,
|
||||
retry,
|
||||
overwrite,
|
||||
addProduct,
|
||||
removeProduct,
|
||||
update,
|
||||
checkout
|
||||
};
|
||||
}
|
||||
287
composables/usePageContent.ts
Normal file
287
composables/usePageContent.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
// Page content for static pages
|
||||
// Migrated from Strapi CMS to enable better styling and semantic URLs
|
||||
|
||||
export interface ContactInfo {
|
||||
name: string;
|
||||
street: string;
|
||||
postalCode: string;
|
||||
city: string;
|
||||
phone: string;
|
||||
fax?: string;
|
||||
email: string;
|
||||
website: string;
|
||||
vatId: string;
|
||||
}
|
||||
|
||||
export interface OpeningHoursDay {
|
||||
day: string;
|
||||
hours: string;
|
||||
}
|
||||
|
||||
export interface PageSection {
|
||||
title?: string;
|
||||
content: string; // markdown or HTML
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONTACT INFORMATION
|
||||
// =============================================================================
|
||||
|
||||
export const CONTACT_INFO: ContactInfo = {
|
||||
name: "Max Müller",
|
||||
street: "Rotenbergstraße 39",
|
||||
postalCode: "70190",
|
||||
city: "Stuttgart",
|
||||
phone: "+49 711 262 49 64",
|
||||
fax: "+49 711 262 48 60",
|
||||
email: "paperwork@muellerprints.de",
|
||||
website: "https://www.muellerprints.de",
|
||||
vatId: "DE 147595459",
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// OPENING HOURS
|
||||
// =============================================================================
|
||||
|
||||
export const OPENING_HOURS: OpeningHoursDay[] = [
|
||||
{ day: "Montag", hours: "09:00 - 18:00" },
|
||||
{ day: "Dienstag", hours: "09:00 - 18:00" },
|
||||
{ day: "Mittwoch", hours: "09:00 - 18:00" },
|
||||
{ day: "Donnerstag", hours: "09:00 - 18:00" },
|
||||
{ day: "Freitag", hours: "09:00 - 16:00" },
|
||||
{ day: "Samstag", hours: "Geschlossen" },
|
||||
{ day: "Sonntag", hours: "Geschlossen" },
|
||||
];
|
||||
|
||||
// =============================================================================
|
||||
// ABOUT PAGE CONTENT
|
||||
// =============================================================================
|
||||
|
||||
export const ABOUT_CONTENT = {
|
||||
hero: {
|
||||
title: "Über uns",
|
||||
subtitle: "Handarbeit aus Stuttgart seit über 30 Jahren",
|
||||
image: "/images/production/04.png",
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
content: `**Wir kombinieren traditionelles Buchbinderhandwerk mit moderner Technik, um einzigartige und langlebige Drucksachen zu erschaffen.**
|
||||
|
||||
Jedes unserer Produkte wird mit viel Liebe zum Detail und in echter Handarbeit gefertigt.
|
||||
|
||||
Da unsere Editionen auf max. 250 Exemplare limitiert sind, kannst Du sicher sein, ein besonderes Stück zu besitzen – hergestellt in unserer Werkstatt in Stuttgart.`,
|
||||
},
|
||||
{
|
||||
title: "Unsere Philosophie",
|
||||
content: `Wir glauben an Nachhaltigkeit und Qualität. Deshalb verwenden wir ausschließlich **100% Recyclingpapier** mit FSC-Zertifizierung und produzieren CO₂-neutral.
|
||||
|
||||
Jedes Notizbuch erzählt eine Geschichte – von den Händen, die es gebunden haben, bis zu den Ideen, die Du darin festhältst.`,
|
||||
},
|
||||
],
|
||||
images: ["/images/production/01.png", "/images/production/02.png", "/images/production/03.png"],
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// LEGAL PAGE CONTENT (Markdown)
|
||||
// =============================================================================
|
||||
|
||||
export const IMPRESSUM_CONTENT = `# Impressum
|
||||
|
||||
## muellerprints.
|
||||
|
||||
**Inhaber:** Max Müller
|
||||
Rotenbergstraße 39
|
||||
70190 Stuttgart
|
||||
T +49 (0)711 / 262 49 64
|
||||
F +49 (0)711 / 262 48 60
|
||||
paperwork@muellerprints.de
|
||||
[www.muellerprints.de](https://www.muellerprints.de)
|
||||
|
||||
Umsatzsteuer-Identifikationsnummer gemäß § 27 a Umsatzsteuergesetz: DE 147595459
|
||||
|
||||
Plattform der EU-Kommission zur Online-Streitbeilegung: [https://ec.europa.eu/consumers/odr](https://ec.europa.eu/consumers/odr)
|
||||
|
||||
Wir sind zur Teilnahme an einem Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle weder verpflichtet noch bereit.
|
||||
|
||||
© muellerprints. Stuttgart 2025
|
||||
|
||||
Mitglied der Initiative "Fairness im Handel".
|
||||
Nähere Informationen: [https://www.fairness-im-handel.de](https://www.fairness-im-handel.de)`;
|
||||
|
||||
export const KONTAKT_CONTENT = `# Kontakt
|
||||
|
||||
Für Fragen, Anregungen oder Probleme stehen wir Ihnen gerne zur Verfügung.
|
||||
|
||||
Sie erreichen unseren Kundenservice telefonisch unter **+49 711 262 49 64** oder per E-Mail unter [paperwork@muellerprints.de](mailto:paperwork@muellerprints.de).`;
|
||||
|
||||
export const AGB_CONTENT = `# Allgemeine Geschäftsbedingungen mit Kundeninformationen
|
||||
|
||||
## Inhaltsverzeichnis
|
||||
1. Geltungsbereich
|
||||
2. Vertragsschluss
|
||||
3. Widerrufsrecht
|
||||
4. Preise und Zahlungsbedingungen
|
||||
5. Liefer- und Versandbedingungen
|
||||
6. Eigentumsvorbehalt
|
||||
7. Mängelhaftung (Gewährleistung)
|
||||
8. Anwendbares Recht
|
||||
9. Alternative Streitbeilegung
|
||||
|
||||
## 1) Geltungsbereich
|
||||
|
||||
1.1 Diese Allgemeinen Geschäftsbedingungen (nachfolgend "AGB") des Max Müller (nachfolgend "Verkäufer"), gelten für alle Verträge über die Lieferung von Waren, die ein Verbraucher oder Unternehmer (nachfolgend „Kunde") mit dem Verkäufer hinsichtlich der vom Verkäufer in seinem Online-Shop dargestellten Waren abschließt. Hiermit wird der Einbeziehung von eigenen Bedingungen des Kunden widersprochen, es sei denn, es ist etwas anderes vereinbart.
|
||||
|
||||
1.2 Verbraucher im Sinne dieser AGB ist jede natürliche Person, die ein Rechtsgeschäft zu Zwecken abschließt, die überwiegend weder ihrer gewerblichen noch ihrer selbständigen beruflichen Tätigkeit zugerechnet werden können.
|
||||
|
||||
1.3 Unternehmer im Sinne dieser AGB ist eine natürliche oder juristische Person oder eine rechtsfähige Personengesellschaft, die bei Abschluss eines Rechtsgeschäfts in Ausübung ihrer gewerblichen oder selbständigen beruflichen Tätigkeit handelt.
|
||||
|
||||
## 2) Vertragsschluss
|
||||
|
||||
2.1 Die im Online-Shop des Verkäufers enthaltenen Produktbeschreibungen stellen keine verbindlichen Angebote seitens des Verkäufers dar, sondern dienen zur Abgabe eines verbindlichen Angebots durch den Kunden.
|
||||
|
||||
2.2 Der Kunde kann das Angebot über das in den Online-Shop des Verkäufers integrierte Online-Bestellformular abgeben. Dabei gibt der Kunde, nachdem er die ausgewählten Waren in den virtuellen Warenkorb gelegt und den elektronischen Bestellprozess durchlaufen hat, durch Klicken des den Bestellvorgang abschließenden Buttons ein rechtlich verbindliches Vertragsangebot in Bezug auf die im Warenkorb enthaltenen Waren ab.
|
||||
|
||||
2.3 Der Verkäufer kann das Angebot des Kunden innerhalb von fünf Tagen annehmen, indem er dem Kunden eine schriftliche Auftragsbestätigung oder eine Auftragsbestätigung in Textform (Fax oder E-Mail) übermittelt, wobei insoweit der Zugang der Auftragsbestätigung beim Kunden maßgeblich ist, oder indem er dem Kunden die bestellte Ware liefert, wobei insoweit der Zugang der Ware beim Kunden maßgeblich ist, oder indem er den Kunden nach Abgabe von dessen Bestellung zur Zahlung auffordert.
|
||||
|
||||
2.4 Die Annahme des Angebots (und damit der Vertragsschluss) erfolgt erst mit der Versendung der Ware bzw. der ausdrücklichen Auftragsbestätigung. Bei elektronischer Zahlung erfolgt die Annahme mit Bestätigung der Zahlungstransaktion.
|
||||
|
||||
## 3) Widerrufsrecht
|
||||
|
||||
3.1 Verbrauchern steht grundsätzlich ein Widerrufsrecht zu.
|
||||
|
||||
3.2 Nähere Informationen zum Widerrufsrecht ergeben sich aus der Widerrufsbelehrung des Verkäufers.
|
||||
|
||||
## 4) Preise und Zahlungsbedingungen
|
||||
|
||||
4.1 Sofern sich aus der Produktbeschreibung des Verkäufers nichts anderes ergibt, handelt es sich bei den angegebenen Preisen um Gesamtpreise, die die gesetzliche Umsatzsteuer enthalten. Gegebenenfalls zusätzlich anfallende Liefer- und Versandkosten werden in der jeweiligen Produktbeschreibung gesondert angegeben.
|
||||
|
||||
4.2 Die Zahlungsmöglichkeit/en wird/werden dem Kunden im Online-Shop des Verkäufers mitgeteilt.
|
||||
|
||||
4.3 Bei Auswahl der Zahlungsart „PayPal" erfolgt die Zahlungsabwicklung über den Zahlungsdienstleister PayPal (Europe) S.à r.l. et Cie, S.C.A., 22-24 Boulevard Royal, L-2449 Luxembourg, unter Geltung der PayPal-Nutzungsbedingungen.
|
||||
|
||||
## 5) Liefer- und Versandbedingungen
|
||||
|
||||
5.1 Die Lieferung von Waren erfolgt auf dem Versandweg an die vom Kunden angegebene Lieferanschrift, sofern nichts anderes vereinbart ist.
|
||||
|
||||
5.2 Sendet das Transportunternehmen die versandte Ware an den Verkäufer zurück, da eine Zustellung beim Kunden nicht möglich war, trägt der Kunde die Kosten für den erfolglosen Versand.
|
||||
|
||||
5.3 Bei Selbstabholung informiert der Verkäufer den Kunden zunächst per E-Mail darüber, dass die von ihm bestellte Ware zur Abholung bereit steht. Nach Erhalt dieser E-Mail kann der Kunde die Ware nach Absprache mit dem Verkäufer am Sitz des Verkäufers abholen.
|
||||
|
||||
## 6) Eigentumsvorbehalt
|
||||
|
||||
Tritt der Verkäufer in Vorleistung, behält er sich bis zur vollständigen Bezahlung des geschuldeten Kaufpreises das Eigentum an der gelieferten Ware vor.
|
||||
|
||||
## 7) Mängelhaftung (Gewährleistung)
|
||||
|
||||
7.1 Ist die Kaufsache mangelhaft, gelten die Vorschriften der gesetzlichen Mängelhaftung.
|
||||
|
||||
7.2 Handelt der Kunde als Verbraucher, wird er gebeten, angelieferte Waren mit offensichtlichen Transportschäden bei dem Zusteller zu reklamieren und den Verkäufer hiervon in Kenntnis zu setzen.
|
||||
|
||||
## 8) Anwendbares Recht
|
||||
|
||||
Für sämtliche Rechtsbeziehungen der Parteien gilt das Recht der Bundesrepublik Deutschland unter Ausschluss der Gesetze über den internationalen Kauf beweglicher Waren. Bei Verbrauchern gilt diese Rechtswahl nur insoweit, als nicht der gewährte Schutz durch zwingende Bestimmungen des Rechts des Staates, in dem der Verbraucher seinen gewöhnlichen Aufenthalt hat, entzogen wird.
|
||||
|
||||
## 9) Alternative Streitbeilegung
|
||||
|
||||
9.1 Die EU-Kommission stellt im Internet unter folgendem Link eine Plattform zur Online-Streitbeilegung bereit: [https://ec.europa.eu/consumers/odr](https://ec.europa.eu/consumers/odr)
|
||||
|
||||
Diese Plattform dient als Anlaufstelle zur außergerichtlichen Beilegung von Streitigkeiten aus Online-Kauf- oder Dienstleistungsverträgen, an denen ein Verbraucher beteiligt ist.
|
||||
|
||||
9.2 Der Verkäufer ist zur Teilnahme an einem Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle weder verpflichtet noch bereit.`;
|
||||
|
||||
export const DATENSCHUTZ_CONTENT = `# Datenschutzerklärung
|
||||
|
||||
## 1) Information über die Erhebung personenbezogener Daten und Kontaktdaten des Verantwortlichen
|
||||
|
||||
1.1 Wir freuen uns, dass Sie unsere Website besuchen und bedanken uns für Ihr Interesse. Im Folgenden informieren wir Sie über den Umgang mit Ihren personenbezogenen Daten bei der Nutzung unserer Website. Personenbezogene Daten sind hierbei alle Daten, mit denen Sie persönlich identifiziert werden können.
|
||||
|
||||
1.2 Verantwortlicher für die Datenverarbeitung auf dieser Website im Sinne der Datenschutz-Grundverordnung (DSGVO) ist Max Müller, Rotenbergstraße 39, 70190 Stuttgart, Deutschland, Tel.: +49 711 262 49 64, E-Mail: paperwork@muellerprints.de. Der für die Verarbeitung von personenbezogenen Daten Verantwortliche ist diejenige natürliche oder juristische Person, die allein oder gemeinsam mit anderen über die Zwecke und Mittel der Verarbeitung von personenbezogenen Daten entscheidet.
|
||||
|
||||
## 2) Datenerfassung beim Besuch unserer Website
|
||||
|
||||
Bei der bloß informatorischen Nutzung unserer Website, also wenn Sie sich nicht registrieren oder uns anderweitig Informationen übermitteln, erheben wir nur solche Daten, die Ihr Browser an unseren Server übermittelt (sog. „Server-Logfiles"). Wenn Sie unsere Website aufrufen, erheben wir die folgenden Daten, die für uns technisch erforderlich sind, um Ihnen die Website anzuzeigen:
|
||||
|
||||
- Unsere besuchte Website
|
||||
- Datum und Uhrzeit zum Zeitpunkt des Zugriffes
|
||||
- Menge der gesendeten Daten in Byte
|
||||
- Quelle/Verweis, von welchem Sie auf die Seite gelangten
|
||||
- Verwendeter Browser
|
||||
- Verwendetes Betriebssystem
|
||||
- Verwendete IP-Adresse (ggf.: in anonymisierter Form)
|
||||
|
||||
Die Verarbeitung erfolgt gemäß Art. 6 Abs. 1 lit. f DSGVO auf Basis unseres berechtigten Interesses an der Verbesserung der Stabilität und Funktionalität unserer Website. Eine Weitergabe oder anderweitige Verwendung der Daten findet nicht statt. Wir behalten uns allerdings vor, die Server-Logfiles nachträglich zu überprüfen, sollten konkrete Anhaltspunkte auf eine rechtswidrige Nutzung hinweisen.
|
||||
|
||||
## 3) Cookies
|
||||
|
||||
Um den Besuch unserer Website attraktiv zu gestalten und die Nutzung bestimmter Funktionen zu ermöglichen, verwenden wir auf verschiedenen Seiten sogenannte Cookies. Hierbei handelt es sich um kleine Textdateien, die auf Ihrem Endgerät abgelegt werden. Einige der von uns verwendeten Cookies werden nach dem Ende der Browser-Sitzung, also nach Schließen Ihres Browsers, wieder gelöscht (sog. Sitzungs-Cookies). Andere Cookies verbleiben auf Ihrem Endgerät und ermöglichen, Ihren Browser beim nächsten Besuch wiederzuerkennen (sog. persistente Cookies).
|
||||
|
||||
Sie können Ihren Browser so einstellen, dass Sie über das Setzen von Cookies informiert werden und einzeln über deren Annahme entscheiden oder die Annahme von Cookies für bestimmte Fälle oder generell ausschließen können.
|
||||
|
||||
## 4) Kontaktaufnahme
|
||||
|
||||
Im Rahmen der Kontaktaufnahme mit uns (z.B. per Kontaktformular oder E-Mail) werden personenbezogene Daten erhoben. Welche Daten im Falle eines Kontaktformulars erhoben werden, ist aus dem jeweiligen Kontaktformular ersichtlich. Diese Daten werden ausschließlich zum Zweck der Beantwortung Ihres Anliegens bzw. für die Kontaktaufnahme und die damit verbundene technische Administration gespeichert und verwendet.
|
||||
|
||||
Rechtsgrundlage für die Verarbeitung dieser Daten ist unser berechtigtes Interesse an der Beantwortung Ihres Anliegens gemäß Art. 6 Abs. 1 lit. f DSGVO. Zielt Ihre Kontaktierung auf den Abschluss eines Vertrages ab, so ist zusätzliche Rechtsgrundlage für die Verarbeitung Art. 6 Abs. 1 lit. b DSGVO. Ihre Daten werden nach abschließender Bearbeitung Ihrer Anfrage gelöscht.
|
||||
|
||||
## 5) Datenverarbeitung bei Eröffnung eines Kundenkontos und zur Vertragsabwicklung
|
||||
|
||||
Gemäß Art. 6 Abs. 1 lit. b DSGVO werden personenbezogene Daten weiterhin erhoben und verarbeitet, wenn Sie uns diese zur Durchführung eines Vertrages oder bei der Eröffnung eines Kundenkontos mitteilen. Welche Daten erhoben werden, ist aus den jeweiligen Eingabeformularen ersichtlich.
|
||||
|
||||
Eine Löschung Ihres Kundenkontos ist jederzeit möglich und kann durch eine Nachricht an die o.g. Adresse des Verantwortlichen erfolgen. Wir speichern und verwenden die von Ihnen mitgeteilten Daten zur Vertragsabwicklung. Nach vollständiger Abwicklung des Vertrages oder Löschung Ihres Kundenkontos werden Ihre Daten mit Rücksicht auf steuer- und handelsrechtliche Aufbewahrungsfristen gesperrt und nach Ablauf dieser Fristen gelöscht.
|
||||
|
||||
## 6) Nutzung von Kundendaten zur Direktwerbung
|
||||
|
||||
Wenn Sie Ihre E-Mail-Adresse beim Kauf von Waren mitteilen, behalten wir uns vor, Ihnen regelmäßig Angebote für ähnliche Waren aus unserem Sortiment per E-Mail zuzusenden. Hierfür müssen wir keine gesonderte Einwilligung von Ihnen einholen.
|
||||
|
||||
Sie können der Verwendung Ihrer E-Mail-Adresse jederzeit widersprechen, ohne dass hierfür andere als die Übermittlungskosten nach den Basistarifen entstehen. Eine Abmeldung kann über den in jeder E-Mail enthaltenen Link erfolgen.
|
||||
|
||||
## 7) Datenverarbeitung zur Bestellabwicklung
|
||||
|
||||
Die von uns erhobenen personenbezogenen Daten werden im Rahmen der Vertragsabwicklung an das mit der Lieferung beauftragte Transportunternehmen weitergegeben, soweit dies zur Lieferung der Ware erforderlich ist.
|
||||
|
||||
Ihre Zahlungsdaten geben wir im Rahmen der Zahlungsabwicklung an das beauftragte Kreditinstitut weiter, sofern dies für die Zahlungsabwicklung erforderlich ist. Sofern Zahlungsdienstleister eingesetzt werden, informieren wir Sie hierüber nachstehend explizit.
|
||||
|
||||
## 8) PayPal
|
||||
|
||||
Bei Zahlung via PayPal, Kreditkarte via PayPal, Lastschrift via PayPal oder – falls angeboten – "Kauf auf Rechnung" oder "Ratenzahlung" via PayPal geben wir Ihre Zahlungsdaten im Rahmen der Zahlungsabwicklung an die PayPal (Europe) S.à r.l. et Cie, S.C.A., 22-24 Boulevard Royal, L-2449 Luxembourg (nachfolgend "PayPal"), weiter. Die Weitergabe erfolgt gemäß Art. 6 Abs. 1 lit. b DSGVO und nur insoweit, als dies für die Zahlungsabwicklung erforderlich ist.
|
||||
|
||||
## 9) Rechte des Betroffenen
|
||||
|
||||
9.1 Das geltende Datenschutzrecht gewährt Ihnen gegenüber dem Verantwortlichen hinsichtlich der Verarbeitung Ihrer personenbezogenen Daten umfassende Betroffenenrechte (Auskunfts- und Interventionsrechte), über die wir Sie nachstehend informieren:
|
||||
|
||||
- Auskunftsrecht gemäß Art. 15 DSGVO
|
||||
- Recht auf Berichtigung gemäß Art. 16 DSGVO
|
||||
- Recht auf Löschung gemäß Art. 17 DSGVO
|
||||
- Recht auf Einschränkung der Verarbeitung gemäß Art. 18 DSGVO
|
||||
- Recht auf Unterrichtung gemäß Art. 19 DSGVO
|
||||
- Recht auf Datenübertragbarkeit gemäß Art. 20 DSGVO
|
||||
- Recht auf Widerruf erteilter Einwilligungen gemäß Art. 7 Abs. 3 DSGVO
|
||||
- Recht auf Beschwerde gemäß Art. 77 DSGVO
|
||||
|
||||
9.2 WIDERSPRUCHSRECHT
|
||||
|
||||
Wenn wir im Rahmen einer Interessenabwägung Ihre personenbezogenen Daten aufgrund unseres überwiegenden berechtigten Interesses verarbeiten, haben Sie das jederzeitige Recht, aus Gründen, die sich aus Ihrer besonderen Situation ergeben, gegen diese Verarbeitung Widerspruch mit Wirkung für die Zukunft einzulegen.
|
||||
|
||||
Machen Sie von Ihrem Widerspruchsrecht Gebrauch, beenden wir die Verarbeitung der betroffenen Daten. Eine Weiterverarbeitung bleibt aber vorbehalten, wenn wir zwingende schutzwürdige Gründe für die Verarbeitung nachweisen können, die Ihre Interessen, Grundrechte und Grundfreiheiten überwiegen, oder wenn die Verarbeitung der Geltendmachung, Ausübung oder Verteidigung von Rechtsansprüchen dient.
|
||||
|
||||
## 10) Dauer der Speicherung personenbezogener Daten
|
||||
|
||||
Die Dauer der Speicherung von personenbezogenen Daten bemisst sich anhand der jeweiligen gesetzlichen Aufbewahrungsfrist (z.B. handels- und steuerrechtliche Aufbewahrungsfristen). Nach Ablauf der Frist werden die entsprechenden Daten routinemäßig gelöscht, sofern sie nicht mehr zur Vertragserfüllung oder Vertragsanbahnung erforderlich sind und/oder unsererseits kein berechtigtes Interesse an der Weiterspeicherung fortbesteht.`;
|
||||
|
||||
export const VERSAND_CONTENT = `Unsere Lieferungen erfolgen in der Regel innerhalb von **5 Arbeitstagen** nach Bestelleingang, sofern nicht anders vereinbart.
|
||||
|
||||
Die Lieferung erfolgt an die von Ihnen angegebene Lieferadresse. Sollte die Lieferung ausnahmsweise nicht möglich sein, werden wir Sie umgehend darüber informieren und gegebenenfalls einen neuen Liefertermin vereinbaren.
|
||||
|
||||
Die Versandkosten werden während des Bestellvorgangs angezeigt und richten sich nach dem Lieferort sowie dem Gesamtgewicht der Bestellung.`;
|
||||
|
||||
export const ZAHLUNG_CONTENT = `Wir bieten Ihnen verschiedene Zahlungsmöglichkeiten an, um Ihnen den Einkauf so bequem wie möglich zu gestalten.
|
||||
|
||||
Alle Transaktionen werden über verschlüsselte Verbindungen abgewickelt. Ihre sensiblen Daten sind durch modernste Sicherheitsstandards geschützt – Sie können bei uns sorgenfrei einkaufen.
|
||||
|
||||
Bei Zahlung per Kreditkarte oder PayPal werden Sie während des Bestellvorgangs auf die jeweilige Zahlungsplattform weitergeleitet. Nach erfolgreicher Autorisierung kehren Sie automatisch zu uns zurück und erhalten Ihre Bestellbestätigung.`;
|
||||
|
||||
// =============================================================================
|
||||
// GOOGLE MAPS EMBED
|
||||
// =============================================================================
|
||||
|
||||
export const GOOGLE_MAPS_EMBED_URL = "https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2628.8892444744!2d9.1967!3d48.7891!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x4799c37b5e0f5a6b%3A0x6e9f1d2b3c4d5e6f!2sRotenbergstra%C3%9Fe%2039%2C%2070190%20Stuttgart!5e0!3m2!1sde!2sde!4v1234567890";
|
||||
369
composables/useProductContent.ts
Normal file
369
composables/useProductContent.ts
Normal file
@@ -0,0 +1,369 @@
|
||||
// Product content keyed by cover type
|
||||
// This content is displayed on product detail pages as storytelling modules
|
||||
|
||||
export type CoverType = "Hardcover" | "Softcover" | "Heft" | "Spiralbindung";
|
||||
|
||||
export interface FeatureModule {
|
||||
eyebrow?: string;
|
||||
headline: string;
|
||||
subtitle?: string;
|
||||
body: string;
|
||||
image?: string;
|
||||
imageRight?: boolean;
|
||||
}
|
||||
|
||||
export interface UseCase {
|
||||
icon: "pen" | "palette" | "clipboard" | "briefcase";
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FEATURE MODULES - Storytelling sections per cover type
|
||||
// =============================================================================
|
||||
|
||||
export const FEATURE_MODULES: Record<CoverType, FeatureModule[]> = {
|
||||
Hardcover: [
|
||||
{
|
||||
eyebrow: "Handarbeit",
|
||||
headline: "Klassische Fadenheftung",
|
||||
subtitle: "Strapazierfähig bei intensivem Gebrauch",
|
||||
body: `Jedes Heft wird mit Singer-Stich-Heftung von Hand gebunden.
|
||||
Das Ergebnis: Ein Notizbuch, das sich vollständig flach öffnen
|
||||
lässt und auch nach Jahren intensiver Nutzung nicht auseinanderfällt.
|
||||
Der rote Vorsatzbogen setzt einen dezenten Farbakzent –
|
||||
Deine erste Begegnung beim Öffnen.`,
|
||||
image: "/images/features/hardcover/binding-closeup.jpg",
|
||||
imageRight: false,
|
||||
},
|
||||
{
|
||||
eyebrow: "100% Recycling",
|
||||
headline: "VIVUS 89 Papier",
|
||||
subtitle: "Außergewöhnliche Schreibqualität",
|
||||
body: `VIVUS 89 ist nicht irgendein Recyclingpapier.
|
||||
Die matte, ungestrichene Oberfläche mit feiner Textur verhindert
|
||||
Durchschlagen – selbst bei Füllfederhaltern und Aquarellfarben.
|
||||
Mit 120g/qm und 1,2-fachem Volumen fühlt sich jede Seite
|
||||
substanziell an, ohne starr zu wirken.`,
|
||||
image: "/images/features/hardcover/paper-texture.jpg",
|
||||
imageRight: true,
|
||||
},
|
||||
{
|
||||
headline: "160 Seiten für Deine Ideen",
|
||||
subtitle: "Raum für 3-6 Monate intensive Arbeit",
|
||||
body: `160 Seiten bedeuten genug Platz für ein abgeschlossenes Projekt,
|
||||
eine komplette Reise, oder ein Quartal Deines Bullet Journals.
|
||||
Blanko-Seiten geben Dir absolute Freiheit: Skizzieren, Schreiben,
|
||||
Collagieren – ohne Raster, das Deine Kreativität einschränkt.`,
|
||||
image: "/images/features/hardcover/lifestyle-desk.jpg",
|
||||
imageRight: false,
|
||||
},
|
||||
{
|
||||
headline: "Kein Heft gleicht dem anderen",
|
||||
subtitle: "13 einzigartige Mustervarianten",
|
||||
body: `Jeder Einband wird individuell mit variierenden geometrischen
|
||||
Mustern bedruckt. Die Kombination aus mattem Schwarz (300g/qm Karton)
|
||||
und den Mustern macht Dein Heft unverwechselbar.`,
|
||||
image: "/images/features/hardcover/cover-pattern.jpg",
|
||||
imageRight: true,
|
||||
},
|
||||
{
|
||||
eyebrow: "Made in Stuttgart",
|
||||
headline: "Transparente Lieferkette",
|
||||
subtitle: "Messbare Nachhaltigkeit",
|
||||
body: `<strong>100% Recyclingpapier</strong> (FSC® C018175)<br>
|
||||
<strong>CO₂-neutral</strong> produziert<br>
|
||||
<strong>Blauer Engel + EU Ecolabel</strong> zertifiziert<br>
|
||||
<strong>Hergestellt in Stuttgart</strong>, Deutschland<br><br>
|
||||
Keine langen Transportwege, keine Ausbeutung, kein Greenwashing.
|
||||
Nur ehrliches Handwerk mit messbaren Umweltstandards.`,
|
||||
image: "/images/production/04.png",
|
||||
imageRight: false,
|
||||
},
|
||||
],
|
||||
|
||||
Softcover: [
|
||||
{
|
||||
eyebrow: "Leicht & Flexibel",
|
||||
headline: "Der tägliche Begleiter",
|
||||
subtitle: "Passt in jede Tasche",
|
||||
body: `Das Softcover ist Dein unkomplizierter Alltagsbegleiter.
|
||||
Leicht, flexibel und robust – perfekt für unterwegs.
|
||||
Die weiche Hülle schmiegt sich an und übersteht jeden Rucksack.`,
|
||||
image: "/images/features/softcover/flexible-cover.jpg",
|
||||
imageRight: false,
|
||||
},
|
||||
{
|
||||
eyebrow: "100% Recycling",
|
||||
headline: "VIVUS 89 Papier",
|
||||
subtitle: "Dieselbe Qualität, leichteres Format",
|
||||
body: `Auch im Softcover kommt unser bewährtes VIVUS 89 Recyclingpapier
|
||||
zum Einsatz. 120g/qm für optimale Schreibqualität ohne Durchschlagen.
|
||||
Perfekt für Füllfederhalter, Fineliner und Aquarellstifte.`,
|
||||
image: "/images/features/softcover/paper-writing.jpg",
|
||||
imageRight: true,
|
||||
},
|
||||
{
|
||||
headline: "Praktische Steppstich-Bindung",
|
||||
subtitle: "Liegt flach, bleibt offen",
|
||||
body: `Die Steppstich-Bindung ermöglicht ein vollständiges Aufklappen.
|
||||
Ideal zum Schreiben, Zeichnen und für alle, die beide Seiten
|
||||
gleichzeitig nutzen möchten.`,
|
||||
image: "/images/features/softcover/binding-detail.jpg",
|
||||
imageRight: false,
|
||||
},
|
||||
{
|
||||
eyebrow: "Made in Stuttgart",
|
||||
headline: "Nachhaltig produziert",
|
||||
subtitle: "Lokale Fertigung, globale Standards",
|
||||
body: `<strong>FSC® zertifiziert</strong><br>
|
||||
<strong>Blauer Engel</strong> Umweltzeichen<br>
|
||||
<strong>CO₂-neutral</strong> hergestellt<br><br>
|
||||
Kurze Wege, faire Produktion, messbare Nachhaltigkeit.`,
|
||||
image: "/images/features/softcover/workshop.png",
|
||||
imageRight: true,
|
||||
},
|
||||
],
|
||||
|
||||
Heft: [
|
||||
{
|
||||
eyebrow: "Kompakt & Praktisch",
|
||||
headline: "Das klassische Notizheft",
|
||||
subtitle: "Für schnelle Notizen und Ideen",
|
||||
body: `Manchmal braucht man kein dickes Notizbuch – sondern ein
|
||||
handliches Heft für den Moment. Perfekt für Meeting-Notizen,
|
||||
Einkaufslisten oder spontane Skizzen.`,
|
||||
image: "/images/features/heft/compact-size.jpg",
|
||||
imageRight: false,
|
||||
},
|
||||
{
|
||||
eyebrow: "100% Recycling",
|
||||
headline: "Qualität im Kleinformat",
|
||||
subtitle: "VIVUS 89 auch im Heft",
|
||||
body: `Unser Recyclingpapier macht auch im kleinen Format keine
|
||||
Kompromisse. Die gleiche Schreibqualität, die Du von unseren
|
||||
größeren Notizbüchern kennst.`,
|
||||
image: "/images/features/heft/paper-quality.jpg",
|
||||
imageRight: true,
|
||||
},
|
||||
{
|
||||
headline: "Ruckzuck-Heftung",
|
||||
subtitle: "Einfach, aber solide",
|
||||
body: `Die klassische Rückstich-Heftung hält Dein Heft zusammen
|
||||
und ermöglicht ein flaches Aufklappen. Bewährt seit Generationen,
|
||||
nachhaltig für die Zukunft.`,
|
||||
image: "/images/features/heft/binding.jpg",
|
||||
imageRight: false,
|
||||
},
|
||||
],
|
||||
|
||||
Spiralbindung: [
|
||||
{
|
||||
eyebrow: "360° Flexibilität",
|
||||
headline: "Wire-O-Bindung",
|
||||
subtitle: "Komplett umklappbar",
|
||||
body: `Die Spiralbindung lässt sich vollständig umklappen –
|
||||
ideal für beengte Arbeitsflächen. Schreibe auf einer Seite,
|
||||
während die andere flach auf dem Tisch liegt.`,
|
||||
image: "/images/features/spiral/360-flip.jpg",
|
||||
imageRight: false,
|
||||
},
|
||||
{
|
||||
eyebrow: "100% Recycling",
|
||||
headline: "VIVUS 89 Papier",
|
||||
subtitle: "Premium-Qualität, flexibles Format",
|
||||
body: `Unser bewährtes Recyclingpapier in der praktischen
|
||||
Spiralbindung. 120g/qm verhindern Durchschlagen –
|
||||
auch bei intensiver Nutzung mit verschiedenen Stiften.`,
|
||||
image: "/images/features/spiral/paper-texture.jpg",
|
||||
imageRight: true,
|
||||
},
|
||||
{
|
||||
headline: "Seiten heraustrennbar",
|
||||
subtitle: "Perforation für sauberes Abtrennen",
|
||||
body: `Jede Seite lässt sich sauber heraustrennen – perfekt,
|
||||
wenn Du Notizen weitergeben oder Skizzen verschenken möchtest.
|
||||
Die Mikroperforation sorgt für glatte Kanten.`,
|
||||
image: "/images/features/spiral/tear-out.jpg",
|
||||
imageRight: false,
|
||||
},
|
||||
{
|
||||
eyebrow: "Made in Stuttgart",
|
||||
headline: "Nachhaltig & Praktisch",
|
||||
subtitle: "Das Beste aus beiden Welten",
|
||||
body: `<strong>Recycling-Drahtbindung</strong><br>
|
||||
<strong>FSC® zertifiziertes Papier</strong><br>
|
||||
<strong>CO₂-neutral</strong> produziert<br><br>
|
||||
Funktionalität trifft Nachhaltigkeit.`,
|
||||
image: "/images/features/spiral/workshop.jpg",
|
||||
imageRight: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// TECHNICAL SPECS - Collapsible details per cover type
|
||||
// =============================================================================
|
||||
|
||||
export const TECHNICAL_SPECS: Record<CoverType, Record<string, string>> = {
|
||||
Hardcover: {
|
||||
Format: "150 × 210 mm (A5)",
|
||||
Seitenanzahl: "160 Seiten (80 Blatt)",
|
||||
Papier: "VIVUS 89, 120g/qm, 100% Recycling",
|
||||
Einband: "300g/qm Recyclingkarton",
|
||||
Bindung: "Klassische Fadenheftung (Singer-Stich)",
|
||||
Vorsatzpapier: "Rot durchgefärbt",
|
||||
Zertifizierung: "FSC® C018175, Blauer Engel, EU Ecolabel",
|
||||
Herstellung: "Stuttgart, Deutschland",
|
||||
},
|
||||
Softcover: {
|
||||
Format: "150 × 210 mm (A5)",
|
||||
Seitenanzahl: "96 Seiten (48 Blatt)",
|
||||
Papier: "VIVUS 89, 120g/qm, 100% Recycling",
|
||||
Einband: "250g/qm Recyclingkarton, flexibel",
|
||||
Bindung: "Steppstich-Heftung",
|
||||
Zertifizierung: "FSC® C018175, Blauer Engel, EU Ecolabel",
|
||||
Herstellung: "Stuttgart, Deutschland",
|
||||
},
|
||||
Heft: {
|
||||
Format: "148 × 210 mm (A5)",
|
||||
Seitenanzahl: "20, 40 oder 60 Seiten",
|
||||
Papier: "VIVUS 89, 120g/qm, 100% Recycling",
|
||||
Einband: "200g/qm Recyclingkarton",
|
||||
Bindung: "Rückstich-Heftung",
|
||||
Zertifizierung: "FSC® C018175, Blauer Engel",
|
||||
Herstellung: "Stuttgart, Deutschland",
|
||||
},
|
||||
Spiralbindung: {
|
||||
Format: "150 × 210 mm (A5)",
|
||||
Seitenanzahl: "120 Seiten (60 Blatt)",
|
||||
Papier: "VIVUS 89, 120g/qm, 100% Recycling",
|
||||
Einband: "300g/qm Recyclingkarton",
|
||||
Bindung: "Wire-O-Bindung (Doppeldraht)",
|
||||
Perforation: "Mikroperforation zum Heraustrennen",
|
||||
Zertifizierung: "FSC® C018175, Blauer Engel, EU Ecolabel",
|
||||
Herstellung: "Stuttgart, Deutschland",
|
||||
},
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// USE CASES - Same for all products
|
||||
// =============================================================================
|
||||
|
||||
export const USE_CASES: UseCase[] = [
|
||||
{
|
||||
icon: "pen",
|
||||
title: "Schreibprojekte",
|
||||
description: "Romane, Tagebuch, Morning Pages",
|
||||
},
|
||||
{
|
||||
icon: "palette",
|
||||
title: "Mixed-Media",
|
||||
description: "Aquarell, Collage, Skizzen",
|
||||
},
|
||||
{
|
||||
icon: "clipboard",
|
||||
title: "Sketchbook",
|
||||
description: "Layouts ohne Raster",
|
||||
},
|
||||
{
|
||||
icon: "briefcase",
|
||||
title: "Arbeit & Studium",
|
||||
description: "Meetings, Forschung, Konzepte",
|
||||
},
|
||||
];
|
||||
|
||||
// =============================================================================
|
||||
// HELPER: Get cover type from product.cover.name
|
||||
// =============================================================================
|
||||
|
||||
export function getCoverType(coverName?: string): CoverType {
|
||||
if (!coverName) return "Hardcover";
|
||||
const name = coverName.toLowerCase();
|
||||
if (name.includes("heft")) return "Heft";
|
||||
if (name.includes("hardcover")) return "Hardcover";
|
||||
if (name.includes("softcover")) return "Softcover";
|
||||
if (name.includes("spiral") || name.includes("wire")) return "Spiralbindung";
|
||||
return "Hardcover"; // fallback
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COMPOSABLE
|
||||
// =============================================================================
|
||||
|
||||
import type { Product } from "~/types";
|
||||
|
||||
export function useProductContent(
|
||||
coverName: Ref<string | undefined> | ComputedRef<string | undefined>,
|
||||
product?: Ref<Product | null> | ComputedRef<Product | null>
|
||||
) {
|
||||
const coverType = computed(() => getCoverType(coverName.value));
|
||||
const featureModules = computed(() => FEATURE_MODULES[coverType.value] ?? []);
|
||||
|
||||
// Derive technical specs from product data when available
|
||||
const technicalSpecs = computed(() => {
|
||||
const baseSpecs = TECHNICAL_SPECS[coverType.value] ?? {};
|
||||
const prod = product?.value;
|
||||
|
||||
if (!prod) return baseSpecs;
|
||||
|
||||
const derivedSpecs: Record<string, string> = {};
|
||||
|
||||
// Format from copyText
|
||||
if (prod.cover?.copyText?.format) {
|
||||
derivedSpecs["Format"] = prod.cover.copyText.format;
|
||||
}
|
||||
|
||||
// Seitenanzahl from pages
|
||||
if (prod.pages?.name) {
|
||||
derivedSpecs["Seitenanzahl"] = prod.pages.name;
|
||||
} else if (prod.pages?.count) {
|
||||
const sheets = Math.floor(prod.pages.count / 2);
|
||||
derivedSpecs["Seitenanzahl"] = `${prod.pages.count} Seiten (${sheets} Blatt)`;
|
||||
}
|
||||
|
||||
// Parse paper string: "VIVUS 89: 100% Recyclingpapier, ... Inhalt: 120g/qm, Einband: 300g/qm. Zertifizierung: ..."
|
||||
if (prod.cover?.copyText?.paper) {
|
||||
const paperText = prod.cover.copyText.paper;
|
||||
|
||||
// Extract paper name and description (before "Inhalt:")
|
||||
const inhaltMatch = paperText.match(/^(.+?)(?:\s*Inhalt:|$)/);
|
||||
if (inhaltMatch) {
|
||||
derivedSpecs["Papier"] = inhaltMatch[1].trim().replace(/,\s*$/, "");
|
||||
}
|
||||
|
||||
// Extract Inhalt (paper weight)
|
||||
const inhaltWeightMatch = paperText.match(/Inhalt:\s*([^,]+)/);
|
||||
if (inhaltWeightMatch) {
|
||||
derivedSpecs["Papiergewicht"] = inhaltWeightMatch[1].trim();
|
||||
}
|
||||
|
||||
// Extract Einband (cover weight)
|
||||
const einbandMatch = paperText.match(/Einband:\s*([^.]+)/);
|
||||
if (einbandMatch) {
|
||||
derivedSpecs["Einband"] = einbandMatch[1].trim();
|
||||
}
|
||||
|
||||
// Extract Zertifizierung
|
||||
const zertMatch = paperText.match(/Zertifizierung:\s*(.+)$/);
|
||||
if (zertMatch) {
|
||||
derivedSpecs["Zertifizierung"] = zertMatch[1].trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Bindung from cover binding
|
||||
if (prod.cover?.binding) {
|
||||
derivedSpecs["Bindung"] = prod.cover.binding;
|
||||
}
|
||||
|
||||
// Merge: product data overrides base specs
|
||||
return { ...baseSpecs, ...derivedSpecs };
|
||||
});
|
||||
|
||||
return {
|
||||
coverType,
|
||||
featureModules,
|
||||
technicalSpecs,
|
||||
useCases: USE_CASES,
|
||||
};
|
||||
}
|
||||
147
composables/useShopApi.ts
Normal file
147
composables/useShopApi.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import type {
|
||||
Product,
|
||||
Order,
|
||||
ProductVariantResponse,
|
||||
PatternVariantsResponse,
|
||||
ProductCover,
|
||||
Legal,
|
||||
DeliveryMethod,
|
||||
PaymentMethod,
|
||||
ApiResponse
|
||||
} from "~/types";
|
||||
|
||||
/**
|
||||
* Shop API composable.
|
||||
* All requests go through dedicated Nuxt server API routes.
|
||||
* The API token is handled server-side and never exposed to clients.
|
||||
*/
|
||||
export function useShopApi() {
|
||||
return {
|
||||
// Products
|
||||
async getProducts(coverId?: string, page = 1, pageSize = 24) {
|
||||
const params = new URLSearchParams();
|
||||
if (coverId) params.append("cover", coverId);
|
||||
params.append("page", page.toString());
|
||||
params.append("pageSize", pageSize.toString());
|
||||
|
||||
return await $fetch<ApiResponse<Product>>(`/api/products?${params}`);
|
||||
},
|
||||
|
||||
async getCheapestProducts(coverId?: string, page = 1, pageSize = 24) {
|
||||
const params = new URLSearchParams();
|
||||
if (coverId) params.append("cover", coverId);
|
||||
params.append("page", page.toString());
|
||||
params.append("pageSize", pageSize.toString());
|
||||
|
||||
return await $fetch<ApiResponse<Product>>(`/api/products/promo?${params}`);
|
||||
},
|
||||
|
||||
async getProductById(id: string) {
|
||||
return await $fetch<Product>(`/api/products/${id}`);
|
||||
},
|
||||
|
||||
async getProductBySlug(slug: string) {
|
||||
return await $fetch<Product | null>(`/api/product/${slug}`);
|
||||
},
|
||||
|
||||
async getProductVariants(id: string): Promise<ProductVariantResponse> {
|
||||
return await $fetch<ProductVariantResponse>(`/api/products/${id}/variants`);
|
||||
},
|
||||
|
||||
async getProductVariantsByProductId(id: number): Promise<ProductVariantResponse> {
|
||||
return await $fetch<ProductVariantResponse>(`/api/products/${id}/variants`);
|
||||
},
|
||||
|
||||
async getPatternVariants(id: string): Promise<PatternVariantsResponse> {
|
||||
return await $fetch<PatternVariantsResponse>(`/api/products/${id}/variants/pattern`);
|
||||
},
|
||||
|
||||
async getPatternVariantsByProductId(id: number): Promise<PatternVariantsResponse> {
|
||||
return await $fetch<PatternVariantsResponse>(`/api/products/${id}/variants/pattern`);
|
||||
},
|
||||
|
||||
// Product metadata
|
||||
async getProductRulings() {
|
||||
return await $fetch<ApiResponse<unknown>>(`/api/product-rulings`);
|
||||
},
|
||||
|
||||
async getProductPatterns() {
|
||||
return await $fetch<ApiResponse<unknown>>(`/api/product-patterns`);
|
||||
},
|
||||
|
||||
async getProductPages() {
|
||||
return await $fetch<ApiResponse<unknown>>(`/api/product-pages`);
|
||||
},
|
||||
|
||||
async getProductCovers() {
|
||||
return await $fetch<ApiResponse<ProductCover>>(`/api/product-covers`);
|
||||
},
|
||||
|
||||
async getProductCoverById(id: string) {
|
||||
return await $fetch<ApiResponse<ProductCover>>(`/api/product-covers/${id}`);
|
||||
},
|
||||
|
||||
// Orders
|
||||
async createOrder(): Promise<Order> {
|
||||
return await $fetch<Order>("/api/orders", { method: "POST" });
|
||||
},
|
||||
|
||||
async getOrder(uuid: string): Promise<Order> {
|
||||
return await $fetch<Order>(`/api/orders/${uuid}`);
|
||||
},
|
||||
|
||||
async updateOrder(uuid: string, data: Partial<Order>): Promise<Order> {
|
||||
return await $fetch<Order>(`/api/orders/${uuid}`, {
|
||||
method: "PUT",
|
||||
body: { data }
|
||||
});
|
||||
},
|
||||
|
||||
async addProductToCart(uuid: string, productId: number, count = 1): Promise<Order> {
|
||||
return await $fetch<Order>(`/api/orders/${uuid}/add-product/${productId}?count=${count}`, {
|
||||
method: "PUT"
|
||||
});
|
||||
},
|
||||
|
||||
async removeProductFromCart(uuid: string, productId: number, count = 1): Promise<Order> {
|
||||
return await $fetch<Order>(`/api/orders/${uuid}/remove-product/${productId}?count=${count}`, {
|
||||
method: "PUT"
|
||||
});
|
||||
},
|
||||
|
||||
async checkoutOrder(uuid: string): Promise<{ id: string }> {
|
||||
const returnUrl = import.meta.client ? window.location.href : "";
|
||||
return await $fetch<{ id: string }>(`/api/orders/${uuid}/checkout?returnUrl=${encodeURIComponent(returnUrl)}`, {
|
||||
method: "POST"
|
||||
});
|
||||
},
|
||||
|
||||
async capturePayment(uuid: string, paypalOrderId: string): Promise<Order> {
|
||||
return await $fetch<Order>(`/api/orders/${uuid}/capture/${paypalOrderId}`, {
|
||||
method: "POST"
|
||||
});
|
||||
},
|
||||
|
||||
// Payment & Delivery
|
||||
async getPaymentMethods() {
|
||||
return await $fetch<ApiResponse<PaymentMethod>>(`/api/payments`);
|
||||
},
|
||||
|
||||
async getDeliveryMethods() {
|
||||
return await $fetch<ApiResponse<DeliveryMethod>>(`/api/deliveries`);
|
||||
},
|
||||
|
||||
// Content
|
||||
async getLegal(): Promise<Legal> {
|
||||
return await $fetch<Legal>(`/api/legal`);
|
||||
},
|
||||
|
||||
async getContent(): Promise<unknown> {
|
||||
return await $fetch<unknown>(`/api/content`);
|
||||
},
|
||||
|
||||
async getWebsites(): Promise<unknown> {
|
||||
return await $fetch<unknown>(`/api/websites`);
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user