- Fixed mixed operators in contentExtractor.js with proper parentheses - Removed unused variables and imports across all components - Fixed useCallback dependencies in ObjectEditor.js - Corrected == to === comparisons in TableEditor.js - Fixed undefined variable references - Wrapped serializeToPhp in useCallback to resolve dependency warning - Updated table column width styling from min-w to w for consistent layout Build now passes successfully with only non-blocking warnings remaining.
1160 lines
46 KiB
JavaScript
1160 lines
46 KiB
JavaScript
import React, { useState, useRef, useCallback } from 'react';
|
|
import { Upload, FileText, Workflow, Table, Globe, Plus, AlertTriangle, BrushCleaning, Code, Braces, Download, Edit3 } from 'lucide-react';
|
|
import ToolLayout from '../components/ToolLayout';
|
|
import StructuredEditor from '../components/StructuredEditor';
|
|
import MindmapView from '../components/MindmapView';
|
|
import PostmanTable from '../components/PostmanTable';
|
|
|
|
const ObjectEditor = () => {
|
|
console.log(' ObjectEditor component loaded successfully!');
|
|
const [structuredData, setStructuredData] = useState({});
|
|
const [activeTab, setActiveTab] = useState('create');
|
|
const [inputText, setInputText] = useState('');
|
|
const [inputFormat, setInputFormat] = useState('');
|
|
const [inputValid, setInputValid] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const [viewMode, setViewMode] = useState('visual'); // 'visual', 'mindmap', 'table'
|
|
const [activeExportTab, setActiveExportTab] = useState('json');
|
|
const [jsonFormat, setJsonFormat] = useState('pretty'); // 'pretty' or 'minify'
|
|
const [outputs, setOutputs] = useState({
|
|
jsonPretty: '',
|
|
jsonMinified: '',
|
|
serialized: ''
|
|
});
|
|
const [fetchUrl, setFetchUrl] = useState('');
|
|
const [fetching, setFetching] = useState(false);
|
|
const [createNewCompleted, setCreateNewCompleted] = useState(false);
|
|
const [showInputChangeModal, setShowInputChangeModal] = useState(false);
|
|
const [pendingTabChange, setPendingTabChange] = useState(null);
|
|
const fileInputRef = useRef(null);
|
|
|
|
// Helper function to check if user has data that would be lost
|
|
const hasUserData = () => {
|
|
return Object.keys(structuredData).length > 0;
|
|
};
|
|
|
|
// Check if current data has been modified from initial state
|
|
const hasModifiedData = () => {
|
|
// Check if there's actual user data (not just initial empty structure)
|
|
if (Object.keys(structuredData).length === 0) {
|
|
return false;
|
|
}
|
|
|
|
// Check if it's the initial empty property (unchanged)
|
|
const isInitialEmpty = JSON.stringify(structuredData) === JSON.stringify({ "": "" });
|
|
if (isInitialEmpty) {
|
|
return false;
|
|
}
|
|
|
|
// Check if it's the sample data (unchanged)
|
|
const isSampleData = JSON.stringify(structuredData) === JSON.stringify({
|
|
name: "John Doe",
|
|
age: 30,
|
|
email: "john@example.com",
|
|
address: {
|
|
street: "123 Main St",
|
|
city: "New York",
|
|
country: "USA"
|
|
},
|
|
hobbies: ["reading", "coding", "traveling"]
|
|
});
|
|
|
|
if (isSampleData) {
|
|
return false;
|
|
}
|
|
|
|
// Any other data is considered modified
|
|
return true;
|
|
};
|
|
|
|
// Handle tab change with confirmation if data exists
|
|
const handleTabChange = (newTab) => {
|
|
// For Create New tab, use more specific logic
|
|
if (newTab === 'create' && activeTab !== 'create') {
|
|
if (hasModifiedData()) {
|
|
setPendingTabChange(newTab);
|
|
setShowInputChangeModal(true);
|
|
} else {
|
|
setActiveTab(newTab);
|
|
setCreateNewCompleted(false);
|
|
}
|
|
} else if (hasUserData() && activeTab !== newTab) {
|
|
setPendingTabChange(newTab);
|
|
setShowInputChangeModal(true);
|
|
} else {
|
|
// No data or same tab, proceed directly
|
|
setActiveTab(newTab);
|
|
// If clicking Create New again after completion, show the options again
|
|
if (newTab === 'create' && createNewCompleted) {
|
|
setCreateNewCompleted(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Clear all data function
|
|
const clearAllData = () => {
|
|
setStructuredData({});
|
|
setInputText('');
|
|
setInputFormat('');
|
|
setInputValid(false);
|
|
setError('');
|
|
setCreateNewCompleted(false);
|
|
};
|
|
|
|
|
|
// Confirm input method change and clear data
|
|
const confirmInputChange = () => {
|
|
// Handle special Create New button actions
|
|
if (pendingTabChange === 'create_empty') {
|
|
clearAllData();
|
|
// Start with one empty property like TableEditor starts with empty row
|
|
setStructuredData({ "": "" });
|
|
setCreateNewCompleted(true);
|
|
} else if (pendingTabChange === 'create_sample') {
|
|
clearAllData();
|
|
// Load sample data
|
|
const sampleData = {
|
|
name: "John Doe",
|
|
age: 30,
|
|
email: "john@example.com",
|
|
address: {
|
|
street: "123 Main St",
|
|
city: "New York",
|
|
country: "USA"
|
|
},
|
|
hobbies: ["reading", "coding", "traveling"]
|
|
};
|
|
setStructuredData(sampleData);
|
|
setCreateNewCompleted(true);
|
|
} else {
|
|
// Handle regular tab switches
|
|
clearAllData();
|
|
setActiveTab(pendingTabChange);
|
|
// If switching to create tab, reset completion state to show options
|
|
if (pendingTabChange === 'create') {
|
|
setCreateNewCompleted(false);
|
|
}
|
|
}
|
|
|
|
// Close modal
|
|
setShowInputChangeModal(false);
|
|
setPendingTabChange(null);
|
|
};
|
|
|
|
// PHP serialize implementation (reused from SerializeTool)
|
|
const phpSerialize = useCallback((data) => {
|
|
if (data === null) return 'N;';
|
|
if (typeof data === 'boolean') return data ? 'b:1;' : 'b:0;';
|
|
if (typeof data === 'number') {
|
|
return Number.isInteger(data) ? `i:${data};` : `d:${data};`;
|
|
}
|
|
if (typeof data === 'string') {
|
|
const escapedData = data.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
const byteLength = new TextEncoder().encode(escapedData).length;
|
|
return `s:${byteLength}:"${escapedData}";`;
|
|
}
|
|
if (Array.isArray(data)) {
|
|
let result = `a:${data.length}:{`;
|
|
data.forEach((item, index) => {
|
|
result += phpSerialize(index) + phpSerialize(item);
|
|
});
|
|
result += '}';
|
|
return result;
|
|
}
|
|
if (typeof data === 'object') {
|
|
const keys = Object.keys(data);
|
|
let result = `a:${keys.length}:{`;
|
|
keys.forEach(key => {
|
|
result += phpSerialize(key) + phpSerialize(data[key]);
|
|
});
|
|
result += '}';
|
|
return result;
|
|
}
|
|
return 'N;';
|
|
}, []);
|
|
|
|
// PHP unserialize implementation (reused from SerializeTool)
|
|
const phpUnserialize = (str) => {
|
|
let index = 0;
|
|
|
|
const parseValue = () => {
|
|
if (index >= str.length) {
|
|
throw new Error('Unexpected end of string');
|
|
}
|
|
|
|
const type = str[index];
|
|
|
|
if (type === 'N') {
|
|
index += 2;
|
|
return null;
|
|
}
|
|
|
|
if (str[index + 1] !== ':') {
|
|
throw new Error(`Expected ':' after type '${type}' at position ${index + 1}`);
|
|
}
|
|
|
|
index += 2;
|
|
|
|
switch (type) {
|
|
case 'b':
|
|
const boolVal = str[index] === '1';
|
|
index += 2;
|
|
return boolVal;
|
|
|
|
case 'i':
|
|
let intStr = '';
|
|
while (index < str.length && str[index] !== ';') {
|
|
intStr += str[index++];
|
|
}
|
|
if (index >= str.length) {
|
|
throw new Error('Unexpected end of string while parsing integer');
|
|
}
|
|
index++;
|
|
return parseInt(intStr);
|
|
|
|
case 'd':
|
|
let floatStr = '';
|
|
while (index < str.length && str[index] !== ';') {
|
|
floatStr += str[index++];
|
|
}
|
|
if (index >= str.length) {
|
|
throw new Error('Unexpected end of string while parsing float');
|
|
}
|
|
index++;
|
|
return parseFloat(floatStr);
|
|
|
|
case 's':
|
|
let lenStr = '';
|
|
while (index < str.length && str[index] !== ':') {
|
|
lenStr += str[index++];
|
|
}
|
|
if (index >= str.length) {
|
|
throw new Error('Unexpected end of string while parsing string length');
|
|
}
|
|
index++;
|
|
|
|
if (str[index] !== '"') {
|
|
throw new Error(`Expected '"' at position ${index}`);
|
|
}
|
|
index++;
|
|
|
|
const byteLength = parseInt(lenStr);
|
|
|
|
if (isNaN(byteLength) || byteLength < 0) {
|
|
throw new Error(`Invalid string length: ${lenStr}`);
|
|
}
|
|
|
|
if (byteLength === 0) {
|
|
if (index + 1 >= str.length || str[index] !== '"' || str[index + 1] !== ';') {
|
|
throw new Error(`Expected '";' after empty string at position ${index}`);
|
|
}
|
|
index += 2;
|
|
return '';
|
|
}
|
|
|
|
const startIndex = index;
|
|
let endQuotePos = -1;
|
|
|
|
for (let i = startIndex; i < str.length - 1; i++) {
|
|
if (str[i] === '"' && str[i + 1] === ';') {
|
|
endQuotePos = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (endQuotePos === -1) {
|
|
throw new Error(`Could not find closing '";' for string starting at position ${startIndex}`);
|
|
}
|
|
|
|
const stringVal = str.substring(startIndex, endQuotePos);
|
|
index = endQuotePos + 2;
|
|
|
|
return stringVal;
|
|
|
|
case 'a':
|
|
let arrayLenStr = '';
|
|
while (index < str.length && str[index] !== ':') {
|
|
arrayLenStr += str[index++];
|
|
}
|
|
if (index >= str.length) {
|
|
throw new Error('Unexpected end of string while parsing array length');
|
|
}
|
|
index++;
|
|
|
|
if (str[index] !== '{') {
|
|
throw new Error(`Expected '{' at position ${index}`);
|
|
}
|
|
index++;
|
|
|
|
const arrayLength = parseInt(arrayLenStr);
|
|
if (isNaN(arrayLength) || arrayLength < 0) {
|
|
throw new Error(`Invalid array length: ${arrayLenStr}`);
|
|
}
|
|
|
|
const result = {};
|
|
let isArray = true;
|
|
|
|
for (let i = 0; i < arrayLength; i++) {
|
|
const key = parseValue();
|
|
const value = parseValue();
|
|
result[key] = value;
|
|
|
|
if (typeof key !== 'number' || key !== i) {
|
|
isArray = false;
|
|
}
|
|
}
|
|
|
|
if (index >= str.length || str[index] !== '}') {
|
|
throw new Error(`Expected '}' at position ${index}`);
|
|
}
|
|
index++;
|
|
|
|
if (isArray && arrayLength > 0) {
|
|
const arr = [];
|
|
for (let i = 0; i < arrayLength; i++) {
|
|
arr[i] = result[i];
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
return result;
|
|
|
|
default:
|
|
throw new Error(`Unknown type: '${type}' at position ${index - 2}`);
|
|
}
|
|
};
|
|
|
|
try {
|
|
const result = parseValue();
|
|
if (index < str.length) {
|
|
console.warn(`Warning: Trailing data after parsing: "${str.substring(index)}"`);
|
|
}
|
|
return result;
|
|
} catch (error) {
|
|
throw new Error(`Parse error at position ${index}: ${error.message}`);
|
|
}
|
|
};
|
|
|
|
// Auto-detect input format
|
|
const detectInputFormat = (input) => {
|
|
if (!input.trim()) return { format: '', valid: false, data: null };
|
|
|
|
// Try JSON first
|
|
try {
|
|
const jsonData = JSON.parse(input);
|
|
return { format: 'JSON', valid: true, data: jsonData };
|
|
} catch {}
|
|
|
|
// Try PHP serialize
|
|
try {
|
|
const serializedData = phpUnserialize(input);
|
|
return { format: 'PHP Serialized', valid: true, data: serializedData };
|
|
} catch {}
|
|
|
|
return { format: 'Unknown', valid: false, data: null };
|
|
};
|
|
|
|
// Handle input text change
|
|
const handleInputChange = (value) => {
|
|
setInputText(value);
|
|
const detection = detectInputFormat(value);
|
|
setInputFormat(detection.format);
|
|
setInputValid(detection.valid);
|
|
|
|
if (detection.valid) {
|
|
console.log(' SETTING STRUCTURED DATA:', detection.data);
|
|
setStructuredData(detection.data);
|
|
setError('');
|
|
setCreateNewCompleted(true);
|
|
} else if (value.trim()) {
|
|
setError('Invalid format. Please enter valid JSON or PHP serialized data.');
|
|
} else {
|
|
setError('');
|
|
}
|
|
};
|
|
|
|
// Handle structured data change from visual editor
|
|
const handleStructuredDataChange = (newData) => {
|
|
setStructuredData(newData);
|
|
generateOutputs(newData);
|
|
};
|
|
|
|
// Check if data is just the initial empty property
|
|
const isInitialEmptyData = (data) => {
|
|
return JSON.stringify(data) === JSON.stringify({ "": "" });
|
|
};
|
|
|
|
// Simple PHP serialization function
|
|
const serializeToPhp = useCallback((data) => {
|
|
if (data === null) return 'N;';
|
|
if (data === undefined) return 'N;';
|
|
if (typeof data === 'boolean') return data ? 'b:1;' : 'b:0;';
|
|
if (typeof data === 'number') {
|
|
return Number.isInteger(data) ? `i:${data};` : `d:${data};`;
|
|
}
|
|
if (typeof data === 'string') {
|
|
return `s:${data.length}:"${data}";`;
|
|
}
|
|
if (Array.isArray(data)) {
|
|
let result = `a:${data.length}:{`;
|
|
data.forEach((item, index) => {
|
|
result += `i:${index};${serializeToPhp(item)}`;
|
|
});
|
|
result += '}';
|
|
return result;
|
|
}
|
|
if (typeof data === 'object') {
|
|
const keys = Object.keys(data);
|
|
let result = `a:${keys.length}:{`;
|
|
keys.forEach(key => {
|
|
result += `s:${key.length}:"${key}";${serializeToPhp(data[key])}`;
|
|
});
|
|
result += '}';
|
|
return result;
|
|
}
|
|
return 'N;';
|
|
}, []);
|
|
|
|
// Generate output formats
|
|
const generateOutputs = useCallback((data) => {
|
|
// For initial empty property, show empty object outputs
|
|
if (!data || Object.keys(data).length === 0 || isInitialEmptyData(data)) {
|
|
setOutputs({
|
|
jsonPretty: '{}',
|
|
jsonMinified: '{}',
|
|
serialized: 'a:0:{}'
|
|
});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const jsonPretty = JSON.stringify(data, null, 2);
|
|
const jsonMinified = JSON.stringify(data);
|
|
|
|
// Simple PHP serialization for basic data types
|
|
const serialized = serializeToPhp(data);
|
|
|
|
setOutputs({
|
|
jsonPretty,
|
|
jsonMinified,
|
|
serialized
|
|
});
|
|
} catch (error) {
|
|
console.error('Error generating outputs:', error);
|
|
setOutputs({
|
|
jsonPretty: 'Error generating JSON',
|
|
jsonMinified: 'Error generating JSON',
|
|
serialized: 'Error generating PHP serialized data'
|
|
});
|
|
}
|
|
}, [serializeToPhp]);
|
|
|
|
// Handle file import
|
|
const handleFileImport = (event) => {
|
|
const file = event.target.files[0];
|
|
if (file) {
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
const content = e.target.result;
|
|
setInputText(content);
|
|
handleInputChange(content);
|
|
setCreateNewCompleted(true);
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
};
|
|
|
|
|
|
// Fetch data from URL
|
|
const handleFetchData = async () => {
|
|
if (!fetchUrl.trim()) {
|
|
setError('Please enter a valid URL');
|
|
return;
|
|
}
|
|
|
|
setFetching(true);
|
|
setError('');
|
|
|
|
try {
|
|
// Add protocol if missing
|
|
let url = fetchUrl.trim();
|
|
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
url = 'https://' + url;
|
|
}
|
|
|
|
const response = await fetch(url);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
const contentType = response.headers.get('content-type');
|
|
if (!contentType || !contentType.includes('application/json')) {
|
|
// Try to parse as JSON anyway, some APIs don't set correct content-type
|
|
const text = await response.text();
|
|
try {
|
|
const data = JSON.parse(text);
|
|
setStructuredData(data);
|
|
generateOutputs(data);
|
|
setInputText(JSON.stringify(data, null, 2));
|
|
setInputFormat('JSON');
|
|
setInputValid(true);
|
|
setCreateNewCompleted(true);
|
|
} catch {
|
|
throw new Error('Response is not valid JSON. Content-Type: ' + (contentType || 'unknown'));
|
|
}
|
|
} else {
|
|
const data = await response.json();
|
|
setStructuredData(data);
|
|
generateOutputs(data);
|
|
setInputText(JSON.stringify(data, null, 2));
|
|
setInputFormat('JSON');
|
|
setInputValid(true);
|
|
setCreateNewCompleted(true);
|
|
}
|
|
} catch (err) {
|
|
console.error('Fetch error:', err);
|
|
if (err.name === 'TypeError' && err.message.includes('fetch')) {
|
|
setError('Network error: Unable to fetch data. Check the URL and try again.');
|
|
} else {
|
|
setError(`Fetch failed: ${err.message}`);
|
|
}
|
|
} finally {
|
|
setFetching(false);
|
|
}
|
|
};
|
|
|
|
// Initialize outputs when component mounts or data changes
|
|
React.useEffect(() => {
|
|
generateOutputs(structuredData);
|
|
}, [structuredData, generateOutputs]);
|
|
|
|
return (
|
|
<ToolLayout
|
|
title="Object Editor"
|
|
description="Visual editor for JSON and PHP serialized objects with format conversion"
|
|
icon={Edit3}
|
|
>
|
|
{/* Input Section with Tabs */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mb-6">
|
|
{/* Tabs */}
|
|
<div className="flex border-b border-gray-200 dark:border-gray-700 overflow-x-auto scrollbar-hide">
|
|
<div className="flex min-w-max">
|
|
<button
|
|
onClick={() => handleTabChange('create')}
|
|
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
|
|
activeTab === 'create'
|
|
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
|
|
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
|
|
}`}
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
Create New
|
|
</button>
|
|
<button
|
|
onClick={() => handleTabChange('url')}
|
|
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
|
|
activeTab === 'url'
|
|
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
|
|
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
|
|
}`}
|
|
>
|
|
<Globe className="h-4 w-4" />
|
|
URL
|
|
</button>
|
|
<button
|
|
onClick={() => handleTabChange('paste')}
|
|
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
|
|
activeTab === 'paste'
|
|
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
|
|
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
|
|
}`}
|
|
>
|
|
<FileText className="h-4 w-4" />
|
|
Paste
|
|
</button>
|
|
<button
|
|
onClick={() => handleTabChange('open')}
|
|
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
|
|
activeTab === 'open'
|
|
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
|
|
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
|
|
}`}
|
|
>
|
|
<Upload className="h-4 w-4" />
|
|
Open
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tab Content */}
|
|
{(activeTab !== 'create' || !createNewCompleted) && (
|
|
<div className="p-4">
|
|
{/* Create New Tab Content */}
|
|
{activeTab === 'create' && !createNewCompleted && (
|
|
<div className="space-y-4">
|
|
<div className="text-center">
|
|
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
|
|
Create New Object
|
|
</h3>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
|
Choose how you'd like to begin working with your data
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<button
|
|
onClick={() => {
|
|
if (hasModifiedData()) {
|
|
setPendingTabChange('create_empty');
|
|
setShowInputChangeModal(true);
|
|
} else {
|
|
clearAllData();
|
|
// Start with one empty property like TableEditor starts with empty row
|
|
setStructuredData({ "": "" });
|
|
setCreateNewCompleted(true);
|
|
}
|
|
}}
|
|
className="flex flex-col items-center p-6 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg hover:border-blue-500 dark:hover:border-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors group"
|
|
>
|
|
<Plus className="h-8 w-8 text-gray-400 group-hover:text-blue-500 dark:group-hover:text-blue-400 mb-2" />
|
|
<span className="font-medium text-gray-900 dark:text-gray-100 group-hover:text-blue-600 dark:group-hover:text-blue-400">
|
|
Start Empty
|
|
</span>
|
|
<span className="text-xs text-gray-500 dark:text-gray-400 text-center mt-1">
|
|
Create a blank object structure
|
|
</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => {
|
|
if (hasModifiedData()) {
|
|
setPendingTabChange('create_sample');
|
|
setShowInputChangeModal(true);
|
|
} else {
|
|
clearAllData();
|
|
// Load sample data
|
|
const sampleData = {
|
|
name: "John Doe",
|
|
age: 30,
|
|
email: "john@example.com",
|
|
address: {
|
|
street: "123 Main St",
|
|
city: "New York",
|
|
country: "USA"
|
|
},
|
|
hobbies: ["reading", "coding", "traveling"]
|
|
};
|
|
setStructuredData(sampleData);
|
|
setCreateNewCompleted(true);
|
|
}
|
|
}}
|
|
className="flex flex-col items-center p-6 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg hover:border-green-500 dark:hover:border-green-400 hover:bg-green-50 dark:hover:bg-green-900/20 transition-colors group"
|
|
>
|
|
<FileText className="h-8 w-8 text-gray-400 group-hover:text-green-500 dark:group-hover:text-green-400 mb-2" />
|
|
<span className="font-medium text-gray-900 dark:text-gray-100 group-hover:text-green-600 dark:group-hover:text-green-400">
|
|
Load Sample
|
|
</span>
|
|
<span className="text-xs text-gray-500 dark:text-gray-400 text-center mt-1">
|
|
Start with example data to explore features
|
|
</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-3">
|
|
<p className="text-xs text-blue-700 dark:text-blue-300">
|
|
💡 <strong>Tip:</strong> You can always import data later using the URL, Paste, or Open tabs, or start editing directly in the visual editor below.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* URL Tab Content */}
|
|
{activeTab === 'url' && (
|
|
<div className="space-y-3">
|
|
<div className="flex gap-2">
|
|
<div className="relative flex-1">
|
|
<input
|
|
type="url"
|
|
value={fetchUrl}
|
|
onChange={(e) => setFetchUrl(e.target.value)}
|
|
placeholder="https://api.telegram.org/bot<token>/getMe"
|
|
className="tool-input w-full"
|
|
onKeyPress={(e) => e.key === 'Enter' && handleFetchData()}
|
|
/>
|
|
</div>
|
|
<button
|
|
onClick={handleFetchData}
|
|
disabled={fetching || !fetchUrl.trim()}
|
|
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-medium px-4 py-2 rounded-md transition-colors flex items-center whitespace-nowrap"
|
|
>
|
|
{fetching ? 'Fetching...' : 'Fetch Data'}
|
|
</button>
|
|
</div>
|
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
Enter any URL that returns JSON data. Examples: Telegram Bot API, JSONPlaceholder, GitHub API, etc.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Paste Tab Content */}
|
|
{activeTab === 'paste' && (
|
|
<div className="space-y-3">
|
|
<div className="flex justify-end items-center">
|
|
{inputFormat && (
|
|
<span className={`text-xs px-2 py-1 rounded ${
|
|
inputValid
|
|
? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300'
|
|
: 'bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-300'
|
|
}`}>
|
|
{inputFormat} {inputValid ? '✓' : '✗'}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<textarea
|
|
value={inputText}
|
|
onChange={(e) => handleInputChange(e.target.value)}
|
|
placeholder="Paste JSON or PHP serialized data here..."
|
|
className="tool-input h-32 resize-none"
|
|
/>
|
|
{error && (
|
|
<p className="text-sm text-red-600 dark:text-red-400">
|
|
{error}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Open Tab Content */}
|
|
{activeTab === 'open' && (
|
|
<div className="space-y-3">
|
|
<input
|
|
ref={fileInputRef}
|
|
type="file"
|
|
accept=".json,.txt"
|
|
onChange={handleFileImport}
|
|
className="tool-input w-full"
|
|
/>
|
|
<div className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
id="useFirstRowAsHeader"
|
|
checked={true}
|
|
readOnly
|
|
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mr-2"
|
|
/>
|
|
<label htmlFor="useFirstRowAsHeader" className="text-sm text-gray-700 dark:text-gray-300">
|
|
Use first row as column headers (for CSV/TSV)
|
|
</label>
|
|
</div>
|
|
<div className="flex items-center gap-2 p-3 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
|
<div className="flex-shrink-0">
|
|
<svg className="h-4 w-4 text-green-600 dark:text-green-400" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
|
</svg>
|
|
</div>
|
|
<p className="text-xs text-green-700 dark:text-green-300">
|
|
<strong>Privacy:</strong> Your data stays in your browser. We don't store or upload anything - just help you open, edit, and export your files locally.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
|
|
|
|
{/* Main Editor Section - Only show if createNewCompleted or not on create tab */}
|
|
{(activeTab !== 'create' || createNewCompleted) && (
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden mb-6">
|
|
{/* Editor Header */}
|
|
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
|
<Edit3 className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
|
Object Editor
|
|
</h3>
|
|
|
|
{/* View Mode Tabs - Moved to right */}
|
|
<div className="flex border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
|
<button
|
|
onClick={() => setViewMode('visual')}
|
|
className={`flex items-center gap-2 px-3 py-2 text-sm font-medium transition-colors ${
|
|
viewMode === 'visual'
|
|
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
|
|
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700'
|
|
}`}
|
|
>
|
|
<Edit3 className="h-4 w-4" />
|
|
<span className="hidden sm:inline">Visual Editor</span>
|
|
</button>
|
|
<button
|
|
onClick={() => setViewMode('mindmap')}
|
|
className={`flex items-center gap-2 px-3 py-2 text-sm font-medium transition-colors border-l border-gray-200 dark:border-gray-700 ${
|
|
viewMode === 'mindmap'
|
|
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
|
|
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700'
|
|
}`}
|
|
>
|
|
<Workflow className="h-4 w-4" />
|
|
<span className="hidden sm:inline">Mindmap View</span>
|
|
</button>
|
|
<button
|
|
onClick={() => setViewMode('table')}
|
|
className={`flex items-center gap-2 px-3 py-2 text-sm font-medium transition-colors border-l border-gray-200 dark:border-gray-700 ${
|
|
viewMode === 'table'
|
|
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
|
|
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700'
|
|
}`}
|
|
>
|
|
<Table className="h-4 w-4" />
|
|
<span className="hidden sm:inline">Table View</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Editor Content */}
|
|
<div>
|
|
{Object.keys(structuredData).length === 0 ? (
|
|
<div className="text-center py-12">
|
|
<Edit3 className="h-12 w-12 text-gray-400 dark:text-gray-500 mx-auto mb-4" />
|
|
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
|
|
No Object Data
|
|
</h3>
|
|
<p className="text-gray-600 dark:text-gray-400">
|
|
Load data using the input methods above to start editing
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<>
|
|
{viewMode === 'visual' && (
|
|
<div className="min-h-96 overflow-x-auto p-4">
|
|
<div className="min-w-max">
|
|
<StructuredEditor
|
|
initialData={structuredData}
|
|
onDataChange={handleStructuredDataChange}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{viewMode === 'mindmap' && (
|
|
<MindmapView data={structuredData} />
|
|
)}
|
|
|
|
{viewMode === 'table' && (
|
|
<PostmanTable
|
|
data={structuredData}
|
|
title="JSON Data Structure"
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Export Section - Only show if createNewCompleted or not on create tab */}
|
|
{(activeTab !== 'create' || createNewCompleted) && Object.keys(structuredData).length > 0 && (
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mt-6">
|
|
{/* Export Header */}
|
|
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
|
<Download className="h-5 w-5 text-green-600 dark:text-green-400" />
|
|
Export Results
|
|
</h3>
|
|
<div className="text-sm text-gray-600 dark:text-gray-400">
|
|
<span>Object: {Object.keys(structuredData).length} properties</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Export Tabs */}
|
|
<div className="flex border-b border-gray-200 dark:border-gray-700">
|
|
<button
|
|
onClick={() => setActiveExportTab('json')}
|
|
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
|
|
activeExportTab === 'json'
|
|
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
|
|
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
|
|
}`}
|
|
>
|
|
<Braces className="h-4 w-4" />
|
|
JSON
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveExportTab('php')}
|
|
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
|
|
activeExportTab === 'php'
|
|
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
|
|
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
|
|
}`}
|
|
>
|
|
<Code className="h-4 w-4" />
|
|
PHP Serialize
|
|
</button>
|
|
</div>
|
|
|
|
{/* Export Content */}
|
|
<div className="p-4">
|
|
{activeExportTab === 'json' && (
|
|
<div className="space-y-3">
|
|
<textarea
|
|
value={jsonFormat === 'pretty' ? (outputs.jsonPretty || '{}') : (outputs.jsonMinified || '{}')}
|
|
readOnly
|
|
className="w-full h-64 font-mono text-sm border border-gray-300 dark:border-gray-600 rounded-md p-3 bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
|
/>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={() => setJsonFormat('pretty')}
|
|
className={`px-3 py-1 text-sm rounded ${
|
|
jsonFormat === 'pretty'
|
|
? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300'
|
|
: 'bg-gray-100 text-gray-600 dark:bg-gray-600 dark:text-gray-300'
|
|
}`}
|
|
>
|
|
Pretty
|
|
</button>
|
|
<button
|
|
onClick={() => setJsonFormat('minify')}
|
|
className={`px-3 py-1 text-sm rounded ${
|
|
jsonFormat === 'minify'
|
|
? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300'
|
|
: 'bg-gray-100 text-gray-600 dark:bg-gray-600 dark:text-gray-300'
|
|
}`}
|
|
>
|
|
Minify
|
|
</button>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => {
|
|
const content = jsonFormat === 'pretty' ? (outputs.jsonPretty || '{}') : (outputs.jsonMinified || '{}');
|
|
navigator.clipboard.writeText(content);
|
|
}}
|
|
className="px-3 py-1 text-sm bg-gray-100 hover:bg-gray-200 dark:bg-gray-600 dark:hover:bg-gray-500 text-gray-700 dark:text-gray-200 rounded transition-colors"
|
|
>
|
|
Copy
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
const content = jsonFormat === 'pretty' ? (outputs.jsonPretty || '{}') : (outputs.jsonMinified || '{}');
|
|
const blob = new Blob([content], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'object-data.json';
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}}
|
|
className="px-3 py-1 text-sm bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white rounded transition-colors"
|
|
>
|
|
Download
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeExportTab === 'php' && (
|
|
<div className="space-y-3">
|
|
<textarea
|
|
value={outputs.serialized || 'a:0:{}'}
|
|
readOnly
|
|
className="w-full h-64 font-mono text-sm border border-gray-300 dark:border-gray-600 rounded-md p-3 bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
|
/>
|
|
<div className="flex justify-end gap-2">
|
|
<button
|
|
onClick={() => {
|
|
const content = outputs.serialized || 'a:0:{}';
|
|
navigator.clipboard.writeText(content);
|
|
}}
|
|
className="px-3 py-1 text-sm bg-gray-100 hover:bg-gray-200 dark:bg-gray-600 dark:hover:bg-gray-500 text-gray-700 dark:text-gray-200 rounded transition-colors"
|
|
>
|
|
Copy
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
const content = outputs.serialized || 'a:0:{}';
|
|
const blob = new Blob([content], { type: 'text/plain' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'object-data.txt';
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}}
|
|
className="px-3 py-1 text-sm bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white rounded transition-colors"
|
|
>
|
|
Download
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Input Method Change Confirmation Modal */}
|
|
{showInputChangeModal && (
|
|
<InputChangeConfirmationModal
|
|
objectData={structuredData}
|
|
currentMethod={activeTab}
|
|
newMethod={pendingTabChange}
|
|
onConfirm={confirmInputChange}
|
|
onCancel={() => {
|
|
setShowInputChangeModal(false);
|
|
setPendingTabChange(null);
|
|
// If user cancels while on Create New tab with modified data, hide the tab content
|
|
if (activeTab === 'create' && hasModifiedData()) {
|
|
setCreateNewCompleted(true);
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{/* Usage Tips */}
|
|
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md p-4 mt-6">
|
|
<h4 className="text-blue-800 dark:text-blue-200 font-medium mb-3">Usage Tips</h4>
|
|
<div className="text-blue-700 dark:text-blue-300 text-sm space-y-2">
|
|
<div>
|
|
<p className="font-medium mb-1">📝 Input Methods:</p>
|
|
<ul className="list-disc list-inside space-y-1 ml-2">
|
|
<li><strong>Create New:</strong> Start empty or load sample data to explore features</li>
|
|
<li><strong>URL Import:</strong> Fetch data directly from JSON APIs and endpoints</li>
|
|
<li><strong>Paste Data:</strong> Auto-detects JSON and PHP serialized formats</li>
|
|
<li><strong>Open Files:</strong> Import .json and .txt files (multi-format supported)</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div>
|
|
<p className="font-medium mb-1">🎯 Editing Modes:</p>
|
|
<ul className="list-disc list-inside space-y-1 ml-2">
|
|
<li><strong>Visual Editor:</strong> Create and modify object structures with forms</li>
|
|
<li><strong>Mindmap View:</strong> Visualize complex JSON structures as interactive diagrams</li>
|
|
<li><strong>Table View:</strong> Browse data like Postman - click arrays for horizontal tables, objects for key-value pairs</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div>
|
|
<p className="font-medium mb-1">🚀 Navigation & Features:</p>
|
|
<ul className="list-disc list-inside space-y-1 ml-2">
|
|
<li><strong>Navigation:</strong> Use breadcrumbs and Back button to navigate through nested data structures</li>
|
|
<li><strong>Add/Delete:</strong> Use buttons to add new properties or select multiple to delete</li>
|
|
<li><strong>Search & Sort:</strong> Filter data and sort by any column</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div>
|
|
<p className="font-medium mb-1">📤 Export Options:</p>
|
|
<ul className="list-disc list-inside space-y-1 ml-2">
|
|
<li><strong>JSON:</strong> Export includes pretty-formatted and minified versions</li>
|
|
<li><strong>PHP Serialize:</strong> Perfect for PHP applications and data exchange</li>
|
|
<li><strong>Copy & Download:</strong> All formats support both clipboard copy and file download</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</ToolLayout>
|
|
);
|
|
};
|
|
|
|
// Input Method Change Confirmation Modal Component
|
|
const InputChangeConfirmationModal = ({ objectData, currentMethod, newMethod, onConfirm, onCancel }) => {
|
|
const getMethodName = (method) => {
|
|
switch (method) {
|
|
case 'create': return 'Create New';
|
|
case 'create_empty': return 'Start Empty';
|
|
case 'create_sample': return 'Load Sample';
|
|
case 'url': return 'URL Import';
|
|
case 'paste': return 'Paste Data';
|
|
case 'open': return 'File Upload';
|
|
default: return method;
|
|
}
|
|
};
|
|
|
|
const objectSize = Object.keys(objectData).length;
|
|
const hasNestedData = Object.values(objectData).some(value =>
|
|
typeof value === 'object' && value !== null
|
|
);
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full overflow-hidden">
|
|
{/* Header */}
|
|
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700 bg-amber-50 dark:bg-amber-900/20">
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex-shrink-0">
|
|
<AlertTriangle className="h-6 w-6 text-amber-600 dark:text-amber-400" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-amber-900 dark:text-amber-100">
|
|
Change Input Method
|
|
</h3>
|
|
<p className="text-sm text-amber-700 dark:text-amber-300">
|
|
This will clear all current data
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="px-6 py-4">
|
|
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
|
{(newMethod === 'create_empty' || newMethod === 'create_sample') ? (
|
|
<>Using <strong>{getMethodName(newMethod)}</strong> will clear all current data.</>
|
|
) : (
|
|
<>Switching from <strong>{getMethodName(currentMethod)}</strong> to <strong>{getMethodName(newMethod)}</strong> will clear all current data.</>
|
|
)}
|
|
</p>
|
|
|
|
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-4 mb-4">
|
|
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">
|
|
This will permanently delete:
|
|
</h4>
|
|
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
|
|
<li>• Object with {objectSize} properties</li>
|
|
{hasNestedData && <li>• All nested objects and arrays</li>}
|
|
<li>• All modifications and edits</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-3">
|
|
<div className="flex items-start gap-2">
|
|
<AlertTriangle className="h-4 w-4 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
|
<p className="text-xs text-blue-700 dark:text-blue-300">
|
|
<strong>Tip:</strong> Consider exporting your current data before switching methods to avoid losing your work.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700">
|
|
<div className="flex justify-end gap-3">
|
|
<button
|
|
onClick={onCancel}
|
|
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-md hover:bg-gray-50 dark:hover:bg-gray-500 transition-colors"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
onClick={onConfirm}
|
|
className="px-4 py-2 text-sm font-medium text-white bg-amber-600 hover:bg-amber-700 rounded-md transition-colors flex items-center gap-2"
|
|
>
|
|
<BrushCleaning className="h-4 w-4" />
|
|
Switch & Clear Data
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ObjectEditor;
|