+
Strompreis [€/kWh]
setPreis(e.target.value)}
@@ -477,6 +477,236 @@ function ElektroRechner({ mobile }) {
);
}
+// ─────────────────────────────────────────────────────────────── 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'}],
@@ -487,7 +717,7 @@ const TOOL_DEFS = [
{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:'timestamp', label:'🕐 Timestamp', desc:'Unix Timestamps umrechnen', 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},
];
@@ -501,7 +731,7 @@ export default function DevTools({ toast, mobile }) {
Dev-Tools
- Konverter · Formatter · Helfer
+ Konverter · Formatter · Helfer
{TOOL_DEFS.map(t=>(
@@ -509,7 +739,7 @@ export default function DevTools({ toast, mobile }) {
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.2)',
+ 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&&
bald}
@@ -519,7 +749,7 @@ export default function DevTools({ toast, mobile }) {
{active?.label}
-
{active?.desc}
+
{active?.desc}
{active?.tabs&&(
diff --git a/frontend/src/tools/skizze.jsx b/frontend/src/tools/skizze.jsx
index b4cb76d..a760887 100644
--- a/frontend/src/tools/skizze.jsx
+++ b/frontend/src/tools/skizze.jsx
@@ -158,7 +158,7 @@ function Shape({ s, selected, onClick }) {
// ── Properties Panel ──────────────────────────────────────────────────────────
function PropsPanel({ shape, onChange, onDelete, vars, scale, onScaleChange }) {
if (!shape) return (
-
+
Form auswählen oder zeichnen.
V=Auswählen R=Rect L=Linie C=Kreis
@@ -207,7 +207,7 @@ function PropsPanel({ shape, onChange, onDelete, vars, scale, onScaleChange }) {
onScaleChange(parseFloat(e.target.value)||1)}
step={0.1} min={0.1} style={{...S.inp,flex:1,fontSize:12,padding:'5px 8px'}}/>
- mm/px
+ mm/px
@@ -386,7 +386,7 @@ export default function Skizze({ toast, mobile }) {
border: tool===t.id?'none':'1px solid rgba(255,255,255,0.1)',
}}>{t.label}
))}
-
+
{TOOLS.find(t=>t.id===tool)?.title}
@@ -433,7 +433,7 @@ export default function Skizze({ toast, mobile }) {
{Object.entries(vars).map(([k,v])=>(
{setFormula(f=>f+k);formulaRef.current?.focus();}}>
{k}
- ={typeof v==='number'?v.toFixed(2):v}
+ ={typeof v==='number'?v.toFixed(2):v}
))}
@@ -463,7 +463,7 @@ export default function Skizze({ toast, mobile }) {
{/* Ergebnisse */}
{results.length===0&&Object.keys(vars).length===0&&(
-
+
Formen zeichnen → benennen → hier rechnen
)}
@@ -482,7 +482,7 @@ export default function Skizze({ toast, mobile }) {
+ color:'rgba(255,255,255,0.5)',fontSize:12,padding:'0 2px',lineHeight:1}}>✕
))}