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:
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" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user