Improve ObjectEditor and Add TableEditor

This commit is contained in:
dwindown
2025-09-23 14:17:13 +07:00
parent cf750114f7
commit 977e784df2
15 changed files with 5329 additions and 345 deletions

View File

@@ -0,0 +1,75 @@
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// Log the error for debugging
this.setState({
error: error,
errorInfo: errorInfo
});
// You can also log the error to an error reporting service here
console.error('ErrorBoundary caught an error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
// Fallback UI
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center p-4">
<div className="max-w-md w-full bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 text-center">
<div className="mb-4">
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 dark:bg-red-900/20">
<svg className="h-6 w-6 text-red-600 dark:text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
</div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
Something went wrong
</h2>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
The application encountered an error. This might be due to browser compatibility issues.
</p>
<div className="space-y-3">
<button
onClick={() => window.location.reload()}
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md transition-colors"
>
Reload Page
</button>
<button
onClick={() => this.setState({ hasError: false, error: null, errorInfo: null })}
className="w-full bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200 font-medium py-2 px-4 rounded-md transition-colors"
>
Try Again
</button>
</div>
<div className="mt-4 text-xs text-gray-500 dark:text-gray-400">
If you're using Telegram's built-in browser, try opening this link in your default browser for better compatibility.
</div>
</div>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect, useRef } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Home, Hash, FileSpreadsheet, Wand2, GitCompare, Menu, X, LinkIcon, Code2, ChevronDown, Type, Edit3 } from 'lucide-react';
import { Home, Hash, FileSpreadsheet, Wand2, GitCompare, Menu, X, LinkIcon, Code2, ChevronDown, Type, Edit3, Table } from 'lucide-react';
import ThemeToggle from './ThemeToggle';
import ToolSidebar from './ToolSidebar';
@@ -36,6 +36,7 @@ const Layout = ({ children }) => {
const tools = [
{ path: '/object-editor', name: 'Object Editor', icon: Edit3, description: 'Visual editor for JSON & PHP objects' },
{ path: '/table-editor', name: 'Table Editor', icon: Table, description: 'Import, edit & export tabular data' },
{ path: '/url', name: 'URL Tool', icon: LinkIcon, description: 'URL encode/decode' },
{ path: '/base64', name: 'Base64 Tool', icon: Hash, description: 'Base64 encode/decode' },
{ path: '/csv-json', name: 'CSV/JSON Tool', icon: FileSpreadsheet, description: 'Convert CSV ↔ JSON' },

View File

@@ -106,9 +106,17 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
delete current[key];
}
// Check if we're removing from root level and it's the last property
if (parentPath === 'root' && Object.keys(newData).length === 0) {
// Add an empty property to maintain initial state, like TableEditor maintains at least one row
newData[''] = '';
console.log('🔄 ADDED INITIAL EMPTY PROPERTY - Last root property was deleted');
}
console.log('🗑️ REMOVE PROPERTY - After:', {
parentPath,
remainingKeys: Array.isArray(current) ? current.length : Object.keys(current)
remainingKeys: Array.isArray(current) ? current.length : Object.keys(current),
totalRootKeys: Object.keys(newData).length
});
updateData(newData);
@@ -223,6 +231,25 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
return current;
};
// Helper function to display string values with proper unescaping
const getDisplayValue = (value) => {
if (value === null) return 'null';
if (value === undefined) return '';
const stringValue = value.toString();
// If it's a string, unescape common JSON escape sequences for display
if (typeof value === 'string') {
return stringValue
.replace(/\\"/g, '"') // Unescape quotes
.replace(/\\'/g, "'") // Unescape single quotes
.replace(/\\\//g, '/') // Unescape forward slashes
.replace(/\\\\/g, '\\'); // Unescape backslashes (do this last)
}
return stringValue;
};
const renameKey = (oldKey, newKey, path) => {
if (oldKey === newKey || !newKey.trim()) return;
@@ -401,11 +428,7 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
) : (
typeof value === 'string' && value.includes('\n') ? (
<textarea
value={
value === null ? 'null' :
value === undefined ? '' :
value.toString()
}
value={getDisplayValue(value)}
onChange={(e) => updateValue(e.target.value, path)}
className="flex-1 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 min-w-0 resize-y"
placeholder="Long text value"
@@ -414,11 +437,7 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
) : (
<input
type="text"
value={
value === null ? 'null' :
value === undefined ? '' :
value.toString()
}
value={getDisplayValue(value)}
onChange={(e) => updateValue(e.target.value, path)}
className="flex-1 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 min-w-0"
placeholder="Value"
@@ -500,17 +519,9 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
};
return (
<div className="border border-gray-300 dark:border-gray-600 rounded-lg p-4 bg-white dark:bg-gray-800 min-h-96 w-full">
<div className="flex items-center justify-between mb-4">
<div className="min-h-96 w-full">
<div className="mb-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">Structured Data Editor</h3>
<button
onClick={() => addProperty(data, 'root')}
className="flex items-center space-x-1 px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors flex-shrink-0"
>
<Plus className="h-4 w-4" />
<span className="hidden sm:inline">Add Property</span>
<span className="sm:hidden">Add</span>
</button>
</div>
<div className="overflow-x-hidden">
@@ -524,6 +535,15 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
renderValue(value, key, `root.${key}`, 'root')
)
)}
{/* Root level Add Property button */}
<button
onClick={() => addProperty(data, 'root')}
className="flex items-center space-x-1 px-2 py-1 text-sm text-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900 rounded"
>
<Plus className="h-4 w-4" />
<span>Add Property</span>
</button>
</div>
</div>
);

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Search, LinkIcon, Hash, FileSpreadsheet, Wand2, GitCompare, Home, ChevronLeft, ChevronRight, Type, Edit3 } from 'lucide-react';
import { Search, LinkIcon, Hash, Table, Wand2, GitCompare, Home, ChevronLeft, ChevronRight, Type, Edit3 } from 'lucide-react';
const ToolSidebar = () => {
const location = useLocation();
@@ -10,9 +10,9 @@ const ToolSidebar = () => {
const tools = [
{ path: '/', name: 'Home', icon: Home, description: 'Back to homepage' },
{ path: '/object-editor', name: 'Object Editor', icon: Edit3, description: 'Visual editor for JSON & PHP objects' },
{ path: '/table-editor', name: 'Table Editor', icon: Table, description: 'Import, edit & export tabular data' },
{ path: '/url', name: 'URL Tool', icon: LinkIcon, description: 'URL encode/decode' },
{ path: '/base64', name: 'Base64 Tool', icon: Hash, description: 'Base64 encode/decode' },
{ path: '/csv-json', name: 'CSV/JSON Tool', icon: FileSpreadsheet, description: 'Convert CSV ↔ JSON' },
{ path: '/beautifier', name: 'Beautifier Tool', icon: Wand2, description: 'Beautify/minify code' },
{ path: '/diff', name: 'Diff Tool', icon: GitCompare, description: 'Compare text differences' },
{ path: '/text-length', name: 'Text Length Checker', icon: Type, description: 'Analyze text length & stats' },