Improve ObjectEditor and Add TableEditor
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Search, Code, Link2, FileText, Hash, RefreshCw, GitCompare, Type, Edit3 } from 'lucide-react';
|
||||
import { Search, Code, Link2, FileText, Hash, RefreshCw, GitCompare, Type, Edit3, Table } from 'lucide-react';
|
||||
import ToolCard from '../components/ToolCard';
|
||||
|
||||
const Home = () => {
|
||||
@@ -14,6 +14,13 @@ const Home = () => {
|
||||
path: '/object-editor',
|
||||
tags: ['Visual', 'JSON', 'PHP', 'Objects', 'Editor']
|
||||
},
|
||||
{
|
||||
icon: Table,
|
||||
title: 'Table Editor',
|
||||
description: 'Import, edit, and export tabular data from URLs, files, or paste CSV/JSON',
|
||||
path: '/table-editor',
|
||||
tags: ['Table', 'CSV', 'JSON', 'Data', 'Editor']
|
||||
},
|
||||
{
|
||||
icon: Link2,
|
||||
title: 'URL Encoder/Decoder',
|
||||
@@ -28,13 +35,6 @@ const Home = () => {
|
||||
path: '/base64',
|
||||
tags: ['Base64', 'Encode', 'Binary']
|
||||
},
|
||||
{
|
||||
icon: RefreshCw,
|
||||
title: 'CSV ↔ JSON Converter',
|
||||
description: 'Convert between CSV and JSON formats with custom delimiters',
|
||||
path: '/csv-json',
|
||||
tags: ['CSV', 'JSON', 'Convert']
|
||||
},
|
||||
{
|
||||
icon: FileText,
|
||||
title: 'Code Beautifier/Minifier',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
3637
src/pages/TableEditor.js
Normal file
3637
src/pages/TableEditor.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,16 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Type, Copy, RotateCcw } from 'lucide-react';
|
||||
import { Type, Copy, RotateCcw, Globe, Download, AlertCircle, CheckCircle, 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,
|
||||
@@ -65,8 +71,52 @@ const TextLengthTool = () => {
|
||||
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 = () => {
|
||||
@@ -101,6 +151,121 @@ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deseru
|
||||
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-400 hover:text-gray-600 dark:text-gray-500 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-400 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-500 dark:text-gray-400">
|
||||
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">
|
||||
@@ -138,9 +303,32 @@ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deseru
|
||||
|
||||
{/* Statistics */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Text Statistics
|
||||
</h3>
|
||||
<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">
|
||||
@@ -220,30 +408,6 @@ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deseru
|
||||
</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>
|
||||
|
||||
@@ -251,11 +415,13 @@ Typing time: ${getTypingTime()}` : ''}`;
|
||||
<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>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user