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:
@@ -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
2733
src/pages/InvoiceEditor.js
Normal file
File diff suppressed because it is too large
Load Diff
242
src/pages/InvoicePreview.js
Normal file
242
src/pages/InvoicePreview.js
Normal 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;
|
||||
461
src/pages/InvoicePreviewMinimal.js
Normal file
461
src/pages/InvoicePreviewMinimal.js
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user