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 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 08:54:03 +02:00
parent 3c59713c25
commit 211bd464d2
2 changed files with 86 additions and 5 deletions

View File

@@ -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 (
<section className="flex gap-4 bg-slate-50 rounded-xl p-3">
{/* Image side — ~40% */}
<div className="w-2/5 flex-shrink-0 flex items-center justify-center bg-slate-200 rounded-lg overflow-hidden min-h-[140px] max-h-[220px]">
{imageUrl ? (
<img
src={imageUrl}
alt="Vorschau"
className="w-full h-full object-contain"
style={{ maxHeight: 220 }}
/>
) : (
<span className="text-slate-400 text-sm">Kein Bild</span>
)}
</div>
{/* Info side — ~60% */}
<div className="flex-1 min-w-0 flex flex-col gap-3">
{quickFields.map(key => {
const fieldDef = editableFields?.[key];
return (
<div key={key}>
<FieldLabel name={key} />
<div className="mt-1">
{fieldDef ? (
<EditableField
fieldKey={key}
value={values[key]}
fieldDef={fieldDef}
onChange={val => handleChange(key, val)}
/>
) : (
<ReadOnlyValue fieldKey={key} value={values[key]} />
)}
</div>
{dirty[key] && (
<span className="text-xs text-indigo-500 mt-0.5 block">geändert</span>
)}
</div>
);
})}
{quickRel && (
<div>
<RelationManager recordId={record.id} rel={quickRel} />
</div>
)}
</div>
</section>
);
}
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 */}
<div className="px-6 py-5 space-y-5 overflow-y-auto">
{/* Hero panel (pictures) */}
{meta.heroPanel && (
<HeroPanel
record={record}
meta={meta}
values={values}
dirty={dirty}
handleChange={handleChange}
/>
)}
{/* Editable fields */}
{editableKeys.length > 0 && (
<section>
@@ -364,8 +440,8 @@ export default function RecordModal({ record, meta, onClose, onSaved }) {
</section>
)}
{/* 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 => (
<RelationManager key={rel.key} recordId={record.id} rel={rel} />
))}

View File

@@ -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'] },