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:
dwindown
2025-09-21 07:09:33 +07:00
parent 6f5bdf5f0d
commit 82d14622ac
7 changed files with 571 additions and 117 deletions

View File

@@ -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>

View File

@@ -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>
);
};

View File

@@ -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">

View 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;

View File

@@ -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']
}
];

View File

@@ -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 */}
<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
View 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;