Add 5 new tools: QR-Code, Markdown, Color Converter, JSON Formatter, Regex Tester

- backend/tools/qrcode_gen.py: POST /api/qrcode/generate, returns base64 PNG (qrcode[pil])
- backend/tools/markdown_tool.py: POST /api/markdown/render, extensions: tables/fenced_code/nl2br
- backend/tools/colorconverter.py: POST /api/color/convert, HEX/RGB/HSL via colorsys (no deps)
- backend/tools/jsonformatter.py: POST /api/json/format, returns formatted JSON with line/col errors
- backend/tools/regextester.py: POST /api/regex/test, flags i/m/s, returns matches with positions
- QrCodeTool.jsx: generate + download PNG button
- MarkdownTool.jsx: split editor/preview, debounce 500ms, white preview bg
- ColorConverterTool.jsx: color swatch preview, per-format copy buttons
- JsonFormatterTool.jsx: indent toggle 2/4, pre result box with copy
- RegexTesterTool.jsx: debounce 400ms, yellow match highlighting, flag checkboxes
- All blueprints registered in app.py; qrcode[pil] + markdown added to requirements.txt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Nirodan
2026-04-24 18:19:34 +02:00
parent 45e1934bee
commit 34c82f3dca
15 changed files with 629 additions and 2 deletions
+11 -1
View File
@@ -9,6 +9,11 @@ import JwtDecoderTool from './components/JwtDecoderTool';
import PasswordGenTool from './components/PasswordGenTool';
import TimestampTool from './components/TimestampTool';
import TextDiffTool from './components/TextDiffTool';
import QrCodeTool from './components/QrCodeTool';
import MarkdownTool from './components/MarkdownTool';
import ColorConverterTool from './components/ColorConverterTool';
import JsonFormatterTool from './components/JsonFormatterTool';
import RegexTesterTool from './components/RegexTesterTool';
import NavBar from './components/NavBar';
import ToolOverview from './components/ToolOverview';
import AdminDashboard from './components/AdminDashboard';
@@ -50,7 +55,12 @@ function App() {
<Route path="/tools/jwt" element={isLoggedIn ? <JwtDecoderTool /> : <Navigate to="/login" />} />
<Route path="/tools/password" element={isLoggedIn ? <PasswordGenTool />: <Navigate to="/login" />} />
<Route path="/tools/timestamp" element={isLoggedIn ? <TimestampTool /> : <Navigate to="/login" />} />
<Route path="/tools/textdiff" element={isLoggedIn ? <TextDiffTool /> : <Navigate to="/login" />} />
<Route path="/tools/textdiff" element={isLoggedIn ? <TextDiffTool /> : <Navigate to="/login" />} />
<Route path="/tools/qrcode" element={isLoggedIn ? <QrCodeTool /> : <Navigate to="/login" />} />
<Route path="/tools/markdown" element={isLoggedIn ? <MarkdownTool /> : <Navigate to="/login" />} />
<Route path="/tools/color" element={isLoggedIn ? <ColorConverterTool /> : <Navigate to="/login" />} />
<Route path="/tools/json" element={isLoggedIn ? <JsonFormatterTool /> : <Navigate to="/login" />} />
<Route path="/tools/regex" element={isLoggedIn ? <RegexTesterTool /> : <Navigate to="/login" />} />
<Route
path="/admin"
element={
@@ -0,0 +1,85 @@
import { useState } from 'react';
import axios from '../services/api';
const resultBox = {
marginTop: '8px',
padding: '10px 14px',
background: 'var(--surface-2)',
border: '1px solid var(--border)',
borderRadius: '12px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: '8px',
};
function CopyBtn({ text }) {
const [copied, setCopied] = useState(false);
const copy = () => {
navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
};
return (
<button className="ghost" onClick={copy} style={{ flexShrink: 0, margin: 0 }}>
{copied ? 'Kopiert!' : 'Kopieren'}
</button>
);
}
function ColorConverterTool() {
const [value, setValue] = useState('');
const [format, setFormat] = useState('hex');
const [result, setResult] = useState(null);
const [error, setError] = useState('');
const convert = async () => {
setError('');
setResult(null);
try {
const res = await axios.post('/api/color/convert', { value, from_format: format });
setResult(res.data);
} catch (err) {
setError(err.response?.data?.message || 'Fehler bei der Konvertierung');
}
};
const placeholders = { hex: '#ff6600', rgb: '255, 102, 0', hsl: '24, 100%, 50%' };
return (
<div className="main-content">
<h2>Farb-Konverter</h2>
<input
type="text"
value={value}
onChange={(e) => { setValue(e.target.value); setResult(null); setError(''); }}
placeholder={placeholders[format]}
/>
<select value={format} onChange={(e) => { setFormat(e.target.value); setResult(null); }} style={{ marginTop: '8px' }}>
<option value="hex">HEX</option>
<option value="rgb">RGB</option>
<option value="hsl">HSL</option>
</select>
<button onClick={convert}>Konvertieren</button>
{error && <p className="error">{error}</p>}
{result && (
<>
<div style={{ marginTop: '12px', height: '80px', borderRadius: '12px', background: result.hex, border: '1px solid var(--border)' }} />
{[
{ label: 'HEX', val: result.hex },
{ label: 'RGB', val: result.rgb },
{ label: 'HSL', val: result.hsl },
].map(({ label, val }) => (
<div key={label} style={resultBox}>
<span style={{ color: 'var(--muted)', fontWeight: 600, minWidth: '40px' }}>{label}</span>
<span style={{ fontFamily: 'monospace', color: 'var(--text)', flex: 1 }}>{val}</span>
<CopyBtn text={val} />
</div>
))}
</>
)}
</div>
);
}
export default ColorConverterTool;
@@ -0,0 +1,78 @@
import { useState } from 'react';
import axios from '../services/api';
function JsonFormatterTool() {
const [input, setInput] = useState('');
const [indent, setIndent] = useState(2);
const [result, setResult] = useState('');
const [error, setError] = useState('');
const [copied, setCopied] = useState(false);
const format = async () => {
setError('');
setResult('');
try {
const res = await axios.post('/api/json/format', { text: input, indent });
setResult(res.data.result);
} catch (err) {
setError(err.response?.data?.message || 'Ungültiges JSON');
}
};
const copy = () => {
navigator.clipboard.writeText(result);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
};
return (
<div className="main-content">
<h2>JSON Formatter</h2>
<textarea
rows={10}
value={input}
onChange={(e) => { setInput(e.target.value); setResult(''); setError(''); }}
placeholder='{"key": "value"}'
style={{ resize: 'vertical', fontFamily: 'monospace', fontSize: '0.875rem' }}
/>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginTop: '4px' }}>
{[2, 4].map((n) => (
<button key={n} className={indent === n ? '' : 'ghost'} onClick={() => setIndent(n)} style={{ margin: 0 }}>
{n} Spaces
</button>
))}
<button onClick={format} style={{ marginLeft: 'auto', margin: 0 }}>Formatieren</button>
</div>
{error && <p className="error">{error}</p>}
{result && (
<div style={{ marginTop: '12px', position: 'relative' }}>
<button
className="ghost"
onClick={copy}
style={{ position: 'absolute', top: '8px', right: '8px', margin: 0, zIndex: 1 }}
>
{copied ? 'Kopiert!' : 'Kopieren'}
</button>
<pre style={{
margin: 0,
padding: '14px',
paddingTop: '40px',
background: 'var(--surface-2)',
border: '1px solid var(--border)',
borderRadius: '12px',
color: 'var(--text)',
fontFamily: 'monospace',
fontSize: '0.875rem',
overflowX: 'auto',
whiteSpace: 'pre-wrap',
wordBreak: 'break-all',
}}>
{result}
</pre>
</div>
)}
</div>
);
}
export default JsonFormatterTool;
+58
View File
@@ -0,0 +1,58 @@
import { useState, useEffect, useRef } from 'react';
import axios from '../services/api';
function MarkdownTool() {
const [text, setText] = useState('');
const [html, setHtml] = useState('');
const timer = useRef(null);
useEffect(() => {
clearTimeout(timer.current);
if (!text) { setHtml(''); return; }
timer.current = setTimeout(async () => {
try {
const res = await axios.post('/api/markdown/render', { text });
setHtml(res.data.html);
} catch {
setHtml('<p style="color:red">Fehler beim Rendern</p>');
}
}, 500);
return () => clearTimeout(timer.current);
}, [text]);
return (
<div className="main-content">
<h2>Markdown Editor</h2>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
<div>
<p className="muted" style={{ marginBottom: '6px', fontWeight: 600 }}>Editor</p>
<textarea
rows={20}
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Markdown eingeben..."
style={{ resize: 'vertical', fontFamily: 'monospace', fontSize: '0.875rem' }}
/>
</div>
<div>
<p className="muted" style={{ marginBottom: '6px', fontWeight: 600 }}>Vorschau</p>
<div
dangerouslySetInnerHTML={{ __html: html || '<span style="color:#999">Vorschau erscheint hier...</span>' }}
style={{
minHeight: '460px',
padding: '16px',
background: '#ffffff',
color: '#0f172a',
border: '1px solid var(--border)',
borderRadius: '12px',
overflowY: 'auto',
lineHeight: 1.7,
}}
/>
</div>
</div>
</div>
);
}
export default MarkdownTool;
+47
View File
@@ -0,0 +1,47 @@
import { useState } from 'react';
import axios from '../services/api';
function QrCodeTool() {
const [input, setInput] = useState('');
const [image, setImage] = useState('');
const [error, setError] = useState('');
const generate = async () => {
setError('');
try {
const res = await axios.post('/api/qrcode/generate', { text: input });
setImage(res.data.image);
} catch (err) {
setError(err.response?.data?.message || 'Fehler beim Generieren');
}
};
const download = () => {
const a = document.createElement('a');
a.href = image;
a.download = 'qrcode.png';
a.click();
};
return (
<div className="main-content">
<h2>QR-Code Generator</h2>
<input
type="text"
value={input}
onChange={(e) => { setInput(e.target.value); setImage(''); setError(''); }}
placeholder="Text oder URL eingeben"
/>
<button onClick={generate}>Generieren</button>
{error && <p className="error">{error}</p>}
{image && (
<div style={{ marginTop: '16px', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' }}>
<img src={image} alt="QR-Code" style={{ width: 220, height: 220, borderRadius: '12px', background: '#fff', padding: '8px' }} />
<button className="ghost" onClick={download} style={{ margin: 0 }}>Herunterladen</button>
</div>
)}
</div>
);
}
export default QrCodeTool;
+116
View File
@@ -0,0 +1,116 @@
import { useState, useEffect, useRef } from 'react';
import axios from '../services/api';
function highlightText(text, matches) {
const parts = [];
let last = 0;
for (const m of matches) {
if (m.start > last) parts.push({ text: text.slice(last, m.start), highlight: false });
parts.push({ text: text.slice(m.start, m.end), highlight: true });
last = m.end;
}
if (last < text.length) parts.push({ text: text.slice(last), highlight: false });
return parts;
}
function RegexTesterTool() {
const [pattern, setPattern] = useState('');
const [flags, setFlags] = useState({ i: false, m: false, s: false });
const [text, setText] = useState('');
const [result, setResult] = useState(null);
const [patternError, setPatternError] = useState('');
const timer = useRef(null);
useEffect(() => {
clearTimeout(timer.current);
if (!pattern || !text) { setResult(null); setPatternError(''); return; }
timer.current = setTimeout(async () => {
try {
const activeFlags = Object.entries(flags).filter(([, v]) => v).map(([k]) => k);
const res = await axios.post('/api/regex/test', { pattern, text, flags: activeFlags });
if (res.data.error) {
setPatternError(res.data.error);
setResult(null);
} else {
setPatternError('');
setResult(res.data);
}
} catch {
setPatternError('Fehler beim Testen');
}
}, 400);
return () => clearTimeout(timer.current);
}, [pattern, text, flags]);
const parts = result?.matches?.length && text ? highlightText(text, result.matches) : null;
return (
<div className="main-content">
<h2>Regex Tester</h2>
<input
type="text"
value={pattern}
onChange={(e) => setPattern(e.target.value)}
placeholder={String.raw`Regex Pattern, z.B. \d+`}
style={{ fontFamily: 'monospace' }}
/>
{patternError && <p className="error" style={{ marginTop: '4px' }}>{patternError}</p>}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '16px', margin: '10px 0 12px' }}>
{[
{ key: 'i', label: 'Case Insensitive (i)' },
{ key: 'm', label: 'Multiline (m)' },
{ key: 's', label: 'Dotall (s)' },
].map(({ key, label }) => (
<label key={key} style={{ display: 'flex', alignItems: 'center', gap: '6px', cursor: 'pointer', color: 'var(--text)', fontWeight: 500 }}>
<input
type="checkbox"
checked={flags[key]}
onChange={(e) => setFlags(prev => ({ ...prev, [key]: e.target.checked }))}
style={{ width: 'auto', accentColor: 'var(--accent)' }}
/>
{label}
</label>
))}
</div>
<textarea
rows={8}
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Testtext eingeben..."
style={{ resize: 'vertical', fontFamily: 'monospace', fontSize: '0.875rem' }}
/>
{result !== null && (
<>
<p style={{ marginTop: '8px', color: result.count > 0 ? 'var(--accent)' : 'var(--muted)', fontWeight: 600 }}>
{result.count} {result.count === 1 ? 'Match' : 'Matches'}
</p>
{parts && (
<div style={{
marginTop: '8px',
padding: '12px 14px',
background: 'var(--surface-2)',
border: '1px solid var(--border)',
borderRadius: '12px',
fontFamily: 'monospace',
fontSize: '0.875rem',
whiteSpace: 'pre-wrap',
wordBreak: 'break-all',
color: 'var(--text)',
}}>
{parts.map((p, i) =>
p.highlight
? <mark key={i} style={{ background: '#fbbf24', color: '#0f172a', borderRadius: '3px', padding: '0 1px' }}>{p.text}</mark>
: <span key={i}>{p.text}</span>
)}
</div>
)}
</>
)}
</div>
);
}
export default RegexTesterTool;
+5
View File
@@ -10,6 +10,11 @@ const TOOLS = [
{ icon: '🔐', path: '/tools/password', title: 'Passwort-Generator', desc: 'Sichere Passwörter generieren' },
{ icon: '🕐', path: '/tools/timestamp', title: 'Timestamp Converter', desc: 'Unix Timestamp in Datum umrechnen' },
{ icon: '📝', path: '/tools/textdiff', title: 'Text Diff', desc: 'Zwei Texte vergleichen' },
{ icon: '📷', path: '/tools/qrcode', title: 'QR-Code Generator', desc: 'Text oder URL als QR-Code generieren' },
{ icon: '📄', path: '/tools/markdown', title: 'Markdown Editor', desc: 'Markdown live rendern und vorschauen' },
{ icon: '🎨', path: '/tools/color', title: 'Farb-Konverter', desc: 'HEX, RGB und HSL konvertieren' },
{ icon: '{ }', path: '/tools/json', title: 'JSON Formatter', desc: 'JSON formatieren und validieren' },
{ icon: '🔍', path: '/tools/regex', title: 'Regex Tester', desc: 'Reguläre Ausdrücke live testen' },
];
function ToolOverview() {