From 211bd464d293965df4472b013261516228555740 Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 22 May 2026 08:54:03 +0200 Subject: [PATCH] feat: picture modal hero panel with image preview Adds a split-layout hero section at the top of the RecordModal for pictures: left side shows the image preview (~40%), right side shows status, design (both editable) and the linked words relation manager inline. Remaining fields (blurhash, picture_link, metadata) continue to appear in the sections below. Co-Authored-By: Claude Sonnet 4.6 --- src/components/RecordModal.jsx | 86 ++++++++++++++++++++++++++++++++-- src/lib/tables.js | 5 ++ 2 files changed, 86 insertions(+), 5 deletions(-) diff --git a/src/components/RecordModal.jsx b/src/components/RecordModal.jsx index 1e295d2..e688000 100644 --- a/src/components/RecordModal.jsx +++ b/src/components/RecordModal.jsx @@ -234,6 +234,64 @@ function RelationManager({ recordId, rel }) { ); } +function HeroPanel({ record, meta, values, dirty, handleChange }) { + const { heroPanel, editableFields, fetchRelated } = meta; + const { imageField, quickFields, quickRelatedKey } = heroPanel; + const imageUrl = values[imageField]; + const quickRel = fetchRelated?.find(r => r.key === quickRelatedKey); + + return ( +
+ {/* Image side — ~40% */} +
+ {imageUrl ? ( + Vorschau + ) : ( + Kein Bild + )} +
+ + {/* Info side — ~60% */} +
+ {quickFields.map(key => { + const fieldDef = editableFields?.[key]; + return ( +
+ +
+ {fieldDef ? ( + handleChange(key, val)} + /> + ) : ( + + )} +
+ {dirty[key] && ( + geändert + )} +
+ ); + })} + + {quickRel && ( +
+ +
+ )} +
+
+ ); +} + export default function RecordModal({ record, meta, onClose, onSaved }) { const [values, setValues] = useState({}); const [dirty, setDirty] = useState({}); @@ -273,11 +331,18 @@ export default function RecordModal({ record, meta, onClose, onSaved }) { if (!record) return null; + // Fields that are already shown in the hero panel — exclude from regular sections + const heroExclude = new Set([ + ...(meta.heroPanel?.quickFields || []), + ...(meta.heroPanel ? [meta.heroPanel.imageField] : []), + ]); + const heroRelKey = meta.heroPanel?.quickRelatedKey; + // All field keys from the record const allKeys = Object.keys(record); - // Split into editable and read-only - const editableKeys = allKeys.filter(k => !isReadOnly(k) && meta.editableFields?.[k]); - const extraKeys = allKeys.filter(k => !isReadOnly(k) && !meta.editableFields?.[k] && !Array.isArray(record[k])); + // Split into editable and read-only (skip hero fields) + const editableKeys = allKeys.filter(k => !isReadOnly(k) && meta.editableFields?.[k] && !heroExclude.has(k)); + const extraKeys = allKeys.filter(k => !isReadOnly(k) && !meta.editableFields?.[k] && !Array.isArray(record[k]) && !heroExclude.has(k)); const arrayKeys = allKeys.filter(k => Array.isArray(record[k])); const roKeys = allKeys.filter(k => isReadOnly(k)); @@ -305,6 +370,17 @@ export default function RecordModal({ record, meta, onClose, onSaved }) { {/* Body */}
+ {/* Hero panel (pictures) */} + {meta.heroPanel && ( + + )} + {/* Editable fields */} {editableKeys.length > 0 && (
@@ -364,8 +440,8 @@ export default function RecordModal({ record, meta, onClose, onSaved }) {
)} - {/* Related data with link/unlink */} - {meta.fetchRelated?.map(rel => ( + {/* Related data with link/unlink (skip the one already in hero) */} + {meta.fetchRelated?.filter(r => r.key !== heroRelKey).map(rel => ( ))} diff --git a/src/lib/tables.js b/src/lib/tables.js index 911ef8d..db33492 100644 --- a/src/lib/tables.js +++ b/src/lib/tables.js @@ -45,6 +45,11 @@ export const TABLES = { primaryLabel: 'design', columns: ['design', 'status', 'picture_link', 'blurhash', 'created_at'], linkedFields: {}, + heroPanel: { + imageField: 'picture_link', + quickFields: ['status', 'design'], + quickRelatedKey: 'words', + }, editableFields: { design: { type: 'text' }, status: { type: 'select', options: ['published', 'blocked', 'uploaded', 'requested', 'generated'] },