Improve ObjectEditor and Add TableEditor

This commit is contained in:
dwindown
2025-09-23 14:17:13 +07:00
parent cf750114f7
commit 977e784df2
15 changed files with 5329 additions and 345 deletions

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Layout from './components/Layout';
import ErrorBoundary from './components/ErrorBoundary';
import Home from './pages/Home';
import JsonTool from './pages/JsonTool';
import SerializeTool from './pages/SerializeTool';
@@ -11,28 +12,32 @@ import BeautifierTool from './pages/BeautifierTool';
import DiffTool from './pages/DiffTool';
import TextLengthTool from './pages/TextLengthTool';
import ObjectEditor from './pages/ObjectEditor';
import TableEditor from './pages/TableEditor';
import './index.css';
function App() {
return (
<Router>
<Layout>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/json" element={<JsonTool />} />
<Route path="/serialize" element={<SerializeTool />} />
<Route path="/url" element={<UrlTool />} />
<Route path="/base64" element={<Base64Tool />} />
<Route path="/csv-json" element={<CsvJsonTool />} />
<Route path="/beautifier" element={<BeautifierTool />} />
<Route path="/diff" element={<DiffTool />} />
<Route path="/text-length" element={<TextLengthTool />} />
<Route path="/object-editor" element={<ObjectEditor />} />
<ErrorBoundary>
<Router>
<Layout>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/json" element={<JsonTool />} />
<Route path="/serialize" element={<SerializeTool />} />
<Route path="/url" element={<UrlTool />} />
<Route path="/base64" element={<Base64Tool />} />
<Route path="/csv-json" element={<CsvJsonTool />} />
<Route path="/beautifier" element={<BeautifierTool />} />
<Route path="/diff" element={<DiffTool />} />
<Route path="/text-length" element={<TextLengthTool />} />
<Route path="/object-editor" element={<ObjectEditor />} />
<Route path="/table-editor" element={<TableEditor />} />
</Routes>
</Layout>
</Router>
</Routes>
</Layout>
</Router>
</ErrorBoundary>
);
}

View File

