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:
@@ -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} />
|
||||
))}
|
||||
|
||||
|
||||
@@ -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'] },
|
||||
|
||||
Reference in New Issue
Block a user