fix: auto-refresh preview after element edits, improved rendering

Preview now reloads display data whenever elements change.
Card style renders with proper label/value/unit layout.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Mueller 2026-04-07 03:53:17 +02:00
parent 19497fe335
commit 4964a2e8ab

View File

@ -9,37 +9,35 @@ interface LayoutElement {
width: number | null;
height: number | null;
font_size: number;
font_weight: string;
text_align: string;
style: string;
static_text: string | null;
datapoint_id: number | null;
icon: string | null;
}
interface DisplayJson {
display_id: number;
layout: {
id: number;
width: number;
height: number;
};
elements: Array<{
type: string;
x: number;
y: number;
width?: number;
height?: number;
font_size: number;
style: string;
static_text?: string;
label?: string;
value?: string;
unit?: string;
}>;
interface DisplayApiElement {
type: string;
x: number;
y: number;
width?: number;
height?: number;
font_size?: number;
font_weight?: string;
text_align?: string;
style?: string;
icon?: string;
text?: string;
label?: string;
value?: string | null;
unit?: string | null;
}
interface Display {
id: number;
name: string;
layout_id: number | null;
interface DisplayApiResponse {
display: { id: number; name: string; width: number; height: number; refresh_sec: number };
layout: { type: string };
elements: DisplayApiElement[];
}
interface EpaperPreviewProps {
@ -49,119 +47,146 @@ interface EpaperPreviewProps {
layoutHeight?: number;
}
const SCALE = 0.5;
const SCALE = 0.6;
export default function EpaperPreview({ layoutId, elements, layoutWidth = 800, layoutHeight = 480 }: EpaperPreviewProps) {
const [displayJson, setDisplayJson] = useState<DisplayJson | null>(null);
const [liveElements, setLiveElements] = useState<DisplayApiElement[] | null>(null);
const [displayId, setDisplayId] = useState<number | null>(null);
// Find a display that uses this layout
useEffect(() => {
// Find a display using this layout and fetch its JSON
api.get<Display[]>('/api/displays')
api.get<any[]>('/api/displays')
.then((displays) => {
const d = displays.find((disp) => disp.layout_id === layoutId);
if (!d) return;
return api.get<DisplayJson>(`/api/display/${d.id}`).then(setDisplayJson).catch(() => {});
const d = displays.find((disp: any) => disp.layout_id === layoutId);
setDisplayId(d ? d.id : null);
})
.catch(() => {});
.catch(() => setDisplayId(null));
}, [layoutId]);
// Reload display data whenever elements change OR displayId is found
useEffect(() => {
if (!displayId) { setLiveElements(null); return; }
api.get<DisplayApiResponse>(`/api/display/${displayId}`)
.then((data) => setLiveElements(data.elements))
.catch(() => setLiveElements(null));
}, [displayId, elements]); // elements is a new array ref after each selectLayout()
const w = layoutWidth * SCALE;
const h = layoutHeight * SCALE;
const renderEl = (el: typeof elements[0], _idx?: number) => {
const left = el.x * SCALE;
const top = el.y * SCALE;
const fs = Math.max(8, el.font_size * SCALE);
if (el.type === 'line') {
return (
<div
key={el.id}
style={{ position: 'absolute', left, top, width: (el.width ?? 100) * SCALE, height: 1, backgroundColor: '#000' }}
/>
);
}
if (el.type === 'rect') {
return (
<div
key={el.id}
style={{
position: 'absolute', left, top,
width: (el.width ?? 80) * SCALE,
height: (el.height ?? 40) * SCALE,
border: '1px solid #000',
}}
/>
);
}
// label or value elements
const text = el.static_text ?? (el.type === 'label' ? `[DP ${el.datapoint_id ?? '?'}]` : `[Wert]`);
const isCard = el.style === 'card';
const renderElements = liveElements || [];
const isLive = liveElements !== null;
// If no display linked and no elements, show hint
if (!isLive && elements.length === 0) {
return (
<div
key={el.id}
style={{
position: 'absolute', left, top,
fontSize: fs,
fontFamily: 'monospace',
color: '#000',
background: isCard ? '#e8e8e8' : 'transparent',
border: isCard ? '1px solid #aaa' : 'none',
padding: isCard ? '2px 4px' : 0,
whiteSpace: 'nowrap',
lineHeight: 1.2,
}}
>
{text}
<div className="mt-4 p-4 bg-gray-50 rounded border text-sm text-gray-400 text-center">
Fuege Elemente hinzu um die Vorschau zu sehen.
</div>
);
};
}
// Use live display JSON if available, else render from elements prop
const liveEls = displayJson?.elements;
// Build render list: use live data if available, otherwise use raw elements
const items: DisplayApiElement[] = isLive
? renderElements
: elements.map((el) => ({
type: el.type,
x: el.x,
y: el.y,
width: el.width ?? undefined,
height: el.height ?? undefined,
font_size: el.font_size,
font_weight: el.font_weight,
text_align: el.text_align,
style: el.style,
icon: el.icon ?? undefined,
text: el.type === 'label' ? (el.static_text || '') : undefined,
label: el.type === 'value' ? `DP #${el.datapoint_id || '?'}` : undefined,
value: el.type === 'value' ? '—' : undefined,
unit: '',
}));
return (
<div className="mt-4">
<p className="text-xs text-gray-500 mb-2">
E-Paper Vorschau ({layoutWidth}x{layoutHeight}px){liveEls ? ' — Live-Daten' : ' — Design-Modus'}
Vorschau ({layoutWidth}x{layoutHeight})
{isLive ? ' — Live-Daten' : ' — Layout-Struktur (kein Display zugewiesen)'}
</p>
<div
style={{
position: 'relative',
width: w,
height: h,
backgroundColor: '#f5f0e8',
border: '2px solid #1a1a1a',
borderRadius: 4,
overflow: 'hidden',
boxShadow: '2px 2px 8px rgba(0,0,0,0.2)',
}}
>
{liveEls
? liveEls.map((el, idx) => {
const left = el.x * SCALE;
const top = el.y * SCALE;
const fs = Math.max(8, el.font_size * SCALE);
<div style={{
position: 'relative', width: w, height: h,
backgroundColor: '#f0f0e8', border: '3px solid #222',
borderRadius: 4, overflow: 'hidden',
boxShadow: '2px 2px 8px rgba(0,0,0,0.25)',
fontFamily: 'Arial, sans-serif',
}}>
{items.map((el, idx) => {
const left = el.x * SCALE;
const top = el.y * SCALE;
const fontSize = Math.max(7, (el.font_size || 24) * SCALE);
const isBold = el.font_weight === 'bold';
if (el.type === 'line') {
return <div key={idx} style={{ position: 'absolute', left, top, width: (el.width ?? 100) * SCALE, height: 1, backgroundColor: '#000' }} />;
}
if (el.type === 'rect') {
return <div key={idx} style={{ position: 'absolute', left, top, width: (el.width ?? 80) * SCALE, height: (el.height ?? 40) * SCALE, border: '1px solid #000' }} />;
}
if (el.type === 'line') {
return <div key={idx} style={{
position: 'absolute', left, top,
width: (el.width || 800) * SCALE,
height: Math.max(1, (el.height || 2) * SCALE),
backgroundColor: '#000',
}} />;
}
const text = el.static_text ?? (el.type === 'value' ? `${el.value ?? '?'} ${el.unit ?? ''}` : (el.label ?? ''));
const isCard = el.style === 'card';
if (el.type === 'rect') {
return <div key={idx} style={{
position: 'absolute', left, top,
width: (el.width || 100) * SCALE,
height: (el.height || 50) * SCALE,
border: '1px solid #000',
}} />;
}
return (
<div key={idx} style={{ position: 'absolute', left, top, fontSize: fs, fontFamily: 'monospace', color: '#000', background: isCard ? '#e8e8e8' : 'transparent', border: isCard ? '1px solid #aaa' : 'none', padding: isCard ? '2px 4px' : 0, whiteSpace: 'nowrap', lineHeight: 1.2 }}>
{text}
if (el.type === 'label') {
const align = el.text_align || 'left';
return <div key={idx} style={{
position: 'absolute', left, top,
fontSize, fontWeight: isBold ? 'bold' : 'normal',
color: '#000', whiteSpace: 'nowrap',
...(align === 'center' ? { width: w, left: 0, textAlign: 'center' } : {}),
...(align === 'right' ? { right: 0, textAlign: 'right' } : {}),
}}>
{el.text || ''}
</div>;
}
if (el.type === 'value') {
const isCard = el.style === 'card';
const elW = (el.width || 370) * SCALE;
const elH = (el.height || 90) * SCALE;
if (isCard) {
return <div key={idx} style={{
position: 'absolute', left, top,
width: elW, height: elH,
border: '1px solid #aaa', borderRadius: 4 * SCALE,
background: '#e8e8e0', padding: 4 * SCALE,
display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
}}>
<div style={{ fontSize: fontSize * 0.45, color: '#555', textTransform: 'uppercase', letterSpacing: 0.5 }}>
{el.icon ? `[${el.icon}] ` : ''}{el.label || ''}
</div>
);
})
: elements.map((el) => renderEl(el, el.id))}
<div style={{ fontSize, fontWeight: 'bold', textAlign: 'right' }}>
{el.value ?? '—'} <span style={{ fontSize: fontSize * 0.5, color: '#555' }}>{el.unit || ''}</span>
</div>
</div>;
}
return <div key={idx} style={{
position: 'absolute', left, top, fontSize, color: '#000', whiteSpace: 'nowrap',
}}>
{el.label}: <strong>{el.value ?? '—'}</strong> {el.unit || ''}
</div>;
}
return null;
})}
</div>
</div>
);