import React, { useState, useEffect, useRef } from 'react'; import { Plus, Upload, FileText, Globe, Download, X, Table, Trash2, Database, Braces, Code, Eye, Minimize2, Maximize2, Search, ArrowUpDown, AlertTriangle, Edit3, ChevronUp, ChevronDown } from 'lucide-react'; import ToolLayout from '../components/ToolLayout'; import CodeMirrorEditor from '../components/CodeMirrorEditor'; import StructuredEditor from "../components/StructuredEditor"; import SEO from '../components/SEO'; import RelatedTools from '../components/RelatedTools'; import Papa from "papaparse"; const TableEditor = () => { const exportCardRef = useRef(null); const [data, setData] = useState([]); const [columns, setColumns] = useState([]); // Sync table data to localStorage for navigation guard useEffect(() => { localStorage.setItem('tableEditorData', JSON.stringify(data)); }, [data]); const [inputText, setInputText] = useState(""); const [url, setUrl] = useState(""); const [isLoading, setIsLoading] = useState(false); // eslint-disable-next-line no-unused-vars 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 [isTableFullscreen, setIsTableFullscreen] = useState(false); // For fullscreen table view const [frozenColumns, setFrozenColumns] = useState(0); // Number of columns to freeze on horizontal scroll const [columnWidths, setColumnWidths] = useState({}); // Store custom column widths const [resizing, setResizing] = useState(null); // Track which column is being resized 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 const [pasteCollapsed, setPasteCollapsed] = useState(false); // Track if paste input is collapsed const [pasteDataSummary, setPasteDataSummary] = useState(null); // Summary of pasted data const [urlDataSummary, setUrlDataSummary] = useState(null); // Summary of URL fetched data const [fileDataSummary, setFileDataSummary] = useState(null); // Summary of file uploaded data const [exportExpanded, setExportExpanded] = useState(false); // Track if export section is expanded const [usageTipsExpanded, setUsageTipsExpanded] = useState(false); // Track if usage tips is expanded // SQL Export specific state const [sqlTableName, setSqlTableName] = useState(""); // Table name for SQL export const [sqlPrimaryKey, setSqlPrimaryKey] = useState(""); // Primary key column for SQL export // Button feedback states const [copiedButton, setCopiedButton] = useState(null); const [downloadedButton, setDownloadedButton] = useState(null); // Helper function to check if user has data that would be lost 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) => { const tables = {}; const lines = sqlContent.split("\n"); let currentTable = null; let currentSchema = ""; let insideCreateTable = false; let insideInsert = false; 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]; const rows = parseInsertValues(valuesStr); 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) => { 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) => { // 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()); } const processedValues = values.map((val) => { val = String(val).trim(); // Remove quotes and handle NULL if (val === "NULL") return ""; if (val.startsWith("'") && val.endsWith("'")) { let unquoted = val.slice(1, -1); // Handle escaped JSON properly (same logic as main parser) if (unquoted.includes('\\"') || unquoted.includes("\\'")) { // Try to detect if this is escaped JSON const isEscapedJson = (str) => { const unescaped = str.replace(/\\"/g, '"').replace(/\\'/g, "'"); const trimmed = unescaped.trim(); if ( (trimmed.startsWith("{") && trimmed.endsWith("}")) || (trimmed.startsWith("[") && trimmed.endsWith("]")) ) { try { JSON.parse(trimmed); return true; } catch (e) { return false; } } return false; }; // If it's escaped JSON, unescape it if (isEscapedJson(unquoted)) { unquoted = unquoted.replace(/\\"/g, '"').replace(/\\'/g, "'"); } 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, "'"); } 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); }); 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 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) => { 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(""); return tableData.length; // Return actual row count }; // Parse SQL data const parseSqlData = (text) => { 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 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 if (value.includes('\\"') || value.includes("\\'")) { // 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(); if ( (trimmed.startsWith("{") && trimmed.endsWith("}")) || (trimmed.startsWith("[") && trimmed.endsWith("]")) ) { try { JSON.parse(trimmed); return true; } catch (e) { return false; } } return false; }; // If it's escaped JSON, unescape it if (isEscapedJson(value)) { value = value.replace(/\\"/g, '"').replace(/\\'/g, "'"); } else { // Only unescape single quotes for non-JSON strings if (value.includes("\\'")) { value = value.replace(/\\'/g, "'"); } } } 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) => { 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(""); return tableData.length; // Return row count for summary }; // Handle text input const handleTextInput = () => { if (!inputText.trim()) { setError("Please enter some data"); setPasteCollapsed(false); return; } const trimmed = inputText.trim(); let format = ''; let rowCount = 0; try { // Try to detect format if (trimmed.startsWith("[") && trimmed.endsWith("]")) { // JSON array rowCount = parseJsonData(trimmed); format = 'JSON'; } else if ( trimmed.toLowerCase().includes("insert into") && trimmed.toLowerCase().includes("values") ) { // SQL INSERT statements parseSqlData(trimmed); format = 'SQL'; // Get row count from state after parse rowCount = data.length; } else { // CSV/TSV parseData(trimmed, useFirstRowAsHeader); format = trimmed.includes('\t') ? 'TSV' : 'CSV'; // Get row count from state after parse rowCount = data.length; } // Collapse input and show summary setPasteDataSummary({ format: format, size: inputText.length, rows: rowCount || data.length // Use rowCount if available, fallback to data.length }); setPasteCollapsed(true); setCreateNewCompleted(true); setError(''); } catch (err) { // Keep input expanded on error setPasteCollapsed(false); setError(err.message || 'Failed to parse data'); } }; // 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(); let format = ''; let rowCount = 0; if (contentType.includes("application/json") || url.includes(".json")) { rowCount = parseJsonData(text); format = 'JSON'; } else { rowCount = parseData(text, useFirstRowAsHeader); format = text.includes('\t') ? 'TSV' : 'CSV'; } // Set summary for URL fetch setUrlDataSummary({ format: format, size: text.length, rows: rowCount, url: url.trim() }); setCreateNewCompleted(true); } catch (err) { setError(`Failed to fetch data: ${err.message}`); setUrlDataSummary(null); } 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 = "") => { const discoveredTables = parseMultiTableSQL(sqlContent); const tableNames = Object.keys(discoveredTables); 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; setIsLoading(true); setError(""); const reader = new FileReader(); reader.onload = (e) => { try { const content = e.target.result; let format = ''; let rowCount = 0; // Check if it's SQL file for multi-table support if (file.name.toLowerCase().endsWith(".sql")) { initializeTablesFromSQL(content, file.name); format = 'SQL'; // For SQL, we need to wait for state update, use a timeout setTimeout(() => { setFileDataSummary({ format: format, size: content.length, rows: data.length, filename: file.name }); }, 100); } else if (file.name.toLowerCase().endsWith(".json")) { rowCount = parseJsonData(content); format = 'JSON'; setFileDataSummary({ format: format, size: content.length, rows: rowCount, filename: file.name }); } else { rowCount = parseData(content, useFirstRowAsHeader); format = file.name.toLowerCase().endsWith(".tsv") ? 'TSV' : 'CSV'; setFileDataSummary({ format: format, size: content.length, rows: rowCount, filename: file.name }); } setCreateNewCompleted(true); } catch (err) { console.error("❌ File upload error:", err); setError("Failed to read file: " + err.message); setFileDataSummary(null); } 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]); }; // Column resize functions const getColumnWidth = (columnId) => { return columnWidths[columnId] || 150; // Default width }; const handleResizeStart = (e, columnId) => { e.preventDefault(); const startX = e.clientX; const startWidth = getColumnWidth(columnId); setResizing({ columnId, startX, startWidth }); const handleMouseMove = (e) => { if (!resizing && resizing?.columnId === columnId) return; const deltaX = e.clientX - startX; const newWidth = Math.max(50, startWidth + deltaX); // Minimum width of 50px setColumnWidths(prev => ({ ...prev, [columnId]: newWidth })); }; const handleMouseUp = () => { setResizing(null); document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); }; // 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-trigger header editing for the new column setEditingHeader(newColumnId); // Auto-scroll to the right to show the new column and focus on header input setTimeout(() => { // Try multiple selectors to find the table container const selectors = [ '[class*="overflow-auto"][class*="max-h-"]', '.overflow-auto', 'div[class*="overflow-auto"]' ]; let tableContainer = null; for (const selector of selectors) { tableContainer = document.querySelector(selector); if (tableContainer) break; } if (tableContainer) { // Check if horizontal scrolling is needed const needsScroll = tableContainer.scrollWidth > tableContainer.clientWidth; if (needsScroll) { // Smooth scroll to the far right to show the new column tableContainer.scrollTo({ left: tableContainer.scrollWidth - tableContainer.clientWidth, behavior: 'smooth' }); } } // Focus on the header input field after scroll setTimeout(() => { const headerInput = document.querySelector(`input[value="${newColumn.name}"]`); if (headerInput) { headerInput.focus(); headerInput.select(); // Select all text for easy replacement } }, 100); }, 200); }; // 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 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; } // 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) && String(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 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); }; 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" && ( urlDataSummary ? (
✓ Data loaded: {urlDataSummary.format} ({urlDataSummary.size.toLocaleString()} chars, {urlDataSummary.rows} rows)
) : (
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" && ( pasteCollapsed ? (
✓ Data loaded: {pasteDataSummary.format} ({pasteDataSummary.size.toLocaleString()} chars, {pasteDataSummary.rows} rows)
) : (
{error && (

Invalid Data: {error}

)}
) )} {activeTab === "upload" && ( fileDataSummary ? (
✓ File loaded: {fileDataSummary.format} ({fileDataSummary.size.toLocaleString()} chars, {fileDataSummary.rows} rows) - {fileDataSummary.filename}
) : (

🔒 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 && (
Frozen
)}
{/* Table */}
{columns.map((column, index) => { const isFrozen = index < frozenColumns; const leftOffset = isFrozen ? 40 + columns.slice(0, index).reduce((acc, col) => acc + getColumnWidth(col.id), 0) : 0; return ( ); })} {/* System Column - Add Column */} {filteredAndSortedData.map((row) => ( {columns.map((column, index) => { const isFrozen = index < frozenColumns; const leftOffset = isFrozen ? 40 + columns.slice(0, index).reduce((acc, col) => acc + getColumnWidth(col.id), 0) : 0; return (
0 ? "sticky left-0 z-20 bg-blue-50 dark:!bg-blue-900" : "" }`} style={{ width: '40px', maxWidth: '40px', minWidth: '40px' }} > 0 } onChange={(e) => { if (e.target.checked) { setSelectedRows( new Set( filteredAndSortedData.map((row) => row.id), ), ); } else { setSelectedRows(new Set()); } }} className="rounded border-gray-300 dark:border-gray-600" />
{ const newSelected = new Set(selectedColumns); if (e.target.checked) { newSelected.add(column.id); } else { newSelected.delete(column.id); } setSelectedColumns(newSelected); }} onClick={(e) => e.stopPropagation()} className="mr-2" />
{editingHeader === column.id ? ( handleHeaderEdit(column.id, e.target.value) } onBlur={() => setEditingHeader(null)} onKeyDown={(e) => handleHeaderKeyDown(e, column.id) } className="flex-1 bg-transparent border-b border-blue-500 focus:outline-none text-gray-900 dark:text-gray-100 font-medium" autoFocus /> ) : ( setEditingHeader(column.id)} > {column.name} )}
{/* Resize handle */}
handleResizeStart(e, column.id)} title="Drag to resize column" />
0 ? "sticky left-0 z-10 bg-blue-50 dark:!bg-blue-900" : "" }`} style={{ width: '40px', maxWidth: '40px', minWidth: '40px' }} > { const newSelected = new Set(selectedRows); if (e.target.checked) { newSelected.add(row.id); } else { newSelected.delete(row.id); } setSelectedRows(newSelected); }} className="rounded border-gray-300 dark:border-gray-600" /> {editingCell?.rowId === row.id && editingCell?.columnId === column.id ? ( (() => { const cellValue = String(row[column.id] || ""); const isLongValue = cellValue.length > 100 || cellValue.includes("\n"); if (isLongValue) { return (