mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-22 10:51:02 +01:00
Initial Commit
This commit is contained in:
committed by
Sam Chau
parent
28e0cad4a1
commit
525d86063d
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# Node
|
||||
node_modules/
|
||||
dist/
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
10
backend/Dockerfile
Normal file
10
backend/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
FROM python:3.9
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD ["flask", "run", "--host=0.0.0.0"]
|
||||
14
backend/app.py
Normal file
14
backend/app.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from flask import Flask
|
||||
from flask_cors import CORS
|
||||
from app.routes import regex_routes, format_routes
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
CORS(app, resources={r"/*": {"origins": "http://localhost:3000"}})
|
||||
app.register_blueprint(regex_routes.bp)
|
||||
app.register_blueprint(format_routes.bp)
|
||||
return app
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = create_app()
|
||||
app.run(debug=True)
|
||||
0
backend/app/__init__.py
Normal file
0
backend/app/__init__.py
Normal file
0
backend/app/routes/__init__.py
Normal file
0
backend/app/routes/__init__.py
Normal file
33
backend/app/routes/format_routes.py
Normal file
33
backend/app/routes/format_routes.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# app/routes/format_routes.py
|
||||
|
||||
from flask import Blueprint, request, jsonify
|
||||
from app.utils.file_operations import FORMAT_DIR, REGEX_DIR, save_to_file, load_all_from_directory, delete_file, load_from_file
|
||||
|
||||
bp = Blueprint('format', __name__, url_prefix='/format')
|
||||
|
||||
@bp.route('', methods=['GET', 'POST'])
|
||||
def handle_items():
|
||||
if request.method == 'POST':
|
||||
data = request.json
|
||||
saved_data = save_to_file(FORMAT_DIR, data)
|
||||
return jsonify(saved_data), 201
|
||||
else:
|
||||
items = load_all_from_directory(FORMAT_DIR)
|
||||
return jsonify(items)
|
||||
|
||||
@bp.route('/<int:id>', methods=['GET', 'PUT', 'DELETE'])
|
||||
def handle_item(id):
|
||||
if request.method == 'GET':
|
||||
item = load_from_file(FORMAT_DIR, id)
|
||||
if item:
|
||||
return jsonify(item)
|
||||
return jsonify({"error": "Item not found"}), 404
|
||||
elif request.method == 'PUT':
|
||||
data = request.json
|
||||
data['id'] = id
|
||||
saved_data = save_to_file(FORMAT_DIR, data)
|
||||
return jsonify(saved_data)
|
||||
elif request.method == 'DELETE':
|
||||
if delete_file(FORMAT_DIR, id):
|
||||
return jsonify({"message": f"Item with ID {id} deleted."}), 200
|
||||
return jsonify({"error": f"Item with ID {id} not found."}), 404
|
||||
72
backend/app/routes/regex_routes.py
Normal file
72
backend/app/routes/regex_routes.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from app.utils.file_operations import REGEX_DIR, save_to_file, load_all_from_directory, delete_file, load_from_file
|
||||
import requests
|
||||
import logging
|
||||
|
||||
bp = Blueprint('regex', __name__, url_prefix='/regex')
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
@bp.route('/regex101', methods=['POST'])
|
||||
def regex101_proxy():
|
||||
try:
|
||||
# Log the incoming request data
|
||||
logging.debug(f"Received data from frontend: {request.json}")
|
||||
|
||||
# Validate the request data before sending
|
||||
required_fields = ['regex', 'flags', 'delimiter', 'flavor']
|
||||
for field in required_fields:
|
||||
if field not in request.json:
|
||||
logging.error(f"Missing required field: {field}")
|
||||
return jsonify({"error": f"Missing required field: {field}"}), 400
|
||||
|
||||
# Send the POST request to regex101 with a timeout
|
||||
response = requests.post('https://regex101.com/api/regex', json=request.json, timeout=10)
|
||||
|
||||
# Log the response from regex101
|
||||
logging.debug(f"Response from regex101: {response.text}")
|
||||
|
||||
# Return the JSON response and status code back to the frontend
|
||||
return jsonify(response.json()), response.status_code
|
||||
|
||||
except requests.RequestException as e:
|
||||
# Log the exception details
|
||||
logging.error(f"Request to regex101 failed: {e}")
|
||||
return jsonify({"error": "Failed to connect to regex101"}), 500
|
||||
|
||||
except Exception as e:
|
||||
# Log any other exception that might occur
|
||||
logging.error(f"An unexpected error occurred: {e}")
|
||||
return jsonify({"error": "An unexpected error occurred"}), 500
|
||||
|
||||
@bp.route('', methods=['GET', 'POST'])
|
||||
def handle_items():
|
||||
if request.method == 'POST':
|
||||
data = request.json
|
||||
# Ensure regex101Link is included in the data
|
||||
if 'regex101Link' not in data:
|
||||
data['regex101Link'] = ''
|
||||
saved_data = save_to_file(REGEX_DIR, data)
|
||||
return jsonify(saved_data), 201
|
||||
else:
|
||||
items = load_all_from_directory(REGEX_DIR)
|
||||
return jsonify(items)
|
||||
|
||||
@bp.route('/<int:id>', methods=['GET', 'PUT', 'DELETE'])
|
||||
def handle_item(id):
|
||||
if request.method == 'GET':
|
||||
item = load_from_file(REGEX_DIR, id)
|
||||
if item:
|
||||
return jsonify(item)
|
||||
return jsonify({"error": "Item not found"}), 404
|
||||
elif request.method == 'PUT':
|
||||
data = request.json
|
||||
data['id'] = id
|
||||
# Ensure regex101Link is included in the data
|
||||
if 'regex101Link' not in data:
|
||||
data['regex101Link'] = ''
|
||||
saved_data = save_to_file(REGEX_DIR, data)
|
||||
return jsonify(saved_data)
|
||||
elif request.method == 'DELETE':
|
||||
if delete_file(REGEX_DIR, id):
|
||||
return jsonify({"message": f"Item with ID {id} deleted."}), 200
|
||||
return jsonify({"error": f"Item with ID {id} not found."}), 404
|
||||
0
backend/app/utils/__init__.py
Normal file
0
backend/app/utils/__init__.py
Normal file
91
backend/app/utils/file_operations.py
Normal file
91
backend/app/utils/file_operations.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import os
|
||||
import yaml
|
||||
import datetime
|
||||
|
||||
REGEX_DIR = 'regex_patterns'
|
||||
FORMAT_DIR = 'custom_formats'
|
||||
|
||||
os.makedirs(REGEX_DIR, exist_ok=True)
|
||||
os.makedirs(FORMAT_DIR, exist_ok=True)
|
||||
|
||||
def get_next_id(directory):
|
||||
files = [f for f in os.listdir(directory) if f.endswith('.yml')]
|
||||
if not files:
|
||||
return 1
|
||||
return max(int(f.split('_')[0]) for f in files) + 1
|
||||
|
||||
def generate_filename(directory, id, name):
|
||||
sanitized_name = name.replace(' ', '_').lower()
|
||||
return f"{directory}/{id}_{sanitized_name}.yml"
|
||||
|
||||
def get_current_timestamp():
|
||||
return datetime.datetime.now().isoformat()
|
||||
|
||||
def save_to_file(directory, data):
|
||||
if 'id' in data and data['id'] != 0:
|
||||
# Handle updating existing records
|
||||
existing_files = [f for f in os.listdir(directory) if f.startswith(f"{data['id']}_") and f.endswith('.yml')]
|
||||
if existing_files:
|
||||
existing_filename = os.path.join(directory, existing_files[0])
|
||||
existing_data = load_from_file(directory, data['id'])
|
||||
if existing_data:
|
||||
data['date_created'] = existing_data.get('date_created', get_current_timestamp())
|
||||
else:
|
||||
data['date_created'] = get_current_timestamp()
|
||||
data['date_modified'] = get_current_timestamp()
|
||||
new_filename = generate_filename(directory, data['id'], data['name'])
|
||||
|
||||
# Remove the old file if the name has changed
|
||||
if existing_filename != new_filename:
|
||||
os.remove(existing_filename)
|
||||
|
||||
with open(new_filename, 'w') as file:
|
||||
yaml.dump(data, file)
|
||||
return data
|
||||
else:
|
||||
# If existing file not found, treat it as new
|
||||
data['id'] = get_next_id(directory)
|
||||
data['date_created'] = get_current_timestamp()
|
||||
data['date_modified'] = get_current_timestamp()
|
||||
else:
|
||||
# Handle new records
|
||||
data['id'] = get_next_id(directory)
|
||||
data['date_created'] = get_current_timestamp()
|
||||
data['date_modified'] = get_current_timestamp()
|
||||
|
||||
new_filename = generate_filename(directory, data['id'], data['name'])
|
||||
with open(new_filename, 'w') as file:
|
||||
yaml.dump(data, file)
|
||||
|
||||
return data
|
||||
|
||||
def load_from_file(directory, id):
|
||||
files = [f for f in os.listdir(directory) if f.startswith(f"{id}_") and f.endswith('.yml')]
|
||||
if files:
|
||||
filename = os.path.join(directory, files[0])
|
||||
with open(filename, 'r') as file:
|
||||
data = yaml.safe_load(file)
|
||||
if 'conditions' not in data:
|
||||
data['conditions'] = [] # Ensure conditions is always a list
|
||||
if 'regex101Link' not in data:
|
||||
data['regex101Link'] = '' # Ensure regex101Link is always present
|
||||
return data
|
||||
return None
|
||||
|
||||
def load_all_from_directory(directory):
|
||||
items = []
|
||||
for filename in os.listdir(directory):
|
||||
if filename.endswith('.yml'):
|
||||
with open(os.path.join(directory, filename), 'r') as file:
|
||||
data = yaml.safe_load(file)
|
||||
if 'regex101Link' not in data:
|
||||
data['regex101Link'] = '' # Ensure regex101Link is always present
|
||||
items.append(data)
|
||||
return items
|
||||
|
||||
def delete_file(directory, id):
|
||||
files = [f for f in os.listdir(directory) if f.startswith(f"{id}_") and f.endswith('.yml')]
|
||||
if files:
|
||||
os.remove(os.path.join(directory, files[0]))
|
||||
return True
|
||||
return False
|
||||
28
backend/custom_formats/1_t1.yml
Normal file
28
backend/custom_formats/1_t1.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
conditions:
|
||||
- name: EbP
|
||||
negate: false
|
||||
regex_name: EbP
|
||||
required: false
|
||||
- name: DON
|
||||
negate: false
|
||||
regex_name: DON
|
||||
required: false
|
||||
- name: D-Z0N3
|
||||
negate: false
|
||||
regex_name: D-Z0N3
|
||||
required: false
|
||||
- flag: golden-popcorn
|
||||
name: Golden Popcorn
|
||||
negate: false
|
||||
required: true
|
||||
- flag: internal
|
||||
name: Internal
|
||||
negate: false
|
||||
required: true
|
||||
date_created: '2024-08-15T11:17:33.784024'
|
||||
date_modified: '2024-08-15T12:02:29.062961'
|
||||
description: Tier 1 Release Groups
|
||||
id: 1
|
||||
name: T1
|
||||
tags:
|
||||
- Release Group Tier
|
||||
9
backend/regex_patterns/1_ebp.yml
Normal file
9
backend/regex_patterns/1_ebp.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
date_created: '2024-08-15T10:46:19.929335'
|
||||
date_modified: '2024-08-15T12:02:00.531530'
|
||||
description: 'Release group: EbP'
|
||||
id: 1
|
||||
name: EbP
|
||||
pattern: (?<=^|[\s.-])EbP\b
|
||||
tags:
|
||||
- Release Group
|
||||
- HDB Internal
|
||||
9
backend/regex_patterns/2_don.yml
Normal file
9
backend/regex_patterns/2_don.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
date_created: '2024-08-15T10:46:36.718655'
|
||||
date_modified: '2024-08-15T13:15:49.306167'
|
||||
description: 'Release Group: DON'
|
||||
id: 2
|
||||
name: DON
|
||||
pattern: (?<=^|[\s.-])DON\b
|
||||
tags:
|
||||
- Release Group
|
||||
- HDB Internal
|
||||
9
backend/regex_patterns/3_d-z0n3.yml
Normal file
9
backend/regex_patterns/3_d-z0n3.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
date_created: '2024-08-15T10:46:41.183752'
|
||||
date_modified: '2024-08-15T12:54:02.651463'
|
||||
description: 'Release Group: D-Z0N3'
|
||||
id: 3
|
||||
name: D-Z0N3
|
||||
pattern: (?<=^|[\s.-])D-Z0N3\b
|
||||
tags:
|
||||
- Release Group
|
||||
- AHD Internal
|
||||
4
backend/requirements.txt
Normal file
4
backend/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
Flask==2.0.1
|
||||
Flask-CORS==3.0.10
|
||||
PyYAML==5.4.1
|
||||
requests==2.26.0
|
||||
19
docker-compose.yml
Normal file
19
docker-compose.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
frontend:
|
||||
build: ./frontend
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- ./frontend:/app
|
||||
- /app/node_modules
|
||||
environment:
|
||||
- VITE_API_URL=http://localhost:5000
|
||||
|
||||
backend:
|
||||
build: ./backend
|
||||
ports:
|
||||
- "5000:5000"
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
24
frontend/.gitignore
vendored
Normal file
24
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
10
frontend/Dockerfile
Normal file
10
frontend/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
FROM node:16
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
|
||||
8
frontend/README.md
Normal file
8
frontend/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# React + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
38
frontend/eslint.config.js
Normal file
38
frontend/eslint.config.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import react from 'eslint-plugin-react'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
|
||||
export default [
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
ignores: ['dist'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
settings: { react: { version: '18.3' } },
|
||||
plugins: {
|
||||
react,
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
...react.configs.recommended.rules,
|
||||
...react.configs['jsx-runtime'].rules,
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react/jsx-no-target-blank': 'off',
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
5309
frontend/package-lock.json
generated
Normal file
5309
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
frontend/package.json
Normal file
25
frontend/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.21.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"postcss": "^8.4.21",
|
||||
"tailwindcss": "^3.3.1",
|
||||
"vite": "^4.2.0"
|
||||
}
|
||||
}
|
||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
1
frontend/public/vite.svg
Normal file
1
frontend/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
33
frontend/src/App.jsx
Normal file
33
frontend/src/App.jsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import RegexManager from './components/regex/RegexManager';
|
||||
import CustomFormatManager from './components/format/FormatManager';
|
||||
import Navbar from './components/ui/Navbar';
|
||||
|
||||
function App() {
|
||||
const [activeTab, setActiveTab] = useState('regex');
|
||||
const [darkMode, setDarkMode] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (darkMode) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
}, [darkMode]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-gray-100">
|
||||
<Navbar
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
darkMode={darkMode}
|
||||
setDarkMode={setDarkMode}
|
||||
/>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-6">
|
||||
{activeTab === 'regex' ? <RegexManager /> : <CustomFormatManager />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
93
frontend/src/api/api.js
Normal file
93
frontend/src/api/api.js
Normal file
@@ -0,0 +1,93 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const API_BASE_URL = 'http://localhost:5000';
|
||||
|
||||
export const getRegexes = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/regex`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching regexes:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const saveRegex = async (regex) => {
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE_URL}/regex`, regex);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error saving regex:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateRegex = async (id, regex) => {
|
||||
try {
|
||||
const response = await axios.put(`${API_BASE_URL}/regex/${id}`, regex);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error updating regex:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteRegex = async (id) => {
|
||||
try {
|
||||
const response = await axios.delete(`${API_BASE_URL}/regex/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error deleting regex:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getFormats = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/format`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching formats:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const saveFormat = async (format) => {
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE_URL}/format`, format);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error saving format:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateFormat = async (id, format) => {
|
||||
try {
|
||||
const response = await axios.put(`${API_BASE_URL}/format/${id}`, format);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error updating format:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteFormat = async (id) => {
|
||||
try {
|
||||
const response = await axios.delete(`${API_BASE_URL}/format/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error deleting format:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const createRegex101Link = async (regexData) => {
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE_URL}/regex/regex101`, regexData);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error creating regex101 link:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
60
frontend/src/components/condition/ConditionCard.jsx
Normal file
60
frontend/src/components/condition/ConditionCard.jsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function ConditionCard({ condition, onEdit }) {
|
||||
const conditionType = condition.regex_name
|
||||
? 'Regex'
|
||||
: condition.min !== undefined && condition.max !== undefined
|
||||
? 'Size'
|
||||
: 'Flag';
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => onEdit(condition)}
|
||||
className="bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg p-4 shadow-sm cursor-pointer hover:shadow-lg hover:border-blue-400 dark:hover:border-blue-500 transition-shadow"
|
||||
>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h4 className="font-bold text-md dark:text-gray-200">{condition.name}</h4>
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">
|
||||
{conditionType}
|
||||
</span>
|
||||
</div>
|
||||
{conditionType === 'Regex' && (
|
||||
<div className="bg-gray-100 dark:bg-gray-600 rounded p-2 mb-2">
|
||||
<pre className="text-sm font-mono text-gray-800 dark:text-gray-200 whitespace-pre-wrap">
|
||||
{condition.regex_name}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{conditionType === 'Size' && (
|
||||
<p className="text-gray-600 dark:text-gray-400 text-sm mb-2">
|
||||
Size: {condition.min || 'Any'} - {condition.max || 'Any'} bytes
|
||||
</p>
|
||||
)}
|
||||
<div className="flex space-x-2 mt-2">
|
||||
<span className={`text-xs font-semibold inline-block py-1 px-2 rounded ${condition.required ? 'bg-green-500 text-white' : 'bg-blue-500 text-white'}`}>
|
||||
{condition.required ? 'Required' : 'Optional'}
|
||||
</span>
|
||||
{condition.negate && (
|
||||
<span className="text-xs font-semibold inline-block py-1 px-2 rounded bg-red-500 text-white">
|
||||
Negated
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ConditionCard.propTypes = {
|
||||
condition: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
regex_name: PropTypes.string,
|
||||
min: PropTypes.number,
|
||||
max: PropTypes.number,
|
||||
negate: PropTypes.bool,
|
||||
required: PropTypes.bool,
|
||||
flag: PropTypes.string, // Include flag in prop types
|
||||
}),
|
||||
onEdit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ConditionCard;
|
||||
262
frontend/src/components/condition/ConditionModal.jsx
Normal file
262
frontend/src/components/condition/ConditionModal.jsx
Normal file
@@ -0,0 +1,262 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from '../ui/Modal';
|
||||
|
||||
function ConditionModal({
|
||||
condition = null,
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
onDelete = null,
|
||||
regexes,
|
||||
level = 0
|
||||
}) {
|
||||
const [name, setName] = useState('');
|
||||
const [type, setType] = useState('regex');
|
||||
const [regexName, setRegexName] = useState('');
|
||||
const [minSize, setMinSize] = useState('');
|
||||
const [maxSize, setMaxSize] = useState('');
|
||||
const [flag, setFlag] = useState('');
|
||||
const [negate, setNegate] = useState(false);
|
||||
const [required, setRequired] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const initialConditionRef = useRef(condition);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
initialConditionRef.current = condition;
|
||||
if (condition) {
|
||||
setName(condition.name);
|
||||
if (condition.regex_name) {
|
||||
setType('regex');
|
||||
setRegexName(condition.regex_name);
|
||||
} else if (condition.min !== undefined && condition.max !== undefined) {
|
||||
setType('size');
|
||||
setMinSize(condition.min);
|
||||
setMaxSize(condition.max);
|
||||
} else if (condition.flag) {
|
||||
setType('flag');
|
||||
setFlag(condition.flag);
|
||||
}
|
||||
setNegate(condition.negate || false);
|
||||
setRequired(condition.required || false);
|
||||
} else {
|
||||
setName('');
|
||||
setType('regex');
|
||||
setRegexName('');
|
||||
setMinSize('');
|
||||
setMaxSize('');
|
||||
setFlag('');
|
||||
setNegate(false);
|
||||
setRequired(false);
|
||||
}
|
||||
setError('');
|
||||
}
|
||||
}, [condition, isOpen]);
|
||||
|
||||
const handleSave = () => {
|
||||
if (!name.trim()) {
|
||||
setError('Condition name is required.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'regex' && !regexName) {
|
||||
setError('Please select a regex pattern.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'size' && (!minSize || !maxSize)) {
|
||||
setError('Both minimum and maximum sizes are required.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'flag' && !flag) {
|
||||
setError('Please select a flag.');
|
||||
return;
|
||||
}
|
||||
|
||||
const newCondition = {
|
||||
name,
|
||||
negate,
|
||||
required,
|
||||
...(type === 'regex' ? { regex_name: regexName } : {}),
|
||||
...(type === 'size' ? { min: parseInt(minSize), max: parseInt(maxSize) } : {}),
|
||||
...(type === 'flag' ? { flag } : {}),
|
||||
};
|
||||
onSave(newCondition);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (initialConditionRef.current) {
|
||||
onDelete(initialConditionRef.current);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
level={level}
|
||||
title={initialConditionRef.current ? 'Edit Condition' : 'Add Condition'}
|
||||
disableCloseOnOutsideClick={true}
|
||||
disableCloseOnEscape={true}
|
||||
className="max-w-2xl min-h-72"
|
||||
>
|
||||
{error && <div className="text-red-500 mb-4">{error}</div>}
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Condition Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Enter condition name"
|
||||
className="w-full p-3 border rounded dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Condition Type
|
||||
</label>
|
||||
<select
|
||||
value={type}
|
||||
onChange={(e) => setType(e.target.value)}
|
||||
className="w-full p-3 border rounded dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
|
||||
>
|
||||
<option value="regex">Regex</option>
|
||||
<option value="size">Size</option>
|
||||
<option value="flag">Flag</option>
|
||||
</select>
|
||||
</div>
|
||||
{type === 'regex' && (
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Regex Pattern
|
||||
</label>
|
||||
<select
|
||||
value={regexName}
|
||||
onChange={(e) => setRegexName(e.target.value)}
|
||||
className="w-full p-3 border rounded dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
|
||||
>
|
||||
<option value="">Select a regex</option>
|
||||
{regexes.map((regex) => (
|
||||
<option key={regex.id} value={regex.name}>
|
||||
{regex.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
{type === 'size' && (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Minimum Size (bytes)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={minSize}
|
||||
onChange={(e) => setMinSize(e.target.value)}
|
||||
placeholder="Enter minimum size"
|
||||
className="w-full p-3 border rounded dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Maximum Size (bytes)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={maxSize}
|
||||
onChange={(e) => setMaxSize(e.target.value)}
|
||||
placeholder="Enter maximum size"
|
||||
className="w-full p-3 border rounded dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{type === 'flag' && (
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Flag
|
||||
</label>
|
||||
<select
|
||||
value={flag}
|
||||
onChange={(e) => setFlag(e.target.value)}
|
||||
className="w-full p-3 border rounded dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
|
||||
>
|
||||
<option value="">Select a flag</option>
|
||||
<option value="golden-popcorn">Golden Popcorn</option>
|
||||
<option value="internal">Internal</option>
|
||||
<option value="freeleech">Freeleech</option>
|
||||
<option value="halfleech">Halfleech</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-4">
|
||||
<label className="flex items-center dark:text-gray-300">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={negate}
|
||||
onChange={(e) => setNegate(e.target.checked)}
|
||||
className="mr-2"
|
||||
/>
|
||||
Negate (invert the condition)
|
||||
</label>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<label className="flex items-center dark:text-gray-300">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={required}
|
||||
onChange={(e) => setRequired(e.target.checked)}
|
||||
className="mr-2"
|
||||
/>
|
||||
Required (condition must be met)
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="bg-blue-500 text-white px-4 py-3 rounded hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
{initialConditionRef.current && onDelete && (
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
className="bg-red-500 text-white px-4 py-3 rounded hover:bg-red-600 transition-colors"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
ConditionModal.propTypes = {
|
||||
condition: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
regex_name: PropTypes.string,
|
||||
min: PropTypes.number,
|
||||
max: PropTypes.number,
|
||||
negate: PropTypes.bool,
|
||||
required: PropTypes.bool,
|
||||
flag: PropTypes.string,
|
||||
}),
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func,
|
||||
regexes: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
level: PropTypes.number,
|
||||
};
|
||||
|
||||
export default ConditionModal;
|
||||
59
frontend/src/components/format/FormatCard.jsx
Normal file
59
frontend/src/components/format/FormatCard.jsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function FormatCard({ format, onEdit, showDate, formatDate }) {
|
||||
return (
|
||||
<div
|
||||
className="bg-white dark:bg-gray-800 shadow-lg hover:shadow-xl rounded-lg p-4 cursor-pointer border border-gray-200 dark:border-gray-700 transition-all duration-300 ease-in-out transform hover:-translate-y-1"
|
||||
onClick={() => onEdit(format)}
|
||||
>
|
||||
<h3 className="font-bold text-lg mb-2 text-gray-800 dark:text-gray-200">{format.name}</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400 text-sm mb-2">{format.description}</p>
|
||||
{showDate && (
|
||||
<p className="text-gray-500 dark:text-gray-400 text-xs mb-2">
|
||||
Modified: {formatDate(format.date_modified)}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex flex-wrap -m-1 mt-4">
|
||||
{format.conditions.map((condition, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className={`text-xs font-medium inline-block py-1 px-2 rounded m-1 ${
|
||||
condition.negate ? 'bg-red-500 text-white' :
|
||||
condition.required ? 'bg-green-500 text-white' : 'bg-blue-500 text-white'
|
||||
}`}
|
||||
style={{ minHeight: '1.5rem', lineHeight: '1.5rem' }}
|
||||
>
|
||||
{condition.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-wrap mt-2">
|
||||
{format.tags && format.tags.map(tag => (
|
||||
<span key={tag} className="bg-blue-100 text-blue-800 text-xs font-medium mr-2 mb-1 px-2.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
FormatCard.propTypes = {
|
||||
format: PropTypes.shape({
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
description: PropTypes.string,
|
||||
conditions: PropTypes.arrayOf(PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
negate: PropTypes.bool,
|
||||
required: PropTypes.bool,
|
||||
})),
|
||||
date_modified: PropTypes.string.isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.string),
|
||||
}).isRequired,
|
||||
onEdit: PropTypes.func.isRequired,
|
||||
showDate: PropTypes.bool.isRequired,
|
||||
formatDate: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default FormatCard;
|
||||
107
frontend/src/components/format/FormatManager.jsx
Normal file
107
frontend/src/components/format/FormatManager.jsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import FormatCard from './FormatCard';
|
||||
import FormatModal from './FormatModal';
|
||||
import AddNewCard from '../ui/AddNewCard';
|
||||
import { getFormats } from '../../api/api';
|
||||
import FilterMenu from '../ui/FilterMenu';
|
||||
import SortMenu from '../ui/SortMenu';
|
||||
|
||||
function FormatManager() {
|
||||
const [formats, setFormats] = useState([]);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [selectedFormat, setSelectedFormat] = useState(null);
|
||||
const [sortBy, setSortBy] = useState('title');
|
||||
const [filterType, setFilterType] = useState('none');
|
||||
const [filterValue, setFilterValue] = useState('');
|
||||
const [allTags, setAllTags] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchFormats();
|
||||
}, []);
|
||||
|
||||
const fetchFormats = async () => {
|
||||
try {
|
||||
const fetchedFormats = await getFormats();
|
||||
setFormats(fetchedFormats);
|
||||
const tags = [...new Set(fetchedFormats.flatMap(format => format.tags || []))];
|
||||
setAllTags(tags);
|
||||
} catch (error) {
|
||||
console.error('Error fetching formats:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenModal = (format = null) => {
|
||||
setSelectedFormat(format);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setSelectedFormat(null);
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
const handleSaveFormat = () => {
|
||||
fetchFormats();
|
||||
handleCloseModal();
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleString();
|
||||
};
|
||||
|
||||
const sortedAndFilteredFormats = formats
|
||||
.filter(format => {
|
||||
if (filterType === 'tag') {
|
||||
return format.tags && format.tags.includes(filterValue);
|
||||
}
|
||||
if (filterType === 'date') {
|
||||
const formatDate = new Date(format.date_modified);
|
||||
const filterDate = new Date(filterValue);
|
||||
return formatDate.toDateString() === filterDate.toDateString();
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (sortBy === 'title') return a.name.localeCompare(b.name);
|
||||
if (sortBy === 'dateCreated') return new Date(b.date_created) - new Date(a.date_created);
|
||||
if (sortBy === 'dateModified') return new Date(b.date_modified) - new Date(a.date_modified);
|
||||
return 0;
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-4">Manage Custom Formats</h2>
|
||||
<div className="mb-4 flex items-center space-x-4">
|
||||
<SortMenu sortBy={sortBy} setSortBy={setSortBy} />
|
||||
<FilterMenu
|
||||
filterType={filterType}
|
||||
setFilterType={setFilterType}
|
||||
filterValue={filterValue}
|
||||
setFilterValue={setFilterValue}
|
||||
allTags={allTags}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 mb-4">
|
||||
{sortedAndFilteredFormats.map((format) => (
|
||||
<FormatCard
|
||||
key={format.id}
|
||||
format={format}
|
||||
onEdit={() => handleOpenModal(format)}
|
||||
showDate={sortBy !== 'title'}
|
||||
formatDate={formatDate}
|
||||
/>
|
||||
))}
|
||||
<AddNewCard onAdd={() => handleOpenModal()} />
|
||||
</div>
|
||||
<FormatModal
|
||||
format={selectedFormat}
|
||||
isOpen={isModalOpen}
|
||||
onClose={handleCloseModal}
|
||||
onSave={handleSaveFormat}
|
||||
allTags={allTags}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FormatManager;
|
||||
235
frontend/src/components/format/FormatModal.jsx
Normal file
235
frontend/src/components/format/FormatModal.jsx
Normal file
@@ -0,0 +1,235 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { saveFormat, deleteFormat, getRegexes } from '../../api/api';
|
||||
import ConditionModal from '../condition/ConditionModal';
|
||||
import ConditionCard from '../condition/ConditionCard';
|
||||
import Modal from '../ui/Modal';
|
||||
|
||||
function FormatModal({ format = null, isOpen, onClose, onSave }) {
|
||||
const [name, setName] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [conditions, setConditions] = useState([]);
|
||||
const [isConditionModalOpen, setIsConditionModalOpen] = useState(false);
|
||||
const [selectedCondition, setSelectedCondition] = useState(null);
|
||||
const [regexes, setRegexes] = useState([]);
|
||||
const [error, setError] = useState('');
|
||||
const [tags, setTags] = useState([]);
|
||||
const [newTag, setNewTag] = useState('');
|
||||
const initialFormatRef = useRef(format);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
initialFormatRef.current = format;
|
||||
if (format) {
|
||||
setName(format.name);
|
||||
setDescription(format.description);
|
||||
setConditions(format.conditions || []);
|
||||
setTags(format.tags || []);
|
||||
} else {
|
||||
setName('');
|
||||
setDescription('');
|
||||
setConditions([]);
|
||||
setTags([]);
|
||||
}
|
||||
setError('');
|
||||
setNewTag('');
|
||||
fetchRegexes();
|
||||
}
|
||||
}, [format, isOpen]);
|
||||
|
||||
const fetchRegexes = async () => {
|
||||
try {
|
||||
const fetchedRegexes = await getRegexes();
|
||||
setRegexes(fetchedRegexes);
|
||||
} catch (error) {
|
||||
console.error('Error fetching regexes:', error);
|
||||
setError('Failed to fetch regexes. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!name.trim() || !description.trim() || conditions.length === 0) {
|
||||
setError('Name, description, and at least one condition are required.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await saveFormat({ id: initialFormatRef.current ? initialFormatRef.current.id : 0, name, description, conditions, tags });
|
||||
onSave();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Error saving format:', error);
|
||||
setError('Failed to save format. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (initialFormatRef.current && initialFormatRef.current.id) {
|
||||
try {
|
||||
await deleteFormat(initialFormatRef.current.id);
|
||||
onSave();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Error deleting format:', error);
|
||||
setError('Failed to delete format. Please try again.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenConditionModal = (condition = null) => {
|
||||
setSelectedCondition(condition);
|
||||
setIsConditionModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseConditionModal = () => {
|
||||
setIsConditionModalOpen(false);
|
||||
};
|
||||
|
||||
const handleSaveCondition = (newCondition) => {
|
||||
if (selectedCondition) {
|
||||
setConditions(conditions.map(c => c === selectedCondition ? newCondition : c));
|
||||
} else {
|
||||
setConditions([...conditions, newCondition]);
|
||||
}
|
||||
setIsConditionModalOpen(false);
|
||||
};
|
||||
|
||||
const handleDeleteCondition = (conditionToDelete) => {
|
||||
setConditions(conditions.filter(c => c !== conditionToDelete));
|
||||
};
|
||||
|
||||
const handleAddTag = () => {
|
||||
if (newTag.trim() && !tags.includes(newTag.trim())) {
|
||||
setTags([...tags, newTag.trim()]);
|
||||
setNewTag('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveTag = (tagToRemove) => {
|
||||
setTags(tags.filter(tag => tag !== tagToRemove));
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={initialFormatRef.current ? 'Edit Custom Format' : 'Add Custom Format'}
|
||||
className="max-w-3xl min-h-96"
|
||||
>
|
||||
{error && <div className="text-red-500 mb-4">{error}</div>}
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Format Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Enter format name"
|
||||
className="w-full p-3 border rounded dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Description
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Enter description"
|
||||
className="w-full p-3 border rounded dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Tags
|
||||
</label>
|
||||
<div className="flex flex-wrap mb-2">
|
||||
{tags.map(tag => (
|
||||
<span key={tag} className="bg-blue-100 text-blue-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300">
|
||||
{tag}
|
||||
<button onClick={() => handleRemoveTag(tag)} className="ml-1 text-xs">×</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex">
|
||||
<input
|
||||
type="text"
|
||||
value={newTag}
|
||||
onChange={(e) => setNewTag(e.target.value)}
|
||||
placeholder="Add a tag"
|
||||
className="flex-grow p-2 border rounded-l dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
|
||||
/>
|
||||
<button
|
||||
onClick={handleAddTag}
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded-r hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="font-bold mb-4 dark:text-gray-300">Conditions:</h3>
|
||||
<div className="mb-6 max-h-96 overflow-y-auto grid grid-cols-1 gap-4">
|
||||
{conditions.map((condition, index) => (
|
||||
<ConditionCard
|
||||
key={index}
|
||||
condition={condition}
|
||||
onEdit={() => handleOpenConditionModal(condition)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleOpenConditionModal()}
|
||||
className="bg-green-500 text-white px-4 py-3 rounded hover:bg-green-600 mb-6 transition-colors"
|
||||
>
|
||||
Add Condition
|
||||
</button>
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="bg-blue-500 text-white px-4 py-3 rounded hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
{format && (
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
className="bg-red-500 text-white px-4 py-3 rounded hover:bg-red-600 transition-colors"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<ConditionModal
|
||||
condition={selectedCondition}
|
||||
isOpen={isConditionModalOpen}
|
||||
onClose={handleCloseConditionModal}
|
||||
onSave={handleSaveCondition}
|
||||
onDelete={handleDeleteCondition}
|
||||
regexes={regexes}
|
||||
level={1}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
FormatModal.propTypes = {
|
||||
format: PropTypes.shape({
|
||||
id: PropTypes.number,
|
||||
name: PropTypes.string.isRequired,
|
||||
description: PropTypes.string.isRequired,
|
||||
conditions: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
negate: PropTypes.bool,
|
||||
required: PropTypes.bool,
|
||||
})
|
||||
).isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.string),
|
||||
}),
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default FormatModal;
|
||||
50
frontend/src/components/regex/RegexCard.jsx
Normal file
50
frontend/src/components/regex/RegexCard.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function RegexCard({ regex, onEdit, showDate, formatDate }) {
|
||||
return (
|
||||
<div
|
||||
className="bg-white dark:bg-gray-800 shadow-lg hover:shadow-xl rounded-lg p-4 cursor-pointer border border-gray-200 dark:border-gray-700 transition-all duration-300 ease-in-out transform hover:-translate-y-1"
|
||||
onClick={() => onEdit(regex)}
|
||||
>
|
||||
<h3 className="font-bold text-lg mb-2 text-gray-800 dark:text-gray-200 truncate">
|
||||
{regex.name}
|
||||
</h3>
|
||||
<div className="bg-gray-100 dark:bg-gray-700 rounded p-2 mb-2">
|
||||
<pre className="text-sm font-mono text-gray-800 dark:text-gray-200 whitespace-pre-wrap">
|
||||
{regex.pattern}
|
||||
</pre>
|
||||
</div>
|
||||
<p className="text-gray-500 dark:text-gray-400 text-xs mb-2">
|
||||
{regex.description}
|
||||
</p>
|
||||
{showDate && (
|
||||
<p className="text-gray-500 dark:text-gray-400 text-xs mb-2">
|
||||
Modified: {formatDate(regex.date_modified)}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex flex-wrap">
|
||||
{regex.tags && regex.tags.map(tag => (
|
||||
<span key={tag} className="bg-blue-100 text-blue-800 text-xs font-medium mr-2 mb-1 px-2.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
RegexCard.propTypes = {
|
||||
regex: PropTypes.shape({
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
pattern: PropTypes.string.isRequired,
|
||||
description: PropTypes.string,
|
||||
date_modified: PropTypes.string.isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.string),
|
||||
}).isRequired,
|
||||
onEdit: PropTypes.func.isRequired,
|
||||
showDate: PropTypes.bool.isRequired,
|
||||
formatDate: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default RegexCard;
|
||||
108
frontend/src/components/regex/RegexManager.jsx
Normal file
108
frontend/src/components/regex/RegexManager.jsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import RegexCard from './RegexCard';
|
||||
import RegexModal from './RegexModal';
|
||||
import AddNewCard from '../ui/AddNewCard';
|
||||
import { getRegexes } from '../../api/api';
|
||||
import FilterMenu from '../ui/FilterMenu';
|
||||
import SortMenu from '../ui/SortMenu';
|
||||
|
||||
function RegexManager() {
|
||||
const [regexes, setRegexes] = useState([]);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [selectedRegex, setSelectedRegex] = useState(null);
|
||||
const [sortBy, setSortBy] = useState('title');
|
||||
const [filterType, setFilterType] = useState('none');
|
||||
const [filterValue, setFilterValue] = useState('');
|
||||
const [allTags, setAllTags] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchRegexes();
|
||||
}, []);
|
||||
|
||||
const fetchRegexes = async () => {
|
||||
try {
|
||||
const fetchedRegexes = await getRegexes();
|
||||
setRegexes(fetchedRegexes);
|
||||
const tags = [...new Set(fetchedRegexes.flatMap(regex => regex.tags || []))];
|
||||
setAllTags(tags);
|
||||
} catch (error) {
|
||||
console.error('Error fetching regexes:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenModal = (regex = null) => {
|
||||
setSelectedRegex(regex);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setSelectedRegex(null);
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
const handleSaveRegex = () => {
|
||||
fetchRegexes();
|
||||
handleCloseModal();
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleString();
|
||||
};
|
||||
|
||||
const sortedAndFilteredRegexes = regexes
|
||||
.filter(regex => {
|
||||
if (filterType === 'tag') {
|
||||
return regex.tags && regex.tags.includes(filterValue);
|
||||
}
|
||||
if (filterType === 'date') {
|
||||
const regexDate = new Date(regex.date_modified);
|
||||
const filterDate = new Date(filterValue);
|
||||
return regexDate.toDateString() === filterDate.toDateString();
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (sortBy === 'title') return a.name.localeCompare(b.name);
|
||||
if (sortBy === 'dateCreated') return new Date(b.date_created) - new Date(a.date_created);
|
||||
if (sortBy === 'dateModified') return new Date(b.date_modified) - new Date(a.date_modified);
|
||||
return 0;
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-4">Manage Regex Patterns</h2>
|
||||
<div className="mb-4 flex items-center space-x-4">
|
||||
<SortMenu sortBy={sortBy} setSortBy={setSortBy} />
|
||||
<FilterMenu
|
||||
filterType={filterType}
|
||||
setFilterType={setFilterType}
|
||||
filterValue={filterValue}
|
||||
setFilterValue={setFilterValue}
|
||||
allTags={allTags}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 mb-4">
|
||||
{sortedAndFilteredRegexes.map((regex) => (
|
||||
<RegexCard
|
||||
key={regex.id}
|
||||
regex={regex}
|
||||
onEdit={() => handleOpenModal(regex)}
|
||||
showDate={sortBy !== 'title'}
|
||||
formatDate={formatDate}
|
||||
/>
|
||||
))}
|
||||
<AddNewCard onAdd={() => handleOpenModal()} />
|
||||
</div>
|
||||
<RegexModal
|
||||
regex={selectedRegex}
|
||||
isOpen={isModalOpen}
|
||||
onClose={handleCloseModal}
|
||||
onSave={handleSaveRegex}
|
||||
allTags={allTags}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default RegexManager;
|
||||
225
frontend/src/components/regex/RegexModal.jsx
Normal file
225
frontend/src/components/regex/RegexModal.jsx
Normal file
@@ -0,0 +1,225 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { saveRegex, deleteRegex, createRegex101Link } from '../../api/api';
|
||||
import Modal from '../ui/Modal';
|
||||
|
||||
function RegexModal({ regex = null, isOpen, onClose, onSave }) {
|
||||
const [name, setName] = useState('');
|
||||
const [pattern, setPattern] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [tags, setTags] = useState([]);
|
||||
const [newTag, setNewTag] = useState('');
|
||||
const [regex101Link, setRegex101Link] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const initialRegexRef = useRef(regex);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
initialRegexRef.current = regex;
|
||||
if (regex) {
|
||||
setName(regex.name);
|
||||
setPattern(regex.pattern);
|
||||
setDescription(regex.description);
|
||||
setTags(regex.tags || []);
|
||||
setRegex101Link(regex.regex101Link || '');
|
||||
} else {
|
||||
setName('');
|
||||
setPattern('');
|
||||
setDescription('');
|
||||
setTags([]);
|
||||
setRegex101Link('');
|
||||
}
|
||||
setError('');
|
||||
setNewTag('');
|
||||
}
|
||||
}, [regex, isOpen]);
|
||||
|
||||
const handleCreateRegex101Link = async () => {
|
||||
try {
|
||||
const response = await createRegex101Link({
|
||||
regex: pattern,
|
||||
flavor: 'pcre',
|
||||
flags: 'gm',
|
||||
delimiter: '/',
|
||||
});
|
||||
const permalink = `https://regex101.com/r/${response.permalinkFragment}`;
|
||||
setRegex101Link(permalink);
|
||||
window.open(permalink, '_blank');
|
||||
} catch (error) {
|
||||
console.error('Error creating regex101 link:', error);
|
||||
setError('Failed to create regex101 link. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!name.trim() || !pattern.trim() || !description.trim()) {
|
||||
setError('Name, pattern, and description are all required.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await saveRegex({
|
||||
id: regex ? regex.id : 0,
|
||||
name,
|
||||
pattern,
|
||||
description,
|
||||
tags,
|
||||
regex101Link
|
||||
});
|
||||
onSave();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Error saving regex:', error);
|
||||
setError('Failed to save regex. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (regex && regex.id) {
|
||||
try {
|
||||
await deleteRegex(regex.id);
|
||||
onSave();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Error deleting regex:', error);
|
||||
setError('Failed to delete regex. Please try again.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddTag = () => {
|
||||
if (newTag.trim() && !tags.includes(newTag.trim())) {
|
||||
setTags([...tags, newTag.trim()]);
|
||||
setNewTag('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveTag = (tagToRemove) => {
|
||||
setTags(tags.filter(tag => tag !== tagToRemove));
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={initialRegexRef.current ? 'Edit Regex Pattern' : 'Add Regex Pattern'}
|
||||
>
|
||||
{error && <div className="text-red-500 mb-4">{error}</div>}
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Regex Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Enter regex name"
|
||||
className="w-full p-2 border rounded dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Regex Pattern
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={pattern}
|
||||
onChange={(e) => setPattern(e.target.value)}
|
||||
placeholder="Enter regex pattern"
|
||||
className="w-full p-2 border rounded font-mono dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Description
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Enter description"
|
||||
className="w-full p-2 border rounded dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Tags
|
||||
</label>
|
||||
<div className="flex flex-wrap mb-2">
|
||||
{tags.map(tag => (
|
||||
<span key={tag} className="bg-blue-100 text-blue-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300">
|
||||
{tag}
|
||||
<button onClick={() => handleRemoveTag(tag)} className="ml-1 text-xs">×</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex">
|
||||
<input
|
||||
type="text"
|
||||
value={newTag}
|
||||
onChange={(e) => setNewTag(e.target.value)}
|
||||
placeholder="Add a tag"
|
||||
className="flex-grow p-2 border rounded-l dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
|
||||
/>
|
||||
<button
|
||||
onClick={handleAddTag}
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded-r hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<button
|
||||
onClick={handleCreateRegex101Link}
|
||||
className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600 transition-colors"
|
||||
>
|
||||
Test this regex
|
||||
</button>
|
||||
{regex101Link && (
|
||||
<p className="mt-2">
|
||||
<a
|
||||
href={regex101Link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 hover:underline"
|
||||
>
|
||||
Open in Regex101
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
{regex && (
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600 transition-colors"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
RegexModal.propTypes = {
|
||||
regex: PropTypes.shape({
|
||||
id: PropTypes.number,
|
||||
name: PropTypes.string.isRequired,
|
||||
pattern: PropTypes.string.isRequired,
|
||||
description: PropTypes.string,
|
||||
tags: PropTypes.arrayOf(PropTypes.string),
|
||||
}),
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default RegexModal;
|
||||
19
frontend/src/components/ui/AddNewCard.jsx
Normal file
19
frontend/src/components/ui/AddNewCard.jsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function AddNewCard({ onAdd }) {
|
||||
return (
|
||||
<div
|
||||
className="bg-white dark:bg-gray-800 shadow-lg hover:shadow-xl rounded-lg p-4 cursor-pointer border-2 border-dashed border-gray-400 dark:border-gray-600 flex items-center justify-center transition-all duration-300 ease-in-out transform hover:-translate-y-1"
|
||||
onClick={onAdd}
|
||||
style={{ minHeight: '150px' }}
|
||||
>
|
||||
<span className="text-4xl text-gray-400 dark:text-gray-500">+</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
AddNewCard.propTypes = {
|
||||
onAdd: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default AddNewCard;
|
||||
97
frontend/src/components/ui/FilterMenu.jsx
Normal file
97
frontend/src/components/ui/FilterMenu.jsx
Normal file
@@ -0,0 +1,97 @@
|
||||
// FilterMenu.jsx
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function FilterMenu({ filterType, setFilterType, filterValue, setFilterValue, allTags }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef(null);
|
||||
|
||||
const options = [
|
||||
{ value: 'none', label: 'No Filter' },
|
||||
{ value: 'tag', label: 'Filter by Tag' },
|
||||
{ value: 'date', label: 'Filter by Date' },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event) {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="relative inline-block text-left" ref={dropdownRef}>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-between items-center w-full rounded-md border border-gray-600 shadow-sm px-4 py-2 bg-gray-700 text-sm font-medium text-white hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
{options.find(option => option.value === filterType)?.label}
|
||||
<svg className="-mr-1 ml-2 h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isOpen && (
|
||||
<div className="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-gray-700 ring-1 ring-black ring-opacity-5 z-50">
|
||||
<div className="py-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
|
||||
{options.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
onClick={() => {
|
||||
setFilterType(option.value);
|
||||
setFilterValue('');
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className={`${
|
||||
filterType === option.value ? 'bg-gray-600 text-white' : 'text-gray-200'
|
||||
} block w-full text-left px-4 py-2 text-sm hover:bg-gray-600 hover:text-white`}
|
||||
role="menuitem"
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{filterType === 'tag' && (
|
||||
<select
|
||||
value={filterValue}
|
||||
onChange={(e) => setFilterValue(e.target.value)}
|
||||
className="appearance-none bg-gray-700 text-white py-2 px-4 pr-8 rounded-md border border-gray-600 leading-tight focus:outline-none focus:bg-gray-600 focus:border-white cursor-pointer"
|
||||
>
|
||||
<option value="">Select a tag</option>
|
||||
{allTags.map(tag => (
|
||||
<option key={tag} value={tag}>{tag}</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
{filterType === 'date' && (
|
||||
<input
|
||||
type="date"
|
||||
value={filterValue}
|
||||
onChange={(e) => setFilterValue(e.target.value)}
|
||||
className="bg-gray-700 text-white py-2 px-4 rounded-md border border-gray-600 leading-tight focus:outline-none focus:bg-gray-600 focus:border-white cursor-pointer"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
FilterMenu.propTypes = {
|
||||
filterType: PropTypes.string.isRequired,
|
||||
setFilterType: PropTypes.func.isRequired,
|
||||
filterValue: PropTypes.string.isRequired,
|
||||
setFilterValue: PropTypes.func.isRequired,
|
||||
allTags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
};
|
||||
|
||||
export default FilterMenu;
|
||||
99
frontend/src/components/ui/Modal.jsx
Normal file
99
frontend/src/components/ui/Modal.jsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function Modal({
|
||||
isOpen,
|
||||
onClose,
|
||||
title,
|
||||
children,
|
||||
level = 0,
|
||||
disableCloseOnOutsideClick = false,
|
||||
disableCloseOnEscape = false,
|
||||
}) {
|
||||
const modalRef = useRef();
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
const [shouldRender, setShouldRender] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setShouldRender(true);
|
||||
setTimeout(() => setIsAnimating(true), 10);
|
||||
} else {
|
||||
setIsAnimating(false);
|
||||
const timer = setTimeout(() => setShouldRender(false), 300);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && !disableCloseOnEscape) {
|
||||
const handleEscape = (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleEscape);
|
||||
};
|
||||
}
|
||||
}, [isOpen, onClose, disableCloseOnEscape]);
|
||||
|
||||
if (!shouldRender) return null;
|
||||
|
||||
const handleClickOutside = (e) => {
|
||||
if (modalRef.current && !modalRef.current.contains(e.target) && !disableCloseOnOutsideClick) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed inset-0 overflow-y-auto h-full w-full flex items-center justify-center transition-opacity duration-300 ease-in-out ${
|
||||
isAnimating ? 'opacity-100' : 'opacity-0'
|
||||
}`}
|
||||
style={{ zIndex: 1000 + level * 10 }}
|
||||
onClickCapture={handleClickOutside}
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50" style={{ zIndex: 1000 + level * 10 }}></div>
|
||||
<div
|
||||
ref={modalRef}
|
||||
className={`relative bg-white dark:bg-gray-800 p-8 rounded-lg shadow-xl w-full max-w-lg transform transition-all duration-300 ease-in-out ${
|
||||
isAnimating ? 'translate-y-0 opacity-100' : 'translate-y-10 opacity-0'
|
||||
}`}
|
||||
style={{
|
||||
zIndex: 1001 + level * 10,
|
||||
minHeight: '300px',
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-2xl font-bold dark:text-gray-200">{title}</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Modal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
level: PropTypes.number,
|
||||
disableCloseOnOutsideClick: PropTypes.bool,
|
||||
disableCloseOnEscape: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
65
frontend/src/components/ui/Navbar.jsx
Normal file
65
frontend/src/components/ui/Navbar.jsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function ToggleSwitch({ checked, onChange }) {
|
||||
return (
|
||||
<label className="flex items-center cursor-pointer">
|
||||
<div className="relative">
|
||||
<input type="checkbox" className="sr-only" checked={checked} onChange={onChange} />
|
||||
<div className={`block w-14 h-8 rounded-full ${checked ? 'bg-blue-600' : 'bg-gray-600'}`}></div>
|
||||
<div className={`dot absolute left-1 top-1 bg-white w-6 h-6 rounded-full transition ${checked ? 'transform translate-x-6' : ''}`}></div>
|
||||
</div>
|
||||
<div className="ml-3 text-gray-300 font-medium">
|
||||
{checked ? 'Dark' : 'Light'}
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
ToggleSwitch.propTypes = {
|
||||
checked: PropTypes.bool.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
function Navbar({ activeTab, setActiveTab, darkMode, setDarkMode }) {
|
||||
return (
|
||||
<nav className="bg-gray-800 shadow-md">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
<div className="flex items-center space-x-8">
|
||||
<h1 className="text-2xl font-bold text-white">
|
||||
Regex and Format Manager
|
||||
</h1>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
className={`px-3 py-2 rounded-md text-sm font-medium ${
|
||||
activeTab === 'regex' ? 'bg-gray-900 text-white' : 'text-gray-300 hover:bg-gray-700 hover:text-white'
|
||||
}`}
|
||||
onClick={() => setActiveTab('regex')}
|
||||
>
|
||||
Regex
|
||||
</button>
|
||||
<button
|
||||
className={`px-3 py-2 rounded-md text-sm font-medium ${
|
||||
activeTab === 'format' ? 'bg-gray-900 text-white' : 'text-gray-300 hover:bg-gray-700 hover:text-white'
|
||||
}`}
|
||||
onClick={() => setActiveTab('format')}
|
||||
>
|
||||
Custom Format
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ToggleSwitch checked={darkMode} onChange={() => setDarkMode(!darkMode)} />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
Navbar.propTypes = {
|
||||
activeTab: PropTypes.string.isRequired,
|
||||
setActiveTab: PropTypes.func.isRequired,
|
||||
darkMode: PropTypes.bool.isRequired,
|
||||
setDarkMode: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
70
frontend/src/components/ui/SortMenu.jsx
Normal file
70
frontend/src/components/ui/SortMenu.jsx
Normal file
@@ -0,0 +1,70 @@
|
||||
// SortMenu.jsx
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function SortMenu({ sortBy, setSortBy }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef(null);
|
||||
|
||||
const options = [
|
||||
{ value: 'title', label: 'Sort by Title' },
|
||||
{ value: 'dateCreated', label: 'Sort by Date Created' },
|
||||
{ value: 'dateModified', label: 'Sort by Date Modified' },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event) {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative inline-block text-left" ref={dropdownRef}>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-between items-center w-full rounded-md border border-gray-600 shadow-sm px-4 py-2 bg-gray-700 text-sm font-medium text-white hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
{options.find(option => option.value === sortBy)?.label}
|
||||
<svg className="-mr-1 ml-2 h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isOpen && (
|
||||
<div className="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-gray-700 ring-1 ring-black ring-opacity-5 z-50">
|
||||
<div className="py-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
|
||||
{options.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
onClick={() => {
|
||||
setSortBy(option.value);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className={`${
|
||||
sortBy === option.value ? 'bg-gray-600 text-white' : 'text-gray-200'
|
||||
} block w-full text-left px-4 py-2 text-sm hover:bg-gray-600 hover:text-white`}
|
||||
role="menuitem"
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SortMenu.propTypes = {
|
||||
sortBy: PropTypes.string.isRequired,
|
||||
setSortBy: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default SortMenu;
|
||||
15
frontend/src/index.css
Normal file
15
frontend/src/index.css
Normal file
@@ -0,0 +1,15 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Fira+Code:wght@400;500&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:wght@400;700&display=swap');
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html {
|
||||
font-family: "Schibsted Grotesk", sans-serif;
|
||||
}
|
||||
|
||||
code, pre, .font-mono {
|
||||
font-family: 'Fira Code', monospace;
|
||||
}
|
||||
10
frontend/src/main.jsx
Normal file
10
frontend/src/main.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
31
frontend/tailwind.config.js
Normal file
31
frontend/tailwind.config.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
// tailwind.config.js
|
||||
module.exports = {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
keyframes: {
|
||||
'modal-open': {
|
||||
'0%': { opacity: 0, transform: 'scale(0.95)' },
|
||||
'100%': { opacity: 1, transform: 'scale(1)' },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
'modal-open': 'modal-open 0.3s ease-out forwards',
|
||||
},
|
||||
colors: {
|
||||
'dark-bg': '#1a1c23',
|
||||
'dark-card': '#2a2e37',
|
||||
'dark-text': '#e2e8f0',
|
||||
'dark-border': '#4a5568',
|
||||
'dark-button': '#3182ce',
|
||||
'dark-button-hover': '#2c5282',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
10
frontend/vite.config.js
Normal file
10
frontend/vite.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 3000,
|
||||
},
|
||||
})
|
||||
8
old/custom_formats/0.yml
Normal file
8
old/custom_formats/0.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
conditions:
|
||||
- name: A
|
||||
negate: false
|
||||
regex_name: A
|
||||
required: true
|
||||
description: A
|
||||
id: 0
|
||||
name: A
|
||||
85
old/server.py
Normal file
85
old/server.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from flask import Flask, request, jsonify, render_template
|
||||
from flask_cors import CORS
|
||||
import os
|
||||
import yaml
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
||||
REGEX_DIR = 'regex_patterns'
|
||||
FORMAT_DIR = 'custom_formats'
|
||||
|
||||
os.makedirs(REGEX_DIR, exist_ok=True)
|
||||
os.makedirs(FORMAT_DIR, exist_ok=True)
|
||||
|
||||
def get_next_regex_id():
|
||||
regex_files = [f for f in os.listdir(REGEX_DIR) if f.endswith('.yml')]
|
||||
if not regex_files:
|
||||
return 1
|
||||
max_id = max(int(f.split('.')[0]) for f in regex_files)
|
||||
return max_id + 1
|
||||
|
||||
@app.route('/')
|
||||
def home():
|
||||
return render_template('index.html')
|
||||
|
||||
@app.route('/save_regex', methods=['POST'])
|
||||
def save_regex():
|
||||
data = request.json
|
||||
if 'id' not in data or data['id'] == 0:
|
||||
data['id'] = get_next_regex_id()
|
||||
filename = f"{REGEX_DIR}/{data['id']}.yml"
|
||||
with open(filename, 'w') as file:
|
||||
yaml.dump(data, file)
|
||||
return jsonify({"message": f"Regex saved to {filename}"}), 200
|
||||
|
||||
@app.route('/save_format', methods=['POST'])
|
||||
def save_format():
|
||||
data = request.json
|
||||
if 'id' not in data:
|
||||
return jsonify({"error": "Missing 'id' in request data"}), 400
|
||||
filename = f"{FORMAT_DIR}/{data['id']}.yml"
|
||||
with open(filename, 'w') as file:
|
||||
yaml.dump(data, file)
|
||||
return jsonify({"message": f"Format saved to {filename}"}), 200
|
||||
|
||||
@app.route('/get_regexes', methods=['GET'])
|
||||
def get_regexes():
|
||||
regexes = []
|
||||
for filename in os.listdir(REGEX_DIR):
|
||||
if filename.endswith('.yml'):
|
||||
with open(os.path.join(REGEX_DIR, filename), 'r') as file:
|
||||
regex = yaml.safe_load(file)
|
||||
regexes.append(regex)
|
||||
return jsonify(regexes), 200
|
||||
|
||||
@app.route('/get_formats', methods=['GET'])
|
||||
def get_formats():
|
||||
formats = []
|
||||
for filename in os.listdir(FORMAT_DIR):
|
||||
if filename.endswith('.yml'):
|
||||
with open(os.path.join(FORMAT_DIR, filename), 'r') as file:
|
||||
format_data = yaml.safe_load(file)
|
||||
formats.append(format_data)
|
||||
return jsonify(formats), 200
|
||||
|
||||
@app.route('/delete_regex/<int:id>', methods=['DELETE'])
|
||||
def delete_regex(id):
|
||||
filename = f"{REGEX_DIR}/{id}.yml"
|
||||
if os.path.exists(filename):
|
||||
os.remove(filename)
|
||||
return jsonify({"message": f"Regex with ID {id} deleted."}), 200
|
||||
else:
|
||||
return jsonify({"error": f"Regex with ID {id} not found."}), 404
|
||||
|
||||
@app.route('/delete_format/<int:id>', methods=['DELETE'])
|
||||
def delete_format(id):
|
||||
filename = f"{FORMAT_DIR}/{id}.yml"
|
||||
if os.path.exists(filename):
|
||||
os.remove(filename)
|
||||
return jsonify({"message": f"Format with ID {id} deleted."}), 200
|
||||
else:
|
||||
return jsonify({"error": f"Format with ID {id} not found."}), 404
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
||||
787
old/templates/index.html
Normal file
787
old/templates/index.html
Normal file
@@ -0,0 +1,787 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Regex and Custom Format Manager</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #4CAF50;
|
||||
--primary-hover-color: #45a049;
|
||||
--secondary-color: #333;
|
||||
--background-color: #f2f2f2;
|
||||
--accent-color: #3498db;
|
||||
--accent-hover-color: #2980b9;
|
||||
--tab-active-bg-color: #888;
|
||||
--tab-bg-color: #ccc;
|
||||
--border-color: #ccc;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--primary-color: #2ecc71;
|
||||
--primary-hover-color: #27ae60;
|
||||
--secondary-color: #f2f2f2;
|
||||
--background-color: #121212;
|
||||
--accent-color: #3498db;
|
||||
--accent-hover-color: #2980b9;
|
||||
--tab-active-bg-color: #444;
|
||||
--tab-bg-color: #222;
|
||||
--border-color: #555;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
line-height: 1.6;
|
||||
background-color: var(--background-color);
|
||||
color: var(--secondary-color);
|
||||
transition: background-color 0.3s, color 0.3s;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
button {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: var(--background-color);
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus,
|
||||
button:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--primary-hover-color);
|
||||
}
|
||||
|
||||
.card-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--tab-bg-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
width: 200px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Modal styles */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.3s ease, visibility 0.3s ease;
|
||||
}
|
||||
|
||||
.modal.show {
|
||||
display: flex;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.modal.hide {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.3s ease, visibility 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: var(--background-color);
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
width: 500px;
|
||||
max-width: 90%;
|
||||
position: relative;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
|
||||
transform: translateY(-20px);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.modal.show .modal-content {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
|
||||
.dark-mode-toggle {
|
||||
background-color: var(--accent-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.3s;
|
||||
margin-left: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.dark-mode-toggle:hover {
|
||||
background-color: var(--accent-hover-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: var(--secondary-color);
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.checkbox-container {
|
||||
display: block;
|
||||
position: relative;
|
||||
padding-left: 35px;
|
||||
margin-bottom: 12px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.checkbox-container input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
background-color: var(--background-color);
|
||||
border: 2px solid var(--accent-color);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.checkbox-container:hover input~.checkmark {
|
||||
background-color: var(--accent-color);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.checkbox-container input:checked~.checkmark {
|
||||
background-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.checkmark:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.checkbox-container input:checked~.checkmark:after {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.checkbox-container .checkmark:after {
|
||||
left: 9px;
|
||||
top: 5px;
|
||||
width: 5px;
|
||||
height: 10px;
|
||||
border: solid white;
|
||||
border-width: 0 3px 3px 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
display: none;
|
||||
background-color: var(--secondary-color);
|
||||
color: var(--background-color);
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
z-index: 1000;
|
||||
max-width: 300px;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body data-theme="dark">
|
||||
<h1>Regex and Custom Format Manager</h1>
|
||||
<button class="dark-mode-toggle" onclick="toggleDarkMode()">🌙</button>
|
||||
<div class="tab">
|
||||
<button class="tablinks" onclick="openTab(event, 'RegexTab')" id="defaultOpen">Regex</button>
|
||||
<button class="tablinks" onclick="openTab(event, 'CustomFormatTab')">Custom Format</button>
|
||||
</div>
|
||||
|
||||
<div id="CustomFormatTab" class="tabcontent">
|
||||
<h2>Manage Custom Formats</h2>
|
||||
<div id="customFormatList" class="card-container"></div>
|
||||
<button onclick="openCustomFormatModal()">Add New Custom Format</button>
|
||||
</div>
|
||||
|
||||
<div id="RegexTab" class="tabcontent">
|
||||
<h2>Manage Regex Patterns</h2>
|
||||
<div id="regexList" class="card-container"></div>
|
||||
<button onclick="openRegexModal()">Add New Regex Pattern</button>
|
||||
</div>
|
||||
|
||||
<!-- Custom Format Modal -->
|
||||
<div id="customFormatModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeCustomFormatModal()">×</span>
|
||||
<h3>Edit/Create Custom Format</h3>
|
||||
<input type="text" id="formatName" placeholder="Format Name">
|
||||
<input type="text" id="formatDescription" placeholder="Description">
|
||||
|
||||
<h3>Conditions</h3>
|
||||
<div id="conditionsList" class="card-container"></div>
|
||||
<button onclick="openConditionModal()">Add Condition</button>
|
||||
|
||||
<button onclick="saveFormat()">Save Format</button>
|
||||
<button id="deleteFormatButton" onclick="deleteFormat()"
|
||||
style="background-color: red; color: white; display: none;">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Condition Modal -->
|
||||
<div id="conditionModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeConditionModal()">×</span>
|
||||
<h3>Add/Edit Condition</h3>
|
||||
<input type="text" id="conditionName" placeholder="Condition Name">
|
||||
<select id="conditionType" onchange="toggleConditionFields()">
|
||||
<option value="regex">Regex</option>
|
||||
<option value="size">Size</option>
|
||||
</select>
|
||||
<div id="regexCondition">
|
||||
<select id="existingRegex"></select>
|
||||
</div>
|
||||
<div id="sizeCondition" style="display:none;">
|
||||
<input type="number" id="minSize" placeholder="Min Size (bytes)">
|
||||
<input type="number" id="maxSize" placeholder="Max Size (bytes)">
|
||||
</div>
|
||||
<div class="checkbox-group">
|
||||
<label class="checkbox-container"
|
||||
onmouseover="showTooltip(event, 'Negate: Inverts the condition. If checked, the condition is true when it\'s not met.')"
|
||||
onmouseout="hideTooltip()">
|
||||
<input type="checkbox" id="conditionNegate">
|
||||
<span class="checkmark"></span>
|
||||
Negate
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox-group">
|
||||
<label class="checkbox-container"
|
||||
onmouseover="showTooltip(event, 'Required: Makes the condition mandatory. If checked, the format is invalid when this condition is not met.')"
|
||||
onmouseout="hideTooltip()">
|
||||
<input type="checkbox" id="conditionRequired">
|
||||
<span class="checkmark"></span>
|
||||
Required
|
||||
</label>
|
||||
</div>
|
||||
<button onclick="saveCondition()">Save Condition</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="regexModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeRegexModal()">×</span>
|
||||
<h3>Edit/Create Regex Pattern</h3>
|
||||
<input type="text" id="regexName" placeholder="Regex Name">
|
||||
<input type="text" id="regexPattern" placeholder="Regex Pattern" oninput="testRegex()">
|
||||
<input type="text" id="regexDescription" placeholder="Description">
|
||||
<h3>Test Regex</h3>
|
||||
<input type="text" id="testPhrase" placeholder="Enter phrase to test" oninput="testRegex()">
|
||||
<div id="testResult"></div>
|
||||
<div id="regexError" style="color: red; margin-top: 10px;"></div>
|
||||
<button onclick="saveRegex()">Save Regex</button>
|
||||
<button id="deleteRegexButton" onclick="deleteRegex()"
|
||||
style="background-color: red; color: white; display: none;">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tooltip" class="tooltip"></div>
|
||||
|
||||
<script>
|
||||
const API_BASE_URL = 'http://localhost:5000';
|
||||
let currentFormatConditions = [];
|
||||
|
||||
document.getElementById("defaultOpen").click();
|
||||
|
||||
function openTab(evt, tabName) {
|
||||
var i, tabcontent, tablinks;
|
||||
tabcontent = document.getElementsByClassName("tabcontent");
|
||||
for (i = 0; i < tabcontent.length; i++) {
|
||||
tabcontent[i].style.display = "none";
|
||||
}
|
||||
tablinks = document.getElementsByClassName("tablinks");
|
||||
for (i = 0; i < tablinks.length; i++) {
|
||||
tablinks[i].className = tablinks[i].className.replace(" active", "");
|
||||
}
|
||||
document.getElementById(tabName).style.display = "block";
|
||||
evt.currentTarget.className += " active";
|
||||
}
|
||||
|
||||
function toggleDarkMode() {
|
||||
const currentTheme = document.body.getAttribute('data-theme');
|
||||
document.body.setAttribute('data-theme', currentTheme === 'light' ? 'dark' : 'light');
|
||||
}
|
||||
|
||||
// Ensure Regex Dropdown is populated when the modal is opened
|
||||
function openCustomFormatModal(format = null) {
|
||||
const modal = document.getElementById('customFormatModal');
|
||||
modal.style.display = 'flex';
|
||||
setTimeout(() => {
|
||||
modal.classList.add('show');
|
||||
modal.classList.remove('hide');
|
||||
}, 10); // Small delay to trigger the transition
|
||||
|
||||
const deleteButton = document.getElementById('deleteFormatButton');
|
||||
|
||||
if (format) {
|
||||
loadCustomFormat(format);
|
||||
deleteButton.style.display = 'inline-block'; // Show delete button
|
||||
deleteButton.setAttribute('onclick', `deleteFormat(${format.id})`); // Set delete action
|
||||
} else {
|
||||
clearFormatInputs();
|
||||
deleteButton.style.display = 'none'; // Hide delete button
|
||||
}
|
||||
}
|
||||
|
||||
function closeCustomFormatModal() {
|
||||
const modal = document.getElementById('customFormatModal');
|
||||
modal.classList.add('hide');
|
||||
modal.classList.remove('show');
|
||||
setTimeout(() => {
|
||||
modal.style.display = 'none';
|
||||
}, 300); // Hide after fade-out
|
||||
}
|
||||
|
||||
function openRegexModal(regex = null) {
|
||||
const modal = document.getElementById('regexModal');
|
||||
modal.style.display = 'flex';
|
||||
setTimeout(() => {
|
||||
modal.classList.add('show');
|
||||
modal.classList.remove('hide');
|
||||
}, 10); // Small delay to trigger the transition
|
||||
|
||||
const deleteButton = document.getElementById('deleteRegexButton');
|
||||
|
||||
if (regex) {
|
||||
loadRegex(regex);
|
||||
deleteButton.style.display = 'inline-block'; // Show delete button
|
||||
deleteButton.setAttribute('onclick', `deleteRegex(${regex.id})`); // Set delete action
|
||||
} else {
|
||||
clearRegexInputs();
|
||||
deleteButton.style.display = 'none'; // Hide delete button
|
||||
}
|
||||
}
|
||||
function closeRegexModal() {
|
||||
const modal = document.getElementById('regexModal');
|
||||
modal.classList.add('hide');
|
||||
modal.classList.remove('show');
|
||||
setTimeout(() => {
|
||||
modal.style.display = 'none';
|
||||
}, 300); // Hide after fade-out
|
||||
}
|
||||
|
||||
function openConditionModal(condition = null) {
|
||||
const modal = document.getElementById('conditionModal');
|
||||
modal.style.display = 'flex';
|
||||
setTimeout(() => {
|
||||
modal.classList.add('show');
|
||||
modal.classList.remove('hide');
|
||||
}, 10); // Small delay to trigger the transition
|
||||
|
||||
if (condition) {
|
||||
loadCondition(condition);
|
||||
} else {
|
||||
clearConditionInputs();
|
||||
}
|
||||
}
|
||||
|
||||
function loadCondition(condition) {
|
||||
document.getElementById('conditionName').value = condition.name;
|
||||
document.getElementById('conditionType').value = condition.regex_name ? 'regex' : 'size';
|
||||
toggleConditionFields(); // Toggle visibility based on type
|
||||
|
||||
if (condition.regex_name) {
|
||||
document.getElementById('existingRegex').value = condition.regex_name;
|
||||
} else {
|
||||
document.getElementById('minSize').value = condition.min || '';
|
||||
document.getElementById('maxSize').value = condition.max || '';
|
||||
}
|
||||
|
||||
document.getElementById('conditionNegate').checked = condition.negate;
|
||||
document.getElementById('conditionRequired').checked = condition.required;
|
||||
}
|
||||
|
||||
function closeConditionModal() {
|
||||
const modal = document.getElementById('conditionModal');
|
||||
modal.classList.add('hide');
|
||||
modal.classList.remove('show');
|
||||
setTimeout(() => {
|
||||
modal.style.display = 'none';
|
||||
}, 300); // Hide after fade-out
|
||||
}
|
||||
|
||||
// Close modal when clicking outside of it
|
||||
window.onclick = function (event) {
|
||||
const customFormatModal = document.getElementById('customFormatModal');
|
||||
const regexModal = document.getElementById('regexModal');
|
||||
const conditionModal = document.getElementById('conditionModal');
|
||||
if (event.target === customFormatModal) {
|
||||
closeCustomFormatModal();
|
||||
}
|
||||
if (event.target === regexModal) {
|
||||
closeRegexModal();
|
||||
}
|
||||
if (event.target === conditionModal) {
|
||||
closeConditionModal();
|
||||
}
|
||||
}
|
||||
function saveRegex() {
|
||||
const regex = {
|
||||
id: 0, // This will be set by the backend to the next available ID
|
||||
name: document.getElementById('regexName').value,
|
||||
pattern: document.getElementById('regexPattern').value,
|
||||
description: document.getElementById('regexDescription').value
|
||||
};
|
||||
fetch(`${API_BASE_URL}/save_regex`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(regex),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Success:', data);
|
||||
clearRegexInputs();
|
||||
updateRegexList();
|
||||
updateRegexSelect(); // Refresh the regex select list after saving
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function clearRegexInputs() {
|
||||
document.getElementById('regexName').value = '';
|
||||
document.getElementById('regexPattern').value = '';
|
||||
document.getElementById('regexDescription').value = '';
|
||||
}
|
||||
|
||||
function testRegex() {
|
||||
const pattern = document.getElementById('regexPattern').value;
|
||||
const phrase = document.getElementById('testPhrase').value;
|
||||
const resultDiv = document.getElementById('testResult');
|
||||
const errorDiv = document.getElementById('regexError');
|
||||
|
||||
resultDiv.innerHTML = '';
|
||||
errorDiv.innerHTML = '';
|
||||
|
||||
try {
|
||||
const regex = new RegExp(pattern, 'gi');
|
||||
|
||||
if (regex.test(phrase)) {
|
||||
const matches = phrase.replace(regex, match => `<mark>${match}</mark>`);
|
||||
resultDiv.innerHTML = `Matched: ${matches}`;
|
||||
} else {
|
||||
resultDiv.innerHTML = 'No match found.';
|
||||
}
|
||||
} catch (e) {
|
||||
errorDiv.innerHTML = `Invalid regex: ${e.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
function loadRegex(regex) {
|
||||
document.getElementById('regexName').value = regex.name;
|
||||
document.getElementById('regexPattern').value = regex.pattern;
|
||||
document.getElementById('regexDescription').value = regex.description;
|
||||
testRegex();
|
||||
}
|
||||
|
||||
function updateRegexList() {
|
||||
fetch(`${API_BASE_URL}/get_regexes`)
|
||||
.then(response => response.json())
|
||||
.then(regexes => {
|
||||
const list = document.getElementById('regexList');
|
||||
list.innerHTML = '';
|
||||
regexes.forEach(regex => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
card.innerHTML = `${regex.name}`;
|
||||
card.onclick = () => openRegexModal(regex);
|
||||
list.appendChild(card);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function updateRegexSelect() {
|
||||
fetch(`${API_BASE_URL}/get_regexes`)
|
||||
.then(response => response.json())
|
||||
.then(regexes => {
|
||||
const select = document.getElementById('existingRegex');
|
||||
select.innerHTML = '';
|
||||
regexes.forEach(regex => {
|
||||
const option = document.createElement('option');
|
||||
option.value = regex.name;
|
||||
option.textContent = `${regex.name}: ${regex.pattern}`;
|
||||
select.appendChild(option);
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function addCondition() {
|
||||
const condition = {
|
||||
name: document.getElementById('conditionName').value,
|
||||
negate: document.getElementById('conditionNegate').checked,
|
||||
required: document.getElementById('conditionRequired').checked
|
||||
};
|
||||
|
||||
const conditionType = document.getElementById('conditionType').value;
|
||||
if (conditionType === 'regex') {
|
||||
condition.regex_name = document.getElementById('existingRegex').value;
|
||||
} else if (conditionType === 'size') {
|
||||
condition.min = parseInt(document.getElementById('minSize').value);
|
||||
condition.max = parseInt(document.getElementById('maxSize').value);
|
||||
}
|
||||
|
||||
currentFormatConditions.push(condition);
|
||||
updateConditionsList();
|
||||
clearConditionInputs();
|
||||
}
|
||||
|
||||
function clearConditionInputs() {
|
||||
document.getElementById('conditionName').value = '';
|
||||
document.getElementById('conditionType').value = 'regex';
|
||||
document.getElementById('conditionNegate').checked = false;
|
||||
document.getElementById('conditionRequired').checked = false;
|
||||
document.getElementById('minSize').value = '';
|
||||
document.getElementById('maxSize').value = '';
|
||||
}
|
||||
|
||||
function updateConditionsList() {
|
||||
const list = document.getElementById('conditionsList');
|
||||
list.innerHTML = '';
|
||||
currentFormatConditions.forEach((condition, index) => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
card.textContent = `${condition.name} (${condition.regex_name ? 'regex: ' + condition.regex_name : 'size'})`;
|
||||
if (condition.negate) card.textContent += ' [Negated]';
|
||||
if (condition.required) card.textContent += ' [Required]';
|
||||
card.onclick = () => openConditionModal(condition); // Click to edit
|
||||
list.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function editCondition(index) {
|
||||
const condition = currentFormatConditions[index];
|
||||
document.getElementById('conditionName').value = condition.name;
|
||||
document.getElementById('conditionType').value = condition.regex_name ? 'regex' : 'size';
|
||||
toggleConditionFields(); // Toggle visibility based on type
|
||||
|
||||
if (condition.regex_name) {
|
||||
document.getElementById('existingRegex').value = condition.regex_name;
|
||||
} else {
|
||||
document.getElementById('minSize').value = condition.min || '';
|
||||
document.getElementById('maxSize').value = condition.max || '';
|
||||
}
|
||||
|
||||
document.getElementById('conditionNegate').checked = condition.negate;
|
||||
document.getElementById('conditionRequired').checked = condition.required;
|
||||
|
||||
// Update the 'add' button to save the changes
|
||||
document.querySelector('button[onclick="addCondition()"]').setAttribute('onclick', `saveCondition(${index})`);
|
||||
}
|
||||
|
||||
function saveCondition() {
|
||||
const condition = {
|
||||
name: document.getElementById('conditionName').value,
|
||||
negate: document.getElementById('conditionNegate').checked,
|
||||
required: document.getElementById('conditionRequired').checked
|
||||
};
|
||||
|
||||
if (document.getElementById('conditionType').value === 'regex') {
|
||||
condition.regex_name = document.getElementById('existingRegex').value;
|
||||
} else {
|
||||
condition.min = parseInt(document.getElementById('minSize').value);
|
||||
condition.max = parseInt(document.getElementById('maxSize').value);
|
||||
}
|
||||
|
||||
currentFormatConditions.push(condition);
|
||||
updateConditionsList();
|
||||
closeConditionModal();
|
||||
}
|
||||
function saveFormat() {
|
||||
const format = {
|
||||
id: 0, // This will be set by the backend to the next available ID
|
||||
name: document.getElementById('formatName').value,
|
||||
description: document.getElementById('formatDescription').value,
|
||||
conditions: currentFormatConditions
|
||||
};
|
||||
fetch(`${API_BASE_URL}/save_format`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(format),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Success:', data);
|
||||
clearFormatInputs();
|
||||
updateCustomFormatList();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function clearFormatInputs() {
|
||||
document.getElementById('formatName').value = '';
|
||||
document.getElementById('formatDescription').value = '';
|
||||
currentFormatConditions = [];
|
||||
updateConditionsList();
|
||||
}
|
||||
|
||||
function loadCustomFormat(format) {
|
||||
document.getElementById('formatName').value = format.name;
|
||||
document.getElementById('formatDescription').value = format.description;
|
||||
currentFormatConditions = format.conditions || [];
|
||||
updateConditionsList();
|
||||
}
|
||||
|
||||
function updateCustomFormatList() {
|
||||
fetch(`${API_BASE_URL}/get_formats`)
|
||||
.then(response => response.json())
|
||||
.then(formats => {
|
||||
const list = document.getElementById('customFormatList');
|
||||
list.innerHTML = '';
|
||||
formats.forEach(format => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
card.innerHTML = `${format.name}`;
|
||||
card.onclick = () => openCustomFormatModal(format);
|
||||
list.appendChild(card);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function deleteRegex(id) {
|
||||
if (confirm("Are you sure you want to delete this regex?")) {
|
||||
fetch(`${API_BASE_URL}/delete_regex/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Delete Success:', data);
|
||||
closeRegexModal(); // Close the modal after deletion
|
||||
updateRegexList(); // Refresh the list after deletion
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function deleteFormat(id) {
|
||||
if (confirm("Are you sure you want to delete this custom format?")) {
|
||||
fetch(`${API_BASE_URL}/delete_format/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Delete Success:', data);
|
||||
closeCustomFormatModal(); // Close the modal after deletion
|
||||
updateCustomFormatList(); // Refresh the list after deletion
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function showTooltip(event, text) {
|
||||
const tooltip = document.getElementById('tooltip');
|
||||
tooltip.innerHTML = text;
|
||||
tooltip.style.display = 'block';
|
||||
tooltip.style.left = (event.pageX + 10) + 'px';
|
||||
tooltip.style.top = (event.pageY + 10) + 'px';
|
||||
}
|
||||
|
||||
function hideTooltip() {
|
||||
const tooltip = document.getElementById('tooltip');
|
||||
tooltip.style.display = 'none';
|
||||
}
|
||||
|
||||
|
||||
// On page load
|
||||
updateCustomFormatList();
|
||||
updateRegexList();
|
||||
updateRegexSelect();
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user