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; width: number | null;
height: number | null; height: number | null;
font_size: number; font_size: number;
font_weight: string;
text_align: string;
style: string; style: string;
static_text: string | null; static_text: string | null;
datapoint_id: number | null; datapoint_id: number | null;
icon: string | null;
} }
interface DisplayJson { interface DisplayApiElement {
display_id: number;
layout: {
id: number;
width: number;
height: number;
};
elements: Array<{
type: string; type: string;
x: number; x: number;
y: number; y: number;
width?: number; width?: number;
height?: number; height?: number;
font_size: number; font_size?: number;
style: string; font_weight?: string;
static_text?: string; text_align?: string;
style?: string;
icon?: string;
text?: string;
label?: string; label?: string;
value?: string; value?: string | null;
unit?: string; unit?: string | null;
}>;
} }
interface Display { interface DisplayApiResponse {
id: number; display: { id: number; name: string; width: number; height: number; refresh_sec: number };
name: string; layout: { type: string };
layout_id: number | null; elements: DisplayApiElement[];
} }
interface EpaperPreviewProps { interface EpaperPreviewProps {
@ -49,119 +47,146 @@ interface EpaperPreviewProps {
layoutHeight?: number; layoutHeight?: number;
} }
const SCALE = 0.5; const SCALE = 0.6;
export default function EpaperPreview({ layoutId, elements, layoutWidth = 800, layoutHeight = 480 }: EpaperPreviewProps) { 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(() => { useEffect(() => {
// Find a display using this layout and fetch its JSON api.get<any[]>('/api/displays')
api.get<Display[]>('/api/displays')
.then((displays) => { .then((displays) => {
const d = displays.find((disp) => disp.layout_id === layoutId); const d = displays.find((disp: any) => disp.layout_id === layoutId);
if (!d) return; setDisplayId(d ? d.id : null);
return api.get<DisplayJson>(`/api/display/${d.id}`).then(setDisplayJson).catch(() => {});
}) })
.catch(() => {}); .catch(() => setDisplayId(null));
}, [layoutId]); }, [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 w = layoutWidth * SCALE;
const h = layoutHeight * SCALE; const h = layoutHeight * SCALE;
const renderEl = (el: typeof elements[0], _idx?: number) => { const renderElements = liveElements || [];
const left = el.x * SCALE; const isLive = liveElements !== null;
const top = el.y * SCALE;
const fs = Math.max(8, el.font_size * SCALE);
if (el.type === 'line') { // If no display linked and no elements, show hint
if (!isLive && elements.length === 0) {
return ( return (
<div <div className="mt-4 p-4 bg-gray-50 rounded border text-sm text-gray-400 text-center">
key={el.id} Fuege Elemente hinzu um die Vorschau zu sehen.
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';
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> </div>
); );
}; }
// Use live display JSON if available, else render from elements prop // Build render list: use live data if available, otherwise use raw elements
const liveEls = displayJson?.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 ( return (
<div className="mt-4"> <div className="mt-4">
<p className="text-xs text-gray-500 mb-2"> <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> </p>
<div <div style={{
style={{ position: 'relative', width: w, height: h,
position: 'relative', backgroundColor: '#f0f0e8', border: '3px solid #222',
width: w, borderRadius: 4, overflow: 'hidden',
height: h, boxShadow: '2px 2px 8px rgba(0,0,0,0.25)',
backgroundColor: '#f5f0e8', fontFamily: 'Arial, sans-serif',
border: '2px solid #1a1a1a', }}>
borderRadius: 4, {items.map((el, idx) => {
overflow: 'hidden',
boxShadow: '2px 2px 8px rgba(0,0,0,0.2)',
}}
>
{liveEls
? liveEls.map((el, idx) => {
const left = el.x * SCALE; const left = el.x * SCALE;
const top = el.y * SCALE; const top = el.y * SCALE;
const fs = Math.max(8, el.font_size * SCALE); const fontSize = Math.max(7, (el.font_size || 24) * SCALE);
const isBold = el.font_weight === 'bold';
if (el.type === 'line') { if (el.type === 'line') {
return <div key={idx} style={{ position: 'absolute', left, top, width: (el.width ?? 100) * SCALE, height: 1, backgroundColor: '#000' }} />; return <div key={idx} style={{
position: 'absolute', left, top,
width: (el.width || 800) * SCALE,
height: Math.max(1, (el.height || 2) * SCALE),
backgroundColor: '#000',
}} />;
} }
if (el.type === 'rect') { 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' }} />; return <div key={idx} style={{
position: 'absolute', left, top,
width: (el.width || 100) * SCALE,
height: (el.height || 50) * SCALE,
border: '1px solid #000',
}} />;
} }
const text = el.static_text ?? (el.type === 'value' ? `${el.value ?? '?'} ${el.unit ?? ''}` : (el.label ?? '')); 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 isCard = el.style === 'card';
const elW = (el.width || 370) * SCALE;
const elH = (el.height || 90) * SCALE;
return ( if (isCard) {
<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 }}> return <div key={idx} style={{
{text} 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> </div>
); <div style={{ fontSize, fontWeight: 'bold', textAlign: 'right' }}>
}) {el.value ?? '—'} <span style={{ fontSize: fontSize * 0.5, color: '#555' }}>{el.unit || ''}</span>
: elements.map((el) => renderEl(el, el.id))} </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>
</div> </div>
); );