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
|
||||
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),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -841,6 +841,7 @@ export const de: Dictionary = {
|
||||
bestBeforeDateLabel: "Mindesthaltbarkeitsdatum",
|
||||
glassCountLabel: "Anzahl Gläser",
|
||||
remainingGlassesLabel: "Verbleibender Bestand",
|
||||
imagesLabel: "Chargen-Bilder",
|
||||
save: "Speichern",
|
||||
},
|
||||
trackOrderPage: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -543,6 +543,7 @@ export interface Dictionary {
|
||||
bestBeforeDateLabel: string;
|
||||
glassCountLabel: string;
|
||||
remainingGlassesLabel: string;
|
||||
imagesLabel: string;
|
||||
save: string;
|
||||
};
|
||||
trackOrderPage: {
|
||||
|
||||
+7
-2
@@ -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),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user