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:
dwindown
2025-10-15 00:12:54 +07:00
parent 14a07a6cba
commit f60c1d16c8
11 changed files with 3222 additions and 254 deletions

View File

@@ -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">