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:
10
src/App.js
10
src/App.js
@@ -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
36
src/components/AdBlock.js
Normal 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;
|
||||
32
src/components/AdColumn.js
Normal file
32
src/components/AdColumn.js
Normal 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;
|
||||
531
src/components/AdvancedURLFetch.js
Normal file
531
src/components/AdvancedURLFetch.js
Normal 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;
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
66
src/components/MobileAdBanner.js
Normal file
66
src/components/MobileAdBanner.js
Normal 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
123
src/components/ProBadge.js
Normal 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;
|
||||
96
src/components/RelatedTools.js
Normal file
96
src/components/RelatedTools.js
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
81
src/config/features.js
Normal 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;
|
||||
};
|
||||
@@ -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
100
src/data/faqs.js
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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
2277
src/pages/MarkdownEditor.js
Normal file
File diff suppressed because it is too large
Load Diff
93
src/pages/NotFound.js
Normal file
93
src/pages/NotFound.js
Normal 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;
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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:{...}</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;
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
380
src/styles/markdown-preview.css
Normal file
380
src/styles/markdown-preview.css
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user