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 }) {
|
export default function RecordModal({ record, meta, onClose, onSaved }) {
|
||||||
const [values, setValues] = useState({});
|
const [values, setValues] = useState({});
|
||||||
const [dirty, setDirty] = useState({});
|
const [dirty, setDirty] = useState({});
|
||||||
@@ -273,11 +331,18 @@ export default function RecordModal({ record, meta, onClose, onSaved }) {
|
|||||||
|
|
||||||
if (!record) return null;
|
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
|
// All field keys from the record
|
||||||
const allKeys = Object.keys(record);
|
const allKeys = Object.keys(record);
|
||||||
// Split into editable and read-only
|
// Split into editable and read-only (skip hero fields)
|
||||||
const editableKeys = allKeys.filter(k => !isReadOnly(k) && meta.editableFields?.[k]);
|
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]));
|
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 arrayKeys = allKeys.filter(k => Array.isArray(record[k]));
|
||||||
const roKeys = allKeys.filter(k => isReadOnly(k));
|
const roKeys = allKeys.filter(k => isReadOnly(k));
|
||||||
|
|
||||||
@@ -305,6 +370,17 @@ export default function RecordModal({ record, meta, onClose, onSaved }) {
|
|||||||
{/* Body */}
|
{/* Body */}
|
||||||
<div className="px-6 py-5 space-y-5 overflow-y-auto">
|
<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 */}
|
{/* Editable fields */}
|
||||||
{editableKeys.length > 0 && (
|
{editableKeys.length > 0 && (
|
||||||
<section>
|
<section>
|
||||||
@@ -364,8 +440,8 @@ export default function RecordModal({ record, meta, onClose, onSaved }) {
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Related data with link/unlink */}
|
{/* Related data with link/unlink (skip the one already in hero) */}
|
||||||
{meta.fetchRelated?.map(rel => (
|
{meta.fetchRelated?.filter(r => r.key !== heroRelKey).map(rel => (
|
||||||
<RelationManager key={rel.key} recordId={record.id} rel={rel} />
|
<RelationManager key={rel.key} recordId={record.id} rel={rel} />
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ export const TABLES = {
|
|||||||
primaryLabel: 'design',
|
primaryLabel: 'design',
|
||||||
columns: ['design', 'status', 'picture_link', 'blurhash', 'created_at'],
|
columns: ['design', 'status', 'picture_link', 'blurhash', 'created_at'],
|
||||||
linkedFields: {},
|
linkedFields: {},
|
||||||
|
heroPanel: {
|
||||||
|
imageField: 'picture_link',
|
||||||
|
quickFields: ['status', 'design'],
|
||||||
|
quickRelatedKey: 'words',
|
||||||
|
},
|
||||||
editableFields: {
|
editableFields: {
|
||||||
design: { type: 'text' },
|
design: { type: 'text' },
|
||||||
status: { type: 'select', options: ['published', 'blocked', 'uploaded', 'requested', 'generated'] },
|
status: { type: 'select', options: ['published', 'blocked', 'uploaded', 'requested', 'generated'] },
|
||||||
|
|||||||
Reference in New Issue
Block a user