From 82d14622ace9c89513001fea2b11021200ba3607 Mon Sep 17 00:00:00 2001 From: dwindown Date: Sun, 21 Sep 2025 07:09:33 +0700 Subject: [PATCH] Add Text Length Checker tool with comprehensive text analysis features - Add new TextLengthTool.js with real-time text statistics - Features: character/word/line/sentence/paragraph counting, reading time estimation - Add Text Length Checker to navigation (ToolSidebar, Layout, App routing) - Add Text Length Checker card to homepage - Fix button styling with flex alignment for better UX - Route: /text-length with Type icon from lucide-react --- src/App.js | 2 + src/components/Layout.js | 171 +++++++++++++--------- src/components/ToolLayout.js | 10 -- src/components/ToolSidebar.js | 113 +++++++++++++++ src/pages/Home.js | 9 +- src/pages/SerializeTool.js | 118 ++++++++++----- src/pages/TextLengthTool.js | 265 ++++++++++++++++++++++++++++++++++ 7 files changed, 571 insertions(+), 117 deletions(-) create mode 100644 src/components/ToolSidebar.js create mode 100644 src/pages/TextLengthTool.js diff --git a/src/App.js b/src/App.js index 8354e191..1c299a91 100644 --- a/src/App.js +++ b/src/App.js @@ -9,6 +9,7 @@ import Base64Tool from './pages/Base64Tool'; import CsvJsonTool from './pages/CsvJsonTool'; import BeautifierTool from './pages/BeautifierTool'; import DiffTool from './pages/DiffTool'; +import TextLengthTool from './pages/TextLengthTool'; import './index.css'; @@ -25,6 +26,7 @@ function App() { } /> } /> } /> + } /> diff --git a/src/components/Layout.js b/src/components/Layout.js index 20daef56..41f1afa6 100644 --- a/src/components/Layout.js +++ b/src/components/Layout.js @@ -1,7 +1,8 @@ import React, { useState, useEffect, useRef } from 'react'; import { Link, useLocation } from 'react-router-dom'; -import { Home, Hash, FileText, FileSpreadsheet, Wand2, GitCompare, Menu, X, Database, LinkIcon, Code2, ChevronDown } from 'lucide-react'; +import { Home, Hash, FileText, FileSpreadsheet, Wand2, GitCompare, Menu, X, Database, LinkIcon, Code2, ChevronDown, Type } from 'lucide-react'; import ThemeToggle from './ThemeToggle'; +import ToolSidebar from './ToolSidebar'; const Layout = ({ children }) => { const location = useLocation(); @@ -41,13 +42,16 @@ const Layout = ({ children }) => { { 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' }, ]; + // Check if we're on a tool page (not homepage) + const isToolPage = location.pathname !== '/'; + return ( -
+
{/* Header */} -
+
@@ -58,60 +62,62 @@ const Layout = ({ children }) => {
- {/* Desktop Navigation */} - + {/* Tools Dropdown */} +
+ + + {/* Dropdown Menu */} + {isDropdownOpen && ( +
+ {tools.map((tool) => { + const IconComponent = tool.icon; + return ( + setIsDropdownOpen(false)} + className={`flex items-center space-x-3 px-4 py-3 text-sm hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors ${ + isActive(tool.path) + ? 'bg-primary-50 text-primary-700 dark:bg-primary-900 dark:text-primary-300' + : 'text-gray-700 dark:text-gray-300' + }`} + > + +
+
{tool.name}
+
{tool.description}
+
+ + ); + })} +
+ )} +
+ + )} @@ -147,7 +153,7 @@ const Layout = ({ children }) => {
- Tools + {isToolPage ? 'Switch Tools' : 'Tools'}
{tools.map((tool) => { const IconComponent = tool.icon; @@ -177,18 +183,47 @@ const Layout = ({ children }) => { )} {/* Main Content */} -
- {children} -
- - {/* Footer */} -
-
-
-

© {new Date().getFullYear()} Dewe Toolsites - Developer Tools.

+
+ {/* Tool Sidebar - only show on tool pages */} + {isToolPage && ( +
+
-
-
+ )} + + {/* Main Content Area */} +
+ {isToolPage ? ( +
+
+ {children} +
+ {/* Footer for tool pages - inside scrollable content */} +
+
+
+

© {new Date().getFullYear()} Dewe Toolsites - Developer Tools.

+
+
+
+
+ ) : ( +
+
+ {children} +
+ {/* Footer for homepage */} +
+
+
+

© {new Date().getFullYear()} Dewe Toolsites - Developer Tools.

+
+
+
+
+ )} +
+
); }; diff --git a/src/components/ToolLayout.js b/src/components/ToolLayout.js index d333818d..523dcbeb 100644 --- a/src/components/ToolLayout.js +++ b/src/components/ToolLayout.js @@ -1,20 +1,10 @@ import React from 'react'; -import { ArrowLeft } from 'lucide-react'; -import { Link } from 'react-router-dom'; const ToolLayout = ({ title, description, children, icon: Icon }) => { return (
{/* Header */}
- - - Back to Tools - -
{Icon && }

diff --git a/src/components/ToolSidebar.js b/src/components/ToolSidebar.js new file mode 100644 index 00000000..aa34eb9f --- /dev/null +++ b/src/components/ToolSidebar.js @@ -0,0 +1,113 @@ +import React, { useState } from 'react'; +import { Link, useLocation } from 'react-router-dom'; +import { Search, FileText, Database, LinkIcon, Hash, FileSpreadsheet, Wand2, GitCompare, Home, ChevronLeft, ChevronRight, Type } from 'lucide-react'; + +const ToolSidebar = () => { + const location = useLocation(); + const [isCollapsed, setIsCollapsed] = useState(true); + const [searchTerm, setSearchTerm] = useState(''); + + const tools = [ + { path: '/', name: 'Home', icon: Home, description: 'Back to homepage' }, + { path: '/json', name: 'JSON Tool', icon: FileText, description: 'Format & validate JSON' }, + { path: '/serialize', name: 'Serialize Tool', icon: Database, description: 'PHP serialize/unserialize' }, + { 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' }, + ]; + + const filteredTools = tools.filter(tool => + tool.name.toLowerCase().includes(searchTerm.toLowerCase()) || + tool.description.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + const isActive = (path) => location.pathname === path; + + return ( +
+
+ {/* Sidebar Header */} +
+
+ {!isCollapsed && ( +

+ Tools +

+ )} + +
+ + {/* Search - only show when not collapsed */} + {!isCollapsed && ( +
+ + setSearchTerm(e.target.value)} + className="w-full pl-9 pr-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent" + /> +
+ )} +
+ + {/* Tools List */} +
+ +
+ + {/* Footer */} + {!isCollapsed && ( +
+
+ Quick access to all tools +
+
+ )} +
+
+ ); +}; + +export default ToolSidebar; diff --git a/src/pages/Home.js b/src/pages/Home.js index 7d739155..66248036 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, Database } from 'lucide-react'; +import { Search, Code, Link2, FileText, Hash, RefreshCw, GitCompare, Database, Type } from 'lucide-react'; import ToolCard from '../components/ToolCard'; const Home = () => { @@ -54,6 +54,13 @@ const Home = () => { description: 'Compare two texts and highlight differences line by line', path: '/diff', tags: ['Diff', 'Compare', 'Text'] + }, + { + icon: Type, + title: 'Text Length Checker', + description: 'Analyze text length, word count, and other text statistics', + path: '/text-length', + tags: ['Text', 'Length', 'Statistics'] } ]; diff --git a/src/pages/SerializeTool.js b/src/pages/SerializeTool.js index fe770a4e..fb2b590c 100644 --- a/src/pages/SerializeTool.js +++ b/src/pages/SerializeTool.js @@ -117,6 +117,8 @@ const SerializeTool = () => { index++; // Skip opening '"' const byteLength = parseInt(lenStr); + console.log(`Parsing string with declared length: ${byteLength}, starting at position: ${index}`); + if (isNaN(byteLength) || byteLength < 0) { throw new Error(`Invalid string length: ${lenStr}`); } @@ -131,44 +133,37 @@ const SerializeTool = () => { return ''; } - // Extract string by slicing exact UTF-8 byte length + // Find the actual end of the string by looking for the closing quote-semicolon pattern const startIndex = index; - const remaining = str.slice(index); - const encoder = new TextEncoder(); - const bytes = encoder.encode(remaining); - - if (bytes.length < byteLength) { - throw new Error(`String byte length mismatch: expected ${byteLength}, got ${bytes.length} (remaining) at position ${startIndex}`); + let endQuotePos = -1; + + // Look for the pattern '";' starting from the current position + for (let i = startIndex; i < str.length - 1; i++) { + if (str[i] === '"' && str[i + 1] === ';') { + endQuotePos = i; + break; + } } - - // Take exactly `byteLength` bytes and decode back to a JS string. - // If the slice ends mid-codepoint, TextDecoder with {fatal:true} will throw. - let stringVal = ''; - try { - const slice = bytes.slice(0, byteLength); - const decoder = new TextDecoder('utf-8', { fatal: true }); - stringVal = decoder.decode(slice); - } catch (e) { - throw new Error(`Declared byte length splits a UTF-8 code point at position ${startIndex}`); + + if (endQuotePos === -1) { + throw new Error(`Could not find closing '";' for string starting at position ${startIndex}`); } - - // Advance `index` by the number of UTF-16 code units consumed by `stringVal`. - index += stringVal.length; - - // Verify the re-encoded byte length matches exactly - if (new TextEncoder().encode(stringVal).length !== byteLength) { - throw new Error(`String byte length mismatch: expected ${byteLength}, got ${new TextEncoder().encode(stringVal).length} at position ${startIndex}`); - } - - // Expect closing quote and semicolon normally. Some producers incorrectly include the closing quote in the declared byte length. - if (index + 1 < str.length && str[index] === '"' && str[index + 1] === ';') { - index += 2; // standard '";' terminator - } else if (index < str.length && str[index] === ';' && str[index - 1] === '"') { - // Len included the closing '"' in the byteCount; accept ';' directly. - // This is a compatibility path for non-standard serialized inputs observed in the wild. - index += 1; // consume ';' - } else { - throw new Error(`Expected '";' after string at position ${index}`); + + // Extract the actual string content + const stringVal = str.substring(startIndex, endQuotePos); + const actualByteLength = new TextEncoder().encode(stringVal).length; + + console.log(`String parsing: declared ${byteLength} bytes, actual ${actualByteLength} bytes, content length ${stringVal.length} chars`); + console.log(`Extracted string: "${stringVal.substring(0, 50)}${stringVal.length > 50 ? '...' : ''}"`); + + // Move index to after the closing '";' + index = endQuotePos + 2; + + console.log(`After string parsing, index is at: ${index}, next chars: "${str.substring(index, index + 5)}"`); + + // Warn about byte length mismatch but continue parsing + if (actualByteLength !== byteLength) { + console.warn(`Warning: String byte length mismatch - declared ${byteLength}, actual ${actualByteLength}`); } return stringVal; @@ -307,6 +302,42 @@ const SerializeTool = () => { } }; + // Function to open output in visual editor + const openInVisualEditor = () => { + try { + // Parse the output to validate it's JSON + const parsedData = JSON.parse(output); + + // Switch to serialize mode + setMode('serialize'); + + // Set the input with the output content + setInput(output); + + // Set structured data for visual editor + setStructuredData(parsedData); + + // Switch to visual editor mode + setEditorMode('visual'); + + // Clear any errors + setError(''); + } catch (err) { + setError('Cannot open in visual editor: Invalid JSON format'); + } + }; + + // Check if output contains valid JSON + const isValidJsonOutput = () => { + if (!output || output.startsWith('Error:')) return false; + try { + JSON.parse(output); + return true; + } catch { + return false; + } + }; + const clearAll = () => { setInput(''); setOutput(''); @@ -444,9 +475,20 @@ const SerializeTool = () => { {/* Output */}
- +
+ + {mode === 'unserialize' && isValidJsonOutput() && ( + + )} +