773 lines
39 KiB
JavaScript
773 lines
39 KiB
JavaScript
import { useState } from 'react';
|
||
import { S } from '../lib.js';
|
||
|
||
const MONTHS = ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'];
|
||
const WEEKDAYS = ['Sonntag','Montag','Dienstag','Mittwoch','Donnerstag','Freitag','Samstag'];
|
||
|
||
function parseField(val) {
|
||
val = String(val).trim();
|
||
if (val==='*'||val==='?') return {type:'all'};
|
||
if (val.startsWith('*/')) { const n=parseInt(val.slice(2)); return {type:'step',step:n}; }
|
||
if (val.includes('/')&&!val.startsWith('*/')) { const [s,st]=val.split('/'); return {type:'stepFrom',from:parseInt(s),step:parseInt(st)}; }
|
||
if (val.includes(',')) return {type:'list',vals:val.split(',').map(v=>parseInt(v))};
|
||
if (val.includes('-')) { const [a,b]=val.split('-').map(Number); return {type:'range',from:a,to:b}; }
|
||
const n=parseInt(val); if (!isNaN(n)) return {type:'single',val:n};
|
||
return {type:'raw',val};
|
||
}
|
||
|
||
function describeField(f, names) {
|
||
switch(f.type) {
|
||
case 'all': return null;
|
||
case 'step': return `alle ${f.step}`;
|
||
case 'stepFrom': return `ab ${f.from} alle ${f.step}`;
|
||
case 'list': return f.vals.map(v=>names?.[v]||v).join(', ');
|
||
case 'range': return `${names?.[f.from]||f.from}–${names?.[f.to]||f.to}`;
|
||
case 'single': return names?.[f.val] || String(f.val);
|
||
default: return f.val;
|
||
}
|
||
}
|
||
|
||
function explainCron(expr) {
|
||
const parts = expr.trim().split(/\s+/);
|
||
const specials = {'@yearly':'0 0 1 1 *','@annually':'0 0 1 1 *','@monthly':'0 0 1 * *',
|
||
'@weekly':'0 0 * * 0','@daily':'0 0 * * *','@midnight':'0 0 * * *','@hourly':'0 * * * *'};
|
||
if (parts.length===1) {
|
||
if (parts[0]==='@reboot') return {ok:true,human:'Beim Systemstart',fields:null};
|
||
if (specials[parts[0]]) return explainCron(specials[parts[0]]);
|
||
}
|
||
if (parts.length!==5) return {ok:false,error:'Ein Crontab besteht aus 5 Feldern: Minute Stunde Tag Monat Wochentag'};
|
||
const [minP,hrP,domP,monP,dowP] = parts;
|
||
const min=parseField(minP), hr=parseField(hrP), dom=parseField(domP), mon=parseField(monP), dow=parseField(dowP);
|
||
const out = [];
|
||
if (hr.type==='all'&&min.type==='all') out.push('Jede Minute');
|
||
else if (hr.type==='all'&&min.type==='step') out.push(`Alle ${min.step} Minuten`);
|
||
else if (hr.type==='all') out.push(`Jede Stunde um :${String(describeField(min)).padStart(2,'0')}`);
|
||
else if (min.type==='all') out.push(`Jede Minute der Stunde(n) ${describeField(hr)}`);
|
||
else { const m=describeField(min), h=describeField(hr); out.push(`${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')} Uhr`); }
|
||
if (dow.type!=='all') out.push(`jeden ${describeField(dow,WEEKDAYS)}`);
|
||
if (dom.type!=='all') { const d=describeField(dom); out.push(`am ${d}. des Monats`); }
|
||
if (mon.type!=='all') out.push(`im ${describeField(mon,MONTHS)}`);
|
||
return {ok:true,human:out.join(', '),fields:{min,hr,dom,mon,dow},raw:parts};
|
||
}
|
||
|
||
function nextRuns(expr, count=5) {
|
||
try {
|
||
const parts = expr.trim().split(/\s+/);
|
||
if (parts.length!==5) return [];
|
||
const [minP,hrP,domP,monP,dowP] = parts;
|
||
const fMin=parseField(minP), fHr=parseField(hrP), fDom=parseField(domP), fMon=parseField(monP), fDow=parseField(dowP);
|
||
const match = (f, v) => {
|
||
switch(f.type){
|
||
case 'all': return true;
|
||
case 'single': return v===f.val||(f.val===7&&v===0);
|
||
case 'range': return v>=f.from&&v<=f.to;
|
||
case 'step': return v%f.step===0;
|
||
case 'stepFrom': return v>=f.from&&(v-f.from)%f.step===0;
|
||
case 'list': return f.vals.includes(v);
|
||
default: return false;
|
||
}
|
||
};
|
||
const results=[], d=new Date();
|
||
d.setSeconds(0); d.setMilliseconds(0); d.setMinutes(d.getMinutes()+1);
|
||
for (let i=0;i<50000&&results.length<count;i++){
|
||
if (match(fMon,d.getMonth()+1)&&match(fDom,d.getDate())&&
|
||
(fDow.type==='all'||match(fDow,d.getDay()))&&
|
||
match(fHr,d.getHours())&&match(fMin,d.getMinutes())) results.push(new Date(d));
|
||
d.setMinutes(d.getMinutes()+1);
|
||
}
|
||
return results;
|
||
} catch { return []; }
|
||
}
|
||
|
||
// ── Erklärer ──────────────────────────────────────────────────────────────────
|
||
function CronExplainer() {
|
||
const [input, setInput] = useState('');
|
||
const expr = input.trim();
|
||
const result = expr ? explainCron(expr) : null;
|
||
const runs = result?.ok ? nextRuns(expr) : [];
|
||
const fmtDt = d => d.toLocaleString('de-DE',{weekday:'short',day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'});
|
||
const EXAMPLES = [['0 8 * * 1','Mo 8:00'],['*/15 * * * *','alle 15 Min'],['0 0 1 * *','1. jeden Monat'],['30 18 * * 1-5','Mo–Fr 18:30'],['0 9,17 * * *','9 & 17 Uhr'],['0 0 * * 0','So 0:00']];
|
||
return (
|
||
<div>
|
||
<div style={{marginBottom:10}}>
|
||
<label style={{...S.head,display:'block',marginBottom:6,fontSize:10}}>CRONTAB AUSDRUCK</label>
|
||
<input value={input} onChange={e=>setInput(e.target.value)} placeholder="z.B. 0 8 * * 1"
|
||
autoCapitalize="none" style={{...S.inp,fontFamily:"'Courier New',monospace",fontSize:16}}/>
|
||
</div>
|
||
<div style={{display:'flex',gap:5,flexWrap:'wrap',marginBottom:16}}>
|
||
{EXAMPLES.map(([ex,l])=>(
|
||
<button key={ex} onClick={()=>setInput(ex)} style={{padding:'4px 10px',borderRadius:6,
|
||
fontFamily:'monospace',fontSize:10,cursor:'pointer',
|
||
background:'rgba(255,255,255,0.05)',border:'1px solid rgba(255,255,255,0.1)',color:'rgba(255,255,255,0.5)'}}>
|
||
<code>{ex}</code> <span style={{color:'rgba(255,255,255,0.5)',fontSize:9}}>({l})</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
{result && (result.ok ? (
|
||
<div>
|
||
<div style={{background:'rgba(78,205,196,0.08)',border:'1px solid rgba(78,205,196,0.2)',
|
||
borderRadius:10,padding:'14px 16px',marginBottom:12}}>
|
||
<div style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:9,marginBottom:5}}>BEDEUTET</div>
|
||
<div style={{color:'#fff',fontFamily:'monospace',fontSize:16,fontWeight:700}}>{result.human}</div>
|
||
</div>
|
||
{result.fields && (
|
||
<div style={{display:'grid',gridTemplateColumns:'repeat(5,1fr)',gap:8,marginBottom:12}}>
|
||
{[['MIN',0],[' STD',1],['TAG',2],['MON',3],['WTAG',4]].map(([lbl,i])=>{
|
||
const raw=expr.split(/\s+/)[i];
|
||
const fNames=i===3?MONTHS:i===4?WEEKDAYS:null;
|
||
const fd=Object.values(result.fields)[i];
|
||
return (
|
||
<div key={lbl} style={{background:'rgba(255,255,255,0.04)',border:'1px solid rgba(255,255,255,0.07)',
|
||
borderRadius:8,padding:'8px 10px',textAlign:'center'}}>
|
||
<div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:8,marginBottom:4}}>{lbl}</div>
|
||
<div style={{color:'#4ecdc4',fontFamily:"'Courier New',monospace",fontSize:14,fontWeight:700}}>{raw}</div>
|
||
<div style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:9,marginTop:3}}>
|
||
{fd?.type==='all'?'jede/r':(describeField(fd,fNames)||'–')}
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
{runs.length>0&&(
|
||
<div>
|
||
<div style={{...S.head,marginBottom:6,fontSize:10}}>NÄCHSTE AUSFÜHRUNGEN</div>
|
||
{runs.map((d,i)=>(
|
||
<div key={i} style={{display:'flex',alignItems:'center',gap:10,padding:'5px 0',borderBottom:'1px solid rgba(255,255,255,0.04)'}}>
|
||
<span style={{color:'rgba(78,205,196,0.5)',fontFamily:'monospace',fontSize:10,minWidth:16,textAlign:'right'}}>{i+1}.</span>
|
||
<span style={{color:'rgba(255,255,255,0.7)',fontFamily:'monospace',fontSize:11}}>{fmtDt(d)}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<div style={{background:'rgba(255,107,157,0.08)',border:'1px solid rgba(255,107,157,0.2)',
|
||
borderRadius:8,padding:12,color:'#ff6b9d',fontFamily:'monospace',fontSize:11}}>⚠ {result.error}</div>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── Builder ───────────────────────────────────────────────────────────────────
|
||
function CronBuilder() {
|
||
const [minute, setMinute] = useState('0');
|
||
const [hour, setHour] = useState('8');
|
||
const [dom, setDom] = useState('*');
|
||
const [month, setMonth] = useState('*');
|
||
const [dow, setDow] = useState('*');
|
||
const [preset, setPreset] = useState('');
|
||
const [copied, setCopied] = useState(false);
|
||
|
||
const cron = `${minute} ${hour} ${dom} ${month} ${dow}`;
|
||
const result = explainCron(cron);
|
||
const runs = result?.ok ? nextRuns(cron) : [];
|
||
const fmtDt = d => d.toLocaleString('de-DE',{weekday:'short',day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'});
|
||
|
||
const copy = async () => { try { await navigator.clipboard.writeText(cron); setCopied(true); setTimeout(()=>setCopied(false),1500); } catch {} };
|
||
|
||
const PRESETS = [
|
||
{k:'every_min', l:'Jede Minute', v:['*','*','*','*','*']},
|
||
{k:'every_hour', l:'Jede Stunde', v:['0','*','*','*','*']},
|
||
{k:'every_day_8', l:'Tägl. 8:00', v:['0','8','*','*','*']},
|
||
{k:'every_mon', l:'Montag 8:00', v:['0','8','*','*','1']},
|
||
{k:'every_fri', l:'Freitag 17:00', v:['0','17','*','*','5']},
|
||
{k:'weekdays_9', l:'Mo–Fr 9:00', v:['0','9','*','*','1-5']},
|
||
{k:'first_month', l:'1. des Monats', v:['0','0','1','*','*']},
|
||
{k:'midnight', l:'Mitternacht', v:['0','0','*','*','*']},
|
||
];
|
||
|
||
const applyPreset = p => {
|
||
setPreset(p.k);
|
||
const [mi,hr,dm,mo,dw] = p.v;
|
||
setMinute(mi); setHour(hr); setDom(dm); setMonth(mo); setDow(dw);
|
||
};
|
||
|
||
const Lbl = ({c}) => <div style={{...S.head,marginBottom:4,fontSize:9}}>{c}</div>;
|
||
const Sel = ({value,onChange,children}) => (
|
||
<select value={value} onChange={e=>{onChange(e.target.value);setPreset('');}}
|
||
style={{...S.inp,fontSize:12,padding:'6px 8px',width:'100%'}}>{children}</select>
|
||
);
|
||
|
||
return (
|
||
<div>
|
||
<div style={{marginBottom:14}}>
|
||
<Lbl c="SCHNELLAUSWAHL"/>
|
||
<div style={{display:'flex',gap:6,flexWrap:'wrap'}}>
|
||
{PRESETS.map(p=>(
|
||
<button key={p.k} onClick={()=>applyPreset(p)} style={{
|
||
padding:'5px 11px',borderRadius:6,fontFamily:'monospace',fontSize:10,cursor:'pointer',
|
||
background:preset===p.k?'rgba(78,205,196,0.15)':'rgba(255,255,255,0.05)',
|
||
border:`1px solid ${preset===p.k?'rgba(78,205,196,0.3)':'rgba(255,255,255,0.1)'}`,
|
||
color:preset===p.k?'#4ecdc4':'rgba(255,255,255,0.55)',
|
||
}}>{p.l}</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:10,marginBottom:14}}>
|
||
<div><Lbl c="MINUTE"/>
|
||
<Sel value={minute} onChange={setMinute}>
|
||
<option value="*">* jede</option>
|
||
{[5,10,15,20,30].map(n=><option key={n} value={`*/${n}`}>alle {n} min</option>)}
|
||
{Array.from({length:60},(_,i)=><option key={i} value={i}>{String(i).padStart(2,'0')}</option>)}
|
||
</Sel>
|
||
</div>
|
||
<div><Lbl c="STUNDE"/>
|
||
<Sel value={hour} onChange={setHour}>
|
||
<option value="*">* jede</option>
|
||
{Array.from({length:24},(_,i)=><option key={i} value={i}>{String(i).padStart(2,'0')}:00</option>)}
|
||
<option value="9,17">9 & 17 Uhr</option>
|
||
<option value="8,12,17">8, 12 & 17 Uhr</option>
|
||
</Sel>
|
||
</div>
|
||
<div><Lbl c="WOCHENTAG"/>
|
||
<Sel value={dow} onChange={setDow}>
|
||
<option value="*">* jeder</option>
|
||
<option value="1-5">Mo–Fr (Werktage)</option>
|
||
<option value="6,0">Sa & So (Wochenende)</option>
|
||
{WEEKDAYS.map((d,i)=><option key={i} value={i}>{d}</option>)}
|
||
</Sel>
|
||
</div>
|
||
<div><Lbl c="TAG DES MONATS"/>
|
||
<Sel value={dom} onChange={setDom}>
|
||
<option value="*">* jeder</option>
|
||
{Array.from({length:31},(_,i)=><option key={i+1} value={i+1}>{i+1}.</option>)}
|
||
<option value="1,15">1. & 15.</option>
|
||
</Sel>
|
||
</div>
|
||
<div><Lbl c="MONAT"/>
|
||
<Sel value={month} onChange={setMonth}>
|
||
<option value="*">* jeder</option>
|
||
{MONTHS.map((m,i)=><option key={i+1} value={i+1}>{m}</option>)}
|
||
</Sel>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{background:'rgba(0,0,0,0.3)',border:'1px solid rgba(255,255,255,0.1)',borderRadius:10,padding:'14px 16px'}}>
|
||
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:10,flexWrap:'wrap',gap:8}}>
|
||
<code style={{color:'#4ecdc4',fontFamily:"'Courier New',monospace",fontSize:22,fontWeight:700,letterSpacing:3}}>{cron}</code>
|
||
<button onClick={copy} style={{
|
||
background:copied?'rgba(78,205,196,0.15)':'rgba(255,255,255,0.06)',
|
||
border:`1px solid ${copied?'rgba(78,205,196,0.3)':'rgba(255,255,255,0.1)'}`,
|
||
borderRadius:7,color:copied?'#4ecdc4':'rgba(255,255,255,0.5)',
|
||
cursor:'pointer',fontFamily:'monospace',fontSize:11,padding:'6px 14px'}}>
|
||
{copied?'✓ Kopiert':'⎘ Kopieren'}
|
||
</button>
|
||
</div>
|
||
{result?.ok&&<div style={{color:'rgba(255,255,255,0.65)',fontFamily:'monospace',fontSize:13,marginBottom:10}}>{result.human}</div>}
|
||
{runs.length>0&&(
|
||
<div style={{display:'flex',gap:6,flexWrap:'wrap'}}>
|
||
{runs.slice(0,3).map((d,i)=>(
|
||
<span key={i} style={{background:'rgba(255,255,255,0.04)',borderRadius:5,padding:'3px 8px',
|
||
color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:10}}>{fmtDt(d)}</span>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── Tool-Definitionen ─────────────────────────────────────────────────────────
|
||
// ── Elektro-Hilfskomponenten (AUSSERHALB damit keine Remounts) ────────────────
|
||
function ElektroInp({ vals, setVals, k, label, unit, placeholder }) {
|
||
return (
|
||
<div style={{ flex:1, minWidth:100 }}>
|
||
<div style={{ color:'rgba(255,255,255,0.55)', fontFamily:'monospace', fontSize:9, marginBottom:3 }}>
|
||
{label} [{unit}]
|
||
</div>
|
||
<input type="number" value={vals[k]||''} onChange={e=>setVals(p=>({...p,[k]:e.target.value}))}
|
||
placeholder={placeholder||''}
|
||
style={{ ...S.inp, fontSize:13, padding:'7px 9px', width:'100%', boxSizing:'border-box' }}/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function ElektroResult({ label, value, highlight }) {
|
||
return (
|
||
<div style={{ display:'flex', justifyContent:'space-between', alignItems:'center',
|
||
padding:'8px 12px', marginBottom:5, borderRadius:8,
|
||
background: highlight ? 'rgba(78,205,196,0.1)' : 'rgba(255,255,255,0.03)',
|
||
border: `1px solid ${highlight ? 'rgba(78,205,196,0.25)' : 'rgba(255,255,255,0.07)'}` }}>
|
||
<span style={{ color:'rgba(255,255,255,0.55)', fontFamily:'monospace', fontSize:11 }}>{label}</span>
|
||
<span style={{ color: highlight ? '#4ecdc4' : '#fff', fontFamily:"'Courier New',monospace",
|
||
fontSize:14, fontWeight:700 }}>{value}</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function ElektroSection({ title, children }) {
|
||
return (
|
||
<div style={{ marginBottom:20 }}>
|
||
<div style={{ ...S.head, marginBottom:10, color:'#4ecdc4' }}>{title}</div>
|
||
{children}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── Elektro-Rechner ───────────────────────────────────────────────────────────
|
||
function ElektroRechner({ mobile }) {
|
||
const [vals, setVals] = useState({});
|
||
const [preis, setPreis] = useState('0.38');
|
||
|
||
const n = k => { const v = parseFloat(vals[k]); return isNaN(v) ? null : v; };
|
||
const fmt = (v, unit, digits=3) => v == null ? '—' : `${parseFloat(v.toPrecision(digits))} ${unit}`;
|
||
|
||
// Res und Sec als direkte Aliase (kein Render-Problem, keine Inputs)
|
||
|
||
const cols = mobile ? '1fr' : '1fr 1fr';
|
||
|
||
// ── Ohmsches Gesetz ──
|
||
const U1=n('u1'), I1=n('i1'), R1=n('r1');
|
||
const ohmRes = {
|
||
U: U1 ?? (I1!=null&&R1!=null ? I1*R1 : null),
|
||
I: I1 ?? (U1!=null&&R1!=null ? U1/R1 : null),
|
||
R: R1 ?? (U1!=null&&I1!=null ? U1/I1 : null),
|
||
};
|
||
|
||
// ── Leistung P = U * I ──
|
||
const U2=n('u2'), I2=n('i2'), P2=n('p2'), R2=n('r2');
|
||
const leistRes = (() => {
|
||
if (U2!=null&&I2!=null) return { P:U2*I2, U:U2, I:I2, R:U2/I2 };
|
||
if (P2!=null&&U2!=null) return { P:P2, I:P2/U2, U:U2, R:U2*U2/P2 };
|
||
if (P2!=null&&I2!=null) return { P:P2, U:P2/I2, I:I2, R:P2/(I2*I2) };
|
||
if (P2!=null&&R2!=null) return { P:P2, U:Math.sqrt(P2*R2), I:Math.sqrt(P2/R2), R:R2 };
|
||
if (U2!=null&&R2!=null) return { P:U2*U2/R2, U:U2, I:U2/R2, R:R2 };
|
||
if (I2!=null&&R2!=null) return { P:I2*I2*R2, U:I2*R2, I:I2, R:R2 };
|
||
return null;
|
||
})();
|
||
|
||
// ── Energie & Kosten ──
|
||
const Pw=n('pw'), Std=n('std'), Tage=n('tage');
|
||
const kwhTag = Pw!=null&&Std!=null ? (Pw/1000)*Std : null;
|
||
const kwhMonat= kwhTag!=null&&Tage!=null ? kwhTag*Tage : (kwhTag!=null ? kwhTag*30 : null);
|
||
const kwhJahr = kwhTag!=null ? kwhTag*365 : null;
|
||
const ep = parseFloat(preis);
|
||
const kostenTag = kwhTag!=null ? kwhTag*ep : null;
|
||
const kostenMonat = kwhMonat!=null ? kwhMonat*ep : null;
|
||
const kostenJahr = kwhJahr!=null ? kwhJahr*ep : null;
|
||
|
||
// ── Wirkungsgrad ──
|
||
const Pa=n('pa'), Pe=n('pe');
|
||
const eta = Pa!=null&&Pe!=null&&Pe>0 ? (Pa/Pe)*100 : null;
|
||
const verlust = Pa!=null&&Pe!=null ? Pe-Pa : null;
|
||
|
||
// ── Serienschaltung Widerstände ──
|
||
const r_ser = ['rs1','rs2','rs3','rs4'].map(k=>n(k)).filter(v=>v!=null);
|
||
const R_ser = r_ser.reduce((a,b)=>a+b, 0) || null;
|
||
|
||
// ── Parallelschaltung Widerstände ──
|
||
const r_par = ['rp1','rp2','rp3','rp4'].map(k=>n(k)).filter(v=>v!=null);
|
||
const R_par = r_par.length ? 1/(r_par.reduce((a,b)=>a+1/b, 0)) : null;
|
||
|
||
// ── Kondensator Ladestrom ──
|
||
const Cap=n('cap'), Uc=n('uc'), Tc=n('tc');
|
||
const Ic = Cap!=null&&Uc!=null&&Tc!=null ? Cap*(Uc/Tc) : null;
|
||
|
||
return (
|
||
<div>
|
||
{/* Info */}
|
||
<div style={{ color:'rgba(255,255,255,0.5)', fontFamily:'monospace', fontSize:9,
|
||
marginBottom:16, lineHeight:1.8 }}>
|
||
Felder ausfüllen → Ergebnis erscheint automatisch · Mindestens 2 bekannte Größen eingeben
|
||
</div>
|
||
|
||
<div style={{ display:'grid', gridTemplateColumns:cols, gap:20 }}>
|
||
<div>
|
||
{/* Ohmsches Gesetz */}
|
||
<ElektroSection title="⚡ Ohmsches Gesetz U = R · I">
|
||
<div style={{ display:'flex', gap:8, marginBottom:10, flexWrap:'wrap' }}>
|
||
<ElektroInp vals={vals} setVals={setVals} k="u1" label="Spannung" unit="V" placeholder="230"/>
|
||
<ElektroInp vals={vals} setVals={setVals} k="i1" label="Strom" unit="A" placeholder="2"/>
|
||
<ElektroInp vals={vals} setVals={setVals} k="r1" label="Widerstand" unit="Ω" placeholder="115"/>
|
||
</div>
|
||
<ElektroResult label="U – Spannung" value={fmt(ohmRes.U,'V')} highlight={!U1&&ohmRes.U!=null}/>
|
||
<ElektroResult label="I – Strom" value={fmt(ohmRes.I,'A')} highlight={!I1&&ohmRes.I!=null}/>
|
||
<ElektroResult label="R – Widerstand" value={fmt(ohmRes.R,'Ω')} highlight={!R1&&ohmRes.R!=null}/>
|
||
</ElektroSection>
|
||
|
||
{/* Leistung */}
|
||
<ElektroSection title="🔋 Leistung P = U · I = U²/R = I²·R">
|
||
<div style={{ display:'flex', gap:8, marginBottom:10, flexWrap:'wrap' }}>
|
||
<ElektroInp vals={vals} setVals={setVals} k="u2" label="Spannung" unit="V"/>
|
||
<ElektroInp vals={vals} setVals={setVals} k="i2" label="Strom" unit="A"/>
|
||
<ElektroInp vals={vals} setVals={setVals} k="p2" label="Leistung" unit="W"/>
|
||
<ElektroInp vals={vals} setVals={setVals} k="r2" label="Widerstand" unit="Ω"/>
|
||
</div>
|
||
{leistRes ? <>
|
||
<ElektroResult label="P – Leistung" value={fmt(leistRes.P,'W')} highlight={!P2}/>
|
||
<ElektroResult label="U – Spannung" value={fmt(leistRes.U,'V')} highlight={!U2}/>
|
||
<ElektroResult label="I – Strom" value={fmt(leistRes.I,'A')} highlight={!I2}/>
|
||
<ElektroResult label="R – Widerstand" value={fmt(leistRes.R,'Ω')} highlight={!R2}/>
|
||
</> : <div style={{color:'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:11}}>Mindestens 2 Werte eingeben</div>}
|
||
</ElektroSection>
|
||
|
||
{/* Wirkungsgrad */}
|
||
<ElektroSection title="🔄 Wirkungsgrad η = P_ab / P_zu">
|
||
<div style={{ display:'flex', gap:8, marginBottom:10 }}>
|
||
<ElektroInp vals={vals} setVals={setVals} k="pa" label="Abgabe-Leistung" unit="W" placeholder="800"/>
|
||
<ElektroInp vals={vals} setVals={setVals} k="pe" label="Aufnahme-Leistung" unit="W" placeholder="1000"/>
|
||
</div>
|
||
<ElektroResult label="η – Wirkungsgrad" value={eta!=null ? `${eta.toFixed(1)} %` : '—'} highlight={eta!=null}/>
|
||
<ElektroResult label="Verlustleistung" value={fmt(verlust,'W')}/>
|
||
</ElektroSection>
|
||
</div>
|
||
|
||
<div>
|
||
{/* Stromkosten */}
|
||
<ElektroSection title="💶 Stromkosten">
|
||
<div style={{ display:'flex', gap:8, marginBottom:10, flexWrap:'wrap' }}>
|
||
<ElektroInp vals={vals} setVals={setVals} k="pw" label="Leistung" unit="W" placeholder="100"/>
|
||
<ElektroInp vals={vals} setVals={setVals} k="std" label="Stunden/Tag" unit="h" placeholder="5"/>
|
||
<ElektroInp vals={vals} setVals={setVals} k="tage" label="Tage/Monat" unit="d" placeholder="30"/>
|
||
</div>
|
||
<div style={{ marginBottom:8 }}>
|
||
<div style={{ color:'rgba(255,255,255,0.55)', fontFamily:'monospace', fontSize:9, marginBottom:3 }}>
|
||
Strompreis [€/kWh]
|
||
</div>
|
||
<input type="number" value={preis} onChange={e=>setPreis(e.target.value)}
|
||
step="0.01" min="0"
|
||
style={{ ...S.inp, fontSize:13, padding:'7px 9px', width:'100%', boxSizing:'border-box' }}/>
|
||
</div>
|
||
<ElektroResult label="kWh/Tag" value={kwhTag!=null ? `${kwhTag.toFixed(3)} kWh` : '—'}/>
|
||
<ElektroResult label="Kosten/Tag" value={kostenTag!=null ? `${kostenTag.toFixed(3)} €` : '—'}/>
|
||
<ElektroResult label="kWh/Monat" value={kwhMonat!=null ? `${kwhMonat.toFixed(2)} kWh` : '—'}/>
|
||
<ElektroResult label="Kosten/Monat" value={kostenMonat!=null ? `${kostenMonat.toFixed(2)} €`: '—'} highlight={kostenMonat!=null}/>
|
||
<ElektroResult label="kWh/Jahr" value={kwhJahr!=null ? `${kwhJahr.toFixed(1)} kWh` : '—'}/>
|
||
<ElektroResult label="Kosten/Jahr" value={kostenJahr!=null ? `${kostenJahr.toFixed(2)} €` : '—'} highlight={kostenJahr!=null}/>
|
||
</ElektroSection>
|
||
|
||
{/* Widerstände */}
|
||
<ElektroSection title="🔗 Widerstandsschaltung">
|
||
<div style={{ marginBottom:8 }}>
|
||
<div style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:9, marginBottom:5 }}>SERIE (R = R1+R2+…)</div>
|
||
<div style={{ display:'flex', gap:6, flexWrap:'wrap', marginBottom:8 }}>
|
||
{['rs1','rs2','rs3','rs4'].map(k=><ElektroInp vals={vals} setVals={setVals} key={k} k={k} label={`R${k.slice(2)}`} unit="Ω"/>)}
|
||
</div>
|
||
<ElektroResult label="Gesamtwiderstand (Serie)" value={r_ser.length>=2 ? fmt(R_ser,'Ω') : '—'} highlight={r_ser.length>=2}/>
|
||
</div>
|
||
<div>
|
||
<div style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:9, marginBottom:5 }}>PARALLEL (1/R = 1/R1+1/R2+…)</div>
|
||
<div style={{ display:'flex', gap:6, flexWrap:'wrap', marginBottom:8 }}>
|
||
{['rp1','rp2','rp3','rp4'].map(k=><ElektroInp vals={vals} setVals={setVals} key={k} k={k} label={`R${k.slice(2)}`} unit="Ω"/>)}
|
||
</div>
|
||
<ElektroResult label="Gesamtwiderstand (Parallel)" value={r_par.length>=2 ? fmt(R_par,'Ω') : '—'} highlight={r_par.length>=2}/>
|
||
</div>
|
||
</ElektroSection>
|
||
|
||
{/* Kondensator */}
|
||
<ElektroSection title="⚡ Kondensator I = C · (ΔU/Δt)">
|
||
<div style={{ display:'flex', gap:8, flexWrap:'wrap', marginBottom:10 }}>
|
||
<ElektroInp vals={vals} setVals={setVals} k="cap" label="Kapazität" unit="F" placeholder="0.001"/>
|
||
<ElektroInp vals={vals} setVals={setVals} k="uc" label="Spannung" unit="V" placeholder="230"/>
|
||
<ElektroInp vals={vals} setVals={setVals} k="tc" label="Zeit Δt" unit="s" placeholder="0.02"/>
|
||
</div>
|
||
<ElektroResult label="I – Ladestrom" value={fmt(Ic,'A')} highlight={Ic!=null}/>
|
||
</ElektroSection>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Reset */}
|
||
<button onClick={()=>setVals({})}
|
||
style={{ ...S.btn('#ff6b9d',true), marginTop:8, fontSize:11, padding:'6px 16px' }}>
|
||
✕ Alle Felder leeren
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────── Datetime ──
|
||
const MOSES_EPOCH = new Date('1970-01-01T00:00:00Z'); // Moseszeit = Unix time (Sekunden)
|
||
|
||
function pad2(n){ return String(n).padStart(2,'0'); }
|
||
|
||
function toLocalISO(d){
|
||
return `${d.getFullYear()}-${pad2(d.getMonth()+1)}-${pad2(d.getDate())}T${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
|
||
}
|
||
|
||
function formatDE(d){
|
||
return `${pad2(d.getDate())}.${pad2(d.getMonth()+1)}.${d.getFullYear()} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
|
||
}
|
||
|
||
function DtRow({ label, value, muted }) {
|
||
return (
|
||
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',
|
||
padding:'7px 10px',borderRadius:7,background:'rgba(255,255,255,0.03)',marginBottom:4}}>
|
||
<span style={{color:'rgba(255,255,255,0.55)',fontFamily:'monospace',fontSize:11}}>{label}</span>
|
||
<code style={{color: muted ? 'rgba(255,255,255,0.55)' : '#4ecdc4',fontFamily:"'Courier New',monospace",fontSize:13,fontWeight:700}}>{value}</code>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function DatetimeRechner({ mobile }) {
|
||
const now = new Date();
|
||
|
||
// ── Tab 1: Umrechner ──
|
||
const [conv, setConv] = useState({ unix:'', local:'', de:'' });
|
||
const [convResult, setConvResult] = useState(null);
|
||
|
||
function convFromUnix(val) {
|
||
const n = parseInt(val);
|
||
if (isNaN(n)) { setConvResult(null); return; }
|
||
const d = new Date(n * 1000);
|
||
setConvResult({
|
||
de: formatDE(d),
|
||
local: toLocalISO(d),
|
||
unix: String(n),
|
||
wday: WEEKDAYS[d.getDay()],
|
||
kw: getKW(d),
|
||
});
|
||
}
|
||
|
||
function convFromLocal(val) {
|
||
if (!val) { setConvResult(null); return; }
|
||
const d = new Date(val);
|
||
if (isNaN(d)) { setConvResult(null); return; }
|
||
setConvResult({
|
||
de: formatDE(d),
|
||
local: toLocalISO(d),
|
||
unix: String(Math.floor(d.getTime()/1000)),
|
||
wday: WEEKDAYS[d.getDay()],
|
||
kw: getKW(d),
|
||
});
|
||
}
|
||
|
||
function getKW(d) {
|
||
const tmp = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
|
||
tmp.setUTCDate(tmp.getUTCDate() + 4 - (tmp.getUTCDay()||7));
|
||
const y = new Date(Date.UTC(tmp.getUTCFullYear(),0,1));
|
||
return Math.ceil((((tmp-y)/86400000)+1)/7);
|
||
}
|
||
|
||
// ── Tab 2: Geburtstag ──
|
||
const [bday, setBday] = useState('');
|
||
const [bdayResult, setBdayResult] = useState(null);
|
||
|
||
function calcBday(val) {
|
||
setBday(val);
|
||
if (!val) { setBdayResult(null); return; }
|
||
const birth = new Date(val);
|
||
if (isNaN(birth)) { setBdayResult(null); return; }
|
||
const today = new Date();
|
||
let age = today.getFullYear() - birth.getFullYear();
|
||
const hadBday = today.getMonth() > birth.getMonth() ||
|
||
(today.getMonth() === birth.getMonth() && today.getDate() >= birth.getDate());
|
||
if (!hadBday) age--;
|
||
|
||
const nextBday = new Date(today.getFullYear(), birth.getMonth(), birth.getDate());
|
||
if (nextBday <= today) nextBday.setFullYear(today.getFullYear()+1);
|
||
const diffMs = nextBday - today;
|
||
const diffD = Math.ceil(diffMs / 86400000);
|
||
const totalD = Math.floor((today - birth) / 86400000);
|
||
|
||
setBdayResult({ age, diffD, totalD, wday: WEEKDAYS[birth.getDay()],
|
||
geburtstag: `${pad2(birth.getDate())}.${pad2(birth.getMonth()+1)}.${birth.getFullYear()}`,
|
||
nextDate: `${pad2(nextBday.getDate())}.${pad2(nextBday.getMonth()+1)}.${nextBday.getFullYear()}`,
|
||
});
|
||
}
|
||
|
||
// ── Tab 3: Zeitdifferenz ──
|
||
const [dtA, setDtA] = useState('');
|
||
const [dtB, setDtB] = useState('');
|
||
const [diffResult, setDiffResult] = useState(null);
|
||
|
||
function calcDiff(a, b) {
|
||
if (!a || !b) { setDiffResult(null); return; }
|
||
const dA = new Date(a), dB = new Date(b);
|
||
if (isNaN(dA)||isNaN(dB)) { setDiffResult(null); return; }
|
||
const ms = Math.abs(dB - dA);
|
||
const s = Math.floor(ms/1000);
|
||
const m = Math.floor(s/60);
|
||
const h = Math.floor(m/60);
|
||
const d = Math.floor(h/24);
|
||
const weeks = Math.floor(d/7);
|
||
const months = Math.abs(dB.getMonth()-dA.getMonth() + (dB.getFullYear()-dA.getFullYear())*12);
|
||
setDiffResult({ ms, s, m, h, d, weeks, months });
|
||
}
|
||
|
||
const inp = { ...S.inp, fontSize:13, padding:'7px 9px', width:'100%', boxSizing:'border-box' };
|
||
const lbl = { color:'rgba(255,255,255,0.55)', fontFamily:'monospace', fontSize:10, marginBottom:4 };
|
||
|
||
const [tab, setTab] = useState('conv');
|
||
const tabs = [{id:'conv',label:'⇄ Umrechnen'},{id:'bday',label:'🎂 Geburtstag'},{id:'diff',label:'⏱ Differenz'}];
|
||
|
||
return (
|
||
<div>
|
||
<div style={{display:'flex',gap:6,marginBottom:18,flexWrap:'wrap'}}>
|
||
{tabs.map(t=>(
|
||
<button key={t.id} onClick={()=>setTab(t.id)} style={{
|
||
padding:'6px 16px',borderRadius:20,fontFamily:'monospace',fontSize:11,cursor:'pointer',
|
||
background:tab===t.id?'#4ecdc4':'rgba(255,255,255,0.05)',
|
||
color: tab===t.id?'#0d0d0f':'rgba(255,255,255,0.65)',
|
||
border: tab===t.id?'none':'1px solid rgba(255,255,255,0.12)',
|
||
fontWeight: tab===t.id?700:400}}>{t.label}</button>
|
||
))}
|
||
</div>
|
||
|
||
{tab==='conv' && (
|
||
<div>
|
||
<div style={{display:'grid',gridTemplateColumns:mobile?'1fr':'1fr 1fr',gap:12,marginBottom:16}}>
|
||
<div>
|
||
<div style={lbl}>Unix / Moseszeit (Sekunden)</div>
|
||
<input style={inp} type="number" placeholder="z.B. 1685554093"
|
||
value={conv.unix} onChange={e=>{ setConv(p=>({...p,unix:e.target.value})); convFromUnix(e.target.value); }}/>
|
||
</div>
|
||
<div>
|
||
<div style={lbl}>Datum & Uhrzeit</div>
|
||
<input style={inp} type="datetime-local"
|
||
value={conv.local} onChange={e=>{ setConv(p=>({...p,local:e.target.value})); convFromLocal(e.target.value); }}/>
|
||
</div>
|
||
</div>
|
||
<button style={{...S.btn('#4ecdc4',false), fontSize:11, padding:'6px 14px', marginBottom:16}}
|
||
onClick={()=>{ const d=new Date(); const v=String(Math.floor(d.getTime()/1000)); setConv({unix:v,local:toLocalISO(d),de:''}); convFromUnix(v); }}>
|
||
Jetzt verwenden
|
||
</button>
|
||
{convResult && (
|
||
<div>
|
||
<DtRow label="Deutsch (DE)" value={convResult.de}/>
|
||
<DtRow label="ISO lokal" value={convResult.local}/>
|
||
<DtRow label="Unix / Moseszeit" value={convResult.unix}/>
|
||
<DtRow label="Wochentag" value={convResult.wday}/>
|
||
<DtRow label="Kalenderwoche" value={`KW ${convResult.kw}`} muted/>
|
||
</div>
|
||
)}
|
||
{!convResult && (
|
||
<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:11,padding:'10px 0'}}>
|
||
Unix-Timestamp oder Datum eingeben → alle Formate werden berechnet
|
||
</div>
|
||
)}
|
||
<div style={{marginTop:14,padding:'10px 12px',borderRadius:8,background:'rgba(255,255,255,0.03)',
|
||
border:'1px solid rgba(255,255,255,0.07)'}}>
|
||
<div style={{color:'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:9,marginBottom:6}}>BEISPIEL</div>
|
||
<div style={{color:'rgba(255,255,255,0.6)',fontFamily:'monospace',fontSize:11}}>
|
||
Moseszeit <code style={{color:'#4ecdc4'}}>1685554093</code> = <code style={{color:'#4ecdc4'}}>31.05.2026 17:28:13</code>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{tab==='bday' && (
|
||
<div>
|
||
<div style={{marginBottom:16}}>
|
||
<div style={lbl}>Geburtsdatum</div>
|
||
<input style={{...inp, maxWidth:220}} type="date" value={bday}
|
||
onChange={e=>calcBday(e.target.value)}/>
|
||
</div>
|
||
{bdayResult && (
|
||
<div>
|
||
<DtRow label="Geburtsdatum" value={`${bdayResult.geburtstag} (${bdayResult.wday})`}/>
|
||
<DtRow label="Alter" value={`${bdayResult.age} Jahre`}/>
|
||
<DtRow label="Nächster Geburtstag" value={bdayResult.nextDate}/>
|
||
<DtRow label="Tage bis Geburtstag" value={`${bdayResult.diffD} Tage`}/>
|
||
<DtRow label="Gelebte Tage" value={`${bdayResult.totalD.toLocaleString('de-DE')} Tage`} muted/>
|
||
</div>
|
||
)}
|
||
{!bdayResult && (
|
||
<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:11}}>
|
||
Geburtsdatum eingeben
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{tab==='diff' && (
|
||
<div>
|
||
<div style={{display:'grid',gridTemplateColumns:mobile?'1fr':'1fr 1fr',gap:12,marginBottom:16}}>
|
||
<div>
|
||
<div style={lbl}>Von</div>
|
||
<input style={inp} type="datetime-local" value={dtA}
|
||
onChange={e=>{ setDtA(e.target.value); calcDiff(e.target.value, dtB); }}/>
|
||
</div>
|
||
<div>
|
||
<div style={lbl}>Bis</div>
|
||
<input style={inp} type="datetime-local" value={dtB}
|
||
onChange={e=>{ setDtB(e.target.value); calcDiff(dtA, e.target.value); }}/>
|
||
</div>
|
||
</div>
|
||
{diffResult && (
|
||
<div>
|
||
<DtRow label="Millisekunden" value={diffResult.ms.toLocaleString('de-DE')} muted/>
|
||
<DtRow label="Sekunden" value={diffResult.s.toLocaleString('de-DE')}/>
|
||
<DtRow label="Minuten" value={diffResult.m.toLocaleString('de-DE')}/>
|
||
<DtRow label="Stunden" value={diffResult.h.toLocaleString('de-DE')}/>
|
||
<DtRow label="Tage" value={diffResult.d.toLocaleString('de-DE')}/>
|
||
<DtRow label="Wochen" value={`~${diffResult.weeks}`} muted/>
|
||
<DtRow label="Monate" value={`~${diffResult.months}`} muted/>
|
||
</div>
|
||
)}
|
||
{!diffResult && (
|
||
<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:11}}>
|
||
Zwei Zeitpunkte eingeben
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const TOOL_DEFS = [
|
||
{id:'cron', label:'⏱ Crontab', desc:'Crontab erklären und erstellen', ready:true,
|
||
tabs:[{id:'explain',label:'Erklären'},{id:'build',label:'Erstellen'}],
|
||
components:{explain:CronExplainer,build:CronBuilder}},
|
||
{id:'elektro', label:'⚡ Elektro', desc:'Ohm, Leistung, Kosten, Widerstände', ready:true, component:ElektroRechner},
|
||
{id:'json', label:'{ } JSON', desc:'JSON formatieren', ready:false},
|
||
{id:'base64', label:'🔐 Base64', desc:'Text ↔ Base64', ready:false},
|
||
{id:'regex', label:'🔍 Regex', desc:'Regex testen', ready:false},
|
||
{id:'diff', label:'± Text Diff', desc:'Texte vergleichen', ready:false},
|
||
{id:'jwt', label:'🔑 JWT', desc:'JWT dekodieren', ready:false},
|
||
{id:'datetime', label:'📅 Datetime', desc:'Zeiten umrechnen · Geburtstag · Differenz', ready:true, component:DatetimeRechner},
|
||
{id:'color', label:'🎨 Farben', desc:'HEX/RGB/HSL umrechnen', ready:false},
|
||
];
|
||
|
||
export default function DevTools({ toast, mobile }) {
|
||
const [activeTool, setActiveTool] = useState('cron');
|
||
const [activeTab, setActiveTab] = useState('explain');
|
||
const active = TOOL_DEFS.find(t=>t.id===activeTool);
|
||
const ActiveComp = active?.tabs ? active.components[activeTab] : active?.component;
|
||
|
||
return (
|
||
<div style={{padding:mobile?'14px 14px 90px':'36px 44px',maxWidth:860}}>
|
||
<div style={{display:'flex',alignItems:'center',gap:12,marginBottom:20,flexWrap:'wrap'}}>
|
||
<h1 style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:mobile?17:22,margin:0}}>Dev-Tools</h1>
|
||
<span style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:10}}>Konverter · Formatter · Helfer</span>
|
||
</div>
|
||
<div style={{display:'flex',gap:7,flexWrap:'wrap',marginBottom:20}}>
|
||
{TOOL_DEFS.map(t=>(
|
||
<button key={t.id} title={t.desc}
|
||
onClick={()=>{ if(t.ready){setActiveTool(t.id);setActiveTab(t.tabs?.[0]?.id||'');} else toast('Kommt bald 🚧'); }}
|
||
style={{padding:'8px 14px',borderRadius:10,fontFamily:'monospace',fontSize:11,cursor:t.ready?'pointer':'default',
|
||
background:activeTool===t.id?'#4ecdc4':t.ready?'rgba(255,255,255,0.06)':'rgba(255,255,255,0.02)',
|
||
color: activeTool===t.id?'#0d0d0f':t.ready?'rgba(255,255,255,0.65)':'rgba(255,255,255,0.4)',
|
||
border: activeTool===t.id?'none':t.ready?'1px solid rgba(255,255,255,0.1)':'1px solid rgba(255,255,255,0.05)',
|
||
fontWeight: activeTool===t.id?700:400}}>
|
||
{t.label}{!t.ready&&<span style={{marginLeft:4,fontSize:8,opacity:0.4}}>bald</span>}
|
||
</button>
|
||
))}
|
||
</div>
|
||
<div style={{...S.card}}>
|
||
<div style={{display:'flex',alignItems:'center',gap:10,marginBottom:14,flexWrap:'wrap'}}>
|
||
<div style={{...S.head,marginBottom:0}}>{active?.label}</div>
|
||
<span style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:10}}>{active?.desc}</span>
|
||
</div>
|
||
{active?.tabs&&(
|
||
<div style={{display:'flex',gap:6,marginBottom:16}}>
|
||
{active.tabs.map(tab=>(
|
||
<button key={tab.id} onClick={()=>setActiveTab(tab.id)} style={{
|
||
padding:'6px 18px',borderRadius:20,fontFamily:'monospace',fontSize:11,cursor:'pointer',
|
||
background:activeTab===tab.id?'#4ecdc4':'rgba(255,255,255,0.05)',
|
||
color: activeTab===tab.id?'#0d0d0f':'rgba(255,255,255,0.5)',
|
||
border: activeTab===tab.id?'none':'1px solid rgba(255,255,255,0.1)',
|
||
fontWeight: activeTab===tab.id?700:400}}>
|
||
{tab.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
{ActiveComp&&<ActiveComp toast={toast} mobile={mobile}/>}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|