feat: major update - Markdown Editor, CodeMirror upgrades, SEO improvements, tool cleanup

- Added new Markdown Editor with live preview, GFM support, PDF/HTML/DOCX export
- Upgraded all paste fields to CodeMirror with syntax highlighting and expand/collapse
- Enhanced Object Editor with advanced URL fetching and preview mode
- Improved export views with syntax highlighting in Table/Object editors
- Implemented SEO improvements (FAQ schema, breadcrumbs, internal linking)
- Added Related Tools recommendations component
- Created custom 404 page with tool suggestions
- Consolidated tools: removed JSON, Serialize, CSV-JSON (merged into main editors)
- Updated documentation and cleaned up redundant files
- Updated release notes with user-centric improvements
This commit is contained in:
dwindown
2025-10-22 15:20:22 +07:00
parent 08d345eaeb
commit fb9c944366
40 changed files with 6927 additions and 1714 deletions

View File

@@ -4,22 +4,21 @@ import { HelmetProvider } from 'react-helmet-async';
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';
import UrlTool from './pages/UrlTool';
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 ObjectEditor from './pages/ObjectEditor';
import TableEditor from './pages/TableEditor';
import InvoiceEditor from './pages/InvoiceEditor';
import MarkdownEditor from './pages/MarkdownEditor';
import InvoicePreview from './pages/InvoicePreview';
import InvoicePreviewMinimal from './pages/InvoicePreviewMinimal';
import ReleaseNotes from './pages/ReleaseNotes';
import TermsOfService from './pages/TermsOfService';
import PrivacyPolicy from './pages/PrivacyPolicy';
import NotFound from './pages/NotFound';
import { initGA } from './utils/analytics';
import './index.css';
@@ -37,23 +36,22 @@ function App() {
<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 />} />
<Route path="/invoice-editor" element={<InvoiceEditor />} />
<Route path="/markdown-editor" element={<MarkdownEditor />} />
<Route path="/invoice-preview" element={<InvoicePreview />} />
<Route path="/invoice-preview-minimal" element={<InvoicePreviewMinimal />} />
<Route path="/whats-new" element={<ReleaseNotes />} />
<Route path="/release-notes" element={<ReleaseNotes />} />
<Route path="/privacy" element={<PrivacyPolicy />} />
<Route path="/terms" element={<TermsOfService />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Layout>
</Router>

36
src/components/AdBlock.js Normal file
View File

@@ -0,0 +1,36 @@
import React, { useEffect } from 'react';
/**
* AdBlock Component - Individual ad unit
* Displays a single Google AdSense ad
*/
const AdBlock = ({ slot, size = '300x250', className = '' }) => {
useEffect(() => {
try {
// Push ad to AdSense queue
(window.adsbygoogle = window.adsbygoogle || []).push({});
} catch (e) {
console.error('AdSense error:', e);
}
}, []);
const [width, height] = size.split('x');
return (
<div className={`bg-gray-100 dark:bg-gray-800 rounded-lg overflow-hidden ${className}`}>
<ins
className="adsbygoogle"
style={{
display: 'block',
width: `${width}px`,
height: `${height}px`
}}
data-ad-client="ca-pub-8644544686212757"
data-ad-slot={slot}
data-ad-format="fixed"
/>
</div>
);
};
export default AdBlock;

View File

@@ -0,0 +1,32 @@
import React from 'react';
import AdBlock from './AdBlock';
/**
* AdColumn Component - Desktop sidebar with 3 ad units
* Hidden on mobile/tablet, visible on desktop (1200px+)
* All ads are 300x250 (Medium Rectangle) to comply with Google AdSense policies
* - Ads must be fully viewable without scrolling
* - No scrollable containers allowed
*/
const AdColumn = ({
slot1 = 'REPLACE_WITH_SLOT_1',
slot2 = 'REPLACE_WITH_SLOT_2',
slot3 = 'REPLACE_WITH_SLOT_3'
}) => {
return (
<aside className="hidden xl:block w-[300px] flex-shrink-0">
<div className="fixed top-20 right-8 w-[300px] space-y-5">
{/* Ad 1: Medium Rectangle */}
<AdBlock slot={slot1} size="300x250" />
{/* Ad 2: Medium Rectangle */}
<AdBlock slot={slot2} size="300x250" />
{/* Ad 3: Medium Rectangle */}
<AdBlock slot={slot3} size="300x250" />
</div>
</aside>
);
};
export default AdColumn;

View File

@@ -0,0 +1,531 @@
import React, { useState, useEffect } from 'react';
import { ChevronDown, ChevronUp, Plus, X, Save, FolderOpen, Braces, Code } from 'lucide-react';
import ProBadge, { ProFeatureLock } from './ProBadge';
import { isFeatureEnabled, getFeatureInfo } from '../config/features';
import CodeMirrorEditor from './CodeMirrorEditor';
import StructuredEditor from './StructuredEditor';
const AdvancedURLFetch = ({
url,
onUrlChange,
onFetch,
fetching,
showAdvanced = false,
onToggleAdvanced,
onUpgrade = null, // Callback for upgrade action
onEditBodyVisually = null // Callback to switch to visual editor
}) => {
const isProFeatureEnabled = isFeatureEnabled('ADVANCED_URL_FETCH');
const featureInfo = getFeatureInfo('ADVANCED_URL_FETCH');
const [method, setMethod] = useState('GET');
const [headers, setHeaders] = useState([{ key: '', value: '', enabled: true }]);
const [body, setBody] = useState('');
const [bodyViewMode, setBodyViewMode] = useState('raw'); // 'raw' or 'visual'
const [bodyStructuredData, setBodyStructuredData] = useState({});
const [queryParams, setQueryParams] = useState([{ key: '', value: '', enabled: true }]);
const [authType, setAuthType] = useState('none');
const [authToken, setAuthToken] = useState('');
const [authUsername, setAuthUsername] = useState('');
const [authPassword, setAuthPassword] = useState('');
const [presetName, setPresetName] = useState('');
const [savedPresets, setSavedPresets] = useState([]);
// Load saved presets from localStorage
useEffect(() => {
const saved = localStorage.getItem('urlFetchPresets');
if (saved) {
try {
setSavedPresets(JSON.parse(saved));
} catch (e) {
console.error('Failed to load presets:', e);
}
}
}, []);
// Add header row
const addHeader = () => {
setHeaders([...headers, { key: '', value: '', enabled: true }]);
};
// Remove header row
const removeHeader = (index) => {
setHeaders(headers.filter((_, i) => i !== index));
};
// Update header
const updateHeader = (index, field, value) => {
const newHeaders = [...headers];
newHeaders[index][field] = value;
setHeaders(newHeaders);
};
// Add query param row
const addQueryParam = () => {
setQueryParams([...queryParams, { key: '', value: '', enabled: true }]);
};
// Remove query param row
const removeQueryParam = (index) => {
setQueryParams(queryParams.filter((_, i) => i !== index));
};
// Update query param
const updateQueryParam = (index, field, value) => {
const newParams = [...queryParams];
newParams[index][field] = value;
setQueryParams(newParams);
};
// Build final URL with query params
const buildFinalUrl = () => {
const baseUrl = url.trim();
const enabledParams = queryParams.filter(p => p.enabled && p.key.trim());
if (enabledParams.length === 0) return baseUrl;
const urlObj = new URL(baseUrl.startsWith('http') ? baseUrl : 'https://' + baseUrl);
enabledParams.forEach(param => {
urlObj.searchParams.append(param.key, param.value);
});
return urlObj.toString();
};
// Build headers object
const buildHeaders = () => {
const headersObj = {};
// Add custom headers
headers
.filter(h => h.enabled && h.key.trim())
.forEach(h => {
headersObj[h.key] = h.value;
});
// Add auth headers
if (authType === 'bearer' && authToken.trim()) {
headersObj['Authorization'] = `Bearer ${authToken}`;
} else if (authType === 'apikey' && authToken.trim()) {
headersObj['X-API-Key'] = authToken;
} else if (authType === 'basic' && authUsername.trim()) {
const credentials = btoa(`${authUsername}:${authPassword}`);
headersObj['Authorization'] = `Basic ${credentials}`;
}
// Add content-type for POST/PUT/PATCH with body
if (['POST', 'PUT', 'PATCH'].includes(method) && body.trim() && !headersObj['Content-Type']) {
headersObj['Content-Type'] = 'application/json';
}
return headersObj;
};
// Handle fetch with advanced options
const handleAdvancedFetch = () => {
const finalUrl = buildFinalUrl();
const finalHeaders = buildHeaders();
const finalBody = ['POST', 'PUT', 'PATCH'].includes(method) && body.trim() ? body : undefined;
onFetch({
url: finalUrl,
method,
headers: finalHeaders,
body: finalBody
});
};
// Save preset
const savePreset = () => {
if (!presetName.trim()) {
alert('Please enter a preset name');
return;
}
const preset = {
name: presetName,
url,
method,
headers: headers.filter(h => h.key.trim()),
body,
queryParams: queryParams.filter(p => p.key.trim()),
authType,
authToken,
authUsername,
// Don't save password for security
timestamp: Date.now()
};
const newPresets = [...savedPresets, preset];
setSavedPresets(newPresets);
localStorage.setItem('urlFetchPresets', JSON.stringify(newPresets));
setPresetName('');
alert('Preset saved!');
};
// Load preset
const loadPreset = (preset) => {
onUrlChange(preset.url);
setMethod(preset.method);
setHeaders(preset.headers.length > 0 ? preset.headers : [{ key: '', value: '', enabled: true }]);
setBody(preset.body || '');
setQueryParams(preset.queryParams.length > 0 ? preset.queryParams : [{ key: '', value: '', enabled: true }]);
setAuthType(preset.authType);
setAuthToken(preset.authToken || '');
setAuthUsername(preset.authUsername || '');
};
// Delete preset
const deletePreset = (index) => {
// eslint-disable-next-line no-restricted-globals
if (confirm('Delete this preset?')) {
const newPresets = savedPresets.filter((_, i) => i !== index);
setSavedPresets(newPresets);
localStorage.setItem('urlFetchPresets', JSON.stringify(newPresets));
}
};
return (
<div className="space-y-3">
{/* Basic URL Input */}
<div className="flex gap-2">
<select
value={method}
onChange={(e) => setMethod(e.target.value)}
className="tool-input w-24"
disabled={!showAdvanced || !isProFeatureEnabled}
title={!isProFeatureEnabled ? "PRO feature - Upgrade to use other methods" : ""}
>
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="PATCH">PATCH</option>
<option value="DELETE">DELETE</option>
</select>
<div className="relative flex-1">
<input
type="url"
value={url}
onChange={(e) => onUrlChange(e.target.value)}
placeholder="https://api.example.com/endpoint"
className="tool-input w-full"
onKeyPress={(e) => e.key === 'Enter' && (showAdvanced ? handleAdvancedFetch() : onFetch())}
/>
</div>
<button
onClick={showAdvanced ? handleAdvancedFetch : onFetch}
disabled={fetching || !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"
>
{fetching ? 'Fetching...' : 'Fetch Data'}
</button>
</div>
{/* Toggle Advanced */}
{/* Advanced Options Toggle - Hidden for now */}
{false && (
<button
onClick={onToggleAdvanced}
className="text-sm text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-2"
>
{showAdvanced ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
{showAdvanced ? 'Hide' : 'Show'} Advanced Options
{!isProFeatureEnabled && <ProBadge size="xs" />}
</button>
)}
{/* Advanced Options */}
{showAdvanced && (
isProFeatureEnabled ? (
<div className="space-y-4 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg border border-gray-200 dark:border-gray-600">
{/* Query Parameters */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
Query Parameters
</label>
<button
onClick={addQueryParam}
className="text-xs text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-1"
>
<Plus className="h-3 w-3" /> Add
</button>
</div>
<div className="space-y-2">
{queryParams.map((param, index) => (
<div key={index} className="flex gap-2 items-center">
<input
type="checkbox"
checked={param.enabled}
onChange={(e) => updateQueryParam(index, 'enabled', e.target.checked)}
className="w-4 h-4"
/>
<input
type="text"
value={param.key}
onChange={(e) => updateQueryParam(index, 'key', e.target.value)}
placeholder="key"
className="tool-input flex-1 text-sm"
/>
<input
type="text"
value={param.value}
onChange={(e) => updateQueryParam(index, 'value', e.target.value)}
placeholder="value"
className="tool-input flex-1 text-sm"
/>
<button
onClick={() => removeQueryParam(index)}
className="text-red-600 hover:text-red-700 dark:text-red-400"
>
<X className="h-4 w-4" />
</button>
</div>
))}
</div>
</div>
{/* Headers */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
Headers
</label>
<button
onClick={addHeader}
className="text-xs text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-1"
>
<Plus className="h-3 w-3" /> Add
</button>
</div>
<div className="space-y-2">
{headers.map((header, index) => (
<div key={index} className="flex gap-2 items-center">
<input
type="checkbox"
checked={header.enabled}
onChange={(e) => updateHeader(index, 'enabled', e.target.checked)}
className="w-4 h-4"
/>
<input
type="text"
value={header.key}
onChange={(e) => updateHeader(index, 'key', e.target.value)}
placeholder="Header-Name"
className="tool-input flex-1 text-sm"
/>
<input
type="text"
value={header.value}
onChange={(e) => updateHeader(index, 'value', e.target.value)}
placeholder="value"
className="tool-input flex-1 text-sm"
/>
<button
onClick={() => removeHeader(index)}
className="text-red-600 hover:text-red-700 dark:text-red-400"
>
<X className="h-4 w-4" />
</button>
</div>
))}
</div>
</div>
{/* Authentication */}
<div>
<label className="text-sm font-medium text-gray-700 dark:text-gray-300 block mb-2">
Authentication
</label>
<select
value={authType}
onChange={(e) => setAuthType(e.target.value)}
className="tool-input w-full mb-2"
>
<option value="none">No Auth</option>
<option value="bearer">Bearer Token</option>
<option value="apikey">API Key</option>
<option value="basic">Basic Auth</option>
</select>
{authType === 'bearer' && (
<input
type="text"
value={authToken}
onChange={(e) => setAuthToken(e.target.value)}
placeholder="Enter bearer token"
className="tool-input w-full"
/>
)}
{authType === 'apikey' && (
<input
type="text"
value={authToken}
onChange={(e) => setAuthToken(e.target.value)}
placeholder="Enter API key"
className="tool-input w-full"
/>
)}
{authType === 'basic' && (
<div className="space-y-2">
<input
type="text"
value={authUsername}
onChange={(e) => setAuthUsername(e.target.value)}
placeholder="Username"
className="tool-input w-full"
/>
<input
type="password"
value={authPassword}
onChange={(e) => setAuthPassword(e.target.value)}
placeholder="Password"
className="tool-input w-full"
/>
</div>
)}
</div>
{/* Request Body */}
{['POST', 'PUT', 'PATCH'].includes(method) && (
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
Request Body (JSON)
</label>
<div className="flex items-center gap-2">
<button
onClick={() => {
if (bodyViewMode === 'raw') {
// Switch to visual: parse JSON
try {
const data = JSON.parse(body || '{}');
setBodyStructuredData(data);
setBodyViewMode('visual');
} catch (e) {
alert('Invalid JSON. Please fix syntax errors first.');
}
} else {
// Switch to raw: stringify structured data
setBody(JSON.stringify(bodyStructuredData, null, 2));
setBodyViewMode('raw');
}
}}
className="text-xs text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-1"
title={bodyViewMode === 'raw' ? 'Switch to visual editor' : 'Switch to raw JSON'}
>
{bodyViewMode === 'raw' ? (
<>
<Braces className="h-3 w-3" /> Visual Editor
</>
) : (
<>
<Code className="h-3 w-3" /> Raw JSON
</>
)}
</button>
</div>
</div>
{bodyViewMode === 'raw' ? (
<div className="border border-gray-300 dark:border-gray-600 rounded-md overflow-hidden">
<CodeMirrorEditor
value={body}
onChange={setBody}
language="json"
placeholder='{"key": "value"}'
height="150px"
/>
</div>
) : (
<div className="border border-gray-300 dark:border-gray-600 rounded-md p-3 bg-white dark:bg-gray-800 max-h-96 overflow-y-auto">
<StructuredEditor
initialData={bodyStructuredData}
onDataChange={(newData) => {
setBodyStructuredData(newData);
setBody(JSON.stringify(newData, null, 2));
}}
readOnly={false}
/>
</div>
)}
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
💡 Tip: Toggle between raw JSON and visual tree editor for easier editing
</p>
</div>
)}
{/* Save/Load Presets */}
<div className="border-t border-gray-300 dark:border-gray-600 pt-4">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300 block mb-2">
Request Presets
</label>
<div className="flex gap-2 mb-3">
<input
type="text"
value={presetName}
onChange={(e) => setPresetName(e.target.value)}
placeholder="Preset name"
className="tool-input flex-1 text-sm"
/>
<button
onClick={savePreset}
className="bg-green-600 hover:bg-green-700 text-white px-3 py-1 rounded text-sm flex items-center gap-1"
>
<Save className="h-4 w-4" /> Save
</button>
</div>
{savedPresets.length > 0 && (
<div className="space-y-1">
{savedPresets.map((preset, index) => (
<div
key={index}
className="flex items-center justify-between p-2 bg-white dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-600"
>
<button
onClick={() => loadPreset(preset)}
className="flex-1 text-left text-sm text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 flex items-center gap-2"
>
<FolderOpen className="h-4 w-4" />
<span className="font-medium">{preset.name}</span>
<span className="text-xs text-gray-500">({preset.method})</span>
</button>
<button
onClick={() => deletePreset(index)}
className="text-red-600 hover:text-red-700 dark:text-red-400"
>
<X className="h-4 w-4" />
</button>
</div>
))}
</div>
)}
</div>
</div>
) : (
<ProFeatureLock
featureName={featureInfo?.name || 'Advanced URL Fetch'}
featureDescription={featureInfo?.description || 'Unlock custom HTTP methods, headers, authentication, and request body configuration'}
onUpgrade={onUpgrade}
/>
)
)}
<p className="text-xs text-gray-500 dark:text-gray-400">
{showAdvanced
? 'Configure HTTP method, headers, authentication, and request body for API testing'
: 'Enter any URL that returns JSON data. Examples: Telegram Bot API, JSONPlaceholder, GitHub API, etc.'
}
</p>
</div>
);
};
export default AdvancedURLFetch;

View File

@@ -1,10 +1,12 @@
import React, { useRef, useEffect, useState, useCallback } from 'react';
import { EditorView } from '@codemirror/view';
import { EditorView, keymap } from '@codemirror/view';
import { EditorState } from '@codemirror/state';
import { basicSetup } from 'codemirror';
import { json } from '@codemirror/lang-json';
import { sql } from '@codemirror/lang-sql';
import { markdown } from '@codemirror/lang-markdown';
import { oneDark } from '@codemirror/theme-one-dark';
import { indentWithTab } from '@codemirror/commands';
import { Maximize2, Minimize2 } from 'lucide-react';
const CodeMirrorEditor = ({
@@ -15,7 +17,8 @@ const CodeMirrorEditor = ({
language = 'json',
maxLines = 12,
showToggle = true,
cardRef = null // Reference to the card header for scroll target
cardRef = null, // Reference to the card header for scroll target
height = '350px' // Configurable height, default 350px
}) => {
const editorRef = useRef(null);
const viewRef = useRef(null);
@@ -53,33 +56,42 @@ const CodeMirrorEditor = ({
langExtension = [json()];
} else if (language === 'sql') {
langExtension = [sql()];
} else if (language === 'markdown') {
langExtension = [markdown()];
}
const extensions = [
basicSetup,
...langExtension,
// Enable line wrapping for single-line content
...(isSingleLine ? [EditorView.lineWrapping] : []),
// Enable Tab key to insert spaces (2 spaces for indentation)
keymap.of([indentWithTab]),
// Enable line wrapping for single-line content OR markdown
...(isSingleLine || language === 'markdown' ? [EditorView.lineWrapping] : []),
EditorView.theme({
'&': {
fontSize: '14px',
width: '100%',
maxWidth: '100%',
},
'.cm-content': {
padding: '12px',
maxWidth: '100%',
},
'.cm-focused': {
outline: 'none',
},
'.cm-editor': {
borderRadius: '6px',
maxWidth: '100%',
},
'.cm-scroller': {
overflowY: 'auto',
overflowX: 'auto',
height: '100%',
maxWidth: '100%',
},
'.cm-line': {
wordBreak: isSingleLine ? 'break-word' : 'normal',
wordBreak: isSingleLine || language === 'markdown' ? 'break-word' : 'normal',
}
}),
EditorView.updateListener.of((update) => {
@@ -102,14 +114,22 @@ const CodeMirrorEditor = ({
viewRef.current = view;
// Expose view on the DOM element for toolbar access
if (editorRef.current) {
const cmEditor = editorRef.current.querySelector('.cm-editor');
if (cmEditor) {
cmEditor.cmView = { view };
}
}
// Apply styles immediately after editor creation
setTimeout(() => {
const editorElement = editorRef.current?.querySelector('.cm-editor');
const scrollerElement = editorRef.current?.querySelector('.cm-scroller');
if (editorElement) {
editorElement.style.height = '350px';
editorElement.style.maxHeight = '350px';
editorElement.style.height = height;
editorElement.style.maxHeight = height;
}
if (scrollerElement) {
@@ -128,7 +148,7 @@ const CodeMirrorEditor = ({
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isDark, isSingleLine]); // Recreate when theme or line count changes
}, [isDark, isSingleLine, height, language]); // Recreate when theme, line count, height, or language changes
// Apply overflow and height styles
const applyEditorStyles = useCallback(() => {
@@ -142,8 +162,8 @@ const CodeMirrorEditor = ({
editorElement.style.height = 'auto';
editorElement.style.maxHeight = 'none';
} else {
editorElement.style.height = '350px';
editorElement.style.maxHeight = '350px';
editorElement.style.height = height;
editorElement.style.maxHeight = height;
}
}
@@ -152,7 +172,7 @@ const CodeMirrorEditor = ({
scrollerElement.style.overflowY = isExpanded ? 'visible' : 'auto';
scrollerElement.style.height = '100%';
}
}, [isExpanded]);
}, [isExpanded, height]);
// Apply styles on mount, expand/collapse, and content changes
useEffect(() => {
@@ -182,7 +202,7 @@ const CodeMirrorEditor = ({
ref={editorRef}
className={`dewedev-code-mirror border border-gray-300 dark:border-gray-600 rounded-md overflow-hidden ${
isDark ? 'bg-gray-900' : 'bg-white'
} ${isExpanded ? 'h-auto' : 'h-[350px]'}`}
} ${isExpanded ? 'h-auto' : `h-[${height}]`}`}
/>
{showToggle && (
<button

View File

@@ -57,7 +57,7 @@ const Layout = ({ children }) => {
{/* Header */}
<header className="fixed top-0 left-0 right-0 z-50 bg-white/80 dark:bg-slate-800/80 backdrop-blur-md shadow-lg border-b border-slate-200/50 dark:border-slate-700/50 flex-shrink-0">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className={isToolPage ? "px-4 sm:px-6 lg:px-8" : "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"}>
<div className="flex justify-between items-center h-16">
<button onClick={() => navigateWithGuard('/')} className="flex items-center group">
<div className="relative">
@@ -250,16 +250,16 @@ const Layout = ({ children }) => {
)}
{/* Main Content */}
<div className="flex flex-1 pt-16">
<div className="flex flex-1 pt-16 min-w-0 w-full max-w-full overflow-x-hidden">
{/* Main Content Area */}
<main className="flex-1 flex flex-col">
<main className="flex-1 flex flex-col min-w-0 w-full max-w-full">
{isToolPage && !isInvoicePreviewPage ? (
<div className="block">
<div className="hidden lg:block fixed top-16 left-0 z-[9999]">
<ToolSidebar navigateWithGuard={navigateWithGuard} />
</div>
<div className="flex-1 flex flex-col min-h-0 pl-0 lg:pl-16">
<div className="flex-1 p-4 sm:p-6 w-full min-w-0 overflow-auto">
<div className="flex-1 flex flex-col pl-0 lg:pl-16 min-w-0">
<div className="p-4 sm:p-6 w-full min-w-0 max-w-full overflow-x-hidden">
{children}
</div>
</div>

View File

@@ -0,0 +1,66 @@
import React, { useEffect, useState } from 'react';
import { X } from 'lucide-react';
/**
* MobileAdBanner Component - Sticky bottom banner for mobile
* Visible only on mobile/tablet, hidden on desktop
* Includes close button for better UX
*/
const MobileAdBanner = ({ slot = 'REPLACE_WITH_MOBILE_SLOT' }) => {
const [visible, setVisible] = useState(true);
const [closed, setClosed] = useState(false);
useEffect(() => {
// Check if user previously closed the banner (session storage)
const wasClosed = sessionStorage.getItem('mobileAdClosed');
if (wasClosed === 'true') {
setClosed(true);
setVisible(false);
}
}, []);
useEffect(() => {
if (visible && !closed) {
try {
(window.adsbygoogle = window.adsbygoogle || []).push({});
} catch (e) {
console.error('AdSense error:', e);
}
}
}, [visible, closed]);
const handleClose = () => {
setVisible(false);
setClosed(true);
sessionStorage.setItem('mobileAdClosed', 'true');
};
if (!visible || closed) return null;
return (
<div className="xl:hidden fixed bottom-0 left-0 right-0 z-50 bg-white dark:bg-gray-900 shadow-lg border-t border-gray-200 dark:border-gray-700">
<button
onClick={handleClose}
className="absolute top-1 right-1 p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 bg-white dark:bg-gray-800 rounded-full shadow-sm"
aria-label="Close ad"
>
<X className="h-4 w-4" />
</button>
<div className="flex justify-center py-2">
<ins
className="adsbygoogle"
style={{
display: 'inline-block',
width: '320px',
height: '50px'
}}
data-ad-client="ca-pub-8644544686212757"
data-ad-slot={slot}
data-ad-format="fixed"
/>
</div>
</div>
);
};
export default MobileAdBanner;

123
src/components/ProBadge.js Normal file
View File

@@ -0,0 +1,123 @@
import React from 'react';
import { Crown, Lock } from 'lucide-react';
/**
* ProBadge Component
*
* Displays a PRO badge for premium features
* Can be used inline or as a button to trigger upgrade flow
*/
const ProBadge = ({
variant = 'badge', // 'badge' | 'button' | 'inline'
size = 'sm', // 'xs' | 'sm' | 'md' | 'lg'
onClick = null,
showIcon = true,
className = ''
}) => {
const sizeClasses = {
xs: 'text-xs px-1.5 py-0.5',
sm: 'text-xs px-2 py-1',
md: 'text-sm px-3 py-1.5',
lg: 'text-base px-4 py-2'
};
const iconSizes = {
xs: 'h-2.5 w-2.5',
sm: 'h-3 w-3',
md: 'h-4 w-4',
lg: 'h-5 w-5'
};
if (variant === 'inline') {
return (
<span className={`inline-flex items-center gap-1 text-amber-600 dark:text-amber-400 font-semibold ${className}`}>
{showIcon && <Crown className={iconSizes[size]} />}
<span className={sizeClasses[size]}>PRO</span>
</span>
);
}
if (variant === 'button') {
return (
<button
onClick={onClick}
className={`inline-flex items-center gap-1.5 bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-600 hover:to-orange-600 text-white font-semibold rounded-full transition-all transform hover:scale-105 ${sizeClasses[size]} ${className}`}
>
{showIcon && <Crown className={iconSizes[size]} />}
Upgrade to PRO
</button>
);
}
// Default badge variant
return (
<span className={`inline-flex items-center gap-1 bg-gradient-to-r from-amber-500 to-orange-500 text-white font-bold rounded-full ${sizeClasses[size]} ${className}`}>
{showIcon && <Crown className={iconSizes[size]} />}
PRO
</span>
);
};
/**
* ProFeatureLock Component
*
* Displays a locked feature message with upgrade prompt
*/
export const ProFeatureLock = ({
featureName,
featureDescription,
onUpgrade = null,
compact = false
}) => {
const handleUpgrade = () => {
if (onUpgrade) {
onUpgrade();
} else {
// Default: scroll to top and show upgrade info
window.scrollTo({ top: 0, behavior: 'smooth' });
alert('Upgrade to PRO to unlock this feature!\n\nPRO features will be available soon.');
}
};
if (compact) {
return (
<div className="flex items-center gap-2 p-2 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg">
<Lock className="h-4 w-4 text-amber-600 dark:text-amber-400 flex-shrink-0" />
<span className="text-sm text-amber-800 dark:text-amber-300 flex-1">
<ProBadge variant="inline" size="xs" showIcon={false} /> feature
</span>
<button
onClick={handleUpgrade}
className="text-xs text-amber-700 dark:text-amber-300 hover:underline font-medium whitespace-nowrap"
>
Upgrade
</button>
</div>
);
}
return (
<div className="p-4 bg-gradient-to-br from-amber-50 to-orange-50 dark:from-amber-900/20 dark:to-orange-900/20 border-2 border-amber-200 dark:border-amber-700 rounded-lg">
<div className="flex items-start gap-3">
<div className="p-2 bg-amber-100 dark:bg-amber-800/50 rounded-lg">
<Lock className="h-5 w-5 text-amber-600 dark:text-amber-400" />
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<h4 className="font-semibold text-gray-900 dark:text-gray-100">
{featureName}
</h4>
<ProBadge size="sm" />
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
{featureDescription}
</p>
<ProBadge variant="button" size="md" onClick={handleUpgrade} />
</div>
</div>
</div>
);
};
export default ProBadge;

View File

@@ -0,0 +1,96 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { ArrowRight } from 'lucide-react';
/**
* Related Tools Component
* Shows related tools at the bottom of each tool page for internal linking
*/
const RELATED_TOOLS = {
'markdown-editor': [
{ name: 'Code Beautifier', path: '/beautifier', desc: 'Format and beautify your code' },
{ name: 'Text Length Checker', path: '/text-length', desc: 'Count words and characters' },
{ name: 'Diff Tool', path: '/diff', desc: 'Compare text differences' }
],
'object-editor': [
{ name: 'Table Editor', path: '/table-editor', desc: 'Edit JSON as table' },
{ name: 'Code Beautifier', path: '/beautifier', desc: 'Format & beautify code' },
{ name: 'Markdown Editor', path: '/markdown-editor', desc: 'Write documentation' }
],
'table-editor': [
{ name: 'Object Editor', path: '/object-editor', desc: 'Edit JSON visually' },
{ name: 'Code Beautifier', path: '/beautifier', desc: 'Format & beautify code' },
{ name: 'Markdown Editor', path: '/markdown-editor', desc: 'Create documentation' }
],
'invoice-editor': [
{ name: 'Markdown Editor', path: '/markdown-editor', desc: 'Create documentation' },
{ name: 'Table Editor', path: '/table-editor', desc: 'Manage data tables' },
{ name: 'Object Editor', path: '/object-editor', desc: 'Edit JSON data' }
],
'beautifier': [
{ name: 'Markdown Editor', path: '/markdown-editor', desc: 'Write documentation' },
{ name: 'Diff Tool', path: '/diff', desc: 'Compare code changes' },
{ name: 'Object Editor', path: '/object-editor', desc: 'Edit JSON visually' }
],
'diff': [
{ name: 'Beautifier', path: '/beautifier', desc: 'Format code first' },
{ name: 'Markdown Editor', path: '/markdown-editor', desc: 'Document changes' },
{ name: 'Text Length', path: '/text-length', desc: 'Analyze text' }
],
'text-length': [
{ name: 'Markdown Editor', path: '/markdown-editor', desc: 'Write content' },
{ name: 'Diff Tool', path: '/diff', desc: 'Compare texts' },
{ name: 'Beautifier', path: '/beautifier', desc: 'Format code' }
],
'url': [
{ name: 'Base64 Encoder', path: '/base64', desc: 'Encode/decode data' },
{ name: 'Code Beautifier', path: '/beautifier', desc: 'Format code' },
{ name: 'Object Editor', path: '/object-editor', desc: 'Edit JSON data' }
],
'base64': [
{ name: 'URL Encoder', path: '/url', desc: 'Encode URLs' },
{ name: 'Object Editor', path: '/object-editor', desc: 'Edit JSON & PHP data' },
{ name: 'Code Beautifier', path: '/beautifier', desc: 'Format code' }
]
};
const RelatedTools = ({ toolId }) => {
const relatedTools = RELATED_TOOLS[toolId];
if (!relatedTools || relatedTools.length === 0) {
return null;
}
return (
<div className="mt-8 bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-gray-800 dark:to-gray-900 rounded-lg border border-blue-100 dark:border-gray-700 p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<span className="text-blue-600 dark:text-blue-400"></span>
You Might Also Like
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{relatedTools.map((tool) => (
<Link
key={tool.path}
to={tool.path}
className="group bg-white dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700 hover:border-blue-500 dark:hover:border-blue-500 hover:shadow-md transition-all duration-200"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<h4 className="font-medium text-gray-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
{tool.name}
</h4>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
{tool.desc}
</p>
</div>
<ArrowRight className="h-5 w-5 text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 group-hover:translate-x-1 transition-all flex-shrink-0 ml-2" />
</div>
</Link>
))}
</div>
</div>
);
};
export default RelatedTools;

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { Helmet } from 'react-helmet-async';
import TOOL_FAQS from '../data/faqs';
const SEO = ({
title,
@@ -7,7 +8,8 @@ const SEO = ({
keywords,
path = '/',
type = 'website',
image = 'https://dewe.dev/og-image.png'
image = 'https://dewe.dev/og-image.png',
toolId = null
}) => {
const siteUrl = 'https://dewe.dev';
const fullUrl = `${siteUrl}${path}`;
@@ -17,6 +19,47 @@ const SEO = ({
const defaultKeywords = 'developer tools, json editor, csv converter, base64 encoder, url encoder, code beautifier, diff tool, web developer utilities, online tools';
const metaKeywords = keywords || defaultKeywords;
// Get FAQ data for this tool
const faqs = toolId && TOOL_FAQS[toolId] ? TOOL_FAQS[toolId] : null;
// Generate breadcrumb schema
const breadcrumbSchema = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": siteUrl
}
]
};
// Add current page to breadcrumb if not homepage
if (path !== '/') {
breadcrumbSchema.itemListElement.push({
"@type": "ListItem",
"position": 2,
"name": title || "Page",
"item": fullUrl
});
}
// Generate FAQ schema if FAQs exist
const faqSchema = faqs ? {
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": faqs.map(faq => ({
"@type": "Question",
"name": faq.question,
"acceptedAnswer": {
"@type": "Answer",
"text": faq.answer
}
}))
} : null;
return (
<Helmet>
{/* Basic Meta Tags */}
@@ -46,7 +89,7 @@ const SEO = ({
<meta name="author" content="Developer Tools" />
<meta name="language" content="English" />
{/* JSON-LD Structured Data */}
{/* JSON-LD Structured Data - WebApplication */}
<script type="application/ld+json">
{JSON.stringify({
"@context": "https://schema.org",
@@ -68,6 +111,18 @@ const SEO = ({
}
})}
</script>
{/* JSON-LD Structured Data - Breadcrumb */}
<script type="application/ld+json">
{JSON.stringify(breadcrumbSchema)}
</script>
{/* JSON-LD Structured Data - FAQ (if available) */}
{faqSchema && (
<script type="application/ld+json">
{JSON.stringify(faqSchema)}
</script>
)}
</Helmet>
);
};

View File

@@ -8,7 +8,8 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
const isInternalUpdate = useRef(false);
const [nestedEditModal, setNestedEditModal] = useState(null); // { path, value, type: 'json' | 'serialized' }
const [nestedData, setNestedData] = useState(null);
const [editMode, setEditMode] = useState(false); // Internal edit mode state
// Start in edit mode if readOnly is false
const [editMode, setEditMode] = useState(readOnlyProp === false);
// Use internal editMode if readOnlyProp is not explicitly set, otherwise use prop
const readOnly = readOnlyProp !== false ? readOnlyProp : !editMode;

View File

@@ -1,29 +1,45 @@
import React from 'react';
import AdColumn from './AdColumn';
import MobileAdBanner from './MobileAdBanner';
const ToolLayout = ({ title, description, children, icon: Icon }) => {
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 w-full" style={{ maxWidth: 'min(80rem, calc(100vw - 2rem))' }}>
{/* Header */}
<div className="mb-6 sm:mb-8">
<div className="flex items-center space-x-2 sm:space-x-3 mb-2">
{Icon && <Icon className="h-6 w-6 sm:h-8 sm:w-8 text-primary-600 flex-shrink-0" />}
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-white truncate">
{title}
</h1>
<>
<div className="w-full max-w-full px-0 sm:px-6 lg:px-8 flex gap-5 min-w-0">
{/* Main Content */}
<div className="flex-1 min-w-0 max-w-full">
{/* Header */}
<div className="mb-6 sm:mb-8 min-w-0">
<div className="flex items-center space-x-2 sm:space-x-3 mb-2 min-w-0">
{Icon && <Icon className="h-6 w-6 sm:h-8 sm:w-8 text-primary-600 flex-shrink-0" />}
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-white truncate min-w-0">
{title}
</h1>
</div>
{description && (
<p className="text-gray-600 dark:text-gray-300 text-base sm:text-lg break-words">
{description}
</p>
)}
</div>
{/* Tool Content */}
<div className="space-y-4 sm:space-y-6 w-full max-w-full min-w-0">
{children}
</div>
</div>
{description && (
<p className="text-gray-600 dark:text-gray-300 text-base sm:text-lg">
{description}
</p>
)}
{/* Desktop Ad Column - Hidden on mobile */}
<AdColumn />
</div>
{/* Tool Content */}
<div className="space-y-4 sm:space-y-6 w-full">
{children}
</div>
</div>
{/* Mobile Ad Banner - Hidden on desktop */}
<MobileAdBanner />
{/* Add padding to bottom on mobile to prevent content overlap with sticky ad */}
<div className="xl:hidden h-16" />
</>
);
};

81
src/config/features.js Normal file
View File

@@ -0,0 +1,81 @@
/**
* Feature Toggle System
*
* Controls which features are available in FREE vs PRO versions
* Currently static, will be dynamic from database in the future
*/
// User tier - will be fetched from database/auth in the future
export const USER_TIER = {
FREE: 'free',
PRO: 'pro'
};
// Current user tier (static for now, will be dynamic)
// TODO: Replace with actual user tier from authentication/database
export const getCurrentUserTier = () => {
// For development/testing, you can change this
const staticTier = USER_TIER.PRO; // Change to USER_TIER.PRO to test pro features
// In the future, this will be:
// return getUserFromAuth()?.tier || USER_TIER.FREE;
return staticTier;
};
// Feature flags
export const FEATURES = {
// Advanced URL Fetch with headers, methods, body, auth
ADVANCED_URL_FETCH: {
name: 'Advanced URL Fetch',
description: 'Custom HTTP methods, headers, authentication, and request body',
tier: USER_TIER.PRO,
enabled: (userTier) => userTier === USER_TIER.PRO
},
// Future pro features can be added here
BULK_OPERATIONS: {
name: 'Bulk Operations',
description: 'Process multiple files or operations at once',
tier: USER_TIER.PRO,
enabled: (userTier) => userTier === USER_TIER.PRO
},
EXPORT_TEMPLATES: {
name: 'Export Templates',
description: 'Save and reuse custom export templates',
tier: USER_TIER.PRO,
enabled: (userTier) => userTier === USER_TIER.PRO
},
CLOUD_SYNC: {
name: 'Cloud Sync',
description: 'Sync your data and settings across devices',
tier: USER_TIER.PRO,
enabled: (userTier) => userTier === USER_TIER.PRO
}
};
// Helper function to check if a feature is enabled
export const isFeatureEnabled = (featureName) => {
const feature = FEATURES[featureName];
if (!feature) return false;
const userTier = getCurrentUserTier();
return feature.enabled(userTier);
};
// Helper function to get user tier
export const getUserTier = () => {
return getCurrentUserTier();
};
// Helper function to check if user is pro
export const isProUser = () => {
return getCurrentUserTier() === USER_TIER.PRO;
};
// Helper function to get feature info
export const getFeatureInfo = (featureName) => {
return FEATURES[featureName] || null;
};

View File

@@ -63,6 +63,14 @@ export const TOOLS = [
tags: ['Table', 'CSV', 'JSON', 'Data', 'Editor'],
category: 'editor'
},
{
path: '/markdown-editor',
name: 'Markdown Editor',
icon: FileText,
description: 'Write and preview markdown with live rendering, syntax highlighting, and export options',
tags: ['Markdown', 'Editor', 'Preview', 'Export', 'GFM'],
category: 'editor'
},
{
path: '/invoice-editor',
name: 'Invoice Editor',

100
src/data/faqs.js Normal file
View File

@@ -0,0 +1,100 @@
/**
* FAQ Data for SEO Schema Markup
* Used to generate FAQPage structured data for better search visibility
*/
export const TOOL_FAQS = {
'markdown-editor': [
{
question: "Is this markdown editor free to use?",
answer: "Yes, completely free with no limits. All processing happens in your browser with no data upload or storage. No account needed."
},
{
question: "Does it support GitHub Flavored Markdown?",
answer: "Yes, full GitHub Flavored Markdown (GFM) support including tables, task lists, strikethrough, and syntax highlighting for code blocks."
},
{
question: "Can I export my markdown to PDF?",
answer: "Yes, you can export your markdown to PDF, HTML, or plain text with one click. The PDF export maintains proper formatting and styling."
},
{
question: "Is my data safe and private?",
answer: "100% safe. All processing happens locally in your browser. We never upload, store, or transmit your data to any server. Your content stays on your device."
},
{
question: "Does it work offline?",
answer: "Yes, once loaded, the markdown editor works completely offline. No internet connection required for editing or exporting."
}
],
'object-editor': [
{
question: "What is the Object Editor used for?",
answer: "The Object Editor is a visual tool for editing JSON objects, nested data structures, arrays, and complex data. Perfect for developers working with APIs, configuration files, or database records."
},
{
question: "Can I import JSON from a URL?",
answer: "Yes, you can fetch JSON data directly from any URL or API endpoint. The editor will parse and display it in an easy-to-edit visual format."
},
{
question: "Does it validate JSON syntax?",
answer: "Yes, real-time JSON validation with detailed error messages. The editor highlights syntax errors and helps you fix them quickly."
},
{
question: "Is my data stored anywhere?",
answer: "No, all data processing happens in your browser. We never upload, store, or transmit your data. 100% privacy-first and secure."
},
{
question: "Can I edit nested objects and arrays?",
answer: "Yes, full support for nested objects, arrays, and complex data structures. Add, edit, delete properties at any level with visual controls."
}
],
'table-editor': [
{
question: "What file formats can I import?",
answer: "Import CSV, TSV, JSON, Excel (XLSX/XLS), SQL dumps, and paste from spreadsheets like Google Sheets or Excel. Automatic format detection included."
},
{
question: "Can I export to Excel format?",
answer: "Yes, export to Excel (XLSX), CSV, TSV, JSON, Markdown tables, HTML tables, and SQL INSERT statements. Choose the format you need."
},
{
question: "Is there a row or column limit?",
answer: "No hard limit. The editor efficiently handles thousands of rows and columns in your browser without performance issues."
},
{
question: "Can I edit JSON data in table cells?",
answer: "Yes, the editor detects JSON/serialized data in cells and lets you edit it visually in the Object Editor. Seamless integration between tools."
},
{
question: "Does it work with SQL databases?",
answer: "Yes, import SQL dumps from phpMyAdmin or other tools, edit the data visually, and export back to SQL INSERT statements for database import."
}
],
'invoice-editor': [
{
question: "Is the invoice editor free?",
answer: "Yes, completely free with no limits. Create unlimited invoices with professional templates. No account or subscription required."
},
{
question: "What invoice templates are available?",
answer: "Multiple professional templates: Standard, Modern, Minimal, and Classic. Each template is customizable with your branding and colors."
},
{
question: "Can I save my invoices?",
answer: "Yes, save invoices as JSON files to your device. Load them anytime to edit or create new invoices. All data stays on your device."
},
{
question: "Can I export to PDF?",
answer: "Yes, export invoices to PDF with professional formatting. Perfect for sending to clients or printing. One-click export included."
},
{
question: "Does it calculate taxes and totals automatically?",
answer: "Yes, automatic calculation of subtotals, taxes, discounts, and final totals. Supports multiple tax rates and discount types (percentage or fixed amount)."
}
]
};
export default TOOL_FAQS;

View File

@@ -8,6 +8,8 @@
html {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
overflow-x: hidden;
width: 100%;
max-width: 100vw;
}
body {
@@ -16,6 +18,13 @@
max-width: 100vw;
}
#root {
overflow-x: hidden;
width: 100%;
max-width: 100vw;
min-width: 0;
}
code, pre {
font-family: 'JetBrains Mono', Monaco, 'Cascadia Code', 'Segoe UI Mono', 'Roboto Mono', monospace;
}
@@ -38,6 +47,14 @@
@apply px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 rounded-md font-medium transition-colors duration-200;
}
.tool-button-primary {
@apply flex items-center px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md font-medium transition-colors duration-200;
}
.toolbar-btn {
@apply p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md transition-colors duration-200 text-gray-700 dark:text-gray-300 font-medium text-sm;
}
.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;
}

View File

@@ -1,315 +0,0 @@
import React, { useState } from 'react';
import { RefreshCw, Upload } from 'lucide-react';
import ToolLayout from '../components/ToolLayout';
import CopyButton from '../components/CopyButton';
const CsvJsonTool = () => {
const [input, setInput] = useState('');
const [output, setOutput] = useState('');
const [mode, setMode] = useState('csv-to-json'); // 'csv-to-json' or 'json-to-csv'
const [delimiter, setDelimiter] = useState(',');
const [hasHeaders, setHasHeaders] = useState(true);
const csvToJson = () => {
try {
const lines = input.trim().split('\n');
if (lines.length === 0) {
setOutput('Error: No data to convert');
return;
}
const headers = hasHeaders ? lines[0].split(delimiter).map(h => h.trim()) : null;
const dataLines = hasHeaders ? lines.slice(1) : lines;
const result = dataLines.map((line, index) => {
const values = line.split(delimiter).map(v => v.trim());
if (hasHeaders && headers) {
const obj = {};
headers.forEach((header, i) => {
obj[header] = values[i] || '';
});
return obj;
} else {
return values;
}
});
setOutput(JSON.stringify(result, null, 2));
} catch (err) {
setOutput(`Error: ${err.message}`);
}
};
const jsonToCsv = () => {
try {
const data = JSON.parse(input);
let csv = '';
if (Array.isArray(data)) {
// Handle array of objects (original functionality)
if (data.length === 0) {
setOutput('Error: Empty array');
return;
}
// Get headers from first object
const headers = Object.keys(data[0]);
// Add headers if enabled
if (hasHeaders) {
csv += headers.join(delimiter) + '\n';
}
// Add data rows
data.forEach(row => {
const values = headers.map(header => {
const value = row[header] || '';
// Escape values containing delimiter or quotes
if (typeof value === 'string' && (value.includes(delimiter) || value.includes('"') || value.includes('\n'))) {
return `"${value.replace(/"/g, '""')}"`;
}
return value;
});
csv += values.join(delimiter) + '\n';
});
} else if (typeof data === 'object' && data !== null) {
// Handle single object as key-value pairs
// Add headers if enabled
if (hasHeaders) {
csv += `Key${delimiter}Value\n`;
}
// Add key-value rows
Object.entries(data).forEach(([key, value]) => {
// Format the key
let formattedKey = key;
if (typeof key === 'string' && (key.includes(delimiter) || key.includes('"') || key.includes('\n'))) {
formattedKey = `"${key.replace(/"/g, '""')}"`;
}
// Format the value
let formattedValue = '';
if (value === null) {
formattedValue = 'null';
} else if (value === undefined) {
formattedValue = 'undefined';
} else if (typeof value === 'object') {
// Convert objects/arrays to JSON string
formattedValue = JSON.stringify(value);
} else {
formattedValue = String(value);
}
// Escape value if needed
if (typeof formattedValue === 'string' && (formattedValue.includes(delimiter) || formattedValue.includes('"') || formattedValue.includes('\n'))) {
formattedValue = `"${formattedValue.replace(/"/g, '""')}"`;
}
csv += `${formattedKey}${delimiter}${formattedValue}\n`;
});
} else {
setOutput('Error: JSON must be an object or an array of objects');
return;
}
setOutput(csv.trim());
} catch (err) {
setOutput(`Error: ${err.message}`);
}
};
const handleProcess = () => {
if (mode === 'csv-to-json') {
csvToJson();
} else {
jsonToCsv();
}
};
const handleFileUpload = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
setInput(e.target.result);
};
reader.readAsText(file);
}
};
const clearAll = () => {
setInput('');
setOutput('');
};
const loadSample = () => {
if (mode === 'csv-to-json') {
setInput(`name,age,email,city
John Doe,30,john@example.com,New York
Jane Smith,25,jane@example.com,Los Angeles
Bob Johnson,35,bob@example.com,Chicago`);
} else {
setInput(`[
{
"name": "John Doe",
"age": 30,
"email": "john@example.com",
"city": "New York"
},
{
"name": "Jane Smith",
"age": 25,
"email": "jane@example.com",
"city": "Los Angeles"
}
]`);
}
};
return (
<ToolLayout
title="CSV ↔ JSON Converter"
description="Convert between CSV and JSON formats with custom delimiters"
icon={RefreshCw}
>
{/* Mode Toggle */}
<div className="flex bg-gray-100 dark:bg-gray-800 rounded-lg p-1 mb-6 w-fit">
<button
onClick={() => setMode('csv-to-json')}
className={`px-4 py-2 rounded-md font-medium transition-colors ${
mode === 'csv-to-json'
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
: 'text-gray-600 dark:text-gray-400'
}`}
>
CSV JSON
</button>
<button
onClick={() => setMode('json-to-csv')}
className={`px-4 py-2 rounded-md font-medium transition-colors ${
mode === 'json-to-csv'
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
: 'text-gray-600 dark:text-gray-400'
}`}
>
JSON CSV
</button>
</div>
{/* Options */}
<div className="flex flex-wrap items-center gap-4 mb-6 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="flex items-center space-x-2">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
Delimiter:
</label>
<select
value={delimiter}
onChange={(e) => setDelimiter(e.target.value)}
className="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 text-sm"
>
<option value=",">Comma (,)</option>
<option value=";">Semicolon (;)</option>
<option value="\t">Tab</option>
<option value="|">Pipe (|)</option>
</select>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="hasHeaders"
checked={hasHeaders}
onChange={(e) => setHasHeaders(e.target.checked)}
className="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/>
<label htmlFor="hasHeaders" className="text-sm font-medium text-gray-700 dark:text-gray-300">
First row contains headers
</label>
</div>
</div>
{/* Controls */}
<div className="flex flex-wrap gap-3 mb-6">
<button onClick={handleProcess} className="tool-button">
{mode === 'csv-to-json' ? 'Convert to JSON' : 'Convert to CSV'}
</button>
<label className="tool-button-secondary cursor-pointer flex items-center space-x-2">
<Upload className="h-4 w-4" />
<span>Upload File</span>
<input
type="file"
onChange={handleFileUpload}
className="hidden"
accept=".csv,.json,.txt"
/>
</label>
<button onClick={loadSample} className="tool-button-secondary">
Load Sample
</button>
<button onClick={clearAll} className="tool-button-secondary">
Clear All
</button>
</div>
{/* Input/Output Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Input */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{mode === 'csv-to-json' ? 'CSV Input' : 'JSON Input'}
</label>
<div className="relative">
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder={
mode === 'csv-to-json'
? 'Paste your CSV data here...'
: 'Paste your JSON array here...'
}
className="tool-input h-96"
/>
</div>
</div>
{/* Output */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{mode === 'csv-to-json' ? 'JSON Output' : 'CSV Output'}
</label>
<div className="relative">
<textarea
value={output}
readOnly
placeholder={
mode === 'csv-to-json'
? 'JSON output will appear here...'
: 'CSV output will appear here...'
}
className="tool-input h-96 bg-gray-50 dark:bg-gray-800"
/>
{output && <CopyButton text={output} />}
</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> CSV to JSON converts each row to an object with column headers as keys</li>
<li> JSON to CSV requires an array of objects with consistent properties</li>
<li> Choose the appropriate delimiter for your CSV format</li>
<li> Toggle "First row contains headers" based on your data structure</li>
</ul>
</div>
</ToolLayout>
);
};
export default CsvJsonTool;

View File

@@ -6,6 +6,8 @@ import {
} from 'lucide-react';
import ToolLayout from '../components/ToolLayout';
import CodeMirrorEditor from '../components/CodeMirrorEditor';
import SEO from '../components/SEO';
import RelatedTools from '../components/RelatedTools';
const InvoiceEditor = () => {
const navigate = useNavigate();
@@ -745,39 +747,48 @@ const InvoiceEditor = () => {
};
return (
<ToolLayout
title="Invoice Editor"
description="Create, edit, and export professional invoices with PDF generation"
>
<div className="space-y-4 sm:space-y-6 w-full">
{/* Input Section */}
<div className="w-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="border-b border-gray-200 dark:border-gray-700">
<nav className="flex space-x-2 sm:space-x-8 px-4 sm:px-6 overflow-x-auto" aria-label="Tabs">
{[
{ id: 'create', name: 'Create New', icon: Plus },
{ id: 'url', name: 'URL', icon: Globe },
{ id: 'paste', name: 'Paste', icon: FileText },
{ id: 'open', name: 'Open', icon: Upload }
].map((tab) => {
const Icon = tab.icon;
return (
<button
key={tab.id}
onClick={() => handleTabChange(tab.id)}
className={`${
activeTab === tab.id
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
} whitespace-nowrap py-4 px-1 sm:px-2 border-b-2 font-medium text-sm flex items-center gap-1 sm:gap-2 transition-colors min-w-0 flex-shrink-0`}
>
<Icon className="h-4 w-4" />
{tab.name}
</button>
);
})}
</nav>
</div>
<>
<SEO
title="Free Invoice Generator - Professional Invoice Templates"
description="✓ Free invoice generator ✓ Professional templates ✓ PDF export ✓ Auto-calculate totals ✓ No signup. Create invoices now!"
keywords="invoice generator, invoice maker, invoice template, free invoice, invoice editor, pdf invoice, professional invoice, online invoice, invoice creator"
path="/invoice-editor"
toolId="invoice-editor"
/>
<ToolLayout
title="Invoice Editor"
description="Create, edit, and export professional invoices with PDF generation"
icon={FileText}
>
<div className="space-y-4 sm:space-y-6 w-full">
{/* Input Section */}
<div className="w-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="border-b border-gray-200 dark:border-gray-700">
<nav className="flex space-x-2 sm:space-x-8 px-4 sm:px-6 overflow-x-auto" aria-label="Tabs">
{[
{ id: 'create', name: 'Create New', icon: Plus },
{ id: 'url', name: 'URL', icon: Globe },
{ id: 'paste', name: 'Paste', icon: FileText },
{ id: 'open', name: 'Open', icon: Upload }
].map((tab) => {
const Icon = tab.icon;
return (
<button
key={tab.id}
onClick={() => handleTabChange(tab.id)}
className={`${
activeTab === tab.id
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
} whitespace-nowrap py-4 px-1 sm:px-2 border-b-2 font-medium text-sm flex items-center gap-1 sm:gap-2 transition-colors min-w-0 flex-shrink-0`}
>
<Icon className="h-4 w-4" />
{tab.name}
</button>
);
})}
</nav>
</div>
{/* Tab Content */}
{(activeTab !== 'create' || !createNewCompleted) && (
@@ -2122,7 +2133,11 @@ const InvoiceEditor = () => {
/>
</div>
{/* Related Tools */}
<RelatedTools toolId="invoice-editor" />
</ToolLayout>
</>
);
};

View File

@@ -1,254 +0,0 @@
import React, { useState } from 'react';
import { Code, AlertCircle, CheckCircle, Edit3 } from 'lucide-react';
import ToolLayout from '../components/ToolLayout';
import CopyButton from '../components/CopyButton';
import StructuredEditor from '../components/StructuredEditor';
import CodeEditor from '../components/CodeEditor';
const JsonTool = () => {
const [input, setInput] = useState('');
const [output, setOutput] = useState('');
const [error, setError] = useState('');
const [isValid, setIsValid] = useState(null);
const [editorMode, setEditorMode] = useState('text'); // 'text' or 'visual'
const [structuredData, setStructuredData] = useState({});
const formatJson = () => {
try {
const parsed = JSON.parse(input);
const formatted = JSON.stringify(parsed, null, 2);
setOutput(formatted);
setError('');
setIsValid(true);
} catch (err) {
setError(`Invalid JSON: ${err.message}`);
setOutput('');
setIsValid(false);
}
};
const minifyJson = () => {
try {
const parsed = JSON.parse(input);
const minified = JSON.stringify(parsed);
setOutput(minified);
setError('');
setIsValid(true);
} catch (err) {
setError(`Invalid JSON: ${err.message}`);
setOutput('');
setIsValid(false);
}
};
const validateJson = () => {
try {
JSON.parse(input);
setError('');
setIsValid(true);
setOutput('✅ Valid JSON');
} catch (err) {
setError(`Invalid JSON: ${err.message}`);
setIsValid(false);
setOutput('');
}
};
const clearAll = () => {
setInput('');
setOutput('');
setError('');
setIsValid(null);
};
const handleStructuredDataChange = (newData) => {
setStructuredData(newData);
setInput(JSON.stringify(newData, null, 2));
setError('');
setIsValid(true);
};
const switchToVisualEditor = () => {
try {
const parsed = input ? JSON.parse(input) : {};
setStructuredData(parsed);
setEditorMode('visual');
setError('');
setIsValid(true);
} catch (err) {
setError(`Cannot switch to visual editor: ${err.message}`);
setIsValid(false);
}
};
const switchToTextEditor = () => {
setEditorMode('text');
};
const loadSample = () => {
const sample = {
"name": "John Doe",
"age": 30,
"email": "john@example.com",
"address": {
"street": "123 Main St",
"city": "New York",
"zipCode": "10001"
},
"hobbies": ["reading", "coding", "traveling"],
"isActive": true
};
setInput(JSON.stringify(sample, null, 2));
setStructuredData(sample);
};
return (
<ToolLayout
title="JSON Encoder/Decoder"
description="Format, validate, and minify JSON data with syntax highlighting"
icon={Code}
>
{/* Editor Mode Toggle */}
<div className="flex bg-gray-100 dark:bg-gray-800 rounded-lg p-1 mb-6 w-fit">
<button
onClick={switchToTextEditor}
className={`flex items-center space-x-2 px-4 py-2 rounded-md font-medium transition-colors ${
editorMode === 'text'
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
: 'text-gray-600 dark:text-gray-400'
}`}
>
<Code className="h-4 w-4" />
<span>Text Editor</span>
</button>
<button
onClick={switchToVisualEditor}
className={`flex items-center space-x-2 px-4 py-2 rounded-md font-medium transition-colors ${
editorMode === 'visual'
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
: 'text-gray-600 dark:text-gray-400'
}`}
>
<Edit3 className="h-4 w-4" />
<span>Visual Editor</span>
</button>
</div>
{/* Controls */}
<div className="flex flex-wrap gap-3 mb-6">
<button onClick={formatJson} className="tool-button">
Format JSON
</button>
<button onClick={minifyJson} className="tool-button">
Minify JSON
</button>
<button onClick={validateJson} className="tool-button">
Validate JSON
</button>
<button onClick={loadSample} className="tool-button-secondary">
Load Sample
</button>
<button onClick={clearAll} className="tool-button-secondary">
Clear All
</button>
</div>
{/* Status Indicator */}
{isValid !== null && (
<div className={`flex items-center space-x-2 p-3 rounded-md mb-4 ${
isValid
? 'bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300'
: 'bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300'
}`}>
{isValid ? (
<CheckCircle className="h-5 w-5" />
) : (
<AlertCircle className="h-5 w-5" />
)}
<span className="font-medium">
{isValid ? 'Valid JSON' : 'Invalid JSON'}
</span>
</div>
)}
{/* Error Display */}
{error && (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md p-4 mb-4">
<div className="flex items-start space-x-2">
<AlertCircle className="h-5 w-5 text-red-500 mt-0.5" />
<div>
<h4 className="text-red-800 dark:text-red-200 font-medium">Error</h4>
<p className="text-red-700 dark:text-red-300 text-sm mt-1">{error}</p>
</div>
</div>
</div>
)}
{/* Input/Output Grid */}
<div className={`grid gap-6 ${
editorMode === 'visual'
? 'grid-cols-1'
: 'grid-cols-1 lg:grid-cols-2'
}`}>
{/* Input */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{editorMode === 'text' ? 'Input JSON' : 'Visual JSON Editor'}
</label>
<div className="relative">
{editorMode === 'text' ? (
<CodeEditor
value={input}
onChange={(value) => setInput(value)}
language="json"
placeholder="Paste your JSON here..."
height="400px"
className="w-full"
/>
) : (
<div className="min-h-96">
<StructuredEditor
initialData={structuredData}
onDataChange={handleStructuredDataChange}
/>
</div>
)}
</div>
</div>
{/* Output */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Output
</label>
<div className="relative">
<CodeEditor
value={output}
language="json"
readOnly={true}
placeholder="Formatted JSON will appear here..."
height="400px"
className="w-full"
/>
<div className="absolute top-2 right-2">
<CopyButton text={output} />
</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> Use "Format JSON" to beautify and indent your JSON</li>
<li> Use "Minify JSON" to compress JSON by removing whitespace</li>
<li> Use "Validate JSON" to check if your JSON syntax is correct</li>
<li> Click the copy button to copy the output to your clipboard</li>
</ul>
</div>
</ToolLayout>
);
};
export default JsonTool;

2277
src/pages/MarkdownEditor.js Normal file

File diff suppressed because it is too large Load Diff

93
src/pages/NotFound.js Normal file
View File

@@ -0,0 +1,93 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Home, Search, FileText, Edit3, Table, FileCode } from 'lucide-react';
import SEO from '../components/SEO';
const NotFound = () => {
const popularTools = [
{ name: 'Markdown Editor', path: '/markdown-editor', icon: FileText, desc: 'Write & preview markdown' },
{ name: 'Object Editor', path: '/object-editor', icon: Edit3, desc: 'Visual JSON editor' },
{ name: 'Table Editor', path: '/table-editor', icon: Table, desc: 'Edit CSV, JSON, Excel' },
{ name: 'Code Beautifier', path: '/beautifier', icon: FileCode, desc: 'Format & beautify code' }
];
return (
<>
<SEO
title="Page Not Found - 404"
description="The page you're looking for doesn't exist. Explore our free developer tools including JSON formatter, markdown editor, and more."
path="/404"
/>
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 dark:from-slate-900 dark:via-slate-800 dark:to-indigo-900 flex items-center justify-center px-4">
<div className="max-w-2xl w-full text-center">
{/* 404 Number */}
<div className="mb-8">
<h1 className="text-9xl font-bold bg-gradient-to-r from-blue-600 via-purple-600 to-indigo-600 bg-clip-text text-transparent animate-pulse">
404
</h1>
<div className="mt-4 flex items-center justify-center gap-2">
<Search className="h-6 w-6 text-gray-400" />
<p className="text-2xl font-semibold text-gray-700 dark:text-gray-300">
Page Not Found
</p>
</div>
</div>
{/* Message */}
<p className="text-lg text-gray-600 dark:text-gray-400 mb-12">
Oops! The page you're looking for doesn't exist. It might have been moved or deleted.
</p>
{/* Popular Tools */}
<div className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-6">
Try These Popular Tools Instead:
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{popularTools.map((tool) => {
const Icon = tool.icon;
return (
<Link
key={tool.path}
to={tool.path}
className="group bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700 hover:border-blue-500 dark:hover:border-blue-500 hover:shadow-lg transition-all duration-200"
>
<div className="flex items-start gap-4">
<div className="p-3 bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 rounded-lg group-hover:scale-110 transition-transform">
<Icon className="h-6 w-6 text-blue-600 dark:text-blue-400" />
</div>
<div className="flex-1 text-left">
<h3 className="font-semibold text-gray-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
{tool.name}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
{tool.desc}
</p>
</div>
</div>
</Link>
);
})}
</div>
</div>
{/* Home Button */}
<Link
to="/"
className="inline-flex items-center gap-2 px-8 py-4 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white font-semibold rounded-lg shadow-lg hover:shadow-xl transition-all duration-200 transform hover:scale-105"
>
<Home className="h-5 w-5" />
Go to Homepage
</Link>
{/* Search Suggestion */}
<p className="mt-8 text-sm text-gray-500 dark:text-gray-400">
Or use the search bar at the top to find what you need
</p>
</div>
</div>
</>
);
};
export default NotFound;

View File

@@ -1,10 +1,14 @@
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { Plus, Upload, FileText, Globe, Edit3, Download, Workflow, Table, Braces, Code, AlertTriangle, ChevronUp, ChevronDown } from 'lucide-react';
import ToolLayout from '../components/ToolLayout';
import ToolLayout from '../components/ToolLayout';
import StructuredEditor from '../components/StructuredEditor';
import MindmapView from '../components/MindmapView';
import PostmanTable from '../components/PostmanTable';
import CodeMirrorEditor from '../components/CodeMirrorEditor';
import AdvancedURLFetch from '../components/AdvancedURLFetch';
import SEO from '../components/SEO';
import RelatedTools from '../components/RelatedTools';
import { extractContentFromUrl, CONTENT_TYPE_INFO } from '../utils/contentExtractor';
const ObjectEditor = () => {
const exportCardRef = useRef(null);
@@ -29,6 +33,7 @@ const ObjectEditor = () => {
});
const [fetchUrl, setFetchUrl] = useState('');
const [fetching, setFetching] = useState(false);
// const [showAdvanced, setShowAdvanced] = useState(false); // Hidden for now
const [createNewCompleted, setCreateNewCompleted] = useState(false);
const [showInputChangeModal, setShowInputChangeModal] = useState(false);
const [pendingTabChange, setPendingTabChange] = useState(null);
@@ -603,43 +608,124 @@ const ObjectEditor = () => {
reader.readAsText(file);
};
// Fetch data from URL
const handleFetchData = async () => {
if (!fetchUrl.trim()) {
// Fetch data from URL with advanced content extraction
const handleFetchData = async (advancedOptions = null) => {
console.log('🚀 handleFetchData called with URL:', fetchUrl);
console.log('🔧 Advanced options:', advancedOptions);
const urlToFetch = advancedOptions?.url || fetchUrl.trim();
if (!urlToFetch) {
setError('Please enter a valid URL');
return;
}
setFetching(true);
setError('');
console.log('✅ Starting fetch process...');
try {
// Add protocol if missing
let url = fetchUrl.trim();
let url = urlToFetch;
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'https://' + url;
}
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
// Determine if this is an advanced request (has custom options)
const isAdvancedRequest = advancedOptions && (
advancedOptions.method !== 'GET' ||
Object.keys(advancedOptions.headers || {}).length > 0 ||
advancedOptions.body
);
console.log('🎯 Is advanced request:', isAdvancedRequest);
// Build fetch options for advanced mode
const fetchOptions = advancedOptions ? {
method: advancedOptions.method || 'GET',
headers: advancedOptions.headers || {},
body: advancedOptions.body || undefined
} : {};
console.log('📡 Fetch options:', fetchOptions);
// Try direct fetch first (for APIs)
try {
const response = await fetch(url, fetchOptions);
if (response.ok) {
const contentType = response.headers.get('content-type');
const text = await response.text();
// Try to parse as JSON
if (contentType?.includes('application/json') || text.trim().startsWith('{') || text.trim().startsWith('[')) {
try {
const data = JSON.parse(text);
setStructuredData(data);
generateOutputs(data);
setInputText(JSON.stringify(data, null, 2));
setInputFormat('JSON');
setInputValid(true);
setCreateNewCompleted(true);
// Set URL data summary
setUrlDataSummary({
format: 'JSON',
size: text.length,
properties: Object.keys(data).length,
url: url,
contentType: isAdvancedRequest ? `API Response (${advancedOptions.method})` : 'API Response'
});
setFetching(false);
return;
} catch (e) {
// Not valid JSON, continue to content extraction only if simple GET
if (isAdvancedRequest) {
throw new Error('Response is not valid JSON. Content-Type: ' + (contentType || 'unknown'));
}
}
} else if (isAdvancedRequest) {
// For advanced requests with non-JSON response, show error
throw new Error('Response is not JSON. Content-Type: ' + (contentType || 'unknown'));
}
} else {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
} catch (e) {
// If advanced request fails, don't try content extraction
if (isAdvancedRequest) {
throw e;
}
// CORS error or network error, will try with content extractor for simple GET
console.log('Direct fetch failed, trying content extractor:', e.message);
}
const contentType = response.headers.get('content-type');
const text = await response.text();
let data;
// Use advanced content extractor ONLY for simple GET requests to HTML pages
const result = await extractContentFromUrl(url);
if (!contentType || !contentType.includes('application/json')) {
// Try to parse as JSON anyway, some APIs don't set correct content-type
try {
data = JSON.parse(text);
} catch {
throw new Error('Response is not valid JSON. Content-Type: ' + (contentType || 'unknown'));
}
} else {
data = JSON.parse(text);
}
// Create structured data from extracted content
const data = {
url: result.url,
title: result.title,
contentType: result.contentType,
contentQuality: CONTENT_TYPE_INFO[result.contentType]?.label || result.contentType,
metadata: {
description: result.description,
author: result.author,
publishDate: result.publishDate,
wordCount: result.metrics.articleWordCount,
readingTime: Math.ceil(result.metrics.articleWordCount / 200) + ' min'
},
structure: result.structure,
content: {
articleText: result.articleText,
allText: result.allText
},
metrics: result.metrics
};
setStructuredData(data);
generateOutputs(data);
@@ -648,13 +734,18 @@ const ObjectEditor = () => {
setInputValid(true);
setCreateNewCompleted(true);
// Set URL data summary
// Set URL data summary with content type info
const contentInfo = CONTENT_TYPE_INFO[result.contentType];
setUrlDataSummary({
format: 'JSON',
size: text.length,
format: 'Extracted Content',
size: JSON.stringify(data).length,
properties: Object.keys(data).length,
url: url
url: url,
contentType: result.contentType,
contentTypeLabel: contentInfo?.label,
contentTypeEmoji: contentInfo?.emoji
});
} catch (err) {
console.error('Fetch error:', err);
setError(`Failed to fetch data: ${err.message}`);
@@ -670,11 +761,19 @@ const ObjectEditor = () => {
}, [structuredData, generateOutputs]);
return (
<ToolLayout
title="Object Editor"
description="Visual editor for JSON and PHP serialized objects with format conversion"
icon={Edit3}
>
<>
<SEO
title="Online JSON & Object Editor - Visual Data Editor"
description="✓ Edit JSON visually ✓ Fetch from URLs ✓ Nested objects & arrays ✓ Format conversion ✓ Privacy-first. Try it free - no data upload!"
keywords="json editor, object editor, json formatter, json validator, visual json editor, online json tool, api testing, json viewer, nested json editor"
path="/object-editor"
toolId="object-editor"
/>
<ToolLayout
title="Object Editor"
description="Visual editor for JSON and PHP serialized objects with format conversion"
icon={Edit3}
>
{/* Input Section with Tabs */}
<div className="w-full bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mb-4 sm:mb-6">
{/* Tabs */}
@@ -812,46 +911,43 @@ const ObjectEditor = () => {
{/* URL Tab Content */}
{activeTab === 'url' && (
urlDataSummary ? (
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<span className="text-sm text-green-700 dark:text-green-300 break-words">
✓ Data loaded: {urlDataSummary.format} ({urlDataSummary.size.toLocaleString()} chars, {urlDataSummary.properties} {urlDataSummary.properties === 1 ? 'property' : 'properties'})
</span>
<button
onClick={() => setUrlDataSummary(null)}
className="text-sm text-blue-600 dark:text-blue-400 hover:underline whitespace-nowrap"
>
Fetch New URL ▼
</button>
</div>
</div>
) : (
<div className="space-y-3">
<div className="flex gap-2">
<div className="relative flex-1">
<input
type="url"
value={fetchUrl}
onChange={(e) => setFetchUrl(e.target.value)}
placeholder="https://api.telegram.org/bot<token>/getMe"
className="tool-input w-full"
onKeyPress={(e) => e.key === 'Enter' && handleFetchData()}
/>
<div className="p-4">
{urlDataSummary && (
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div className="flex flex-col gap-1">
<span className="text-sm text-green-700 dark:text-green-300 break-words">
✓ Data loaded: {urlDataSummary.format} ({urlDataSummary.size.toLocaleString()} chars, {urlDataSummary.properties} {urlDataSummary.properties === 1 ? 'property' : 'properties'})
</span>
{urlDataSummary.contentTypeLabel && (
<span className="text-xs text-gray-600 dark:text-gray-400">
{urlDataSummary.contentTypeEmoji} {urlDataSummary.contentTypeLabel}
</span>
)}
</div>
<button
onClick={() => setUrlDataSummary(null)}
className="text-sm text-blue-600 dark:text-blue-400 hover:underline whitespace-nowrap"
>
Fetch New URL ▼
</button>
</div>
<button
onClick={handleFetchData}
disabled={fetching || !fetchUrl.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"
>
{fetching ? 'Fetching...' : 'Fetch Data'}
</button>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">
Enter any URL that returns JSON data. Examples: Telegram Bot API, JSONPlaceholder, GitHub API, etc.
</p>
)}
{/* Keep component mounted but hidden to preserve state */}
<div style={{ display: urlDataSummary ? 'none' : 'block' }}>
<AdvancedURLFetch
url={fetchUrl}
onUrlChange={setFetchUrl}
onFetch={handleFetchData}
fetching={fetching}
showAdvanced={false}
onToggleAdvanced={() => {}}
onUpgrade={() => {}}
/>
</div>
)
</div>
)}
{/* Paste Tab Content */}
@@ -1282,7 +1378,11 @@ const ObjectEditor = () => {
</div>
)}
</div>
{/* Related Tools */}
<RelatedTools toolId="object-editor" />
</ToolLayout>
</>
);
};

View File

@@ -1,547 +0,0 @@
import React, { useState } from 'react';
import { Database, Edit3 } from 'lucide-react';
import ToolLayout from '../components/ToolLayout';
import CopyButton from '../components/CopyButton';
import StructuredEditor from '../components/StructuredEditor';
const SerializeTool = () => {
const [input, setInput] = useState('');
const [output, setOutput] = useState('');
const [mode, setMode] = useState('serialize'); // 'serialize' or 'unserialize'
const [error, setError] = useState('');
const [editorMode, setEditorMode] = useState('text'); // 'text' or 'visual'
const [structuredData, setStructuredData] = useState({});
// Simple PHP serialize implementation for common data types
const phpSerialize = (data) => {
if (data === null) return 'N;';
if (typeof data === 'boolean') return data ? 'b:1;' : 'b:0;';
if (typeof data === 'number') {
return Number.isInteger(data) ? `i:${data};` : `d:${data};`;
}
if (typeof data === 'string') {
// Escape quotes and backslashes in the string first
const escapedData = data.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
// PHP serialize requires UTF-8 byte length of the ESCAPED string
const byteLength = new TextEncoder().encode(escapedData).length;
return `s:${byteLength}:"${escapedData}";`;
}
if (Array.isArray(data)) {
let result = `a:${data.length}:{`;
data.forEach((item, index) => {
result += phpSerialize(index) + phpSerialize(item);
});
result += '}';
return result;
}
if (typeof data === 'object') {
const keys = Object.keys(data);
let result = `a:${keys.length}:{`;
keys.forEach(key => {
result += phpSerialize(key) + phpSerialize(data[key]);
});
result += '}';
return result;
}
return 'N;';
};
// Simple PHP unserialize implementation
const phpUnserialize = (str) => {
let index = 0;
const parseValue = () => {
if (index >= str.length) {
throw new Error('Unexpected end of string');
}
const type = str[index];
// Handle NULL case (no colon after N)
if (type === 'N') {
index += 2; // Skip 'N;'
return null;
}
// For all other types, expect colon after type
if (str[index + 1] !== ':') {
throw new Error(`Expected ':' after type '${type}' at position ${index + 1}`);
}
index += 2; // Skip type and ':'
switch (type) {
case 'b':
const boolVal = str[index] === '1';
index += 2; // Skip value and ';'
return boolVal;
case 'i':
let intStr = '';
while (index < str.length && str[index] !== ';') {
intStr += str[index++];
}
if (index >= str.length) {
throw new Error('Unexpected end of string while parsing integer');
}
index++; // Skip ';'
return parseInt(intStr);
case 'd':
let floatStr = '';
while (index < str.length && str[index] !== ';') {
floatStr += str[index++];
}
if (index >= str.length) {
throw new Error('Unexpected end of string while parsing float');
}
index++; // Skip ';'
return parseFloat(floatStr);
case 's':
let lenStr = '';
while (index < str.length && str[index] !== ':') {
lenStr += str[index++];
}
if (index >= str.length) {
throw new Error('Unexpected end of string while parsing string length');
}
index++; // Skip ':'
// Expect opening quote
if (str[index] !== '"') {
throw new Error(`Expected '"' at position ${index}`);
}
index++; // Skip opening '"'
const byteLength = parseInt(lenStr);
if (isNaN(byteLength) || byteLength < 0) {
throw new Error(`Invalid string length: ${lenStr}`);
}
// Handle empty strings
if (byteLength === 0) {
// Expect closing quote and semicolon immediately
if (index + 1 >= str.length || str[index] !== '"' || str[index + 1] !== ';') {
throw new Error(`Expected '";' after empty string at position ${index}`);
}
index += 2; // Skip closing '";'
return '';
}
// Find the actual end of the string by looking for the closing quote-semicolon pattern
const startIndex = index;
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;
}
}
if (endQuotePos === -1) {
throw new Error(`Could not find closing '";' for string starting at position ${startIndex}`);
}
// Extract the actual string content
const stringVal = str.substring(startIndex, endQuotePos);
const actualByteLength = new TextEncoder().encode(stringVal).length;
// Move index to after the closing '";'
index = endQuotePos + 2;
// Warn about byte length mismatch but continue parsing
if (actualByteLength !== byteLength) {
console.warn(`Warning: String byte length mismatch - declared ${byteLength}, actual ${actualByteLength}`);
}
return stringVal;
case 'a':
let arrayLenStr = '';
while (index < str.length && str[index] !== ':') {
arrayLenStr += str[index++];
}
if (index >= str.length) {
throw new Error('Unexpected end of string while parsing array length');
}
index++; // Skip ':'
// Expect opening brace
if (str[index] !== '{') {
throw new Error(`Expected '{' at position ${index}`);
}
index++; // Skip '{'
const arrayLength = parseInt(arrayLenStr);
if (isNaN(arrayLength) || arrayLength < 0) {
throw new Error(`Invalid array length: ${arrayLenStr}`);
}
const result = {};
let isArray = true;
for (let i = 0; i < arrayLength; i++) {
const key = parseValue();
const value = parseValue();
result[key] = value;
// Check if this looks like a sequential array
if (typeof key !== 'number' || key !== i) {
isArray = false;
}
}
// Expect closing brace
if (index >= str.length || str[index] !== '}') {
throw new Error(`Expected '}' at position ${index}`);
}
index++; // Skip '}'
// Convert to array if all keys are sequential integers starting from 0
if (isArray && arrayLength > 0) {
const arr = [];
for (let i = 0; i < arrayLength; i++) {
arr[i] = result[i];
}
return arr;
}
return result;
default:
throw new Error(`Unknown type: '${type}' at position ${index - 2}`);
}
};
try {
const result = parseValue();
// Check if there's unexpected trailing data
if (index < str.length) {
console.warn(`Warning: Trailing data after parsing: "${str.substring(index)}"`);
}
return result;
} catch (error) {
throw new Error(`Parse error at position ${index}: ${error.message}`);
}
};
const handleSerialize = () => {
try {
const data = JSON.parse(input);
const serialized = phpSerialize(data);
setOutput(serialized);
} catch (err) {
setOutput(`Error: ${err.message}`);
}
};
const handleUnserialize = () => {
try {
const unserialized = phpUnserialize(input);
setOutput(JSON.stringify(unserialized, null, 2));
} catch (err) {
setOutput(`Error: ${err.message}`);
}
};
const handleProcess = () => {
if (mode === 'serialize') {
handleSerialize();
} else {
handleUnserialize();
}
};
// Editor mode switching functions
const switchToTextEditor = () => {
if (editorMode === 'visual') {
try {
const jsonString = JSON.stringify(structuredData, null, 2);
setInput(jsonString);
} catch (err) {
setError('Error converting structured data to JSON');
}
}
setEditorMode('text');
};
const switchToVisualEditor = () => {
if (editorMode === 'text' && input.trim()) {
try {
const parsed = JSON.parse(input);
setStructuredData(parsed);
setError('');
} catch (err) {
setError('Invalid JSON format. Please fix the JSON before switching to visual editor.');
return;
}
}
setEditorMode('visual');
};
const handleStructuredDataChange = (newData) => {
setStructuredData(newData);
try {
const jsonString = JSON.stringify(newData, null, 2);
setInput(jsonString);
setError('');
} catch (err) {
setError('Error updating JSON from structured data');
}
};
// 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('');
};
const loadSample = () => {
if (mode === 'serialize') {
setInput(`{
"name": "John Doe",
"age": 30,
"active": true,
"scores": [85, 92, 78],
"address": {
"street": "123 Main St",
"city": "New York"
}
}`);
} else {
setInput('a:4:{s:4:"name";s:8:"John Doe";s:3:"age";i:30;s:6:"active";b:1;s:6:"scores";a:3:{i:0;i:85;i:1;i:92;i:2;i:78;}}');
}
};
return (
<ToolLayout
title="Serialize Encoder/Decoder"
description="Encode and decode serialized data (PHP serialize format)"
icon={Database}
>
{/* Mode Toggle */}
<div className="flex bg-gray-100 dark:bg-gray-800 rounded-lg p-1 mb-6 w-fit">
<button
onClick={() => setMode('serialize')}
className={`px-4 py-2 rounded-md font-medium transition-colors ${
mode === 'serialize'
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
: 'text-gray-600 dark:text-gray-400'
}`}
>
Serialize
</button>
<button
onClick={() => setMode('unserialize')}
className={`px-4 py-2 rounded-md font-medium transition-colors ${
mode === 'unserialize'
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
: 'text-gray-600 dark:text-gray-400'
}`}
>
Unserialize
</button>
</div>
{/* Editor Mode Toggle - only show in serialize mode */}
{mode === 'serialize' && (
<div className="flex bg-gray-100 dark:bg-gray-800 rounded-lg p-1 mb-6 w-fit">
<button
onClick={switchToTextEditor}
className={`flex items-center space-x-2 px-4 py-2 rounded-md font-medium transition-colors ${
editorMode === 'text'
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
: 'text-gray-600 dark:text-gray-400'
}`}
>
<Database className="h-4 w-4" />
<span>Text Editor</span>
</button>
<button
onClick={switchToVisualEditor}
className={`flex items-center space-x-2 px-4 py-2 rounded-md font-medium transition-colors ${
editorMode === 'visual'
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
: 'text-gray-600 dark:text-gray-400'
}`}
>
<Edit3 className="h-4 w-4" />
<span>Visual Editor</span>
</button>
</div>
)}
{/* Controls */}
<div className="flex flex-wrap gap-3 mb-6">
<button onClick={handleProcess} className="tool-button">
{mode === 'serialize' ? 'Serialize Data' : 'Unserialize Data'}
</button>
<button onClick={loadSample} className="tool-button-secondary">
Load Sample
</button>
<button onClick={clearAll} className="tool-button-secondary">
Clear All
</button>
</div>
{/* Input/Output Grid */}
<div className={`grid gap-6 ${
mode === 'serialize' && editorMode === 'visual'
? 'grid-cols-1'
: 'grid-cols-1 lg:grid-cols-2'
}`}>
{/* Input */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{mode === 'serialize'
? (editorMode === 'text' ? 'JSON to Serialize' : 'Visual Data Editor')
: 'Serialized Data to Decode'
}
</label>
<div className="relative">
{mode === 'serialize' && editorMode === 'visual' ? (
<div className="min-h-96">
<StructuredEditor
initialData={structuredData}
onDataChange={handleStructuredDataChange}
/>
</div>
) : (
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder={
mode === 'serialize'
? 'Enter JSON data to serialize...'
: 'Enter serialized data to decode...'
}
className="tool-input h-96"
/>
)}
</div>
{error && (
<p className="text-sm text-red-600 dark:text-red-400 mt-2">
{error}
</p>
)}
</div>
{/* Output */}
<div className="space-y-2">
<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}
readOnly
placeholder={
mode === 'serialize'
? 'Serialized data will appear here...'
: 'Decoded JSON will appear here...'
}
className="tool-input h-96 bg-gray-50 dark:bg-gray-800"
/>
{output && <CopyButton text={output} />}
</div>
</div>
</div>
{/* Serialize Format Reference */}
<div className="bg-gray-50 dark:bg-gray-800 rounded-md p-4 mt-6">
<h4 className="text-gray-800 dark:text-gray-200 font-medium mb-3">PHP Serialize Format Reference</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-600 dark:text-gray-400">String:</span>
<span className="ml-2 font-mono text-gray-800 dark:text-gray-200">s:length:"value";</span>
</div>
<div>
<span className="text-gray-600 dark:text-gray-400">Integer:</span>
<span className="ml-2 font-mono text-gray-800 dark:text-gray-200">i:value;</span>
</div>
<div>
<span className="text-gray-600 dark:text-gray-400">Boolean:</span>
<span className="ml-2 font-mono text-gray-800 dark:text-gray-200">b:0; or b:1;</span>
</div>
<div>
<span className="text-gray-600 dark:text-gray-400">Null:</span>
<span className="ml-2 font-mono text-gray-800 dark:text-gray-200">N;</span>
</div>
<div>
<span className="text-gray-600 dark:text-gray-400">Array:</span>
<span className="ml-2 font-mono">a:length:&#123;...&#125;</span>
</div>
<div>
<span className="text-gray-600 dark:text-gray-400">Float:</span>
<span className="ml-2 font-mono text-gray-800 dark:text-gray-200">d:value;</span>
</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> PHP serialize format is commonly used for storing complex data structures</li>
<li> Input JSON data to serialize it into PHP format</li>
<li> Paste serialized data to convert it back to readable JSON</li>
<li> Supports strings, integers, floats, booleans, arrays, and objects</li>
</ul>
</div>
</ToolLayout>
);
};
export default SerializeTool;

View File

@@ -3,6 +3,8 @@ import { Plus, Upload, FileText, Globe, Download, X, Table, Trash2, Database, Br
import ToolLayout from '../components/ToolLayout';
import CodeMirrorEditor from '../components/CodeMirrorEditor';
import StructuredEditor from "../components/StructuredEditor";
import SEO from '../components/SEO';
import RelatedTools from '../components/RelatedTools';
import Papa from "papaparse";
const TableEditor = () => {
@@ -1838,11 +1840,19 @@ const TableEditor = () => {
return (
<ToolLayout
title="Table Editor"
description="Import, edit, and export tabular data from various sources"
icon={Table}
>
<>
<SEO
title="Excel-Like Table Editor - Import CSV, JSON, Excel Online"
description="✓ Import CSV, JSON, Excel ✓ Visual editing ✓ Export to multiple formats ✓ Sort & filter ✓ No installation. Edit tables online now!"
keywords="table editor, csv editor, excel online, json to csv, csv to json, data editor, spreadsheet editor, online table, sql editor, database editor"
path="/table-editor"
toolId="table-editor"
/>
<ToolLayout
title="Table Editor"
description="Import, edit, and export tabular data from various sources"
icon={Table}
>
{/* Input Section with Tabs */}
<div className="w-full bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
{/* Tabs */}
@@ -2344,7 +2354,7 @@ const TableEditor = () => {
<thead className="bg-gray-50 dark:bg-gray-700 sticky top-[-1px] z-10">
<tr>
<th
className={`px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border-r border-gray-200 dark:border-gray-600 ${
className={`px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 tracking-wider border-r border-gray-200 dark:border-gray-600 ${
frozenColumns > 0
? "sticky left-0 z-20 bg-blue-50 dark:!bg-blue-900"
: ""
@@ -3261,7 +3271,11 @@ const TableEditor = () => {
</div>
)}
</div>
{/* Related Tools */}
<RelatedTools toolId="table-editor" />
</ToolLayout>
</>
);
};

View File

@@ -0,0 +1,380 @@
/* GitHub-style Markdown Preview Styling */
.markdown-preview {
color: #24292f;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 1.6;
word-wrap: break-word;
overflow-wrap: break-word;
max-width: 100%;
word-break: break-word;
}
/* Ensure all child elements respect container width */
.markdown-preview * {
max-width: 100%;
box-sizing: border-box;
}
.dark .markdown-preview {
color: #c9d1d9;
}
.markdown-preview h1,
.markdown-preview h2,
.markdown-preview h3,
.markdown-preview h4,
.markdown-preview h5,
.markdown-preview h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
}
.markdown-preview h1 {
font-size: 2em;
border-bottom: 1px solid #d0d7de;
padding-bottom: 0.3em;
}
.dark .markdown-preview h1 {
border-bottom-color: #21262d;
}
.markdown-preview h2 {
font-size: 1.5em;
border-bottom: 1px solid #d0d7de;
padding-bottom: 0.3em;
}
.dark .markdown-preview h2 {
border-bottom-color: #21262d;
}
.markdown-preview h3 {
font-size: 1.25em;
}
.markdown-preview h4 {
font-size: 1em;
}
.markdown-preview h5 {
font-size: 0.875em;
}
.markdown-preview h6 {
font-size: 0.85em;
color: #57606a;
}
.dark .markdown-preview h6 {
color: #8b949e;
}
.markdown-preview p {
margin-top: 0;
margin-bottom: 16px;
}
/* Inline code - with background */
.markdown-preview code {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
background-color: rgba(175, 184, 193, 0.2);
border-radius: 6px;
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
}
.dark .markdown-preview code {
background-color: rgba(110, 118, 129, 0.4);
}
/* Code block wrapper with header */
.markdown-preview .code-block-wrapper {
margin-bottom: 16px;
border-radius: 8px;
overflow: hidden;
border: 1px solid #d0d7de;
background-color: #f6f8fa;
}
.dark .markdown-preview .code-block-wrapper {
border-color: #30363d;
background-color: #0d1117;
}
/* Code block header */
.markdown-preview .code-block-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 10px;
background-color: #f6f8fa;
border-bottom: 1px solid #d0d7de;
font-size: 12px;
}
.dark .markdown-preview .code-block-header {
background-color: #161b22;
border-bottom-color: #30363d;
}
/* Language label */
.markdown-preview .code-block-language {
font-weight: 600;
color: #57606a;
text-transform: uppercase;
font-size: 10px;
letter-spacing: 0.5px;
}
.dark .markdown-preview .code-block-language {
color: #8b949e;
}
/* Copy button */
.markdown-preview .code-block-copy {
padding: 2px 6px;
background-color: transparent;
border: 1px solid #d0d7de;
border-radius: 6px;
color: #24292f;
font-size: 10px;
cursor: pointer;
transition: all 0.2s;
font-weight: 500;
}
.markdown-preview .code-block-copy:hover {
background-color: #f3f4f6;
border-color: #1f2328;
}
.dark .markdown-preview .code-block-copy {
color: #c9d1d9;
border-color: #30363d;
}
.dark .markdown-preview .code-block-copy:hover {
background-color: #21262d;
border-color: #8b949e;
}
/* Code blocks - with background */
.markdown-preview .code-block-wrapper pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #0d1117;
margin: 0;
border-radius: 0;
}
/* Legacy pre blocks (without wrapper) */
.markdown-preview pre:not(.code-block-wrapper pre) {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #afb8c133;
border-radius: 6px;
margin-bottom: 16px;
}
.dark .markdown-preview pre:not(.code-block-wrapper pre) {
background-color: rgba(110, 118, 129, 0.4);
}
/* Code inside pre blocks - NO background (transparent) */
.markdown-preview pre code {
display: inline;
max-width: auto;
padding: 0;
margin: 0;
overflow: visible;
line-height: inherit;
word-wrap: normal;
background-color: transparent !important;
border: 0;
border-radius: 0;
}
/* Preserve highlight.js syntax highlighting colors */
.markdown-preview pre code.hljs {
background: transparent !important;
padding: 0 !important;
}
.markdown-preview table {
border-spacing: 0;
border-collapse: collapse;
display: block;
width: 100%;
max-width: 100%;
overflow-x: auto;
margin-bottom: 16px;
}
.markdown-preview table tr {
background-color: #ffffff;
border-top: 1px solid #d0d7de;
}
.dark .markdown-preview table tr {
background-color: #0d1117;
border-top-color: #21262d;
}
.markdown-preview table tr:nth-child(2n) {
background-color: #f6f8fa;
}
.dark .markdown-preview table tr:nth-child(2n) {
background-color: #161b22;
}
.markdown-preview table th,
.markdown-preview table td {
padding: 6px 13px;
border: 1px solid #d0d7de;
}
.dark .markdown-preview table th,
.dark .markdown-preview table td {
border-color: #21262d;
}
.markdown-preview table th {
font-weight: 600;
background-color: #f6f8fa;
}
.dark .markdown-preview table th {
background-color: #161b22;
}
.markdown-preview blockquote {
padding: 0 1em;
color: #57606a;
border-left: 0.25em solid #d0d7de;
margin: 0 0 16px 0;
}
.dark .markdown-preview blockquote {
color: #8b949e;
border-left-color: #3b434b;
}
.markdown-preview ul,
.markdown-preview ol {
padding-left: 2em;
margin-top: 0;
margin-bottom: 16px;
}
/* Nested lists */
.markdown-preview ul ul,
.markdown-preview ul ol,
.markdown-preview ol ul,
.markdown-preview ol ol {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
/* List items */
.markdown-preview li {
margin-bottom: 0.25em;
line-height: 1.6;
}
.markdown-preview li + li {
margin-top: 0.25em;
}
/* Better bullet points */
.markdown-preview ul > li {
list-style-type: disc;
}
.markdown-preview ul ul > li {
list-style-type: circle;
}
.markdown-preview ul ul ul > li {
list-style-type: square;
}
/* Ordered list styling */
.markdown-preview ol > li {
list-style-type: decimal;
}
.markdown-preview ol ol > li {
list-style-type: lower-alpha;
}
.markdown-preview ol ol ol > li {
list-style-type: lower-roman;
}
/* List item content spacing */
.markdown-preview li > p {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
.markdown-preview li > p:first-child {
margin-top: 0;
}
.markdown-preview li > p:last-child {
margin-bottom: 0;
}
.markdown-preview hr {
height: 0.25em;
padding: 0;
margin: 24px 0;
background-color: #d0d7de;
border: 0;
}
.dark .markdown-preview hr {
background-color: #21262d;
}
.markdown-preview a {
color: #0969da;
text-decoration: none;
}
.dark .markdown-preview a {
color: #58a6ff;
}
.markdown-preview a:hover {
text-decoration: underline;
}
.markdown-preview strong {
font-weight: 600;
}
.markdown-preview em {
font-style: italic;
}
.markdown-preview u {
text-decoration: underline;
}
.markdown-preview img {
max-width: 100%;
height: auto;
border-radius: 6px;
margin: 16px 0;
}