Dateien nach "frontend/src/tools" hochladen
This commit is contained in:
@@ -48,8 +48,14 @@ function compressImage(file) {
|
|||||||
|
|
||||||
// ── Stepper ───────────────────────────────────────────────────────────────────
|
// ── Stepper ───────────────────────────────────────────────────────────────────
|
||||||
function Stepper({ label, value, onChange, step, unit, min = 0, hint }) {
|
function Stepper({ label, value, onChange, step, unit, min = 0, hint }) {
|
||||||
const dec = () => onChange(Math.max(min, Math.round((value - step) * 10000) / 10000));
|
const [raw, setRaw] = useState(String(value));
|
||||||
const inc = () => onChange(Math.round((value + step) * 10000) / 10000);
|
|
||||||
|
// Sync wenn value sich von außen ändert (z.B. beim Bearbeiten eines Archiv-Eintrags)
|
||||||
|
useEffect(() => { setRaw(String(value)); }, [value]);
|
||||||
|
|
||||||
|
const dec = () => { const v = Math.max(min, Math.round((value - step) * 10000) / 10000); onChange(v); setRaw(String(v)); };
|
||||||
|
const inc = () => { const v = Math.round((value + step) * 10000) / 10000; onChange(v); setRaw(String(v)); };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ color:'rgba(255,255,255,0.4)', fontSize:10, fontFamily:'monospace', marginBottom:5, letterSpacing:1 }}>
|
<div style={{ color:'rgba(255,255,255,0.4)', fontSize:10, fontFamily:'monospace', marginBottom:5, letterSpacing:1 }}>
|
||||||
@@ -57,8 +63,18 @@ function Stepper({ label, value, onChange, step, unit, min = 0, hint }) {
|
|||||||
</div>
|
</div>
|
||||||
<div style={{ display:'flex', alignItems:'center', background:'rgba(255,255,255,0.05)', borderRadius:8, border:'1px solid rgba(255,255,255,0.1)', overflow:'hidden' }}>
|
<div style={{ display:'flex', alignItems:'center', background:'rgba(255,255,255,0.05)', borderRadius:8, border:'1px solid rgba(255,255,255,0.1)', overflow:'hidden' }}>
|
||||||
<button onClick={dec} style={{ padding:'10px 12px', background:'transparent', border:'none', color:'rgba(255,255,255,0.6)', fontSize:20, cursor:'pointer', flexShrink:0, lineHeight:1 }}>−</button>
|
<button onClick={dec} style={{ padding:'10px 12px', background:'transparent', border:'none', color:'rgba(255,255,255,0.6)', fontSize:20, cursor:'pointer', flexShrink:0, lineHeight:1 }}>−</button>
|
||||||
<input type="number" value={value} step={step}
|
<input
|
||||||
onChange={e => onChange(parseFloat(e.target.value) || 0)}
|
type="number" value={raw} step={step}
|
||||||
|
onChange={e => {
|
||||||
|
setRaw(e.target.value);
|
||||||
|
const parsed = parseFloat(e.target.value);
|
||||||
|
if (!isNaN(parsed)) onChange(Math.max(min, parsed));
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
const parsed = parseFloat(raw);
|
||||||
|
const safe = isNaN(parsed) ? min : Math.max(min, parsed);
|
||||||
|
onChange(safe); setRaw(String(safe));
|
||||||
|
}}
|
||||||
style={{ flex:1, background:'transparent', border:'none', color:'#fff', fontSize:15, fontFamily:"'Space Mono',monospace", textAlign:'center', outline:'none', minWidth:0, padding:'10px 4px' }}
|
style={{ flex:1, background:'transparent', border:'none', color:'#fff', fontSize:15, fontFamily:"'Space Mono',monospace", textAlign:'center', outline:'none', minWidth:0, padding:'10px 4px' }}
|
||||||
/>
|
/>
|
||||||
<button onClick={inc} style={{ padding:'10px 12px', background:'transparent', border:'none', color:'rgba(255,255,255,0.6)', fontSize:20, cursor:'pointer', flexShrink:0, lineHeight:1 }}>+</button>
|
<button onClick={inc} style={{ padding:'10px 12px', background:'transparent', border:'none', color:'rgba(255,255,255,0.6)', fontSize:20, cursor:'pointer', flexShrink:0, lineHeight:1 }}>+</button>
|
||||||
@@ -69,11 +85,23 @@ function Stepper({ label, value, onChange, step, unit, min = 0, hint }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function SmallInput({ label, value, onChange, step = 0.01 }) {
|
function SmallInput({ label, value, onChange, step = 0.01 }) {
|
||||||
|
const [raw, setRaw] = useState(String(value));
|
||||||
|
useEffect(() => { setRaw(String(value)); }, [value]);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ color:'rgba(255,255,255,0.6)', fontSize:9, fontFamily:'monospace', marginBottom:3 }}>{label}</div>
|
<div style={{ color:'rgba(255,255,255,0.6)', fontSize:9, fontFamily:'monospace', marginBottom:3 }}>{label}</div>
|
||||||
<input type="number" value={value} step={step}
|
<input
|
||||||
onChange={e => onChange(parseFloat(e.target.value) || 0)}
|
type="number" value={raw} step={step}
|
||||||
|
onChange={e => {
|
||||||
|
setRaw(e.target.value);
|
||||||
|
const parsed = parseFloat(e.target.value);
|
||||||
|
if (!isNaN(parsed)) onChange(parsed);
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
const parsed = parseFloat(raw);
|
||||||
|
if (!isNaN(parsed)) { onChange(parsed); setRaw(String(parsed)); }
|
||||||
|
else setRaw(String(value));
|
||||||
|
}}
|
||||||
style={{ ...S.inp, padding:'7px 8px', fontSize:13 }}
|
style={{ ...S.inp, padding:'7px 8px', fontSize:13 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -125,10 +153,12 @@ export default function Kalkulator3D({ toast, editData, onEditDone, mobile }) {
|
|||||||
const handleImagePick = async e => {
|
const handleImagePick = async e => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
if (!file.type.startsWith('image/')) { toast('Nur Bilddateien erlaubt (JPG, PNG, …)', 'error'); e.target.value = ''; return; }
|
||||||
|
if (file.size > 20 * 1024 * 1024) { toast(`Bild zu groß (${(file.size/1024/1024).toFixed(1)} MB) – max. 20 MB`, 'error'); e.target.value = ''; return; }
|
||||||
try {
|
try {
|
||||||
const compressed = await compressImage(file);
|
const compressed = await compressImage(file);
|
||||||
setImage(compressed);
|
setImage(compressed);
|
||||||
} catch { toast('Bild konnte nicht geladen werden', 'error'); }
|
} catch { toast('Bild konnte nicht verarbeitet werden – ist die Datei beschädigt?', 'error'); }
|
||||||
e.target.value = '';
|
e.target.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user