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
This commit is contained in:
@@ -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() {
|
||||
<Route path="/csv-json" element={<CsvJsonTool />} />
|
||||
<Route path="/beautifier" element={<BeautifierTool />} />
|
||||
<Route path="/diff" element={<DiffTool />} />
|
||||
<Route path="/text-length" element={<TextLengthTool />} />
|
||||
|
||||
</Routes>
|
||||
</Layout>
|
||||
|
||||
@@ -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 (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex flex-col">
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-50 bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700">
|
||||
<header className="sticky top-0 z-50 bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
<Link to="/" className="flex items-center space-x-2">
|
||||
@@ -58,60 +62,62 @@ const Layout = ({ children }) => {
|
||||
</Link>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
{/* Desktop Navigation */}
|
||||
<nav className="hidden md:flex items-center space-x-6">
|
||||
<Link
|
||||
to="/"
|
||||
className={`flex items-center space-x-1 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
isActive('/')
|
||||
? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300'
|
||||
: 'text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Home className="h-4 w-4" />
|
||||
<span>Home</span>
|
||||
</Link>
|
||||
|
||||
{/* Tools Dropdown */}
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
||||
className="flex items-center space-x-1 px-3 py-2 rounded-md text-sm font-medium text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors"
|
||||
{/* Desktop Navigation - only show on homepage */}
|
||||
{!isToolPage && (
|
||||
<nav className="hidden md:flex items-center space-x-6">
|
||||
<Link
|
||||
to="/"
|
||||
className={`flex items-center space-x-1 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
isActive('/')
|
||||
? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300'
|
||||
: 'text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<span>Tools</span>
|
||||
<ChevronDown className={`h-4 w-4 transition-transform ${
|
||||
isDropdownOpen ? 'rotate-180' : ''
|
||||
}`} />
|
||||
</button>
|
||||
<Home className="h-4 w-4" />
|
||||
<span>Home</span>
|
||||
</Link>
|
||||
|
||||
{/* Dropdown Menu */}
|
||||
{isDropdownOpen && (
|
||||
<div className="absolute top-full left-0 mt-2 w-64 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-2 z-50">
|
||||
{tools.map((tool) => {
|
||||
const IconComponent = tool.icon;
|
||||
return (
|
||||
<Link
|
||||
key={tool.path}
|
||||
to={tool.path}
|
||||
onClick={() => 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'
|
||||
}`}
|
||||
>
|
||||
<IconComponent className="h-4 w-4" />
|
||||
<div>
|
||||
<div className="font-medium">{tool.name}</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">{tool.description}</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
{/* Tools Dropdown */}
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
||||
className="flex items-center space-x-1 px-3 py-2 rounded-md text-sm font-medium text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors"
|
||||
>
|
||||
<span>Tools</span>
|
||||
<ChevronDown className={`h-4 w-4 transition-transform ${
|
||||
isDropdownOpen ? 'rotate-180' : ''
|
||||
}`} />
|
||||
</button>
|
||||
|
||||
{/* Dropdown Menu */}
|
||||
{isDropdownOpen && (
|
||||
<div className="absolute top-full left-0 mt-2 w-64 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-2 z-50">
|
||||
{tools.map((tool) => {
|
||||
const IconComponent = tool.icon;
|
||||
return (
|
||||
<Link
|
||||
key={tool.path}
|
||||
to={tool.path}
|
||||
onClick={() => 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'
|
||||
}`}
|
||||
>
|
||||
<IconComponent className="h-4 w-4" />
|
||||
<div>
|
||||
<div className="font-medium">{tool.name}</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">{tool.description}</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
)}
|
||||
|
||||
<ThemeToggle />
|
||||
|
||||
@@ -147,7 +153,7 @@ const Layout = ({ children }) => {
|
||||
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 pt-2 mt-2">
|
||||
<div className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider px-3 py-1">
|
||||
Tools
|
||||
{isToolPage ? 'Switch Tools' : 'Tools'}
|
||||
</div>
|
||||
{tools.map((tool) => {
|
||||
const IconComponent = tool.icon;
|
||||
@@ -177,18 +183,47 @@ const Layout = ({ children }) => {
|
||||
)}
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 mt-16">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="text-center text-gray-600 dark:text-gray-400">
|
||||
<p>© {new Date().getFullYear()} Dewe Toolsites - Developer Tools.</p>
|
||||
<div className="flex flex-1">
|
||||
{/* Tool Sidebar - only show on tool pages */}
|
||||
{isToolPage && (
|
||||
<div className="hidden lg:block flex-shrink-0">
|
||||
<ToolSidebar />
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)}
|
||||
|
||||
{/* Main Content Area */}
|
||||
<main className={`flex-1 flex flex-col ${isToolPage ? 'overflow-hidden' : ''}`}>
|
||||
{isToolPage ? (
|
||||
<div className="flex-1 overflow-auto">
|
||||
<div className="px-4 sm:px-6 lg:px-8 py-8">
|
||||
{children}
|
||||
</div>
|
||||
{/* Footer for tool pages - inside scrollable content */}
|
||||
<footer className="bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="text-center text-gray-600 dark:text-gray-400">
|
||||
<p>© {new Date().getFullYear()} Dewe Toolsites - Developer Tools.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{children}
|
||||
</div>
|
||||
{/* Footer for homepage */}
|
||||
<footer className="bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 mt-16">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="text-center text-gray-600 dark:text-gray-400">
|
||||
<p>© {new Date().getFullYear()} Dewe Toolsites - Developer Tools.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center text-primary-600 hover:text-primary-700 mb-4"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Back to Tools
|
||||
</Link>
|
||||
|
||||
<div className="flex items-center space-x-3 mb-2">
|
||||
{Icon && <Icon className="h-8 w-8 text-primary-600" />}
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
|
||||
113
src/components/ToolSidebar.js
Normal file
113
src/components/ToolSidebar.js
Normal file
@@ -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 (
|
||||
<div className={`bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 transition-all duration-300 sticky top-16 ${
|
||||
isCollapsed ? 'w-16' : 'w-64'
|
||||
}`} style={{ height: 'calc(100vh - 4rem)' }}>
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Sidebar Header */}
|
||||
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between">
|
||||
{!isCollapsed && (
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Tools
|
||||
</h2>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||
className="p-1 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
{isCollapsed ? (
|
||||
<ChevronRight className="h-4 w-4 text-gray-500" />
|
||||
) : (
|
||||
<ChevronLeft className="h-4 w-4 text-gray-500" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Search - only show when not collapsed */}
|
||||
{!isCollapsed && (
|
||||
<div className="relative mt-3">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search tools..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tools List */}
|
||||
<div className="flex-1 overflow-y-auto py-2">
|
||||
<nav className="space-y-1 px-2">
|
||||
{filteredTools.map((tool) => {
|
||||
const IconComponent = tool.icon;
|
||||
return (
|
||||
<Link
|
||||
key={tool.path}
|
||||
to={tool.path}
|
||||
className={`group flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors ${
|
||||
isActive(tool.path)
|
||||
? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300'
|
||||
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-50 dark:text-gray-300 dark:hover:text-white dark:hover:bg-gray-700'
|
||||
}`}
|
||||
title={isCollapsed ? tool.name : ''}
|
||||
>
|
||||
<IconComponent className={`h-5 w-5 ${isCollapsed ? '' : 'mr-3'} flex-shrink-0`} />
|
||||
{!isCollapsed && (
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium truncate">{tool.name}</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 truncate">
|
||||
{tool.description}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
{!isCollapsed && (
|
||||
<div className="p-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 text-center">
|
||||
Quick access to all tools
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolSidebar;
|
||||
@@ -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']
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -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);
|
||||
let endQuotePos = -1;
|
||||
|
||||
if (bytes.length < byteLength) {
|
||||
throw new Error(`String byte length mismatch: expected ${byteLength}, got ${bytes.length} (remaining) at position ${startIndex}`);
|
||||
// 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;
|
||||
// Extract the actual string content
|
||||
const stringVal = str.substring(startIndex, endQuotePos);
|
||||
const actualByteLength = new TextEncoder().encode(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}`);
|
||||
}
|
||||
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 ? '...' : ''}"`);
|
||||
|
||||
// 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}`);
|
||||
// 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 */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{mode === 'serialize' ? 'Serialized Output' : 'JSON Output'}
|
||||
</label>
|
||||
<div className="flex justify-between items-center">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{mode === 'serialize' ? 'Serialized Output' : 'JSON Output'}
|
||||
</label>
|
||||
{mode === 'unserialize' && isValidJsonOutput() && (
|
||||
<button
|
||||
onClick={openInVisualEditor}
|
||||
className="flex items-center space-x-1 px-3 py-1 text-xs font-medium text-primary-600 dark:text-primary-400 bg-primary-50 dark:bg-primary-900/20 border border-primary-200 dark:border-primary-800 rounded-md hover:bg-primary-100 dark:hover:bg-primary-900/30 transition-colors"
|
||||
>
|
||||
<Edit3 className="h-3 w-3" />
|
||||
<span>View in Visual Editor</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative">
|
||||
<textarea
|
||||
value={output}
|
||||
|
||||
265
src/pages/TextLengthTool.js
Normal file
265
src/pages/TextLengthTool.js
Normal file
@@ -0,0 +1,265 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Type, Copy, RotateCcw } from 'lucide-react';
|
||||
import ToolLayout from '../components/ToolLayout';
|
||||
import CopyButton from '../components/CopyButton';
|
||||
|
||||
const TextLengthTool = () => {
|
||||
const [text, setText] = useState('');
|
||||
const [stats, setStats] = useState({
|
||||
characters: 0,
|
||||
charactersNoSpaces: 0,
|
||||
words: 0,
|
||||
sentences: 0,
|
||||
paragraphs: 0,
|
||||
lines: 0,
|
||||
bytes: 0
|
||||
});
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
|
||||
// Calculate text statistics
|
||||
useEffect(() => {
|
||||
const calculateStats = () => {
|
||||
if (!text) {
|
||||
setStats({
|
||||
characters: 0,
|
||||
charactersNoSpaces: 0,
|
||||
words: 0,
|
||||
sentences: 0,
|
||||
paragraphs: 0,
|
||||
lines: 0,
|
||||
bytes: 0
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Characters
|
||||
const characters = text.length;
|
||||
const charactersNoSpaces = text.replace(/\s/g, '').length;
|
||||
|
||||
// Words (split by whitespace and filter empty strings)
|
||||
const words = text.trim() ? text.trim().split(/\s+/).length : 0;
|
||||
|
||||
// Sentences (split by sentence endings)
|
||||
const sentences = text.trim() ? text.split(/[.!?]+/).filter(s => s.trim().length > 0).length : 0;
|
||||
|
||||
// Paragraphs (split by double line breaks or more)
|
||||
const paragraphs = text.trim() ? text.split(/\n\s*\n/).filter(p => p.trim().length > 0).length : 0;
|
||||
|
||||
// Lines (split by line breaks)
|
||||
const lines = text ? text.split('\n').length : 0;
|
||||
|
||||
// Bytes (UTF-8 encoding)
|
||||
const bytes = new TextEncoder().encode(text).length;
|
||||
|
||||
setStats({
|
||||
characters,
|
||||
charactersNoSpaces,
|
||||
words,
|
||||
sentences,
|
||||
paragraphs,
|
||||
lines,
|
||||
bytes
|
||||
});
|
||||
};
|
||||
|
||||
calculateStats();
|
||||
}, [text]);
|
||||
|
||||
const clearText = () => {
|
||||
setText('');
|
||||
};
|
||||
|
||||
const loadSample = () => {
|
||||
setText(`Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
|
||||
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
|
||||
|
||||
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum! Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium?`);
|
||||
};
|
||||
|
||||
const formatNumber = (num) => {
|
||||
return num.toLocaleString();
|
||||
};
|
||||
|
||||
const getReadingTime = () => {
|
||||
// Average reading speed: 200-250 words per minute
|
||||
const wordsPerMinute = 225;
|
||||
const minutes = Math.ceil(stats.words / wordsPerMinute);
|
||||
return minutes === 1 ? '1 minute' : `${minutes} minutes`;
|
||||
};
|
||||
|
||||
const getTypingTime = () => {
|
||||
// Average typing speed: 40 words per minute
|
||||
const wordsPerMinute = 40;
|
||||
const minutes = Math.ceil(stats.words / wordsPerMinute);
|
||||
return minutes === 1 ? '1 minute' : `${minutes} minutes`;
|
||||
};
|
||||
|
||||
return (
|
||||
<ToolLayout
|
||||
title="Text Length Checker"
|
||||
description="Analyze text length, word count, and other text statistics"
|
||||
icon={Type}
|
||||
>
|
||||
{/* Controls */}
|
||||
<div className="flex flex-wrap gap-3 mb-6">
|
||||
<button onClick={loadSample} className="tool-button-secondary">
|
||||
Load Sample Text
|
||||
</button>
|
||||
<button onClick={clearText} className="tool-button-secondary flex items-center whitespace-nowrap">
|
||||
<RotateCcw className="h-4 w-4 mr-2" />
|
||||
Clear Text
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
className="tool-button-secondary"
|
||||
>
|
||||
{showDetails ? 'Hide' : 'Show'} Details
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Text Input */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Text to Analyze
|
||||
</label>
|
||||
<div className="relative">
|
||||
<textarea
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
placeholder="Enter or paste your text here to analyze its length and statistics..."
|
||||
className="tool-input h-96 resize-none"
|
||||
style={{ minHeight: '400px' }}
|
||||
/>
|
||||
{text && <CopyButton text={text} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Statistics */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Text Statistics
|
||||
</h3>
|
||||
|
||||
{/* Main Stats Grid */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
||||
<div className="text-2xl font-bold text-primary-600 dark:text-primary-400">
|
||||
{formatNumber(stats.characters)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Characters</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
||||
<div className="text-2xl font-bold text-primary-600 dark:text-primary-400">
|
||||
{formatNumber(stats.charactersNoSpaces)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Characters (no spaces)</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
||||
<div className="text-2xl font-bold text-green-600 dark:text-green-400">
|
||||
{formatNumber(stats.words)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Words</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
||||
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400">
|
||||
{formatNumber(stats.lines)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Lines</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Additional Stats (when details are shown) */}
|
||||
{showDetails && (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
||||
<div className="text-xl font-bold text-purple-600 dark:text-purple-400">
|
||||
{formatNumber(stats.sentences)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Sentences</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
||||
<div className="text-xl font-bold text-orange-600 dark:text-orange-400">
|
||||
{formatNumber(stats.paragraphs)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Paragraphs</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
||||
<div className="text-xl font-bold text-red-600 dark:text-red-400">
|
||||
{formatNumber(stats.bytes)} bytes
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Size (UTF-8 encoding)</div>
|
||||
</div>
|
||||
|
||||
{/* Reading & Typing Time */}
|
||||
{stats.words > 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<div className="text-lg font-semibold text-blue-800 dark:text-blue-200">
|
||||
📖 {getReadingTime()}
|
||||
</div>
|
||||
<div className="text-sm text-blue-600 dark:text-blue-400">Estimated reading time</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-4">
|
||||
<div className="text-lg font-semibold text-green-800 dark:text-green-200">
|
||||
⌨️ {getTypingTime()}
|
||||
</div>
|
||||
<div className="text-sm text-green-600 dark:text-green-400">Estimated typing time</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Copy Statistics */}
|
||||
{(stats.characters > 0 || stats.words > 0) && (
|
||||
<div className="mt-4">
|
||||
<button
|
||||
onClick={() => {
|
||||
const statsText = `Text Statistics:
|
||||
Characters: ${formatNumber(stats.characters)}
|
||||
Characters (no spaces): ${formatNumber(stats.charactersNoSpaces)}
|
||||
Words: ${formatNumber(stats.words)}
|
||||
Lines: ${formatNumber(stats.lines)}
|
||||
Sentences: ${formatNumber(stats.sentences)}
|
||||
Paragraphs: ${formatNumber(stats.paragraphs)}
|
||||
Bytes: ${formatNumber(stats.bytes)}
|
||||
${stats.words > 0 ? `Reading time: ${getReadingTime()}
|
||||
Typing time: ${getTypingTime()}` : ''}`;
|
||||
navigator.clipboard.writeText(statsText);
|
||||
}}
|
||||
className="flex items-center space-x-2 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
<span>Copy Statistics</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Usage Tips */}
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md p-4 mt-6">
|
||||
<h4 className="text-blue-800 dark:text-blue-200 font-medium mb-2">Usage Tips</h4>
|
||||
<ul className="text-blue-700 dark:text-blue-300 text-sm space-y-1">
|
||||
<li>• Perfect for checking character limits for social media posts, essays, or articles</li>
|
||||
<li>• Real-time counting updates as you type or paste text</li>
|
||||
<li>• Includes reading and typing time estimates based on average speeds</li>
|
||||
<li>• Byte count shows the actual storage size of your text in UTF-8 encoding</li>
|
||||
<li>• Use "Show Details" to see additional statistics like sentences and paragraphs</li>
|
||||
</ul>
|
||||
</div>
|
||||
</ToolLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default TextLengthTool;
|
||||
Reference in New Issue
Block a user