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:
172
server/api/__sitemap__/urls.ts
Normal file
172
server/api/__sitemap__/urls.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { fetchCms } from "~/server/utils/cmsApi";
|
||||
|
||||
function slugify(text: string): string {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/ü/g, "ue")
|
||||
.replace(/ä/g, "ae")
|
||||
.replace(/ö/g, "oe")
|
||||
.replace(/ß/g, "ss");
|
||||
}
|
||||
|
||||
interface ProductData {
|
||||
id: number;
|
||||
slug?: string;
|
||||
updatedAt?: string;
|
||||
cover?: { id: number } | { data: { id: number } };
|
||||
attributes?: {
|
||||
slug: string;
|
||||
updatedAt: string;
|
||||
cover?: { data: { id: number } };
|
||||
};
|
||||
}
|
||||
|
||||
interface CoverData {
|
||||
id: number;
|
||||
name?: string;
|
||||
sort?: number;
|
||||
updatedAt?: string;
|
||||
attributes?: {
|
||||
name: string;
|
||||
sort?: number;
|
||||
updatedAt: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
data: T[];
|
||||
meta?: {
|
||||
pagination?: {
|
||||
total: number;
|
||||
pageSize: number;
|
||||
pageCount: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default defineSitemapEventHandler(async (event) => {
|
||||
const config = useRuntimeConfig(event);
|
||||
const siteUrl = config.siteUrl || "https://muellerprints-paperwork.com";
|
||||
const urls: { loc: string; lastmod?: string; priority?: number }[] = [];
|
||||
const allPageSize = 20; // must match notebooks/index.vue pageSize
|
||||
const coverPageSize = 24; // must match notebooks/[cover].vue pageSize
|
||||
|
||||
try {
|
||||
// Fetch all products for detail page URLs
|
||||
const products = await fetchCms<ApiResponse<ProductData>>("/products", {
|
||||
query: {
|
||||
"pagination[pageSize]": "1000",
|
||||
"fields[0]": "slug",
|
||||
"fields[1]": "updatedAt",
|
||||
"populate[cover][fields][0]": "id"
|
||||
}
|
||||
});
|
||||
|
||||
// Product detail pages
|
||||
for (const product of products.data || []) {
|
||||
const slug = product.slug || product.attributes?.slug;
|
||||
const updatedAt = product.updatedAt || product.attributes?.updatedAt;
|
||||
|
||||
if (slug) {
|
||||
urls.push({
|
||||
loc: `${siteUrl}/details/${slug}`,
|
||||
lastmod: updatedAt,
|
||||
priority: 0.8
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch promo products count for pagination (matches what the page actually shows)
|
||||
const promoProducts = await fetchCms<ApiResponse<ProductData>>("/promo-products", {
|
||||
query: {
|
||||
"pagination[pageSize]": "1",
|
||||
"pagination[page]": "1"
|
||||
}
|
||||
});
|
||||
const totalPromoProducts = promoProducts.meta?.pagination?.total || 0;
|
||||
const totalNotebooksPages = Math.ceil(totalPromoProducts / allPageSize);
|
||||
|
||||
// Add /notebooks pagination pages
|
||||
for (let page = 2; page <= totalNotebooksPages; page++) {
|
||||
urls.push({
|
||||
loc: `${siteUrl}/notebooks?page=${page}`,
|
||||
priority: 0.7
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch all covers
|
||||
const covers = await fetchCms<ApiResponse<CoverData>>("/product-covers", {
|
||||
query: {
|
||||
"fields[0]": "name",
|
||||
"fields[1]": "sort",
|
||||
"fields[2]": "updatedAt"
|
||||
}
|
||||
});
|
||||
|
||||
// Sort covers by sort field
|
||||
const sortedCovers = (covers.data || []).sort((a, b) => {
|
||||
const sortA = a.sort ?? a.attributes?.sort ?? 0;
|
||||
const sortB = b.sort ?? b.attributes?.sort ?? 0;
|
||||
return sortA - sortB;
|
||||
});
|
||||
|
||||
// Cover category pages with slugs and pagination
|
||||
for (const cover of sortedCovers) {
|
||||
const name = cover.name || cover.attributes?.name;
|
||||
const updatedAt = cover.updatedAt || cover.attributes?.updatedAt;
|
||||
|
||||
if (name) {
|
||||
const slug = slugify(name);
|
||||
|
||||
// Main category page
|
||||
urls.push({
|
||||
loc: `${siteUrl}/notebooks/${slug}`,
|
||||
lastmod: updatedAt,
|
||||
priority: 0.7
|
||||
});
|
||||
|
||||
// Fetch promo product count for this cover (matches page display)
|
||||
const coverId = cover.id;
|
||||
const coverPromo = await fetchCms<ApiResponse<ProductData>>("/promo-products", {
|
||||
query: {
|
||||
"filters[cover]": String(coverId),
|
||||
"pagination[pageSize]": "1",
|
||||
"pagination[page]": "1"
|
||||
}
|
||||
});
|
||||
const coverTotal = coverPromo.meta?.pagination?.total || 0;
|
||||
const coverPageCount = Math.ceil(coverTotal / coverPageSize);
|
||||
|
||||
// Add pagination pages for this category
|
||||
for (let page = 2; page <= coverPageCount; page++) {
|
||||
urls.push({
|
||||
loc: `${siteUrl}/notebooks/${slug}?page=${page}`,
|
||||
priority: 0.6
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching sitemap data:", error);
|
||||
}
|
||||
|
||||
// Static pages
|
||||
urls.push(
|
||||
{ loc: `${siteUrl}/`, priority: 1.0 },
|
||||
{ loc: `${siteUrl}/notebooks`, priority: 0.9 },
|
||||
// Info pages
|
||||
{ loc: `${siteUrl}/about`, priority: 0.6 },
|
||||
{ loc: `${siteUrl}/kontakt`, priority: 0.5 },
|
||||
{ loc: `${siteUrl}/anfahrt`, priority: 0.4 },
|
||||
{ loc: `${siteUrl}/oeffnungszeiten`, priority: 0.4 },
|
||||
// Legal pages
|
||||
{ loc: `${siteUrl}/impressum`, priority: 0.2 },
|
||||
{ loc: `${siteUrl}/datenschutz`, priority: 0.2 },
|
||||
{ loc: `${siteUrl}/agb`, priority: 0.2 },
|
||||
{ loc: `${siteUrl}/versand`, priority: 0.3 },
|
||||
{ loc: `${siteUrl}/zahlung`, priority: 0.3 }
|
||||
);
|
||||
|
||||
return urls;
|
||||
});
|
||||
55
server/api/contact.post.ts
Normal file
55
server/api/contact.post.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
|
||||
const { name, email, subject, message } = body;
|
||||
|
||||
if (!name || !email || !message) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: "Name, E-Mail und Nachricht sind erforderlich",
|
||||
});
|
||||
}
|
||||
|
||||
// Validate email format
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: "Ungültige E-Mail-Adresse",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Send email via Mail service
|
||||
const config = useRuntimeConfig();
|
||||
const mailApiUrl = config.mailApiUrl || "http://mail:2222";
|
||||
|
||||
await $fetch(`${mailApiUrl}/send`, {
|
||||
method: "POST",
|
||||
body: {
|
||||
to: "paperwork@muellerprints.de",
|
||||
subject: `Kontaktanfrage: ${subject || "Anfrage über Website"}`,
|
||||
body: `
|
||||
Name: ${name}
|
||||
E-Mail: ${email}
|
||||
Betreff: ${subject || "Kontaktanfrage über Website"}
|
||||
|
||||
Nachricht:
|
||||
${message}
|
||||
|
||||
---
|
||||
Diese Nachricht wurde über das Kontaktformular auf muellerprints.de gesendet.
|
||||
`.trim(),
|
||||
replyTo: email,
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Failed to send contact email:", error);
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: "E-Mail konnte nicht gesendet werden",
|
||||
});
|
||||
}
|
||||
});
|
||||
5
server/api/content.get.ts
Normal file
5
server/api/content.get.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { fetchCms } from "~/server/utils/cmsApi";
|
||||
|
||||
export default defineEventHandler(async () => {
|
||||
return await fetchCms(`/content`);
|
||||
});
|
||||
5
server/api/deliveries.get.ts
Normal file
5
server/api/deliveries.get.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { fetchCms } from "~/server/utils/cmsApi";
|
||||
|
||||
export default defineEventHandler(async () => {
|
||||
return await fetchCms(`/deliveries`);
|
||||
});
|
||||
26
server/api/health.get.ts
Normal file
26
server/api/health.get.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
const config = useRuntimeConfig();
|
||||
const healthStatus: Record<string, unknown> = { status: "ok" };
|
||||
|
||||
// Check CMS connectivity (CMS returns 204 No Content for /_health)
|
||||
try {
|
||||
const cmsUrl = config.cmsInternalUrl || "http://cms:5555";
|
||||
await $fetch(`${cmsUrl}/_health`, {
|
||||
timeout: 5000,
|
||||
ignoreResponseError: false
|
||||
});
|
||||
// If we get here without error, CMS is responding (even with 204)
|
||||
healthStatus.cms_responding = true;
|
||||
} catch (error) {
|
||||
healthStatus.cms_responding = false;
|
||||
healthStatus.status = "degraded";
|
||||
}
|
||||
|
||||
// Check if API token is configured
|
||||
healthStatus.api_token_configured = !!config.shopApiToken;
|
||||
|
||||
const statusCode = healthStatus.status === "ok" ? 200 : 503;
|
||||
setResponseStatus(event, statusCode);
|
||||
|
||||
return healthStatus;
|
||||
});
|
||||
5
server/api/legal.get.ts
Normal file
5
server/api/legal.get.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { fetchCms } from "~/server/utils/cmsApi";
|
||||
|
||||
export default defineEventHandler(async () => {
|
||||
return await fetchCms(`/legal`);
|
||||
});
|
||||
10
server/api/orders/[uuid].get.ts
Normal file
10
server/api/orders/[uuid].get.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { fetchCms } from "~/server/utils/cmsApi";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const uuid = getRouterParam(event, "uuid");
|
||||
if (!uuid) {
|
||||
throw createError({ statusCode: 400, statusMessage: "Missing order UUID" });
|
||||
}
|
||||
|
||||
return await fetchCms(`/orders/${uuid}/cart`);
|
||||
});
|
||||
15
server/api/orders/[uuid].put.ts
Normal file
15
server/api/orders/[uuid].put.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { fetchCms } from "~/server/utils/cmsApi";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const uuid = getRouterParam(event, "uuid");
|
||||
if (!uuid) {
|
||||
throw createError({ statusCode: 400, statusMessage: "Missing order UUID" });
|
||||
}
|
||||
|
||||
const body = await readBody(event);
|
||||
|
||||
return await fetchCms(`/orders/${uuid}/cart`, {
|
||||
method: "PUT",
|
||||
body
|
||||
});
|
||||
});
|
||||
20
server/api/orders/[uuid]/add-product/[productId].put.ts
Normal file
20
server/api/orders/[uuid]/add-product/[productId].put.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { fetchCms } from "~/server/utils/cmsApi";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const uuid = getRouterParam(event, "uuid");
|
||||
const productId = getRouterParam(event, "productId");
|
||||
const query = getQuery(event);
|
||||
|
||||
if (!uuid) {
|
||||
throw createError({ statusCode: 400, statusMessage: "Missing order UUID" });
|
||||
}
|
||||
if (!productId) {
|
||||
throw createError({ statusCode: 400, statusMessage: "Missing product ID" });
|
||||
}
|
||||
|
||||
const count = query.count || 1;
|
||||
|
||||
return await fetchCms(`/orders/${uuid}/add-product/${productId}?count=${count}`, {
|
||||
method: "PUT"
|
||||
});
|
||||
});
|
||||
17
server/api/orders/[uuid]/capture/[paypalOrderId].post.ts
Normal file
17
server/api/orders/[uuid]/capture/[paypalOrderId].post.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { fetchCms } from "~/server/utils/cmsApi";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const uuid = getRouterParam(event, "uuid");
|
||||
const paypalOrderId = getRouterParam(event, "paypalOrderId");
|
||||
|
||||
if (!uuid) {
|
||||
throw createError({ statusCode: 400, statusMessage: "Missing order UUID" });
|
||||
}
|
||||
if (!paypalOrderId) {
|
||||
throw createError({ statusCode: 400, statusMessage: "Missing PayPal order ID" });
|
||||
}
|
||||
|
||||
return await fetchCms(`/orders/${uuid}/capture/${paypalOrderId}`, {
|
||||
method: "POST"
|
||||
});
|
||||
});
|
||||
16
server/api/orders/[uuid]/checkout.post.ts
Normal file
16
server/api/orders/[uuid]/checkout.post.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { fetchCms } from "~/server/utils/cmsApi";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const uuid = getRouterParam(event, "uuid");
|
||||
const query = getQuery(event);
|
||||
|
||||
if (!uuid) {
|
||||
throw createError({ statusCode: 400, statusMessage: "Missing order UUID" });
|
||||
}
|
||||
|
||||
const returnUrl = query.returnUrl || "";
|
||||
|
||||
return await fetchCms(`/orders/${uuid}/checkout?returnUrl=${encodeURIComponent(String(returnUrl))}`, {
|
||||
method: "POST"
|
||||
});
|
||||
});
|
||||
20
server/api/orders/[uuid]/remove-product/[productId].put.ts
Normal file
20
server/api/orders/[uuid]/remove-product/[productId].put.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { fetchCms } from "~/server/utils/cmsApi";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const uuid = getRouterParam(event, "uuid");
|
||||
const productId = getRouterParam(event, "productId");
|
||||
const query = getQuery(event);
|
||||
|
||||
if (!uuid) {
|
||||
throw createError({ statusCode: 400, statusMessage: "Missing order UUID" });
|
||||
}
|
||||
if (!productId) {
|
||||
throw createError({ statusCode: 400, statusMessage: "Missing product ID" });
|
||||
}
|
||||
|
||||
const count = query.count || 1;
|
||||
|
||||
return await fetchCms(`/orders/${uuid}/remove-product/${productId}?count=${count}`, {
|
||||
method: "PUT"
|
||||
});
|
||||
});
|
||||
5
server/api/orders/index.post.ts
Normal file
5
server/api/orders/index.post.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { fetchCms } from "~/server/utils/cmsApi";
|
||||
|
||||
export default defineEventHandler(async () => {
|
||||
return await fetchCms(`/orders/`, { method: "POST" });
|
||||
});
|
||||
5
server/api/payments.get.ts
Normal file
5
server/api/payments.get.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { fetchCms } from "~/server/utils/cmsApi";
|
||||
|
||||
export default defineEventHandler(async () => {
|
||||
return await fetchCms(`/payments`);
|
||||
});
|
||||
18
server/api/product-covers/[id].get.ts
Normal file
18
server/api/product-covers/[id].get.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { fetchCms } from "~/server/utils/cmsApi";
|
||||
import { resolveCoverId } from "~/server/utils/coverSlug";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const idOrSlug = getRouterParam(event, "id");
|
||||
if (!idOrSlug) {
|
||||
throw createError({ statusCode: 400, statusMessage: "Missing cover ID" });
|
||||
}
|
||||
|
||||
// Resolve slug to ID
|
||||
const coverId = await resolveCoverId(idOrSlug);
|
||||
|
||||
if (!coverId) {
|
||||
throw createError({ statusCode: 404, statusMessage: "Cover not found" });
|
||||
}
|
||||
|
||||
return await fetchCms(`/product-covers/${coverId}?populate[copyText]=true`);
|
||||
});
|
||||
5
server/api/product-covers/index.get.ts
Normal file
5
server/api/product-covers/index.get.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { fetchCms } from "~/server/utils/cmsApi";
|
||||
|
||||
export default defineEventHandler(async () => {
|
||||
return await fetchCms(`/product-covers?populate[copyText]=true`);
|
||||
});
|
||||
5
server/api/product-pages.get.ts
Normal file
5
server/api/product-pages.get.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { fetchCms } from "~/server/utils/cmsApi";
|
||||
|
||||
export default defineEventHandler(async () => {
|
||||
return await fetchCms(`/product-pages`);
|
||||
});
|
||||
5
server/api/product-patterns.get.ts
Normal file
5
server/api/product-patterns.get.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { fetchCms } from "~/server/utils/cmsApi";
|
||||
|
||||
export default defineEventHandler(async () => {
|
||||
return await fetchCms(`/product-patterns`);
|
||||
});
|
||||
5
server/api/product-rulings.get.ts
Normal file
5
server/api/product-rulings.get.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { fetchCms } from "~/server/utils/cmsApi";
|
||||
|
||||
export default defineEventHandler(async () => {
|
||||
return await fetchCms(`/product-ruling`);
|
||||
});
|
||||
15
server/api/product/[slug].get.ts
Normal file
15
server/api/product/[slug].get.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { fetchCms } from "~/server/utils/cmsApi";
|
||||
|
||||
/**
|
||||
* Get product by slug - optimized for SSR.
|
||||
* This route fetches directly from CMS and returns the first matching product.
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const slug = getRouterParam(event, "slug");
|
||||
if (!slug) {
|
||||
throw createError({ statusCode: 400, statusMessage: "Missing slug parameter" });
|
||||
}
|
||||
|
||||
const response = await fetchCms<{ data: unknown[] }>(`/products?filters[slug]=${slug}`);
|
||||
return response.data?.[0] ?? null;
|
||||
});
|
||||
10
server/api/products/[id]/variants.get.ts
Normal file
10
server/api/products/[id]/variants.get.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { fetchCms } from "~/server/utils/cmsApi";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const id = getRouterParam(event, "id");
|
||||
if (!id) {
|
||||
throw createError({ statusCode: 400, statusMessage: "Missing product ID" });
|
||||
}
|
||||
|
||||
return await fetchCms(`/products/${id}/variants`);
|
||||
});
|
||||
10
server/api/products/[id]/variants/pattern.get.ts
Normal file
10
server/api/products/[id]/variants/pattern.get.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { fetchCms } from "~/server/utils/cmsApi";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const id = getRouterParam(event, "id");
|
||||
if (!id) {
|
||||
throw createError({ statusCode: 400, statusMessage: "Missing product ID" });
|
||||
}
|
||||
|
||||
return await fetchCms(`/products/${id}/variants/pattern`);
|
||||
});
|
||||
12
server/api/products/index.get.ts
Normal file
12
server/api/products/index.get.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { fetchCms } from "~/server/utils/cmsApi";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const query = getQuery(event);
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (query.cover) params.append("filters[cover]", String(query.cover));
|
||||
params.append("pagination[page]", String(query.page || 1));
|
||||
params.append("pagination[pageSize]", String(query.pageSize || 24));
|
||||
|
||||
return await fetchCms(`/products?${params}`);
|
||||
});
|
||||
21
server/api/products/promo.get.ts
Normal file
21
server/api/products/promo.get.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { fetchCms } from "~/server/utils/cmsApi";
|
||||
import { resolveCoverId } from "~/server/utils/coverSlug";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const query = getQuery(event);
|
||||
|
||||
const params = new URLSearchParams();
|
||||
|
||||
// Resolve cover slug to ID if provided
|
||||
if (query.cover) {
|
||||
const coverId = await resolveCoverId(String(query.cover));
|
||||
if (coverId) {
|
||||
params.append("filters[cover]", coverId);
|
||||
}
|
||||
}
|
||||
|
||||
params.append("pagination[page]", String(query.page || 1));
|
||||
params.append("pagination[pageSize]", String(query.pageSize || 24));
|
||||
|
||||
return await fetchCms(`/promo-products?${params}`);
|
||||
});
|
||||
5
server/api/websites.get.ts
Normal file
5
server/api/websites.get.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { fetchCms } from "~/server/utils/cmsApi";
|
||||
|
||||
export default defineEventHandler(async () => {
|
||||
return await fetchCms(`/website?populate=*`);
|
||||
});
|
||||
Reference in New Issue
Block a user