feat: eigene Bilder pro Charge
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 2m21s

Chargen hatten bisher kein eigenes Bild-Feld (nur Ernten und Produkte) —
neue Migration ergaenzt Batch.images (Json-Array, gleiches Muster wie
Harvest.images/Product.images). Admin kann Chargenfotos jetzt unter
/admin/chargen/[id] hochladen (AdminImageListField, identisch zum
Ernten-Formular). Die oeffentliche Chargen-Seite (/charge/[code]) zeigt
diese Fotos jetzt bevorzugt, faellt ohne eigene Chargenbilder weiterhin
auf die Erntebilder zurueck.

Hinweis fuer den naechsten Live-Deploy: die generierte Migration hat
nebenbei eine bereits laenger bestehende Schema-Abweichung mitkorrigiert
(harvests.images und products.images/tasteTags werden jetzt als NOT NULL
erzwungen, wie im Schema schon laenger deklariert) - "prisma migrate
deploy" schlaegt fehl, falls dort noch echte NULL-Werte stehen sollten.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Sonnet 5 committed 2026-09-12 04:08:39 +02:00
1 parent 6742152626
commit 7d5c172487
9 files changed
+49 -6

No files matched your search

@@ -0,0 +1,17 @@
/*
Warnings:
- Made the column `images` on table `harvests` required. This step will fail if there are existing NULL values in that column.
- Made the column `tasteTags` on table `products` required. This step will fail if there are existing NULL values in that column.
- Made the column `images` on table `products` required. This step will fail if there are existing NULL values in that column.
*/
-- AlterTable
ALTER TABLE "batches" ADD COLUMN "images" JSONB NOT NULL DEFAULT '[]';
-- AlterTable
ALTER TABLE "harvests" ALTER COLUMN "images" SET NOT NULL;
-- AlterTable
ALTER TABLE "products" ALTER COLUMN "tasteTags" SET NOT NULL,
ALTER COLUMN "images" SET NOT NULL;
+5
View File
@@ -171,6 +171,11 @@ model Batch {
bestBeforeDate DateTime
glassCount Int
remainingGlasses Int
/// Liste von Bild-URLs als JSON-Array (String[] ist nicht Provider-uebergreifend:
/// MySQL kennt keine nativen Skalar-Arrays) — siehe lib/json-array.ts fuer den
/// Normalisierungs-Helfer beim Lesen. Eigene Fotos dieser konkreten Charge
/// (z.B. Etikett/Abfuellung), unabhaengig von Harvest.images.
images Json @default("[]")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -147,6 +147,7 @@ export async function createBatchAction(formData: FormData): Promise<void> {
bestBeforeDate: new Date(str(formData, "bestBeforeDate")),
glassCount: num(formData, "glassCount"),
remainingGlasses: num(formData, "remainingGlasses"),
images: strArray(formData, "images").filter(isValidUploadPath),
},
});
@@ -167,6 +168,7 @@ export async function updateBatchAction(formData: FormData): Promise<void> {
bestBeforeDate: new Date(str(formData, "bestBeforeDate")),
glassCount: num(formData, "glassCount"),
remainingGlasses: num(formData, "remainingGlasses"),
images: strArray(formData, "images").filter(isValidUploadPath),
},
});
+10 -2
View File
@@ -1,6 +1,8 @@
import { notFound } from "next/navigation";
import { prisma } from "@/lib/prisma";
import { asStringArray } from "@/lib/json-array";
import { BackLink } from "@/components/back-link";
import { AdminImageListField } from "@/components/admin-image-list-field";
import { updateBatchAction } from "../../actions";
import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary";
@@ -8,8 +10,9 @@ import { getDictionary } from "@/i18n/get-dictionary";
export default async function AdminBatchEditPage(props: PageProps<"/admin/chargen/[id]">) {
const t = getDictionary(await getLocale());
const { id } = await props.params;
const batch = await prisma.batch.findUnique({ where: { id }, include: { harvest: true } });
if (!batch) notFound();
const rawBatch = await prisma.batch.findUnique({ where: { id }, include: { harvest: true } });
if (!rawBatch) notFound();
const batch = { ...rawBatch, images: asStringArray(rawBatch.images) };
return (
<div className="max-w-xl">
@@ -64,6 +67,11 @@ export default async function AdminBatchEditPage(props: PageProps<"/admin/charge
/>
</label>
</div>
<AdminImageListField
name="images"
label={t.adminBatchEditPage.imagesLabel}
initialValues={batch.images}
/>
<button type="submit" className="bg-forest text-cream px-6 py-2 text-sm">
{t.adminBatchEditPage.save}
</button>
+5 -2
View File
@@ -39,6 +39,9 @@ export default async function BatchPage(props: PageProps<"/charge/[code]">) {
{ label: t.chargePage.stepFilling, value: formatDate(batch.fillDate, locale) },
{ label: t.chargePage.stepBestBefore, value: formatDate(batch.bestBeforeDate, locale) },
];
// Eigene Chargenfotos (z.B. Etikett/Abfuellung dieser konkreten Charge)
// gehen vor, falls gepflegt — sonst wie bisher die Erntebilder.
const displayImages = batch.images.length > 0 ? batch.images : batch.harvest.images;
return (
<div className="py-20">
@@ -57,7 +60,7 @@ export default async function BatchPage(props: PageProps<"/charge/[code]">) {
<div className="mt-10 grid sm:grid-cols-2 gap-6">
<ScrollReveal>
<ContentImage
src={batch.harvest.images[0] ?? null}
src={displayImages[0] ?? null}
alt={`${batch.harvest.type} ${batch.harvest.year}`}
tone="honey"
className="aspect-square"
@@ -67,7 +70,7 @@ export default async function BatchPage(props: PageProps<"/charge/[code]">) {
</ScrollReveal>
<ScrollReveal delay={0.1}>
<ContentImage
src={batch.harvest.images[1] ?? null}
src={displayImages[1] ?? null}
alt={batch.harvest.location}
tone="forest"
className="aspect-square"
+1
View File
@@ -841,6 +841,7 @@ export const de: Dictionary = {
bestBeforeDateLabel: "Mindesthaltbarkeitsdatum",
glassCountLabel: "Anzahl Gläser",
remainingGlassesLabel: "Verbleibender Bestand",
imagesLabel: "Chargen-Bilder",
save: "Speichern",
},
trackOrderPage: {
+1
View File
@@ -841,6 +841,7 @@ export const en: Dictionary = {
bestBeforeDateLabel: "Best-before date",
glassCountLabel: "Number of jars",
remainingGlassesLabel: "Remaining stock",
imagesLabel: "Batch images",
save: "Save",
},
trackOrderPage: {
+1
View File
@@ -543,6 +543,7 @@ export interface Dictionary {
bestBeforeDateLabel: string;
glassCountLabel: string;
remainingGlassesLabel: string;
imagesLabel: string;
save: string;
};
trackOrderPage: {
+7 -2
View File
@@ -153,7 +153,8 @@ export async function getHarvestByYearType(
export async function getBatchByCode(
code: string
): Promise<
| (Batch & {
| (Omit<Batch, "images"> & {
images: string[];
harvest: HarvestWithImages;
products: (Omit<Product, "images"> & { images: string[] })[];
})
@@ -164,5 +165,9 @@ export async function getBatchByCode(
include: { harvest: true, products: true },
});
if (!batch) return null;
return { ...batch, harvest: withImages(batch.harvest), products: batch.products.map(withImages) };
return {
...withImages(batch),
harvest: withImages(batch.harvest),
products: batch.products.map(withImages),
};
}