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:
@@ -18,6 +18,11 @@ from tools import (
|
|||||||
passwordgen_blueprint,
|
passwordgen_blueprint,
|
||||||
timestamp_blueprint,
|
timestamp_blueprint,
|
||||||
textdiff_blueprint,
|
textdiff_blueprint,
|
||||||
|
qrcode_blueprint,
|
||||||
|
markdown_blueprint,
|
||||||
|
color_blueprint,
|
||||||
|
json_formatter_blueprint,
|
||||||
|
regex_blueprint,
|
||||||
)
|
)
|
||||||
from admin import admin_bp
|
from admin import admin_bp
|
||||||
|
|
||||||
@@ -34,6 +39,11 @@ app.register_blueprint(jwt_decoder_blueprint)
|
|||||||
app.register_blueprint(passwordgen_blueprint)
|
app.register_blueprint(passwordgen_blueprint)
|
||||||
app.register_blueprint(timestamp_blueprint)
|
app.register_blueprint(timestamp_blueprint)
|
||||||
app.register_blueprint(textdiff_blueprint)
|
app.register_blueprint(textdiff_blueprint)
|
||||||
|
app.register_blueprint(qrcode_blueprint)
|
||||||
|
app.register_blueprint(markdown_blueprint)
|
||||||
|
app.register_blueprint(color_blueprint)
|
||||||
|
app.register_blueprint(json_formatter_blueprint)
|
||||||
|
app.register_blueprint(regex_blueprint)
|
||||||
app.register_blueprint(admin_bp)
|
app.register_blueprint(admin_bp)
|
||||||
|
|
||||||
# 🌐 React-Frontend ausliefern
|
# 🌐 React-Frontend ausliefern
|
||||||
|
|||||||
@@ -5,4 +5,6 @@ mysql-connector-python
|
|||||||
werkzeug>=2.3
|
werkzeug>=2.3
|
||||||
PyJWT
|
PyJWT
|
||||||
bcrypt
|
bcrypt
|
||||||
python-dotenv
|
python-dotenv
|
||||||
|
qrcode[pil]
|
||||||
|
markdown
|
||||||
@@ -5,3 +5,8 @@ from .jwtdecoder import jwt_decoder_blueprint
|
|||||||
from .passwordgen import passwordgen_blueprint
|
from .passwordgen import passwordgen_blueprint
|
||||||
from .timestamp import timestamp_blueprint
|
from .timestamp import timestamp_blueprint
|
||||||
from .textdiff import textdiff_blueprint
|
from .textdiff import textdiff_blueprint
|
||||||
|
from .qrcode_gen import qrcode_blueprint
|
||||||
|
from .markdown_tool import markdown_blueprint
|
||||||
|
from .colorconverter import color_blueprint
|
||||||
|
from .jsonformatter import json_formatter_blueprint
|
||||||
|
from .regextester import regex_blueprint
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
import colorsys
|
||||||
|
import re
|
||||||
|
from util.logger import logger
|
||||||
|
from auth.token import verify_token
|
||||||
|
|
||||||
|
color_blueprint = Blueprint('color_tool', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
def hex_to_rgb(hex_str):
|
||||||
|
hex_str = hex_str.strip().lstrip('#')
|
||||||
|
if len(hex_str) == 3:
|
||||||
|
hex_str = ''.join(c * 2 for c in hex_str)
|
||||||
|
if len(hex_str) != 6:
|
||||||
|
raise ValueError("Ungültiger HEX-Wert")
|
||||||
|
return int(hex_str[0:2], 16), int(hex_str[2:4], 16), int(hex_str[4:6], 16)
|
||||||
|
|
||||||
|
|
||||||
|
def rgb_to_hex(r, g, b):
|
||||||
|
return f"#{r:02x}{g:02x}{b:02x}"
|
||||||
|
|
||||||
|
|
||||||
|
def rgb_to_hsl(r, g, b):
|
||||||
|
# colorsys uses HLS order (hue, lightness, saturation)
|
||||||
|
h, l, s = colorsys.rgb_to_hls(r / 255, g / 255, b / 255)
|
||||||
|
return round(h * 360), round(s * 100), round(l * 100)
|
||||||
|
|
||||||
|
|
||||||
|
def hsl_to_rgb(h, s, l):
|
||||||
|
# CSS HSL: hue 0-360, saturation 0-100, lightness 0-100
|
||||||
|
r, g, b = colorsys.hls_to_rgb(h / 360, l / 100, s / 100)
|
||||||
|
return round(r * 255), round(g * 255), round(b * 255)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_rgb(value):
|
||||||
|
nums = re.findall(r'\d+', value)
|
||||||
|
if len(nums) < 3:
|
||||||
|
raise ValueError("Ungültiger RGB-Wert")
|
||||||
|
r, g, b = int(nums[0]), int(nums[1]), int(nums[2])
|
||||||
|
if not all(0 <= x <= 255 for x in (r, g, b)):
|
||||||
|
raise ValueError("RGB-Werte müssen zwischen 0 und 255 liegen")
|
||||||
|
return r, g, b
|
||||||
|
|
||||||
|
|
||||||
|
def parse_hsl(value):
|
||||||
|
nums = re.findall(r'[\d.]+', value)
|
||||||
|
if len(nums) < 3:
|
||||||
|
raise ValueError("Ungültiger HSL-Wert")
|
||||||
|
h, s, l = float(nums[0]), float(nums[1]), float(nums[2])
|
||||||
|
if not (0 <= h <= 360 and 0 <= s <= 100 and 0 <= l <= 100):
|
||||||
|
raise ValueError("HSL-Werte außerhalb des gültigen Bereichs")
|
||||||
|
return h, s, l
|
||||||
|
|
||||||
|
|
||||||
|
@color_blueprint.route('/api/color/convert', methods=['POST'])
|
||||||
|
def convert_color():
|
||||||
|
user = verify_token()
|
||||||
|
if not user:
|
||||||
|
return jsonify({"message": "Nicht autorisiert"}), 401
|
||||||
|
try:
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
value = data.get("value", "").strip()
|
||||||
|
from_format = data.get("from_format", "hex").lower()
|
||||||
|
|
||||||
|
if from_format == "hex":
|
||||||
|
r, g, b = hex_to_rgb(value)
|
||||||
|
elif from_format == "rgb":
|
||||||
|
r, g, b = parse_rgb(value)
|
||||||
|
elif from_format == "hsl":
|
||||||
|
h, s, l = parse_hsl(value)
|
||||||
|
r, g, b = hsl_to_rgb(h, s, l)
|
||||||
|
else:
|
||||||
|
return jsonify({"message": "Unbekanntes Format"}), 400
|
||||||
|
|
||||||
|
hex_val = rgb_to_hex(r, g, b)
|
||||||
|
hue, sat, lig = rgb_to_hsl(r, g, b)
|
||||||
|
|
||||||
|
logger.info(f"Farbe konvertiert von {user['username']}")
|
||||||
|
return jsonify({
|
||||||
|
"hex": hex_val,
|
||||||
|
"rgb": f"rgb({r}, {g}, {b})",
|
||||||
|
"hsl": f"hsl({hue}, {sat}%, {lig}%)",
|
||||||
|
})
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"message": str(e)}), 400
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler Farbkonverter: {e}")
|
||||||
|
return jsonify({"message": "Fehler bei der Konvertierung"}), 500
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
import json
|
||||||
|
from util.logger import logger
|
||||||
|
from auth.token import verify_token
|
||||||
|
|
||||||
|
json_formatter_blueprint = Blueprint('json_formatter_tool', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@json_formatter_blueprint.route('/api/json/format', methods=['POST'])
|
||||||
|
def format_json():
|
||||||
|
user = verify_token()
|
||||||
|
if not user:
|
||||||
|
return jsonify({"message": "Nicht autorisiert"}), 401
|
||||||
|
try:
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
text = data.get("text", "")
|
||||||
|
indent = int(data.get("indent", 2))
|
||||||
|
|
||||||
|
parsed = json.loads(text)
|
||||||
|
formatted = json.dumps(parsed, indent=indent, ensure_ascii=False)
|
||||||
|
return jsonify({"result": formatted})
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
return jsonify({"message": f"JSON-Fehler in Zeile {e.lineno}, Spalte {e.colno}: {e.msg}"}), 400
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler JSON-Formatter: {e}")
|
||||||
|
return jsonify({"message": "Fehler beim Formatieren"}), 500
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
import markdown
|
||||||
|
from util.logger import logger
|
||||||
|
from auth.token import verify_token
|
||||||
|
|
||||||
|
markdown_blueprint = Blueprint('markdown_tool', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@markdown_blueprint.route('/api/markdown/render', methods=['POST'])
|
||||||
|
def render_markdown():
|
||||||
|
user = verify_token()
|
||||||
|
if not user:
|
||||||
|
return jsonify({"message": "Nicht autorisiert"}), 401
|
||||||
|
try:
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
text = data.get("text", "")
|
||||||
|
html = markdown.markdown(text, extensions=["tables", "fenced_code", "nl2br"])
|
||||||
|
return jsonify({"html": html})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler Markdown: {e}")
|
||||||
|
return jsonify({"message": "Fehler beim Rendern"}), 500
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
import qrcode
|
||||||
|
import io
|
||||||
|
import base64
|
||||||
|
from util.logger import logger
|
||||||
|
from auth.token import verify_token
|
||||||
|
|
||||||
|
qrcode_blueprint = Blueprint('qrcode_tool', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@qrcode_blueprint.route('/api/qrcode/generate', methods=['POST'])
|
||||||
|
def generate_qrcode():
|
||||||
|
user = verify_token()
|
||||||
|
if not user:
|
||||||
|
return jsonify({"message": "Nicht autorisiert"}), 401
|
||||||
|
try:
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
text = data.get("text", "").strip()
|
||||||
|
if not text:
|
||||||
|
return jsonify({"message": "Text darf nicht leer sein"}), 400
|
||||||
|
|
||||||
|
qr = qrcode.QRCode(box_size=10, border=4)
|
||||||
|
qr.add_data(text)
|
||||||
|
qr.make(fit=True)
|
||||||
|
img = qr.make_image(fill_color="black", back_color="white")
|
||||||
|
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
img.save(buffer, format="PNG")
|
||||||
|
b64 = base64.b64encode(buffer.getvalue()).decode()
|
||||||
|
|
||||||
|
logger.info(f"QR-Code generiert von {user['username']}")
|
||||||
|
return jsonify({"image": f"data:image/png;base64,{b64}"})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler QR-Code: {e}")
|
||||||
|
return jsonify({"message": "Fehler beim Generieren"}), 500
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
import re
|
||||||
|
from util.logger import logger
|
||||||
|
from auth.token import verify_token
|
||||||
|
|
||||||
|
regex_blueprint = Blueprint('regex_tool', __name__)
|
||||||
|
|
||||||
|
_FLAG_MAP = {"i": re.IGNORECASE, "m": re.MULTILINE, "s": re.DOTALL}
|
||||||
|
|
||||||
|
|
||||||
|
@regex_blueprint.route('/api/regex/test', methods=['POST'])
|
||||||
|
def test_regex():
|
||||||
|
user = verify_token()
|
||||||
|
if not user:
|
||||||
|
return jsonify({"message": "Nicht autorisiert"}), 401
|
||||||
|
try:
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
pattern = data.get("pattern", "")
|
||||||
|
text = data.get("text", "")
|
||||||
|
flags_list = data.get("flags", [])
|
||||||
|
|
||||||
|
combined = 0
|
||||||
|
for f in flags_list:
|
||||||
|
if f in _FLAG_MAP:
|
||||||
|
combined |= _FLAG_MAP[f]
|
||||||
|
|
||||||
|
try:
|
||||||
|
compiled = re.compile(pattern, combined)
|
||||||
|
except re.error as e:
|
||||||
|
return jsonify({"matches": [], "count": 0, "error": str(e)})
|
||||||
|
|
||||||
|
matches = [
|
||||||
|
{"match": m.group(), "start": m.start(), "end": m.end(), "groups": list(m.groups())}
|
||||||
|
for m in compiled.finditer(text)
|
||||||
|
]
|
||||||
|
|
||||||
|
logger.info(f"Regex getestet von {user['username']}")
|
||||||
|
return jsonify({"matches": matches, "count": len(matches), "error": None})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler Regex-Tester: {e}")
|
||||||
|
return jsonify({"message": "Fehler beim Testen"}), 500
|
||||||
+11
-1
@@ -9,6 +9,11 @@ import JwtDecoderTool from './components/JwtDecoderTool';
|
|||||||
import PasswordGenTool from './components/PasswordGenTool';
|
import PasswordGenTool from './components/PasswordGenTool';
|
||||||
import TimestampTool from './components/TimestampTool';
|
import TimestampTool from './components/TimestampTool';
|
||||||
import TextDiffTool from './components/TextDiffTool';
|
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 NavBar from './components/NavBar';
|
||||||
import ToolOverview from './components/ToolOverview';
|
import ToolOverview from './components/ToolOverview';
|
||||||
import AdminDashboard from './components/AdminDashboard';
|
import AdminDashboard from './components/AdminDashboard';
|
||||||
@@ -50,7 +55,12 @@ function App() {
|
|||||||
<Route path="/tools/jwt" element={isLoggedIn ? <JwtDecoderTool /> : <Navigate to="/login" />} />
|
<Route path="/tools/jwt" element={isLoggedIn ? <JwtDecoderTool /> : <Navigate to="/login" />} />
|
||||||
<Route path="/tools/password" element={isLoggedIn ? <PasswordGenTool />: <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/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
|
<Route
|
||||||
path="/admin"
|
path="/admin"
|
||||||
element={
|
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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -10,6 +10,11 @@ const TOOLS = [
|
|||||||
{ icon: '🔐', path: '/tools/password', title: 'Passwort-Generator', desc: 'Sichere Passwörter generieren' },
|
{ 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/timestamp', title: 'Timestamp Converter', desc: 'Unix Timestamp in Datum umrechnen' },
|
||||||
{ icon: '📝', path: '/tools/textdiff', title: 'Text Diff', desc: 'Zwei Texte vergleichen' },
|
{ 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() {
|
function ToolOverview() {
|
||||||
|
|||||||
Reference in New Issue
Block a user