@@ -0,0 +1,75 @@
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// Log the error for debugging
this.setState({
error: error,
errorInfo: errorInfo
});
// You can also log the error to an error reporting service here
console.error('ErrorBoundary caught an error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
// Fallback UI
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center p-4">
<div className="max-w-md w-full bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 text-center">
<div className="mb-4">
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 dark:bg-red-900/20">
<svg className="h-6 w-6 text-red-600 dark:text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
</div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
Something went wrong
</h2>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
The application encountered an error. This might be due to browser compatibility issues.
</p>
<div className="space-y-3">
<button
onClick={() => window.location.reload()}
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md transition-colors"
>
Reload Page
</button>
<button
onClick={() => this.setState({ hasError: false, error: null, errorInfo: null })}
className="w-full bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200 font-medium py-2 px-4 rounded-md transition-colors"
>
Try Again
</button>
</div>
<div className="mt-4 text-xs text-gray-500 dark:text-gray-400">
If you're using Telegram's built-in browser, try opening this link in your default browser for better compatibility.
</div>
</div>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect, useRef } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Home, Hash, FileSpreadsheet, Wand2, GitCompare, Menu, X, LinkIcon, Code2, ChevronDown, Type, Edit3 } from 'lucide-react';
import { Home, Hash, FileSpreadsheet, Wand2, GitCompare, Menu, X, LinkIcon, Code2, ChevronDown, Type, Edit3, Table } from 'lucide-react';
import ThemeToggle from './ThemeToggle';
import ToolSidebar from './ToolSidebar';
@@ -36,6 +36,7 @@ const Layout = ({ children }) => {
const tools = [
{ path: '/object-editor', name: 'Object Editor', icon: Edit3, description: 'Visual editor for JSON & PHP objects' },
{ path: '/table-editor', name: 'Table Editor', icon: Table, description: 'Import, edit & export tabular data' },
{ 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' },

View File

@@ -106,9 +106,17 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
delete current[key];
}
// Check if we're removing from root level and it's the last property
if (parentPath === 'root' && Object.keys(newData).length === 0) {
// Add an empty property to maintain initial state, like TableEditor maintains at least one row
newData[''] = '';
console.log('🔄 ADDED INITIAL EMPTY PROPERTY - Last root property was deleted');
}
console.log('🗑️ REMOVE PROPERTY - After:', {
parentPath,
remainingKeys: Array.isArray(current) ? current.length : Object.keys(current)
remainingKeys: Array.isArray(current) ? current.length : Object.keys(current),
totalRootKeys: Object.keys(newData).length
});
updateData(newData);
@@ -223,6 +231,25 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
return current;
};
// Helper function to display string values with proper unescaping
const getDisplayValue = (value) => {
if (value === null) return 'null';
if (value === undefined) return '';
const stringValue = value.toString();
// If it's a string, unescape common JSON escape sequences for display
if (typeof value === 'string') {
return stringValue
.replace(/\\"/g, '"') // Unescape quotes
.replace(/\\'/g, "'") // Unescape single quotes
.replace(/\\\//g, '/') // Unescape forward slashes
.replace(/\\\\/g, '\\'); // Unescape backslashes (do this last)
}
return stringValue;
};
const renameKey = (oldKey, newKey, path) => {
if (oldKey === newKey || !newKey.trim()) return;
@@ -401,11 +428,7 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
) : (
typeof value === 'string' && value.includes('\n') ? (
<textarea
value={
value === null ? 'null' :
value === undefined ? '' :
value.toString()
}
value={getDisplayValue(value)}
onChange={(e) => updateValue(e.target.value, path)}
className="flex-1 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 min-w-0 resize-y"
placeholder="Long text value"
@@ -414,11 +437,7 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
) : (
<input
type="text"
value={
value === null ? 'null' :
value === undefined ? '' :
value.toString()
}
value={getDisplayValue(value)}
onChange={(e) => updateValue(e.target.value, path)}
className="flex-1 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 min-w-0"
placeholder="Value"
@@ -500,17 +519,9 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
};
return (
<div className="border border-gray-300 dark:border-gray-600 rounded-lg p-4 bg-white dark:bg-gray-800 min-h-96 w-full">
<div className="flex items-center justify-between mb-4">
<div className="min-h-96 w-full">
<div className="mb-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">Structured Data Editor</h3>
<button
onClick={() => addProperty(data, 'root')}
className="flex items-center space-x-1 px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors flex-shrink-0"
>
<Plus className="h-4 w-4" />
<span className="hidden sm:inline">Add Property</span>
<span className="sm:hidden">Add</span>
</button>
</div>
<div className="overflow-x-hidden">
@@ -524,6 +535,15 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
renderValue(value, key, `root.${key}`, 'root')
)
)}
{/* Root level Add Property button */}
<button
onClick={() => addProperty(data, 'root')}
className="flex items-center space-x-1 px-2 py-1 text-sm text-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900 rounded"
>
<Plus className="h-4 w-4" />
<span>Add Property</span>
</button>
</div>
</div>
);

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Search, LinkIcon, Hash, FileSpreadsheet, Wand2, GitCompare, Home, ChevronLeft, ChevronRight, Type, Edit3 } from 'lucide-react';
import { Search, LinkIcon, Hash, Table, Wand2, GitCompare, Home, ChevronLeft, ChevronRight, Type, Edit3 } from 'lucide-react';
const ToolSidebar = () => {
const location = useLocation();
@@ -10,9 +10,9 @@ const ToolSidebar = () => {
const tools = [
{ path: '/', name: 'Home', icon: Home, description: 'Back to homepage' },
{ path: '/object-editor', name: 'Object Editor', icon: Edit3, description: 'Visual editor for JSON & PHP objects' },
{ path: '/table-editor', name: 'Table Editor', icon: Table, description: 'Import, edit & export tabular data' },
{ 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' },

View File

@@ -34,4 +34,13 @@
.copy-button {
@apply absolute top-2 right-2 p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-md transition-colors duration-200;
}
.scrollbar-hide {
-ms-overflow-style: none; /* Internet Explorer 10+ */
scrollbar-width: none; /* Firefox */
}
.scrollbar-hide::-webkit-scrollbar {
display: none; /* Safari and Chrome */
}
}

View File

@@ -2,6 +2,10 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { initBrowserCompat } from './utils/browserCompat';
// Initialize browser compatibility fixes before React renders
initBrowserCompat();
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(

View File

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

File diff suppressed because it is too large Load Diff

View File

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

147
src/utils/browserCompat.js Normal file
View File

@@ -0,0 +1,147 @@
// Browser compatibility utilities for handling different browser environments
/**
* Detect if the app is running in Telegram's built-in browser
*/
export const isTelegramBrowser = () => {
const userAgent = navigator.userAgent.toLowerCase();
return userAgent.includes('telegram') ||
userAgent.includes('tgios') ||
userAgent.includes('tgandroid') ||
// Check for Telegram-specific window properties
(window.TelegramWebviewProxy !== undefined) ||
// Check for common Telegram browser characteristics
(userAgent.includes('mobile') && userAgent.includes('webkit') && !userAgent.includes('chrome'));
};
/**
* Detect if the app is running in any mobile in-app browser
*/
export const isInAppBrowser = () => {
const userAgent = navigator.userAgent.toLowerCase();
return userAgent.includes('wv') || // WebView
userAgent.includes('telegram') ||
userAgent.includes('fbav') || // Facebook
userAgent.includes('fban') || // Facebook
userAgent.includes('instagram') ||
userAgent.includes('twitter') ||
userAgent.includes('line') ||
userAgent.includes('whatsapp');
};
/**
* Get browser information
*/
export const getBrowserInfo = () => {
const userAgent = navigator.userAgent;
return {
userAgent,
isTelegram: isTelegramBrowser(),
isInApp: isInAppBrowser(),
isMobile: /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent),
isIOS: /iPad|iPhone|iPod/.test(userAgent),
isAndroid: /Android/.test(userAgent)
};
};
/**
* Add polyfills and compatibility fixes for problematic browsers
*/
export const addCompatibilityFixes = () => {
// Fix for missing or problematic console methods in some browsers
if (!window.console) {
window.console = {
log: () => {},
error: () => {},
warn: () => {},
info: () => {},
debug: () => {}
};
}
// Ensure console methods exist and are functions
['log', 'error', 'warn', 'info', 'debug'].forEach(method => {
if (typeof console[method] !== 'function') {
console[method] = () => {};
}
});
// Add requestAnimationFrame polyfill if missing
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = (callback) => {
return setTimeout(callback, 1000 / 60);
};
}
// Add cancelAnimationFrame polyfill if missing
if (!window.cancelAnimationFrame) {
window.cancelAnimationFrame = (id) => {
clearTimeout(id);
};
}
// Fix for missing or problematic localStorage in some browsers
try {
localStorage.setItem('test', 'test');
localStorage.removeItem('test');
} catch (e) {
window.localStorage = {
getItem: () => null,
setItem: () => {},
removeItem: () => {},
clear: () => {},
length: 0,
key: () => null
};
}
// Fix for missing or problematic sessionStorage
try {
sessionStorage.setItem('test', 'test');
sessionStorage.removeItem('test');
} catch (e) {
window.sessionStorage = {
getItem: () => null,
setItem: () => {},
removeItem: () => {},
clear: () => {},
length: 0,
key: () => null
};
}
};
/**
* Initialize compatibility fixes
*/
export const initBrowserCompat = () => {
const browserInfo = getBrowserInfo();
// Log browser info for debugging
console.log('Browser Info:', browserInfo);
// Add compatibility fixes
addCompatibilityFixes();
// Add specific fixes for Telegram browser
if (browserInfo.isTelegram) {
console.log('Telegram browser detected - applying compatibility fixes');
// Add Telegram-specific error handling
window.addEventListener('error', (event) => {
console.log('Global error caught in Telegram browser:', event.error);
// Prevent the error from bubbling up and showing the error overlay
event.preventDefault();
return true;
});
window.addEventListener('unhandledrejection', (event) => {
console.log('Unhandled promise rejection in Telegram browser:', event.reason);
// Prevent the error from bubbling up
event.preventDefault();
return true;
});
}
return browserInfo;
};

View File

@@ -0,0 +1,371 @@
// Content extraction and article detection utilities
/**
* Content classification types
*/
export const CONTENT_TYPES = {
RICH_ARTICLE: 'rich_article',
GENERAL_CONTENT: 'general_content',
LIMITED_CONTENT: 'limited_content',
NO_CONTENT: 'no_content'
};
/**
* Content type display information
*/
export const CONTENT_TYPE_INFO = {
[CONTENT_TYPES.RICH_ARTICLE]: {
label: 'Rich Article Content',
emoji: '🟢',
description: 'Clear article structure with headings and paragraphs',
color: 'text-green-600 dark:text-green-400'
},
[CONTENT_TYPES.GENERAL_CONTENT]: {
label: 'General Web Content',
emoji: '🟡',
description: 'Readable text mixed with navigation and UI elements',
color: 'text-yellow-600 dark:text-yellow-400'
},
[CONTENT_TYPES.LIMITED_CONTENT]: {
label: 'Limited Text Content',
emoji: '🟠',
description: 'Mostly UI/navigation with minimal readable text',
color: 'text-orange-600 dark:text-orange-400'
},
[CONTENT_TYPES.NO_CONTENT]: {
label: 'No Readable Content',
emoji: '🔴',
description: 'Images, videos, or heavily JavaScript-dependent content',
color: 'text-red-600 dark:text-red-400'
}
};
/**
* CORS proxy services for fetching external content
*/
const CORS_PROXIES = [
'https://api.allorigins.win/get?url=',
'https://corsproxy.io/?',
'https://cors-anywhere.herokuapp.com/',
'https://thingproxy.freeboard.io/fetch/'
];
/**
* Fetch and parse HTML content from URL with CORS proxy fallback
*/
export const fetchUrlContent = async (url) => {
try {
// Validate URL
const urlObj = new URL(url);
if (!['http:', 'https:'].includes(urlObj.protocol)) {
throw new Error('Only HTTP and HTTPS URLs are supported');
}
// First try direct fetch (works for same-origin or CORS-enabled sites)
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'User-Agent': 'Mozilla/5.0 (compatible; TextAnalyzer/1.0)'
}
});
if (response.ok) {
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('text/html')) {
const html = await response.text();
return { html, url: response.url, contentType };
}
}
} catch (directError) {
console.log('Direct fetch failed, trying CORS proxy:', directError.message);
}
// Try CORS proxies
let lastError = null;
for (const proxy of CORS_PROXIES) {
try {
let proxyUrl;
let response;
if (proxy.includes('allorigins.win')) {
// AllOrigins returns JSON with contents
proxyUrl = `${proxy}${encodeURIComponent(url)}`;
response = await fetch(proxyUrl);
if (response.ok) {
const data = await response.json();
if (data.contents) {
return {
html: data.contents,
url: data.status.url || url,
contentType: 'text/html'
};
}
}
} else {
// Other proxies return HTML directly
proxyUrl = `${proxy}${url}`;
response = await fetch(proxyUrl, {
headers: {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
}
});
if (response.ok) {
const contentType = response.headers.get('content-type') || 'text/html';
if (contentType.includes('text/html') || contentType.includes('text/plain')) {
const html = await response.text();
return { html, url, contentType };
}
}
}
} catch (proxyError) {
lastError = proxyError;
console.log(`Proxy ${proxy} failed:`, proxyError.message);
continue;
}
}
throw new Error(`All fetch methods failed. Last error: ${lastError?.message || 'Unknown error'}`);
} catch (error) {
throw new Error(`Failed to fetch content: ${error.message}`);
}
};
/**
* Parse HTML and create DOM
*/
export const parseHtml = (html) => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
return doc;
};
/**
* Detect article elements and structure
*/
export const detectArticleStructure = (doc) => {
const structure = {
hasArticleTag: false,
hasMainTag: false,
headingCount: 0,
paragraphCount: 0,
hasMetaArticle: false,
hasJsonLd: false,
wordCount: 0,
linkDensity: 0
};
// Check for semantic HTML5 tags
structure.hasArticleTag = doc.querySelector('article') !== null;
structure.hasMainTag = doc.querySelector('main') !== null;
// Count headings
structure.headingCount = doc.querySelectorAll('h1, h2, h3, h4, h5, h6').length;
// Count paragraphs
structure.paragraphCount = doc.querySelectorAll('p').length;
// Check meta tags for articles
const metaTags = doc.querySelectorAll('meta[property^="og:"], meta[name^="article:"]');
structure.hasMetaArticle = Array.from(metaTags).some(meta =>
meta.getAttribute('property') === 'og:type' && meta.getAttribute('content') === 'article' ||
meta.getAttribute('name')?.startsWith('article:')
);
// Check for JSON-LD structured data
const jsonLdScripts = doc.querySelectorAll('script[type="application/ld+json"]');
structure.hasJsonLd = Array.from(jsonLdScripts).some(script => {
try {
const data = JSON.parse(script.textContent);
const type = data['@type'] || (Array.isArray(data) ? data[0]['@type'] : null);
return type && ['Article', 'NewsArticle', 'BlogPosting'].includes(type);
} catch {
return false;
}
});
return structure;
};
/**
* Extract clean text from article elements
*/
export const extractArticleText = (doc) => {
const articleSelectors = [
'article',
'main article',
'[role="main"] article',
'.article-content',
'.post-content',
'.entry-content',
'.content-body'
];
// Try to find article container
let articleContainer = null;
for (const selector of articleSelectors) {
articleContainer = doc.querySelector(selector);
if (articleContainer) break;
}
// If no article container, try main content area
if (!articleContainer) {
const mainSelectors = ['main', '[role="main"]', '#main', '#content', '.main-content'];
for (const selector of mainSelectors) {
articleContainer = doc.querySelector(selector);
if (articleContainer) break;
}
}
// Extract text from container or full document
const container = articleContainer || doc.body;
if (!container) return { text: '', elements: [] };
// Remove unwanted elements
const unwantedSelectors = [
'script', 'style', 'nav', 'header', 'footer', 'aside',
'.navigation', '.nav', '.menu', '.sidebar', '.ads', '.advertisement',
'.social-share', '.comments', '.related-posts', '.author-bio'
];
const clone = container.cloneNode(true);
unwantedSelectors.forEach(selector => {
clone.querySelectorAll(selector).forEach(el => el.remove());
});
// Extract text from meaningful elements
const meaningfulElements = clone.querySelectorAll('h1, h2, h3, h4, h5, h6, p, li, blockquote, pre');
const elements = Array.from(meaningfulElements).map(el => ({
tag: el.tagName.toLowerCase(),
text: el.textContent.trim(),
length: el.textContent.trim().length
})).filter(el => el.length > 0);
const text = elements.map(el => el.text).join('\n\n');
return { text, elements };
};
/**
* Extract all visible text from page
*/
export const extractAllText = (doc) => {
const clone = doc.body.cloneNode(true);
// Remove unwanted elements
const unwantedSelectors = ['script', 'style', 'noscript'];
unwantedSelectors.forEach(selector => {
clone.querySelectorAll(selector).forEach(el => el.remove());
});
const text = clone.textContent || clone.innerText || '';
return text.replace(/\s+/g, ' ').trim();
};
/**
* Calculate content quality metrics
*/
export const calculateContentMetrics = (doc, articleText, allText) => {
const metrics = {
articleWordCount: articleText.split(/\s+/).filter(w => w.length > 0).length,
totalWordCount: allText.split(/\s+/).filter(w => w.length > 0).length,
contentRatio: 0,
linkCount: doc.querySelectorAll('a[href]').length,
imageCount: doc.querySelectorAll('img').length,
headingCount: doc.querySelectorAll('h1, h2, h3, h4, h5, h6').length,
paragraphCount: doc.querySelectorAll('p').length,
linkDensity: 0
};
if (metrics.totalWordCount > 0) {
metrics.contentRatio = metrics.articleWordCount / metrics.totalWordCount;
metrics.linkDensity = metrics.linkCount / metrics.totalWordCount;
}
return metrics;
};
/**
* Classify content type based on structure and metrics
*/
export const classifyContent = (structure, metrics, articleText) => {
const wordCount = metrics.articleWordCount;
const contentRatio = metrics.contentRatio;
const hasStructure = structure.hasArticleTag || structure.hasMainTag || structure.hasMetaArticle;
const hasGoodStructure = structure.headingCount >= 2 && structure.paragraphCount >= 3;
// Rich Article Content
if ((hasStructure || hasGoodStructure) && wordCount >= 300 && contentRatio > 0.6) {
return CONTENT_TYPES.RICH_ARTICLE;
}
// General Web Content
if (wordCount >= 100 && contentRatio > 0.3) {
return CONTENT_TYPES.GENERAL_CONTENT;
}
// Limited Content
if (wordCount >= 20) {
return CONTENT_TYPES.LIMITED_CONTENT;
}
// No readable content
return CONTENT_TYPES.NO_CONTENT;
};
/**
* Main function to extract and analyze content from URL
*/
export const extractContentFromUrl = async (url) => {
try {
// Fetch content
const { html, url: finalUrl, contentType } = await fetchUrlContent(url);
// Parse HTML
const doc = parseHtml(html);
// Detect article structure
const structure = detectArticleStructure(doc);
// Extract text content
const { text: articleText, elements } = extractArticleText(doc);
const allText = extractAllText(doc);
// Calculate metrics
const metrics = calculateContentMetrics(doc, articleText, allText);
// Classify content
const contentClassification = classifyContent(structure, metrics, articleText);
// Get page metadata
const title = doc.querySelector('title')?.textContent?.trim() || '';
const description = doc.querySelector('meta[name="description"]')?.getAttribute('content') || '';
return {
success: true,
url: finalUrl,
title,
description,
contentType: contentClassification,
structure,
metrics,
articleText,
allText,
elements,
extractedAt: new Date().toISOString()
};
} catch (error) {
return {
success: false,
error: error.message,
url
};
}
};

View File

@@ -22,7 +22,12 @@ module.exports = {
},
fontFamily: {
mono: ['JetBrains Mono', 'Monaco', 'Cascadia Code', 'Segoe UI Mono', 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', 'Fira Code', 'Droid Sans Mono', 'Courier New', 'monospace'],
}
},
maxWidth: {
'1/4': '25%',
'1/2': '50%',
'3/4': '75%',
}
},
},
plugins: [],

27
temp_export_tabs.js Normal file
View File

@@ -0,0 +1,27 @@
{/* Export Tabs */}
<div className="flex border-b border-gray-200 dark:border-gray-700 overflow-x-auto scrollbar-hide">
<div className="flex min-w-max">
<button
onClick={() => setActiveExportTab('json')}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
activeExportTab === 'json'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
<Braces className="h-4 w-4" />
JSON
</button>
<button
onClick={() => setActiveExportTab('php')}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
activeExportTab === 'php'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
<Code className="h-4 w-4" />
PHP
</button>
</div>
</div>