feat: Object Editor Preview Mode & Mobile Optimizations
Major Enhancements: - Added Preview/Edit mode toggle to StructuredEditor component * Preview mode: Read-only view with full text visibility * Edit mode: Full editing capabilities with all controls * Toggle positioned below title, responsive on mobile * Works in both main ObjectEditor view and nested modals - Clickable nested data in Preview mode * JSON/serialized values are blue and clickable * Opens modal directly without switching to Edit mode * Hover effects and tooltips for better UX * No longer need edit mode just to explore structure Mobile Responsiveness Improvements: - Fixed all data load notices in ObjectEditor (URL, Paste, Open tabs) - Fixed all data load notices in TableEditor (URL, Paste, Open tabs) - Notices now stack vertically on mobile with proper spacing - Added break-words for long text, whitespace-nowrap for buttons - Dark mode colors added for better visibility Table Editor Fixes: - Fixed sticky header showing row underneath (top-[-1px]) - Made Export section header mobile responsive - Updated object modal footer layout: * Format info and properties combined on single line * Buttons moved to separate row below * Changed 'Apply Changes' to 'Save Changes' for consistency StructuredEditor Improvements: - Moved overflow-x handling from ObjectEditor to StructuredEditor - Now works consistently in main view and nested modals - Long strings scroll horizontally everywhere - 'Add Property' button hidden in Preview mode - Improved chevron colors for dark mode visibility Technical Changes: - StructuredEditor now manages its own editMode state - readOnly prop can still be passed from parent if needed - Proper conditional rendering for all UI elements - Consistent mobile-first responsive design patterns
This commit is contained in:
15
package-lock.json
generated
15
package-lock.json
generated
@@ -14,6 +14,7 @@
|
|||||||
"@codemirror/lang-html": "^6.4.9",
|
"@codemirror/lang-html": "^6.4.9",
|
||||||
"@codemirror/lang-javascript": "^6.2.4",
|
"@codemirror/lang-javascript": "^6.2.4",
|
||||||
"@codemirror/lang-json": "^6.0.2",
|
"@codemirror/lang-json": "^6.0.2",
|
||||||
|
"@codemirror/lang-sql": "^6.10.0",
|
||||||
"@codemirror/search": "^6.5.11",
|
"@codemirror/search": "^6.5.11",
|
||||||
"@codemirror/state": "^6.5.2",
|
"@codemirror/state": "^6.5.2",
|
||||||
"@codemirror/theme-one-dark": "^6.1.3",
|
"@codemirror/theme-one-dark": "^6.1.3",
|
||||||
@@ -2174,6 +2175,20 @@
|
|||||||
"@lezer/json": "^1.0.0"
|
"@lezer/json": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@codemirror/lang-sql": {
|
||||||
|
"version": "6.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.10.0.tgz",
|
||||||
|
"integrity": "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.0.0",
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@lezer/common": "^1.2.0",
|
||||||
|
"@lezer/highlight": "^1.0.0",
|
||||||
|
"@lezer/lr": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@codemirror/language": {
|
"node_modules/@codemirror/language": {
|
||||||
"version": "6.11.3",
|
"version": "6.11.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"@codemirror/lang-html": "^6.4.9",
|
"@codemirror/lang-html": "^6.4.9",
|
||||||
"@codemirror/lang-javascript": "^6.2.4",
|
"@codemirror/lang-javascript": "^6.2.4",
|
||||||
"@codemirror/lang-json": "^6.0.2",
|
"@codemirror/lang-json": "^6.0.2",
|
||||||
|
"@codemirror/lang-sql": "^6.10.0",
|
||||||
"@codemirror/search": "^6.5.11",
|
"@codemirror/search": "^6.5.11",
|
||||||
"@codemirror/state": "^6.5.2",
|
"@codemirror/state": "^6.5.2",
|
||||||
"@codemirror/theme-one-dark": "^6.1.3",
|
"@codemirror/theme-one-dark": "^6.1.3",
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
{
|
{
|
||||||
"changelog": [
|
"changelog": [
|
||||||
|
{
|
||||||
|
"date": "2025-10-15",
|
||||||
|
"changes": [
|
||||||
|
{
|
||||||
|
"datetime": "2025-10-15T22:32:00+07:00",
|
||||||
|
"type": "enhancement",
|
||||||
|
"title": "Object Editor Preview Mode & Mobile Optimizations",
|
||||||
|
"description": "Added Preview/Edit mode toggle to Object Editor's Tree View with read-only preview mode for better data visibility. Implemented clickable nested data values in preview mode - no need to switch to edit mode to explore JSON/serialized structures. Fixed mobile responsiveness for all data load notices across Object and Table editors. Improved Table Editor sticky header positioning and export section layout on mobile. Moved horizontal overflow handling from ObjectEditor to StructuredEditor component for consistent behavior in main view and nested modals. Updated TableEditor object modal footer layout and changed 'Apply Changes' to 'Save Changes' for UX consistency."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"date": "2025-10-14",
|
"date": "2025-10-14",
|
||||||
"changes": [
|
"changes": [
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { useRef, useEffect, useState } from 'react';
|
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
||||||
import { EditorView } from '@codemirror/view';
|
import { EditorView } from '@codemirror/view';
|
||||||
import { EditorState } from '@codemirror/state';
|
import { EditorState } from '@codemirror/state';
|
||||||
import { basicSetup } from 'codemirror';
|
import { basicSetup } from 'codemirror';
|
||||||
import { json } from '@codemirror/lang-json';
|
import { json } from '@codemirror/lang-json';
|
||||||
|
import { sql } from '@codemirror/lang-sql';
|
||||||
import { oneDark } from '@codemirror/theme-one-dark';
|
import { oneDark } from '@codemirror/theme-one-dark';
|
||||||
import { Maximize2, Minimize2 } from 'lucide-react';
|
import { Maximize2, Minimize2 } from 'lucide-react';
|
||||||
|
|
||||||
@@ -13,7 +14,8 @@ const CodeMirrorEditor = ({
|
|||||||
className = '',
|
className = '',
|
||||||
language = 'json',
|
language = 'json',
|
||||||
maxLines = 12,
|
maxLines = 12,
|
||||||
showToggle = true
|
showToggle = true,
|
||||||
|
cardRef = null // Reference to the card header for scroll target
|
||||||
}) => {
|
}) => {
|
||||||
const editorRef = useRef(null);
|
const editorRef = useRef(null);
|
||||||
const viewRef = useRef(null);
|
const viewRef = useRef(null);
|
||||||
@@ -38,13 +40,26 @@ const CodeMirrorEditor = ({
|
|||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Detect if content is single-line (minified)
|
||||||
|
const isSingleLine = (value || '').split('\n').length === 1;
|
||||||
|
|
||||||
// Initialize editor only once
|
// Initialize editor only once
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editorRef.current || viewRef.current) return;
|
if (!editorRef.current || viewRef.current) return;
|
||||||
|
|
||||||
|
// Language extension
|
||||||
|
let langExtension = [];
|
||||||
|
if (language === 'json') {
|
||||||
|
langExtension = [json()];
|
||||||
|
} else if (language === 'sql') {
|
||||||
|
langExtension = [sql()];
|
||||||
|
}
|
||||||
|
|
||||||
const extensions = [
|
const extensions = [
|
||||||
basicSetup,
|
basicSetup,
|
||||||
language === 'json' ? json() : [],
|
...langExtension,
|
||||||
|
// Enable line wrapping for single-line content
|
||||||
|
...(isSingleLine ? [EditorView.lineWrapping] : []),
|
||||||
EditorView.theme({
|
EditorView.theme({
|
||||||
'&': {
|
'&': {
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
@@ -58,6 +73,13 @@ const CodeMirrorEditor = ({
|
|||||||
},
|
},
|
||||||
'.cm-editor': {
|
'.cm-editor': {
|
||||||
borderRadius: '6px',
|
borderRadius: '6px',
|
||||||
|
},
|
||||||
|
'.cm-scroller': {
|
||||||
|
overflowY: 'auto',
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
'.cm-line': {
|
||||||
|
wordBreak: isSingleLine ? 'break-word' : 'normal',
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
EditorView.updateListener.of((update) => {
|
EditorView.updateListener.of((update) => {
|
||||||
@@ -80,6 +102,25 @@ const CodeMirrorEditor = ({
|
|||||||
|
|
||||||
viewRef.current = view;
|
viewRef.current = view;
|
||||||
|
|
||||||
|
// Apply styles immediately after editor creation
|
||||||
|
setTimeout(() => {
|
||||||
|
const editorElement = editorRef.current?.querySelector('.cm-editor');
|
||||||
|
const scrollerElement = editorRef.current?.querySelector('.cm-scroller');
|
||||||
|
|
||||||
|
if (editorElement) {
|
||||||
|
editorElement.style.height = '350px';
|
||||||
|
editorElement.style.maxHeight = '350px';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scrollerElement) {
|
||||||
|
scrollerElement.style.overflowY = 'auto';
|
||||||
|
scrollerElement.style.overflowX = isSingleLine ? 'hidden' : 'auto';
|
||||||
|
scrollerElement.style.height = '100%';
|
||||||
|
}
|
||||||
|
|
||||||
|
// No manual wrapping needed - EditorView.lineWrapping handles it
|
||||||
|
}, 0);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (viewRef.current) {
|
if (viewRef.current) {
|
||||||
viewRef.current.destroy();
|
viewRef.current.destroy();
|
||||||
@@ -87,13 +128,15 @@ const CodeMirrorEditor = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [isDark]); // Only recreate on theme change
|
}, [isDark, isSingleLine]); // Recreate when theme or line count changes
|
||||||
|
|
||||||
// Handle height changes without recreating editor
|
// Apply overflow and height styles
|
||||||
useEffect(() => {
|
const applyEditorStyles = useCallback(() => {
|
||||||
if (!viewRef.current) return;
|
if (!viewRef.current || !editorRef.current) return;
|
||||||
|
|
||||||
|
const editorElement = editorRef.current.querySelector('.cm-editor');
|
||||||
|
const scrollerElement = editorRef.current.querySelector('.cm-scroller');
|
||||||
|
|
||||||
const editorElement = editorRef.current?.querySelector('.cm-editor');
|
|
||||||
if (editorElement) {
|
if (editorElement) {
|
||||||
if (isExpanded) {
|
if (isExpanded) {
|
||||||
editorElement.style.height = 'auto';
|
editorElement.style.height = 'auto';
|
||||||
@@ -103,8 +146,19 @@ const CodeMirrorEditor = ({
|
|||||||
editorElement.style.maxHeight = '350px';
|
editorElement.style.maxHeight = '350px';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle scrolling
|
||||||
|
if (scrollerElement) {
|
||||||
|
scrollerElement.style.overflowY = isExpanded ? 'visible' : 'auto';
|
||||||
|
scrollerElement.style.height = '100%';
|
||||||
|
}
|
||||||
}, [isExpanded]);
|
}, [isExpanded]);
|
||||||
|
|
||||||
|
// Apply styles on mount, expand/collapse, and content changes
|
||||||
|
useEffect(() => {
|
||||||
|
applyEditorStyles();
|
||||||
|
}, [applyEditorStyles]);
|
||||||
|
|
||||||
// Update content when value changes externally
|
// Update content when value changes externally
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (viewRef.current && value !== viewRef.current.state.doc.toString()) {
|
if (viewRef.current && value !== viewRef.current.state.doc.toString()) {
|
||||||
@@ -116,8 +170,11 @@ const CodeMirrorEditor = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
viewRef.current.dispatch(transaction);
|
viewRef.current.dispatch(transaction);
|
||||||
|
|
||||||
|
// Re-apply styles after content change (e.g., tab switch)
|
||||||
|
setTimeout(() => applyEditorStyles(), 10);
|
||||||
}
|
}
|
||||||
}, [value]);
|
}, [value, applyEditorStyles]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`relative ${className}`}>
|
<div className={`relative ${className}`}>
|
||||||
@@ -132,7 +189,12 @@ const CodeMirrorEditor = ({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsExpanded(!isExpanded);
|
setIsExpanded(!isExpanded);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
// Scroll to card header if cardRef is provided, otherwise scroll to top
|
||||||
|
if (cardRef?.current) {
|
||||||
|
cardRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
} else {
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
}, 50);
|
}, 50);
|
||||||
}}
|
}}
|
||||||
className="absolute bottom-2 right-2 p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 bg-white dark:bg-gray-800 rounded border border-gray-300 dark:border-gray-600 shadow-sm z-10"
|
className="absolute bottom-2 right-2 p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 bg-white dark:bg-gray-800 rounded border border-gray-300 dark:border-gray-600 shadow-sm z-10"
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { Plus, Minus, ChevronDown, ChevronRight, Type, Hash, ToggleLeft, List, Braces, Edit3, X } from 'lucide-react';
|
import { Plus, Minus, ChevronDown, ChevronRight, Type, Hash, ToggleLeft, List, Braces, Edit3, X, Eye, Pencil } from 'lucide-react';
|
||||||
|
|
||||||
const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyProp = false }) => {
|
||||||
const [data, setData] = useState(initialData);
|
const [data, setData] = useState(initialData);
|
||||||
const [expandedNodes, setExpandedNodes] = useState(new Set(['root']));
|
const [expandedNodes, setExpandedNodes] = useState(new Set(['root']));
|
||||||
const [fieldTypes, setFieldTypes] = useState({}); // Track intended types for fields
|
const [fieldTypes, setFieldTypes] = useState({}); // Track intended types for fields
|
||||||
const isInternalUpdate = useRef(false);
|
const isInternalUpdate = useRef(false);
|
||||||
const [nestedEditModal, setNestedEditModal] = useState(null); // { path, value, type: 'json' | 'serialized' }
|
const [nestedEditModal, setNestedEditModal] = useState(null); // { path, value, type: 'json' | 'serialized' }
|
||||||
const [nestedData, setNestedData] = useState(null);
|
const [nestedData, setNestedData] = useState(null);
|
||||||
|
const [editMode, setEditMode] = useState(false); // Internal edit mode state
|
||||||
|
|
||||||
|
// Use internal editMode if readOnlyProp is not explicitly set, otherwise use prop
|
||||||
|
const readOnly = readOnlyProp !== false ? readOnlyProp : !editMode;
|
||||||
|
|
||||||
// Update internal data when initialData prop changes (but not from internal updates)
|
// Update internal data when initialData prop changes (but not from internal updates)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -533,7 +537,7 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
|||||||
{canExpand && (
|
{canExpand && (
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleNode(path)}
|
onClick={() => toggleNode(path)}
|
||||||
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
|
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
{isExpanded ? (
|
{isExpanded ? (
|
||||||
<ChevronDown className="h-4 w-4" />
|
<ChevronDown className="h-4 w-4" />
|
||||||
@@ -558,74 +562,106 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
|||||||
// Object properties: icon + editable key + colon (compact)
|
// Object properties: icon + editable key + colon (compact)
|
||||||
<>
|
<>
|
||||||
{getTypeIcon(value)}
|
{getTypeIcon(value)}
|
||||||
<input
|
{readOnly ? (
|
||||||
type="text"
|
<span className="px-2 py-1 text-sm text-gray-900 dark:text-gray-100 font-mono">
|
||||||
defaultValue={key}
|
{key}
|
||||||
onBlur={(e) => {
|
</span>
|
||||||
const newKey = e.target.value.trim();
|
) : (
|
||||||
if (newKey && newKey !== key) {
|
<input
|
||||||
renameKey(key, newKey, path);
|
type="text"
|
||||||
}
|
defaultValue={key}
|
||||||
}}
|
onBlur={(e) => {
|
||||||
onKeyDown={(e) => {
|
const newKey = e.target.value.trim();
|
||||||
if (e.key === 'Enter') {
|
if (newKey && newKey !== key) {
|
||||||
e.target.blur(); // Trigger blur to save changes
|
renameKey(key, newKey, path);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="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 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 min-w-0"
|
onKeyDown={(e) => {
|
||||||
placeholder="Property name"
|
if (e.key === 'Enter') {
|
||||||
style={{width: '120px'}} // Fixed width for consistency
|
e.target.blur(); // Trigger blur to save changes
|
||||||
/>
|
}
|
||||||
<span className="text-gray-500 hidden sm:inline">:</span>
|
}}
|
||||||
|
className="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 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 min-w-0"
|
||||||
|
placeholder="Property name"
|
||||||
|
style={{width: '120px'}} // Fixed width for consistency
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span className="text-gray-500 inline">:</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!canExpand ? (
|
{!canExpand ? (
|
||||||
typeof value === 'boolean' ? (
|
typeof value === 'boolean' ? (
|
||||||
<div className="flex-1 flex items-center space-x-2">
|
<div className="flex-1 flex items-center space-x-2">
|
||||||
<button
|
{readOnly ? (
|
||||||
onClick={() => updateValue((!value).toString(), path)}
|
<span className="text-sm text-gray-900 dark:text-gray-100 font-mono">
|
||||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 ${
|
{value.toString()}
|
||||||
value ? 'bg-blue-600' : 'bg-gray-200 dark:bg-gray-600'
|
</span>
|
||||||
}`}
|
) : (
|
||||||
>
|
<>
|
||||||
<span
|
<button
|
||||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
onClick={() => updateValue((!value).toString(), path)}
|
||||||
value ? 'translate-x-6' : 'translate-x-1'
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 ${
|
||||||
}`}
|
value ? 'bg-blue-600' : 'bg-gray-200 dark:bg-gray-600'
|
||||||
/>
|
}`}
|
||||||
</button>
|
>
|
||||||
<span className="text-sm text-gray-600 dark:text-gray-400 font-mono">
|
<span
|
||||||
{value.toString()}
|
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||||
</span>
|
value ? 'translate-x-6' : 'translate-x-1'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<span className="text-sm text-gray-600 dark:text-gray-400 font-mono">
|
||||||
|
{value.toString()}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex-1 flex items-center gap-2">
|
<div className="flex-1 flex items-center gap-2">
|
||||||
{(fieldTypes[path] === 'longtext' || (typeof value === 'string' && value.includes('\n'))) ? (
|
{readOnly ? (
|
||||||
<textarea
|
typeof value === 'string' && detectNestedData(value) ? (
|
||||||
value={getDisplayValue(value)}
|
<span
|
||||||
onChange={(e) => updateValue(e.target.value, path)}
|
onClick={() => openNestedEditor(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 items"
|
className="px-2 py-1 text-sm text-blue-600 dark:text-blue-400 font-mono break-all cursor-pointer hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded transition-colors"
|
||||||
placeholder="Long text value"
|
title={`Click to view nested ${detectNestedData(value).type} data`}
|
||||||
rows={3}
|
>
|
||||||
/>
|
{getDisplayValue(value)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="px-2 py-1 text-sm text-gray-900 dark:text-gray-100 font-mono break-all">
|
||||||
|
{getDisplayValue(value)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<input
|
<>
|
||||||
type="text"
|
{(fieldTypes[path] === 'longtext' || (typeof value === 'string' && value.includes('\n'))) ? (
|
||||||
value={getDisplayValue(value)}
|
<textarea
|
||||||
onChange={(e) => updateValue(e.target.value, path)}
|
value={getDisplayValue(value)}
|
||||||
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"
|
onChange={(e) => updateValue(e.target.value, path)}
|
||||||
placeholder="Value"
|
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 items"
|
||||||
/>
|
placeholder="Long text value"
|
||||||
)}
|
rows={3}
|
||||||
{typeof value === 'string' && detectNestedData(value) && (
|
/>
|
||||||
<button
|
) : (
|
||||||
onClick={() => openNestedEditor(value, path)}
|
<input
|
||||||
className="p-1 text-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900 rounded flex-shrink-0"
|
type="text"
|
||||||
title={`Edit nested ${detectNestedData(value).type} data`}
|
value={getDisplayValue(value)}
|
||||||
>
|
onChange={(e) => updateValue(e.target.value, path)}
|
||||||
<Edit3 className="h-4 w-4" />
|
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"
|
||||||
</button>
|
placeholder="Value"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{typeof value === 'string' && detectNestedData(value) && (
|
||||||
|
<button
|
||||||
|
onClick={() => openNestedEditor(value, path)}
|
||||||
|
className="p-1 text-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900 rounded flex-shrink-0"
|
||||||
|
title={`Edit nested ${detectNestedData(value).type} data`}
|
||||||
|
>
|
||||||
|
<Edit3 className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -635,38 +671,40 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center space-x-2 sm:space-x-2">
|
{!readOnly && (
|
||||||
<select
|
<div className="flex items-center space-x-2 sm:space-x-2">
|
||||||
value={
|
<select
|
||||||
fieldTypes[path] || (
|
value={
|
||||||
value === null ? 'null' :
|
fieldTypes[path] || (
|
||||||
value === undefined ? 'string' :
|
value === null ? 'null' :
|
||||||
typeof value === 'string' ? (value.includes('\n') ? 'longtext' : 'string') :
|
value === undefined ? 'string' :
|
||||||
typeof value === 'number' ? 'number' :
|
typeof value === 'string' ? (value.includes('\n') ? 'longtext' : 'string') :
|
||||||
typeof value === 'boolean' ? 'boolean' :
|
typeof value === 'number' ? 'number' :
|
||||||
Array.isArray(value) ? 'array' : 'object'
|
typeof value === 'boolean' ? 'boolean' :
|
||||||
)
|
Array.isArray(value) ? 'array' : 'object'
|
||||||
}
|
)
|
||||||
onChange={(e) => changeType(e.target.value, path)}
|
}
|
||||||
className="px-2 py-1 text-xs 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"
|
onChange={(e) => changeType(e.target.value, path)}
|
||||||
>
|
className="px-2 py-1 text-xs 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"
|
||||||
<option value="string">String</option>
|
>
|
||||||
<option value="longtext">Long Text</option>
|
<option value="string">String</option>
|
||||||
<option value="number">Number</option>
|
<option value="longtext">Long Text</option>
|
||||||
<option value="boolean">Boolean</option>
|
<option value="number">Number</option>
|
||||||
<option value="array">Array</option>
|
<option value="boolean">Boolean</option>
|
||||||
<option value="object">Object</option>
|
<option value="array">Array</option>
|
||||||
<option value="null">Null</option>
|
<option value="object">Object</option>
|
||||||
</select>
|
<option value="null">Null</option>
|
||||||
|
</select>
|
||||||
<button
|
|
||||||
onClick={() => removeProperty(key, parentPath)}
|
<button
|
||||||
className="p-1 text-red-600 hover:bg-red-100 dark:hover:bg-red-900 rounded flex-shrink-0"
|
onClick={() => removeProperty(key, parentPath)}
|
||||||
title="Remove property"
|
className="p-1 text-red-600 hover:bg-red-100 dark:hover:bg-red-900 rounded flex-shrink-0"
|
||||||
>
|
title="Remove property"
|
||||||
<Minus className="h-4 w-4" />
|
>
|
||||||
</button>
|
<Minus className="h-4 w-4" />
|
||||||
</div>
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -677,26 +715,30 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
|||||||
{value.map((item, index) =>
|
{value.map((item, index) =>
|
||||||
renderValue(item, index.toString(), `${path}.${index}`, path)
|
renderValue(item, index.toString(), `${path}.${index}`, path)
|
||||||
)}
|
)}
|
||||||
<button
|
{!readOnly && (
|
||||||
onClick={() => addArrayItem(value, path)}
|
<button
|
||||||
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"
|
onClick={() => addArrayItem(value, path)}
|
||||||
>
|
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 Item</span>
|
<Plus className="h-4 w-4" />
|
||||||
</button>
|
<span>Add Item</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{Object.entries(value).map(([k, v]) =>
|
{Object.entries(value).map(([k, v]) =>
|
||||||
renderValue(v, k, `${path}.${k}`, path)
|
renderValue(v, k, `${path}.${k}`, path)
|
||||||
)}
|
)}
|
||||||
<button
|
{!readOnly && (
|
||||||
onClick={() => addProperty(value, path)}
|
<button
|
||||||
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"
|
onClick={() => addProperty(value, path)}
|
||||||
>
|
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>
|
<Plus className="h-4 w-4" />
|
||||||
</button>
|
<span>Add Property</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -708,29 +750,65 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-96 w-full">
|
<div className="min-h-96 w-full">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">Structured Data Editor</h3>
|
<div className="flex flex-col gap-3 mb-3">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">Structured Data Editor</h3>
|
||||||
|
|
||||||
|
{/* Mode Toggle - Below title on mobile, inline on desktop */}
|
||||||
|
{readOnlyProp === false && (
|
||||||
|
<div className="flex border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden bg-white dark:bg-gray-800 shadow-sm w-fit">
|
||||||
|
<button
|
||||||
|
onClick={() => setEditMode(false)}
|
||||||
|
className={`flex items-center gap-2 px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||||
|
!editMode
|
||||||
|
? 'bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-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'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Eye className="h-3.5 w-3.5" />
|
||||||
|
<span>Preview</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setEditMode(true)}
|
||||||
|
className={`flex items-center gap-2 px-3 py-1.5 text-xs font-medium transition-colors border-l border-gray-200 dark:border-gray-700 ${
|
||||||
|
editMode
|
||||||
|
? 'bg-orange-50 dark:bg-orange-900/20 text-orange-700 dark:text-orange-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'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Pencil className="h-3.5 w-3.5" />
|
||||||
|
<span>Edit</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-x-hidden">
|
<div className="w-full overflow-hidden">
|
||||||
{Object.keys(data).length === 0 ? (
|
<div className="w-full overflow-x-auto">
|
||||||
<div className="text-center text-gray-500 dark:text-gray-400 py-8">
|
<div className="min-w-max">
|
||||||
<Braces className="h-12 w-12 mx-auto mb-2 opacity-50" />
|
{Object.keys(data).length === 0 ? (
|
||||||
<p>No properties yet. Click "Add Property" to start building your data structure.</p>
|
<div className="text-center text-gray-500 dark:text-gray-400 py-8">
|
||||||
|
<Braces className="h-12 w-12 mx-auto mb-2 opacity-50" />
|
||||||
|
<p>No properties yet. Click "Add Property" to start building your data structure.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
Object.entries(data).map(([key, value]) =>
|
||||||
|
renderValue(value, key, `root.${key}`, 'root')
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Root level Add Property button */}
|
||||||
|
{!readOnly && (
|
||||||
|
<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>
|
||||||
) : (
|
</div>
|
||||||
Object.entries(data).map(([key, value]) =>
|
|
||||||
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>
|
||||||
|
|
||||||
{/* Nested Data Editor Modal */}
|
{/* Nested Data Editor Modal */}
|
||||||
@@ -749,7 +827,7 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={closeNestedEditor}
|
onClick={closeNestedEditor}
|
||||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
|
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded self-start"
|
||||||
>
|
>
|
||||||
<X className="h-5 w-5" />
|
<X className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -50,9 +50,9 @@ const Home = () => {
|
|||||||
<span className="animate-pulse">_</span>
|
<span className="animate-pulse">_</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="text-5xl hidden md:text-7xl font-bold bg-gradient-to-r from-blue-600 via-purple-600 to-indigo-600 bg-clip-text text-transparent mb-6">
|
<h1 className="text-5xl md:text-7xl font-bold bg-gradient-to-r from-blue-600 via-purple-600 to-indigo-600 bg-clip-text text-transparent mb-6">
|
||||||
{SITE_CONFIG.title}
|
{SITE_CONFIG.title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className="text-xl md:text-2xl text-slate-600 dark:text-slate-300 mb-4 max-w-3xl mx-auto leading-relaxed">
|
<p className="text-xl md:text-2xl text-slate-600 dark:text-slate-300 mb-4 max-w-3xl mx-auto leading-relaxed">
|
||||||
{SITE_CONFIG.subtitle}
|
{SITE_CONFIG.subtitle}
|
||||||
@@ -123,101 +123,102 @@ const Home = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tools Grid */}
|
|
||||||
<div className={`transition-all duration-1000 delay-300 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'}`}>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 mb-20">
|
|
||||||
{filteredTools.map((tool, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={`transition-all duration-500 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'}`}
|
|
||||||
style={{ transitionDelay: `${400 + index * 100}ms` }}
|
|
||||||
>
|
|
||||||
<ToolCard
|
|
||||||
icon={tool.icon}
|
|
||||||
title={tool.name}
|
|
||||||
description={tool.description}
|
|
||||||
path={tool.path}
|
|
||||||
tags={tool.tags}
|
|
||||||
category={tool.category}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* No Results */}
|
|
||||||
{filteredTools.length === 0 && (
|
|
||||||
<div className="text-center py-20">
|
|
||||||
<div className="text-6xl mb-4">🔍</div>
|
|
||||||
<p className="text-slate-500 dark:text-slate-400 text-xl mb-2">
|
|
||||||
No tools found matching "{searchTerm}"
|
|
||||||
</p>
|
|
||||||
<p className="text-slate-400 dark:text-slate-500">
|
|
||||||
Try searching for "editor", "encode", or "format"
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Features Section */}
|
|
||||||
<div className={`py-20 transition-all duration-1000 delay-700 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'}`}>
|
|
||||||
<div className="text-center mb-16">
|
|
||||||
<h2 className="text-3xl md:text-4xl font-bold text-slate-800 dark:text-white mb-4">
|
|
||||||
Built for Developers
|
|
||||||
</h2>
|
|
||||||
<p className="text-xl text-slate-600 dark:text-slate-300 max-w-2xl mx-auto">
|
|
||||||
Every tool is crafted with developer experience and performance in mind
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
{/* Tools Grid */}
|
||||||
<div className="group text-center p-8 rounded-2xl bg-white/50 dark:bg-slate-800/50 backdrop-blur-sm border border-slate-200 dark:border-slate-700 hover:border-blue-300 dark:hover:border-blue-600 transition-all duration-300 hover:shadow-xl hover:shadow-blue-500/10">
|
<div className={`transition-all duration-1000 delay-300 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'}`}>
|
||||||
<div className="bg-gradient-to-br from-blue-500 to-blue-600 rounded-2xl w-16 h-16 flex items-center justify-center mx-auto mb-6 group-hover:scale-110 transition-transform duration-300">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 mb-20">
|
||||||
<Zap className="h-8 w-8 text-white" />
|
{filteredTools.map((tool, index) => (
|
||||||
</div>
|
<div
|
||||||
<h3 className="text-xl font-bold text-slate-800 dark:text-white mb-3">
|
key={index}
|
||||||
Lightning Fast
|
className={`transition-all duration-500 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'}`}
|
||||||
</h3>
|
style={{ transitionDelay: `${400 + index * 100}ms` }}
|
||||||
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
|
>
|
||||||
Optimized algorithms and local processing ensure instant results
|
<ToolCard
|
||||||
|
icon={tool.icon}
|
||||||
|
title={tool.name}
|
||||||
|
description={tool.description}
|
||||||
|
path={tool.path}
|
||||||
|
tags={tool.tags}
|
||||||
|
category={tool.category}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* No Results */}
|
||||||
|
{filteredTools.length === 0 && (
|
||||||
|
<div className="text-center py-20">
|
||||||
|
<div className="text-6xl mb-4">🔍</div>
|
||||||
|
<p className="text-slate-500 dark:text-slate-400 text-xl mb-2">
|
||||||
|
No tools found matching "{searchTerm}"
|
||||||
|
</p>
|
||||||
|
<p className="text-slate-400 dark:text-slate-500">
|
||||||
|
Try searching for "editor", "encode", or "format"
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<div className="group text-center p-8 rounded-2xl bg-white/50 dark:bg-slate-800/50 backdrop-blur-sm border border-slate-200 dark:border-slate-700 hover:border-purple-300 dark:hover:border-purple-600 transition-all duration-300 hover:shadow-xl hover:shadow-purple-500/10">
|
|
||||||
<div className="bg-gradient-to-br from-purple-500 to-purple-600 rounded-2xl w-16 h-16 flex items-center justify-center mx-auto mb-6 group-hover:scale-110 transition-transform duration-300">
|
{/* Features Section */}
|
||||||
<Shield className="h-8 w-8 text-white" />
|
<div className={`py-20 transition-all duration-1000 delay-700 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'}`}>
|
||||||
</div>
|
<div className="text-center mb-16">
|
||||||
<h3 className="text-xl font-bold text-slate-800 dark:text-white mb-3">
|
<h2 className="text-3xl md:text-4xl font-bold text-slate-800 dark:text-white mb-4">
|
||||||
Privacy First
|
Built for Developers
|
||||||
</h3>
|
</h2>
|
||||||
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
|
<p className="text-xl text-slate-600 dark:text-slate-300 max-w-2xl mx-auto">
|
||||||
Your data never leaves your browser. Zero tracking, zero storage
|
Every tool is crafted with developer experience and performance in mind
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="group text-center p-8 rounded-2xl bg-white/50 dark:bg-slate-800/50 backdrop-blur-sm border border-slate-200 dark:border-slate-700 hover:border-green-300 dark:hover:border-green-600 transition-all duration-300 hover:shadow-xl hover:shadow-green-500/10">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||||
<div className="bg-gradient-to-br from-green-500 to-green-600 rounded-2xl w-16 h-16 flex items-center justify-center mx-auto mb-6 group-hover:scale-110 transition-transform duration-300">
|
<div className="group text-center p-8 rounded-2xl bg-white/50 dark:bg-slate-800/50 backdrop-blur-sm border border-slate-200 dark:border-slate-700 hover:border-blue-300 dark:hover:border-blue-600 transition-all duration-300 hover:shadow-xl hover:shadow-blue-500/10">
|
||||||
<Cpu className="h-8 w-8 text-white" />
|
<div className="bg-gradient-to-br from-blue-500 to-blue-600 rounded-2xl w-16 h-16 flex items-center justify-center mx-auto mb-6 group-hover:scale-110 transition-transform duration-300">
|
||||||
|
<Zap className="h-8 w-8 text-white" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-slate-800 dark:text-white mb-3">
|
||||||
|
Lightning Fast
|
||||||
|
</h3>
|
||||||
|
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
|
||||||
|
Optimized algorithms and local processing ensure instant results
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-xl font-bold text-slate-800 dark:text-white mb-3">
|
|
||||||
No Limits
|
<div className="group text-center p-8 rounded-2xl bg-white/50 dark:bg-slate-800/50 backdrop-blur-sm border border-slate-200 dark:border-slate-700 hover:border-purple-300 dark:hover:border-purple-600 transition-all duration-300 hover:shadow-xl hover:shadow-purple-500/10">
|
||||||
</h3>
|
<div className="bg-gradient-to-br from-purple-500 to-purple-600 rounded-2xl w-16 h-16 flex items-center justify-center mx-auto mb-6 group-hover:scale-110 transition-transform duration-300">
|
||||||
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
|
<Shield className="h-8 w-8 text-white" />
|
||||||
Handle massive files and complex data without restrictions
|
</div>
|
||||||
</p>
|
<h3 className="text-xl font-bold text-slate-800 dark:text-white mb-3">
|
||||||
</div>
|
Privacy First
|
||||||
|
</h3>
|
||||||
<div className="group text-center p-8 rounded-2xl bg-white/50 dark:bg-slate-800/50 backdrop-blur-sm border border-slate-200 dark:border-slate-700 hover:border-indigo-300 dark:hover:border-indigo-600 transition-all duration-300 hover:shadow-xl hover:shadow-indigo-500/10">
|
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
|
||||||
<div className="bg-gradient-to-br from-indigo-500 to-indigo-600 rounded-2xl w-16 h-16 flex items-center justify-center mx-auto mb-6 group-hover:scale-110 transition-transform duration-300">
|
Your data never leaves your browser. Zero tracking, zero storage
|
||||||
<Code className="h-8 w-8 text-white" />
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="group text-center p-8 rounded-2xl bg-white/50 dark:bg-slate-800/50 backdrop-blur-sm border border-slate-200 dark:border-slate-700 hover:border-green-300 dark:hover:border-green-600 transition-all duration-300 hover:shadow-xl hover:shadow-green-500/10">
|
||||||
|
<div className="bg-gradient-to-br from-green-500 to-green-600 rounded-2xl w-16 h-16 flex items-center justify-center mx-auto mb-6 group-hover:scale-110 transition-transform duration-300">
|
||||||
|
<Cpu className="h-8 w-8 text-white" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-slate-800 dark:text-white mb-3">
|
||||||
|
No Limits
|
||||||
|
</h3>
|
||||||
|
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
|
||||||
|
Handle massive files and complex data without restrictions
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="group text-center p-8 rounded-2xl bg-white/50 dark:bg-slate-800/50 backdrop-blur-sm border border-slate-200 dark:border-slate-700 hover:border-indigo-300 dark:hover:border-indigo-600 transition-all duration-300 hover:shadow-xl hover:shadow-indigo-500/10">
|
||||||
|
<div className="bg-gradient-to-br from-indigo-500 to-indigo-600 rounded-2xl w-16 h-16 flex items-center justify-center mx-auto mb-6 group-hover:scale-110 transition-transform duration-300">
|
||||||
|
<Code className="h-8 w-8 text-white" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-slate-800 dark:text-white mb-3">
|
||||||
|
Dev Focused
|
||||||
|
</h3>
|
||||||
|
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
|
||||||
|
Syntax highlighting, shortcuts, and workflows developers love
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-xl font-bold text-slate-800 dark:text-white mb-3">
|
|
||||||
Dev Focused
|
|
||||||
</h3>
|
|
||||||
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
|
|
||||||
Syntax highlighting, shortcuts, and workflows developers love
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -844,7 +844,7 @@ const InvoiceEditor = () => {
|
|||||||
type="url"
|
type="url"
|
||||||
value={url}
|
value={url}
|
||||||
onChange={(e) => setUrl(e.target.value)}
|
onChange={(e) => setUrl(e.target.value)}
|
||||||
placeholder="https://api.telegram.org/bot<token>/getMe"
|
placeholder="https://your-url.com/invoice/your-invoice"
|
||||||
className="tool-input w-full"
|
className="tool-input w-full"
|
||||||
onKeyPress={(e) => e.key === 'Enter' && handleUrlFetch()}
|
onKeyPress={(e) => e.key === 'Enter' && handleUrlFetch()}
|
||||||
/>
|
/>
|
||||||
@@ -858,7 +858,7 @@ const InvoiceEditor = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
<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.
|
Enter any URL that returns exported JSON data from your previous invoice work.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,33 +4,10 @@ import ToolLayout from '../components/ToolLayout';
|
|||||||
import StructuredEditor from '../components/StructuredEditor';
|
import StructuredEditor from '../components/StructuredEditor';
|
||||||
import MindmapView from '../components/MindmapView';
|
import MindmapView from '../components/MindmapView';
|
||||||
import PostmanTable from '../components/PostmanTable';
|
import PostmanTable from '../components/PostmanTable';
|
||||||
import CodeEditor from '../components/CodeEditor';
|
|
||||||
import CodeMirrorEditor from '../components/CodeMirrorEditor';
|
import CodeMirrorEditor from '../components/CodeMirrorEditor';
|
||||||
|
|
||||||
// Hook to detect dark mode
|
|
||||||
const useDarkMode = () => {
|
|
||||||
const [isDark, setIsDark] = useState(() => {
|
|
||||||
return document.documentElement.classList.contains('dark');
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const observer = new MutationObserver(() => {
|
|
||||||
setIsDark(document.documentElement.classList.contains('dark'));
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(document.documentElement, {
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ['class']
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => observer.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return isDark;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ObjectEditor = () => {
|
const ObjectEditor = () => {
|
||||||
const isDark = useDarkMode();
|
const exportCardRef = useRef(null);
|
||||||
const [structuredData, setStructuredData] = useState({});
|
const [structuredData, setStructuredData] = useState({});
|
||||||
|
|
||||||
// Sync structured data to localStorage for navigation guard
|
// Sync structured data to localStorage for navigation guard
|
||||||
@@ -58,9 +35,53 @@ const ObjectEditor = () => {
|
|||||||
const fileInputRef = useRef(null);
|
const fileInputRef = useRef(null);
|
||||||
const [pasteCollapsed, setPasteCollapsed] = useState(false);
|
const [pasteCollapsed, setPasteCollapsed] = useState(false);
|
||||||
const [pasteDataSummary, setPasteDataSummary] = useState(null);
|
const [pasteDataSummary, setPasteDataSummary] = useState(null);
|
||||||
|
const [urlDataSummary, setUrlDataSummary] = useState(null);
|
||||||
|
const [fileDataSummary, setFileDataSummary] = useState(null);
|
||||||
const [outputExpanded, setOutputExpanded] = useState(false);
|
const [outputExpanded, setOutputExpanded] = useState(false);
|
||||||
const [usageTipsExpanded, setUsageTipsExpanded] = useState(false);
|
const [usageTipsExpanded, setUsageTipsExpanded] = useState(false);
|
||||||
|
|
||||||
|
// Button feedback states
|
||||||
|
const [copiedButton, setCopiedButton] = useState(null);
|
||||||
|
const [downloadedButton, setDownloadedButton] = useState(null);
|
||||||
|
|
||||||
|
const copyToClipboard = async (text) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to copy:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export functions
|
||||||
|
const getExportData = (format) => {
|
||||||
|
if (Object.keys(structuredData).length === 0) return "";
|
||||||
|
|
||||||
|
switch (format) {
|
||||||
|
case "json":
|
||||||
|
// Convert data to use column names instead of column IDs
|
||||||
|
// Use Object.fromEntries with columns.map to preserve column order
|
||||||
|
const jsonData = structuredData;
|
||||||
|
return jsonFormat === "pretty"
|
||||||
|
? JSON.stringify(jsonData, null, 2)
|
||||||
|
: JSON.stringify(jsonData);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadFile = (content, filename, mimeType) => {
|
||||||
|
const blob = new Blob([content], { type: mimeType });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
// Helper function to check if user has data that would be lost
|
// Helper function to check if user has data that would be lost
|
||||||
const hasUserData = () => {
|
const hasUserData = () => {
|
||||||
return Object.keys(structuredData).length > 0;
|
return Object.keys(structuredData).length > 0;
|
||||||
@@ -549,28 +570,38 @@ const ObjectEditor = () => {
|
|||||||
// Handle file import (auto-load, same as Table/Invoice Editor)
|
// Handle file import (auto-load, same as Table/Invoice Editor)
|
||||||
const handleFileImport = (event) => {
|
const handleFileImport = (event) => {
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
if (file) {
|
if (!file) return;
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (e) => {
|
|
||||||
try {
|
|
||||||
const content = e.target.result;
|
|
||||||
const detection = detectInputFormat(content);
|
|
||||||
|
|
||||||
if (detection.valid) {
|
|
||||||
setStructuredData(detection.data);
|
|
||||||
setCreateNewCompleted(true);
|
|
||||||
setError('');
|
|
||||||
} else {
|
|
||||||
setError(detection.error || 'Invalid format. Please enter valid JSON or PHP serialized data.');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError('Failed to read file: ' + err.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsText(file);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const content = e.target.result;
|
||||||
|
const detection = detectInputFormat(content);
|
||||||
|
|
||||||
|
if (detection.valid) {
|
||||||
|
setStructuredData(detection.data);
|
||||||
|
generateOutputs(detection.data);
|
||||||
|
setInputText(content);
|
||||||
|
setInputFormat(detection.format);
|
||||||
|
setInputValid(true);
|
||||||
|
setError('');
|
||||||
|
setCreateNewCompleted(true);
|
||||||
|
|
||||||
|
// Set file data summary
|
||||||
|
setFileDataSummary({
|
||||||
|
format: detection.format,
|
||||||
|
size: content.length,
|
||||||
|
properties: Object.keys(detection.data).length,
|
||||||
|
filename: file.name
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stay on Open tab - don't switch to paste
|
||||||
|
} else {
|
||||||
|
setError(detection.error || 'Invalid file format');
|
||||||
|
setFileDataSummary(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
};
|
||||||
|
|
||||||
// Fetch data from URL
|
// Fetch data from URL
|
||||||
const handleFetchData = async () => {
|
const handleFetchData = async () => {
|
||||||
@@ -596,36 +627,38 @@ const ObjectEditor = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const contentType = response.headers.get('content-type');
|
const contentType = response.headers.get('content-type');
|
||||||
|
const text = await response.text();
|
||||||
|
let data;
|
||||||
|
|
||||||
if (!contentType || !contentType.includes('application/json')) {
|
if (!contentType || !contentType.includes('application/json')) {
|
||||||
// Try to parse as JSON anyway, some APIs don't set correct content-type
|
// Try to parse as JSON anyway, some APIs don't set correct content-type
|
||||||
const text = await response.text();
|
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(text);
|
data = JSON.parse(text);
|
||||||
setStructuredData(data);
|
|
||||||
generateOutputs(data);
|
|
||||||
setInputText(JSON.stringify(data, null, 2));
|
|
||||||
setInputFormat('JSON');
|
|
||||||
setInputValid(true);
|
|
||||||
setCreateNewCompleted(true);
|
|
||||||
} catch {
|
} catch {
|
||||||
throw new Error('Response is not valid JSON. Content-Type: ' + (contentType || 'unknown'));
|
throw new Error('Response is not valid JSON. Content-Type: ' + (contentType || 'unknown'));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const data = await response.json();
|
data = JSON.parse(text);
|
||||||
setStructuredData(data);
|
|
||||||
generateOutputs(data);
|
|
||||||
setInputText(JSON.stringify(data, null, 2));
|
|
||||||
setInputFormat('JSON');
|
|
||||||
setInputValid(true);
|
|
||||||
setCreateNewCompleted(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setStructuredData(data);
|
||||||
|
generateOutputs(data);
|
||||||
|
setInputText(JSON.stringify(data, null, 2));
|
||||||
|
setInputFormat('JSON');
|
||||||
|
setInputValid(true);
|
||||||
|
setCreateNewCompleted(true);
|
||||||
|
|
||||||
|
// Set URL data summary
|
||||||
|
setUrlDataSummary({
|
||||||
|
format: 'JSON',
|
||||||
|
size: text.length,
|
||||||
|
properties: Object.keys(data).length,
|
||||||
|
url: url
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Fetch error:', err);
|
console.error('Fetch error:', err);
|
||||||
if (err.name === 'TypeError' && err.message.includes('fetch')) {
|
setError(`Failed to fetch data: ${err.message}`);
|
||||||
setError('Network error: Unable to fetch data. Check the URL and try again.');
|
setUrlDataSummary(null);
|
||||||
} else {
|
|
||||||
setError(`Fetch failed: ${err.message}`);
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
setFetching(false);
|
setFetching(false);
|
||||||
}
|
}
|
||||||
@@ -779,43 +812,59 @@ const ObjectEditor = () => {
|
|||||||
|
|
||||||
{/* URL Tab Content */}
|
{/* URL Tab Content */}
|
||||||
{activeTab === 'url' && (
|
{activeTab === 'url' && (
|
||||||
<div className="space-y-3">
|
urlDataSummary ? (
|
||||||
<div className="flex gap-2">
|
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
||||||
<div className="relative flex-1">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||||
<input
|
<span className="text-sm text-green-700 dark:text-green-300 break-words">
|
||||||
type="url"
|
✓ Data loaded: {urlDataSummary.format} ({urlDataSummary.size.toLocaleString()} chars, {urlDataSummary.properties} {urlDataSummary.properties === 1 ? 'property' : 'properties'})
|
||||||
value={fetchUrl}
|
</span>
|
||||||
onChange={(e) => setFetchUrl(e.target.value)}
|
<button
|
||||||
placeholder="https://api.telegram.org/bot<token>/getMe"
|
onClick={() => setUrlDataSummary(null)}
|
||||||
className="tool-input w-full"
|
className="text-sm text-blue-600 dark:text-blue-400 hover:underline whitespace-nowrap"
|
||||||
onKeyPress={(e) => e.key === 'Enter' && handleFetchData()}
|
>
|
||||||
/>
|
Fetch New URL ▼
|
||||||
|
</button>
|
||||||
</div>
|
</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>
|
</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.
|
<div className="space-y-3">
|
||||||
</p>
|
<div className="flex gap-2">
|
||||||
</div>
|
<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 */}
|
{/* Paste Tab Content */}
|
||||||
{activeTab === 'paste' && (
|
{activeTab === 'paste' && (
|
||||||
pasteCollapsed ? (
|
pasteCollapsed ? (
|
||||||
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||||
<span className="text-sm text-green-700 dark:text-green-300">
|
<span className="text-sm text-green-700 dark:text-green-300 break-words">
|
||||||
✓ Object loaded: {pasteDataSummary.format} ({pasteDataSummary.size.toLocaleString()} chars, {pasteDataSummary.properties} {pasteDataSummary.properties === 1 ? 'property' : 'properties'})
|
✓ Object loaded: {pasteDataSummary.format} ({pasteDataSummary.size.toLocaleString()} chars, {pasteDataSummary.properties} {pasteDataSummary.properties === 1 ? 'property' : 'properties'})
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => setPasteCollapsed(false)}
|
onClick={() => setPasteCollapsed(false)}
|
||||||
className="text-sm text-blue-600 hover:underline"
|
className="text-sm text-blue-600 dark:text-blue-400 hover:underline whitespace-nowrap"
|
||||||
>
|
>
|
||||||
Edit Input ▼
|
Edit Input ▼
|
||||||
</button>
|
</button>
|
||||||
@@ -868,20 +917,36 @@ const ObjectEditor = () => {
|
|||||||
|
|
||||||
{/* Open Tab Content */}
|
{/* Open Tab Content */}
|
||||||
{activeTab === 'open' && (
|
{activeTab === 'open' && (
|
||||||
<div className="space-y-3">
|
fileDataSummary ? (
|
||||||
<input
|
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
||||||
ref={fileInputRef}
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||||
type="file"
|
<span className="text-sm text-green-700 dark:text-green-300 break-words">
|
||||||
accept=".json,.txt"
|
✓ File loaded: {fileDataSummary.format} ({fileDataSummary.size.toLocaleString()} chars, {fileDataSummary.properties} {fileDataSummary.properties === 1 ? 'property' : 'properties'}) - {fileDataSummary.filename}
|
||||||
onChange={handleFileImport}
|
</span>
|
||||||
className="tool-input"
|
<button
|
||||||
/>
|
onClick={() => setFileDataSummary(null)}
|
||||||
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-md p-3">
|
className="text-sm text-blue-600 dark:text-blue-400 hover:underline whitespace-nowrap"
|
||||||
<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.
|
Upload New File ▼
|
||||||
</p>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".json,.txt"
|
||||||
|
onChange={handleFileImport}
|
||||||
|
className="tool-input"
|
||||||
|
/>
|
||||||
|
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-md p-3">
|
||||||
|
<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>
|
||||||
)}
|
)}
|
||||||
@@ -900,7 +965,7 @@ const ObjectEditor = () => {
|
|||||||
Object Editor
|
Object Editor
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{/* View Mode Tabs - Moved to right */}
|
{/* View Mode Tabs */}
|
||||||
<div className="flex border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
<div className="flex border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewMode('visual')}
|
onClick={() => setViewMode('visual')}
|
||||||
@@ -911,7 +976,7 @@ const ObjectEditor = () => {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Edit3 className="h-4 w-4" />
|
<Edit3 className="h-4 w-4" />
|
||||||
<span className="hidden sm:inline">Visual Editor</span>
|
<span className="hidden sm:inline">Tree</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewMode('mindmap')}
|
onClick={() => setViewMode('mindmap')}
|
||||||
@@ -922,7 +987,7 @@ const ObjectEditor = () => {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Workflow className="h-4 w-4" />
|
<Workflow className="h-4 w-4" />
|
||||||
<span className="hidden sm:inline">Mindmap View</span>
|
<span className="hidden sm:inline">Mindmap</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewMode('table')}
|
onClick={() => setViewMode('table')}
|
||||||
@@ -933,7 +998,7 @@ const ObjectEditor = () => {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Table className="h-4 w-4" />
|
<Table className="h-4 w-4" />
|
||||||
<span className="hidden sm:inline">Table View</span>
|
<span className="hidden sm:inline">Table</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -954,15 +1019,11 @@ const ObjectEditor = () => {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{viewMode === 'visual' && (
|
{viewMode === 'visual' && (
|
||||||
<div className="w-full overflow-hidden">
|
<div className="p-4">
|
||||||
<div className="w-full overflow-x-auto p-4">
|
<StructuredEditor
|
||||||
<div className="min-w-max">
|
initialData={structuredData}
|
||||||
<StructuredEditor
|
onDataChange={handleStructuredDataChange}
|
||||||
initialData={structuredData}
|
/>
|
||||||
onDataChange={handleStructuredDataChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -987,6 +1048,7 @@ const ObjectEditor = () => {
|
|||||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mt-6">
|
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mt-6">
|
||||||
{/* Export Header - Collapsible */}
|
{/* Export Header - Collapsible */}
|
||||||
<div
|
<div
|
||||||
|
ref={exportCardRef}
|
||||||
onClick={() => setOutputExpanded(!outputExpanded)}
|
onClick={() => setOutputExpanded(!outputExpanded)}
|
||||||
className="px-4 py-3 border-b border-gray-200 dark:border-gray-700 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
className="px-4 py-3 border-b border-gray-200 dark:border-gray-700 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||||
>
|
>
|
||||||
@@ -1035,13 +1097,14 @@ const ObjectEditor = () => {
|
|||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
{activeExportTab === 'json' && (
|
{activeExportTab === 'json' && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<CodeEditor
|
<CodeMirrorEditor
|
||||||
value={jsonFormat === 'pretty' ? (outputs.jsonPretty || '{}') : (outputs.jsonMinified || '{}')}
|
value={jsonFormat === 'pretty' ? (outputs.jsonPretty || '{}') : (outputs.jsonMinified || '{}')}
|
||||||
language="json"
|
language="json"
|
||||||
readOnly={true}
|
readOnly={true}
|
||||||
height="300px"
|
maxLines={12}
|
||||||
|
showToggle={true}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
theme={isDark ? 'dark' : 'light'}
|
cardRef={exportCardRef}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -1070,26 +1133,27 @@ const ObjectEditor = () => {
|
|||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const content = jsonFormat === 'pretty' ? (outputs.jsonPretty || '{}') : (outputs.jsonMinified || '{}');
|
const content = jsonFormat === 'pretty' ? (outputs.jsonPretty || '{}') : (outputs.jsonMinified || '{}');
|
||||||
navigator.clipboard.writeText(content);
|
copyToClipboard(content);
|
||||||
|
setCopiedButton('json');
|
||||||
|
setTimeout(() => setCopiedButton(null), 2000);
|
||||||
}}
|
}}
|
||||||
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"
|
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
|
{copiedButton === 'json' ? '✓ Copied!' : 'Copy'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const content = jsonFormat === 'pretty' ? (outputs.jsonPretty || '{}') : (outputs.jsonMinified || '{}');
|
downloadFile(
|
||||||
const blob = new Blob([content], { type: 'application/json' });
|
getExportData("json"),
|
||||||
const url = URL.createObjectURL(blob);
|
"object-data.json",
|
||||||
const a = document.createElement('a');
|
"application/json",
|
||||||
a.href = url;
|
);
|
||||||
a.download = 'object-data.json';
|
setDownloadedButton('json');
|
||||||
a.click();
|
setTimeout(() => setDownloadedButton(null), 2000);
|
||||||
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"
|
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
|
{downloadedButton === 'json' ? '✓ Downloaded!' : 'Download'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1098,23 +1162,26 @@ const ObjectEditor = () => {
|
|||||||
|
|
||||||
{activeExportTab === 'php' && (
|
{activeExportTab === 'php' && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<CodeEditor
|
<CodeMirrorEditor
|
||||||
value={outputs.serialized || 'a:0:{}'}
|
value={outputs.serialized || 'a:0:{}'}
|
||||||
language="javascript"
|
language="javascript"
|
||||||
readOnly={true}
|
readOnly={true}
|
||||||
height="300px"
|
maxLines={12}
|
||||||
|
showToggle={true}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
theme={isDark ? 'dark' : 'light'}
|
cardRef={exportCardRef}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const content = outputs.serialized || 'a:0:{}';
|
const content = outputs.serialized || 'a:0:{}';
|
||||||
navigator.clipboard.writeText(content);
|
copyToClipboard(content);
|
||||||
|
setCopiedButton('php');
|
||||||
|
setTimeout(() => setCopiedButton(null), 2000);
|
||||||
}}
|
}}
|
||||||
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"
|
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
|
{copiedButton === 'php' ? '✓ Copied!' : 'Copy'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -1124,12 +1191,16 @@ const ObjectEditor = () => {
|
|||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = 'object-data.txt';
|
a.download = 'object-data.txt';
|
||||||
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
|
setDownloadedButton('php');
|
||||||
|
setTimeout(() => setDownloadedButton(null), 2000);
|
||||||
}}
|
}}
|
||||||
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"
|
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
|
{downloadedButton === 'php' ? '✓ Downloaded!' : 'Download'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,35 +1,12 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { Plus, Upload, FileText, Globe, Download, X, Table, Trash2, Database, Braces, Code, Eye, Minimize2, Maximize2, Search, ArrowUpDown, AlertTriangle, Edit3, ChevronUp, ChevronDown } from 'lucide-react';
|
import { Plus, Upload, FileText, Globe, Download, X, Table, Trash2, Database, Braces, Code, Eye, Minimize2, Maximize2, Search, ArrowUpDown, AlertTriangle, Edit3, ChevronUp, ChevronDown } from 'lucide-react';
|
||||||
import ToolLayout from '../components/ToolLayout';
|
import ToolLayout from '../components/ToolLayout';
|
||||||
import CodeEditor from '../components/CodeEditor';
|
|
||||||
import CodeMirrorEditor from '../components/CodeMirrorEditor';
|
import CodeMirrorEditor from '../components/CodeMirrorEditor';
|
||||||
import StructuredEditor from "../components/StructuredEditor";
|
import StructuredEditor from "../components/StructuredEditor";
|
||||||
import Papa from "papaparse";
|
import Papa from "papaparse";
|
||||||
|
|
||||||
// Hook to detect dark mode
|
|
||||||
const useDarkMode = () => {
|
|
||||||
const [isDark, setIsDark] = useState(() => {
|
|
||||||
return document.documentElement.classList.contains('dark');
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const observer = new MutationObserver(() => {
|
|
||||||
setIsDark(document.documentElement.classList.contains('dark'));
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(document.documentElement, {
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ['class']
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => observer.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return isDark;
|
|
||||||
};
|
|
||||||
|
|
||||||
const TableEditor = () => {
|
const TableEditor = () => {
|
||||||
const isDark = useDarkMode();
|
const exportCardRef = useRef(null);
|
||||||
const [data, setData] = useState([]);
|
const [data, setData] = useState([]);
|
||||||
const [columns, setColumns] = useState([]);
|
const [columns, setColumns] = useState([]);
|
||||||
|
|
||||||
@@ -69,6 +46,8 @@ const TableEditor = () => {
|
|||||||
const [createNewCompleted, setCreateNewCompleted] = useState(false); // Track if user completed Create New step
|
const [createNewCompleted, setCreateNewCompleted] = useState(false); // Track if user completed Create New step
|
||||||
const [pasteCollapsed, setPasteCollapsed] = useState(false); // Track if paste input is collapsed
|
const [pasteCollapsed, setPasteCollapsed] = useState(false); // Track if paste input is collapsed
|
||||||
const [pasteDataSummary, setPasteDataSummary] = useState(null); // Summary of pasted data
|
const [pasteDataSummary, setPasteDataSummary] = useState(null); // Summary of pasted data
|
||||||
|
const [urlDataSummary, setUrlDataSummary] = useState(null); // Summary of URL fetched data
|
||||||
|
const [fileDataSummary, setFileDataSummary] = useState(null); // Summary of file uploaded data
|
||||||
const [exportExpanded, setExportExpanded] = useState(false); // Track if export section is expanded
|
const [exportExpanded, setExportExpanded] = useState(false); // Track if export section is expanded
|
||||||
const [usageTipsExpanded, setUsageTipsExpanded] = useState(false); // Track if usage tips is expanded
|
const [usageTipsExpanded, setUsageTipsExpanded] = useState(false); // Track if usage tips is expanded
|
||||||
|
|
||||||
@@ -76,6 +55,10 @@ const TableEditor = () => {
|
|||||||
const [sqlTableName, setSqlTableName] = useState(""); // Table name for SQL export
|
const [sqlTableName, setSqlTableName] = useState(""); // Table name for SQL export
|
||||||
const [sqlPrimaryKey, setSqlPrimaryKey] = useState(""); // Primary key column for SQL export
|
const [sqlPrimaryKey, setSqlPrimaryKey] = useState(""); // Primary key column for SQL export
|
||||||
|
|
||||||
|
// Button feedback states
|
||||||
|
const [copiedButton, setCopiedButton] = useState(null);
|
||||||
|
const [downloadedButton, setDownloadedButton] = useState(null);
|
||||||
|
|
||||||
// Helper function to check if user has data that would be lost
|
// Helper function to check if user has data that would be lost
|
||||||
const hasUserData = () => {
|
const hasUserData = () => {
|
||||||
// Check if there are multiple tables (imported data)
|
// Check if there are multiple tables (imported data)
|
||||||
@@ -650,55 +633,52 @@ const TableEditor = () => {
|
|||||||
|
|
||||||
// Parse CSV/TSV data
|
// Parse CSV/TSV data
|
||||||
const parseData = (text, hasHeaders = true) => {
|
const parseData = (text, hasHeaders = true) => {
|
||||||
try {
|
const result = Papa.parse(text.trim(), {
|
||||||
const result = Papa.parse(text.trim(), {
|
header: false,
|
||||||
header: false,
|
skipEmptyLines: true,
|
||||||
skipEmptyLines: true,
|
delimiter: text.includes("\t") ? "\t" : ",",
|
||||||
delimiter: text.includes("\t") ? "\t" : ",",
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if (result.errors.length > 0) {
|
if (result.errors.length > 0) {
|
||||||
throw new Error(result.errors[0].message);
|
throw new Error(result.errors[0].message);
|
||||||
}
|
|
||||||
|
|
||||||
const rows = result.data;
|
|
||||||
if (rows.length === 0) {
|
|
||||||
throw new Error("No data found");
|
|
||||||
}
|
|
||||||
|
|
||||||
let headers;
|
|
||||||
let dataRows;
|
|
||||||
|
|
||||||
if (hasHeaders && rows.length > 0) {
|
|
||||||
headers = rows[0].map((header, index) => ({
|
|
||||||
id: `col_${index}`,
|
|
||||||
name: header || `Column ${index + 1}`,
|
|
||||||
type: "text",
|
|
||||||
}));
|
|
||||||
dataRows = rows.slice(1);
|
|
||||||
} else {
|
|
||||||
headers = rows[0].map((_, index) => ({
|
|
||||||
id: `col_${index}`,
|
|
||||||
name: `Column ${index + 1}`,
|
|
||||||
type: "text",
|
|
||||||
}));
|
|
||||||
dataRows = rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tableData = dataRows.map((row, index) => {
|
|
||||||
const rowData = { id: `row_${index}` };
|
|
||||||
headers.forEach((header, colIndex) => {
|
|
||||||
rowData[header.id] = row[colIndex] || "";
|
|
||||||
});
|
|
||||||
return rowData;
|
|
||||||
});
|
|
||||||
|
|
||||||
setColumns(headers);
|
|
||||||
setData(tableData);
|
|
||||||
setError("");
|
|
||||||
} catch (err) {
|
|
||||||
setError(`Failed to parse data: ${err.message}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rows = result.data;
|
||||||
|
if (rows.length === 0) {
|
||||||
|
throw new Error("No data found");
|
||||||
|
}
|
||||||
|
|
||||||
|
let headers;
|
||||||
|
let dataRows;
|
||||||
|
|
||||||
|
if (hasHeaders && rows.length > 0) {
|
||||||
|
headers = rows[0].map((header, index) => ({
|
||||||
|
id: `col_${index}`,
|
||||||
|
name: header || `Column ${index + 1}`,
|
||||||
|
type: "text",
|
||||||
|
}));
|
||||||
|
dataRows = rows.slice(1);
|
||||||
|
} else {
|
||||||
|
headers = rows[0].map((_, index) => ({
|
||||||
|
id: `col_${index}`,
|
||||||
|
name: `Column ${index + 1}`,
|
||||||
|
type: "text",
|
||||||
|
}));
|
||||||
|
dataRows = rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableData = dataRows.map((row, index) => {
|
||||||
|
const rowData = { id: `row_${index}` };
|
||||||
|
headers.forEach((header, colIndex) => {
|
||||||
|
rowData[header.id] = row[colIndex] || "";
|
||||||
|
});
|
||||||
|
return rowData;
|
||||||
|
});
|
||||||
|
|
||||||
|
setColumns(headers);
|
||||||
|
setData(tableData);
|
||||||
|
setError("");
|
||||||
|
return tableData.length; // Return actual row count
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse SQL data
|
// Parse SQL data
|
||||||
@@ -941,40 +921,37 @@ const TableEditor = () => {
|
|||||||
|
|
||||||
// Parse JSON data
|
// Parse JSON data
|
||||||
const parseJsonData = (text) => {
|
const parseJsonData = (text) => {
|
||||||
try {
|
const jsonData = JSON.parse(text);
|
||||||
const jsonData = JSON.parse(text);
|
|
||||||
|
|
||||||
if (!Array.isArray(jsonData)) {
|
if (!Array.isArray(jsonData)) {
|
||||||
throw new Error("JSON must be an array of objects");
|
throw new Error("JSON must be an array of objects");
|
||||||
}
|
|
||||||
|
|
||||||
if (jsonData.length === 0) {
|
|
||||||
throw new Error("Array is empty");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract columns from first object
|
|
||||||
const firstItem = jsonData[0];
|
|
||||||
const headers = Object.keys(firstItem).map((key, index) => ({
|
|
||||||
id: `col_${index}`,
|
|
||||||
name: key,
|
|
||||||
type: typeof firstItem[key] === "number" ? "number" : "text",
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Convert to table format
|
|
||||||
const tableData = jsonData.map((item, index) => {
|
|
||||||
const rowData = { id: `row_${index}` };
|
|
||||||
headers.forEach((header) => {
|
|
||||||
rowData[header.id] = item[header.name] || "";
|
|
||||||
});
|
|
||||||
return rowData;
|
|
||||||
});
|
|
||||||
|
|
||||||
setColumns(headers);
|
|
||||||
setData(tableData);
|
|
||||||
setError("");
|
|
||||||
} catch (err) {
|
|
||||||
setError(`Failed to parse JSON: ${err.message}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (jsonData.length === 0) {
|
||||||
|
throw new Error("Array is empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract columns from first object
|
||||||
|
const firstItem = jsonData[0];
|
||||||
|
const headers = Object.keys(firstItem).map((key, index) => ({
|
||||||
|
id: `col_${index}`,
|
||||||
|
name: key,
|
||||||
|
type: typeof firstItem[key] === "number" ? "number" : "text",
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Convert to table format
|
||||||
|
const tableData = jsonData.map((item, index) => {
|
||||||
|
const rowData = { id: `row_${index}` };
|
||||||
|
headers.forEach((header) => {
|
||||||
|
rowData[header.id] = item[header.name] || "";
|
||||||
|
});
|
||||||
|
return rowData;
|
||||||
|
});
|
||||||
|
|
||||||
|
setColumns(headers);
|
||||||
|
setData(tableData);
|
||||||
|
setError("");
|
||||||
|
return tableData.length; // Return row count for summary
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle text input
|
// Handle text input
|
||||||
@@ -987,15 +964,14 @@ const TableEditor = () => {
|
|||||||
|
|
||||||
const trimmed = inputText.trim();
|
const trimmed = inputText.trim();
|
||||||
let format = '';
|
let format = '';
|
||||||
let success = false;
|
let rowCount = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Try to detect format
|
// Try to detect format
|
||||||
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
||||||
// JSON array
|
// JSON array
|
||||||
parseJsonData(trimmed);
|
rowCount = parseJsonData(trimmed);
|
||||||
format = 'JSON';
|
format = 'JSON';
|
||||||
success = true;
|
|
||||||
} else if (
|
} else if (
|
||||||
trimmed.toLowerCase().includes("insert into") &&
|
trimmed.toLowerCase().includes("insert into") &&
|
||||||
trimmed.toLowerCase().includes("values")
|
trimmed.toLowerCase().includes("values")
|
||||||
@@ -1003,24 +979,25 @@ const TableEditor = () => {
|
|||||||
// SQL INSERT statements
|
// SQL INSERT statements
|
||||||
parseSqlData(trimmed);
|
parseSqlData(trimmed);
|
||||||
format = 'SQL';
|
format = 'SQL';
|
||||||
success = true;
|
// Get row count from state after parse
|
||||||
|
rowCount = data.length;
|
||||||
} else {
|
} else {
|
||||||
// CSV/TSV
|
// CSV/TSV
|
||||||
parseData(trimmed, useFirstRowAsHeader);
|
parseData(trimmed, useFirstRowAsHeader);
|
||||||
format = trimmed.includes('\t') ? 'TSV' : 'CSV';
|
format = trimmed.includes('\t') ? 'TSV' : 'CSV';
|
||||||
success = true;
|
// Get row count from state after parse
|
||||||
|
rowCount = data.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If successful, collapse input and show summary
|
// Collapse input and show summary
|
||||||
if (success && data.length > 0) {
|
setPasteDataSummary({
|
||||||
setPasteDataSummary({
|
format: format,
|
||||||
format: format,
|
size: inputText.length,
|
||||||
size: inputText.length,
|
rows: rowCount || data.length // Use rowCount if available, fallback to data.length
|
||||||
rows: data.length
|
});
|
||||||
});
|
setPasteCollapsed(true);
|
||||||
setPasteCollapsed(true);
|
setCreateNewCompleted(true);
|
||||||
setError('');
|
setError('');
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Keep input expanded on error
|
// Keep input expanded on error
|
||||||
setPasteCollapsed(false);
|
setPasteCollapsed(false);
|
||||||
@@ -1047,14 +1024,28 @@ const TableEditor = () => {
|
|||||||
|
|
||||||
const contentType = response.headers.get("content-type") || "";
|
const contentType = response.headers.get("content-type") || "";
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
|
let format = '';
|
||||||
|
let rowCount = 0;
|
||||||
|
|
||||||
if (contentType.includes("application/json") || url.includes(".json")) {
|
if (contentType.includes("application/json") || url.includes(".json")) {
|
||||||
parseJsonData(text);
|
rowCount = parseJsonData(text);
|
||||||
|
format = 'JSON';
|
||||||
} else {
|
} else {
|
||||||
parseData(text, useFirstRowAsHeader);
|
rowCount = parseData(text, useFirstRowAsHeader);
|
||||||
|
format = text.includes('\t') ? 'TSV' : 'CSV';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set summary for URL fetch
|
||||||
|
setUrlDataSummary({
|
||||||
|
format: format,
|
||||||
|
size: text.length,
|
||||||
|
rows: rowCount,
|
||||||
|
url: url.trim()
|
||||||
|
});
|
||||||
|
setCreateNewCompleted(true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(`Failed to fetch data: ${err.message}`);
|
setError(`Failed to fetch data: ${err.message}`);
|
||||||
|
setUrlDataSummary(null);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -1136,17 +1127,47 @@ const TableEditor = () => {
|
|||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
try {
|
try {
|
||||||
const content = e.target.result;
|
const content = e.target.result;
|
||||||
|
let format = '';
|
||||||
|
let rowCount = 0;
|
||||||
|
|
||||||
// Check if it's SQL file for multi-table support
|
// Check if it's SQL file for multi-table support
|
||||||
if (file.name.toLowerCase().endsWith(".sql")) {
|
if (file.name.toLowerCase().endsWith(".sql")) {
|
||||||
initializeTablesFromSQL(content, file.name);
|
initializeTablesFromSQL(content, file.name);
|
||||||
|
format = 'SQL';
|
||||||
|
// For SQL, we need to wait for state update, use a timeout
|
||||||
|
setTimeout(() => {
|
||||||
|
setFileDataSummary({
|
||||||
|
format: format,
|
||||||
|
size: content.length,
|
||||||
|
rows: data.length,
|
||||||
|
filename: file.name
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
} else if (file.name.toLowerCase().endsWith(".json")) {
|
||||||
|
rowCount = parseJsonData(content);
|
||||||
|
format = 'JSON';
|
||||||
|
setFileDataSummary({
|
||||||
|
format: format,
|
||||||
|
size: content.length,
|
||||||
|
rows: rowCount,
|
||||||
|
filename: file.name
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// Fallback to single-table parsing
|
rowCount = parseData(content, useFirstRowAsHeader);
|
||||||
parseData(content);
|
format = file.name.toLowerCase().endsWith(".tsv") ? 'TSV' : 'CSV';
|
||||||
|
setFileDataSummary({
|
||||||
|
format: format,
|
||||||
|
size: content.length,
|
||||||
|
rows: rowCount,
|
||||||
|
filename: file.name
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCreateNewCompleted(true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("❌ File upload error:", err);
|
console.error("❌ File upload error:", err);
|
||||||
setError("Failed to read file: " + err.message);
|
setError("Failed to read file: " + err.message);
|
||||||
|
setFileDataSummary(null);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -1995,56 +2016,72 @@ const TableEditor = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === "url" && (
|
{activeTab === "url" && (
|
||||||
<div className="space-y-3">
|
urlDataSummary ? (
|
||||||
<div className="flex gap-2">
|
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
||||||
<div className="relative flex-1">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||||
<input
|
<span className="text-sm text-green-700 dark:text-green-300 break-words">
|
||||||
type="url"
|
✓ Data loaded: {urlDataSummary.format} ({urlDataSummary.size.toLocaleString()} chars, {urlDataSummary.rows} rows)
|
||||||
value={url}
|
</span>
|
||||||
onChange={(e) => setUrl(e.target.value)}
|
<button
|
||||||
placeholder="https://api.example.com/data.json or https://example.com/data.csv"
|
onClick={() => setUrlDataSummary(null)}
|
||||||
className="tool-input w-full pr-10"
|
className="text-sm text-blue-600 dark:text-blue-400 hover:underline whitespace-nowrap"
|
||||||
disabled={isLoading}
|
>
|
||||||
/>
|
Fetch New URL ▼
|
||||||
{url && !isLoading && (
|
</button>
|
||||||
<button
|
|
||||||
onClick={() => setUrl("")}
|
|
||||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition-colors"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
onClick={fetchUrlData}
|
|
||||||
disabled={isLoading || !url.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"
|
|
||||||
>
|
|
||||||
{isLoading ? "Fetching..." : "Fetch Data"}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<label className="flex items-center text-sm text-gray-600 dark:text-gray-400">
|
) : (
|
||||||
<input
|
<div className="space-y-3">
|
||||||
type="checkbox"
|
<div className="flex gap-2">
|
||||||
checked={useFirstRowAsHeader}
|
<div className="relative flex-1">
|
||||||
onChange={(e) => setUseFirstRowAsHeader(e.target.checked)}
|
<input
|
||||||
className="mr-2"
|
type="url"
|
||||||
/>
|
value={url}
|
||||||
Use first row as column headers (for CSV/TSV)
|
onChange={(e) => setUrl(e.target.value)}
|
||||||
</label>
|
placeholder="https://api.example.com/data.json or https://example.com/data.csv"
|
||||||
</div>
|
className="tool-input w-full pr-10"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
{url && !isLoading && (
|
||||||
|
<button
|
||||||
|
onClick={() => setUrl("")}
|
||||||
|
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition-colors"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={fetchUrlData}
|
||||||
|
disabled={isLoading || !url.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"
|
||||||
|
>
|
||||||
|
{isLoading ? "Fetching..." : "Fetch Data"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<label className="flex items-center text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={useFirstRowAsHeader}
|
||||||
|
onChange={(e) => setUseFirstRowAsHeader(e.target.checked)}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
Use first row as column headers (for CSV/TSV)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === "paste" && (
|
{activeTab === "paste" && (
|
||||||
pasteCollapsed ? (
|
pasteCollapsed ? (
|
||||||
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||||
<span className="text-sm text-green-700 dark:text-green-300">
|
<span className="text-sm text-green-700 dark:text-green-300 break-words">
|
||||||
✓ Data loaded: {pasteDataSummary.format} ({pasteDataSummary.size.toLocaleString()} chars, {pasteDataSummary.rows} rows)
|
✓ Data loaded: {pasteDataSummary.format} ({pasteDataSummary.size.toLocaleString()} chars, {pasteDataSummary.rows} rows)
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => setPasteCollapsed(false)}
|
onClick={() => setPasteCollapsed(false)}
|
||||||
className="text-sm text-blue-600 hover:underline"
|
className="text-sm text-blue-600 dark:text-blue-400 hover:underline whitespace-nowrap"
|
||||||
>
|
>
|
||||||
Edit Input ▼
|
Edit Input ▼
|
||||||
</button>
|
</button>
|
||||||
@@ -2092,18 +2129,33 @@ const TableEditor = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === "upload" && (
|
{activeTab === "upload" && (
|
||||||
<div className="space-y-3">
|
fileDataSummary ? (
|
||||||
<input
|
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
||||||
type="file"
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||||
accept=".csv,.tsv,.json,.sql"
|
<span className="text-sm text-green-700 dark:text-green-300 break-words">
|
||||||
onChange={handleFileUpload}
|
✓ File loaded: {fileDataSummary.format} ({fileDataSummary.size.toLocaleString()} chars, {fileDataSummary.rows} rows) - {fileDataSummary.filename}
|
||||||
className="tool-input"
|
</span>
|
||||||
/>
|
<button
|
||||||
<label className="flex items-center text-sm text-gray-600 dark:text-gray-400">
|
onClick={() => setFileDataSummary(null)}
|
||||||
|
className="text-sm text-blue-600 dark:text-blue-400 hover:underline whitespace-nowrap"
|
||||||
|
>
|
||||||
|
Upload New File ▼
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="file"
|
||||||
checked={useFirstRowAsHeader}
|
accept=".csv,.tsv,.json,.sql"
|
||||||
onChange={(e) => setUseFirstRowAsHeader(e.target.checked)}
|
onChange={handleFileUpload}
|
||||||
|
className="tool-input"
|
||||||
|
/>
|
||||||
|
<label className="flex items-center text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={useFirstRowAsHeader}
|
||||||
|
onChange={(e) => setUseFirstRowAsHeader(e.target.checked)}
|
||||||
className="mr-2"
|
className="mr-2"
|
||||||
/>
|
/>
|
||||||
Use first row as column headers (for CSV/TSV)
|
Use first row as column headers (for CSV/TSV)
|
||||||
@@ -2115,7 +2167,8 @@ const TableEditor = () => {
|
|||||||
open, edit, and export your files locally.
|
open, edit, and export your files locally.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -2288,7 +2341,7 @@ const TableEditor = () => {
|
|||||||
style={{ maxWidth: '100%' }}
|
style={{ maxWidth: '100%' }}
|
||||||
>
|
>
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead className="bg-gray-50 dark:bg-gray-700 sticky top-0 z-10">
|
<thead className="bg-gray-50 dark:bg-gray-700 sticky top-[-1px] z-10">
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<th
|
||||||
className={`px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border-r border-gray-200 dark:border-gray-600 ${
|
className={`px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border-r border-gray-200 dark:border-gray-600 ${
|
||||||
@@ -2663,10 +2716,11 @@ const TableEditor = () => {
|
|||||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mt-6">
|
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mt-6">
|
||||||
{/* Export Header - Collapsible */}
|
{/* Export Header - Collapsible */}
|
||||||
<div
|
<div
|
||||||
|
ref={exportCardRef}
|
||||||
onClick={() => setExportExpanded(!exportExpanded)}
|
onClick={() => setExportExpanded(!exportExpanded)}
|
||||||
className="px-4 py-3 border-b border-gray-200 dark:border-gray-700 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
className="px-4 py-3 border-b border-gray-200 dark:border-gray-700 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
<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" />
|
<Download className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||||
Export Results
|
Export Results
|
||||||
@@ -2744,12 +2798,14 @@ const TableEditor = () => {
|
|||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
{exportTab === "json" && (
|
{exportTab === "json" && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<CodeEditor
|
<CodeMirrorEditor
|
||||||
value={getExportData("json")}
|
value={getExportData("json")}
|
||||||
language="json"
|
language="json"
|
||||||
readOnly={true}
|
readOnly={true}
|
||||||
height="256px"
|
maxLines={12}
|
||||||
theme={isDark ? 'dark' : 'light'}
|
showToggle={true}
|
||||||
|
className="w-full"
|
||||||
|
cardRef={exportCardRef}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -2776,22 +2832,28 @@ const TableEditor = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => copyToClipboard(getExportData("json"))}
|
onClick={() => {
|
||||||
|
copyToClipboard(getExportData("json"));
|
||||||
|
setCopiedButton('json');
|
||||||
|
setTimeout(() => setCopiedButton(null), 2000);
|
||||||
|
}}
|
||||||
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"
|
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
|
{copiedButton === 'json' ? '✓ Copied!' : 'Copy'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
downloadFile(
|
downloadFile(
|
||||||
getExportData("json"),
|
getExportData("json"),
|
||||||
"table-data.json",
|
"table-data.json",
|
||||||
"application/json",
|
"application/json",
|
||||||
)
|
);
|
||||||
}
|
setDownloadedButton('json');
|
||||||
|
setTimeout(() => setDownloadedButton(null), 2000);
|
||||||
|
}}
|
||||||
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"
|
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
|
{downloadedButton === 'json' ? '✓ Downloaded!' : 'Download'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2800,31 +2862,39 @@ const TableEditor = () => {
|
|||||||
|
|
||||||
{exportTab === "csv" && (
|
{exportTab === "csv" && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<CodeEditor
|
<CodeMirrorEditor
|
||||||
value={getExportData("csv")}
|
value={getExportData("csv")}
|
||||||
language="javascript"
|
language="javascript"
|
||||||
readOnly={true}
|
readOnly={true}
|
||||||
height="256px"
|
maxLines={12}
|
||||||
theme={isDark ? 'dark' : 'light'}
|
showToggle={true}
|
||||||
|
className="w-full"
|
||||||
|
cardRef={exportCardRef}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => copyToClipboard(getExportData("csv"))}
|
onClick={() => {
|
||||||
|
copyToClipboard(getExportData("csv"));
|
||||||
|
setCopiedButton('csv');
|
||||||
|
setTimeout(() => setCopiedButton(null), 2000);
|
||||||
|
}}
|
||||||
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"
|
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
|
{copiedButton === 'csv' ? '✓ Copied!' : 'Copy'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
downloadFile(
|
downloadFile(
|
||||||
getExportData("csv"),
|
getExportData("csv"),
|
||||||
"table-data.csv",
|
"table-data.csv",
|
||||||
"text/csv",
|
"text/csv",
|
||||||
)
|
);
|
||||||
}
|
setDownloadedButton('csv');
|
||||||
|
setTimeout(() => setDownloadedButton(null), 2000);
|
||||||
|
}}
|
||||||
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"
|
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
|
{downloadedButton === 'csv' ? '✓ Downloaded!' : 'Download'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2832,31 +2902,39 @@ const TableEditor = () => {
|
|||||||
|
|
||||||
{exportTab === "tsv" && (
|
{exportTab === "tsv" && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<CodeEditor
|
<CodeMirrorEditor
|
||||||
value={getExportData("tsv")}
|
value={getExportData("tsv")}
|
||||||
language="javascript"
|
language="javascript"
|
||||||
readOnly={true}
|
readOnly={true}
|
||||||
height="256px"
|
maxLines={12}
|
||||||
theme={isDark ? 'dark' : 'light'}
|
showToggle={true}
|
||||||
|
className="w-full"
|
||||||
|
cardRef={exportCardRef}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => copyToClipboard(getExportData("tsv"))}
|
onClick={() => {
|
||||||
|
copyToClipboard(getExportData("tsv"));
|
||||||
|
setCopiedButton('tsv');
|
||||||
|
setTimeout(() => setCopiedButton(null), 2000);
|
||||||
|
}}
|
||||||
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"
|
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
|
{copiedButton === 'tsv' ? '✓ Copied!' : 'Copy'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
downloadFile(
|
downloadFile(
|
||||||
getExportData("tsv"),
|
getExportData("tsv"),
|
||||||
"table-data.tsv",
|
"table-data.tsv",
|
||||||
"text/tab-separated-values",
|
"text/tab-separated-values",
|
||||||
)
|
);
|
||||||
}
|
setDownloadedButton('tsv');
|
||||||
|
setTimeout(() => setDownloadedButton(null), 2000);
|
||||||
|
}}
|
||||||
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"
|
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
|
{downloadedButton === 'tsv' ? '✓ Downloaded!' : 'Download'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2907,12 +2985,14 @@ const TableEditor = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CodeEditor
|
<CodeMirrorEditor
|
||||||
value={getExportData("sql")}
|
value={getExportData("sql")}
|
||||||
language="javascript"
|
language="sql"
|
||||||
readOnly={true}
|
readOnly={true}
|
||||||
height="256px"
|
maxLines={12}
|
||||||
theme={isDark ? 'dark' : 'light'}
|
showToggle={true}
|
||||||
|
className="w-full"
|
||||||
|
cardRef={exportCardRef}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Intelligent Schema Analysis */}
|
{/* Intelligent Schema Analysis */}
|
||||||
@@ -3046,22 +3126,28 @@ const TableEditor = () => {
|
|||||||
|
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => copyToClipboard(getExportData("sql"))}
|
onClick={() => {
|
||||||
|
copyToClipboard(getExportData("sql"));
|
||||||
|
setCopiedButton('sql');
|
||||||
|
setTimeout(() => setCopiedButton(null), 2000);
|
||||||
|
}}
|
||||||
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"
|
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
|
{copiedButton === 'sql' ? '✓ Copied!' : 'Copy'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
downloadFile(
|
downloadFile(
|
||||||
getExportData("sql"),
|
getExportData("sql"),
|
||||||
`${sqlTableName || currentTable || originalFileName || "database"}.sql`,
|
`${sqlTableName || "database"}.sql`,
|
||||||
"application/sql",
|
"application/sql",
|
||||||
)
|
);
|
||||||
}
|
setDownloadedButton('sql');
|
||||||
|
setTimeout(() => setDownloadedButton(null), 2000);
|
||||||
|
}}
|
||||||
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"
|
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
|
{downloadedButton === 'sql' ? '✓ Downloaded!' : 'Download'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -3570,10 +3656,26 @@ const ObjectEditorModal = ({ modal, onClose, onApply }) => {
|
|||||||
Row {modal.rowIndex} • Column: {modal.columnName} • Format:{" "}
|
Row {modal.rowIndex} • Column: {modal.columnName} • Format:{" "}
|
||||||
{modal.format.type.replace("_", " ")}
|
{modal.format.type.replace("_", " ")}
|
||||||
</p>
|
</p>
|
||||||
|
{/* Format info */}
|
||||||
|
<div className="text-sm">
|
||||||
|
<span
|
||||||
|
className={`${isValid ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400"}`}
|
||||||
|
>
|
||||||
|
{isValid ? "✓ Valid" : "✗ Invalid"}{" "}
|
||||||
|
{modal.format.type.replace("_", " ")}
|
||||||
|
</span>
|
||||||
|
{isValid &&
|
||||||
|
structuredData &&
|
||||||
|
typeof structuredData === "object" && (
|
||||||
|
<span className="text-gray-600 dark:text-gray-400">
|
||||||
|
{" • "}{Object.keys(structuredData).length} properties
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 self-start"
|
||||||
>
|
>
|
||||||
<X className="h-6 w-6" />
|
<X className="h-6 w-6" />
|
||||||
</button>
|
</button>
|
||||||
@@ -3633,23 +3735,11 @@ const ObjectEditorModal = ({ modal, onClose, onApply }) => {
|
|||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700 flex-shrink-0">
|
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700 flex-shrink-0">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<span
|
|
||||||
className={`text-sm ${isValid ? "text-green-600" : "text-red-600"}`}
|
{/* Buttons */}
|
||||||
>
|
<div className="flex justify-end space-x-3">
|
||||||
{isValid ? "✓ Valid" : "✗ Invalid"}{" "}
|
|
||||||
{modal.format.type.replace("_", " ")}
|
|
||||||
</span>
|
|
||||||
{isValid &&
|
|
||||||
structuredData &&
|
|
||||||
typeof structuredData === "object" && (
|
|
||||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
{Object.keys(structuredData).length} properties
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex space-x-3">
|
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
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"
|
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"
|
||||||
@@ -3661,7 +3751,7 @@ const ObjectEditorModal = ({ modal, onClose, onApply }) => {
|
|||||||
disabled={!isValid}
|
disabled={!isValid}
|
||||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 rounded-md transition-colors"
|
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 rounded-md transition-colors"
|
||||||
>
|
>
|
||||||
Apply Changes
|
Save Changes
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user