feat: comprehensive editor UX refinement with collapsible sections and data loss prevention
Major improvements to Object Editor, Table Editor, and Invoice Editor: ## UX Enhancements: - Made export sections collapsible across all editors to reduce page height - Added comprehensive, collapsible usage tips with eye-catching design - Implemented consistent input method patterns (file auto-load, inline URL buttons) - Paste sections now collapse after successful parsing with data summaries ## Data Loss Prevention: - Added confirmation modals when switching input methods with existing data - Amber-themed warning design with specific data summaries - Suggests saving before proceeding with destructive actions - Prevents accidental data loss across all editor tools ## Consistency Improvements: - Standardized file input styling with 'tool-input' class - URL fetch buttons now inline (not below input) across all editors - Parse buttons positioned consistently on bottom-right - Auto-load behavior for file inputs matching across editors ## Bug Fixes: - Fixed Table Editor cell text overflow with proper truncation - Fixed Object Editor file input to auto-load content - Removed unnecessary parse buttons and checkboxes - Fixed Invoice Editor URL form layout ## Documentation: - Created EDITOR_TOOL_GUIDE.md with comprehensive patterns - Created EDITOR_CHECKLIST.md for quick reference - Created PROJECT_ROADMAP.md with future plans - Created TODO.md with detailed task lists - Documented data loss prevention patterns - Added code examples and best practices All editors now follow consistent UX patterns with improved user experience and data protection.
This commit is contained in:
@@ -22,6 +22,10 @@ const InvoiceEditor = () => {
|
||||
const [error, setError] = useState('');
|
||||
const fileInputRef = useRef(null);
|
||||
const logoInputRef = useRef(null);
|
||||
const [pasteCollapsed, setPasteCollapsed] = useState(false);
|
||||
const [pasteDataSummary, setPasteDataSummary] = useState(null);
|
||||
const [exportExpanded, setExportExpanded] = useState(false);
|
||||
const [usageTipsExpanded, setUsageTipsExpanded] = useState(false);
|
||||
|
||||
|
||||
|
||||
@@ -632,7 +636,8 @@ const InvoiceEditor = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const handleFileImport = (event) => {
|
||||
// Handle file import (same as Table Editor)
|
||||
const handleFileSelect = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
@@ -642,7 +647,6 @@ const InvoiceEditor = () => {
|
||||
const importedData = JSON.parse(content);
|
||||
setInvoiceData(importedData);
|
||||
setCreateNewCompleted(true);
|
||||
setActiveTab('create');
|
||||
setError('');
|
||||
} catch (err) {
|
||||
setError('Invalid JSON file format');
|
||||
@@ -834,105 +838,109 @@ const InvoiceEditor = () => {
|
||||
|
||||
{activeTab === 'url' && (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Invoice JSON URL
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
placeholder="https://drive.google.com/file/d/... or any JSON URL"
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleUrlFetch}
|
||||
disabled={!url.trim() || isLoading}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
Loading...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Globe className="h-4 w-4" />
|
||||
Fetch Invoice Data
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<div className="flex items-center gap-2 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-4 w-4 text-blue-600 dark:text-blue-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<input
|
||||
type="url"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
placeholder="https://api.telegram.org/bot<token>/getMe"
|
||||
className="tool-input w-full"
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleUrlFetch()}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-blue-700 dark:text-blue-300">
|
||||
<strong>Google Drive:</strong> Use share links like "drive.google.com/file/d/..." - we'll convert them automatically.
|
||||
</p>
|
||||
<button
|
||||
onClick={handleUrlFetch}
|
||||
disabled={isLoading || !url.trim()}
|
||||
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-medium px-4 py-2 rounded-md transition-colors flex items-center whitespace-nowrap"
|
||||
>
|
||||
{isLoading ? 'Fetching...' : 'Fetch Data'}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Enter any URL that returns JSON data. Examples: Telegram Bot API, JSONPlaceholder, GitHub API, etc.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'paste' && (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Paste Invoice JSON Data
|
||||
</label>
|
||||
<CodeMirrorEditor
|
||||
value={inputText}
|
||||
onChange={setInputText}
|
||||
placeholder="Paste your invoice JSON data here..."
|
||||
language="json"
|
||||
maxLines={12}
|
||||
showToggle={true}
|
||||
className="w-full"
|
||||
/>
|
||||
pasteCollapsed ? (
|
||||
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-green-700 dark:text-green-300">
|
||||
✓ Invoice loaded: {pasteDataSummary.invoiceNumber || 'New Invoice'}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setPasteCollapsed(false)}
|
||||
className="text-sm text-blue-600 hover:underline"
|
||||
>
|
||||
Edit Input ▼
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
try {
|
||||
const parsed = JSON.parse(inputText);
|
||||
setInvoiceData(parsed);
|
||||
setCreateNewCompleted(true);
|
||||
setError('');
|
||||
} catch (err) {
|
||||
setError('Invalid JSON format');
|
||||
}
|
||||
}}
|
||||
disabled={!inputText.trim()}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Load Invoice Data
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<CodeMirrorEditor
|
||||
value={inputText}
|
||||
onChange={setInputText}
|
||||
placeholder="Paste your invoice JSON data here..."
|
||||
language="json"
|
||||
maxLines={12}
|
||||
showToggle={true}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
{error && (
|
||||
<div className="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
||||
<p className="text-sm text-red-600 dark:text-red-400">
|
||||
<strong>Invalid Data:</strong> {error}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between flex-shrink-0">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Supports JSON invoice templates
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
try {
|
||||
const parsed = JSON.parse(inputText);
|
||||
setInvoiceData(parsed);
|
||||
setCreateNewCompleted(true);
|
||||
setPasteDataSummary({
|
||||
invoiceNumber: parsed.invoiceNumber || 'New Invoice',
|
||||
size: inputText.length
|
||||
});
|
||||
setPasteCollapsed(true);
|
||||
setError('');
|
||||
} catch (err) {
|
||||
setError('Invalid JSON format: ' + err.message);
|
||||
setPasteCollapsed(false);
|
||||
}
|
||||
}}
|
||||
disabled={!inputText.trim()}
|
||||
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed text-white font-medium px-4 py-2 rounded-md transition-colors flex-shrink-0"
|
||||
>
|
||||
Load Invoice
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{activeTab === 'open' && (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Choose File
|
||||
</label>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".json"
|
||||
onChange={handleFileImport}
|
||||
className="block w-full text-sm text-gray-500 dark:text-gray-400 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100 dark:file:bg-blue-900/20 dark:file:text-blue-300 dark:hover:file:bg-blue-900/30"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-3 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-4 w-4 text-green-600 dark:text-green-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".json"
|
||||
onChange={handleFileSelect}
|
||||
className="tool-input"
|
||||
/>
|
||||
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-md p-3">
|
||||
<p className="text-xs text-green-700 dark:text-green-300">
|
||||
<strong>Privacy:</strong> Your data stays in your browser. We don't store or upload anything.
|
||||
🔒 <strong>Privacy:</strong> Your data stays in your browser. We don't store or upload anything - just help you open, edit, and export your files locally.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1955,14 +1963,21 @@ const InvoiceEditor = () => {
|
||||
{/* Export Section */}
|
||||
{(activeTab !== 'create' || createNewCompleted) && createNewCompleted && (
|
||||
<div className="w-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div className="px-4 sm:px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-3">
|
||||
<Download className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Export Invoice</h2>
|
||||
<div
|
||||
onClick={() => setExportExpanded(!exportExpanded)}
|
||||
className="px-4 sm:px-6 py-4 border-b border-gray-200 dark:border-gray-700 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Download className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Export Invoice</h2>
|
||||
{exportExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 sm:p-6">
|
||||
{exportExpanded && (
|
||||
<div className="p-4 sm:p-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<button
|
||||
onClick={handleGeneratePreview}
|
||||
@@ -1993,9 +2008,75 @@ const InvoiceEditor = () => {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Usage Tips */}
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md overflow-hidden">
|
||||
<div
|
||||
onClick={() => setUsageTipsExpanded(!usageTipsExpanded)}
|
||||
className="px-4 py-3 cursor-pointer hover:bg-blue-100 dark:hover:bg-blue-900/30 transition-colors flex items-center justify-between"
|
||||
>
|
||||
<h4 className="text-blue-800 dark:text-blue-200 font-medium flex items-center gap-2">
|
||||
💡 Usage Tips
|
||||
</h4>
|
||||
{usageTipsExpanded ? <ChevronUp className="h-4 w-4 text-blue-600 dark:text-blue-400" /> : <ChevronDown className="h-4 w-4 text-blue-600 dark:text-blue-400" />}
|
||||
</div>
|
||||
|
||||
{usageTipsExpanded && (
|
||||
<div className="px-4 pb-4 text-blue-700 dark:text-blue-300 text-sm space-y-3">
|
||||
<div>
|
||||
<p className="font-medium mb-1">📝 Input Methods:</p>
|
||||
<ul className="list-disc list-inside space-y-1 ml-2">
|
||||
<li><strong>Create New:</strong> Start empty or load sample invoice to explore features</li>
|
||||
<li><strong>URL Import:</strong> Fetch invoice data directly from JSON endpoints</li>
|
||||
<li><strong>Paste Data:</strong> Auto-detects JSON invoice templates</li>
|
||||
<li><strong>Open Files:</strong> Import .json invoice files</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="font-medium mb-1">✏️ Invoice Editing:</p>
|
||||
<ul className="list-disc list-inside space-y-1 ml-2">
|
||||
<li><strong>Company & Client:</strong> Fill in business details, addresses, and contact info</li>
|
||||
<li><strong>Items:</strong> Add products/services with descriptions, quantities, and prices</li>
|
||||
<li><strong>Fees & Discounts:</strong> Add additional fees or discounts (fixed or percentage)</li>
|
||||
<li><strong>Payment Terms:</strong> Set full payment, installments, or down payment options</li>
|
||||
<li><strong>Digital Signature:</strong> Draw or upload signature for professional invoices</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="font-medium mb-1">🎨 Customization:</p>
|
||||
<ul className="list-disc list-inside space-y-1 ml-2">
|
||||
<li><strong>Settings:</strong> Change color scheme, currency, and display options</li>
|
||||
<li><strong>Logo Upload:</strong> Add your company logo for branding</li>
|
||||
<li><strong>Payment Methods:</strong> Add bank details, payment links, or QR codes</li>
|
||||
<li><strong>Notes & Messages:</strong> Include payment terms, thank you messages, and authorized signatures</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="font-medium mb-1">📤 Export Options:</p>
|
||||
<ul className="list-disc list-inside space-y-1 ml-2">
|
||||
<li><strong>PDF:</strong> Generate professional PDF invoices for clients</li>
|
||||
<li><strong>JSON:</strong> Save as reusable invoice templates</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="font-medium mb-1">💾 Data Privacy:</p>
|
||||
<ul className="list-disc list-inside space-y-1 ml-2">
|
||||
<li><strong>Local Processing:</strong> All data stays in your browser</li>
|
||||
<li><strong>No Upload:</strong> We don't store or transmit your invoice data</li>
|
||||
<li><strong>Secure:</strong> Your business information remains private</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
|
||||
|
||||
Reference in New Issue
Block a user