feat: Wabenhain Honig-Shop Anwendung
Vollstaendige Next.js/Prisma-E-Commerce-Anwendung fuer eine Imkerei: Shop, Warenkorb, Stripe-Checkout, Kundenkonto mit 2FA, Admin-Bereich (Produkte, Ernten, Chargen, Bestellungen, Gutscheine, admin-editierbare Website-Inhalte), i18n (DE/EN), Rate-Limiting/CSP/Audit-Log-Haertung, PostgreSQL- und MySQL-Unterstuetzung ueber austauschbare Prisma- Treiber-Adapter, Unit-/Komponenten-/E2E-Tests.
This commit is contained in:
1 parent
5c76fcc266
commit
c7927f2883
240 files changed
+26096
-137
No files matched your search
+132
@@ -0,0 +1,132 @@
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { asStringArray } from "@/lib/json-array";
|
||||
import type { Product, Harvest, Batch, ProductCategory, Prisma } from "@/generated/prisma/client";
|
||||
|
||||
// tasteTags/images sind im Schema als Json modelliert (MySQL-Kompatibilitaet,
|
||||
// siehe schema.prisma) — hier auf das tatsaechliche Laufzeitformat
|
||||
// (String-Array) zurueckgeschmalert, damit der Rest der App wie zuvor damit
|
||||
// arbeiten kann. normalizeProduct() unten stellt sicher, dass das an jeder
|
||||
// Rueckgabestelle in diesem Modul auch wirklich so ankommt.
|
||||
export type ProductWithRelations = Omit<Product, "tasteTags" | "images"> & {
|
||||
tasteTags: string[];
|
||||
images: string[];
|
||||
harvest: Harvest | null;
|
||||
batch: Batch | null;
|
||||
inventory: { quantity: number } | null;
|
||||
};
|
||||
|
||||
function normalizeProduct<T extends { tasteTags: unknown; images: unknown }>(
|
||||
product: T
|
||||
): Omit<T, "tasteTags" | "images"> & { tasteTags: string[]; images: string[] } {
|
||||
return { ...product, tasteTags: asStringArray(product.tasteTags), images: asStringArray(product.images) };
|
||||
}
|
||||
|
||||
export type ProductSort = "preis-auf" | "preis-ab" | "verfuegbarkeit";
|
||||
|
||||
export interface GetActiveProductsOptions {
|
||||
query?: string;
|
||||
category?: ProductCategory;
|
||||
sort?: ProductSort;
|
||||
/** Nur Produkte mit Lagerbestand > 0 anzeigen. */
|
||||
onlyAvailable?: boolean;
|
||||
}
|
||||
|
||||
export async function getActiveProducts(
|
||||
options: GetActiveProductsOptions = {}
|
||||
): Promise<ProductWithRelations[]> {
|
||||
const trimmed = options.query?.trim();
|
||||
|
||||
let orderBy: Prisma.ProductOrderByWithRelationInput = { createdAt: "asc" };
|
||||
if (options.sort === "preis-auf") orderBy = { priceCents: "asc" };
|
||||
else if (options.sort === "preis-ab") orderBy = { priceCents: "desc" };
|
||||
else if (options.sort === "verfuegbarkeit") orderBy = { inventory: { quantity: "desc" } };
|
||||
|
||||
const products = (
|
||||
await prisma.product.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
...(options.category ? { category: options.category } : {}),
|
||||
...(options.onlyAvailable ? { inventory: { quantity: { gt: 0 } } } : {}),
|
||||
},
|
||||
include: { harvest: true, batch: true, inventory: { select: { quantity: true } } },
|
||||
orderBy,
|
||||
})
|
||||
).map(normalizeProduct);
|
||||
|
||||
if (!trimmed) return products;
|
||||
|
||||
// Teilstring-Suche auf einzelnen tasteTags-Elementen unterstuetzt Prisma
|
||||
// fuer Postgres-Arrays nicht (nur exakte Gleichheit via "has") — bei der
|
||||
// ueberschaubaren Katalogsgroesse wird deshalb hier statt in der DB-Query
|
||||
// gefiltert. Das behebt zugleich, dass mehrwortige, gemischt geschriebene
|
||||
// Tags wie "reines Bienenwachs" bei case-insensitiver Suche nie trafen.
|
||||
const words = trimmed.toLowerCase().split(/\s+/).filter(Boolean);
|
||||
return products.filter((product) => {
|
||||
const haystack = [product.name, product.description, ...product.tasteTags]
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
return words.every((word) => haystack.includes(word));
|
||||
});
|
||||
}
|
||||
|
||||
/** Bis zu `limit` weitere aktive Produkte derselben Kategorie fuer "Das koennte dir auch gefallen". */
|
||||
export async function getRelatedProducts(
|
||||
productId: string,
|
||||
category: ProductCategory,
|
||||
limit = 3
|
||||
): Promise<ProductWithRelations[]> {
|
||||
const products = await prisma.product.findMany({
|
||||
where: { isActive: true, category, id: { not: productId } },
|
||||
include: { harvest: true, batch: true, inventory: { select: { quantity: true } } },
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: limit,
|
||||
});
|
||||
return products.map(normalizeProduct);
|
||||
}
|
||||
|
||||
export async function getProductBySlug(
|
||||
slug: string
|
||||
): Promise<ProductWithRelations | null> {
|
||||
const product = await prisma.product.findUnique({
|
||||
where: { slug },
|
||||
include: { harvest: true, batch: true, inventory: { select: { quantity: true } } },
|
||||
});
|
||||
return product ? normalizeProduct(product) : null;
|
||||
}
|
||||
|
||||
export async function getProductsByIds(
|
||||
ids: string[]
|
||||
): Promise<ProductWithRelations[]> {
|
||||
if (ids.length === 0) return [];
|
||||
const products = await prisma.product.findMany({
|
||||
where: { id: { in: ids } },
|
||||
include: { harvest: true, batch: true, inventory: { select: { quantity: true } } },
|
||||
});
|
||||
return products.map(normalizeProduct);
|
||||
}
|
||||
|
||||
export async function getHarvests(): Promise<(Harvest & { batches: Batch[] })[]> {
|
||||
return prisma.harvest.findMany({
|
||||
include: { batches: true },
|
||||
orderBy: [{ year: "desc" }, { harvestDate: "desc" }],
|
||||
});
|
||||
}
|
||||
|
||||
export async function getHarvestByYearType(
|
||||
year: number,
|
||||
type: string
|
||||
): Promise<(Harvest & { batches: Batch[]; products: Product[] }) | null> {
|
||||
return prisma.harvest.findUnique({
|
||||
where: { year_type: { year, type } },
|
||||
include: { batches: true, products: true },
|
||||
});
|
||||
}
|
||||
|
||||
export async function getBatchByCode(
|
||||
code: string
|
||||
): Promise<(Batch & { harvest: Harvest; products: Product[] }) | null> {
|
||||
return prisma.batch.findUnique({
|
||||
where: { code },
|
||||
include: { harvest: true, products: true },
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user