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 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 (
setInput(e.target.value)} placeholder="z.B. 0 8 * * 1" autoCapitalize="none" style={{...S.inp,fontFamily:"'Courier New',monospace",fontSize:16}}/>
{EXAMPLES.map(([ex,l])=>( ))}
{result && (result.ok ? (
BEDEUTET
{result.human}
{result.fields && (
{[['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 (
{lbl}
{raw}
{fd?.type==='all'?'jede/r':(describeField(fd,fNames)||'–')}
); })}
)} {runs.length>0&&(
NÄCHSTE AUSFÜHRUNGEN
{runs.map((d,i)=>(
{i+1}. {fmtDt(d)}
))}
)}
) : (
⚠ {result.error}
))}
); } // ── 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}) =>
{c}
; const Sel = ({value,onChange,children}) => ( ); return (
{PRESETS.map(p=>( ))}
{[5,10,15,20,30].map(n=>)} {Array.from({length:60},(_,i)=>)}
{Array.from({length:24},(_,i)=>)}
{WEEKDAYS.map((d,i)=>)}
{Array.from({length:31},(_,i)=>)}
{MONTHS.map((m,i)=>)}
{cron}
{result?.ok&&
{result.human}
} {runs.length>0&&(
{runs.slice(0,3).map((d,i)=>( {fmtDt(d)} ))}
)}
); } // ── Tool-Definitionen ───────────────────────────────────────────────────────── // ── Elektro-Hilfskomponenten (AUSSERHALB damit keine Remounts) ──────────────── function ElektroInp({ vals, setVals, k, label, unit, placeholder }) { return (
{label} [{unit}]
setVals(p=>({...p,[k]:e.target.value}))} placeholder={placeholder||''} style={{ ...S.inp, fontSize:13, padding:'7px 9px', width:'100%', boxSizing:'border-box' }}/>
); } function ElektroResult({ label, value, highlight }) { return (
{label} {value}
); } function ElektroSection({ title, children }) { return (
{title}
{children}
); } // ── 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 (
{/* Info */}
Felder ausfüllen → Ergebnis erscheint automatisch · Mindestens 2 bekannte Größen eingeben
{/* Ohmsches Gesetz */}
{/* Leistung */}
{leistRes ? <> :
Mindestens 2 Werte eingeben
}
{/* Wirkungsgrad */}
{/* Stromkosten */}
Strompreis [€/kWh]
setPreis(e.target.value)} step="0.01" min="0" style={{ ...S.inp, fontSize:13, padding:'7px 9px', width:'100%', boxSizing:'border-box' }}/>
{/* Widerstände */}
SERIE (R = R1+R2+…)
{['rs1','rs2','rs3','rs4'].map(k=>)}
=2 ? fmt(R_ser,'Ω') : '—'} highlight={r_ser.length>=2}/>
PARALLEL (1/R = 1/R1+1/R2+…)
{['rp1','rp2','rp3','rp4'].map(k=>)}
=2 ? fmt(R_par,'Ω') : '—'} highlight={r_par.length>=2}/>
{/* Kondensator */}
{/* Reset */}
); } // ─────────────────────────────────────────────────────────────── 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 (
{label} {value}
); } 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 (
{tabs.map(t=>( ))}
{tab==='conv' && (
Unix / Moseszeit (Sekunden)
{ setConv(p=>({...p,unix:e.target.value})); convFromUnix(e.target.value); }}/>
Datum & Uhrzeit
{ setConv(p=>({...p,local:e.target.value})); convFromLocal(e.target.value); }}/>
{convResult && (
)} {!convResult && (
Unix-Timestamp oder Datum eingeben → alle Formate werden berechnet
)}
BEISPIEL
Moseszeit 1685554093 = 31.05.2026 17:28:13
)} {tab==='bday' && (
Geburtsdatum
calcBday(e.target.value)}/>
{bdayResult && (
)} {!bdayResult && (
Geburtsdatum eingeben
)}
)} {tab==='diff' && (
Von
{ setDtA(e.target.value); calcDiff(e.target.value, dtB); }}/>
Bis
{ setDtB(e.target.value); calcDiff(dtA, e.target.value); }}/>
{diffResult && (
)} {!diffResult && (
Zwei Zeitpunkte eingeben
)}
)}
); } 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 (

Dev-Tools

Konverter · Formatter · Helfer
{TOOL_DEFS.map(t=>( ))}
{active?.label}
{active?.desc}
{active?.tabs&&(
{active.tabs.map(tab=>( ))}
)} {ActiveComp&&}
); }