From 34c82f3dcae1d438f39b44faf80520a42f3081bc Mon Sep 17 00:00:00 2001 From: Nirodan Date: Fri, 24 Apr 2026 18:19:34 +0200 Subject: [PATCH] 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 --- backend/app.py | 10 ++ backend/requirements.txt | 4 +- backend/tools/__init__.py | 5 + backend/tools/colorconverter.py | 88 +++++++++++++ backend/tools/jsonformatter.py | 26 ++++ backend/tools/markdown_tool.py | 21 ++++ backend/tools/qrcode_gen.py | 35 ++++++ backend/tools/regextester.py | 41 +++++++ frontend/src/App.jsx | 12 +- .../src/components/ColorConverterTool.jsx | 85 +++++++++++++ frontend/src/components/JsonFormatterTool.jsx | 78 ++++++++++++ frontend/src/components/MarkdownTool.jsx | 58 +++++++++ frontend/src/components/QrCodeTool.jsx | 47 +++++++ frontend/src/components/RegexTesterTool.jsx | 116 ++++++++++++++++++ frontend/src/components/ToolOverview.jsx | 5 + 15 files changed, 629 insertions(+), 2 deletions(-) create mode 100644 backend/tools/colorconverter.py create mode 100644 backend/tools/jsonformatter.py create mode 100644 backend/tools/markdown_tool.py create mode 100644 backend/tools/qrcode_gen.py create mode 100644 backend/tools/regextester.py create mode 100644 frontend/src/components/ColorConverterTool.jsx create mode 100644 frontend/src/components/JsonFormatterTool.jsx create mode 100644 frontend/src/components/MarkdownTool.jsx create mode 100644 frontend/src/components/QrCodeTool.jsx create mode 100644 frontend/src/components/RegexTesterTool.jsx diff --git a/backend/app.py b/backend/app.py index 401dc3e..d37494d 100644 --- a/backend/app.py +++ b/backend/app.py @@ -18,6 +18,11 @@ from tools import ( passwordgen_blueprint, timestamp_blueprint, textdiff_blueprint, + qrcode_blueprint, + markdown_blueprint, + color_blueprint, + json_formatter_blueprint, + regex_blueprint, ) from admin import admin_bp @@ -34,6 +39,11 @@ app.register_blueprint(jwt_decoder_blueprint) app.register_blueprint(passwordgen_blueprint) app.register_blueprint(timestamp_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) # 🌐 React-Frontend ausliefern diff --git a/backend/requirements.txt b/backend/requirements.txt index c47fdf7..19d6011 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -5,4 +5,6 @@ mysql-connector-python werkzeug>=2.3 PyJWT bcrypt -python-dotenv \ No newline at end of file +python-dotenv +qrcode[pil] +markdown \ No newline at end of file diff --git a/backend/tools/__init__.py b/backend/tools/__init__.py index c53c248..4a71b14 100644 --- a/backend/tools/__init__.py +++ b/backend/tools/__init__.py @@ -5,3 +5,8 @@ from .jwtdecoder import jwt_decoder_blueprint from .passwordgen import passwordgen_blueprint from .timestamp import timestamp_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 diff --git a/backend/tools/colorconverter.py b/backend/tools/colorconverter.py new file mode 100644 index 0000000..8173c2c --- /dev/null +++ b/backend/tools/colorconverter.py @@ -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 diff --git a/backend/tools/jsonformatter.py b/backend/tools/jsonformatter.py new file mode 100644 index 0000000..1bbd75a --- /dev/null +++ b/backend/tools/jsonformatter.py @@ -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 diff --git a/backend/tools/markdown_tool.py b/backend/tools/markdown_tool.py new file mode 100644 index 0000000..26c0dfa --- /dev/null +++ b/backend/tools/markdown_tool.py @@ -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 diff --git a/backend/tools/qrcode_gen.py b/backend/tools/qrcode_gen.py new file mode 100644 index 0000000..a7676b2 --- /dev/null +++ b/backend/tools/qrcode_gen.py @@ -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 diff --git a/backend/tools/regextester.py b/backend/tools/regextester.py new file mode 100644 index 0000000..0bf0e3c --- /dev/null +++ b/backend/tools/regextester.py @@ -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 diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 54b0814..6c3d99a 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -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() { : } /> : } /> : } /> - : } /> + : } /> + : } /> + : } /> + : } /> + : } /> + : } /> { + navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + }; + return ( + + ); +} + +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 ( +
+

Farb-Konverter

+ { setValue(e.target.value); setResult(null); setError(''); }} + placeholder={placeholders[format]} + /> + + + {error &&

{error}

} + {result && ( + <> +
+ {[ + { label: 'HEX', val: result.hex }, + { label: 'RGB', val: result.rgb }, + { label: 'HSL', val: result.hsl }, + ].map(({ label, val }) => ( +
+ {label} + {val} + +
+ ))} + + )} +
+ ); +} + +export default ColorConverterTool; diff --git a/frontend/src/components/JsonFormatterTool.jsx b/frontend/src/components/JsonFormatterTool.jsx new file mode 100644 index 0000000..2a1c5b0 --- /dev/null +++ b/frontend/src/components/JsonFormatterTool.jsx @@ -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 ( +
+

JSON Formatter

+