Files
dewedev/src/pages/TextLengthTool.js

432 lines
17 KiB
JavaScript
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react';
import { Type, Copy, RotateCcw, Globe, Download, AlertCircle, Clock, X } from 'lucide-react';
import ToolLayout from '../components/ToolLayout';
import CopyButton from '../components/CopyButton';
import { extractContentFromUrl, CONTENT_TYPE_INFO } from '../utils/contentExtractor';
const TextLengthTool = () => {
const [text, setText] = useState('');
const [url, setUrl] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [urlResult, setUrlResult] = useState(null);
const [error, setError] = useState('');
const [useArticleOnly, setUseArticleOnly] = useState(true);
const [stats, setStats] = useState({
characters: 0,
charactersNoSpaces: 0,
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]);
// Handle URL fetching
const fetchUrlContent = async () => {
if (!url.trim()) {
setError('Please enter a valid URL');
return;
}
setIsLoading(true);
setError('');
setUrlResult(null);
try {
const result = await extractContentFromUrl(url.trim());
if (result.success) {
setUrlResult(result);
// Set text based on user preference
const textToAnalyze = useArticleOnly ? result.articleText : result.allText;
setText(textToAnalyze);
setError('');
} else {
setError(result.error);
setUrlResult(null);
}
} catch (err) {
let errorMessage = err.message;
if (errorMessage.includes('Failed to fetch')) {
errorMessage = 'Unable to fetch content due to CORS restrictions or network issues';
}
setError(errorMessage);
setUrlResult(null);
} finally {
setIsLoading(false);
}
};
const clearText = () => {
setText('');
setUrlResult(null);
setError('');
};
const clearUrl = () => {
setUrl('');
setUrlResult(null);
setError('');
};
const loadSample = () => {
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}
>
{/* URL Input Section */}
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 mb-6">
<div className="flex items-center gap-2 mb-3">
<Globe className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Analyze Content from URL</h3>
</div>
<div className="space-y-3">
<div className="flex gap-2">
<div className="relative flex-1">
<input
type="url"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="https://example.com/article"
className="tool-input w-full pr-10"
disabled={isLoading}
/>
{url && !isLoading && (
<button
onClick={clearUrl}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-600 hover:text-gray-600 dark:text-gray-600 dark:hover:text-gray-300 transition-colors"
>
<X className="h-4 w-4" />
</button>
)}
</div>
<button
onClick={fetchUrlContent}
disabled={isLoading || !url.trim()}
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-medium px-4 py-2 rounded-md transition-colors flex items-center whitespace-nowrap"
>
{isLoading ? (
<>
<Clock className="h-4 w-4 mr-2 animate-spin" />
Fetching...
</>
) : (
<>
<Download className="h-4 w-4 mr-2" />
Fetch Content
</>
)}
</button>
</div>
{/* URL Result Status */}
{urlResult && (
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-3">
<div className="flex items-start gap-3">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<span className="text-lg">{CONTENT_TYPE_INFO[urlResult.contentType].emoji}</span>
<span className={`font-medium ${CONTENT_TYPE_INFO[urlResult.contentType].color}`}>
{CONTENT_TYPE_INFO[urlResult.contentType].label}
</span>
</div>
<div className="text-sm text-gray-600 dark:text-gray-600 mb-2">
{CONTENT_TYPE_INFO[urlResult.contentType].description}
</div>
{urlResult.title && (
<div className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-1">
{urlResult.title}
</div>
)}
<div className="text-xs text-gray-600 dark:text-gray-600">
Article: {urlResult.metrics.articleWordCount} words
Total: {urlResult.metrics.totalWordCount} words
Ratio: {Math.round(urlResult.metrics.contentRatio * 100)}%
</div>
</div>
<div className="flex items-center gap-2">
<label className="flex items-center text-sm">
<input
type="checkbox"
checked={useArticleOnly}
onChange={(e) => {
setUseArticleOnly(e.target.checked);
const textToAnalyze = e.target.checked ? urlResult.articleText : urlResult.allText;
setText(textToAnalyze);
}}
className="mr-2"
/>
Article Only
</label>
</div>
</div>
</div>
)}
{/* Error Display */}
{error && (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-3">
<div className="flex items-start gap-2">
<AlertCircle className="h-4 w-4 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" />
<div className="flex-1">
<div className="text-sm text-red-700 dark:text-red-300 mb-2">{error}</div>
{error.includes('fetch') && (
<div className="text-xs text-red-600 dark:text-red-400">
<p className="mb-1"><strong>Common solutions:</strong></p>
<ul className="list-disc list-inside space-y-1">
<li>Some websites block cross-origin requests for security</li>
<li>Try copying the article text manually and pasting it below</li>
<li>The site might require JavaScript to load content</li>
<li>Check if the URL is accessible and returns HTML content</li>
</ul>
</div>
)}
</div>
</div>
</div>
)}
</div>
</div>
{/* 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">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
Text Statistics
</h3>
{(stats.characters > 0 || stats.words > 0) && (
<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-3 py-1.5 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>
{/* 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-600">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-600">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-600">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-600">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-600">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-600">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-600">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>
)}
</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> <strong>URL Analysis:</strong> Fetch and analyze content from any web page or article</li>
<li> <strong>Smart Content Detection:</strong> Automatically detects articles vs general web content</li>
<li> <strong>Article vs Full Page:</strong> Choose to analyze just the main article or entire page content</li>
<li> <strong>Real-time Counting:</strong> Updates as you type or paste text</li>
<li> <strong>Reading Time:</strong> Estimates based on average reading speed (225 WPM)</li>
<li> <strong>Content Quality:</strong> Shows content-to-noise ratio for web pages</li>
<li> Perfect for checking character limits for social media, essays, or articles</li>
</ul>
</div>
</ToolLayout>
);
};
export default TextLengthTool;