feat: Invoice Editor improvements and code cleanup

Major Invoice Editor updates:
-  Fixed tripled scrollbar issue by removing unnecessary overflow classes
-  Implemented dynamic currency system with JSON data loading
-  Fixed F4 PDF generation error with proper paper size handling
-  Added proper padding to Total section matching table headers
-  Removed print functionality (users can print from PDF download)
-  Streamlined preview toolbar: Back, Size selector, Download PDF
-  Fixed all ESLint warnings and errors
-  Removed console.log statements across codebase for cleaner production
-  Added border-top to Total section for better visual consistency
-  Improved print CSS and removed JSX warnings

Additional improvements:
- Added currencies.json to public folder for proper HTTP access
- Enhanced MinimalTemplate with better spacing and layout
- Clean build with no warnings or errors
- Updated release notes with new features
This commit is contained in:
dwindown
2025-09-28 00:09:06 +07:00
parent b2850ea145
commit 04db088ff9
29 changed files with 5471 additions and 482 deletions

View File

@@ -6,7 +6,6 @@ import { TOOLS, SITE_CONFIG } from '../config/tools';
import { useAnalytics } from '../hooks/useAnalytics';
const Home = () => {
console.log('🏠 NEW Home component loaded - Object Editor should be visible!');
const [searchTerm, setSearchTerm] = useState('');
const [mounted, setMounted] = useState(false);
const { trackSearch } = useAnalytics();

2733
src/pages/InvoiceEditor.js Normal file

File diff suppressed because it is too large Load Diff

242
src/pages/InvoicePreview.js Normal file
View File

@@ -0,0 +1,242 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { ArrowLeft, Download, FileText } from 'lucide-react';
import html2pdf from 'html2pdf.js';
import MinimalTemplate from '../components/invoice-templates/MinimalTemplate';
// Available templates
const templates = {
minimal: {
name: 'Minimal',
description: 'Simple, professional layout',
component: MinimalTemplate
}
};
const InvoicePreview = () => {
const navigate = useNavigate();
const [invoiceData, setInvoiceData] = useState(null);
const [pdfPageSize, setPdfPageSize] = useState('A4');
const [isGenerating, setIsGenerating] = useState(false);
const [selectedTemplate] = useState('minimal');
// Load invoice data from localStorage
useEffect(() => {
try {
const savedInvoice = localStorage.getItem('currentInvoice');
const savedPageSize = localStorage.getItem('pdfPageSize');
if (savedInvoice) {
const parsedInvoice = JSON.parse(savedInvoice);
setInvoiceData(parsedInvoice);
// Set page title with invoice number
document.title = `Invoice Preview - ${parsedInvoice.invoiceNumber || 'Draft'} | DevTools`;
} else {
// No invoice data, redirect back to editor
navigate('/invoice-editor');
}
if (savedPageSize) {
setPdfPageSize(savedPageSize);
}
} catch (error) {
console.error('Failed to load invoice data:', error);
navigate('/invoice-editor', { replace: true });
}
}, [navigate]);
// Format number with thousand separator
const formatNumber = (num) => {
if (!invoiceData?.settings?.thousandSeparator) return num.toString();
return num.toLocaleString('en-US', { minimumFractionDigits: 2 });
};
// Format currency
const formatCurrency = (amount, useThousandSeparator = false) => {
const symbol = invoiceData?.settings?.currency?.symbol || '$';
const formattedAmount = useThousandSeparator ? formatNumber(amount) : amount.toFixed(2);
return `${symbol} ${formattedAmount}`;
};
// Generate PDF from the visible invoice
const handleDownloadPDF = async () => {
if (!invoiceData) return;
setIsGenerating(true);
try {
const element = document.getElementById('invoice-content');
if (!element) {
throw new Error('Invoice content not found');
}
const opt = {
margin: [0.2, 0.4, 0.5, 0.4], // top, left, bottom, right margins in inches - reduced top margin for first page
filename: `invoice-${invoiceData.invoiceNumber || 'draft'}.pdf`,
image: { type: 'png', quality: 0.98 },
html2canvas: {
useCORS: true,
backgroundColor: '#ffffff',
letterRendering: true
},
jsPDF: {
unit: 'in',
format: pdfPageSize === 'F4' ? [8.27, 13] : pdfPageSize.toLowerCase(), // F4 dimensions in inches
orientation: 'portrait'
},
pagebreak: {
mode: ['avoid-all', 'css', 'legacy'],
before: '.page-break-before',
after: '.page-break-after',
avoid: '.page-break-avoid'
}
};
await html2pdf().set(opt).from(element).save();
} catch (error) {
console.error('PDF generation failed:', error);
alert('Failed to generate PDF. Please try again.');
} finally {
setIsGenerating(false);
}
};
// Navigate back to editor
const handleEditInvoice = () => {
// Ensure current invoice data is saved before navigating
if (invoiceData) {
try {
localStorage.setItem('currentInvoice', JSON.stringify(invoiceData));
} catch (error) {
console.error('Failed to save invoice data before edit:', error);
}
}
// Add a parameter to indicate we're editing existing data
navigate('/invoice-editor?mode=edit');
};
if (!invoiceData) {
return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">Loading invoice...</p>
</div>
</div>
);
}
// Get selected template component
const SelectedTemplateComponent = templates[selectedTemplate].component;
return (
<div className="min-h-screen bg-gray-100 dark:bg-slate-900">
{/* Mobile Notice */}
<div className="lg:hidden bg-amber-50 dark:bg-amber-900/20 border-b border-amber-200 dark:border-amber-800 p-4">
<div className="flex items-center gap-3">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-amber-600 dark:text-amber-400" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
</div>
<div className="flex-1">
<h3 className="text-sm font-medium text-amber-800 dark:text-amber-200">
Desktop Mode Recommended
</h3>
<p className="text-sm text-amber-700 dark:text-amber-300 mt-1">
For the best preview experience and accurate PDF generation, please use desktop mode or a larger screen. The invoice preview is optimized for desktop viewing.
</p>
</div>
</div>
</div>
{/* Invoice Preview */}
<div className="max-w-5xl mx-auto p-4 sm:p-6" style={{ maxWidth: 'min(60rem, calc(100vw - 2rem))' }}>
<div className="bg-white dark:bg-slate-800 rounded-lg shadow-sm border border-gray-200 dark:border-slate-700 overflow-hidden">
{(
<div className="px-4 sm:px-6 py-4 border-b border-gray-200 dark:border-slate-700">
<div className="flex items-center justify-between flex-col sm:flex-row gap-4">
<div className="flex items-center gap-3">
<FileText className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Invoice Preview</h2>
<div className="hidden sm:flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
<span></span>
<span>{pdfPageSize} Format</span>
</div>
</div>
<div className="flex items-center gap-2">
{/* Back Button */}
<button
onClick={handleEditInvoice}
className="flex items-center gap-2 px-3 py-2 text-gray-600 dark:text-gray-300 hover:text-gray-800 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-slate-700 rounded-lg transition-colors"
>
<ArrowLeft className="h-4 w-4" />
<span className="hidden sm:inline">Back</span>
</button>
{/* Paper Size Selector */}
<div className="flex items-center gap-2">
<label className="text-sm text-gray-600 dark:text-gray-300">Size:</label>
<select
value={pdfPageSize}
onChange={(e) => {
setPdfPageSize(e.target.value);
localStorage.setItem('pdfPageSize', e.target.value);
}}
className="px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-slate-700 text-gray-900 dark:text-gray-100"
>
<option value="A4">A4</option>
<option value="F4">F4</option>
</select>
</div>
{/* Download PDF Button */}
<button
onClick={handleDownloadPDF}
disabled={isGenerating}
className="flex items-center gap-2 px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors disabled:opacity-50"
>
<Download className="h-4 w-4" />
<span className="hidden sm:inline">{isGenerating ? 'Generating...' : 'Download PDF'}</span>
</button>
</div>
</div>
</div>
)}
<div className="p-4 sm:p-6">
<div className="bg-white rounded-lg shadow-lg overflow-x-auto">
{/* PDF-Ready Invoice Content */}
<div
id="invoice-content"
className="bg-white"
style={{
maxWidth: pdfPageSize === 'A4' ? '720px' : '750px',
width: '100%',
minHeight: pdfPageSize === 'A4' ? '720px' : '750px',
margin: '0 auto',
position: 'relative',
}}
>
<SelectedTemplateComponent
invoiceData={invoiceData}
formatNumber={formatNumber}
formatCurrency={formatCurrency}
/>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default InvoicePreview;

View File

@@ -0,0 +1,461 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { ArrowLeft, Download, FileText, Plus } from 'lucide-react';
import html2pdf from 'html2pdf.js';
const InvoicePreviewMinimal = () => {
const navigate = useNavigate();
const [invoiceData, setInvoiceData] = useState(null);
const [pdfPageSize, setPdfPageSize] = useState('A4');
const [isGenerating, setIsGenerating] = useState(false);
// Load invoice data from localStorage
useEffect(() => {
try {
const savedInvoice = localStorage.getItem('currentInvoice');
const savedPageSize = localStorage.getItem('pdfPageSize');
if (savedInvoice) {
const parsedInvoice = JSON.parse(savedInvoice);
setInvoiceData(parsedInvoice);
// Set page title with invoice number
document.title = `Invoice Preview - ${parsedInvoice.invoiceNumber || 'Draft'} | DevTools`;
} else {
// No invoice data, redirect back to editor
navigate('/invoice-editor');
}
if (savedPageSize) {
setPdfPageSize(savedPageSize);
}
} catch (error) {
console.error('Failed to load invoice data:', error);
navigate('/invoice-editor');
}
}, [navigate]);
// Format number with thousand separator
const formatNumber = (num) => {
if (!invoiceData?.settings?.thousandSeparator) return num.toString();
return num.toLocaleString();
};
// Format currency
const formatCurrency = (amount, useThousandSeparator = false) => {
const symbol = invoiceData?.settings?.currency?.symbol || '$';
const formattedAmount = useThousandSeparator ? formatNumber(amount) : amount.toFixed(2);
return `${symbol}${formattedAmount}`;
};
// Generate PDF from the visible invoice
const handleDownloadPDF = async () => {
if (!invoiceData) return;
setIsGenerating(true);
try {
const element = document.getElementById('minimal-invoice-content');
if (!element) {
throw new Error('Invoice content not found');
}
const opt = {
margin: [15, 15, 15, 15],
filename: `Invoice-${invoiceData.invoiceNumber || new Date().toISOString().split('T')[0]}.pdf`,
image: { type: 'jpeg', quality: 0.98 },
html2canvas: {
scale: 2,
useCORS: true,
letterRendering: true,
allowTaint: true,
backgroundColor: '#ffffff',
width: pdfPageSize === 'A4' ? 794 : 816,
height: pdfPageSize === 'A4' ? 1123 : 1248
},
jsPDF: {
unit: 'px',
format: pdfPageSize === 'A4' ? [794, 1123] : [816, 1248],
orientation: 'portrait'
}
};
await html2pdf().set(opt).from(element).save();
} catch (error) {
console.error('PDF generation failed:', error);
alert('Failed to generate PDF. Please try again.');
} finally {
setIsGenerating(false);
}
};
// Navigate back to editor
const handleEditInvoice = () => {
navigate('/invoice-editor');
};
// Create new invoice
const handleNewInvoice = () => {
localStorage.removeItem('currentInvoice');
navigate('/invoice-editor');
};
if (!invoiceData) {
return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">Loading invoice...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-100">
{/* Top Action Bar */}
<div className="bg-white shadow-sm border-b sticky top-0 z-10">
<div className="max-w-7xl mx-auto px-4 py-3">
<div className="flex items-center justify-between">
{/* Left Actions */}
<div className="flex items-center gap-4">
<button
onClick={handleEditInvoice}
className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition-colors"
>
<ArrowLeft className="h-4 w-4" />
Edit Invoice
</button>
<div className="h-6 w-px bg-gray-300"></div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<FileText className="h-4 w-4" />
<span className="font-medium">Minimal Invoice Preview</span>
<span className="text-gray-400"></span>
<span>{pdfPageSize} Format</span>
</div>
</div>
{/* Right Actions */}
<div className="flex items-center gap-3">
<button
onClick={handleNewInvoice}
className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition-colors"
>
<Plus className="h-4 w-4" />
New Invoice
</button>
<button
onClick={handleDownloadPDF}
disabled={isGenerating}
className="flex items-center gap-2 px-6 py-2 bg-amber-500 hover:bg-amber-600 disabled:bg-amber-400 text-white rounded-lg transition-colors font-medium"
>
<Download className="h-4 w-4" />
{isGenerating ? 'Generating...' : 'Download PDF'}
</button>
</div>
</div>
</div>
</div>
{/* Invoice Preview */}
<div className="max-w-4xl mx-auto p-6">
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
{/* Minimal Invoice Content - Recreating the exact design from image */}
<div
id="minimal-invoice-content"
className="bg-white"
style={{
width: pdfPageSize === 'A4' ? '794px' : '816px',
minHeight: pdfPageSize === 'A4' ? '1123px' : '1248px',
margin: '0 auto',
padding: '60px',
fontSize: '14px',
lineHeight: '1.5',
fontFamily: 'system-ui, -apple-system, sans-serif',
color: '#000000'
}}
>
{/* Header Section */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '80px' }}>
{/* Company Logo & Name */}
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
{/* Golden Star Logo */}
<div style={{
width: '40px',
height: '40px',
background: '#D4AF37',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '20px',
color: 'white',
fontWeight: 'bold'
}}>
</div>
<div>
<h1 style={{
fontSize: '24px',
fontWeight: 'bold',
color: '#000000',
margin: '0',
lineHeight: '1.2'
}}>
{invoiceData.company.name || 'Borcelle'}
</h1>
<p style={{
fontSize: '14px',
color: '#666666',
margin: '2px 0 0 0',
fontWeight: '400'
}}>
Meet All Your Needs
</p>
</div>
</div>
{/* Invoice Title */}
<h2 style={{
fontSize: '48px',
fontWeight: 'bold',
color: '#000000',
margin: '0',
letterSpacing: '2px'
}}>
INVOICE
</h2>
</div>
{/* Invoice Details Section */}
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '60px' }}>
{/* Invoice To */}
<div style={{ flex: 1 }}>
<h3 style={{
fontSize: '16px',
fontWeight: 'bold',
color: '#000000',
marginBottom: '12px'
}}>
Invoice to:
</h3>
<div style={{ fontSize: '16px', fontWeight: 'bold', color: '#000000', marginBottom: '8px' }}>
{invoiceData.client.name || 'Daniel Gallego'}
</div>
<div style={{ fontSize: '14px', color: '#000000', lineHeight: '1.6' }}>
<div>{invoiceData.client.address || '123 Anywhere St.,'}</div>
<div>{invoiceData.client.city || 'Any City, ST 12345'}</div>
</div>
</div>
{/* Invoice Number & Date */}
<div style={{ textAlign: 'right', minWidth: '200px' }}>
<div style={{ marginBottom: '12px' }}>
<span style={{ fontSize: '16px', fontWeight: 'bold', color: '#000000' }}>Invoice# </span>
<span style={{ fontSize: '16px', color: '#000000', marginLeft: '20px' }}>
{invoiceData.invoiceNumber || '52131'}
</span>
</div>
<div>
<span style={{ fontSize: '16px', fontWeight: 'bold', color: '#000000' }}>Date </span>
<span style={{ fontSize: '16px', color: '#000000', marginLeft: '20px' }}>
{invoiceData.date || '01/02/2023'}
</span>
</div>
</div>
</div>
{/* Items Table */}
<div style={{ marginBottom: '40px' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
{/* Table Header */}
<thead>
<tr style={{ borderBottom: '2px solid #000000' }}>
<th style={{
padding: '12px 0',
textAlign: 'left',
fontSize: '16px',
fontWeight: 'bold',
color: '#000000'
}}>Item</th>
<th style={{
padding: '12px 0',
textAlign: 'center',
fontSize: '16px',
fontWeight: 'bold',
color: '#000000'
}}>Quantity</th>
<th style={{
padding: '12px 0',
textAlign: 'center',
fontSize: '16px',
fontWeight: 'bold',
color: '#000000'
}}>Unit Price</th>
<th style={{
padding: '12px 0',
textAlign: 'right',
fontSize: '16px',
fontWeight: 'bold',
color: '#000000'
}}>Total</th>
</tr>
</thead>
{/* Table Body */}
<tbody>
{invoiceData.items.map((item, index) => (
<tr key={item.id} style={{
borderBottom: '1px solid #E5E5E5'
}}>
<td style={{
padding: '16px 0',
fontSize: '14px',
color: '#000000'
}}>
{item.description}
</td>
<td style={{
padding: '16px 0',
textAlign: 'center',
fontSize: '14px',
color: '#000000'
}}>
{formatNumber(item.quantity)}
</td>
<td style={{
padding: '16px 0',
textAlign: 'center',
fontSize: '14px',
color: '#000000'
}}>
{formatCurrency(item.rate, true)}
</td>
<td style={{
padding: '16px 0',
textAlign: 'right',
fontSize: '14px',
color: '#000000'
}}>
{formatCurrency(item.amount, true)}
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Payment Method & Totals Section */}
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '80px' }}>
{/* Payment Method */}
<div style={{ flex: 1, maxWidth: '300px' }}>
<h3 style={{
fontSize: '16px',
fontWeight: 'bold',
color: '#000000',
marginBottom: '16px'
}}>
PAYMENT METHOD
</h3>
<div style={{ fontSize: '14px', color: '#000000', lineHeight: '1.6' }}>
<div style={{ marginBottom: '4px' }}>Rimberio Bank</div>
<div style={{ marginBottom: '4px' }}>Account Name: Alfredo Torres</div>
<div style={{ marginBottom: '4px' }}>Account No.: 0123 4567 8901</div>
<div>Pay by: 23 June 2023</div>
</div>
</div>
{/* Totals */}
<div style={{ textAlign: 'right', minWidth: '200px' }}>
<div style={{ marginBottom: '12px' }}>
<span style={{ fontSize: '14px', color: '#000000' }}>Subtotal</span>
<span style={{ fontSize: '14px', color: '#000000', marginLeft: '40px' }}>
{formatCurrency(invoiceData.subtotal, true)}
</span>
</div>
{invoiceData.taxRate > 0 && (
<div style={{ marginBottom: '12px' }}>
<span style={{ fontSize: '14px', color: '#000000' }}>
Tax ({formatNumber(invoiceData.taxRate)}%)
</span>
<span style={{ fontSize: '14px', color: '#000000', marginLeft: '40px' }}>
{formatCurrency(invoiceData.taxAmount, true)}
</span>
</div>
)}
<div style={{
borderTop: '1px solid #000000',
paddingTop: '12px',
marginTop: '16px'
}}>
<span style={{ fontSize: '18px', fontWeight: 'bold', color: '#000000' }}>Total</span>
<span style={{
fontSize: '18px',
fontWeight: 'bold',
color: '#000000',
marginLeft: '40px'
}}>
{formatCurrency(invoiceData.total, true)}
</span>
</div>
</div>
</div>
{/* Footer Section */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', marginTop: '120px' }}>
{/* Thank You Message */}
<div>
<p style={{ fontSize: '16px', color: '#000000', margin: '0' }}>
Thank you for your business!
</p>
</div>
{/* Signature Line */}
<div style={{ textAlign: 'center' }}>
<div style={{
width: '200px',
borderBottom: '2px solid #D4AF37',
marginBottom: '8px'
}}></div>
<p style={{ fontSize: '12px', color: '#666666', margin: '0' }}>
Authorized Signed
</p>
</div>
</div>
{/* Bottom Golden Bar */}
<div style={{
position: 'absolute',
bottom: '0',
left: '0',
right: '0',
height: '60px',
background: '#D4AF37',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '40px',
color: 'white',
fontSize: '14px'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span>📞</span>
<span>123-456-7890</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span>📍</span>
<span>123 Anywhere St., Any City</span>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default InvoicePreviewMinimal;

View File

@@ -3,6 +3,7 @@ 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('');
@@ -196,11 +197,13 @@ const JsonTool = () => {
</label>
<div className="relative">
{editorMode === 'text' ? (
<textarea
<CodeEditor
value={input}
onChange={(e) => setInput(e.target.value)}
onChange={(value) => setInput(value)}
language="json"
placeholder="Paste your JSON here..."
className="tool-input h-96"
height="400px"
className="w-full"
/>
) : (
<div className="min-h-96">
@@ -219,13 +222,17 @@ const JsonTool = () => {
Output
</label>
<div className="relative">
<textarea
<CodeEditor
value={output}
readOnly
language="json"
readOnly={true}
placeholder="Formatted JSON will appear here..."
className="tool-input h-96 bg-gray-50 dark:bg-gray-800"
height="400px"
className="w-full"
/>
{output && <CopyButton text={output} />}
<div className="absolute top-2 right-2">
<CopyButton text={output} />
</div>
</div>
</div>
</div>

View File

@@ -1,13 +1,41 @@
import React, { useState, useRef, useCallback } from 'react';
import { Upload, FileText, Workflow, Table, Globe, Plus, AlertTriangle, BrushCleaning, Code, Braces, Download, Edit3 } from 'lucide-react';
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { Plus, Upload, FileText, Globe, Edit3, Download, Workflow, Table, Braces, Code, AlertTriangle } from 'lucide-react';
import ToolLayout from '../components/ToolLayout';
import StructuredEditor from '../components/StructuredEditor';
import MindmapView from '../components/MindmapView';
import PostmanTable from '../components/PostmanTable';
import CodeEditor from '../components/CodeEditor';
// Hook to detect dark mode
const useDarkMode = () => {
const [isDark, setIsDark] = useState(() => {
return document.documentElement.classList.contains('dark');
});
useEffect(() => {
const observer = new MutationObserver(() => {
setIsDark(document.documentElement.classList.contains('dark'));
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
return () => observer.disconnect();
}, []);
return isDark;
};
const ObjectEditor = () => {
console.log(' ObjectEditor component loaded successfully!');
const isDark = useDarkMode();
const [structuredData, setStructuredData] = useState({});
// Sync structured data to localStorage for navigation guard
useEffect(() => {
localStorage.setItem('objectEditorData', JSON.stringify(structuredData));
}, [structuredData]);
const [activeTab, setActiveTab] = useState('create');
const [inputText, setInputText] = useState('');
const [inputFormat, setInputFormat] = useState('');
@@ -581,52 +609,53 @@ const ObjectEditor = () => {
icon={Edit3}
>
{/* Input Section with Tabs */}
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mb-6">
<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 */}
<div className="flex border-b border-gray-200 dark:border-gray-700 overflow-x-auto scrollbar-hide">
<div className="flex min-w-max">
<button
onClick={() => handleTabChange('create')}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === 'create'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
<Plus className="h-4 w-4" />
Create New
<Plus className="h-4 w-4 flex-shrink-0" />
<span className="hidden sm:inline">Create New</span>
<span className="sm:hidden">New</span>
</button>
<button
onClick={() => handleTabChange('url')}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === 'url'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
<Globe className="h-4 w-4" />
<Globe className="h-4 w-4 flex-shrink-0" />
URL
</button>
<button
onClick={() => handleTabChange('paste')}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === 'paste'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
<FileText className="h-4 w-4" />
<FileText className="h-4 w-4 flex-shrink-0" />
Paste
</button>
<button
onClick={() => handleTabChange('open')}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === 'open'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
<Upload className="h-4 w-4" />
<Upload className="h-4 w-4 flex-shrink-0" />
Open
</button>
</div>
@@ -634,7 +663,7 @@ const ObjectEditor = () => {
{/* Tab Content */}
{(activeTab !== 'create' || !createNewCompleted) && (
<div className="p-4">
<div className="p-3 sm:p-4">
{/* Create New Tab Content */}
{activeTab === 'create' && !createNewCompleted && (
<div className="space-y-4">
@@ -756,11 +785,14 @@ const ObjectEditor = () => {
</span>
)}
</div>
<textarea
<CodeEditor
value={inputText}
onChange={(e) => handleInputChange(e.target.value)}
onChange={(value) => handleInputChange(value)}
language={inputFormat === 'JSON' ? 'json' : 'javascript'}
placeholder="Paste JSON or PHP serialized data here..."
className="tool-input h-32 resize-none"
height="200px"
className="w-full"
theme={isDark ? 'dark' : 'light'}
/>
{error && (
<p className="text-sm text-red-600 dark:text-red-400">
@@ -875,12 +907,14 @@ const ObjectEditor = () => {
) : (
<>
{viewMode === 'visual' && (
<div className="min-h-96 overflow-x-auto p-4">
<div className="min-w-max">
<StructuredEditor
initialData={structuredData}
onDataChange={handleStructuredDataChange}
/>
<div className="w-full overflow-hidden">
<div className="w-full overflow-x-auto p-4">
<div className="min-w-max">
<StructuredEditor
initialData={structuredData}
onDataChange={handleStructuredDataChange}
/>
</div>
</div>
</div>
)}
@@ -947,10 +981,13 @@ const ObjectEditor = () => {
<div className="p-4">
{activeExportTab === 'json' && (
<div className="space-y-3">
<textarea
<CodeEditor
value={jsonFormat === 'pretty' ? (outputs.jsonPretty || '{}') : (outputs.jsonMinified || '{}')}
readOnly
className="w-full h-64 font-mono text-sm border border-gray-300 dark:border-gray-600 rounded-md p-3 bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100"
language="json"
readOnly={true}
height="300px"
className="w-full"
theme={isDark ? 'dark' : 'light'}
/>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
@@ -1007,10 +1044,13 @@ const ObjectEditor = () => {
{activeExportTab === 'php' && (
<div className="space-y-3">
<textarea
<CodeEditor
value={outputs.serialized || 'a:0:{}'}
readOnly
className="w-full h-64 font-mono text-sm border border-gray-300 dark:border-gray-600 rounded-md p-3 bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100"
language="javascript"
readOnly={true}
height="300px"
className="w-full"
theme={isDark ? 'dark' : 'light'}
/>
<div className="flex justify-end gap-2">
<button
@@ -1191,7 +1231,7 @@ const InputChangeConfirmationModal = ({ objectData, currentMethod, newMethod, on
onClick={onConfirm}
className="px-4 py-2 text-sm font-medium text-white bg-amber-600 hover:bg-amber-700 rounded-md transition-colors flex items-center gap-2"
>
<BrushCleaning className="h-4 w-4" />
<AlertTriangle className="h-4 w-4" />
Switch & Clear Data
</button>
</div>

View File

@@ -26,6 +26,12 @@ const ReleaseNotes = () => {
// Transform commit messages to user-friendly descriptions
const transformations = [
{
pattern: /feat.*invoice.*editor.*improvements/i,
type: 'feature',
title: 'Invoice Editor Major Update',
description: 'Complete overhaul of Invoice Editor with currency system, PDF generation fixes, improved UI/UX, removed print functionality (use PDF download instead), streamlined preview toolbar, and comprehensive bug fixes'
},
{
pattern: /feat.*enhanced.*what.*new.*feature.*non_tools.*category.*global.*footer/i,
type: 'feature',
@@ -269,8 +275,8 @@ const ReleaseNotes = () => {
return (
<ToolLayout
title="Release Notes"
description="Stay updated with the latest features, improvements, and fixes"
title=""
description=""
>
<div className="max-w-4xl mx-auto">
{/* Header */}
@@ -346,18 +352,21 @@ const ReleaseNotes = () => {
return (
<div key={release.hash} className={`p-6 ${index !== dayReleases.length - 1 ? 'border-b border-gray-100 dark:border-gray-700' : ''}`}>
<div className="flex items-start space-x-4">
<div className={`flex-shrink-0 p-2 rounded-lg ${typeConfig.bgColor}`}>
<div className="flex flex-col md:flex-row items-start md:space-x-4">
<div className={`flex-shrink-0 p-2 rounded-lg ${typeConfig.bgColor} flex items-center space-x-2 mb-2`}>
<div className={typeConfig.color}>
{typeConfig.icon}
</div>
<span className={`block md:hidden px-2 py-1 text-xs font-medium rounded-full ${typeConfig.bgColor} ${typeConfig.color}`}>
{typeConfig.label}
</span>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2 mb-2">
<h4 className="font-semibold text-gray-900 dark:text-gray-100">
{release.title}
</h4>
<span className={`px-2 py-1 text-xs font-medium rounded-full ${typeConfig.bgColor} ${typeConfig.color}`}>
<span className={`hidden md:block px-2 py-1 text-xs font-medium rounded-full ${typeConfig.bgColor} ${typeConfig.color}`}>
{typeConfig.label}
</span>
</div>

View File

@@ -117,7 +117,6 @@ const SerializeTool = () => {
index++; // Skip opening '"'
const byteLength = parseInt(lenStr);
console.log(`Parsing string with declared length: ${byteLength}, starting at position: ${index}`);
if (isNaN(byteLength) || byteLength < 0) {
throw new Error(`Invalid string length: ${lenStr}`);
@@ -153,14 +152,9 @@ const SerializeTool = () => {
const stringVal = str.substring(startIndex, endQuotePos);
const actualByteLength = new TextEncoder().encode(stringVal).length;
console.log(`String parsing: declared ${byteLength} bytes, actual ${actualByteLength} bytes, content length ${stringVal.length} chars`);
console.log(`Extracted string: "${stringVal.substring(0, 50)}${stringVal.length > 50 ? '...' : ''}"`);
// Move index to after the closing '";'
index = endQuotePos + 2;
console.log(`After string parsing, index is at: ${index}, next chars: "${str.substring(index, index + 5)}"`);
// Warn about byte length mismatch but continue parsing
if (actualByteLength !== byteLength) {
console.warn(`Warning: String byte length mismatch - declared ${byteLength}, actual ${actualByteLength}`);

View File

@@ -1,32 +1,41 @@
import React, { useState } from "react";
import {
Table,
Database,
Download,
Upload,
FileText,
Search,
Plus,
X,
Braces,
Code,
Eye,
Trash2,
ArrowUpDown,
Edit3,
Globe,
Maximize2,
Minimize2,
BrushCleaning,
AlertTriangle,
} from "lucide-react";
import ToolLayout from "../components/ToolLayout";
import React, { useState, useEffect } from 'react';
import { Plus, Upload, FileText, Globe, Download, X, Table, Trash2, Database, Braces, Code, Eye, Minimize2, Maximize2, Search, ArrowUpDown, AlertTriangle, Edit3 } from 'lucide-react';
import ToolLayout from '../components/ToolLayout';
import CodeEditor from '../components/CodeEditor';
import StructuredEditor from "../components/StructuredEditor";
import Papa from "papaparse";
// Hook to detect dark mode
const useDarkMode = () => {
const [isDark, setIsDark] = useState(() => {
return document.documentElement.classList.contains('dark');
});
useEffect(() => {
const observer = new MutationObserver(() => {
setIsDark(document.documentElement.classList.contains('dark'));
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
return () => observer.disconnect();
}, []);
return isDark;
};
const TableEditor = () => {
const isDark = useDarkMode();
const [data, setData] = useState([]);
const [columns, setColumns] = useState([]);
// Sync table data to localStorage for navigation guard
useEffect(() => {
localStorage.setItem('tableEditorData', JSON.stringify(data));
}, [data]);
const [inputText, setInputText] = useState("");
const [url, setUrl] = useState("");
const [isLoading, setIsLoading] = useState(false);
@@ -498,7 +507,7 @@ const TableEditor = () => {
const processedValues = values.map((val) => {
val = val.trim();
val = String(val).trim();
// Remove quotes and handle NULL
if (val === "NULL") return "";
if (val.startsWith("'") && val.endsWith("'")) {
@@ -1608,7 +1617,7 @@ const TableEditor = () => {
};
}
if (hasNumber && values.every((val) => !isNaN(val) && val.trim() !== "")) {
if (hasNumber && values.every((val) => !isNaN(val) && String(val).trim() !== "")) {
if (allIntegers) {
const maxVal = Math.max(...values.map((v) => Math.abs(Number(v))));
if (maxVal < 128)
@@ -1783,13 +1792,13 @@ const TableEditor = () => {
icon={Table}
>
{/* Input Section with Tabs */}
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mb-6">
<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 */}
<div className="flex border-b border-gray-200 dark:border-gray-700 overflow-x-auto scrollbar-hide">
<div className="flex min-w-max">
<button
onClick={() => handleTabChange("create")}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === "create"
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
@@ -1800,7 +1809,7 @@ const TableEditor = () => {
</button>
<button
onClick={() => handleTabChange("url")}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === "url"
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
@@ -1811,7 +1820,7 @@ const TableEditor = () => {
</button>
<button
onClick={() => handleTabChange("paste")}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === "paste"
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
@@ -1822,7 +1831,7 @@ const TableEditor = () => {
</button>
<button
onClick={() => handleTabChange("upload")}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === "upload"
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
@@ -1997,11 +2006,13 @@ const TableEditor = () => {
{activeTab === "paste" && (
<div className="space-y-3">
<textarea
<CodeEditor
value={inputText}
onChange={(e) => setInputText(e.target.value)}
onChange={(value) => setInputText(value)}
language="javascript"
placeholder="Paste CSV, TSV, JSON, or SQL INSERT statements here..."
className="tool-input h-32 resize-none"
height="128px"
theme={isDark ? 'dark' : 'light'}
/>
<div className="flex items-center justify-between">
<label className="flex items-center text-sm text-gray-600 dark:text-gray-400">
@@ -2055,10 +2066,10 @@ const TableEditor = () => {
{data.length > 0 && (
<div
className={`bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden ${
className={`bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 min-w-0 ${
isTableFullscreen
? "fixed inset-0 z-50 rounded-none border-0 shadow-none"
: ""
? "fixed inset-0 z-50 rounded-none border-0 shadow-none overflow-hidden"
: "overflow-x-auto"
}`}
>
{/* Header */}
@@ -2103,7 +2114,7 @@ const TableEditor = () => {
onClick={clearData}
className="flex items-center gap-2 px-3 py-2 text-sm font-medium transition-colors border-l border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700"
>
<BrushCleaning className="h-4 w-4" />
<AlertTriangle className="h-4 w-4" />
<span className="hidden sm:inline">Clear All</span>
</button>
</div>
@@ -2141,8 +2152,8 @@ const TableEditor = () => {
</div>
)}
{/* Table Body - Edge to Edge */}
<div className="flex flex-col h-full">
{/* Table Body - edge to edge */}
<div className="flex flex-col h-full min-w-0">
{/* Controls */}
<div className="px-4 py-3 flex flex-col sm:flex-row items-start sm:items-center gap-3 sm:gap-4 border-b border-gray-200 dark:border-gray-700">
{/* Search Bar */}
@@ -2215,7 +2226,8 @@ const TableEditor = () => {
{/* Table */}
<div
className={`overflow-auto ${isTableFullscreen ? "max-h-[calc(100vh-200px)]" : "max-h-[500px]"}`}
className={`overflow-auto w-full ${isTableFullscreen ? "max-h-[calc(100vh-200px)]" : "max-h-[500px]"}`}
style={{ maxWidth: '100%' }}
>
<table className="w-full">
<thead className="bg-gray-50 dark:bg-gray-700 sticky top-0 z-10">
@@ -2616,7 +2628,7 @@ const TableEditor = () => {
<div className="flex border-b border-gray-200 dark:border-gray-700">
<button
onClick={() => setExportTab("json")}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
exportTab === "json"
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
@@ -2627,7 +2639,7 @@ const TableEditor = () => {
</button>
<button
onClick={() => setExportTab("csv")}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
exportTab === "csv"
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
@@ -2638,7 +2650,7 @@ const TableEditor = () => {
</button>
<button
onClick={() => setExportTab("tsv")}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
exportTab === "tsv"
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
@@ -2649,7 +2661,7 @@ const TableEditor = () => {
</button>
<button
onClick={() => setExportTab("sql")}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
exportTab === "sql"
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
@@ -2664,10 +2676,12 @@ const TableEditor = () => {
<div className="p-4">
{exportTab === "json" && (
<div className="space-y-3">
<textarea
<CodeEditor
value={getExportData("json")}
readOnly
className="w-full h-64 font-mono text-sm border border-gray-300 dark:border-gray-600 rounded-md p-3 bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100"
language="json"
readOnly={true}
height="256px"
theme={isDark ? 'dark' : 'light'}
/>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
@@ -2718,10 +2732,12 @@ const TableEditor = () => {
{exportTab === "csv" && (
<div className="space-y-3">
<textarea
<CodeEditor
value={getExportData("csv")}
readOnly
className="w-full h-64 font-mono text-sm border border-gray-300 dark:border-gray-600 rounded-md p-3 bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100"
language="javascript"
readOnly={true}
height="256px"
theme={isDark ? 'dark' : 'light'}
/>
<div className="flex justify-end gap-2">
<button
@@ -2748,10 +2764,12 @@ const TableEditor = () => {
{exportTab === "tsv" && (
<div className="space-y-3">
<textarea
<CodeEditor
value={getExportData("tsv")}
readOnly
className="w-full h-64 font-mono text-sm border border-gray-300 dark:border-gray-600 rounded-md p-3 bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100"
language="javascript"
readOnly={true}
height="256px"
theme={isDark ? 'dark' : 'light'}
/>
<div className="flex justify-end gap-2">
<button
@@ -2821,10 +2839,12 @@ const TableEditor = () => {
</div>
</div>
<textarea
<CodeEditor
value={getExportData("sql")}
readOnly
className="w-full h-64 font-mono text-sm border border-gray-300 dark:border-gray-600 rounded-md p-3 bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100"
language="javascript"
readOnly={true}
height="256px"
theme={isDark ? 'dark' : 'light'}
/>
{/* Intelligent Schema Analysis */}
@@ -3162,7 +3182,7 @@ const ClearConfirmationModal = ({
onClick={onConfirm}
className="px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-md transition-colors flex items-center gap-2"
>
<BrushCleaning className="h-4 w-4" />
<AlertTriangle className="h-4 w-4" />
Clear All Data
</button>
</div>
@@ -3559,7 +3579,7 @@ const InputChangeConfirmationModal = ({
onClick={onConfirm}
className="px-4 py-2 text-sm font-medium text-white bg-amber-600 hover:bg-amber-700 rounded-md transition-colors flex items-center gap-2"
>
<BrushCleaning className="h-4 w-4" />
<AlertTriangle className="h-4 w-4" />
Switch & Clear Data
</button>
</div>