Initial Commit

This commit is contained in:
santiagosayshey
2024-08-15 14:29:46 +09:30
committed by Sam Chau
parent 28e0cad4a1
commit 525d86063d
46 changed files with 8273 additions and 0 deletions

13
.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
# Node
node_modules/
dist/
# Python
__pycache__/
*.pyc
# Environment variables
.env
# OS files
.DS_Store

10
backend/Dockerfile Normal file
View 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
View 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
View File

View File

View 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

View 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

View File

View 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

View 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

View 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

View 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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

25
frontend/package.json Normal file
View 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"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

1
frontend/public/vite.svg Normal file
View 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
View 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
View 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;
}
};

View 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;

View 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;

View 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;

View 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;

View 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">&times;</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;

View 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;

View 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;

View 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">&times;</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;

View 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;

View 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;

View 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;

View 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;

View 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
View 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
View 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>,
)

View 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
View 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
View 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
View 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
View 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()">&times;</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()">&times;</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()">&times;</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>