feat: eigene Bilder pro Charge
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 2m21s
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:
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;
|
||||||
@@ -171,6 +171,11 @@ model Batch {
|
|||||||
bestBeforeDate DateTime
|
bestBeforeDate DateTime
|
||||||
glassCount Int
|
glassCount Int
|
||||||
remainingGlasses 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())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ export async function createBatchAction(formData: FormData): Promise<void> {
|
|||||||
bestBeforeDate: new Date(str(formData, "bestBeforeDate")),
|
bestBeforeDate: new Date(str(formData, "bestBeforeDate")),
|
||||||
glassCount: num(formData, "glassCount"),
|
glassCount: num(formData, "glassCount"),
|
||||||
remainingGlasses: num(formData, "remainingGlasses"),
|
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")),
|
bestBeforeDate: new Date(str(formData, "bestBeforeDate")),
|
||||||
glassCount: num(formData, "glassCount"),
|
glassCount: num(formData, "glassCount"),
|
||||||
remainingGlasses: num(formData, "remainingGlasses"),
|
remainingGlasses: num(formData, "remainingGlasses"),
|
||||||
|
images: strArray(formData, "images").filter(isValidUploadPath),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { asStringArray } from "@/lib/json-array";
|
||||||
import { BackLink } from "@/components/back-link";
|
import { BackLink } from "@/components/back-link";
|
||||||
|
import { AdminImageListField } from "@/components/admin-image-list-field";
|
||||||
import { updateBatchAction } from "../../actions";
|
import { updateBatchAction } from "../../actions";
|
||||||
import { getLocale } from "@/i18n/locale";
|
import { getLocale } from "@/i18n/locale";
|
||||||
import { getDictionary } from "@/i18n/get-dictionary";
|
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]">) {
|
export default async function AdminBatchEditPage(props: PageProps<"/admin/chargen/[id]">) {
|
||||||
const t = getDictionary(await getLocale());
|
const t = getDictionary(await getLocale());
|
||||||
const { id } = await props.params;
|
const { id } = await props.params;
|
||||||
const batch = await prisma.batch.findUnique({ where: { id }, include: { harvest: true } });
|
const rawBatch = await prisma.batch.findUnique({ where: { id }, include: { harvest: true } });
|
||||||
if (!batch) notFound();
|
if (!rawBatch) notFound();
|
||||||
|
const batch = { ...rawBatch, images: asStringArray(rawBatch.images) };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-xl">
|
<div className="max-w-xl">
|
||||||
@@ -64,6 +67,11 @@ export default async function AdminBatchEditPage(props: PageProps<"/admin/charge
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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">
|
<button type="submit" className="bg-forest text-cream px-6 py-2 text-sm">
|
||||||
{t.adminBatchEditPage.save}
|
{t.adminBatchEditPage.save}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -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.stepFilling, value: formatDate(batch.fillDate, locale) },
|
||||||
{ label: t.chargePage.stepBestBefore, value: formatDate(batch.bestBeforeDate, 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 (
|
return (
|
||||||
<div className="py-20">
|
<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">
|
<div className="mt-10 grid sm:grid-cols-2 gap-6">
|
||||||
<ScrollReveal>
|
<ScrollReveal>
|
||||||
<ContentImage
|
<ContentImage
|
||||||
src={batch.harvest.images[0] ?? null}
|
src={displayImages[0] ?? null}
|
||||||
alt={`${batch.harvest.type} ${batch.harvest.year}`}
|
alt={`${batch.harvest.type} ${batch.harvest.year}`}
|
||||||
tone="honey"
|
tone="honey"
|
||||||
className="aspect-square"
|
className="aspect-square"
|
||||||
@@ -67,7 +70,7 @@ export default async function BatchPage(props: PageProps<"/charge/[code]">) {
|
|||||||
</ScrollReveal>
|
</ScrollReveal>
|
||||||
<ScrollReveal delay={0.1}>
|
<ScrollReveal delay={0.1}>
|
||||||
<ContentImage
|
<ContentImage
|
||||||
src={batch.harvest.images[1] ?? null}
|
src={displayImages[1] ?? null}
|
||||||
alt={batch.harvest.location}
|
alt={batch.harvest.location}
|
||||||
tone="forest"
|
tone="forest"
|
||||||
className="aspect-square"
|
className="aspect-square"
|
||||||
|
|||||||
@@ -841,6 +841,7 @@ export const de: Dictionary = {
|
|||||||
bestBeforeDateLabel: "Mindesthaltbarkeitsdatum",
|
bestBeforeDateLabel: "Mindesthaltbarkeitsdatum",
|
||||||
glassCountLabel: "Anzahl Gläser",
|
glassCountLabel: "Anzahl Gläser",
|
||||||
remainingGlassesLabel: "Verbleibender Bestand",
|
remainingGlassesLabel: "Verbleibender Bestand",
|
||||||
|
imagesLabel: "Chargen-Bilder",
|
||||||
save: "Speichern",
|
save: "Speichern",
|
||||||
},
|
},
|
||||||
trackOrderPage: {
|
trackOrderPage: {
|
||||||
|
|||||||
@@ -841,6 +841,7 @@ export const en: Dictionary = {
|
|||||||
bestBeforeDateLabel: "Best-before date",
|
bestBeforeDateLabel: "Best-before date",
|
||||||
glassCountLabel: "Number of jars",
|
glassCountLabel: "Number of jars",
|
||||||
remainingGlassesLabel: "Remaining stock",
|
remainingGlassesLabel: "Remaining stock",
|
||||||
|
imagesLabel: "Batch images",
|
||||||
save: "Save",
|
save: "Save",
|
||||||
},
|
},
|
||||||
trackOrderPage: {
|
trackOrderPage: {
|
||||||
|
|||||||
@@ -543,6 +543,7 @@ export interface Dictionary {
|
|||||||
bestBeforeDateLabel: string;
|
bestBeforeDateLabel: string;
|
||||||
glassCountLabel: string;
|
glassCountLabel: string;
|
||||||
remainingGlassesLabel: string;
|
remainingGlassesLabel: string;
|
||||||
|
imagesLabel: string;
|
||||||
save: string;
|
save: string;
|
||||||
};
|
};
|
||||||
trackOrderPage: {
|
trackOrderPage: {
|
||||||
|
|||||||
+7
-2
@@ -153,7 +153,8 @@ export async function getHarvestByYearType(
|
|||||||
export async function getBatchByCode(
|
export async function getBatchByCode(
|
||||||
code: string
|
code: string
|
||||||
): Promise<
|
): Promise<
|
||||||
| (Batch & {
|
| (Omit<Batch, "images"> & {
|
||||||
|
images: string[];
|
||||||
harvest: HarvestWithImages;
|
harvest: HarvestWithImages;
|
||||||
products: (Omit<Product, "images"> & { images: string[] })[];
|
products: (Omit<Product, "images"> & { images: string[] })[];
|
||||||
})
|
})
|
||||||
@@ -164,5 +165,9 @@ export async function getBatchByCode(
|
|||||||
include: { harvest: true, products: true },
|
include: { harvest: true, products: true },
|
||||||
});
|
});
|
||||||
if (!batch) return null;
|
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),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user