+
+
Structured Data Editor
-
@@ -524,6 +535,15 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
renderValue(value, key, `root.${key}`, 'root')
)
)}
+
+ {/* Root level Add Property button */}
+
);
diff --git a/src/components/ToolSidebar.js b/src/components/ToolSidebar.js
index 28964281..87ac8700 100644
--- a/src/components/ToolSidebar.js
+++ b/src/components/ToolSidebar.js
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
-import { Search, LinkIcon, Hash, FileSpreadsheet, Wand2, GitCompare, Home, ChevronLeft, ChevronRight, Type, Edit3 } from 'lucide-react';
+import { Search, LinkIcon, Hash, Table, Wand2, GitCompare, Home, ChevronLeft, ChevronRight, Type, Edit3 } from 'lucide-react';
const ToolSidebar = () => {
const location = useLocation();
@@ -10,9 +10,9 @@ const ToolSidebar = () => {
const tools = [
{ path: '/', name: 'Home', icon: Home, description: 'Back to homepage' },
{ path: '/object-editor', name: 'Object Editor', icon: Edit3, description: 'Visual editor for JSON & PHP objects' },
+ { path: '/table-editor', name: 'Table Editor', icon: Table, description: 'Import, edit & export tabular data' },
{ path: '/url', name: 'URL Tool', icon: LinkIcon, description: 'URL encode/decode' },
{ path: '/base64', name: 'Base64 Tool', icon: Hash, description: 'Base64 encode/decode' },
- { path: '/csv-json', name: 'CSV/JSON Tool', icon: FileSpreadsheet, description: 'Convert CSV β JSON' },
{ path: '/beautifier', name: 'Beautifier Tool', icon: Wand2, description: 'Beautify/minify code' },
{ path: '/diff', name: 'Diff Tool', icon: GitCompare, description: 'Compare text differences' },
{ path: '/text-length', name: 'Text Length Checker', icon: Type, description: 'Analyze text length & stats' },
diff --git a/src/index.css b/src/index.css
index 406f2e34..f0a6d109 100644
--- a/src/index.css
+++ b/src/index.css
@@ -34,4 +34,13 @@
.copy-button {
@apply absolute top-2 right-2 p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-md transition-colors duration-200;
}
+
+ .scrollbar-hide {
+ -ms-overflow-style: none; /* Internet Explorer 10+ */
+ scrollbar-width: none; /* Firefox */
+ }
+
+ .scrollbar-hide::-webkit-scrollbar {
+ display: none; /* Safari and Chrome */
+ }
}
diff --git a/src/index.js b/src/index.js
index 2cb1087e..89736454 100644
--- a/src/index.js
+++ b/src/index.js
@@ -2,6 +2,10 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
+import { initBrowserCompat } from './utils/browserCompat';
+
+// Initialize browser compatibility fixes before React renders
+initBrowserCompat();
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
diff --git a/src/pages/Home.js b/src/pages/Home.js
index 5ab40ec3..e5a8a845 100644
--- a/src/pages/Home.js
+++ b/src/pages/Home.js
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
-import { Search, Code, Link2, FileText, Hash, RefreshCw, GitCompare, Type, Edit3 } from 'lucide-react';
+import { Search, Code, Link2, FileText, Hash, RefreshCw, GitCompare, Type, Edit3, Table } from 'lucide-react';
import ToolCard from '../components/ToolCard';
const Home = () => {
@@ -14,6 +14,13 @@ const Home = () => {
path: '/object-editor',
tags: ['Visual', 'JSON', 'PHP', 'Objects', 'Editor']
},
+ {
+ icon: Table,
+ title: 'Table Editor',
+ description: 'Import, edit, and export tabular data from URLs, files, or paste CSV/JSON',
+ path: '/table-editor',
+ tags: ['Table', 'CSV', 'JSON', 'Data', 'Editor']
+ },
{
icon: Link2,
title: 'URL Encoder/Decoder',
@@ -28,13 +35,6 @@ const Home = () => {
path: '/base64',
tags: ['Base64', 'Encode', 'Binary']
},
- {
- icon: RefreshCw,
- title: 'CSV β JSON Converter',
- description: 'Convert between CSV and JSON formats with custom delimiters',
- path: '/csv-json',
- tags: ['CSV', 'JSON', 'Convert']
- },
{
icon: FileText,
title: 'Code Beautifier/Minifier',
diff --git a/src/pages/ObjectEditor.js b/src/pages/ObjectEditor.js
index 9e13eaed..1a8c836b 100644
--- a/src/pages/ObjectEditor.js
+++ b/src/pages/ObjectEditor.js
@@ -1,5 +1,5 @@
import React, { useState, useRef, useCallback } from 'react';
-import { Edit3, Upload, FileText, Map, Table, Globe } from 'lucide-react';
+import { Edit3, Upload, FileText, Workflow, Table, Globe, Plus, AlertTriangle, BrushCleaning, Code, Braces, Download } from 'lucide-react';
import ToolLayout from '../components/ToolLayout';
import CopyButton from '../components/CopyButton';
import StructuredEditor from '../components/StructuredEditor';
@@ -9,23 +9,143 @@ import PostmanTable from '../components/PostmanTable';
const ObjectEditor = () => {
console.log(' ObjectEditor component loaded successfully!');
const [structuredData, setStructuredData] = useState({});
- const [showInput, setShowInput] = useState(false);
+ const [activeTab, setActiveTab] = useState('create');
const [inputText, setInputText] = useState('');
const [inputFormat, setInputFormat] = useState('');
const [inputValid, setInputValid] = useState(false);
const [error, setError] = useState('');
const [viewMode, setViewMode] = useState('visual'); // 'visual', 'mindmap', 'table'
- const [showOutputs, setShowOutputs] = useState(false);
+ const [activeExportTab, setActiveExportTab] = useState('json');
+ const [jsonFormat, setJsonFormat] = useState('pretty'); // 'pretty' or 'minify'
const [outputs, setOutputs] = useState({
jsonPretty: '',
jsonMinified: '',
serialized: ''
});
- const [showFetch, setShowFetch] = useState(false);
const [fetchUrl, setFetchUrl] = useState('');
const [fetching, setFetching] = useState(false);
+ const [createNewCompleted, setCreateNewCompleted] = useState(false);
+ const [showInputChangeModal, setShowInputChangeModal] = useState(false);
+ const [pendingTabChange, setPendingTabChange] = useState(null);
const fileInputRef = useRef(null);
+ // Helper function to check if user has data that would be lost
+ const hasUserData = () => {
+ return Object.keys(structuredData).length > 0;
+ };
+
+ // Check if current data has been modified from initial state
+ const hasModifiedData = () => {
+ // Check if there's actual user data (not just initial empty structure)
+ if (Object.keys(structuredData).length === 0) {
+ return false;
+ }
+
+ // Check if it's the initial empty property (unchanged)
+ const isInitialEmpty = JSON.stringify(structuredData) === JSON.stringify({ "": "" });
+ if (isInitialEmpty) {
+ return false;
+ }
+
+ // Check if it's the sample data (unchanged)
+ const isSampleData = JSON.stringify(structuredData) === JSON.stringify({
+ name: "John Doe",
+ age: 30,
+ email: "john@example.com",
+ address: {
+ street: "123 Main St",
+ city: "New York",
+ country: "USA"
+ },
+ hobbies: ["reading", "coding", "traveling"]
+ });
+
+ if (isSampleData) {
+ return false;
+ }
+
+ // Any other data is considered modified
+ return true;
+ };
+
+ // Handle tab change with confirmation if data exists
+ const handleTabChange = (newTab) => {
+ // For Create New tab, use more specific logic
+ if (newTab === 'create' && activeTab !== 'create') {
+ if (hasModifiedData()) {
+ setPendingTabChange(newTab);
+ setShowInputChangeModal(true);
+ } else {
+ setActiveTab(newTab);
+ setCreateNewCompleted(false);
+ }
+ } else if (hasUserData() && activeTab !== newTab) {
+ setPendingTabChange(newTab);
+ setShowInputChangeModal(true);
+ } else {
+ // No data or same tab, proceed directly
+ setActiveTab(newTab);
+ // If clicking Create New again after completion, show the options again
+ if (newTab === 'create' && createNewCompleted) {
+ setCreateNewCompleted(false);
+ }
+ }
+ };
+
+ // Clear all data function
+ const clearAllData = () => {
+ setStructuredData({});
+ setInputText('');
+ setInputFormat('');
+ setInputValid(false);
+ setError('');
+ setCreateNewCompleted(false);
+ };
+
+ // Create initial empty object with one property
+ const createInitialEmptyObject = () => {
+ return { "": "" };
+ };
+
+ // Confirm input method change and clear data
+ const confirmInputChange = () => {
+ // Handle special Create New button actions
+ if (pendingTabChange === 'create_empty') {
+ clearAllData();
+ // Start with one empty property like TableEditor starts with empty row
+ setStructuredData({ "": "" });
+ setCreateNewCompleted(true);
+ } else if (pendingTabChange === 'create_sample') {
+ clearAllData();
+ // Load sample data
+ const sampleData = {
+ name: "John Doe",
+ age: 30,
+ email: "john@example.com",
+ address: {
+ street: "123 Main St",
+ city: "New York",
+ country: "USA"
+ },
+ hobbies: ["reading", "coding", "traveling"]
+ };
+ setStructuredData(sampleData);
+ setCreateNewCompleted(true);
+ } else {
+ // Handle regular tab switches
+ clearAllData();
+ setActiveTab(pendingTabChange);
+ // If switching to create tab, reset completion state to show options
+ if (pendingTabChange === 'create') {
+ setCreateNewCompleted(false);
+ }
+ }
+
+ // Close modal
+ setShowInputChangeModal(false);
+ setPendingTabChange(null);
+ };
+
// PHP serialize implementation (reused from SerializeTool)
const phpSerialize = useCallback((data) => {
if (data === null) return 'N;';
@@ -250,6 +370,7 @@ const ObjectEditor = () => {
console.log(' SETTING STRUCTURED DATA:', detection.data);
setStructuredData(detection.data);
setError('');
+ setCreateNewCompleted(true);
} else if (value.trim()) {
setError('Invalid format. Please enter valid JSON or PHP serialized data.');
} else {
@@ -263,22 +384,75 @@ const ObjectEditor = () => {
generateOutputs(newData);
};
- // Generate all output formats
+ // Check if data is just the initial empty property
+ const isInitialEmptyData = (data) => {
+ return JSON.stringify(data) === JSON.stringify({ "": "" });
+ };
+
+ // Simple PHP serialization function
+ const serializeToPhp = (data) => {
+ if (data === null) return 'N;';
+ if (data === undefined) return 'N;';
+ if (typeof data === 'boolean') return data ? 'b:1;' : 'b:0;';
+ if (typeof data === 'number') {
+ return Number.isInteger(data) ? `i:${data};` : `d:${data};`;
+ }
+ if (typeof data === 'string') {
+ return `s:${data.length}:"${data}";`;
+ }
+ if (Array.isArray(data)) {
+ let result = `a:${data.length}:{`;
+ data.forEach((item, index) => {
+ result += `i:${index};${serializeToPhp(item)}`;
+ });
+ result += '}';
+ return result;
+ }
+ if (typeof data === 'object') {
+ const keys = Object.keys(data);
+ let result = `a:${keys.length}:{`;
+ keys.forEach(key => {
+ result += `s:${key.length}:"${key}";${serializeToPhp(data[key])}`;
+ });
+ result += '}';
+ return result;
+ }
+ return 'N;';
+ };
+
+ // Generate output formats
const generateOutputs = useCallback((data) => {
+ // For initial empty property, show empty object outputs
+ if (!data || Object.keys(data).length === 0 || isInitialEmptyData(data)) {
+ setOutputs({
+ jsonPretty: '{}',
+ jsonMinified: '{}',
+ serialized: 'a:0:{}'
+ });
+ return;
+ }
+
try {
const jsonPretty = JSON.stringify(data, null, 2);
const jsonMinified = JSON.stringify(data);
- const serialized = phpSerialize(data);
+
+ // Simple PHP serialization for basic data types
+ const serialized = serializeToPhp(data);
setOutputs({
jsonPretty,
jsonMinified,
serialized
});
- } catch (err) {
- console.error('Error generating outputs:', err);
+ } catch (error) {
+ console.error('Error generating outputs:', error);
+ setOutputs({
+ jsonPretty: 'Error generating JSON',
+ jsonMinified: 'Error generating JSON',
+ serialized: 'Error generating PHP serialized data'
+ });
}
- }, [phpSerialize]);
+ }, []);
// Handle file import
const handleFileImport = (event) => {
@@ -289,7 +463,7 @@ const ObjectEditor = () => {
const content = e.target.result;
setInputText(content);
handleInputChange(content);
- setShowInput(true);
+ setCreateNewCompleted(true);
};
reader.readAsText(file);
}
@@ -374,8 +548,7 @@ const ObjectEditor = () => {
setInputText(JSON.stringify(data, null, 2));
setInputFormat('JSON');
setInputValid(true);
- setShowInput(false); // Hide input on successful fetch
- setShowFetch(false);
+ setCreateNewCompleted(true);
} catch {
throw new Error('Response is not valid JSON. Content-Type: ' + (contentType || 'unknown'));
}
@@ -386,8 +559,7 @@ const ObjectEditor = () => {
setInputText(JSON.stringify(data, null, 2));
setInputFormat('JSON');
setInputValid(true);
- setShowInput(false); // Hide input on successful fetch
- setShowFetch(false);
+ setCreateNewCompleted(true);
}
} catch (err) {
console.error('Fetch error:', err);
@@ -412,280 +584,625 @@ const ObjectEditor = () => {
description="Visual editor for JSON and PHP serialized objects with format conversion"
icon={Edit3}
>
- {/* Input Controls */}
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Fetch URL Input */}
- {showFetch && (
-
-
-
-
setFetchUrl(e.target.value)}
- placeholder="https://api.telegram.org/bot
/getMe"
- className="tool-input flex-1"
- onKeyPress={(e) => e.key === 'Enter' && handleFetchData()}
- />
-
+ {/* Input Section with Tabs */}
+
+ {/* Tabs */}
+
+
+
+
+
+
-
- Enter any URL that returns JSON data. Examples: Telegram Bot API, JSONPlaceholder, GitHub API, etc.
-
- )}
- {/* View Mode Toggle - Desktop Tabs */}
-
-
-
-
-
+ {/* Tab Content */}
+ {(activeTab !== 'create' || !createNewCompleted) && (
+
+ {/* Create New Tab Content */}
+ {activeTab === 'create' && !createNewCompleted && (
+
+
+
+ Create New Object
+
+
+ Choose how you'd like to begin working with your data
+
+
+
+
+
+
+
+
+
+
+
+ π‘ Tip: You can always import data later using the URL, Paste, or Open tabs, or start editing directly in the visual editor below.
+
+
+
+ )}
- {/* View Mode Toggle - Mobile Select */}
-
-
-
-
+ {/* URL Tab Content */}
+ {activeTab === 'url' && (
+
+
+
+ setFetchUrl(e.target.value)}
+ placeholder="https://api.telegram.org/bot/getMe"
+ className="tool-input w-full"
+ onKeyPress={(e) => e.key === 'Enter' && handleFetchData()}
+ />
+
+
+
+
+ Enter any URL that returns JSON data. Examples: Telegram Bot API, JSONPlaceholder, GitHub API, etc.
+
+
+ )}
- {/* Input Section */}
- {showInput && (
-
-
-
- {inputFormat && (
-
- {inputFormat} {inputValid ? 'β' : 'β'}
-
+ {/* Paste Tab Content */}
+ {activeTab === 'paste' && (
+
+
+ {inputFormat && (
+
+ {inputFormat} {inputValid ? 'β' : 'β'}
+
+ )}
+
+
+ )}
+
+ {/* Open Tab Content */}
+ {activeTab === 'open' && (
+
+
+
+
+
+
+
+
+
+ Privacy: Your data stays in your browser. We don't store or upload anything - just help you open, edit, and export your files locally.
+
+
+
)}
-
+
+
+ {/* Main Editor Section - Only show if createNewCompleted or not on create tab */}
+ {(activeTab !== 'create' || createNewCompleted) && (
+
+ {/* Editor Header */}
+
+
+
+
+ Object Editor
+
+
+ {/* View Mode Tabs - Moved to right */}
+
+
+
+
+
+
+
+
+ {/* Editor Content */}
+
+ {Object.keys(structuredData).length === 0 ? (
+
+
+
+ No Object Data
+
+
+ Load data using the input methods above to start editing
+
+
+ ) : (
+ <>
+ {viewMode === 'visual' && (
+
+ )}
+
+ {viewMode === 'mindmap' && (
+
+ )}
+
+ {viewMode === 'table' && (
+
+ )}
+ >
)}
+
)}
- {/* Main Editor Area */}
-
-
- {viewMode === 'visual' && 'Visual Editor'}
- {viewMode === 'mindmap' && 'Mindmap Visualization'}
- {viewMode === 'table' && 'Table View'}
-
-
- {viewMode === 'visual' && (
-
-
-
+ {/* Export Section - Only show if createNewCompleted or not on create tab */}
+ {(activeTab !== 'create' || createNewCompleted) && Object.keys(structuredData).length > 0 && (
+
+ {/* Export Header */}
+
+
+
+
+ Export Results
+
+
+ Object: {Object.keys(structuredData).length} properties
+
- )}
-
- {viewMode === 'mindmap' && (
-
- )}
-
- {viewMode === 'table' && (
-
- )}
-
-
- {/* Toggle Output Button */}
-
-
-
+ {/* Export Tabs */}
+
+
+
+
- {/* Output Actions */}
- {showOutputs && (
-
- {/* JSON Pretty */}
-
-
-
-
- {outputs.jsonPretty &&
}
+ {/* Export Content */}
+
+ {activeExportTab === 'json' && (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {activeExportTab === 'php' && (
+
+
+
+
+
+
+
+ )}
+ )}
- {/* JSON Minified */}
-
-
-
-
- {outputs.jsonMinified && }
-
-
-
- {/* PHP Serialized */}
-
-
-
-
- {outputs.serialized && }
-
-
-
+ {/* Input Method Change Confirmation Modal */}
+ {showInputChangeModal && (
+
{
+ setShowInputChangeModal(false);
+ setPendingTabChange(null);
+ // If user cancels while on Create New tab with modified data, hide the tab content
+ if (activeTab === 'create' && hasModifiedData()) {
+ setCreateNewCompleted(true);
+ }
+ }}
+ />
)}
{/* Usage Tips */}
-
Usage Tips
-
- - β’ Visual Editor: Create and modify object structures with forms
- - β’ Mindmap View: Visualize complex JSON structures as interactive diagrams
- - β’ Table View: Browse data like Postman - click arrays for horizontal tables, objects for key-value pairs
- - β’ Navigation: Use breadcrumbs and Back button to navigate through nested data structures
- - β’ Input Data: Paste JSON/PHP serialized data with auto-detection in the input field
- - β’ Fetch Data: Load JSON from any URL - perfect for APIs like Telegram Bot, GitHub, JSONPlaceholder
- - β’ Import data from files or use the sample data to get started
- - β’ Toggle output formats visibility with the "Show/Hide Output Formats" button
- - β’ Export your data in any format: JSON pretty, minified, or PHP serialized
-
+
Usage Tips
+
+
+
π Input Methods:
+
+ - Create New: Start empty or load sample data to explore features
+ - URL Import: Fetch data directly from JSON APIs and endpoints
+ - Paste Data: Auto-detects JSON and PHP serialized formats
+ - Open Files: Import .json and .txt files (multi-format supported)
+
+
+
+
+
π― Editing Modes:
+
+ - Visual Editor: Create and modify object structures with forms
+ - Mindmap View: Visualize complex JSON structures as interactive diagrams
+ - Table View: Browse data like Postman - click arrays for horizontal tables, objects for key-value pairs
+
+
+
+
+
π Navigation & Features:
+
+ - Navigation: Use breadcrumbs and Back button to navigate through nested data structures
+ - Add/Delete: Use buttons to add new properties or select multiple to delete
+ - Search & Sort: Filter data and sort by any column
+
+
+
+
+
π€ Export Options:
+
+ - JSON: Export includes pretty-formatted and minified versions
+ - PHP Serialize: Perfect for PHP applications and data exchange
+ - Copy & Download: All formats support both clipboard copy and file download
+
+
+
);
};
+// Input Method Change Confirmation Modal Component
+const InputChangeConfirmationModal = ({ objectData, currentMethod, newMethod, onConfirm, onCancel }) => {
+ const getMethodName = (method) => {
+ switch (method) {
+ case 'create': return 'Create New';
+ case 'create_empty': return 'Start Empty';
+ case 'create_sample': return 'Load Sample';
+ case 'url': return 'URL Import';
+ case 'paste': return 'Paste Data';
+ case 'open': return 'File Upload';
+ default: return method;
+ }
+ };
+
+ const objectSize = Object.keys(objectData).length;
+ const hasNestedData = Object.values(objectData).some(value =>
+ typeof value === 'object' && value !== null
+ );
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+
+ Change Input Method
+
+
+ This will clear all current data
+
+
+
+
+
+ {/* Content */}
+
+
+ {(newMethod === 'create_empty' || newMethod === 'create_sample') ? (
+ <>Using {getMethodName(newMethod)} will clear all current data.>
+ ) : (
+ <>Switching from {getMethodName(currentMethod)} to {getMethodName(newMethod)} will clear all current data.>
+ )}
+
+
+
+
+ This will permanently delete:
+
+
+ - β’ Object with {objectSize} properties
+ {hasNestedData && - β’ All nested objects and arrays
}
+ - β’ All modifications and edits
+
+
+
+
+
+
+
+ Tip: Consider exporting your current data before switching methods to avoid losing your work.
+
+
+
+
+
+ {/* Footer */}
+
+
+
+
+
+
+
+
+ );
+};
+
export default ObjectEditor;
diff --git a/src/pages/TableEditor.js b/src/pages/TableEditor.js
new file mode 100644
index 00000000..398ea429
--- /dev/null
+++ b/src/pages/TableEditor.js
@@ -0,0 +1,3637 @@
+import React, { useState, useEffect } from "react";
+import {
+ Table,
+ Database,
+ Download,
+ Upload,
+ FileText,
+ Search,
+ Plus,
+ Minus,
+ X,
+ Edit,
+ Braces,
+ Code,
+ Eye,
+ Columns,
+ Trash2,
+ ArrowUpDown,
+ Edit3,
+ Globe,
+ Maximize2,
+ Minimize2,
+ BrushCleaning,
+ AlertTriangle,
+} from "lucide-react";
+import ToolLayout from "../components/ToolLayout";
+import StructuredEditor from "../components/StructuredEditor";
+import Papa from "papaparse";
+
+const TableEditor = () => {
+ const [data, setData] = useState([]);
+ const [columns, setColumns] = useState([]);
+ const [inputText, setInputText] = useState("");
+ const [url, setUrl] = useState("");
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState("");
+ const [useFirstRowAsHeader, setUseFirstRowAsHeader] = useState(true);
+ const [searchTerm, setSearchTerm] = useState("");
+ const [sortConfig, setSortConfig] = useState({ key: null, direction: "asc" });
+ const [editingCell, setEditingCell] = useState(null);
+ const [selectedRows, setSelectedRows] = useState(new Set());
+ const [activeTab, setActiveTab] = useState("create");
+ const [selectedColumns, setSelectedColumns] = useState(new Set());
+ const [editingHeader, setEditingHeader] = useState(null);
+ const [objectEditorModal, setObjectEditorModal] = useState(null);
+ const [exportTab, setExportTab] = useState("json");
+ const [jsonFormat, setJsonFormat] = useState("pretty"); // 'pretty' or 'minify'
+
+ // Multi-table management state
+ const [tableRegistry, setTableRegistry] = useState({}); // { tableName: { data, columns, modified, originalSchema, originalData } }
+ const [availableTables, setAvailableTables] = useState([]); // List of discovered table names
+ const [currentTable, setCurrentTable] = useState(""); // Currently active table name
+ const [originalFileName, setOriginalFileName] = useState(""); // For export naming
+ const [editingTableName, setEditingTableName] = useState(false); // For single table name editing
+ const [isTableFullscreen, setIsTableFullscreen] = useState(false); // For fullscreen table view
+ const [frozenColumns, setFrozenColumns] = useState(0); // Number of columns to freeze on horizontal scroll
+ const [showClearConfirmModal, setShowClearConfirmModal] = useState(false); // For clear confirmation modal
+ const [showInputChangeModal, setShowInputChangeModal] = useState(false); // For input method change confirmation
+ const [pendingTabChange, setPendingTabChange] = useState(null); // Store pending tab change
+ const [createNewCompleted, setCreateNewCompleted] = useState(false); // Track if user completed Create New step
+
+ // SQL Export specific state
+ const [sqlTableName, setSqlTableName] = useState(""); // Table name for SQL export
+ const [sqlPrimaryKey, setSqlPrimaryKey] = useState(""); // Primary key column for SQL export
+
+ // Helper function to check if user has data that would be lost
+ const hasUserData = () => {
+ // Check if there are multiple tables (imported data)
+ if (availableTables.length > 0) {
+ return true;
+ }
+
+ // Check if there's actual user data (not just initial empty structure)
+ if (data.length === 0) {
+ return false;
+ }
+
+ // Check if it's just the initial empty table structure
+ const isInitialEmptyTable =
+ data.length === 1 &&
+ columns.length === 3 &&
+ columns.some((col) => col.name === "id") &&
+ columns.some((col) => col.name === "name") &&
+ columns.some((col) => col.name === "value") &&
+ data[0] &&
+ Object.values(data[0]).every((val) => val === "" || val === data[0].id);
+
+ if (isInitialEmptyTable) {
+ return false;
+ }
+
+ // Check if all rows are empty
+ const hasNonEmptyData = data.some((row) =>
+ Object.entries(row).some(
+ ([key, value]) =>
+ key !== "id" && value !== null && value !== undefined && value !== "",
+ ),
+ );
+
+ return hasNonEmptyData;
+ };
+
+ // Check if current data has been modified from initial state
+ const hasModifiedData = () => {
+ // Check if there are multiple tables (imported data)
+ if (availableTables.length > 0) {
+ return true;
+ }
+
+ // Check if there's actual user data (not just initial empty structure)
+ if (data.length === 0) {
+ return false;
+ }
+
+ // Check if it's just the initial empty table structure
+ const isInitialEmptyTable =
+ data.length === 1 &&
+ columns.length === 3 &&
+ columns.some((col) => col.name === "id") &&
+ columns.some((col) => col.name === "name") &&
+ columns.some((col) => col.name === "value") &&
+ data[0] &&
+ Object.values(data[0]).every((val) => val === "" || val === data[0].id);
+
+ if (isInitialEmptyTable) {
+ return false;
+ }
+
+ // Check if it's the sample data (unchanged)
+ const isSampleData =
+ data.length === 4 &&
+ columns.length === 5 &&
+ columns.some((col) => col.name === "id") &&
+ columns.some((col) => col.name === "name") &&
+ columns.some((col) => col.name === "email") &&
+ columns.some((col) => col.name === "age") &&
+ columns.some((col) => col.name === "city") &&
+ currentTable === "sample_data";
+
+ if (isSampleData) {
+ // Check if sample data is unchanged
+ const expectedSampleData = [
+ {
+ id: 1,
+ name: "John Doe",
+ email: "john@example.com",
+ age: 30,
+ city: "New York",
+ },
+ {
+ id: 2,
+ name: "Jane Smith",
+ email: "jane@example.com",
+ age: 25,
+ city: "Los Angeles",
+ },
+ {
+ id: 3,
+ name: "Bob Johnson",
+ email: "bob@example.com",
+ age: 35,
+ city: "Chicago",
+ },
+ {
+ id: 4,
+ name: "Alice Brown",
+ email: "alice@example.com",
+ age: 28,
+ city: "Houston",
+ },
+ ];
+
+ const isUnchangedSample = data.every((row, index) => {
+ const expected = expectedSampleData[index];
+ return (
+ expected &&
+ row.col_0 == expected.id &&
+ row.col_1 === expected.name &&
+ row.col_2 === expected.email &&
+ row.col_3 == expected.age &&
+ row.col_4 === expected.city
+ );
+ });
+
+ return !isUnchangedSample;
+ }
+
+ // Any other data is considered modified
+ return true;
+ };
+
+ // Handle tab change with confirmation if data exists
+ const handleTabChange = (newTab) => {
+ // For Create New tab, use more specific logic
+ if (newTab === "create" && activeTab !== "create") {
+ if (hasModifiedData()) {
+ setPendingTabChange(newTab);
+ setShowInputChangeModal(true);
+ } else {
+ setActiveTab(newTab);
+ setCreateNewCompleted(false);
+ }
+ } else if (hasUserData() && activeTab !== newTab) {
+ setPendingTabChange(newTab);
+ setShowInputChangeModal(true);
+ } else {
+ // No data or same tab, proceed directly
+ setActiveTab(newTab);
+ // If clicking Create New again after completion, show the options again
+ if (newTab === "create" && createNewCompleted) {
+ setCreateNewCompleted(false);
+ }
+ // Don't auto-create table for 'create' tab - let user choose Start Empty or Load Sample
+ }
+ };
+
+ // Clear all data function
+ const clearAllData = () => {
+ setData([]);
+ setColumns([]);
+ setTableRegistry({});
+ setAvailableTables([]);
+ setCurrentTable("");
+ setOriginalFileName("");
+ setCreateNewCompleted(false);
+ };
+
+ // Confirm input method change and clear data
+ const confirmInputChange = () => {
+ // Handle special Create New button actions
+ if (pendingTabChange === "create_empty") {
+ clearAllData();
+ createEmptyTable();
+ setCreateNewCompleted(true);
+ } else if (pendingTabChange === "create_sample") {
+ clearAllData();
+ // Load sample data
+ const sampleData = [
+ {
+ id: 1,
+ name: "John Doe",
+ email: "john@example.com",
+ age: 30,
+ city: "New York",
+ },
+ {
+ id: 2,
+ name: "Jane Smith",
+ email: "jane@example.com",
+ age: 25,
+ city: "Los Angeles",
+ },
+ {
+ id: 3,
+ name: "Bob Johnson",
+ email: "bob@example.com",
+ age: 35,
+ city: "Chicago",
+ },
+ {
+ id: 4,
+ name: "Alice Brown",
+ email: "alice@example.com",
+ age: 28,
+ city: "Houston",
+ },
+ ];
+
+ const sampleColumns = [
+ { id: "col_0", name: "id" },
+ { id: "col_1", name: "name" },
+ { id: "col_2", name: "email" },
+ { id: "col_3", name: "age" },
+ { id: "col_4", name: "city" },
+ ];
+
+ const formattedData = sampleData.map((row, index) => ({
+ id: `row_${index}`,
+ col_0: row.id,
+ col_1: row.name,
+ col_2: row.email,
+ col_3: row.age,
+ col_4: row.city,
+ }));
+
+ setData(formattedData);
+ setColumns(sampleColumns);
+ setCurrentTable("sample_data");
+ setOriginalFileName("sample_data");
+ setCreateNewCompleted(true);
+ } else {
+ // Handle regular tab switches
+ clearAllData();
+ setActiveTab(pendingTabChange);
+ // If switching to create tab, reset completion state to show options
+ if (pendingTabChange === "create") {
+ setCreateNewCompleted(false);
+ }
+ }
+
+ // Close modal
+ setShowInputChangeModal(false);
+ setPendingTabChange(null);
+ };
+
+ // SQL parsing functions for multi-table support
+ const parseMultiTableSQL = (sqlContent) => {
+ console.log(
+ "π parseMultiTableSQL called with content length:",
+ sqlContent.length,
+ );
+ const tables = {};
+ const lines = sqlContent.split("\n");
+ let currentTable = null;
+ let currentSchema = "";
+ let insideCreateTable = false;
+ let insideInsert = false;
+
+ console.log("π Total lines to process:", lines.length);
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i].trim();
+
+ // Detect CREATE TABLE statements
+ const createTableMatch = line.match(/CREATE TABLE\s+`?(\w+)`?\s*\(/i);
+ if (createTableMatch) {
+ currentTable = createTableMatch[1];
+ currentSchema = line + "\n";
+ insideCreateTable = true;
+
+ if (!tables[currentTable]) {
+ tables[currentTable] = {
+ name: currentTable,
+ schema: "",
+ data: [],
+ columns: [],
+ rowCount: 0,
+ };
+ }
+ continue;
+ }
+
+ // Continue collecting CREATE TABLE schema
+ if (insideCreateTable) {
+ currentSchema += line + "\n";
+ if (line.includes(");") || line.includes(") ENGINE")) {
+ insideCreateTable = false;
+ if (currentTable && tables[currentTable]) {
+ tables[currentTable].schema = currentSchema;
+ }
+ }
+ continue;
+ }
+
+ // Detect INSERT statements
+ const insertMatch = line.match(
+ /INSERT INTO\s+`?(\w+)`?\s*(?:\([^)]+\))?\s*VALUES/i,
+ );
+ if (insertMatch) {
+ currentTable = insertMatch[1];
+ insideInsert = true;
+
+ if (!tables[currentTable]) {
+ tables[currentTable] = {
+ name: currentTable,
+ schema: "",
+ data: [],
+ columns: [],
+ rowCount: 0,
+ };
+ }
+
+ // Extract column names from INSERT statement
+ const columnsMatch = line.match(/INSERT INTO\s+`?\w+`?\s*\(([^)]+)\)/i);
+ if (columnsMatch && tables[currentTable].columns.length === 0) {
+ const columnNames = columnsMatch[1]
+ .split(",")
+ .map((col) => col.trim().replace(/`/g, ""));
+
+ tables[currentTable].columns = columnNames.map((name, index) => ({
+ id: `col_${index}`,
+ name: name,
+ }));
+ }
+
+ // Extract VALUES data
+ const valuesMatch = line.match(/VALUES\s*(.+)/i);
+ if (valuesMatch) {
+ const valuesStr = valuesMatch[1];
+ console.log(
+ "π Found VALUES in multi-table parser:",
+ valuesStr.substring(0, 100) + "...",
+ );
+ const rows = parseInsertValues(valuesStr);
+ console.log("π parseInsertValues returned", rows.length, "rows");
+ tables[currentTable].data.push(...rows);
+ tables[currentTable].rowCount += rows.length;
+ }
+ continue;
+ }
+
+ // Continue collecting INSERT data from multi-line statements
+ if (
+ insideInsert &&
+ currentTable &&
+ line.includes("(") &&
+ line.includes(")")
+ ) {
+ const rows = parseInsertValues(line);
+ tables[currentTable].data.push(...rows);
+ tables[currentTable].rowCount += rows.length;
+
+ if (line.endsWith(";")) {
+ insideInsert = false;
+ }
+ }
+ }
+
+ return tables;
+ };
+
+ const parseInsertValues = (valuesStr) => {
+ console.log(
+ "π parseInsertValues called with:",
+ valuesStr.substring(0, 200) + "...",
+ );
+ const rows = [];
+
+ // Better parsing: find complete tuples first, then parse values respecting quotes
+ const tuples = [];
+ let currentTuple = "";
+ let parenCount = 0;
+ let inString = false;
+ let stringChar = "";
+
+ for (let i = 0; i < valuesStr.length; i++) {
+ const char = valuesStr[i];
+ const prevChar = i > 0 ? valuesStr[i - 1] : "";
+
+ if (!inString && (char === '"' || char === "'")) {
+ inString = true;
+ stringChar = char;
+ } else if (inString && char === stringChar && prevChar !== "\\") {
+ inString = false;
+ stringChar = "";
+ }
+
+ if (!inString) {
+ if (char === "(") parenCount++;
+ if (char === ")") parenCount--;
+ }
+
+ currentTuple += char;
+
+ if (!inString && parenCount === 0 && char === ")") {
+ tuples.push(currentTuple.trim());
+ currentTuple = "";
+ // Skip comma and whitespace
+ while (
+ i + 1 < valuesStr.length &&
+ (valuesStr[i + 1] === "," || valuesStr[i + 1] === " ")
+ ) {
+ i++;
+ }
+ }
+ }
+
+ tuples.forEach((tuple) => {
+ console.log("π Processing tuple:", tuple.substring(0, 100) + "...");
+
+ // Remove outer parentheses
+ const innerContent = tuple.replace(/^\(|\)$/g, "").trim();
+
+ // Parse values respecting quotes and nested structures
+ const values = [];
+ let currentValue = "";
+ let inString = false;
+ let stringChar = "";
+ let parenCount = 0;
+
+ for (let i = 0; i < innerContent.length; i++) {
+ const char = innerContent[i];
+ const prevChar = i > 0 ? innerContent[i - 1] : "";
+
+ if (!inString && (char === '"' || char === "'")) {
+ inString = true;
+ stringChar = char;
+ currentValue += char;
+ continue;
+ } else if (inString && char === stringChar && prevChar !== "\\") {
+ inString = false;
+ stringChar = "";
+ currentValue += char;
+ continue;
+ }
+
+ if (!inString) {
+ if (char === "(" || char === "{" || char === "[") parenCount++;
+ if (char === ")" || char === "}" || char === "]") parenCount--;
+
+ if (char === "," && parenCount === 0) {
+ values.push(currentValue.trim());
+ currentValue = "";
+ continue;
+ }
+ }
+
+ currentValue += char;
+ }
+
+ if (currentValue.trim()) {
+ values.push(currentValue.trim());
+ }
+
+ console.log("π Parsed", values.length, "values from tuple");
+
+ const processedValues = values.map((val) => {
+ val = val.trim();
+ console.log(
+ "π§ Processing value:",
+ val.substring(0, 50) + (val.length > 50 ? "..." : ""),
+ );
+ // Remove quotes and handle NULL
+ if (val === "NULL") return "";
+ if (val.startsWith("'") && val.endsWith("'")) {
+ let unquoted = val.slice(1, -1);
+ console.log(
+ "π€ Unquoted value:",
+ unquoted.substring(0, 50) + (unquoted.length > 50 ? "..." : ""),
+ );
+
+ // Handle escaped JSON properly (same logic as main parser)
+ if (unquoted.includes('\\"') || unquoted.includes("\\'")) {
+ console.log(
+ "π§ Value contains escaped quotes, checking if JSON...",
+ );
+
+ // Try to detect if this is escaped JSON
+ const isEscapedJson = (str) => {
+ const unescaped = str.replace(/\\"/g, '"').replace(/\\'/g, "'");
+ const trimmed = unescaped.trim();
+ console.log(
+ "π§ͺ Testing unescaped value:",
+ trimmed.substring(0, 50) + (trimmed.length > 50 ? "..." : ""),
+ );
+
+ if (
+ (trimmed.startsWith("{") && trimmed.endsWith("}")) ||
+ (trimmed.startsWith("[") && trimmed.endsWith("]"))
+ ) {
+ try {
+ JSON.parse(trimmed);
+ console.log("β
Valid JSON detected in parseInsertValues!");
+ return true;
+ } catch (e) {
+ console.log("β JSON parse failed:", e.message);
+ return false;
+ }
+ }
+ console.log("β Not JSON format (no brackets)");
+ return false;
+ };
+
+ // If it's escaped JSON, unescape it
+ if (isEscapedJson(unquoted)) {
+ const oldValue = unquoted;
+ unquoted = unquoted.replace(/\\"/g, '"').replace(/\\'/g, "'");
+ console.log(
+ "π― Unescaped JSON in parseInsertValues from:",
+ oldValue.substring(0, 30),
+ "to:",
+ unquoted.substring(0, 30),
+ );
+ } else {
+ // Only unescape single quotes for non-JSON strings
+ unquoted = unquoted.replace(/''/g, "'");
+ }
+ } else {
+ // Only unescape single quotes, preserve JSON structure
+ unquoted = unquoted.replace(/''/g, "'");
+ }
+
+ console.log(
+ "β
Final unquoted value:",
+ unquoted.substring(0, 50) + (unquoted.length > 50 ? "..." : ""),
+ );
+ return unquoted;
+ }
+ return val;
+ });
+
+ const rowObj = {};
+ processedValues.forEach((value, index) => {
+ rowObj[`col_${index}`] = value;
+ });
+ rowObj.id = Date.now() + Math.random(); // Generate unique ID
+ rows.push(rowObj);
+ });
+
+ console.log("π parseInsertValues returning", rows.length, "rows");
+ return rows;
+ };
+
+ // Detect structured data formats in cell values
+ const detectCellFormat = (value) => {
+ if (!value || typeof value !== "string" || value.length < 2) return null;
+
+ const trimmed = value.trim();
+
+ // JSON Object detection
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
+ try {
+ JSON.parse(trimmed);
+ return { type: "json", subtype: "object", icon: Braces };
+ } catch {
+ return null; // If it's not valid JSON, don't treat it as JSON
+ }
+ }
+
+ // JSON Array detection
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
+ try {
+ JSON.parse(trimmed);
+ return { type: "json", subtype: "array", icon: Braces };
+ } catch {
+ return null; // If it's not valid JSON, don't treat it as JSON
+ }
+ }
+
+ // PHP Serialized detection
+ if (trimmed.match(/^[aOs]:\d+:/)) {
+ return { type: "php_serialized", subtype: "serialized", icon: Code };
+ }
+
+ // Base64 JSON detection (common in APIs)
+ if (trimmed.match(/^[A-Za-z0-9+/]+=*$/) && trimmed.length > 20) {
+ try {
+ const decoded = atob(trimmed);
+ JSON.parse(decoded);
+ return { type: "base64_json", subtype: "encoded", icon: Eye };
+ } catch {}
+ }
+
+ return null;
+ };
+
+ // Open Object Editor modal for structured data
+ const openObjectEditor = (rowId, columnId, value, format) => {
+ const row = data.find((r) => r.id === rowId);
+ const column = columns.find((c) => c.id === columnId);
+
+ // Value should already be properly unescaped by the SQL parser
+ setObjectEditorModal({
+ rowId,
+ columnId,
+ rowIndex: data.findIndex((r) => r.id === rowId) + 1,
+ columnName: column?.name || "Unknown",
+ originalValue: value,
+ currentValue: value,
+ format,
+ isValid: true,
+ });
+ };
+
+ // Close Object Editor modal
+ const closeObjectEditor = () => {
+ setObjectEditorModal(null);
+ };
+
+ // Apply changes from Object Editor back to table
+ const applyObjectEditorChanges = (newValue) => {
+ if (!objectEditorModal) return;
+
+ const { rowId, columnId } = objectEditorModal;
+
+ setData(
+ data.map((row) =>
+ row.id === rowId ? { ...row, [columnId]: newValue } : row,
+ ),
+ );
+
+ closeObjectEditor();
+ };
+
+ // Parse CSV/TSV data
+ const parseData = (text, hasHeaders = true) => {
+ try {
+ const result = Papa.parse(text.trim(), {
+ header: false,
+ skipEmptyLines: true,
+ delimiter: text.includes("\t") ? "\t" : ",",
+ });
+
+ if (result.errors.length > 0) {
+ 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}`);
+ }
+ };
+
+ // Parse SQL data
+ const parseSqlData = (text) => {
+ console.log("π parseSqlData called with text length:", text.length);
+ console.log("π First 500 chars:", text.substring(0, 500));
+ try {
+ // Clean the SQL text - remove comments and unnecessary lines
+ const cleanLines = text
+ .split("\n")
+ .map((line) => line.trim())
+ .filter(
+ (line) =>
+ line &&
+ !line.startsWith("--") &&
+ !line.startsWith("/*") &&
+ !line.startsWith("*/") &&
+ !line.startsWith("SET ") &&
+ !line.startsWith("START ") &&
+ !line.startsWith("COMMIT") &&
+ !line.startsWith("/*!") &&
+ !line.startsWith("CREATE ") &&
+ !line.startsWith("ALTER ") &&
+ !line.startsWith("DROP ") &&
+ line !== ";",
+ );
+
+ // Join lines and split by semicolons to handle multi-line INSERT statements
+ const cleanText = cleanLines.join(" ");
+ const statements = cleanText
+ .split(";")
+ .map((stmt) => stmt.trim())
+ .filter((stmt) => stmt);
+
+ // Look for INSERT statements
+ const insertStatements = statements.filter(
+ (stmt) =>
+ stmt.toLowerCase().includes("insert into") &&
+ stmt.toLowerCase().includes("values"),
+ );
+
+ if (insertStatements.length === 0) {
+ throw new Error(
+ "No INSERT statements found. Please provide SQL with INSERT INTO ... VALUES statements.",
+ );
+ }
+
+ // Parse the first INSERT to get table structure
+ const firstInsert = insertStatements[0];
+
+ // Extract table name and columns
+ const tableMatch = firstInsert.match(
+ /insert\s+into\s+`?(\w+)`?\s*\(([^)]+)\)/i,
+ );
+ if (!tableMatch) {
+ throw new Error(
+ "Could not parse table structure. Expected format: INSERT INTO table (col1, col2) VALUES ...",
+ );
+ }
+
+ const tableName = tableMatch[1];
+ const columnsPart = tableMatch[2];
+
+ // Parse column names
+ const columnNames = columnsPart
+ .split(",")
+ .map((col) => col.trim().replace(/[`'"]/g, ""))
+ .filter((col) => col);
+
+ // Create headers
+ const headers = columnNames.map((name, index) => ({
+ id: `col_${index}`,
+ name: name,
+ type: "text",
+ }));
+
+ // Parse all INSERT statements for data
+ const allRows = [];
+
+ insertStatements.forEach((statement, statementIndex) => {
+ // Extract VALUES part
+ const valuesMatch = statement.match(/values\s*(.+)$/i);
+ if (!valuesMatch) return;
+
+ let valuesStr = valuesMatch[1].trim();
+
+ // Handle multiple value sets in one INSERT
+ // Split by ), ( but be careful with nested parentheses
+ const valueSets = [];
+ let currentSet = "";
+ let parenCount = 0;
+ let inString = false;
+ let stringChar = "";
+
+ for (let i = 0; i < valuesStr.length; i++) {
+ const char = valuesStr[i];
+ const prevChar = i > 0 ? valuesStr[i - 1] : "";
+
+ if (!inString && (char === '"' || char === "'")) {
+ inString = true;
+ stringChar = char;
+ } else if (inString && char === stringChar && prevChar !== "\\") {
+ inString = false;
+ stringChar = "";
+ }
+
+ if (!inString) {
+ if (char === "(") parenCount++;
+ if (char === ")") parenCount--;
+ }
+
+ currentSet += char;
+
+ if (!inString && parenCount === 0 && char === ")") {
+ valueSets.push(currentSet.trim());
+ currentSet = "";
+ // Skip comma and whitespace
+ while (
+ i + 1 < valuesStr.length &&
+ (valuesStr[i + 1] === "," || valuesStr[i + 1] === " ")
+ ) {
+ i++;
+ }
+ }
+ }
+
+ // Parse each value set
+ valueSets.forEach((valueSet, setIndex) => {
+ // Remove outer parentheses
+ valueSet = valueSet.replace(/^\(|\)$/g, "").trim();
+ if (valueSet.endsWith(";")) {
+ valueSet = valueSet.slice(0, -1).trim();
+ }
+
+ // Parse individual values
+ const values = [];
+ let currentValue = "";
+ let inString = false;
+ let stringChar = "";
+ let parenCount = 0;
+
+ for (let i = 0; i < valueSet.length; i++) {
+ const char = valueSet[i];
+ const prevChar = i > 0 ? valueSet[i - 1] : "";
+
+ if (!inString && (char === '"' || char === "'")) {
+ inString = true;
+ stringChar = char;
+ continue; // Don't include quote in value
+ } else if (inString && char === stringChar && prevChar !== "\\") {
+ inString = false;
+ stringChar = "";
+ continue; // Don't include quote in value
+ }
+
+ if (!inString) {
+ if (char === "(") parenCount++;
+ if (char === ")") parenCount--;
+
+ if (char === "," && parenCount === 0) {
+ values.push(currentValue.trim());
+ currentValue = "";
+ continue;
+ }
+ }
+
+ currentValue += char;
+ }
+
+ if (currentValue.trim()) {
+ values.push(currentValue.trim());
+ }
+
+ // Create row data
+ if (values.length === headers.length) {
+ const rowData = { id: `row_${allRows.length}` };
+ headers.forEach((header, index) => {
+ let value = values[index] || "";
+
+ // Handle NULL values
+ if (value.toLowerCase() === "null") {
+ value = "";
+ }
+
+ // Clean up the value - handle escaped JSON properly
+ console.log(
+ "π Processing value for row",
+ allRows.length,
+ "column",
+ index,
+ ":",
+ value.substring(0, 100) + (value.length > 100 ? "..." : ""),
+ );
+
+ if (value.includes('\\"') || value.includes("\\'")) {
+ console.log(
+ "π§ Value contains escaped quotes, checking if JSON...",
+ );
+
+ // Try to detect if this is escaped JSON
+ const isEscapedJson = (str) => {
+ // Check if it looks like escaped JSON (starts with '{' or '[' after unescaping)
+ const unescaped = str
+ .replace(/\\"/g, '"')
+ .replace(/\\'/g, "'");
+ const trimmed = unescaped.trim();
+ console.log(
+ "π§ͺ Testing unescaped value:",
+ trimmed.substring(0, 100) +
+ (trimmed.length > 100 ? "..." : ""),
+ );
+
+ if (
+ (trimmed.startsWith("{") && trimmed.endsWith("}")) ||
+ (trimmed.startsWith("[") && trimmed.endsWith("]"))
+ ) {
+ try {
+ JSON.parse(trimmed);
+ console.log("β
Valid JSON detected!");
+ return true;
+ } catch (e) {
+ console.log("β JSON parse failed:", e.message);
+ return false;
+ }
+ }
+ console.log("β Not JSON format (no brackets)");
+ return false;
+ };
+
+ // If it's escaped JSON, unescape it
+ if (isEscapedJson(value)) {
+ const oldValue = value;
+ value = value.replace(/\\"/g, '"').replace(/\\'/g, "'");
+ console.log(
+ "π― Unescaped JSON from:",
+ oldValue.substring(0, 50),
+ "to:",
+ value.substring(0, 50),
+ );
+ } else {
+ // Only unescape single quotes for non-JSON strings
+ if (value.includes("\\'")) {
+ console.log("π§ Unescaping single quotes only");
+ value = value.replace(/\\'/g, "'");
+ }
+ }
+ }
+
+ console.log(
+ "β
Final value for storage:",
+ value.substring(0, 100) + (value.length > 100 ? "..." : ""),
+ );
+
+ rowData[header.id] = value;
+ });
+ allRows.push(rowData);
+ }
+ });
+ });
+
+ if (allRows.length === 0) {
+ throw new Error(
+ "No data rows could be parsed from the SQL statements.",
+ );
+ }
+
+ setColumns(headers);
+ setData(allRows);
+ setError("");
+ } catch (err) {
+ setError(`Failed to parse SQL: ${err.message}`);
+ }
+ };
+
+ // Parse JSON data
+ const parseJsonData = (text) => {
+ try {
+ const jsonData = JSON.parse(text);
+
+ if (!Array.isArray(jsonData)) {
+ 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}`);
+ }
+ };
+
+ // Handle text input
+ const handleTextInput = () => {
+ if (!inputText.trim()) {
+ setError("Please enter some data");
+ return;
+ }
+
+ const trimmed = inputText.trim();
+ console.log(
+ "π― handleTextInput - detecting format for input length:",
+ trimmed.length,
+ );
+ console.log("π First 200 chars:", trimmed.substring(0, 200));
+
+ // Try to detect format
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
+ // JSON array
+ console.log("π Detected JSON array format");
+ parseJsonData(trimmed);
+ } else if (
+ trimmed.toLowerCase().includes("insert into") &&
+ trimmed.toLowerCase().includes("values")
+ ) {
+ // SQL INSERT statements
+ console.log("π Detected SQL INSERT format");
+ parseSqlData(trimmed);
+ } else {
+ // CSV/TSV
+ console.log("π Detected CSV/TSV format");
+ parseData(trimmed, useFirstRowAsHeader);
+ }
+ };
+
+ // Fetch data from URL
+ const fetchUrlData = async () => {
+ if (!url.trim()) {
+ setError("Please enter a valid URL");
+ return;
+ }
+
+ setIsLoading(true);
+ setError("");
+
+ try {
+ const response = await fetch(url.trim());
+
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+ }
+
+ const contentType = response.headers.get("content-type") || "";
+ const text = await response.text();
+
+ if (contentType.includes("application/json") || url.includes(".json")) {
+ parseJsonData(text);
+ } else {
+ parseData(text, useFirstRowAsHeader);
+ }
+ } catch (err) {
+ setError(`Failed to fetch data: ${err.message}`);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // Multi-table management functions
+ const switchToTable = (tableName) => {
+ if (!tableName || !tableRegistry[tableName]) return;
+
+ // Save current table state if we have one
+ if (currentTable && tableRegistry[currentTable]) {
+ setTableRegistry((prev) => ({
+ ...prev,
+ [currentTable]: {
+ ...prev[currentTable],
+ data: data,
+ columns: columns,
+ modified: true,
+ },
+ }));
+ }
+
+ // Load new table
+ const tableData = tableRegistry[tableName];
+ setCurrentTable(tableName);
+ setData(tableData.data);
+ setColumns(tableData.columns);
+ setSearchTerm("");
+ setSelectedRows(new Set());
+ setSelectedColumns(new Set());
+ };
+
+ const initializeTablesFromSQL = (sqlContent, fileName = "") => {
+ console.log("ποΈ initializeTablesFromSQL called with fileName:", fileName);
+ console.log("π SQL content length:", sqlContent.length);
+
+ const discoveredTables = parseMultiTableSQL(sqlContent);
+ const tableNames = Object.keys(discoveredTables);
+
+ console.log("π Discovered tables:", tableNames);
+
+ if (tableNames.length === 0) {
+ console.error("β No tables found in SQL file");
+ setError("No tables found in SQL file");
+ return;
+ }
+
+ // Initialize table registry
+ const registry = {};
+ tableNames.forEach((tableName) => {
+ const tableInfo = discoveredTables[tableName];
+ registry[tableName] = {
+ data: tableInfo.data,
+ columns: tableInfo.columns,
+ modified: false,
+ originalSchema: tableInfo.schema,
+ originalData: [...tableInfo.data],
+ rowCount: tableInfo.rowCount,
+ };
+ });
+
+ setTableRegistry(registry);
+ setAvailableTables(tableNames);
+ setOriginalFileName(fileName.replace(/\.[^/.]+$/, "")); // Remove extension
+
+ // Load first table by default
+ const firstTable = tableNames[0];
+ setCurrentTable(firstTable);
+ setData(registry[firstTable].data);
+ setColumns(registry[firstTable].columns);
+ };
+
+ // Handle file upload
+ const handleFileUpload = (event) => {
+ const file = event.target.files[0];
+ if (!file) return;
+
+ console.log("π File upload started:", file.name, "size:", file.size);
+ setIsLoading(true);
+ setError("");
+
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ try {
+ const content = e.target.result;
+ console.log("π File content loaded, length:", content.length);
+ console.log("π First 300 chars:", content.substring(0, 300));
+
+ // Check if it's SQL file for multi-table support
+ if (file.name.toLowerCase().endsWith(".sql")) {
+ console.log("π Detected SQL file, using multi-table parsing");
+ initializeTablesFromSQL(content, file.name);
+ } else {
+ console.log("π Non-SQL file, using single-table parsing");
+ // Fallback to single-table parsing
+ parseData(content);
+ }
+ } catch (err) {
+ console.error("β File upload error:", err);
+ setError("Failed to read file: " + err.message);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ reader.readAsText(file);
+ };
+
+ // Filter and sort data
+ const filteredAndSortedData = React.useMemo(() => {
+ let filtered = data;
+
+ // Apply search filter
+ if (searchTerm) {
+ filtered = data.filter((row) =>
+ columns.some((col) =>
+ String(row[col.id] || "")
+ .toLowerCase()
+ .includes(searchTerm.toLowerCase()),
+ ),
+ );
+ }
+
+ // Apply sorting
+ if (sortConfig.key) {
+ filtered = [...filtered].sort((a, b) => {
+ const aVal = a[sortConfig.key] || "";
+ const bVal = b[sortConfig.key] || "";
+
+ if (sortConfig.direction === "asc") {
+ return aVal.toString().localeCompare(bVal.toString());
+ } else {
+ return bVal.toString().localeCompare(aVal.toString());
+ }
+ });
+ }
+
+ return filtered;
+ }, [data, searchTerm, sortConfig, columns]);
+
+ // Handle sorting
+ const handleSort = (columnId) => {
+ setSortConfig((prev) => ({
+ key: columnId,
+ direction:
+ prev.key === columnId && prev.direction === "asc" ? "desc" : "asc",
+ }));
+ };
+
+ // Add new row
+ const addRow = () => {
+ const newRow = { id: `row_${Date.now()}` };
+ columns.forEach((col) => {
+ newRow[col.id] = "";
+ });
+ setData([...data, newRow]);
+ };
+
+ // Add new column
+ const addColumn = () => {
+ const newColumnId = `col_${Date.now()}`;
+ const newColumn = {
+ id: newColumnId,
+ name: `Column ${columns.length + 1}`,
+ type: "text",
+ };
+
+ setColumns([...columns, newColumn]);
+
+ // Add empty values for the new column in all existing rows
+ setData(
+ data.map((row) => ({
+ ...row,
+ [newColumnId]: "",
+ })),
+ );
+
+ // Auto-scroll to the right to show the new column
+ setTimeout(() => {
+ const tableContainer = document.querySelector(".overflow-auto");
+ if (tableContainer) {
+ tableContainer.scrollLeft = tableContainer.scrollWidth;
+ }
+ }, 100);
+ };
+
+ // Delete selected rows
+ const deleteSelectedRows = () => {
+ setData(data.filter((row) => !selectedRows.has(row.id)));
+ setSelectedRows(new Set());
+ };
+
+ // Delete selected columns
+ const deleteSelectedColumns = () => {
+ const remainingColumns = columns.filter(
+ (col) => !selectedColumns.has(col.id),
+ );
+ setColumns(remainingColumns);
+
+ // Remove deleted column data from all rows
+ setData(
+ data.map((row) => {
+ const newRow = { ...row };
+ selectedColumns.forEach((colId) => {
+ delete newRow[colId];
+ });
+ return newRow;
+ }),
+ );
+
+ setSelectedColumns(new Set());
+ };
+
+ // Handle cell edit with proper event handling
+ const handleCellEdit = (rowId, columnId, value) => {
+ setData(
+ data.map((row) =>
+ row.id === rowId ? { ...row, [columnId]: value } : row,
+ ),
+ );
+ };
+
+ // Handle cell key events
+ const handleCellKeyDown = (e, rowId, columnId) => {
+ if (e.key === "Enter" || e.key === "Escape") {
+ setEditingCell(null);
+ }
+ };
+
+ // Handle header edit
+ const handleHeaderEdit = (columnId, newName) => {
+ setColumns(
+ columns.map((col) =>
+ col.id === columnId ? { ...col, name: newName } : col,
+ ),
+ );
+ };
+
+ // Handle header key events
+ const handleHeaderKeyDown = (e, columnId) => {
+ if (e.key === "Enter" || e.key === "Escape") {
+ setEditingHeader(null);
+ }
+ };
+
+ // Export functions
+ const getExportData = (format) => {
+ if (data.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 = data.map((row) => {
+ return Object.fromEntries(
+ columns.map((col) => [col.name, row[col.id] || ""]),
+ );
+ });
+ return jsonFormat === "pretty"
+ ? JSON.stringify(jsonData, null, 2)
+ : JSON.stringify(jsonData);
+
+ case "csv":
+ return Papa.unparse({
+ fields: columns.map((col) => col.name),
+ data: data.map((row) => columns.map((col) => row[col.id] || "")),
+ });
+
+ case "tsv":
+ return Papa.unparse(
+ {
+ fields: columns.map((col) => col.name),
+ data: data.map((row) => columns.map((col) => row[col.id] || "")),
+ },
+ { delimiter: "\t" },
+ );
+
+ case "sql":
+ return generateMultiTableSQL();
+
+ 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);
+ };
+
+ const generateMultiTableSQL = () => {
+ // Get current table state without triggering re-render
+ const currentTableData =
+ currentTable && tableRegistry[currentTable]
+ ? {
+ ...tableRegistry[currentTable],
+ data: data,
+ columns: columns,
+ modified: true,
+ }
+ : null;
+
+ const sqlParts = [];
+ const dbName = originalFileName || "database";
+
+ // Add header comment
+ sqlParts.push(`-- Database: ${dbName}`);
+ sqlParts.push(`-- Generated by Table Editor`);
+ sqlParts.push(`-- Export Date: ${new Date().toISOString()}`);
+ sqlParts.push("");
+
+ // If we have multiple tables, export all of them
+ if (availableTables.length > 1) {
+ sqlParts.push(`-- Tables: ${availableTables.join(", ")}`);
+ sqlParts.push("");
+
+ availableTables.forEach((tableName) => {
+ const tableData =
+ tableName === currentTable && currentTableData
+ ? currentTableData
+ : tableRegistry[tableName];
+ if (!tableData) return;
+
+ const actualData = tableData.data;
+ const actualColumns = tableData.columns;
+ const isModified = tableData.modified;
+
+ sqlParts.push(`-- ================================================`);
+ sqlParts.push(
+ `-- Table: ${tableName} (${actualData.length} rows, ${actualColumns.length} columns)`,
+ );
+ sqlParts.push(`-- Status: ${isModified ? "Modified" : "Original"}`);
+ sqlParts.push(`-- ================================================`);
+ sqlParts.push("");
+
+ // Add DROP TABLE
+ sqlParts.push(`DROP TABLE IF EXISTS \`${tableName}\`;`);
+ sqlParts.push("");
+
+ // Add CREATE TABLE (use original schema if available)
+ if (tableData.originalSchema) {
+ sqlParts.push(tableData.originalSchema);
+ } else {
+ // Generate intelligent CREATE TABLE based on data analysis
+ sqlParts.push(
+ generateCreateTableStatement(
+ tableName,
+ actualColumns,
+ sqlPrimaryKey,
+ actualData,
+ ),
+ );
+ }
+ sqlParts.push("");
+
+ // Add INSERT statements if there's data
+ if (actualData.length > 0) {
+ const columnNames = actualColumns.map((col) => col.name);
+ const insertStatements = actualData.map((row) => {
+ const values = actualColumns.map((col) => {
+ const value = row[col.id] || "";
+ if (value === "" || value === null || value === undefined)
+ return "NULL";
+ return typeof value === "string"
+ ? `'${value.replace(/'/g, "''")}'`
+ : value;
+ });
+ return `INSERT INTO \`${tableName}\` (\`${columnNames.join("`, `")}\`) VALUES (${values.join(", ")});`;
+ });
+
+ sqlParts.push(
+ `INSERT INTO \`${tableName}\` (\`${columnNames.join("`, `")}\`) VALUES`,
+ );
+ insertStatements.forEach((stmt, index) => {
+ const values = stmt.match(/VALUES \((.+)\);/)[1];
+ sqlParts.push(
+ `(${values})${index === insertStatements.length - 1 ? ";" : ","}`,
+ );
+ });
+ } else {
+ sqlParts.push(`-- No data for table ${tableName}`);
+ }
+
+ sqlParts.push("");
+ });
+ } else {
+ // Single table export
+ const tableName =
+ sqlTableName || currentTable || originalFileName || "table_data";
+ const columnNames = columns.map((col) => col.name);
+
+ sqlParts.push(`-- Table: ${tableName}`);
+ sqlParts.push("");
+ sqlParts.push(`DROP TABLE IF EXISTS \`${tableName}\`;`);
+ sqlParts.push("");
+
+ // Use original schema if available
+ const singleTableData = currentTableData || tableRegistry[tableName];
+ if (singleTableData?.originalSchema) {
+ sqlParts.push(singleTableData.originalSchema);
+ } else {
+ sqlParts.push(
+ generateCreateTableStatement(tableName, columns, sqlPrimaryKey),
+ );
+ }
+ sqlParts.push("");
+
+ if (data.length > 0) {
+ const insertStatements = data.map((row) => {
+ const values = columns.map((col) => {
+ const value = row[col.id] || "";
+ if (value === "" || value === null || value === undefined)
+ return "NULL";
+ return typeof value === "string"
+ ? `'${value.replace(/'/g, "''")}'`
+ : value;
+ });
+ return `(${values.join(", ")})`;
+ });
+
+ sqlParts.push(
+ `INSERT INTO \`${tableName}\` (\`${columnNames.join("`, `")}\`) VALUES`,
+ );
+ insertStatements.forEach((values, index) => {
+ sqlParts.push(
+ `${values}${index === insertStatements.length - 1 ? ";" : ","}`,
+ );
+ });
+ }
+ }
+
+ return sqlParts.join("\n");
+ };
+
+ // Intelligent column type detection for SQL export
+ const analyzeColumnData = (columnId, tableData) => {
+ const values = tableData
+ .map((row) => row[columnId])
+ .filter((val) => val !== null && val !== undefined && val !== "");
+
+ if (values.length === 0) {
+ return { type: "VARCHAR(100)", nullable: true, analysis: "Empty column" };
+ }
+
+ let maxLength = 0;
+ let hasJson = false;
+ let hasDate = false;
+ let hasNumber = false;
+ let hasEmail = false;
+ let hasUrl = false;
+ let hasPhone = false;
+ let allIntegers = true;
+ let allDecimals = true;
+ let dateFormats = new Set();
+
+ values.forEach((val) => {
+ const strVal = String(val);
+ maxLength = Math.max(maxLength, strVal.length);
+
+ // JSON detection
+ if (
+ (strVal.startsWith("{") && strVal.endsWith("}")) ||
+ (strVal.startsWith("[") && strVal.endsWith("]"))
+ ) {
+ try {
+ JSON.parse(strVal);
+ hasJson = true;
+ } catch {}
+ }
+
+ // Date detection (various formats)
+ const datePatterns = [
+ /^\d{1,2}\s+(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{4}$/i, // "17 Mar 2022"
+ /^\d{4}-\d{2}-\d{2}$/, // "2022-03-17"
+ /^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}$/, // "2022-03-17 10:30:00"
+ /^\d{1,2}\/\d{1,2}\/\d{4}$/, // "3/17/2022"
+ /^\d{2}-\d{2}-\d{4}$/, // "17-03-2022"
+ ];
+
+ datePatterns.forEach((pattern, index) => {
+ if (pattern.test(strVal)) {
+ hasDate = true;
+ dateFormats.add(index);
+ }
+ });
+
+ // Number detection
+ if (!isNaN(strVal) && strVal.trim() !== "") {
+ hasNumber = true;
+ if (!Number.isInteger(Number(strVal))) {
+ allIntegers = false;
+ }
+ } else {
+ allIntegers = false;
+ allDecimals = false;
+ }
+
+ // Email detection
+ if (strVal.includes("@") && strVal.includes(".")) {
+ hasEmail = true;
+ }
+
+ // URL detection
+ if (strVal.startsWith("http://") || strVal.startsWith("https://")) {
+ hasUrl = true;
+ }
+
+ // Phone detection (basic)
+ if (/^[\+\d\s\-\(\)]{10,}$/.test(strVal)) {
+ hasPhone = true;
+ }
+ });
+
+ const nullableCount = tableData.length - values.length;
+ const nullable = nullableCount > 0;
+
+ // Determine best column type
+ if (hasJson) {
+ return {
+ type: "JSON",
+ nullable,
+ analysis: `JSON data detected (max length: ${maxLength})`,
+ fallbackType:
+ maxLength > 65535
+ ? "LONGTEXT"
+ : maxLength > 16777215
+ ? "MEDIUMTEXT"
+ : "TEXT",
+ };
+ }
+
+ if (hasNumber && values.every((val) => !isNaN(val) && val.trim() !== "")) {
+ if (allIntegers) {
+ const maxVal = Math.max(...values.map((v) => Math.abs(Number(v))));
+ if (maxVal < 128)
+ return {
+ type: "TINYINT",
+ nullable,
+ analysis: "Small integers (-128 to 127)",
+ };
+ if (maxVal < 32768)
+ return {
+ type: "SMALLINT",
+ nullable,
+ analysis: "Small integers (-32,768 to 32,767)",
+ };
+ if (maxVal < 2147483648)
+ return { type: "INT", nullable, analysis: "Standard integers" };
+ return { type: "BIGINT", nullable, analysis: "Large integers" };
+ } else {
+ return { type: "DECIMAL(10,2)", nullable, analysis: "Decimal numbers" };
+ }
+ }
+
+ if (hasDate && dateFormats.size === 1) {
+ const formatNames = ["TEXT", "DATE", "DATETIME", "DATE", "DATE"];
+ const detectedFormat = Array.from(dateFormats)[0];
+ return {
+ type: formatNames[detectedFormat] || "TEXT",
+ nullable,
+ analysis: `Date format detected (pattern ${detectedFormat})`,
+ };
+ }
+
+ // Text-based analysis
+ if (maxLength <= 50) {
+ const size = Math.max(50, Math.ceil(maxLength * 1.2));
+ let analysis = `Short text (max: ${maxLength})`;
+ if (hasEmail) analysis += ", emails detected";
+ if (hasPhone) analysis += ", phone numbers detected";
+ return { type: `VARCHAR(${size})`, nullable, analysis };
+ }
+
+ if (maxLength <= 255) {
+ const size = Math.max(100, Math.ceil(maxLength * 1.2));
+ let analysis = `Medium text (max: ${maxLength})`;
+ if (hasUrl) analysis += ", URLs detected";
+ return { type: `VARCHAR(${size})`, nullable, analysis };
+ }
+
+ if (maxLength <= 1000) {
+ const size = Math.ceil(maxLength * 1.2);
+ return {
+ type: `VARCHAR(${size})`,
+ nullable,
+ analysis: `Long text (max: ${maxLength})`,
+ };
+ }
+
+ if (maxLength <= 65535) {
+ return {
+ type: "TEXT",
+ nullable,
+ analysis: `Very long text (max: ${maxLength})`,
+ };
+ }
+
+ if (maxLength <= 16777215) {
+ return {
+ type: "MEDIUMTEXT",
+ nullable,
+ analysis: `Extra long text (max: ${maxLength})`,
+ };
+ }
+
+ return {
+ type: "LONGTEXT",
+ nullable,
+ analysis: `Extremely long text (max: ${maxLength})`,
+ };
+ };
+
+ const generateCreateTableStatement = (
+ tableName,
+ tableColumns,
+ primaryKeyColumn = null,
+ tableData = data,
+ ) => {
+ const analysisResults = [];
+
+ const columnDefs = tableColumns.map((col) => {
+ const analysis = analyzeColumnData(col.id, tableData);
+ analysisResults.push({ column: col.name, ...analysis });
+
+ let def = ` \`${col.name}\` ${analysis.type}`;
+
+ // Add PRIMARY KEY if this column is selected as primary key
+ if (primaryKeyColumn && col.name === primaryKeyColumn) {
+ def += " NOT NULL PRIMARY KEY";
+ } else {
+ def += analysis.nullable ? " DEFAULT NULL" : " NOT NULL";
+ }
+
+ return def;
+ });
+
+ // Store analysis for user display
+ window.lastSqlAnalysis = analysisResults;
+
+ return `CREATE TABLE \`${tableName}\` (\n${columnDefs.join(",\n")}\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`;
+ };
+
+ const copyToClipboard = async (text) => {
+ try {
+ await navigator.clipboard.writeText(text);
+ } catch (err) {
+ console.error("Failed to copy:", err);
+ }
+ };
+
+ const exportToJson = () => {
+ const content = getExportData("json");
+ downloadFile(content, "table-data.json", "application/json");
+ };
+
+ const exportToCsv = () => {
+ const content = getExportData("csv");
+ downloadFile(content, "table-data.csv", "text/csv");
+ };
+
+ const createEmptyTable = () => {
+ const newTableName = "new_table";
+ const initialColumns = [
+ { id: "col_0", name: "id" },
+ { id: "col_1", name: "name" },
+ { id: "col_2", name: "value" },
+ ];
+
+ // Create one empty row to make the table visible
+ const initialData = [
+ {
+ id: Date.now(),
+ col_0: "",
+ col_1: "",
+ col_2: "",
+ },
+ ];
+
+ setData(initialData);
+ setColumns(initialColumns);
+ setCurrentTable(newTableName);
+ setAvailableTables([newTableName]);
+ setTableRegistry({
+ [newTableName]: {
+ data: [],
+ columns: initialColumns,
+ modified: true,
+ originalSchema: "",
+ originalData: [],
+ rowCount: 0,
+ },
+ });
+ setInputText("");
+ setUrl("");
+ setError("");
+ setSearchTerm("");
+ setSortConfig({ key: null, direction: "asc" });
+ setSelectedRows(new Set());
+ setSelectedColumns(new Set());
+ setOriginalFileName("");
+ };
+
+ const clearData = () => {
+ // Show confirmation modal
+ setShowClearConfirmModal(true);
+ };
+
+ const handleClearConfirm = () => {
+ // Proceed with clearing data
+ setData([]);
+ setColumns([]);
+ setInputText("");
+ setUrl("");
+ setError("");
+ setSearchTerm("");
+ setSortConfig({ key: null, direction: "asc" });
+ setSelectedRows(new Set());
+ setSelectedColumns(new Set());
+
+ // Clear multi-table state
+ setTableRegistry({});
+ setAvailableTables([]);
+ setCurrentTable("");
+ setOriginalFileName("");
+ setEditingTableName(false);
+ setFrozenColumns(0);
+
+ // Close modal
+ setShowClearConfirmModal(false);
+ };
+
+ const handleClearCancel = () => {
+ setShowClearConfirmModal(false);
+ };
+
+ return (
+
+ {/* Input Section with Tabs */}
+
+ {/* Tabs */}
+
+
+
+
+
+
+
+
+
+ {/* Tab Content */}
+ {(activeTab !== "create" || !createNewCompleted) && (
+
+ {activeTab === "create" && !createNewCompleted && (
+
+
+
+ Start Building Your Table
+
+
+ Choose how you'd like to begin working with your data
+
+
+
+
+
+
+
+
+
+
+
+ π‘ Tip: You can always import data later
+ using the URL, Paste, or Open tabs, or start editing
+ directly in the table below.
+
+
+
+ )}
+
+ {activeTab === "url" && (
+
+
+
+ setUrl(e.target.value)}
+ placeholder="https://api.example.com/data.json or https://example.com/data.csv"
+ className="tool-input w-full pr-10"
+ disabled={isLoading}
+ />
+ {url && !isLoading && (
+
+ )}
+
+
+
+
+
+ )}
+
+ {activeTab === "paste" && (
+
+ )}
+
+ {activeTab === "upload" && (
+
+
+
+
+
+ π Privacy: Your data stays in your
+ browser. We don't store or upload anything - just help you
+ open, edit, and export your files locally.
+
+
+
+ )}
+
+ )}
+
+
+ {data.length > 0 && (
+
+ {/* Header */}
+
+
+
+
+
+
+
+ {availableTables.length > 1 ? "Multi-Table Database" : "Table Editor"}
+
+ {availableTables.length == 1 && (
+
+ {data.length} rows, {columns.length} columns
+
+ )}
+
+
+
+
+ {/* Action Tabs - Right Side */}
+
+
+
+
+
+
+
+ {/* Table Selector */}
+ {availableTables.length > 1 && (
+
+
+
+ Current Table:
+
+
+
+
+ {data.length} rows, {columns.length} columns
+
+
+ )}
+
+ {/* Table Body - Edge to Edge */}
+
+ {/* Controls */}
+
+ {/* Search Bar */}
+
+
+ setSearchTerm(e.target.value)}
+ placeholder="Search data..."
+ className="tool-input pl-10 w-full"
+ />
+
+
+ {/* Action Buttons Row */}
+
+
+ {selectedRows.size > 0 && (
+
+ )}
+
+ {selectedColumns.size > 0 && (
+
+ )}
+
+
+ {/* Freeze Columns Control */}
+
+
+ Freeze:
+
+
+ {frozenColumns > 0 && (
+
+ )}
+
+
+
+
+ {/* Table */}
+
+
+
+ )}
+
+ {/* Object Editor Modal */}
+ {objectEditorModal && (
+
+ )}
+
+ {/* Clear Confirmation Modal */}
+ {showClearConfirmModal && (
+ {
+ clearAllData();
+ setShowClearConfirmModal(false);
+ }}
+ onCancel={() => setShowClearConfirmModal(false)}
+ />
+ )}
+
+ {/* Input Method Change Confirmation Modal */}
+ {showInputChangeModal && (
+ {
+ // If user cancels while on Create New tab with modified data, hide the tab content
+ if (activeTab === "create" && hasModifiedData()) {
+ setCreateNewCompleted(true);
+ }
+ setShowInputChangeModal(false);
+ setPendingTabChange(null);
+ }}
+ />
+ )}
+
+ {/* Export Section */}
+ {data.length > 0 && (
+
+ {/* Export Header */}
+
+
+
+
+ Export Results
+
+
+ {availableTables.length > 1 ? (
+
+ Database: {originalFileName || "Multi-table"} (
+ {availableTables.length} tables)
+
+ ) : (
+
+ Table: {currentTable || "Data"} ({data.length} rows,{" "}
+ {columns.length} columns)
+
+ )}
+
+
+
+
+ {/* Export Tabs */}
+
+
+
+
+
+
+
+ {/* Export Content */}
+
+ {exportTab === "json" && (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {exportTab === "csv" && (
+
+
+
+
+
+
+
+ )}
+
+ {exportTab === "tsv" && (
+
+
+
+
+
+
+
+ )}
+
+ {exportTab === "sql" && (
+
+ {/* SQL Export Controls */}
+
+
+ SQL Export Settings
+
+
+
+
+ setSqlTableName(e.target.value)}
+ placeholder="Enter table name"
+ className="tool-input w-full text-sm"
+ />
+
+
+
+
+
+
+
+
+
+
+ {/* Intelligent Schema Analysis */}
+ {window.lastSqlAnalysis && (
+
+
+
+
+
+ π Intelligent Schema Analysis
+
+
+
+
+ Auto-detected column types based on your data:
+
+
+
+ {window.lastSqlAnalysis.map((col, index) => (
+
+
+ {col.column}
+
+
+ {col.type}
+
+
+ {col.analysis.split("(")[0]}
+
+
+ ))}
+
+
+ Smart Detection: JSON β
+ JSON/LONGTEXT, Numbers β INT/DECIMAL, Dates β
+ DATE/DATETIME, Text β VARCHAR(optimized size),
+ URLs/Emails β Appropriate sizing
+
+
+
+
+
+ )}
+
+ {/* SQL Schema Notice */}
+
+
+
+
+
+ π SQL Schema Information
+
+
+
+ What this schema does:
+
+
+ -
+ DROP TABLE IF EXISTS - Safely
+ removes existing table if it exists
+
+ -
+ CREATE TABLE - Builds new table
+ with original or generated structure
+
+ -
+ INSERT VALUES - Populates table
+ with your edited data
+
+
+
+
+ π Data Safety Policy:
+
+
+ -
+ β οΈ DESTRUCTIVE: Will completely
+ replace existing table data
+
+ -
+ β
BACKUP FIRST: Always backup your
+ database before importing
+
+ -
+ π― TARGETED: Only affects the
+ specific table(s) in this export
+
+
+
+
+ π₯ How to use:
+
+
+ -
+ phpMyAdmin: Go to SQL tab β Paste β
+ Execute
+
+ -
+ MySQL CLI:{" "}
+
+ mysql -u user -p database < file.sql
+
+
+ -
+ Import Tool: Use database import
+ feature with downloaded .sql file
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+ )}
+
+ {/* Usage Tips */}
+
+
+ Usage Tips
+
+
+
+
π Input Methods:
+
+ -
+ Create New: Start empty or load sample data to
+ explore features
+
+ -
+ URL Import: Fetch data directly from CSV/JSON
+ endpoints
+
+ -
+ Paste Data: Auto-detects CSV, TSV, JSON arrays,
+ or SQL INSERT statements
+
+ -
+ Open Files: Import .csv, .json, .sql files
+ (multi-table SQL files supported)
+
+
+
+
+
+
π― Table Management:
+
+ -
+ Multi-Table Support: SQL files with multiple
+ tables show dropdown selector
+
+ -
+ Edit Headers: Click column headers to rename
+ them
+
+ -
+ Edit Cells: Click any cell to edit inline
+
+ -
+ Object Data: Cells with [OBJ] badge open Object
+ Editor for visual JSON/serialized editing
+
+
+
+
+
+
π Data Operations:
+
+ -
+ Add/Delete: Use buttons to add rows/columns or
+ select multiple to delete
+
+ -
+ Search & Sort: Filter data and sort by any
+ column
+
+ -
+ Bulk Operations: Select multiple rows/columns
+ for batch operations
+
+ -
+ Data Validation: Real-time validation for data
+ types and formats
+
+
+
+
+
+
π€ Export Options:
+
+ -
+ JSON: Export includes pretty-formatted and
+ minified versions
+
+ -
+ CSV/TSV: Standard delimited formats for
+ spreadsheet compatibility
+
+ -
+ SQL: Intelligent schema generation with
+ complete database preservation
+
+ -
+ Copy & Download: All formats support both
+ clipboard copy and file download
+
+
+
+
+
+
+ );
+};
+
+// Clear Confirmation Modal Component
+const ClearConfirmationModal = ({
+ tableCount,
+ rowCount,
+ columnCount,
+ tableName,
+ onConfirm,
+ onCancel,
+}) => {
+ return (
+
+
+ {/* Header */}
+
+
+
+
+
+ Clear All Data
+
+
+ This action cannot be undone
+
+
+
+
+
+ {/* Content */}
+
+
+ Are you sure you want to clear all table data?
+
+
+
+
+ This will permanently delete:
+
+
+ {tableCount > 1 ? (
+ <>
+ - β’ {tableCount} tables
+ -
+ β’ All {rowCount} rows in current table "{tableName}"
+
+ - β’ All imported data and modifications
+ >
+ ) : (
+ <>
+ - β’ All {rowCount} rows
+ - β’ All {columnCount} columns
+ - β’ Table "{tableName}"
+ >
+ )}
+
+
+
+
+
+
+
+ Warning: This action cannot be undone. All your
+ work will be lost.
+
+
+
+
+
+ {/* Footer */}
+
+
+
+
+
+
+
+
+ );
+};
+
+// Object Editor Modal Component
+const ObjectEditorModal = ({ modal, onClose, onApply }) => {
+ // Initialize with parsed data immediately
+ const initializeData = () => {
+ try {
+ let data = modal.originalValue;
+
+ // Handle different formats
+ if (modal.format.type === "base64_json") {
+ data = atob(modal.originalValue);
+ }
+
+ if (modal.format.type === "php_serialized") {
+ return {
+ structuredData: {},
+ currentValue: modal.originalValue,
+ isValid: true,
+ error: "",
+ };
+ }
+
+ const parsed = JSON.parse(data);
+ return {
+ structuredData: parsed,
+ currentValue: data,
+ isValid: true,
+ error: "",
+ };
+ } catch (err) {
+ // Try one more time with basic unescaping if JSON parsing failed
+ try {
+ const unescaped = modal.originalValue
+ .replace(/\\"/g, '"')
+ .replace(/\\'/g, "'");
+ const parsed = JSON.parse(unescaped);
+ return {
+ structuredData: parsed,
+ currentValue: unescaped,
+ isValid: true,
+ error: "",
+ };
+ } catch (secondErr) {
+ return {
+ structuredData: {},
+ currentValue: modal.originalValue,
+ isValid: false,
+ error: err.message,
+ };
+ }
+ }
+ };
+
+ const initialState = initializeData();
+ const [structuredData, setStructuredData] = useState(
+ initialState.structuredData,
+ );
+ const [viewMode, setViewMode] = useState("visual");
+ const [currentValue, setCurrentValue] = useState(initialState.currentValue);
+ const [isValid, setIsValid] = useState(initialState.isValid);
+ const [error, setError] = useState(initialState.error);
+
+ // Debug log to see what we initialized with
+ // console.log('ObjectEditorModal initialized with:', { structuredData, isValid, error });
+
+ // Update current value when structured data changes
+ const handleStructuredDataChange = (newData) => {
+ setStructuredData(newData);
+ try {
+ const jsonString = JSON.stringify(newData, null, 2);
+ setCurrentValue(jsonString);
+ setIsValid(true);
+ setError("");
+ } catch (err) {
+ setIsValid(false);
+ setError(err.message);
+ }
+ };
+
+ // Handle raw text changes
+ const handleRawValueChange = (newValue) => {
+ setCurrentValue(newValue);
+ try {
+ let data = newValue;
+
+ // Handle different formats
+ if (modal.format.type === "base64_json") {
+ data = atob(newValue);
+ }
+
+ if (modal.format.type === "php_serialized") {
+ setIsValid(true);
+ setError("");
+ return;
+ }
+
+ const parsed = JSON.parse(data);
+ setStructuredData(parsed);
+ setIsValid(true);
+ setError("");
+ } catch (err) {
+ setIsValid(false);
+ setError(err.message);
+ }
+ };
+
+ const handleApply = () => {
+ if (!isValid) return;
+
+ let finalValue = currentValue;
+
+ // Handle encoding back if needed
+ if (modal.format.type === "base64_json") {
+ try {
+ // Validate JSON first
+ JSON.parse(currentValue);
+ finalValue = btoa(currentValue);
+ } catch (err) {
+ setError("Invalid JSON for Base64 encoding");
+ return;
+ }
+ }
+
+ onApply(finalValue);
+ };
+
+ const renderVisualEditor = () => {
+ if (!isValid) {
+ return (
+
+
+
+
Invalid or unparseable data
+ {error &&
{error}
}
+
+
+ );
+ }
+
+ if (modal.format.type === "php_serialized") {
+ return (
+
+
+
+
PHP Serialized data - use Raw Editor to modify
+
+
+ );
+ }
+
+ // console.log('Rendering StructuredEditor with data:', structuredData); // Debug log
+ return (
+
+
+
+ );
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+ Object Editor
+
+
+ Row {modal.rowIndex} β’ Column: {modal.columnName} β’ Format:{" "}
+ {modal.format.type.replace("_", " ")}
+
+
+
+
+
+
+ {/* View Mode Tabs */}
+
+
+
+
+
+
+
+ {/* Content */}
+
+ {viewMode === "visual" ? (
+
{renderVisualEditor()}
+ ) : (
+
+ )}
+
+
+ {/* Footer */}
+
+
+
+
+ {isValid ? "β Valid" : "β Invalid"}{" "}
+ {modal.format.type.replace("_", " ")}
+
+ {isValid &&
+ structuredData &&
+ typeof structuredData === "object" && (
+
+ {Object.keys(structuredData).length} properties
+
+ )}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+// Input Method Change Confirmation Modal Component
+const InputChangeConfirmationModal = ({
+ tableCount,
+ rowCount,
+ columnCount,
+ currentMethod,
+ newMethod,
+ onConfirm,
+ onCancel,
+}) => {
+ const getMethodName = (method) => {
+ switch (method) {
+ case "create":
+ return "Create New";
+ case "create_empty":
+ return "Start Empty";
+ case "create_sample":
+ return "Load Sample";
+ case "url":
+ return "URL Import";
+ case "paste":
+ return "Paste Data";
+ case "upload":
+ return "File Upload";
+ default:
+ return method;
+ }
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+
+ Change Input Method
+
+
+ This will clear all current data
+
+
+
+
+
+ {/* Content */}
+
+
+ {newMethod === "create_empty" || newMethod === "create_sample" ? (
+ <>
+ Using {getMethodName(newMethod)} will clear all
+ current data.
+ >
+ ) : (
+ <>
+ Switching from {getMethodName(currentMethod)}{" "}
+ to {getMethodName(newMethod)} will clear all
+ current data.
+ >
+ )}
+
+
+
+
+ This will permanently delete:
+
+
+ {tableCount > 1 ? (
+ <>
+ - β’ {tableCount} imported tables
+ - β’ All {rowCount} rows in current table
+ - β’ All modifications and edits
+ >
+ ) : (
+ <>
+ - β’ All {rowCount} rows
+ - β’ All {columnCount} columns
+ - β’ All modifications and edits
+ >
+ )}
+
+
+
+
+
+
+
+ Tip: Consider exporting your current data
+ before switching methods to avoid losing your work.
+
+
+
+
+
+ {/* Footer */}
+
+
+
+
+
+
+
+
+ );
+};
+
+export default TableEditor;
diff --git a/src/pages/TextLengthTool.js b/src/pages/TextLengthTool.js
index 50ff56b1..98ace6b4 100644
--- a/src/pages/TextLengthTool.js
+++ b/src/pages/TextLengthTool.js
@@ -1,10 +1,16 @@
import React, { useState, useEffect } from 'react';
-import { Type, Copy, RotateCcw } from 'lucide-react';
+import { Type, Copy, RotateCcw, Globe, Download, AlertCircle, CheckCircle, Clock, X } from 'lucide-react';
import ToolLayout from '../components/ToolLayout';
import CopyButton from '../components/CopyButton';
+import { extractContentFromUrl, CONTENT_TYPE_INFO } from '../utils/contentExtractor';
const TextLengthTool = () => {
const [text, setText] = useState('');
+ const [url, setUrl] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+ const [urlResult, setUrlResult] = useState(null);
+ const [error, setError] = useState('');
+ const [useArticleOnly, setUseArticleOnly] = useState(true);
const [stats, setStats] = useState({
characters: 0,
charactersNoSpaces: 0,
@@ -65,8 +71,52 @@ const TextLengthTool = () => {
calculateStats();
}, [text]);
+ // Handle URL fetching
+ const fetchUrlContent = async () => {
+ if (!url.trim()) {
+ setError('Please enter a valid URL');
+ return;
+ }
+
+ setIsLoading(true);
+ setError('');
+ setUrlResult(null);
+
+ try {
+ const result = await extractContentFromUrl(url.trim());
+
+ if (result.success) {
+ setUrlResult(result);
+ // Set text based on user preference
+ const textToAnalyze = useArticleOnly ? result.articleText : result.allText;
+ setText(textToAnalyze);
+ setError('');
+ } else {
+ setError(result.error);
+ setUrlResult(null);
+ }
+ } catch (err) {
+ let errorMessage = err.message;
+ if (errorMessage.includes('Failed to fetch')) {
+ errorMessage = 'Unable to fetch content due to CORS restrictions or network issues';
+ }
+ setError(errorMessage);
+ setUrlResult(null);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
const clearText = () => {
setText('');
+ setUrlResult(null);
+ setError('');
+ };
+
+ const clearUrl = () => {
+ setUrl('');
+ setUrlResult(null);
+ setError('');
};
const loadSample = () => {
@@ -101,6 +151,121 @@ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deseru
description="Analyze text length, word count, and other text statistics"
icon={Type}
>
+ {/* URL Input Section */}
+
+
+
+
Analyze Content from URL
+
+
+
+
+
+ setUrl(e.target.value)}
+ placeholder="https://example.com/article"
+ className="tool-input w-full pr-10"
+ disabled={isLoading}
+ />
+ {url && !isLoading && (
+
+ )}
+
+
+
+
+ {/* URL Result Status */}
+ {urlResult && (
+
+
+
+
+ {CONTENT_TYPE_INFO[urlResult.contentType].emoji}
+
+ {CONTENT_TYPE_INFO[urlResult.contentType].label}
+
+
+
+ {CONTENT_TYPE_INFO[urlResult.contentType].description}
+
+ {urlResult.title && (
+
+ {urlResult.title}
+
+ )}
+
+ Article: {urlResult.metrics.articleWordCount} words β’
+ Total: {urlResult.metrics.totalWordCount} words β’
+ Ratio: {Math.round(urlResult.metrics.contentRatio * 100)}%
+
+
+
+
+
+
+
+ )}
+
+ {/* Error Display */}
+ {error && (
+
+
+
+
+
{error}
+ {error.includes('fetch') && (
+
+
Common solutions:
+
+ - Some websites block cross-origin requests for security
+ - Try copying the article text manually and pasting it below
+ - The site might require JavaScript to load content
+ - Check if the URL is accessible and returns HTML content
+
+
+ )}
+
+
+
+ )}
+
+
+
{/* Controls */}
@@ -251,11 +415,13 @@ Typing time: ${getTypingTime()}` : ''}`;
Usage Tips
- - β’ Perfect for checking character limits for social media posts, essays, or articles
- - β’ Real-time counting updates as you type or paste text
- - β’ Includes reading and typing time estimates based on average speeds
- - β’ Byte count shows the actual storage size of your text in UTF-8 encoding
- - β’ Use "Show Details" to see additional statistics like sentences and paragraphs
+ - β’ URL Analysis: Fetch and analyze content from any web page or article
+ - β’ Smart Content Detection: Automatically detects articles vs general web content
+ - β’ Article vs Full Page: Choose to analyze just the main article or entire page content
+ - β’ Real-time Counting: Updates as you type or paste text
+ - β’ Reading Time: Estimates based on average reading speed (225 WPM)
+ - β’ Content Quality: Shows content-to-noise ratio for web pages
+ - β’ Perfect for checking character limits for social media, essays, or articles
diff --git a/src/utils/browserCompat.js b/src/utils/browserCompat.js
new file mode 100644
index 00000000..ae7ac675
--- /dev/null
+++ b/src/utils/browserCompat.js
@@ -0,0 +1,147 @@
+// Browser compatibility utilities for handling different browser environments
+
+/**
+ * Detect if the app is running in Telegram's built-in browser
+ */
+export const isTelegramBrowser = () => {
+ const userAgent = navigator.userAgent.toLowerCase();
+ return userAgent.includes('telegram') ||
+ userAgent.includes('tgios') ||
+ userAgent.includes('tgandroid') ||
+ // Check for Telegram-specific window properties
+ (window.TelegramWebviewProxy !== undefined) ||
+ // Check for common Telegram browser characteristics
+ (userAgent.includes('mobile') && userAgent.includes('webkit') && !userAgent.includes('chrome'));
+};
+
+/**
+ * Detect if the app is running in any mobile in-app browser
+ */
+export const isInAppBrowser = () => {
+ const userAgent = navigator.userAgent.toLowerCase();
+ return userAgent.includes('wv') || // WebView
+ userAgent.includes('telegram') ||
+ userAgent.includes('fbav') || // Facebook
+ userAgent.includes('fban') || // Facebook
+ userAgent.includes('instagram') ||
+ userAgent.includes('twitter') ||
+ userAgent.includes('line') ||
+ userAgent.includes('whatsapp');
+};
+
+/**
+ * Get browser information
+ */
+export const getBrowserInfo = () => {
+ const userAgent = navigator.userAgent;
+ return {
+ userAgent,
+ isTelegram: isTelegramBrowser(),
+ isInApp: isInAppBrowser(),
+ isMobile: /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent),
+ isIOS: /iPad|iPhone|iPod/.test(userAgent),
+ isAndroid: /Android/.test(userAgent)
+ };
+};
+
+/**
+ * Add polyfills and compatibility fixes for problematic browsers
+ */
+export const addCompatibilityFixes = () => {
+ // Fix for missing or problematic console methods in some browsers
+ if (!window.console) {
+ window.console = {
+ log: () => {},
+ error: () => {},
+ warn: () => {},
+ info: () => {},
+ debug: () => {}
+ };
+ }
+
+ // Ensure console methods exist and are functions
+ ['log', 'error', 'warn', 'info', 'debug'].forEach(method => {
+ if (typeof console[method] !== 'function') {
+ console[method] = () => {};
+ }
+ });
+
+ // Add requestAnimationFrame polyfill if missing
+ if (!window.requestAnimationFrame) {
+ window.requestAnimationFrame = (callback) => {
+ return setTimeout(callback, 1000 / 60);
+ };
+ }
+
+ // Add cancelAnimationFrame polyfill if missing
+ if (!window.cancelAnimationFrame) {
+ window.cancelAnimationFrame = (id) => {
+ clearTimeout(id);
+ };
+ }
+
+ // Fix for missing or problematic localStorage in some browsers
+ try {
+ localStorage.setItem('test', 'test');
+ localStorage.removeItem('test');
+ } catch (e) {
+ window.localStorage = {
+ getItem: () => null,
+ setItem: () => {},
+ removeItem: () => {},
+ clear: () => {},
+ length: 0,
+ key: () => null
+ };
+ }
+
+ // Fix for missing or problematic sessionStorage
+ try {
+ sessionStorage.setItem('test', 'test');
+ sessionStorage.removeItem('test');
+ } catch (e) {
+ window.sessionStorage = {
+ getItem: () => null,
+ setItem: () => {},
+ removeItem: () => {},
+ clear: () => {},
+ length: 0,
+ key: () => null
+ };
+ }
+};
+
+/**
+ * Initialize compatibility fixes
+ */
+export const initBrowserCompat = () => {
+ const browserInfo = getBrowserInfo();
+
+ // Log browser info for debugging
+ console.log('Browser Info:', browserInfo);
+
+ // Add compatibility fixes
+ addCompatibilityFixes();
+
+ // Add specific fixes for Telegram browser
+ if (browserInfo.isTelegram) {
+ console.log('Telegram browser detected - applying compatibility fixes');
+
+ // Add Telegram-specific error handling
+ window.addEventListener('error', (event) => {
+ console.log('Global error caught in Telegram browser:', event.error);
+ // Prevent the error from bubbling up and showing the error overlay
+ event.preventDefault();
+ return true;
+ });
+
+ window.addEventListener('unhandledrejection', (event) => {
+ console.log('Unhandled promise rejection in Telegram browser:', event.reason);
+ // Prevent the error from bubbling up
+ event.preventDefault();
+ return true;
+ });
+ }
+
+ return browserInfo;
+};
diff --git a/src/utils/contentExtractor.js b/src/utils/contentExtractor.js
new file mode 100644
index 00000000..551ec751
--- /dev/null
+++ b/src/utils/contentExtractor.js
@@ -0,0 +1,371 @@
+// Content extraction and article detection utilities
+
+/**
+ * Content classification types
+ */
+export const CONTENT_TYPES = {
+ RICH_ARTICLE: 'rich_article',
+ GENERAL_CONTENT: 'general_content',
+ LIMITED_CONTENT: 'limited_content',
+ NO_CONTENT: 'no_content'
+};
+
+/**
+ * Content type display information
+ */
+export const CONTENT_TYPE_INFO = {
+ [CONTENT_TYPES.RICH_ARTICLE]: {
+ label: 'Rich Article Content',
+ emoji: 'π’',
+ description: 'Clear article structure with headings and paragraphs',
+ color: 'text-green-600 dark:text-green-400'
+ },
+ [CONTENT_TYPES.GENERAL_CONTENT]: {
+ label: 'General Web Content',
+ emoji: 'π‘',
+ description: 'Readable text mixed with navigation and UI elements',
+ color: 'text-yellow-600 dark:text-yellow-400'
+ },
+ [CONTENT_TYPES.LIMITED_CONTENT]: {
+ label: 'Limited Text Content',
+ emoji: 'π ',
+ description: 'Mostly UI/navigation with minimal readable text',
+ color: 'text-orange-600 dark:text-orange-400'
+ },
+ [CONTENT_TYPES.NO_CONTENT]: {
+ label: 'No Readable Content',
+ emoji: 'π΄',
+ description: 'Images, videos, or heavily JavaScript-dependent content',
+ color: 'text-red-600 dark:text-red-400'
+ }
+};
+
+/**
+ * CORS proxy services for fetching external content
+ */
+const CORS_PROXIES = [
+ 'https://api.allorigins.win/get?url=',
+ 'https://corsproxy.io/?',
+ 'https://cors-anywhere.herokuapp.com/',
+ 'https://thingproxy.freeboard.io/fetch/'
+];
+
+/**
+ * Fetch and parse HTML content from URL with CORS proxy fallback
+ */
+export const fetchUrlContent = async (url) => {
+ try {
+ // Validate URL
+ const urlObj = new URL(url);
+ if (!['http:', 'https:'].includes(urlObj.protocol)) {
+ throw new Error('Only HTTP and HTTPS URLs are supported');
+ }
+
+ // First try direct fetch (works for same-origin or CORS-enabled sites)
+ try {
+ const response = await fetch(url, {
+ method: 'GET',
+ headers: {
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+ 'User-Agent': 'Mozilla/5.0 (compatible; TextAnalyzer/1.0)'
+ }
+ });
+
+ if (response.ok) {
+ const contentType = response.headers.get('content-type') || '';
+ if (contentType.includes('text/html')) {
+ const html = await response.text();
+ return { html, url: response.url, contentType };
+ }
+ }
+ } catch (directError) {
+ console.log('Direct fetch failed, trying CORS proxy:', directError.message);
+ }
+
+ // Try CORS proxies
+ let lastError = null;
+
+ for (const proxy of CORS_PROXIES) {
+ try {
+ let proxyUrl;
+ let response;
+
+ if (proxy.includes('allorigins.win')) {
+ // AllOrigins returns JSON with contents
+ proxyUrl = `${proxy}${encodeURIComponent(url)}`;
+ response = await fetch(proxyUrl);
+
+ if (response.ok) {
+ const data = await response.json();
+ if (data.contents) {
+ return {
+ html: data.contents,
+ url: data.status.url || url,
+ contentType: 'text/html'
+ };
+ }
+ }
+ } else {
+ // Other proxies return HTML directly
+ proxyUrl = `${proxy}${url}`;
+ response = await fetch(proxyUrl, {
+ headers: {
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
+ }
+ });
+
+ if (response.ok) {
+ const contentType = response.headers.get('content-type') || 'text/html';
+ if (contentType.includes('text/html') || contentType.includes('text/plain')) {
+ const html = await response.text();
+ return { html, url, contentType };
+ }
+ }
+ }
+ } catch (proxyError) {
+ lastError = proxyError;
+ console.log(`Proxy ${proxy} failed:`, proxyError.message);
+ continue;
+ }
+ }
+
+ throw new Error(`All fetch methods failed. Last error: ${lastError?.message || 'Unknown error'}`);
+
+ } catch (error) {
+ throw new Error(`Failed to fetch content: ${error.message}`);
+ }
+};
+
+/**
+ * Parse HTML and create DOM
+ */
+export const parseHtml = (html) => {
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(html, 'text/html');
+ return doc;
+};
+
+/**
+ * Detect article elements and structure
+ */
+export const detectArticleStructure = (doc) => {
+ const structure = {
+ hasArticleTag: false,
+ hasMainTag: false,
+ headingCount: 0,
+ paragraphCount: 0,
+ hasMetaArticle: false,
+ hasJsonLd: false,
+ wordCount: 0,
+ linkDensity: 0
+ };
+
+ // Check for semantic HTML5 tags
+ structure.hasArticleTag = doc.querySelector('article') !== null;
+ structure.hasMainTag = doc.querySelector('main') !== null;
+
+ // Count headings
+ structure.headingCount = doc.querySelectorAll('h1, h2, h3, h4, h5, h6').length;
+
+ // Count paragraphs
+ structure.paragraphCount = doc.querySelectorAll('p').length;
+
+ // Check meta tags for articles
+ const metaTags = doc.querySelectorAll('meta[property^="og:"], meta[name^="article:"]');
+ structure.hasMetaArticle = Array.from(metaTags).some(meta =>
+ meta.getAttribute('property') === 'og:type' && meta.getAttribute('content') === 'article' ||
+ meta.getAttribute('name')?.startsWith('article:')
+ );
+
+ // Check for JSON-LD structured data
+ const jsonLdScripts = doc.querySelectorAll('script[type="application/ld+json"]');
+ structure.hasJsonLd = Array.from(jsonLdScripts).some(script => {
+ try {
+ const data = JSON.parse(script.textContent);
+ const type = data['@type'] || (Array.isArray(data) ? data[0]['@type'] : null);
+ return type && ['Article', 'NewsArticle', 'BlogPosting'].includes(type);
+ } catch {
+ return false;
+ }
+ });
+
+ return structure;
+};
+
+/**
+ * Extract clean text from article elements
+ */
+export const extractArticleText = (doc) => {
+ const articleSelectors = [
+ 'article',
+ 'main article',
+ '[role="main"] article',
+ '.article-content',
+ '.post-content',
+ '.entry-content',
+ '.content-body'
+ ];
+
+ // Try to find article container
+ let articleContainer = null;
+ for (const selector of articleSelectors) {
+ articleContainer = doc.querySelector(selector);
+ if (articleContainer) break;
+ }
+
+ // If no article container, try main content area
+ if (!articleContainer) {
+ const mainSelectors = ['main', '[role="main"]', '#main', '#content', '.main-content'];
+ for (const selector of mainSelectors) {
+ articleContainer = doc.querySelector(selector);
+ if (articleContainer) break;
+ }
+ }
+
+ // Extract text from container or full document
+ const container = articleContainer || doc.body;
+
+ if (!container) return { text: '', elements: [] };
+
+ // Remove unwanted elements
+ const unwantedSelectors = [
+ 'script', 'style', 'nav', 'header', 'footer', 'aside',
+ '.navigation', '.nav', '.menu', '.sidebar', '.ads', '.advertisement',
+ '.social-share', '.comments', '.related-posts', '.author-bio'
+ ];
+
+ const clone = container.cloneNode(true);
+ unwantedSelectors.forEach(selector => {
+ clone.querySelectorAll(selector).forEach(el => el.remove());
+ });
+
+ // Extract text from meaningful elements
+ const meaningfulElements = clone.querySelectorAll('h1, h2, h3, h4, h5, h6, p, li, blockquote, pre');
+ const elements = Array.from(meaningfulElements).map(el => ({
+ tag: el.tagName.toLowerCase(),
+ text: el.textContent.trim(),
+ length: el.textContent.trim().length
+ })).filter(el => el.length > 0);
+
+ const text = elements.map(el => el.text).join('\n\n');
+
+ return { text, elements };
+};
+
+/**
+ * Extract all visible text from page
+ */
+export const extractAllText = (doc) => {
+ const clone = doc.body.cloneNode(true);
+
+ // Remove unwanted elements
+ const unwantedSelectors = ['script', 'style', 'noscript'];
+ unwantedSelectors.forEach(selector => {
+ clone.querySelectorAll(selector).forEach(el => el.remove());
+ });
+
+ const text = clone.textContent || clone.innerText || '';
+ return text.replace(/\s+/g, ' ').trim();
+};
+
+/**
+ * Calculate content quality metrics
+ */
+export const calculateContentMetrics = (doc, articleText, allText) => {
+ const metrics = {
+ articleWordCount: articleText.split(/\s+/).filter(w => w.length > 0).length,
+ totalWordCount: allText.split(/\s+/).filter(w => w.length > 0).length,
+ contentRatio: 0,
+ linkCount: doc.querySelectorAll('a[href]').length,
+ imageCount: doc.querySelectorAll('img').length,
+ headingCount: doc.querySelectorAll('h1, h2, h3, h4, h5, h6').length,
+ paragraphCount: doc.querySelectorAll('p').length,
+ linkDensity: 0
+ };
+
+ if (metrics.totalWordCount > 0) {
+ metrics.contentRatio = metrics.articleWordCount / metrics.totalWordCount;
+ metrics.linkDensity = metrics.linkCount / metrics.totalWordCount;
+ }
+
+ return metrics;
+};
+
+/**
+ * Classify content type based on structure and metrics
+ */
+export const classifyContent = (structure, metrics, articleText) => {
+ const wordCount = metrics.articleWordCount;
+ const contentRatio = metrics.contentRatio;
+ const hasStructure = structure.hasArticleTag || structure.hasMainTag || structure.hasMetaArticle;
+ const hasGoodStructure = structure.headingCount >= 2 && structure.paragraphCount >= 3;
+
+ // Rich Article Content
+ if ((hasStructure || hasGoodStructure) && wordCount >= 300 && contentRatio > 0.6) {
+ return CONTENT_TYPES.RICH_ARTICLE;
+ }
+
+ // General Web Content
+ if (wordCount >= 100 && contentRatio > 0.3) {
+ return CONTENT_TYPES.GENERAL_CONTENT;
+ }
+
+ // Limited Content
+ if (wordCount >= 20) {
+ return CONTENT_TYPES.LIMITED_CONTENT;
+ }
+
+ // No readable content
+ return CONTENT_TYPES.NO_CONTENT;
+};
+
+/**
+ * Main function to extract and analyze content from URL
+ */
+export const extractContentFromUrl = async (url) => {
+ try {
+ // Fetch content
+ const { html, url: finalUrl, contentType } = await fetchUrlContent(url);
+
+ // Parse HTML
+ const doc = parseHtml(html);
+
+ // Detect article structure
+ const structure = detectArticleStructure(doc);
+
+ // Extract text content
+ const { text: articleText, elements } = extractArticleText(doc);
+ const allText = extractAllText(doc);
+
+ // Calculate metrics
+ const metrics = calculateContentMetrics(doc, articleText, allText);
+
+ // Classify content
+ const contentClassification = classifyContent(structure, metrics, articleText);
+
+ // Get page metadata
+ const title = doc.querySelector('title')?.textContent?.trim() || '';
+ const description = doc.querySelector('meta[name="description"]')?.getAttribute('content') || '';
+
+ return {
+ success: true,
+ url: finalUrl,
+ title,
+ description,
+ contentType: contentClassification,
+ structure,
+ metrics,
+ articleText,
+ allText,
+ elements,
+ extractedAt: new Date().toISOString()
+ };
+
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message,
+ url
+ };
+ }
+};
diff --git a/tailwind.config.js b/tailwind.config.js
index 8ec5a382..9fd42706 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -22,7 +22,12 @@ module.exports = {
},
fontFamily: {
mono: ['JetBrains Mono', 'Monaco', 'Cascadia Code', 'Segoe UI Mono', 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', 'Fira Code', 'Droid Sans Mono', 'Courier New', 'monospace'],
- }
+ },
+ maxWidth: {
+ '1/4': '25%',
+ '1/2': '50%',
+ '3/4': '75%',
+ }
},
},
plugins: [],
diff --git a/temp_export_tabs.js b/temp_export_tabs.js
new file mode 100644
index 00000000..b05a3a89
--- /dev/null
+++ b/temp_export_tabs.js
@@ -0,0 +1,27 @@
+ {/* Export Tabs */}
+
+
+ setActiveExportTab('json')}
+ className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
+ activeExportTab === 'json'
+ ? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
+ : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
+ }`}
+ >
+
+ JSON
+
+ setActiveExportTab('php')}
+ className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
+ activeExportTab === 'php'
+ ? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
+ : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
+ }`}
+ >
+
+ PHP
+
+
+