Files
dewedev/src/pages/ObjectEditor.js
dwindown 5f6aa2210a Fix ESLint errors for successful deployment
- 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.
2025-09-23 14:46:30 +07:00

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;