PWA: SW network-first für HTML, sofortiger Reload bei Update; DevTools: JSON Viewer+Builder
This commit is contained in:
@@ -1,24 +1,56 @@
|
|||||||
// Service Worker – erkennt neue Builds und lädt automatisch neu
|
// Service Worker – Network-first für HTML, kein Cache für Assets
|
||||||
// BUILD_TIME wird beim Deploy vom Backend via /api/build-time geliefert
|
// Stellt sicher dass nach einem Deploy immer der neue Stand geladen wird
|
||||||
|
|
||||||
const CACHE_NAME = 'dickendock-v1';
|
const CACHE_NAME = 'dickendock-v2';
|
||||||
|
|
||||||
self.addEventListener('install', () => {
|
self.addEventListener('install', () => {
|
||||||
self.skipWaiting(); // sofort aktivieren
|
self.skipWaiting(); // sofort aktivieren, nicht auf Tab-Schließen warten
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener('activate', e => {
|
self.addEventListener('activate', e => {
|
||||||
// Alte Caches löschen
|
// Alle alten Caches löschen
|
||||||
e.waitUntil(
|
e.waitUntil(
|
||||||
caches.keys()
|
caches.keys()
|
||||||
.then(keys => Promise.all(keys.map(k => caches.delete(k))))
|
.then(keys => Promise.all(keys.map(k => caches.delete(k))))
|
||||||
.then(() => self.clients.claim())
|
.then(() => self.clients.claim()) // alle offenen Tabs übernehmen
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Alle Requests direkt ans Netzwerk – kein Caching
|
|
||||||
self.addEventListener('fetch', e => {
|
self.addEventListener('fetch', e => {
|
||||||
|
const url = new URL(e.request.url);
|
||||||
|
|
||||||
|
// API-Requests immer direkt ans Netzwerk, nie cachen
|
||||||
|
if (url.pathname.startsWith('/api/')) {
|
||||||
e.respondWith(fetch(e.request));
|
e.respondWith(fetch(e.request));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// index.html und Navigation: Network-first, kein Cache
|
||||||
|
// Damit nach einem Deploy immer die neue Version geladen wird
|
||||||
|
if (e.request.mode === 'navigate' ||
|
||||||
|
url.pathname === '/' ||
|
||||||
|
url.pathname === '/index.html') {
|
||||||
|
e.respondWith(
|
||||||
|
fetch(e.request, { cache: 'no-store' })
|
||||||
|
.catch(() => caches.match('/index.html')) // Fallback wenn offline
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// JS/CSS/Assets: Network-first mit Cache-Fallback
|
||||||
|
// Vite baut mit Content-Hash im Dateinamen → neue Version = neue URL
|
||||||
|
e.respondWith(
|
||||||
|
fetch(e.request)
|
||||||
|
.then(response => {
|
||||||
|
// Erfolgreiche Antwort kurz cachen als Offline-Fallback
|
||||||
|
if (response.ok) {
|
||||||
|
const clone = response.clone();
|
||||||
|
caches.open(CACHE_NAME).then(cache => cache.put(e.request, clone));
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch(() => caches.match(e.request))
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auf Update-Nachricht vom Frontend reagieren
|
// Auf Update-Nachricht vom Frontend reagieren
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react'
|
|||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import App from './App.jsx'
|
import App from './App.jsx'
|
||||||
|
|
||||||
// ── Auto-Reload bei neuem Build – nur beim Seitenwechsel ─────────────────────
|
// ── Auto-Reload bei neuem Build ───────────────────────────────────────────────
|
||||||
const STORED_KEY = 'dd_build_time';
|
const STORED_KEY = 'dd_build_time';
|
||||||
let newBuildAvailable = false;
|
let newBuildAvailable = false;
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ async function checkBuildTime() {
|
|||||||
localStorage.setItem(STORED_KEY, buildTime);
|
localStorage.setItem(STORED_KEY, buildTime);
|
||||||
} else if (stored !== buildTime) {
|
} else if (stored !== buildTime) {
|
||||||
localStorage.setItem(STORED_KEY, buildTime);
|
localStorage.setItem(STORED_KEY, buildTime);
|
||||||
newBuildAvailable = true; // merken, aber noch nicht reloaden
|
newBuildAvailable = true;
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
@@ -32,17 +32,25 @@ export function reloadIfNewBuild() {
|
|||||||
// ── Service Worker ────────────────────────────────────────────────────────────
|
// ── Service Worker ────────────────────────────────────────────────────────────
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
navigator.serviceWorker.register('/sw.js').then(reg => {
|
navigator.serviceWorker.register('/sw.js').then(reg => {
|
||||||
if (reg.waiting) reg.waiting.postMessage('SKIP_WAITING');
|
// Falls schon ein wartender SW da ist → sofort aktivieren
|
||||||
|
if (reg.waiting) {
|
||||||
|
reg.waiting.postMessage('SKIP_WAITING');
|
||||||
|
}
|
||||||
|
|
||||||
reg.addEventListener('updatefound', () => {
|
reg.addEventListener('updatefound', () => {
|
||||||
const nw = reg.installing;
|
const nw = reg.installing;
|
||||||
nw?.addEventListener('statechange', () => {
|
nw?.addEventListener('statechange', () => {
|
||||||
if (nw.state === 'installed' && navigator.serviceWorker.controller) {
|
if (nw.state === 'installed') {
|
||||||
|
// Neuer SW installiert → sofort aktivieren
|
||||||
nw.postMessage('SKIP_WAITING');
|
nw.postMessage('SKIP_WAITING');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Wenn SW-Controller gewechselt hat → sofort neu laden
|
||||||
|
// (nicht erst beim nächsten Seitenwechsel warten)
|
||||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||||
newBuildAvailable = true; // SW-Update → beim nächsten Wechsel laden
|
window.location.reload();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1896,12 +1896,319 @@ function RegexBuilder({ toast, mobile }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────── JSON Tool ──
|
||||||
|
|
||||||
|
// ── JSON Viewer: rekursiver Baum mit auf/zuklappen ──
|
||||||
|
function JsonNode({ data, depth, keyName }) {
|
||||||
|
const [open, setOpen] = useState(depth < 2);
|
||||||
|
const indent = depth * 16;
|
||||||
|
const isObj = data !== null && typeof data === 'object' && !Array.isArray(data);
|
||||||
|
const isArr = Array.isArray(data);
|
||||||
|
const isPrim = !isObj && !isArr;
|
||||||
|
|
||||||
|
const typeColor = {
|
||||||
|
string: '#a8f0eb',
|
||||||
|
number: '#ffe66d',
|
||||||
|
boolean: '#ff6b9d',
|
||||||
|
null: 'rgba(255,255,255,0.3)',
|
||||||
|
};
|
||||||
|
|
||||||
|
function valColor(v) {
|
||||||
|
if (v === null) return typeColor.null;
|
||||||
|
return typeColor[typeof v] || '#fff';
|
||||||
|
}
|
||||||
|
|
||||||
|
function valLabel(v) {
|
||||||
|
if (v === null) return 'null';
|
||||||
|
if (typeof v === 'string') return `"${v}"`;
|
||||||
|
return String(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
function typeTag(v) {
|
||||||
|
if (v === null) return 'null';
|
||||||
|
if (Array.isArray(v)) return `Array[${v.length}]`;
|
||||||
|
if (typeof v === 'object') return `Object{${Object.keys(v).length}}`;
|
||||||
|
return typeof v;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyStyle = {color:'#7eb8ff', fontFamily:"'Courier New',monospace", fontSize:12};
|
||||||
|
const lineBase = {display:'flex', alignItems:'baseline', gap:4, padding:'1px 0',
|
||||||
|
marginLeft:indent, lineHeight:1.7};
|
||||||
|
|
||||||
|
if (isPrim) {
|
||||||
|
return (
|
||||||
|
<div style={lineBase}>
|
||||||
|
{keyName !== undefined && <span style={keyStyle}>"{keyName}"</span>}
|
||||||
|
{keyName !== undefined && <span style={{color:'rgba(255,255,255,0.3)'}}>:</span>}
|
||||||
|
<span style={{color:valColor(data), fontFamily:"'Courier New',monospace", fontSize:12}}>
|
||||||
|
{valLabel(data)}
|
||||||
|
</span>
|
||||||
|
<span style={{color:'rgba(255,255,255,0.2)', fontSize:10, marginLeft:4}}>{typeof data}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = isArr ? data.map((v,i)=>[i,v]) : Object.entries(data);
|
||||||
|
const bracket = isArr ? ['[',']'] : ['{','}'];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{...lineBase, cursor:'pointer', userSelect:'none'}} onClick={()=>setOpen(o=>!o)}>
|
||||||
|
<span style={{color:'rgba(255,255,255,0.3)', fontSize:11, minWidth:14}}>
|
||||||
|
{open ? '▾' : '▸'}
|
||||||
|
</span>
|
||||||
|
{keyName !== undefined && <span style={keyStyle}>"{keyName}"</span>}
|
||||||
|
{keyName !== undefined && <span style={{color:'rgba(255,255,255,0.3)'}}>:</span>}
|
||||||
|
<span style={{color:'rgba(255,255,255,0.5)', fontFamily:"'Courier New',monospace", fontSize:12}}>
|
||||||
|
{bracket[0]}
|
||||||
|
</span>
|
||||||
|
{!open && (
|
||||||
|
<span style={{color:'rgba(255,255,255,0.25)', fontFamily:"'Courier New',monospace", fontSize:11}}>
|
||||||
|
{isArr ? `${entries.length} Einträge` : `${entries.length} Felder`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{!open && <span style={{color:'rgba(255,255,255,0.5)', fontFamily:"'Courier New',monospace", fontSize:12}}>{bracket[1]}</span>}
|
||||||
|
<span style={{color:'rgba(255,255,255,0.2)', fontSize:10, marginLeft:4}}>
|
||||||
|
{typeTag(data)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{open && (
|
||||||
|
<>
|
||||||
|
{entries.map(([k,v]) => (
|
||||||
|
<JsonNode key={k} data={v} depth={depth+1} keyName={k}/>
|
||||||
|
))}
|
||||||
|
<div style={{marginLeft:indent, color:'rgba(255,255,255,0.5)', fontFamily:"'Courier New',monospace", fontSize:12}}>
|
||||||
|
{bracket[1]}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── JSON Builder: Baumstruktur erstellen ──
|
||||||
|
let _nodeId = 1;
|
||||||
|
function newNode(type='string', key='', value='') {
|
||||||
|
return { id: _nodeId++, type, key, value, children:[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
function BuilderNode({ node, depth, onChange, onDelete, onAddSibling }) {
|
||||||
|
const indent = depth * 20;
|
||||||
|
const canHaveChildren = node.type==='object' || node.type==='array';
|
||||||
|
|
||||||
|
function addChild(type) {
|
||||||
|
const child = newNode(type);
|
||||||
|
onChange({...node, children:[...node.children, child]});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateChild(updated) {
|
||||||
|
onChange({...node, children: node.children.map(c => c.id===updated.id ? updated : c)});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteChild(id) {
|
||||||
|
onChange({...node, children: node.children.filter(c => c.id!==id)});
|
||||||
|
}
|
||||||
|
|
||||||
|
const inp = {...S.inp, fontSize:12, padding:'4px 7px', boxSizing:'border-box'};
|
||||||
|
const typeColors = {object:'#4ecdc4', array:'#ffe66d', string:'#a8f0eb', number:'#ff6b9d', boolean:'#c3b1e1', null:'rgba(255,255,255,0.3)'};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{marginLeft:indent, marginBottom:3}}>
|
||||||
|
<div style={{display:'flex', alignItems:'center', gap:6, flexWrap:'wrap'}}>
|
||||||
|
{/* Key (only for object children) */}
|
||||||
|
{depth > 0 && node.type !== '__root' && (
|
||||||
|
<input style={{...inp, width:90, color:'#7eb8ff'}} placeholder='Key'
|
||||||
|
value={node.key} onChange={e=>onChange({...node, key:e.target.value})}/>
|
||||||
|
)}
|
||||||
|
{depth > 0 && <span style={{color:'rgba(255,255,255,0.25)', fontSize:11}}>:</span>}
|
||||||
|
{/* Type */}
|
||||||
|
<select style={{...inp, width:'auto', color: typeColors[node.type]||'#fff', cursor:'pointer'}}
|
||||||
|
value={node.type} onChange={e=>onChange({...node, type:e.target.value, children:[], value:''})}>
|
||||||
|
<option value="string">string</option>
|
||||||
|
<option value="number">number</option>
|
||||||
|
<option value="boolean">boolean</option>
|
||||||
|
<option value="null">null</option>
|
||||||
|
<option value="object">object {'{}'}</option>
|
||||||
|
<option value="array">array []</option>
|
||||||
|
</select>
|
||||||
|
{/* Value for primitives */}
|
||||||
|
{node.type==='string' && <input style={{...inp, flex:1, minWidth:80}} placeholder='"Wert"' value={node.value} onChange={e=>onChange({...node, value:e.target.value})}/>}
|
||||||
|
{node.type==='number' && <input style={{...inp, width:100}} type="number" placeholder='0' value={node.value} onChange={e=>onChange({...node, value:e.target.value})}/>}
|
||||||
|
{node.type==='boolean' && (
|
||||||
|
<select style={{...inp, width:'auto', cursor:'pointer'}} value={node.value} onChange={e=>onChange({...node, value:e.target.value})}>
|
||||||
|
<option value="true">true</option>
|
||||||
|
<option value="false">false</option>
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
{/* Add child button */}
|
||||||
|
{canHaveChildren && (
|
||||||
|
<select style={{...inp, width:'auto', cursor:'pointer', color:'#4ecdc4'}}
|
||||||
|
value="" onChange={e=>{ if(e.target.value) addChild(e.target.value); }}>
|
||||||
|
<option value="">+ Feld hinzufügen</option>
|
||||||
|
<option value="string">string</option>
|
||||||
|
<option value="number">number</option>
|
||||||
|
<option value="boolean">boolean</option>
|
||||||
|
<option value="null">null</option>
|
||||||
|
<option value="object">object</option>
|
||||||
|
<option value="array">array</option>
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
{/* Delete */}
|
||||||
|
{depth > 0 && (
|
||||||
|
<button onClick={onDelete} style={{background:'rgba(255,107,157,0.12)',border:'none',
|
||||||
|
borderRadius:5,color:'#ff6b9d',cursor:'pointer',padding:'3px 8px',fontSize:12}}>✕</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Children */}
|
||||||
|
{canHaveChildren && node.children.map(child=>(
|
||||||
|
<BuilderNode key={child.id} node={child} depth={depth+1}
|
||||||
|
onChange={updateChild} onDelete={()=>deleteChild(child.id)}/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nodeToJson(node) {
|
||||||
|
if (node.type==='object') {
|
||||||
|
const obj = {};
|
||||||
|
node.children.forEach(c => { obj[c.key || `field${c.id}`] = nodeToJson(c); });
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
if (node.type==='array') {
|
||||||
|
return node.children.map(c => nodeToJson(c));
|
||||||
|
}
|
||||||
|
if (node.type==='number') return isNaN(Number(node.value)) ? 0 : Number(node.value);
|
||||||
|
if (node.type==='boolean') return node.value !== 'false';
|
||||||
|
if (node.type==='null') return null;
|
||||||
|
return node.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function JsonTool({ toast, mobile }) {
|
||||||
|
const [tab, setTab] = useState('view');
|
||||||
|
// View tab
|
||||||
|
const [jsonInput, setJsonInput] = useState('');
|
||||||
|
const [parsed, setParsed] = useState(null);
|
||||||
|
const [parseErr, setParseErr] = useState('');
|
||||||
|
|
||||||
|
function parseJson(val) {
|
||||||
|
setJsonInput(val);
|
||||||
|
if (!val.trim()) { setParsed(null); setParseErr(''); return; }
|
||||||
|
try {
|
||||||
|
setParsed(JSON.parse(val));
|
||||||
|
setParseErr('');
|
||||||
|
} catch(e) {
|
||||||
|
setParsed(null);
|
||||||
|
setParseErr(e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatJson() {
|
||||||
|
try {
|
||||||
|
const p = JSON.parse(jsonInput);
|
||||||
|
setJsonInput(JSON.stringify(p, null, 2));
|
||||||
|
setParsed(p);
|
||||||
|
setParseErr('');
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build tab
|
||||||
|
const [root, setRoot] = useState(() => ({...newNode('object'), key:'root'}));
|
||||||
|
const generatedJson = JSON.stringify(nodeToJson(root), null, 2);
|
||||||
|
const [copiedJson, setCopiedJson] = useState(false);
|
||||||
|
|
||||||
|
async function copyJson(text) {
|
||||||
|
try { await navigator.clipboard.writeText(text); setCopiedJson(true); setTimeout(()=>setCopiedJson(false),1800); }
|
||||||
|
catch { toast('Kopieren fehlgeschlagen'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
const inp = {...S.inp, fontSize:13, padding:'7px 9px', boxSizing:'border-box'};
|
||||||
|
const tabs = [{id:'view',label:'👁 JSON ansehen'},{id:'build',label:'🔨 JSON bauen'}];
|
||||||
|
|
||||||
|
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==='view' && (
|
||||||
|
<div>
|
||||||
|
<div style={{marginBottom:10}}>
|
||||||
|
<div style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:10,marginBottom:5}}>JSON einfügen</div>
|
||||||
|
<textarea style={{...inp, width:'100%', minHeight:120, fontFamily:"'Courier New',monospace",
|
||||||
|
fontSize:12, resize:'vertical', lineHeight:1.5,
|
||||||
|
border: parseErr ? '1px solid rgba(255,107,157,0.5)' : undefined}}
|
||||||
|
placeholder='{"name":"Max","age":30}'
|
||||||
|
value={jsonInput} onChange={e=>parseJson(e.target.value)}/>
|
||||||
|
{!!parseErr && <div style={{color:'#ff6b9d',fontFamily:'monospace',fontSize:11,marginTop:4}}>⚠ {parseErr}</div>}
|
||||||
|
</div>
|
||||||
|
<div style={{display:'flex',gap:8,marginBottom:14}}>
|
||||||
|
{!!jsonInput && <button style={{...S.btn('#4ecdc4',false),fontSize:11,padding:'5px 14px'}} onClick={formatJson}>⟳ Formatieren</button>}
|
||||||
|
{!!jsonInput && <button style={{...S.btn('#ff6b9d',false),fontSize:11,padding:'5px 14px'}} onClick={()=>{setJsonInput('');setParsed(null);setParseErr('');}}>✕ Leeren</button>}
|
||||||
|
</div>
|
||||||
|
{parsed !== null && (
|
||||||
|
<div style={{borderRadius:9,background:'rgba(0,0,0,0.2)',border:'1px solid rgba(255,255,255,0.08)',
|
||||||
|
padding:'10px 14px',overflowX:'auto'}}>
|
||||||
|
<JsonNode data={parsed} depth={0}/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!jsonInput && (
|
||||||
|
<div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:11,padding:'16px 0',textAlign:'center'}}>
|
||||||
|
JSON oben einfügen → interaktiver Baum erscheint hier
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tab==='build' && (
|
||||||
|
<div>
|
||||||
|
<div style={{borderRadius:9,background:'rgba(255,255,255,0.03)',
|
||||||
|
border:'1px solid rgba(255,255,255,0.08)',padding:'12px 14px',marginBottom:14,
|
||||||
|
overflowX:'auto'}}>
|
||||||
|
<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:10,marginBottom:10}}>STRUKTUR AUFBAUEN</div>
|
||||||
|
<BuilderNode node={root} depth={0} onChange={setRoot} onDelete={()=>{}}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{borderRadius:9,background:'rgba(0,0,0,0.2)',
|
||||||
|
border:'1px solid rgba(255,255,255,0.08)',padding:'12px 14px'}}>
|
||||||
|
<div style={{display:'flex',alignItems:'center',gap:10,marginBottom:8}}>
|
||||||
|
<span style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:10}}>GENERIERTES JSON</span>
|
||||||
|
<button onClick={()=>copyJson(generatedJson)} style={{
|
||||||
|
background:'rgba(78,205,196,0.1)',border:'1px solid rgba(78,205,196,0.2)',
|
||||||
|
borderRadius:6,color:copiedJson?'#4ecdc4':'rgba(255,255,255,0.5)',
|
||||||
|
cursor:'pointer',padding:'2px 10px',fontSize:11}}>
|
||||||
|
{copiedJson?'✓ Kopiert':'⎘ Kopieren'}
|
||||||
|
</button>
|
||||||
|
<button onClick={()=>{setTab('view');parseJson(generatedJson);}} style={{
|
||||||
|
background:'rgba(255,255,255,0.05)',border:'1px solid rgba(255,255,255,0.1)',
|
||||||
|
borderRadius:6,color:'rgba(255,255,255,0.5)',cursor:'pointer',padding:'2px 10px',fontSize:11}}>
|
||||||
|
👁 Ansehen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<pre style={{margin:0,color:'rgba(255,255,255,0.8)',fontFamily:"'Courier New',monospace",
|
||||||
|
fontSize:12,lineHeight:1.6,overflowX:'auto',whiteSpace:'pre-wrap',wordBreak:'break-all'}}>
|
||||||
|
{generatedJson}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const TOOL_DEFS = [
|
const TOOL_DEFS = [
|
||||||
{id:'cron', label:'⏱ Crontab', desc:'Crontab erklären und erstellen', ready:true,
|
{id:'cron', label:'⏱ Crontab', desc:'Crontab erklären und erstellen', ready:true,
|
||||||
tabs:[{id:'explain',label:'Erklären'},{id:'build',label:'Erstellen'}],
|
tabs:[{id:'explain',label:'Erklären'},{id:'build',label:'Erstellen'}],
|
||||||
components:{explain:CronExplainer,build:CronBuilder}},
|
components:{explain:CronExplainer,build:CronBuilder}},
|
||||||
{id:'elektro', label:'⚡ Elektro', desc:'Ohm, Leistung, Kosten, Widerstände', ready:true, component:ElektroRechner},
|
{id:'elektro', label:'⚡ Elektro', desc:'Ohm, Leistung, Kosten, Widerstände', ready:true, component:ElektroRechner},
|
||||||
{id:'json', label:'{ } JSON', desc:'JSON formatieren', ready:false},
|
{id:'json', label:'{ } JSON', desc:'JSON ansehen & bauen', ready:true, component:JsonTool},
|
||||||
{id:'regex', label:'🔍 Regex', desc:'Regex bauen & erklären', ready:true, component:RegexBuilder},
|
{id:'regex', label:'🔍 Regex', desc:'Regex bauen & erklären', ready:true, component:RegexBuilder},
|
||||||
{id:'diff', label:'± Text Diff', desc:'Zwei Texte vergleichen', ready:true, component:TextDiff},
|
{id:'diff', label:'± Text Diff', desc:'Zwei Texte vergleichen', ready:true, component:TextDiff},
|
||||||
{id:'netzwerk', label:'🌐 Netzwerk', desc:'IP, Subnetz, Geschwindigkeit, CIDR', ready:true, component:NetzwerkRechner},
|
{id:'netzwerk', label:'🌐 Netzwerk', desc:'IP, Subnetz, Geschwindigkeit, CIDR', ready:true, component:NetzwerkRechner},
|
||||||
|
|||||||
Reference in New Issue
Block a user