Add 8 new tools: Hash Verifier, URL Tool, String Utils, Cron Explainer, IP Calc, Lorem Ipsum, CSV Viewer, Notes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,14 @@ from tools import (
|
|||||||
color_blueprint,
|
color_blueprint,
|
||||||
json_formatter_blueprint,
|
json_formatter_blueprint,
|
||||||
regex_blueprint,
|
regex_blueprint,
|
||||||
|
hashverifier_blueprint,
|
||||||
|
url_blueprint,
|
||||||
|
stringutils_blueprint,
|
||||||
|
cron_blueprint,
|
||||||
|
ipcalc_blueprint,
|
||||||
|
lorem_blueprint,
|
||||||
|
csv_blueprint,
|
||||||
|
notes_blueprint,
|
||||||
)
|
)
|
||||||
from admin import admin_bp
|
from admin import admin_bp
|
||||||
|
|
||||||
@@ -44,6 +52,14 @@ app.register_blueprint(markdown_blueprint)
|
|||||||
app.register_blueprint(color_blueprint)
|
app.register_blueprint(color_blueprint)
|
||||||
app.register_blueprint(json_formatter_blueprint)
|
app.register_blueprint(json_formatter_blueprint)
|
||||||
app.register_blueprint(regex_blueprint)
|
app.register_blueprint(regex_blueprint)
|
||||||
|
app.register_blueprint(hashverifier_blueprint)
|
||||||
|
app.register_blueprint(url_blueprint)
|
||||||
|
app.register_blueprint(stringutils_blueprint)
|
||||||
|
app.register_blueprint(cron_blueprint)
|
||||||
|
app.register_blueprint(ipcalc_blueprint)
|
||||||
|
app.register_blueprint(lorem_blueprint)
|
||||||
|
app.register_blueprint(csv_blueprint)
|
||||||
|
app.register_blueprint(notes_blueprint)
|
||||||
app.register_blueprint(admin_bp)
|
app.register_blueprint(admin_bp)
|
||||||
|
|
||||||
# 🌐 React-Frontend ausliefern
|
# 🌐 React-Frontend ausliefern
|
||||||
|
|||||||
@@ -10,3 +10,11 @@ from .markdown_tool import markdown_blueprint
|
|||||||
from .colorconverter import color_blueprint
|
from .colorconverter import color_blueprint
|
||||||
from .jsonformatter import json_formatter_blueprint
|
from .jsonformatter import json_formatter_blueprint
|
||||||
from .regextester import regex_blueprint
|
from .regextester import regex_blueprint
|
||||||
|
from .hashverifier import hashverifier_blueprint
|
||||||
|
from .urltool import url_blueprint
|
||||||
|
from .stringutils import stringutils_blueprint
|
||||||
|
from .cronexplainer import cron_blueprint
|
||||||
|
from .ipcalc import ipcalc_blueprint
|
||||||
|
from .loremipsum import lorem_blueprint
|
||||||
|
from .csvviewer import csv_blueprint
|
||||||
|
from .notes import notes_blueprint
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from util.logger import logger
|
||||||
|
from auth.token import verify_token
|
||||||
|
|
||||||
|
cron_blueprint = Blueprint('cron_tool', __name__)
|
||||||
|
|
||||||
|
MONTH_NAMES = {
|
||||||
|
'1': 'Januar', '2': 'Februar', '3': 'März', '4': 'April',
|
||||||
|
'5': 'Mai', '6': 'Juni', '7': 'Juli', '8': 'August',
|
||||||
|
'9': 'September', '10': 'Oktober', '11': 'November', '12': 'Dezember',
|
||||||
|
}
|
||||||
|
WEEKDAY_NAMES = {
|
||||||
|
'0': 'Sonntag', '1': 'Montag', '2': 'Dienstag', '3': 'Mittwoch',
|
||||||
|
'4': 'Donnerstag', '5': 'Freitag', '6': 'Samstag', '7': 'Sonntag',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_field(field, min_val, max_val):
|
||||||
|
values = set()
|
||||||
|
for part in field.split(','):
|
||||||
|
part = part.strip()
|
||||||
|
if part == '*':
|
||||||
|
values.update(range(min_val, max_val + 1))
|
||||||
|
elif '/' in part:
|
||||||
|
base, step = part.split('/', 1)
|
||||||
|
step = int(step)
|
||||||
|
if base == '*':
|
||||||
|
start, end = min_val, max_val
|
||||||
|
elif '-' in base:
|
||||||
|
s, e = base.split('-')
|
||||||
|
start, end = int(s), int(e)
|
||||||
|
else:
|
||||||
|
start, end = int(base), max_val
|
||||||
|
values.update(range(start, end + 1, step))
|
||||||
|
elif '-' in part:
|
||||||
|
s, e = part.split('-')
|
||||||
|
values.update(range(int(s), int(e) + 1))
|
||||||
|
else:
|
||||||
|
values.add(int(part))
|
||||||
|
return sorted(v for v in values if min_val <= v <= max_val)
|
||||||
|
|
||||||
|
|
||||||
|
def explain_field_text(field, unit, plural, labels=None):
|
||||||
|
if field == '*':
|
||||||
|
return f"Jede(n) {unit}"
|
||||||
|
parts = field.split(',')
|
||||||
|
described = []
|
||||||
|
for p in parts:
|
||||||
|
if p.startswith('*/'):
|
||||||
|
described.append(f"alle {p[2:]} {plural}")
|
||||||
|
elif '-' in p and '/' not in p:
|
||||||
|
a, b = p.split('-', 1)
|
||||||
|
a_l = labels.get(a, a) if labels else a
|
||||||
|
b_l = labels.get(b, b) if labels else b
|
||||||
|
described.append(f"{a_l} bis {b_l}")
|
||||||
|
elif '/' in p:
|
||||||
|
base, step = p.split('/', 1)
|
||||||
|
if '-' in base:
|
||||||
|
a, b = base.split('-', 1)
|
||||||
|
described.append(f"{a}–{b} alle {step}")
|
||||||
|
else:
|
||||||
|
described.append(f"ab {base} alle {step}")
|
||||||
|
else:
|
||||||
|
described.append(labels.get(p, p) if labels else p)
|
||||||
|
return ', '.join(described)
|
||||||
|
|
||||||
|
|
||||||
|
def build_summary(minute, hour, day, month, weekday):
|
||||||
|
parts = []
|
||||||
|
|
||||||
|
if minute == '*' and hour == '*':
|
||||||
|
parts.append("Jede Minute")
|
||||||
|
elif minute.startswith('*/') and hour == '*':
|
||||||
|
parts.append(f"Alle {minute[2:]} Minuten")
|
||||||
|
elif hour != '*' and minute == '0':
|
||||||
|
h = explain_field_text(hour, 'Stunde', 'Stunden')
|
||||||
|
parts.append(f"Um {h}:00 Uhr")
|
||||||
|
else:
|
||||||
|
parts.append(f"Minute: {minute}, Stunde: {hour}")
|
||||||
|
|
||||||
|
if weekday != '*' and day == '*':
|
||||||
|
wd = explain_field_text(weekday, 'Wochentag', 'Wochentage', WEEKDAY_NAMES)
|
||||||
|
parts.append(f"jeden {wd}")
|
||||||
|
elif day != '*' and weekday == '*':
|
||||||
|
parts.append(f"am {day}. des Monats")
|
||||||
|
elif day != '*' and weekday != '*':
|
||||||
|
wd = explain_field_text(weekday, 'Wochentag', 'Wochentage', WEEKDAY_NAMES)
|
||||||
|
parts.append(f"am {day}. oder {wd}")
|
||||||
|
|
||||||
|
if month != '*':
|
||||||
|
m = explain_field_text(month, 'Monat', 'Monate', MONTH_NAMES)
|
||||||
|
parts.append(f"im {m}")
|
||||||
|
|
||||||
|
return ', '.join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def get_next_runs(minute_vals, hour_vals, day_vals, month_vals, weekday_vals):
|
||||||
|
# cron weekday 0=Sun..6=Sat,7=Sun -> python weekday 0=Mon..6=Sun
|
||||||
|
py_weekdays = set((wd + 6) % 7 for wd in weekday_vals)
|
||||||
|
minute_set = set(minute_vals)
|
||||||
|
hour_set = set(hour_vals)
|
||||||
|
day_set = set(day_vals)
|
||||||
|
month_set = set(month_vals)
|
||||||
|
|
||||||
|
now = datetime.now().replace(second=0, microsecond=0) + timedelta(minutes=1)
|
||||||
|
results = []
|
||||||
|
current = now
|
||||||
|
|
||||||
|
for _ in range(2 * 366 * 24 * 60):
|
||||||
|
if len(results) >= 5:
|
||||||
|
break
|
||||||
|
if (current.month in month_set and
|
||||||
|
current.day in day_set and
|
||||||
|
current.weekday() in py_weekdays and
|
||||||
|
current.hour in hour_set and
|
||||||
|
current.minute in minute_set):
|
||||||
|
results.append(current.isoformat())
|
||||||
|
current += timedelta(minutes=1)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
@cron_blueprint.route('/api/cron/explain', methods=['POST'])
|
||||||
|
def explain_cron():
|
||||||
|
user = verify_token()
|
||||||
|
if not user:
|
||||||
|
return jsonify({"message": "Nicht autorisiert"}), 401
|
||||||
|
try:
|
||||||
|
data = request.get_json() or {}
|
||||||
|
expression = data.get("expression", "").strip()
|
||||||
|
|
||||||
|
fields = expression.split()
|
||||||
|
if len(fields) != 5:
|
||||||
|
return jsonify({"error": "Cron-Ausdruck muss genau 5 Felder haben (Minute Stunde Tag Monat Wochentag)"}), 400
|
||||||
|
|
||||||
|
minute_f, hour_f, day_f, month_f, weekday_f = fields
|
||||||
|
|
||||||
|
try:
|
||||||
|
minute_vals = parse_field(minute_f, 0, 59)
|
||||||
|
hour_vals = parse_field(hour_f, 0, 23)
|
||||||
|
day_vals = parse_field(day_f, 1, 31)
|
||||||
|
month_vals = parse_field(month_f, 1, 12)
|
||||||
|
weekday_vals = parse_field(weekday_f, 0, 7)
|
||||||
|
except (ValueError, ZeroDivisionError) as e:
|
||||||
|
return jsonify({"error": f"Ungültiger Cron-Ausdruck: {e}"}), 400
|
||||||
|
|
||||||
|
field_descriptions = {
|
||||||
|
"minute": explain_field_text(minute_f, 'Minute', 'Minuten'),
|
||||||
|
"hour": explain_field_text(hour_f, 'Stunde', 'Stunden'),
|
||||||
|
"day": explain_field_text(day_f, 'Tag', 'Tage'),
|
||||||
|
"month": explain_field_text(month_f, 'Monat', 'Monate', MONTH_NAMES),
|
||||||
|
"weekday": explain_field_text(weekday_f, 'Wochentag', 'Wochentage', WEEKDAY_NAMES),
|
||||||
|
}
|
||||||
|
|
||||||
|
next_runs = get_next_runs(minute_vals, hour_vals, day_vals, month_vals, weekday_vals)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"explanation": build_summary(minute_f, hour_f, day_f, month_f, weekday_f),
|
||||||
|
"fields": field_descriptions,
|
||||||
|
"next_runs": next_runs,
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler cron explain: {e}")
|
||||||
|
return jsonify({"error": "Fehler beim Verarbeiten des Ausdrucks"}), 500
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
from util.logger import logger
|
||||||
|
from auth.token import verify_token
|
||||||
|
|
||||||
|
csv_blueprint = Blueprint('csv_tool', __name__)
|
||||||
|
|
||||||
|
MAX_ROWS = 500
|
||||||
|
|
||||||
|
|
||||||
|
@csv_blueprint.route('/api/csv/parse', methods=['POST'])
|
||||||
|
def parse_csv():
|
||||||
|
user = verify_token()
|
||||||
|
if not user:
|
||||||
|
return jsonify({"message": "Nicht autorisiert"}), 401
|
||||||
|
try:
|
||||||
|
data = request.get_json() or {}
|
||||||
|
text = data.get("text", "")
|
||||||
|
delimiter = data.get("delimiter", ",")
|
||||||
|
|
||||||
|
# Handle escaped tab
|
||||||
|
if delimiter == "\\t" or delimiter == "\t":
|
||||||
|
delimiter = "\t"
|
||||||
|
if not delimiter:
|
||||||
|
delimiter = ","
|
||||||
|
|
||||||
|
reader = csv.reader(io.StringIO(text), delimiter=delimiter)
|
||||||
|
all_rows = list(reader)
|
||||||
|
|
||||||
|
if not all_rows:
|
||||||
|
return jsonify({"headers": [], "rows": [], "total_rows": 0, "truncated": False})
|
||||||
|
|
||||||
|
headers = all_rows[0]
|
||||||
|
data_rows = all_rows[1:]
|
||||||
|
total_rows = len(data_rows)
|
||||||
|
truncated = total_rows > MAX_ROWS
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"headers": headers,
|
||||||
|
"rows": data_rows[:MAX_ROWS],
|
||||||
|
"total_rows": total_rows,
|
||||||
|
"truncated": truncated,
|
||||||
|
})
|
||||||
|
|
||||||
|
except csv.Error as e:
|
||||||
|
return jsonify({"message": f"Ungültiges CSV: {e}"}), 400
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler csvviewer: {e}")
|
||||||
|
return jsonify({"message": "Fehler beim Verarbeiten des CSV"}), 500
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
import hashlib
|
||||||
|
import bcrypt
|
||||||
|
from util.logger import logger
|
||||||
|
from auth.token import verify_token
|
||||||
|
|
||||||
|
hashverifier_blueprint = Blueprint('hashverifier_tool', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@hashverifier_blueprint.route('/api/hash/verify', methods=['POST'])
|
||||||
|
def verify_hash():
|
||||||
|
user = verify_token()
|
||||||
|
if not user:
|
||||||
|
return jsonify({"message": "Nicht autorisiert"}), 401
|
||||||
|
try:
|
||||||
|
data = request.get_json() or {}
|
||||||
|
text = data.get("text", "")
|
||||||
|
hash_val = data.get("hash", "").strip()
|
||||||
|
algorithm = data.get("algorithm", "sha256")
|
||||||
|
|
||||||
|
if algorithm == "md5":
|
||||||
|
computed = hashlib.md5(text.encode()).hexdigest()
|
||||||
|
match = computed.lower() == hash_val.lower()
|
||||||
|
elif algorithm == "sha256":
|
||||||
|
computed = hashlib.sha256(text.encode()).hexdigest()
|
||||||
|
match = computed.lower() == hash_val.lower()
|
||||||
|
elif algorithm == "bcrypt":
|
||||||
|
try:
|
||||||
|
match = bcrypt.checkpw(text.encode(), hash_val.encode())
|
||||||
|
except Exception:
|
||||||
|
return jsonify({"message": "Ungültiger bcrypt-Hash"}), 400
|
||||||
|
else:
|
||||||
|
return jsonify({"message": "Unbekannter Algorithmus"}), 400
|
||||||
|
|
||||||
|
logger.info(f"Hash verify ({algorithm}) von {user['username']}: {match}")
|
||||||
|
return jsonify({"match": match})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler bei Hash-Verifikation: {e}")
|
||||||
|
return jsonify({"message": "Fehler bei der Verifikation"}), 500
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
import ipaddress
|
||||||
|
from util.logger import logger
|
||||||
|
from auth.token import verify_token
|
||||||
|
|
||||||
|
ipcalc_blueprint = Blueprint('ipcalc_tool', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ipcalc_blueprint.route('/api/ip/calculate', methods=['POST'])
|
||||||
|
def ip_calculate():
|
||||||
|
user = verify_token()
|
||||||
|
if not user:
|
||||||
|
return jsonify({"message": "Nicht autorisiert"}), 401
|
||||||
|
try:
|
||||||
|
data = request.get_json() or {}
|
||||||
|
cidr = data.get("cidr", "").strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
network = ipaddress.IPv4Network(cidr, strict=False)
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({"message": f"Ungültige CIDR-Notation: {e}"}), 400
|
||||||
|
|
||||||
|
hosts = list(network.hosts())
|
||||||
|
first_host = str(hosts[0]) if hosts else str(network.network_address)
|
||||||
|
last_host = str(hosts[-1]) if hosts else str(network.broadcast_address)
|
||||||
|
total_hosts = len(hosts)
|
||||||
|
|
||||||
|
# Wildcard = inverse of netmask
|
||||||
|
netmask_int = int(network.netmask)
|
||||||
|
wildcard_int = (~netmask_int) & 0xFFFFFFFF
|
||||||
|
wildcard = str(ipaddress.IPv4Address(wildcard_int))
|
||||||
|
|
||||||
|
ip_class = "Privat" if network.is_private else "Öffentlich"
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"network": str(network.network_address),
|
||||||
|
"broadcast": str(network.broadcast_address),
|
||||||
|
"netmask": str(network.netmask),
|
||||||
|
"wildcard": wildcard,
|
||||||
|
"first_host": first_host,
|
||||||
|
"last_host": last_host,
|
||||||
|
"total_hosts": total_hosts,
|
||||||
|
"prefix_length": network.prefixlen,
|
||||||
|
"ip_class": ip_class,
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler ipcalc: {e}")
|
||||||
|
return jsonify({"message": "Fehler bei der Berechnung"}), 500
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
import random
|
||||||
|
from util.logger import logger
|
||||||
|
from auth.token import verify_token
|
||||||
|
|
||||||
|
lorem_blueprint = Blueprint('lorem_tool', __name__)
|
||||||
|
|
||||||
|
WORDS = [
|
||||||
|
"lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit",
|
||||||
|
"sed", "do", "eiusmod", "tempor", "incididunt", "ut", "labore", "et", "dolore",
|
||||||
|
"magna", "aliqua", "enim", "ad", "minim", "veniam", "quis", "nostrud",
|
||||||
|
"exercitation", "ullamco", "laboris", "nisi", "aliquip", "ex", "ea", "commodo",
|
||||||
|
"consequat", "duis", "aute", "irure", "reprehenderit", "voluptate", "velit",
|
||||||
|
"esse", "cillum", "eu", "fugiat", "nulla", "pariatur", "excepteur", "sint",
|
||||||
|
"occaecat", "cupidatat", "non", "proident", "sunt", "culpa", "qui", "officia",
|
||||||
|
"deserunt", "mollit", "anim", "id", "est", "laborum", "perspiciatis", "unde",
|
||||||
|
"omnis", "iste", "natus", "error", "voluptatem", "accusantium", "doloremque",
|
||||||
|
"laudantium", "totam", "rem", "aperiam", "eaque", "ipsa", "quae", "ab", "illo",
|
||||||
|
"inventore", "veritatis", "quasi", "architecto", "beatae", "vitae", "dicta",
|
||||||
|
"explicabo", "nemo", "ipsam", "quia", "voluptas", "aspernatur", "odit",
|
||||||
|
"fugit", "magni", "dolores", "ratione", "sequi", "nesciunt", "neque", "porro",
|
||||||
|
"quisquam", "adipisci", "numquam", "eius", "modi", "tempora", "incidunt",
|
||||||
|
"soluta", "nobis", "eligendi", "optio", "cumque", "nihil", "impedit", "minus",
|
||||||
|
"maxime", "placeat", "facere", "possimus", "omnis", "assumenda", "repellendus",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def make_sentence():
|
||||||
|
word_count = random.randint(8, 15)
|
||||||
|
words = [random.choice(WORDS) for _ in range(word_count)]
|
||||||
|
return words[0].capitalize() + ' ' + ' '.join(words[1:]) + '.'
|
||||||
|
|
||||||
|
|
||||||
|
def make_paragraph():
|
||||||
|
sentence_count = random.randint(4, 6)
|
||||||
|
return ' '.join(make_sentence() for _ in range(sentence_count))
|
||||||
|
|
||||||
|
|
||||||
|
@lorem_blueprint.route('/api/lorem/generate', methods=['POST'])
|
||||||
|
def generate_lorem():
|
||||||
|
user = verify_token()
|
||||||
|
if not user:
|
||||||
|
return jsonify({"message": "Nicht autorisiert"}), 401
|
||||||
|
try:
|
||||||
|
data = request.get_json() or {}
|
||||||
|
gen_type = data.get("type", "sentences")
|
||||||
|
count = int(data.get("count", 3))
|
||||||
|
count = max(1, min(20, count))
|
||||||
|
|
||||||
|
if gen_type == "words":
|
||||||
|
text = ' '.join(random.choice(WORDS) for _ in range(count))
|
||||||
|
elif gen_type == "sentences":
|
||||||
|
text = ' '.join(make_sentence() for _ in range(count))
|
||||||
|
elif gen_type == "paragraphs":
|
||||||
|
text = '\n\n'.join(make_paragraph() for _ in range(count))
|
||||||
|
else:
|
||||||
|
return jsonify({"message": "Ungültiger Typ"}), 400
|
||||||
|
|
||||||
|
return jsonify({"text": text})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler lorem ipsum: {e}")
|
||||||
|
return jsonify({"message": "Fehler bei der Generierung"}), 500
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from util.logger import logger
|
||||||
|
from util.db_pool import get_connection
|
||||||
|
from auth.token import verify_token
|
||||||
|
|
||||||
|
notes_blueprint = Blueprint('notes_tool', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_table():
|
||||||
|
conn = get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS notes (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
content TEXT,
|
||||||
|
language VARCHAR(50) DEFAULT 'text',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@notes_blueprint.route('/api/notes', methods=['GET'])
|
||||||
|
def get_notes():
|
||||||
|
user = verify_token()
|
||||||
|
if not user:
|
||||||
|
return jsonify({"message": "Nicht autorisiert"}), 401
|
||||||
|
try:
|
||||||
|
ensure_table()
|
||||||
|
conn = get_connection()
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT id, title, content, language, created_at, updated_at FROM notes WHERE user_id = %s ORDER BY updated_at DESC",
|
||||||
|
(user['id'],)
|
||||||
|
)
|
||||||
|
notes = cursor.fetchall()
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
for n in notes:
|
||||||
|
if n.get('created_at'):
|
||||||
|
n['created_at'] = n['created_at'].isoformat()
|
||||||
|
if n.get('updated_at'):
|
||||||
|
n['updated_at'] = n['updated_at'].isoformat()
|
||||||
|
return jsonify(notes)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler notes GET: {e}")
|
||||||
|
return jsonify({"message": "Fehler beim Laden"}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@notes_blueprint.route('/api/notes', methods=['POST'])
|
||||||
|
def create_note():
|
||||||
|
user = verify_token()
|
||||||
|
if not user:
|
||||||
|
return jsonify({"message": "Nicht autorisiert"}), 401
|
||||||
|
try:
|
||||||
|
ensure_table()
|
||||||
|
data = request.get_json() or {}
|
||||||
|
title = data.get("title", "Neue Notiz").strip() or "Neue Notiz"
|
||||||
|
content = data.get("content", "")
|
||||||
|
language = data.get("language", "text")
|
||||||
|
|
||||||
|
conn = get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT INTO notes (user_id, title, content, language) VALUES (%s, %s, %s, %s)",
|
||||||
|
(user['id'], title, content, language)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
note_id = cursor.lastrowid
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
logger.info(f"Notiz erstellt von {user['username']}: id={note_id}")
|
||||||
|
return jsonify({"id": note_id, "title": title, "content": content, "language": language})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler notes POST: {e}")
|
||||||
|
return jsonify({"message": "Fehler beim Erstellen"}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@notes_blueprint.route('/api/notes/<int:note_id>', methods=['PUT'])
|
||||||
|
def update_note(note_id):
|
||||||
|
user = verify_token()
|
||||||
|
if not user:
|
||||||
|
return jsonify({"message": "Nicht autorisiert"}), 401
|
||||||
|
try:
|
||||||
|
data = request.get_json() or {}
|
||||||
|
title = data.get("title", "").strip() or "Neue Notiz"
|
||||||
|
content = data.get("content", "")
|
||||||
|
language = data.get("language", "text")
|
||||||
|
now = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||||
|
|
||||||
|
conn = get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"UPDATE notes SET title=%s, content=%s, language=%s, updated_at=%s WHERE id=%s AND user_id=%s",
|
||||||
|
(title, content, language, now, note_id, user['id'])
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
affected = cursor.rowcount
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if affected == 0:
|
||||||
|
return jsonify({"message": "Notiz nicht gefunden"}), 404
|
||||||
|
return jsonify({"ok": True})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler notes PUT: {e}")
|
||||||
|
return jsonify({"message": "Fehler beim Speichern"}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@notes_blueprint.route('/api/notes/<int:note_id>', methods=['DELETE'])
|
||||||
|
def delete_note(note_id):
|
||||||
|
user = verify_token()
|
||||||
|
if not user:
|
||||||
|
return jsonify({"message": "Nicht autorisiert"}), 401
|
||||||
|
try:
|
||||||
|
conn = get_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"DELETE FROM notes WHERE id=%s AND user_id=%s",
|
||||||
|
(note_id, user['id'])
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
affected = cursor.rowcount
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if affected == 0:
|
||||||
|
return jsonify({"message": "Notiz nicht gefunden"}), 404
|
||||||
|
return jsonify({"ok": True})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler notes DELETE: {e}")
|
||||||
|
return jsonify({"message": "Fehler beim Löschen"}), 500
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
from collections import Counter
|
||||||
|
import re
|
||||||
|
from util.logger import logger
|
||||||
|
from auth.token import verify_token
|
||||||
|
|
||||||
|
stringutils_blueprint = Blueprint('stringutils_tool', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@stringutils_blueprint.route('/api/string/analyze', methods=['POST'])
|
||||||
|
def string_analyze():
|
||||||
|
user = verify_token()
|
||||||
|
if not user:
|
||||||
|
return jsonify({"message": "Nicht autorisiert"}), 401
|
||||||
|
try:
|
||||||
|
data = request.get_json() or {}
|
||||||
|
text = data.get("text", "")
|
||||||
|
operation = data.get("operation", "stats")
|
||||||
|
|
||||||
|
if operation == "stats":
|
||||||
|
words = text.split() if text.strip() else []
|
||||||
|
lines = text.split('\n')
|
||||||
|
return jsonify({
|
||||||
|
"operation": "stats",
|
||||||
|
"chars": len(text),
|
||||||
|
"chars_no_spaces": len(text.replace(' ', '')),
|
||||||
|
"words": len(words),
|
||||||
|
"lines": len(lines),
|
||||||
|
"spaces": text.count(' '),
|
||||||
|
})
|
||||||
|
elif operation == "uppercase":
|
||||||
|
return jsonify({"operation": "uppercase", "result": text.upper()})
|
||||||
|
elif operation == "lowercase":
|
||||||
|
return jsonify({"operation": "lowercase", "result": text.lower()})
|
||||||
|
elif operation == "titlecase":
|
||||||
|
return jsonify({"operation": "titlecase", "result": text.title()})
|
||||||
|
elif operation == "reverse":
|
||||||
|
return jsonify({"operation": "reverse", "result": text[::-1]})
|
||||||
|
elif operation == "trim":
|
||||||
|
return jsonify({"operation": "trim", "result": text.strip()})
|
||||||
|
elif operation == "remove_spaces":
|
||||||
|
return jsonify({"operation": "remove_spaces", "result": text.replace(' ', '')})
|
||||||
|
elif operation == "count_words":
|
||||||
|
words = re.findall(r'\b\w+\b', text.lower())
|
||||||
|
counter = Counter(words)
|
||||||
|
top10 = [{"word": w, "count": c} for w, c in counter.most_common(10)]
|
||||||
|
return jsonify({"operation": "count_words", "words": top10})
|
||||||
|
else:
|
||||||
|
return jsonify({"message": "Unbekannte Operation"}), 400
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler stringutils: {e}")
|
||||||
|
return jsonify({"message": "Fehler bei der Verarbeitung"}), 500
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
from urllib.parse import quote, unquote
|
||||||
|
from util.logger import logger
|
||||||
|
from auth.token import verify_token
|
||||||
|
|
||||||
|
url_blueprint = Blueprint('url_tool', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@url_blueprint.route('/api/url/encode', methods=['POST'])
|
||||||
|
def url_encode():
|
||||||
|
user = verify_token()
|
||||||
|
if not user:
|
||||||
|
return jsonify({"message": "Nicht autorisiert"}), 401
|
||||||
|
try:
|
||||||
|
data = request.get_json() or {}
|
||||||
|
text = data.get("text", "")
|
||||||
|
result = quote(text, safe='')
|
||||||
|
return jsonify({"result": result})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler URL encode: {e}")
|
||||||
|
return jsonify({"message": "Fehler beim Encoding"}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@url_blueprint.route('/api/url/decode', methods=['POST'])
|
||||||
|
def url_decode():
|
||||||
|
user = verify_token()
|
||||||
|
if not user:
|
||||||
|
return jsonify({"message": "Nicht autorisiert"}), 401
|
||||||
|
try:
|
||||||
|
data = request.get_json() or {}
|
||||||
|
text = data.get("text", "")
|
||||||
|
result = unquote(text)
|
||||||
|
return jsonify({"result": result})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler URL decode: {e}")
|
||||||
|
return jsonify({"message": "Fehler beim Decoding"}), 500
|
||||||
@@ -14,6 +14,14 @@ import MarkdownTool from './components/MarkdownTool';
|
|||||||
import ColorConverterTool from './components/ColorConverterTool';
|
import ColorConverterTool from './components/ColorConverterTool';
|
||||||
import JsonFormatterTool from './components/JsonFormatterTool';
|
import JsonFormatterTool from './components/JsonFormatterTool';
|
||||||
import RegexTesterTool from './components/RegexTesterTool';
|
import RegexTesterTool from './components/RegexTesterTool';
|
||||||
|
import HashVerifierTool from './components/HashVerifierTool';
|
||||||
|
import UrlTool from './components/UrlTool';
|
||||||
|
import StringUtilsTool from './components/StringUtilsTool';
|
||||||
|
import CronExplainerTool from './components/CronExplainerTool';
|
||||||
|
import IpCalcTool from './components/IpCalcTool';
|
||||||
|
import LoremIpsumTool from './components/LoremIpsumTool';
|
||||||
|
import CsvViewerTool from './components/CsvViewerTool';
|
||||||
|
import NotesTool from './components/NotesTool';
|
||||||
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';
|
||||||
@@ -61,6 +69,14 @@ function App() {
|
|||||||
<Route path="/tools/color" element={isLoggedIn ? <ColorConverterTool /> : <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/json" element={isLoggedIn ? <JsonFormatterTool /> : <Navigate to="/login" />} />
|
||||||
<Route path="/tools/regex" element={isLoggedIn ? <RegexTesterTool /> : <Navigate to="/login" />} />
|
<Route path="/tools/regex" element={isLoggedIn ? <RegexTesterTool /> : <Navigate to="/login" />} />
|
||||||
|
<Route path="/tools/hashverify" element={isLoggedIn ? <HashVerifierTool /> : <Navigate to="/login" />} />
|
||||||
|
<Route path="/tools/url" element={isLoggedIn ? <UrlTool /> : <Navigate to="/login" />} />
|
||||||
|
<Route path="/tools/string" element={isLoggedIn ? <StringUtilsTool /> : <Navigate to="/login" />} />
|
||||||
|
<Route path="/tools/cron" element={isLoggedIn ? <CronExplainerTool /> : <Navigate to="/login" />} />
|
||||||
|
<Route path="/tools/ipcalc" element={isLoggedIn ? <IpCalcTool /> : <Navigate to="/login" />} />
|
||||||
|
<Route path="/tools/lorem" element={isLoggedIn ? <LoremIpsumTool /> : <Navigate to="/login" />} />
|
||||||
|
<Route path="/tools/csv" element={isLoggedIn ? <CsvViewerTool /> : <Navigate to="/login" />} />
|
||||||
|
<Route path="/tools/notes" element={isLoggedIn ? <NotesTool /> : <Navigate to="/login" />} />
|
||||||
<Route
|
<Route
|
||||||
path="/admin"
|
path="/admin"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import axios from '../services/api';
|
||||||
|
|
||||||
|
const sectionBox = {
|
||||||
|
marginTop: '14px',
|
||||||
|
padding: '14px',
|
||||||
|
background: 'var(--surface-2)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
};
|
||||||
|
|
||||||
|
const EXAMPLES = [
|
||||||
|
{ label: '* * * * *', value: '* * * * *' },
|
||||||
|
{ label: '0 9 * * 1-5', value: '0 9 * * 1-5' },
|
||||||
|
{ label: '0 0 1 * *', value: '0 0 1 * *' },
|
||||||
|
{ label: '*/15 * * * *', value: '*/15 * * * *' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const FIELD_LABELS = [
|
||||||
|
{ key: 'minute', label: 'Minute' },
|
||||||
|
{ key: 'hour', label: 'Stunde' },
|
||||||
|
{ key: 'day', label: 'Tag' },
|
||||||
|
{ key: 'month', label: 'Monat' },
|
||||||
|
{ key: 'weekday', label: 'Wochentag' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function CronExplainerTool() {
|
||||||
|
const [expression, setExpression] = useState('');
|
||||||
|
const [result, setResult] = useState(null);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
const explain = async (expr) => {
|
||||||
|
const val = expr !== undefined ? expr : expression;
|
||||||
|
setError('');
|
||||||
|
setResult(null);
|
||||||
|
if (!val.trim()) {
|
||||||
|
setError('Bitte Cron-Ausdruck eingeben.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await axios.post('/api/cron/explain', { expression: val });
|
||||||
|
setResult(res.data);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.error || err.response?.data?.message || 'Ungültiger Cron-Ausdruck');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const useExample = (val) => {
|
||||||
|
setExpression(val);
|
||||||
|
setError('');
|
||||||
|
setResult(null);
|
||||||
|
explain(val);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (iso) => {
|
||||||
|
const d = new Date(iso);
|
||||||
|
return d.toLocaleString('de-DE', {
|
||||||
|
weekday: 'short', day: '2-digit', month: '2-digit',
|
||||||
|
year: 'numeric', hour: '2-digit', minute: '2-digit',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="main-content">
|
||||||
|
<h2>Cron Erklärer</h2>
|
||||||
|
<p style={{ color: 'var(--muted)', marginBottom: '16px', fontSize: '0.95rem' }}>
|
||||||
|
Cron-Ausdrücke (5 Felder) analysieren und auf Deutsch erklären.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', marginBottom: '10px' }}>
|
||||||
|
{EXAMPLES.map(({ label, value }) => (
|
||||||
|
<button
|
||||||
|
key={value}
|
||||||
|
className="ghost"
|
||||||
|
onClick={() => useExample(value)}
|
||||||
|
style={{ fontFamily: 'monospace', fontSize: '0.85rem' }}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={expression}
|
||||||
|
onChange={(e) => { setExpression(e.target.value); setResult(null); setError(''); }}
|
||||||
|
placeholder="z.B. 0 9 * * 1-5"
|
||||||
|
style={{ fontFamily: 'monospace', flex: 1 }}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && explain()}
|
||||||
|
/>
|
||||||
|
<button onClick={() => explain()} style={{ flexShrink: 0 }}>Erklären</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <p className="error" style={{ marginTop: '12px' }}>{error}</p>}
|
||||||
|
|
||||||
|
{result && (
|
||||||
|
<>
|
||||||
|
<div style={{ ...sectionBox, borderColor: 'var(--accent)' }}>
|
||||||
|
<p style={{ color: 'var(--muted)', fontSize: '0.8rem', marginBottom: '4px', fontWeight: 600 }}>Erklärung</p>
|
||||||
|
<p style={{ color: 'var(--text)', fontWeight: 600, fontSize: '1.05rem' }}>{result.explanation}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={sectionBox}>
|
||||||
|
<p style={{ color: 'var(--muted)', fontSize: '0.8rem', marginBottom: '10px', fontWeight: 600 }}>Felder</p>
|
||||||
|
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style={{ textAlign: 'left', padding: '6px 10px', color: 'var(--muted)', fontSize: '0.8rem', fontWeight: 600, borderBottom: '1px solid var(--border)' }}>Feld</th>
|
||||||
|
<th style={{ textAlign: 'left', padding: '6px 10px', color: 'var(--muted)', fontSize: '0.8rem', fontWeight: 600, borderBottom: '1px solid var(--border)' }}>Ausdruck</th>
|
||||||
|
<th style={{ textAlign: 'left', padding: '6px 10px', color: 'var(--muted)', fontSize: '0.8rem', fontWeight: 600, borderBottom: '1px solid var(--border)' }}>Bedeutung</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{FIELD_LABELS.map(({ key, label }, i) => (
|
||||||
|
<tr key={key} style={{ background: i % 2 === 0 ? 'transparent' : 'rgba(255,255,255,0.02)' }}>
|
||||||
|
<td style={{ padding: '8px 10px', color: 'var(--accent)', fontWeight: 600, fontSize: '0.9rem' }}>{label}</td>
|
||||||
|
<td style={{ padding: '8px 10px', fontFamily: 'monospace', color: 'var(--text)', fontSize: '0.9rem' }}>
|
||||||
|
{expression.split(/\s+/)[i] || '—'}
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '8px 10px', color: 'var(--text)', fontSize: '0.9rem' }}>
|
||||||
|
{result.fields[key]}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{result.next_runs && result.next_runs.length > 0 && (
|
||||||
|
<div style={sectionBox}>
|
||||||
|
<p style={{ color: 'var(--muted)', fontSize: '0.8rem', marginBottom: '10px', fontWeight: 600 }}>Nächste 5 Ausführungen</p>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||||||
|
{result.next_runs.map((run, i) => (
|
||||||
|
<div key={run} style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: '10px',
|
||||||
|
padding: '8px 12px',
|
||||||
|
background: i === 0 ? 'rgba(34,211,238,0.06)' : 'transparent',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: i === 0 ? '1px solid rgba(34,211,238,0.2)' : '1px solid transparent',
|
||||||
|
}}>
|
||||||
|
<span style={{ color: 'var(--muted)', width: '20px', fontSize: '0.85rem' }}>#{i + 1}</span>
|
||||||
|
<span style={{ fontFamily: 'monospace', color: i === 0 ? 'var(--accent)' : 'var(--text)', fontSize: '0.9rem' }}>
|
||||||
|
{formatDate(run)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CronExplainerTool;
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import axios from '../services/api';
|
||||||
|
|
||||||
|
const DELIMITERS = [
|
||||||
|
{ label: 'Komma (,)', value: ',' },
|
||||||
|
{ label: 'Semikolon (;)', value: ';' },
|
||||||
|
{ label: 'Tab', value: '\\t' },
|
||||||
|
{ label: 'Pipe (|)', value: '|' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function CsvViewerTool() {
|
||||||
|
const [text, setText] = useState('');
|
||||||
|
const [delimiter, setDelimiter] = useState(',');
|
||||||
|
const [result, setResult] = useState(null);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [sortCol, setSortCol] = useState(null);
|
||||||
|
const [sortAsc, setSortAsc] = useState(true);
|
||||||
|
|
||||||
|
const parse = async () => {
|
||||||
|
setError('');
|
||||||
|
setResult(null);
|
||||||
|
setSortCol(null);
|
||||||
|
if (!text.trim()) {
|
||||||
|
setError('Bitte CSV-Inhalt eingeben.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await axios.post('/api/csv/parse', { text, delimiter });
|
||||||
|
setResult(res.data);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.message || 'Fehler beim Verarbeiten des CSV');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSort = (colIdx) => {
|
||||||
|
if (sortCol === colIdx) {
|
||||||
|
setSortAsc(!sortAsc);
|
||||||
|
} else {
|
||||||
|
setSortCol(colIdx);
|
||||||
|
setSortAsc(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSortedRows = () => {
|
||||||
|
if (!result) return [];
|
||||||
|
if (sortCol === null) return result.rows;
|
||||||
|
return [...result.rows].sort((a, b) => {
|
||||||
|
const av = a[sortCol] ?? '';
|
||||||
|
const bv = b[sortCol] ?? '';
|
||||||
|
const numA = parseFloat(av);
|
||||||
|
const numB = parseFloat(bv);
|
||||||
|
if (!isNaN(numA) && !isNaN(numB)) {
|
||||||
|
return sortAsc ? numA - numB : numB - numA;
|
||||||
|
}
|
||||||
|
return sortAsc ? av.localeCompare(bv) : bv.localeCompare(av);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="main-content">
|
||||||
|
<h2>CSV Viewer</h2>
|
||||||
|
<p style={{ color: 'var(--muted)', marginBottom: '16px', fontSize: '0.95rem' }}>
|
||||||
|
CSV-Daten einfügen und als Tabelle anzeigen.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
rows={6}
|
||||||
|
value={text}
|
||||||
|
onChange={(e) => { setText(e.target.value); setResult(null); setError(''); }}
|
||||||
|
placeholder="CSV-Inhalt hier einfügen..."
|
||||||
|
style={{ fontFamily: 'monospace', resize: 'vertical' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '8px', alignItems: 'center', marginTop: '8px', flexWrap: 'wrap' }}>
|
||||||
|
<select value={delimiter} onChange={(e) => setDelimiter(e.target.value)} style={{ margin: 0 }}>
|
||||||
|
{DELIMITERS.map(({ label, value }) => (
|
||||||
|
<option key={value} value={value}>{label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button onClick={parse}>Anzeigen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <p className="error" style={{ marginTop: '12px' }}>{error}</p>}
|
||||||
|
|
||||||
|
{result && (
|
||||||
|
<div style={{ marginTop: '16px' }}>
|
||||||
|
{result.truncated && (
|
||||||
|
<div style={{
|
||||||
|
padding: '8px 14px', background: 'rgba(245,158,11,0.1)',
|
||||||
|
border: '1px solid rgba(245,158,11,0.3)', borderRadius: '10px',
|
||||||
|
color: '#f59e0b', fontSize: '0.85rem', marginBottom: '10px',
|
||||||
|
}}>
|
||||||
|
Hinweis: Nur die ersten 500 von {result.total_rows} Zeilen werden angezeigt.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p style={{ color: 'var(--muted)', fontSize: '0.85rem', marginBottom: '8px' }}>
|
||||||
|
{result.rows.length} Zeilen · {result.headers.length} Spalten
|
||||||
|
{!result.truncated && result.total_rows > 0 && ` · ${result.total_rows} gesamt`}
|
||||||
|
</p>
|
||||||
|
<div style={{ overflowX: 'auto', borderRadius: '12px', border: '1px solid var(--border)' }}>
|
||||||
|
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.88rem' }}>
|
||||||
|
<thead>
|
||||||
|
<tr style={{ background: 'var(--surface-2)' }}>
|
||||||
|
<th style={{
|
||||||
|
padding: '8px 10px', textAlign: 'right', color: 'var(--muted)',
|
||||||
|
fontWeight: 600, borderBottom: '1px solid var(--border)',
|
||||||
|
fontSize: '0.78rem', width: '40px',
|
||||||
|
}}>#</th>
|
||||||
|
{result.headers.map((h, i) => (
|
||||||
|
<th
|
||||||
|
key={i}
|
||||||
|
onClick={() => handleSort(i)}
|
||||||
|
style={{
|
||||||
|
padding: '8px 12px', textAlign: 'left', color: 'var(--text)',
|
||||||
|
fontWeight: 600, borderBottom: '1px solid var(--border)',
|
||||||
|
cursor: 'pointer', userSelect: 'none',
|
||||||
|
background: sortCol === i ? 'rgba(34,211,238,0.06)' : 'transparent',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{h || `Spalte ${i + 1}`}
|
||||||
|
{sortCol === i && (
|
||||||
|
<span style={{ marginLeft: '4px', color: 'var(--accent)' }}>
|
||||||
|
{sortAsc ? '↑' : '↓'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{getSortedRows().map((row, ri) => (
|
||||||
|
<tr key={ri} style={{ borderBottom: '1px solid var(--border)' }}>
|
||||||
|
<td style={{
|
||||||
|
padding: '6px 10px', textAlign: 'right',
|
||||||
|
color: 'var(--muted)', fontSize: '0.78rem',
|
||||||
|
}}>{ri + 1}</td>
|
||||||
|
{result.headers.map((_, ci) => (
|
||||||
|
<td key={ci} style={{
|
||||||
|
padding: '6px 12px', color: 'var(--text)',
|
||||||
|
fontFamily: 'monospace', maxWidth: '300px',
|
||||||
|
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
||||||
|
}}>
|
||||||
|
{row[ci] ?? ''}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CsvViewerTool;
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import axios from '../services/api';
|
||||||
|
|
||||||
|
function HashVerifierTool() {
|
||||||
|
const [text, setText] = useState('');
|
||||||
|
const [hash, setHash] = useState('');
|
||||||
|
const [algo, setAlgo] = useState('sha256');
|
||||||
|
const [result, setResult] = useState(null);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const verify = async () => {
|
||||||
|
setError('');
|
||||||
|
setResult(null);
|
||||||
|
if (!text || !hash) {
|
||||||
|
setError('Bitte Text und Hash eingeben.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await axios.post('/api/hash/verify', { text, hash, algorithm: algo });
|
||||||
|
setResult(res.data.match);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.message || 'Fehler bei der Verifikation');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="main-content">
|
||||||
|
<h2>Hash Verifier</h2>
|
||||||
|
<p style={{ color: 'var(--muted)', marginBottom: '16px', fontSize: '0.95rem' }}>
|
||||||
|
Prüft, ob ein Text mit einem gegebenen Hash übereinstimmt.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={text}
|
||||||
|
onChange={(e) => { setText(e.target.value); setResult(null); setError(''); }}
|
||||||
|
placeholder="Originaltext"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={hash}
|
||||||
|
onChange={(e) => { setHash(e.target.value); setResult(null); setError(''); }}
|
||||||
|
placeholder="Hash-Wert"
|
||||||
|
style={{ marginTop: '8px', fontFamily: 'monospace' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<select
|
||||||
|
value={algo}
|
||||||
|
onChange={(e) => { setAlgo(e.target.value); setResult(null); }}
|
||||||
|
style={{ marginTop: '8px' }}
|
||||||
|
>
|
||||||
|
<option value="md5">MD5</option>
|
||||||
|
<option value="sha256">SHA256</option>
|
||||||
|
<option value="bcrypt">bcrypt</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<button onClick={verify} disabled={loading} style={{ marginTop: '8px' }}>
|
||||||
|
{loading ? 'Prüfen...' : 'Prüfen'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{error && <p className="error" style={{ marginTop: '12px' }}>{error}</p>}
|
||||||
|
|
||||||
|
{result !== null && (
|
||||||
|
<div style={{
|
||||||
|
marginTop: '24px',
|
||||||
|
padding: '32px',
|
||||||
|
background: 'var(--surface-2)',
|
||||||
|
border: `2px solid ${result ? '#22c55e' : '#ef4444'}`,
|
||||||
|
borderRadius: '16px',
|
||||||
|
textAlign: 'center',
|
||||||
|
}}>
|
||||||
|
<div style={{ fontSize: '3rem', marginBottom: '8px' }}>
|
||||||
|
{result ? '✓' : '✗'}
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
fontSize: '1.4rem',
|
||||||
|
fontWeight: 700,
|
||||||
|
color: result ? '#22c55e' : '#ef4444',
|
||||||
|
}}>
|
||||||
|
{result ? 'Übereinstimmung' : 'Kein Match'}
|
||||||
|
</div>
|
||||||
|
<div style={{ color: 'var(--muted)', marginTop: '6px', fontSize: '0.9rem' }}>
|
||||||
|
Algorithmus: {algo.toUpperCase()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HashVerifierTool;
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import axios from '../services/api';
|
||||||
|
|
||||||
|
const EXAMPLES = ['192.168.0.0/24', '10.0.0.0/8', '172.16.0.0/12'];
|
||||||
|
|
||||||
|
const INFO_FIELDS = [
|
||||||
|
{ key: 'network', label: 'Netzwerkadresse' },
|
||||||
|
{ key: 'broadcast', label: 'Broadcast-Adresse' },
|
||||||
|
{ key: 'netmask', label: 'Subnetzmaske' },
|
||||||
|
{ key: 'wildcard', label: 'Wildcard-Maske' },
|
||||||
|
{ key: 'first_host', label: 'Erste nutzbare IP' },
|
||||||
|
{ key: 'last_host', label: 'Letzte nutzbare IP' },
|
||||||
|
{ key: 'total_hosts', label: 'Nutzbare Hosts' },
|
||||||
|
{ key: 'prefix_length', label: 'Präfixlänge', format: (v) => `/${v}` },
|
||||||
|
{ key: 'ip_class', label: 'IP-Klasse' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function IpCalcTool() {
|
||||||
|
const [cidr, setCidr] = useState('');
|
||||||
|
const [result, setResult] = useState(null);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [copiedKey, setCopiedKey] = useState('');
|
||||||
|
|
||||||
|
const calculate = async (val) => {
|
||||||
|
const input = val !== undefined ? val : cidr;
|
||||||
|
setError('');
|
||||||
|
setResult(null);
|
||||||
|
if (!input.trim()) {
|
||||||
|
setError('Bitte CIDR eingeben.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await axios.post('/api/ip/calculate', { cidr: input });
|
||||||
|
setResult(res.data);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.message || 'Fehler bei der Berechnung');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const useExample = (val) => {
|
||||||
|
setCidr(val);
|
||||||
|
setError('');
|
||||||
|
setResult(null);
|
||||||
|
calculate(val);
|
||||||
|
};
|
||||||
|
|
||||||
|
const copy = (key, text) => {
|
||||||
|
navigator.clipboard.writeText(String(text));
|
||||||
|
setCopiedKey(key);
|
||||||
|
setTimeout(() => setCopiedKey(''), 1500);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="main-content">
|
||||||
|
<h2>IP / Subnetz Rechner</h2>
|
||||||
|
<p style={{ color: 'var(--muted)', marginBottom: '16px', fontSize: '0.95rem' }}>
|
||||||
|
CIDR-Netzwerke berechnen und analysieren.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', marginBottom: '10px' }}>
|
||||||
|
{EXAMPLES.map((ex) => (
|
||||||
|
<button key={ex} className="ghost" onClick={() => useExample(ex)}
|
||||||
|
style={{ fontFamily: 'monospace', fontSize: '0.85rem' }}>
|
||||||
|
{ex}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={cidr}
|
||||||
|
onChange={(e) => { setCidr(e.target.value); setResult(null); setError(''); }}
|
||||||
|
placeholder="192.168.1.0/24"
|
||||||
|
style={{ fontFamily: 'monospace', flex: 1 }}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && calculate()}
|
||||||
|
/>
|
||||||
|
<button onClick={() => calculate()} style={{ flexShrink: 0 }}>Berechnen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <p className="error" style={{ marginTop: '12px' }}>{error}</p>}
|
||||||
|
|
||||||
|
{result && (
|
||||||
|
<div style={{
|
||||||
|
marginTop: '16px',
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))',
|
||||||
|
gap: '10px',
|
||||||
|
}}>
|
||||||
|
{INFO_FIELDS.map(({ key, label, format }) => {
|
||||||
|
const value = format ? format(result[key]) : result[key];
|
||||||
|
return (
|
||||||
|
<div key={key} style={{
|
||||||
|
padding: '12px 14px',
|
||||||
|
background: 'var(--surface-2)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '4px',
|
||||||
|
}}>
|
||||||
|
<span style={{ fontSize: '0.78rem', color: 'var(--muted)', fontWeight: 600 }}>{label}</span>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px' }}>
|
||||||
|
<span style={{ fontFamily: 'monospace', color: 'var(--text)', fontSize: '0.95rem', wordBreak: 'break-all' }}>
|
||||||
|
{String(value)}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
className="ghost"
|
||||||
|
onClick={() => copy(key, value)}
|
||||||
|
style={{ flexShrink: 0, margin: 0, padding: '4px 10px', fontSize: '0.78rem' }}
|
||||||
|
>
|
||||||
|
{copiedKey === key ? '✓' : 'Kopieren'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IpCalcTool;
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import axios from '../services/api';
|
||||||
|
|
||||||
|
const TYPES = [
|
||||||
|
{ value: 'words', label: 'Wörter' },
|
||||||
|
{ value: 'sentences', label: 'Sätze' },
|
||||||
|
{ value: 'paragraphs', label: 'Absätze' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function LoremIpsumTool() {
|
||||||
|
const [type, setType] = useState('sentences');
|
||||||
|
const [count, setCount] = useState(3);
|
||||||
|
const [result, setResult] = useState('');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
const generate = async () => {
|
||||||
|
setError('');
|
||||||
|
setResult('');
|
||||||
|
try {
|
||||||
|
const res = await axios.post('/api/lorem/generate', { type, count });
|
||||||
|
setResult(res.data.text);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.message || 'Fehler bei der Generierung');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const copy = () => {
|
||||||
|
navigator.clipboard.writeText(result);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 1500);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="main-content">
|
||||||
|
<h2>Lorem Ipsum Generator</h2>
|
||||||
|
<p style={{ color: 'var(--muted)', marginBottom: '16px', fontSize: '0.95rem' }}>
|
||||||
|
Blindtext für Designs und Mockups generieren.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '6px', marginBottom: '12px' }}>
|
||||||
|
{TYPES.map(({ value, label }) => (
|
||||||
|
<button
|
||||||
|
key={value}
|
||||||
|
className={type === value ? '' : 'ghost'}
|
||||||
|
onClick={() => setType(value)}
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px' }}>
|
||||||
|
<label style={{ color: 'var(--text)', fontWeight: 600, fontSize: '0.9rem', whiteSpace: 'nowrap' }}>
|
||||||
|
Anzahl:
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={20}
|
||||||
|
value={count}
|
||||||
|
onChange={(e) => setCount(Math.max(1, Math.min(20, Number(e.target.value))))}
|
||||||
|
style={{ width: '80px' }}
|
||||||
|
/>
|
||||||
|
<span style={{ color: 'var(--muted)', fontSize: '0.85rem' }}>(1–20)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button onClick={generate}>Generieren</button>
|
||||||
|
|
||||||
|
{error && <p className="error" style={{ marginTop: '12px' }}>{error}</p>}
|
||||||
|
|
||||||
|
{result && (
|
||||||
|
<div style={{ marginTop: '14px' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: '6px' }}>
|
||||||
|
<button className="ghost" onClick={copy} style={{ margin: 0, padding: '6px 14px', fontSize: '0.85rem' }}>
|
||||||
|
{copied ? 'Kopiert!' : 'Kopieren'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
rows={10}
|
||||||
|
readOnly
|
||||||
|
value={result}
|
||||||
|
style={{ resize: 'vertical', color: 'var(--muted)', fontFamily: 'inherit', lineHeight: '1.6' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoremIpsumTool;
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
|
import axios from '../services/api';
|
||||||
|
|
||||||
|
const LANGUAGES = [
|
||||||
|
{ value: 'text', label: 'Text' },
|
||||||
|
{ value: 'python', label: 'Python' },
|
||||||
|
{ value: 'javascript', label: 'JavaScript' },
|
||||||
|
{ value: 'sql', label: 'SQL' },
|
||||||
|
{ value: 'bash', label: 'Bash' },
|
||||||
|
{ value: 'json', label: 'JSON' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function NotesTool() {
|
||||||
|
const [notes, setNotes] = useState([]);
|
||||||
|
const [selected, setSelected] = useState(null);
|
||||||
|
const [title, setTitle] = useState('Neue Notiz');
|
||||||
|
const [content, setContent] = useState('');
|
||||||
|
const [language, setLanguage] = useState('text');
|
||||||
|
const [saved, setSaved] = useState(false);
|
||||||
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const debounceRef = useRef(null);
|
||||||
|
const isNew = selected === null;
|
||||||
|
|
||||||
|
const loadNotes = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const res = await axios.get('/api/notes');
|
||||||
|
setNotes(res.data);
|
||||||
|
} catch {
|
||||||
|
setError('Fehler beim Laden der Notizen');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => { loadNotes(); }, [loadNotes]);
|
||||||
|
|
||||||
|
const selectNote = (note) => {
|
||||||
|
setSelected(note.id);
|
||||||
|
setTitle(note.title);
|
||||||
|
setContent(note.content || '');
|
||||||
|
setLanguage(note.language || 'text');
|
||||||
|
setSaved(false);
|
||||||
|
setConfirmDelete(false);
|
||||||
|
setError('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const newNote = () => {
|
||||||
|
setSelected(null);
|
||||||
|
setTitle('Neue Notiz');
|
||||||
|
setContent('');
|
||||||
|
setLanguage('text');
|
||||||
|
setSaved(false);
|
||||||
|
setConfirmDelete(false);
|
||||||
|
setError('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
setError('');
|
||||||
|
try {
|
||||||
|
if (isNew) {
|
||||||
|
const res = await axios.post('/api/notes', { title, content, language });
|
||||||
|
setSelected(res.data.id);
|
||||||
|
await loadNotes();
|
||||||
|
} else {
|
||||||
|
await axios.put(`/api/notes/${selected}`, { title, content, language });
|
||||||
|
await loadNotes();
|
||||||
|
}
|
||||||
|
setSaved(true);
|
||||||
|
setTimeout(() => setSaved(false), 2000);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.message || 'Fehler beim Speichern');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteNote = async () => {
|
||||||
|
if (!confirmDelete) { setConfirmDelete(true); return; }
|
||||||
|
try {
|
||||||
|
await axios.delete(`/api/notes/${selected}`);
|
||||||
|
setNotes(prev => prev.filter(n => n.id !== selected));
|
||||||
|
newNote();
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.message || 'Fehler beim Löschen');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Debounced auto-save for existing notes
|
||||||
|
useEffect(() => {
|
||||||
|
if (isNew) return;
|
||||||
|
clearTimeout(debounceRef.current);
|
||||||
|
debounceRef.current = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
await axios.put(`/api/notes/${selected}`, { title, content, language });
|
||||||
|
await loadNotes();
|
||||||
|
setSaved(true);
|
||||||
|
setTimeout(() => setSaved(false), 1500);
|
||||||
|
} catch { /* silent */ }
|
||||||
|
}, 1000);
|
||||||
|
return () => clearTimeout(debounceRef.current);
|
||||||
|
}, [title, content, language]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
const formatDate = (iso) => {
|
||||||
|
if (!iso) return '';
|
||||||
|
return new Date(iso).toLocaleString('de-DE', {
|
||||||
|
day: '2-digit', month: '2-digit', year: '2-digit',
|
||||||
|
hour: '2-digit', minute: '2-digit',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="main-content" style={{ maxWidth: '1100px' }}>
|
||||||
|
<h2>Notizen & Snippets</h2>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '260px 1fr', gap: '16px', alignItems: 'start' }}>
|
||||||
|
{/* Sidebar */}
|
||||||
|
<div style={{
|
||||||
|
background: 'var(--surface-2)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}>
|
||||||
|
<div style={{ padding: '10px', borderBottom: '1px solid var(--border)' }}>
|
||||||
|
<button onClick={newNote} style={{ width: '100%', margin: 0, padding: '8px', fontSize: '0.85rem' }}>
|
||||||
|
+ Neue Notiz
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div style={{ maxHeight: '500px', overflowY: 'auto' }}>
|
||||||
|
{notes.length === 0 ? (
|
||||||
|
<p style={{ color: 'var(--muted)', padding: '16px', fontSize: '0.85rem', textAlign: 'center' }}>
|
||||||
|
Keine Notizen vorhanden
|
||||||
|
</p>
|
||||||
|
) : notes.map((note) => (
|
||||||
|
<div
|
||||||
|
key={note.id}
|
||||||
|
onClick={() => selectNote(note)}
|
||||||
|
style={{
|
||||||
|
padding: '10px 14px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
borderBottom: '1px solid var(--border)',
|
||||||
|
background: selected === note.id ? 'rgba(34,211,238,0.08)' : 'transparent',
|
||||||
|
borderLeft: selected === note.id ? '3px solid var(--accent)' : '3px solid transparent',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{
|
||||||
|
fontWeight: 600, color: 'var(--text)', fontSize: '0.9rem',
|
||||||
|
marginBottom: '2px', overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
||||||
|
}}>
|
||||||
|
{note.title}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '0.75rem', color: 'var(--muted)' }}>
|
||||||
|
{formatDate(note.updated_at)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Editor */}
|
||||||
|
<div style={{
|
||||||
|
background: 'var(--surface-2)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
padding: '16px',
|
||||||
|
}}>
|
||||||
|
<div style={{ display: 'flex', gap: '8px', marginBottom: '10px', flexWrap: 'wrap', alignItems: 'center' }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => { setTitle(e.target.value); setSaved(false); }}
|
||||||
|
placeholder="Titel"
|
||||||
|
style={{ flex: 1, margin: 0, minWidth: '150px' }}
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
value={language}
|
||||||
|
onChange={(e) => { setLanguage(e.target.value); setSaved(false); }}
|
||||||
|
style={{ margin: 0, width: 'auto' }}
|
||||||
|
>
|
||||||
|
{LANGUAGES.map(({ value, label }) => (
|
||||||
|
<option key={value} value={value}>{label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
rows={16}
|
||||||
|
value={content}
|
||||||
|
onChange={(e) => { setContent(e.target.value); setSaved(false); }}
|
||||||
|
placeholder="Inhalt..."
|
||||||
|
style={{ fontFamily: 'monospace', resize: 'vertical', fontSize: '0.88rem', margin: 0 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{error && <p className="error" style={{ marginTop: '8px', marginBottom: 0 }}>{error}</p>}
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '8px', marginTop: '10px', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||||
|
<button onClick={save} style={{ margin: 0 }}>
|
||||||
|
{isNew ? 'Erstellen' : 'Speichern'}
|
||||||
|
</button>
|
||||||
|
{!isNew && (
|
||||||
|
<button
|
||||||
|
className="ghost"
|
||||||
|
onClick={deleteNote}
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
color: confirmDelete ? '#ef4444' : 'var(--text)',
|
||||||
|
borderColor: confirmDelete ? '#ef4444' : 'var(--border)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{confirmDelete ? 'Wirklich löschen?' : 'Löschen'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{confirmDelete && (
|
||||||
|
<button className="ghost" onClick={() => setConfirmDelete(false)} style={{ margin: 0 }}>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{saved && (
|
||||||
|
<span style={{ color: '#22c55e', fontSize: '0.85rem', fontWeight: 600 }}>✓ Gespeichert</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NotesTool;
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import axios from '../services/api';
|
||||||
|
|
||||||
|
const resultBox = {
|
||||||
|
marginTop: '12px',
|
||||||
|
padding: '12px 14px',
|
||||||
|
background: 'var(--surface-2)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
gap: '10px',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
};
|
||||||
|
|
||||||
|
const statCard = {
|
||||||
|
padding: '12px 16px',
|
||||||
|
background: 'var(--surface-2)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
textAlign: 'center',
|
||||||
|
flex: '1 1 120px',
|
||||||
|
};
|
||||||
|
|
||||||
|
const TRANSFORMS = [
|
||||||
|
{ op: 'uppercase', label: 'Großbuchstaben' },
|
||||||
|
{ op: 'lowercase', label: 'Kleinbuchstaben' },
|
||||||
|
{ op: 'titlecase', label: 'Titelschreibung' },
|
||||||
|
{ op: 'reverse', label: 'Umkehren' },
|
||||||
|
{ op: 'trim', label: 'Leerzeichen trim' },
|
||||||
|
{ op: 'remove_spaces', label: 'Leerzeichen entfernen' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function StringUtilsTool() {
|
||||||
|
const [input, setInput] = useState('');
|
||||||
|
const [result, setResult] = useState(null);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
const run = async (op) => {
|
||||||
|
setError('');
|
||||||
|
setResult(null);
|
||||||
|
if (!input) {
|
||||||
|
setError('Bitte Text eingeben.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await axios.post('/api/string/analyze', { text: input, operation: op });
|
||||||
|
setResult(res.data);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.message || 'Fehler bei der Verarbeitung');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const copy = (text) => {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 1500);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="main-content">
|
||||||
|
<h2>String Utilities</h2>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
rows={6}
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => { setInput(e.target.value); setResult(null); setError(''); }}
|
||||||
|
placeholder="Text eingeben..."
|
||||||
|
style={{ resize: 'vertical' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div style={{ marginTop: '12px' }}>
|
||||||
|
<p style={{ color: 'var(--muted)', fontWeight: 600, marginBottom: '8px', fontSize: '0.9rem' }}>Analyse</p>
|
||||||
|
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||||
|
<button onClick={() => run('stats')}>Statistiken</button>
|
||||||
|
<button onClick={() => run('count_words')}>Worthäufigkeit</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: '12px' }}>
|
||||||
|
<p style={{ color: 'var(--muted)', fontWeight: 600, marginBottom: '8px', fontSize: '0.9rem' }}>Transformation</p>
|
||||||
|
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||||
|
{TRANSFORMS.map(({ op, label }) => (
|
||||||
|
<button key={op} className="ghost" onClick={() => run(op)}>{label}</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <p className="error" style={{ marginTop: '12px' }}>{error}</p>}
|
||||||
|
|
||||||
|
{result && result.operation === 'stats' && (
|
||||||
|
<div style={{ marginTop: '16px' }}>
|
||||||
|
<p style={{ color: 'var(--muted)', fontWeight: 600, marginBottom: '10px', fontSize: '0.9rem' }}>Statistiken</p>
|
||||||
|
<div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
|
||||||
|
{[
|
||||||
|
{ label: 'Zeichen', value: result.chars },
|
||||||
|
{ label: 'Ohne Leerzeichen', value: result.chars_no_spaces },
|
||||||
|
{ label: 'Wörter', value: result.words },
|
||||||
|
{ label: 'Zeilen', value: result.lines },
|
||||||
|
{ label: 'Leerzeichen', value: result.spaces },
|
||||||
|
].map(({ label, value }) => (
|
||||||
|
<div key={label} style={statCard}>
|
||||||
|
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: 'var(--accent)' }}>{value}</div>
|
||||||
|
<div style={{ fontSize: '0.8rem', color: 'var(--muted)', marginTop: '4px' }}>{label}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{result && result.operation === 'count_words' && (
|
||||||
|
<div style={{ marginTop: '16px' }}>
|
||||||
|
<p style={{ color: 'var(--muted)', fontWeight: 600, marginBottom: '10px', fontSize: '0.9rem' }}>Top-Wörter</p>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||||||
|
{result.words.map(({ word, count }, i) => (
|
||||||
|
<div key={word} style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: '10px',
|
||||||
|
padding: '8px 12px',
|
||||||
|
background: 'var(--surface-2)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: '10px',
|
||||||
|
}}>
|
||||||
|
<span style={{ color: 'var(--muted)', width: '20px', fontSize: '0.85rem' }}>#{i + 1}</span>
|
||||||
|
<span style={{ fontFamily: 'monospace', color: 'var(--text)', flex: 1 }}>{word}</span>
|
||||||
|
<span style={{
|
||||||
|
background: 'var(--accent)', color: '#0b1224',
|
||||||
|
borderRadius: '20px', padding: '2px 10px',
|
||||||
|
fontSize: '0.8rem', fontWeight: 700,
|
||||||
|
}}>{count}×</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{result && result.result !== undefined && (
|
||||||
|
<div style={resultBox}>
|
||||||
|
<span style={{ wordBreak: 'break-all', color: 'var(--text)', fontFamily: 'monospace', fontSize: '0.9rem', flex: 1, whiteSpace: 'pre-wrap' }}>
|
||||||
|
{result.result}
|
||||||
|
</span>
|
||||||
|
<button className="ghost" onClick={() => copy(result.result)} style={{ flexShrink: 0, margin: 0 }}>
|
||||||
|
{copied ? 'Kopiert!' : 'Kopieren'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StringUtilsTool;
|
||||||
@@ -15,6 +15,14 @@ const TOOLS = [
|
|||||||
{ icon: '🎨', path: '/tools/color', title: 'Farb-Konverter', desc: 'HEX, RGB und HSL konvertieren' },
|
{ 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/json', title: 'JSON Formatter', desc: 'JSON formatieren und validieren' },
|
||||||
{ icon: '🔍', path: '/tools/regex', title: 'Regex Tester', desc: 'Reguläre Ausdrücke live testen' },
|
{ icon: '🔍', path: '/tools/regex', title: 'Regex Tester', desc: 'Reguläre Ausdrücke live testen' },
|
||||||
|
{ icon: '✅', path: '/tools/hashverify', title: 'Hash Verifier', desc: 'Prüfen ob Text und Hash übereinstimmen' },
|
||||||
|
{ icon: '🔗', path: '/tools/url', title: 'URL Encoder/Decoder', desc: 'URLs kodieren und dekodieren' },
|
||||||
|
{ icon: '✏️', path: '/tools/string', title: 'String Utilities', desc: 'Text analysieren und transformieren' },
|
||||||
|
{ icon: '⏰', path: '/tools/cron', title: 'Cron Erklärer', desc: 'Cron-Ausdrücke auf Deutsch erklären' },
|
||||||
|
{ icon: '🌐', path: '/tools/ipcalc', title: 'IP / Subnetz Rechner', desc: 'CIDR Netzwerke berechnen' },
|
||||||
|
{ icon: '📃', path: '/tools/lorem', title: 'Lorem Ipsum Generator', desc: 'Blindtext generieren' },
|
||||||
|
{ icon: '📊', path: '/tools/csv', title: 'CSV Viewer', desc: 'CSV-Daten als Tabelle anzeigen' },
|
||||||
|
{ icon: '🗒️', path: '/tools/notes', title: 'Notizen & Snippets', desc: 'Code und Texte speichern' },
|
||||||
];
|
];
|
||||||
|
|
||||||
function ToolOverview() {
|
function ToolOverview() {
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import axios from '../services/api';
|
||||||
|
|
||||||
|
const resultBox = {
|
||||||
|
marginTop: '12px',
|
||||||
|
padding: '12px 14px',
|
||||||
|
background: 'var(--surface-2)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
gap: '10px',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
};
|
||||||
|
|
||||||
|
function UrlTool() {
|
||||||
|
const [input, setInput] = useState('');
|
||||||
|
const [result, setResult] = useState('');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
const request = async (action) => {
|
||||||
|
setError('');
|
||||||
|
setResult('');
|
||||||
|
if (!input) {
|
||||||
|
setError('Bitte Text eingeben.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await axios.post(`/api/url/${action}`, { text: input });
|
||||||
|
setResult(res.data.result);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.message || `Fehler beim ${action === 'encode' ? 'Encoding' : 'Decoding'}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const copy = () => {
|
||||||
|
navigator.clipboard.writeText(result);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 1500);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="main-content">
|
||||||
|
<h2>URL Encoder / Decoder</h2>
|
||||||
|
<p style={{ color: 'var(--muted)', marginBottom: '16px', fontSize: '0.95rem' }}>
|
||||||
|
URLs und Strings kodieren oder dekodieren.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
rows={4}
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => { setInput(e.target.value); setResult(''); setError(''); }}
|
||||||
|
placeholder="Text oder URL eingeben..."
|
||||||
|
style={{ fontFamily: 'monospace', resize: 'vertical' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '8px', marginTop: '8px' }}>
|
||||||
|
<button onClick={() => request('encode')}>Encode</button>
|
||||||
|
<button className="ghost" onClick={() => request('decode')}>Decode</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <p className="error" style={{ marginTop: '12px' }}>{error}</p>}
|
||||||
|
|
||||||
|
{result && (
|
||||||
|
<div style={resultBox}>
|
||||||
|
<span style={{ wordBreak: 'break-all', color: 'var(--text)', fontFamily: 'monospace', fontSize: '0.9rem', flex: 1 }}>
|
||||||
|
{result}
|
||||||
|
</span>
|
||||||
|
<button className="ghost" onClick={copy} style={{ flexShrink: 0, margin: 0 }}>
|
||||||
|
{copied ? 'Kopiert!' : 'Kopieren'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UrlTool;
|
||||||
Reference in New Issue
Block a user