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

226
EDITOR_CHECKLIST.md Normal file
View File

@@ -0,0 +1,226 @@
# Editor Tool Consistency Checklist
Quick reference for ensuring your new editor tool follows all consistency patterns.
## ✅ Input Section
### Tab Structure
- [ ] 4 tabs: Create New, URL, Paste, Open
- [ ] Icons: Plus, Globe, FileText, Upload
- [ ] Active tab styling: `bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500`
### Create New Tab
- [ ] Grid layout with 2 buttons (Start Empty, Load Sample)
- [ ] Dashed border buttons with icons
- [ ] Blue tip box at bottom
### URL Tab ⚠️ CRITICAL
- [ ] `<div className="flex gap-2">` wrapper
- [ ] Input in `<div className="relative flex-1">`
- [ ] Button INLINE on the right (not below!)
- [ ] Button text: "Fetch Data" or "Fetching..."
- [ ] Enter key triggers fetch
- [ ] Helper text below
### Paste Tab
- [ ] CodeMirrorEditor component
- [ ] Collapse after successful parse
- [ ] Collapsed state shows summary with "Edit Input ▼" button
- [ ] Error display in red box
- [ ] Parse button on bottom-right
- [ ] Button disabled when invalid
### Open Tab ⚠️ CRITICAL
- [ ] `<input className="tool-input" />` (not custom styling!)
- [ ] Auto-load on file selection (no button needed!)
- [ ] Green privacy notice below
---
## ✅ Main Editor Section
- [ ] Conditional render: `{(activeTab !== 'create' || createNewCompleted) && ...}`
- [ ] White card with border
- [ ] Header with icon and title
- [ ] Content area with padding
### Fullscreen (Optional)
- [ ] `z-[99999]` when fullscreen
- [ ] `!m-0` and `marginTop: "0 !important"` to override spacing
---
## ✅ Export Section
- [ ] Collapsible by default (`exportExpanded` state)
- [ ] Clickable header with hover effect
- [ ] ChevronUp/ChevronDown icons
- [ ] Summary info in header
- [ ] Content only renders when expanded
---
## ✅ Usage Tips
- [ ] Collapsible by default (`usageTipsExpanded` state)
- [ ] Header: "💡 Usage Tips"
- [ ] Clickable header with hover effect
- [ ] ChevronUp/ChevronDown icons
- [ ] Organized by categories:
- [ ] 📝 Input Methods
- [ ] ✏️ Editing Features (editor-specific)
- [ ] 📤 Export Options
- [ ] 💾 Data Privacy
---
## ✅ Data Loss Prevention (CRITICAL!)
### Confirmation Modal
- [ ] Shows when user has data and tries to switch tabs
- [ ] Shows when user clicks Start Empty/Load Sample with existing data
- [ ] Amber header with AlertTriangle icon
- [ ] Lists specific data user will lose (not generic)
- [ ] Blue tip box suggesting to save first
- [ ] "Cancel" button (gray)
- [ ] "Switch & Clear Data" button (amber with icon)
- [ ] z-50 to appear above everything
### Logic
- [ ] `hasUserData()` function implemented
- [ ] `hasModifiedData()` function implemented
- [ ] `handleTabChange()` checks for data before switching
- [ ] `showInputChangeModal` state variable
- [ ] `pendingTabChange` state variable
- [ ] Modal component created as separate component
- [ ] `onConfirm` clears data and switches tab
- [ ] `onCancel` closes modal without action
---
## ✅ State Variables
```jsx
// Required states
const [activeTab, setActiveTab] = useState('create');
const [createNewCompleted, setCreateNewCompleted] = useState(false);
const [inputText, setInputText] = useState('');
const [url, setUrl] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const [pasteCollapsed, setPasteCollapsed] = useState(false);
const [pasteDataSummary, setPasteDataSummary] = useState(null);
const [exportExpanded, setExportExpanded] = useState(false);
const [usageTipsExpanded, setUsageTipsExpanded] = useState(false);
const [showInputChangeModal, setShowInputChangeModal] = useState(false);
const [pendingTabChange, setPendingTabChange] = useState(null);
const fileInputRef = useRef(null);
```
---
## ✅ Error Handling
- [ ] Parse errors keep input expanded
- [ ] Parse errors show in red box with AlertTriangle icon
- [ ] Valid data collapses input and shows editor
- [ ] Error state: `setPasteCollapsed(false)`
- [ ] Success state: `setPasteCollapsed(true)`
---
## ✅ Imports
```jsx
import {
Plus, Globe, FileText, Upload, // Input tabs
Download, Edit3, // Editor/Export
AlertTriangle, // Errors
ChevronUp, ChevronDown, // Collapse indicators
} from 'lucide-react';
import ToolLayout from '../components/ToolLayout';
import CodeMirrorEditor from '../components/CodeMirrorEditor';
```
---
## ✅ Visual Consistency
### Colors
- [ ] Primary action: `bg-blue-600 hover:bg-blue-700`
- [ ] Disabled: `disabled:bg-gray-400`
- [ ] Success: `bg-green-50 dark:bg-green-900/20`
- [ ] Error: `bg-red-50 dark:bg-red-900/20`
- [ ] Info: `bg-blue-50 dark:bg-blue-900/20`
### Spacing
- [ ] Section spacing: `mt-6`
- [ ] Internal spacing: `space-y-3`
- [ ] Padding: `p-4` or `px-4 py-3`
### Typography
- [ ] Headers: `text-lg font-semibold`
- [ ] Body: `text-sm`
- [ ] Helper text: `text-xs`
---
## ✅ Behavior
- [ ] Tab change confirmation when data exists
- [ ] File input auto-loads (no button)
- [ ] Paste requires button click
- [ ] URL supports Enter key
- [ ] Collapsible sections start collapsed
- [ ] Error messages are specific and helpful
---
## ⚠️ Common Mistakes to Avoid
1. ❌ URL button below input → ✅ Button inline on right
2. ❌ Custom file input styling → ✅ Use `tool-input` class
3. ❌ File input with parse button → ✅ Auto-load on selection
4. ❌ Always-visible usage tips → ✅ Collapsible by default
5. ❌ Always-visible export → ✅ Collapsible by default
6. ❌ Parse button on left → ✅ Parse button on right
7. ❌ No error handling → ✅ Show errors, keep input expanded
---
## 🧪 Testing Checklist
- [ ] Create New: Both buttons work
- [ ] URL: Inline button, Enter key works
- [ ] Paste: Valid data collapses, invalid shows error
- [ ] Open: Auto-loads file
- [ ] Export: Collapses/expands correctly
- [ ] Usage Tips: Collapses/expands correctly
- [ ] Tab switching: Confirmation modal works
- [ ] Dark mode: All colors work
- [ ] Mobile: Responsive layout
- [ ] Errors: Specific messages, input stays expanded
---
## 📚 Reference Examples
- **Object Editor**: `/src/pages/ObjectEditor.js`
- **Table Editor**: `/src/pages/TableEditor.js`
- **Invoice Editor**: `/src/pages/InvoiceEditor.js`
---
## 🚀 Quick Start
1. Copy structure from existing editor
2. Replace editor-specific logic
3. Update Usage Tips content
4. Test all input methods
5. Test collapse/expand behavior
6. Test error handling
7. Test dark mode
8. Test mobile view
**Follow this checklist and your editor will be perfectly consistent!**

748
EDITOR_TOOL_GUIDE.md Normal file
View File

@@ -0,0 +1,748 @@
# Editor Tool Building Guide
This guide ensures consistency across all editor tools in the Developer Tools application. Follow these patterns when creating new editor tools.
## Table of Contents
1. [Input Section Patterns](#input-section-patterns)
2. [Main Editor Section](#main-editor-section)
3. [Export/Output Section](#export-output-section)
4. [Usage Tips Section](#usage-tips-section)
5. [State Management](#state-management)
6. [Error Handling](#error-handling)
7. [Complete Example](#complete-example)
---
## Input Section Patterns
### Required Tabs
All editor tools must have these 4 input tabs:
1. **Create New** - Start empty or load sample data
2. **URL** - Fetch data from API endpoints
3. **Paste** - Paste data directly
4. **Open** - Upload files
### Tab Structure
```jsx
<div className="flex border-b border-gray-200 dark:border-gray-700">
<button onClick={() => handleTabChange('create')} className={...}>
<Plus className="h-4 w-4" />
Create New
</button>
<button onClick={() => handleTabChange('url')} className={...}>
<Globe className="h-4 w-4" />
URL
</button>
<button onClick={() => handleTabChange('paste')} className={...}>
<FileText className="h-4 w-4" />
Paste
</button>
<button onClick={() => handleTabChange('open')} className={...}>
<Upload className="h-4 w-4" />
Open
</button>
</div>
```
### Create New Tab
```jsx
{activeTab === 'create' && (
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Start Empty Button */}
<button onClick={handleStartEmpty} className="flex flex-col items-center p-6 border-2 border-dashed...">
<Plus className="h-8 w-8..." />
<span className="font-medium...">Start Empty</span>
<span className="text-xs...">Create a blank structure</span>
</button>
{/* Load Sample Button */}
<button onClick={handleLoadSample} className="flex flex-col items-center p-6 border-2 border-dashed...">
<FileText className="h-8 w-8..." />
<span className="font-medium...">Load Sample</span>
<span className="text-xs...">Start with example data</span>
</button>
</div>
{/* Tip */}
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-3">
<p className="text-xs text-blue-700 dark:text-blue-300">
💡 <strong>Tip:</strong> You can always import data later using the URL, Paste, or Open tabs.
</p>
</div>
</div>
)}
```
### URL Tab (CONSISTENT LAYOUT)
**IMPORTANT:** Button must be INLINE on the right, not below!
```jsx
{activeTab === 'url' && (
<div className="space-y-3">
<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' && handleFetchData()}
/>
</div>
<button
onClick={handleFetchData}
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>
)}
```
### Paste Tab (WITH COLLAPSE)
```jsx
{activeTab === 'paste' && (
pasteCollapsed ? (
// Collapsed State
<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">
Data loaded: {pasteDataSummary.format} ({pasteDataSummary.size.toLocaleString()} chars)
</span>
<button
onClick={() => setPasteCollapsed(false)}
className="text-sm text-blue-600 hover:underline"
>
Edit Input
</button>
</div>
</div>
) : (
// Expanded State
<div className="space-y-3">
<div>
<CodeMirrorEditor
value={inputText}
onChange={setInputText}
language="json"
placeholder="Paste your data here..."
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">
Auto-detects format
</div>
<button
onClick={handleParseData}
disabled={!inputValid || !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"
>
Parse Data
</button>
</div>
</div>
)
)}
```
### Open Tab (CONSISTENT WITH TABLE EDITOR)
**IMPORTANT:** Use `tool-input` class and auto-load on file selection!
```jsx
{activeTab === 'open' && (
<div className="space-y-3">
<input
ref={fileInputRef}
type="file"
accept=".json,.txt"
onChange={handleFileImport}
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 - just help you open, edit, and export your files locally.
</p>
</div>
</div>
)}
```
### Parse Button Behavior
- **Valid data**: Collapse input, show summary, display editor
- **Invalid data**: Show error, keep input expanded, hide editor
- **Button disabled**: When no data or invalid format
---
## Main Editor Section
### Structure
```jsx
{(activeTab !== 'create' || createNewCompleted) && (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden mb-6">
{/* Editor Header */}
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Edit3 className="h-5 w-5 text-blue-600 dark:text-blue-400" />
Your Editor Name
</h3>
{/* Optional: Settings or Actions */}
</div>
</div>
{/* Editor Content */}
<div className="p-4">
{/* Your editor implementation */}
</div>
</div>
)}
```
### Fullscreen Support (Optional)
If your editor supports fullscreen:
```jsx
const [isFullscreen, setIsFullscreen] = useState(false);
<div className={`... ${
isFullscreen
? "fixed inset-0 z-[99999] rounded-none border-0 shadow-none overflow-hidden !m-0"
: "..."
}`}
style={isFullscreen ? { marginTop: "0 !important" } : {}}
>
```
---
## Export/Output Section
### Collapsible Export (REQUIRED)
```jsx
{data && (
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mt-6">
{/* Collapsible Header */}
<div
onClick={() => setExportExpanded(!exportExpanded)}
className="px-4 py-3 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">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Download className="h-5 w-5 text-green-600 dark:text-green-400" />
Export Results
{exportExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</h3>
<div className="text-sm text-gray-600 dark:text-gray-400">
{/* Summary info */}
</div>
</div>
</div>
{/* Export Content - Collapsible */}
{exportExpanded && (
<div>
{/* Export tabs and content */}
</div>
)}
</div>
)}
```
### Export Tabs Pattern
```jsx
<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 ${
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'
}`}
>
<Braces className="h-4 w-4" />
JSON
</button>
{/* More export format tabs */}
</div>
```
---
## Usage Tips Section
### Collapsible Usage Tips (REQUIRED)
```jsx
{/* Usage Tips */}
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md overflow-hidden mt-6">
<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> Description</li>
<li><strong>URL Import:</strong> Description</li>
<li><strong>Paste Data:</strong> Description</li>
<li><strong>Open Files:</strong> Description</li>
</ul>
</div>
<div>
<p className="font-medium mb-1">✏️ Editing Features:</p>
<ul className="list-disc list-inside space-y-1 ml-2">
{/* Your editor-specific features */}
</ul>
</div>
<div>
<p className="font-medium mb-1">📤 Export Options:</p>
<ul className="list-disc list-inside space-y-1 ml-2">
{/* Your export formats */}
</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 data</li>
<li><strong>Secure:</strong> Your information remains private</li>
</ul>
</div>
</div>
)}
</div>
```
---
## Data Loss Prevention (CRITICAL!)
### Confirmation Modal for Tab Changes
**IMPORTANT:** Always confirm before clearing user data when switching input methods!
### When to Show Confirmation
- User has entered/loaded data
- User tries to switch to a different input tab
- User tries to use Create New buttons (Start Empty/Load Sample)
### Implementation Pattern
#### 1. Check if User Has Data
```jsx
const hasUserData = () => {
// Check if there's meaningful data
return Object.keys(editorData).length > 0;
};
const hasModifiedData = () => {
// Check if data has been modified from initial state
// Return false for empty or sample data
// Return true for user-entered data
};
```
#### 2. Handle Tab Change with Confirmation
```jsx
const handleTabChange = (newTab) => {
if (hasModifiedData() && activeTab !== newTab) {
setPendingTabChange(newTab);
setShowInputChangeModal(true);
} else {
setActiveTab(newTab);
}
};
```
#### 3. Confirmation Modal Component
```jsx
const InputChangeConfirmationModal = ({
editorData,
currentMethod,
newMethod,
onConfirm,
onCancel
}) => {
const getMethodName = (method) => {
switch (method) {
case 'create': return 'Create New';
case 'create_empty': return 'Start Empty';
case 'create_sample': return 'Load Sample';
case 'url': return 'URL Import';
case 'paste': return 'Paste Data';
case 'open': return 'File Upload';
default: return method;
}
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full overflow-hidden">
{/* Header - Amber Warning */}
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700 bg-amber-50 dark:bg-amber-900/20">
<div className="flex items-center gap-3">
<div className="flex-shrink-0">
<AlertTriangle className="h-6 w-6 text-amber-600 dark:text-amber-400" />
</div>
<div>
<h3 className="text-lg font-semibold text-amber-800 dark:text-amber-200">
Confirm Action
</h3>
<p className="text-sm text-amber-700 dark:text-amber-300">
{newMethod === 'create_empty' || newMethod === 'create_sample'
? `Using ${getMethodName(newMethod)} will clear your current data.`
: `Switching from ${getMethodName(currentMethod)} to ${getMethodName(newMethod)} will clear your current data.`
}
</p>
</div>
</div>
</div>
{/* Body - Show Current Data Summary */}
<div className="px-6 py-4">
<div className="space-y-3">
<p className="text-sm text-gray-600 dark:text-gray-400">
You currently have:
</p>
<ul className="text-sm text-gray-700 dark:text-gray-300 space-y-1 ml-4">
{/* List current data items */}
<li> [Data summary item 1]</li>
<li> [Data summary item 2]</li>
<li> [Data summary item 3]</li>
</ul>
{/* Blue Tip Box */}
<div className="mt-4 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
<p className="text-xs text-blue-700 dark:text-blue-300">
<strong>Tip:</strong> Consider downloading your current data as JSON before proceeding to save your work.
</p>
</div>
</div>
</div>
{/* Footer - Action Buttons */}
<div className="px-6 py-4 bg-gray-50 dark:bg-gray-700/50 flex justify-end gap-3">
<button
onClick={onCancel}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-md hover:bg-gray-50 dark:hover:bg-gray-500 transition-colors"
>
Cancel
</button>
<button
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"
>
<AlertTriangle className="h-4 w-4" />
Switch & Clear Data
</button>
</div>
</div>
</div>
);
};
```
#### 4. Usage in Component
```jsx
{/* Confirmation Modal */}
{showInputChangeModal && (
<InputChangeConfirmationModal
editorData={editorData}
currentMethod={activeTab}
newMethod={pendingTabChange}
onConfirm={() => {
clearAllData();
setActiveTab(pendingTabChange);
setShowInputChangeModal(false);
setPendingTabChange(null);
}}
onCancel={() => {
setShowInputChangeModal(false);
setPendingTabChange(null);
}}
/>
)}
```
### Visual Design
- **Header**: Amber background with AlertTriangle icon
- **Body**: White/dark background with data summary
- **Tip Box**: Blue background with save suggestion
- **Footer**: Gray background with Cancel and Warning buttons
- **Warning Button**: Amber color with AlertTriangle icon
### Key Points
1. **Always show** when user has meaningful data
2. **List specific data** user will lose (not generic message)
3. **Suggest saving** before proceeding (blue tip box)
4. **Clear button text** - "Switch & Clear Data" (not ambiguous)
5. **z-50** to appear above everything
6. **Amber theme** for warning (not red, not blue)
---
## State Management
### Required State Variables
```jsx
// Tab management
const [activeTab, setActiveTab] = useState('create');
const [createNewCompleted, setCreateNewCompleted] = useState(false);
// Input data
const [inputText, setInputText] = useState('');
const [url, setUrl] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
// Collapse states
const [pasteCollapsed, setPasteCollapsed] = useState(false);
const [pasteDataSummary, setPasteDataSummary] = useState(null);
const [exportExpanded, setExportExpanded] = useState(false);
const [usageTipsExpanded, setUsageTipsExpanded] = useState(false);
// Confirmation modals
const [showInputChangeModal, setShowInputChangeModal] = useState(false);
const [pendingTabChange, setPendingTabChange] = useState(null);
// Refs
const fileInputRef = useRef(null);
// Your editor-specific state
const [editorData, setEditorData] = useState(null);
```
### Tab Change with Confirmation
```jsx
const handleTabChange = (newTab) => {
if (hasModifiedData() && activeTab !== newTab) {
setPendingTabChange(newTab);
setShowInputChangeModal(true);
} else {
setActiveTab(newTab);
}
};
const confirmInputChange = () => {
clearAllData();
setActiveTab(pendingTabChange);
setShowInputChangeModal(false);
setPendingTabChange(null);
};
```
---
## Error Handling
### Parse Data with Error Handling
```jsx
const handleParseData = () => {
try {
const parsed = parseYourData(inputText);
if (parsed.valid) {
setEditorData(parsed.data);
setError('');
setCreateNewCompleted(true);
setPasteDataSummary({
format: parsed.format,
size: inputText.length,
items: parsed.data.length
});
setPasteCollapsed(true);
} else {
// Show error, keep input expanded
setError(parsed.error || 'Invalid data format');
setPasteCollapsed(false);
}
} catch (err) {
setError(err.message || 'Failed to parse data');
setPasteCollapsed(false);
}
};
```
### Error Display
```jsx
{error && (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
<div className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-red-600 dark:text-red-400" />
<p className="text-red-700 dark:text-red-300">{error}</p>
</div>
</div>
)}
```
---
## Complete Example
See existing editors for complete implementations:
- **Object Editor** (`/src/pages/ObjectEditor.js`) - Best for JSON/structured data
- **Table Editor** (`/src/pages/TableEditor.js`) - Best for tabular data
- **Invoice Editor** (`/src/pages/InvoiceEditor.js`) - Best for form-based editors
---
## Checklist for New Editor Tools
### Input Section
- [ ] All 4 tabs implemented (Create New, URL, Paste, Open)
- [ ] URL tab has inline button (not below)
- [ ] Open tab uses `tool-input` class
- [ ] Open tab auto-loads on file selection
- [ ] Paste tab has parse button on bottom-right
- [ ] Paste tab collapses after successful parse
- [ ] Error handling shows errors and keeps input expanded
### Main Editor
- [ ] Only shows after data is loaded
- [ ] Has proper header with icon
- [ ] Implements your editor functionality
- [ ] Optional: Fullscreen support with proper z-index
### Export Section
- [ ] Collapsible by default
- [ ] Header shows summary info
- [ ] Chevron icon indicates state
- [ ] Hover effect on header
- [ ] Export tabs if multiple formats
### Usage Tips
- [ ] Collapsible by default
- [ ] 💡 emoji in header
- [ ] Comprehensive tips organized by category
- [ ] Includes Input Methods, Editing Features, Export Options, Data Privacy
### State & Behavior
- [ ] Tab change confirmation when data exists
- [ ] Proper error handling
- [ ] Loading states for async operations
- [ ] Dark mode support
### Styling
- [ ] Uses Tailwind CSS classes
- [ ] Consistent with other editors
- [ ] Responsive design
- [ ] Proper spacing and layout
---
## Common Patterns
### Button Styling
```jsx
// Primary action button
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"
// Secondary button
className="bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 px-4 py-2 rounded-md transition-colors"
```
### Input Styling
```jsx
// Standard input
className="tool-input w-full"
// Or explicit:
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"
```
### Card/Section Styling
```jsx
className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden"
```
---
## Import Requirements
### Required Icons (from lucide-react)
```jsx
import {
Plus, // Create New
Globe, // URL
FileText, // Paste
Upload, // Open
Download, // Export
Edit3, // Editor
AlertTriangle, // Errors
ChevronUp, // Collapse indicators
ChevronDown, // Collapse indicators
// Add your specific icons
} from 'lucide-react';
```
### Required Components
```jsx
import ToolLayout from '../components/ToolLayout';
import CodeMirrorEditor from '../components/CodeMirrorEditor';
import CodeEditor from '../components/CodeEditor'; // For read-only code display
```
---
## Best Practices
1. **Consistency First**: Always follow existing patterns before innovating
2. **User Data Safety**: Always confirm before clearing user data
3. **Error Messages**: Be specific and helpful
4. **Loading States**: Show loading indicators for async operations
5. **Accessibility**: Use semantic HTML and proper ARIA labels
6. **Dark Mode**: Test in both light and dark modes
7. **Mobile**: Ensure responsive design works on small screens
8. **Performance**: Lazy load heavy components when possible
---
## Testing Your Editor
1. **Input Methods**: Test all 4 tabs work correctly
2. **Parse Valid Data**: Ensure data loads and displays
3. **Parse Invalid Data**: Ensure errors show and input stays expanded
4. **Tab Switching**: Confirm data loss prevention works
5. **Export**: Test all export formats
6. **Collapse/Expand**: Test all collapsible sections
7. **Dark Mode**: Toggle and verify styling
8. **Mobile**: Test on small screen sizes
9. **Edge Cases**: Empty data, very large data, special characters
---
## Questions or Issues?
Refer to existing editors:
- Object Editor: JSON/PHP serialized data handling
- Table Editor: Tabular data with inline editing
- Invoice Editor: Form-based data entry
Follow these patterns and your new editor will be consistent with the rest of the application!

481
PROJECT_ROADMAP.md Normal file
View File

@@ -0,0 +1,481 @@
# Developer Tools - Project Roadmap
**Last Updated:** October 14, 2025
---
## 🎯 Vision
Build a comprehensive suite of developer tools with a focus on:
1. **Privacy-First** - All processing in browser
2. **User-Friendly** - Intuitive UI/UX
3. **Monetization** - Ad-supported with optional ad-free experience
4. **Quality** - Consistent patterns across all tools
---
## 📊 Current Status
### ✅ Completed Tools
- **Object Editor** - JSON/PHP serialized data editor
- **Table Editor** - Tabular data editor with multi-format support
- **Invoice Editor** - Professional invoice generator
### ✅ Completed Infrastructure
- Consistent input patterns (Create/URL/Paste/Open)
- Collapsible sections (Export, Usage Tips)
- Data loss prevention (confirmation modals)
- Dark mode support
- Responsive design
- Documentation (EDITOR_TOOL_GUIDE.md, EDITOR_CHECKLIST.md)
---
## 🚀 Roadmap
### Phase 1: Foundation & Monetization (Current - Week 1-2)
#### Priority 1: Advanced URL Fetch ⭐
**Status:** Planned
**Timeline:** 1-2 days
**Impact:** HIGH - Benefits all existing and future tools
**Features:**
- Simple mode (current: just URL + GET)
- Advanced mode (toggle):
- HTTP method selector (GET, POST, PUT, DELETE, PATCH)
- Custom headers (key-value pairs)
- Request body editor (JSON/form-data)
- Query parameters builder
- Auth presets (Bearer, Basic, API Key)
- Save/load request presets
- Response headers display
- Status code display
**Benefits:**
- Users can fetch from authenticated APIs
- Support for private company APIs
- GitHub API with auth tokens
- GraphQL endpoints
- Any REST API with custom requirements
**Implementation Notes:**
- Add to all existing editors (Object, Table, Invoice)
- Reusable component: `AdvancedURLFetch.jsx`
- Store presets in localStorage
- Validate JSON in request body
---
#### Priority 2: Ad Space Preparation 💰
**Status:** Planned
**Timeline:** 1 day
**Impact:** HIGH - Start monetization immediately
**Desktop Layout:**
```
┌────────────────────────────┬─────────┐
│ │ [Ad] │
│ Main Content │ │
│ (Tool Editor) │ 300px │
│ │ │
│ │ [Ad] │
│ │ │
│ │ [Ad] │
└────────────────────────────┴─────────┘
```
**Mobile Layout:**
```
┌─────────────────────────┐
│ │
│ Main Content │
│ (Scrollable) │
│ │
├─────────────────────────┤
│ [Ad Banner 320x50] │ ← Sticky
└─────────────────────────┘
```
**Specifications:**
- **Desktop:**
- 300px fixed width right column
- Sticky scroll (ads stay visible)
- 3 ad blocks maximum
- Ad sizes: 300x250, 300x600
- Hide below 1200px viewport width
- Main content: `calc(100% - 320px)`
- **Mobile:**
- 320x50 or 320x100 banner
- Sticky bottom position
- Close button for better UX
- Add padding-bottom to content
**Implementation:**
- Create `AdColumn.jsx` component
- Create `AdBlock.jsx` component
- Create `MobileAdBanner.jsx` component
- Update `ToolLayout.jsx` to include ad spaces
- Add responsive breakpoints
- Test with placeholder ads first
---
#### Priority 3: AdSense Integration 💵
**Status:** Planned
**Timeline:** 1 day
**Impact:** HIGH - Start earning revenue
**Steps:**
1. Apply for Google AdSense account
2. Add AdSense script to `index.html`
3. Create ad units in AdSense dashboard
4. Implement ad components with AdSense code
5. Test ad display and responsiveness
6. Monitor ad performance
**Ad Units Needed:**
- Desktop Sidebar 1 (300x250)
- Desktop Sidebar 2 (300x250)
- Desktop Sidebar 3 (300x600)
- Mobile Bottom Banner (320x50)
**Compliance:**
- Add Privacy Policy page
- Add Terms of Service page
- Cookie consent banner (if required)
- GDPR compliance (if applicable)
---
### Phase 2: Content Expansion (Week 3-6)
#### Markdown Editor 📝
**Status:** Planned
**Timeline:** 1-2 weeks
**Impact:** HIGH - Major new feature, attracts new users
**Core Features (MVP):**
- **Input Methods:**
- Create New (empty/sample)
- URL Import (fetch markdown from GitHub, Gist, etc.)
- Paste (markdown, HTML auto-convert, plain text)
- Open Files (.md, .txt, .html, .docx)
- **Editor:**
- CodeMirror with markdown syntax highlighting
- Split view (editor + live preview)
- View modes: Split, Editor Only, Preview Only, Fullscreen
- Markdown toolbar (Bold, Italic, Headers, Links, Images, Code, Lists, Tables)
- Line numbers
- Word count & statistics
- **Preview:**
- Live rendering (marked + DOMPurify)
- Syntax highlighting for code blocks (highlight.js)
- GitHub Flavored Markdown support
- Table of Contents auto-generation
- Mermaid diagram rendering (in preview)
- **Export:**
- Markdown (.md) - Standard, GFM, CommonMark
- HTML (.html) - Standalone with CSS
- Plain Text (.txt)
- PDF (.pdf) - via html2pdf
- DOCX (.docx) - via docx library
**Advanced Features (Post-MVP):**
- Tables support (GitHub-style)
- Task lists (checkboxes)
- Footnotes
- Emoji support (:smile:)
- Math equations (KaTeX)
- Templates (README, Documentation, Blog Post, etc.)
- Markdown linter
- Link checker
- Format beautifier
**Libraries:**
- `marked` or `markdown-it` - Markdown parser
- `turndown` - HTML to Markdown
- `mammoth.js` - DOCX to HTML/Markdown
- `html2pdf.js` - HTML to PDF
- `docx` - Generate DOCX files
- `highlight.js` - Code syntax highlighting
- `mermaid` - Diagram rendering
- `katex` - Math rendering (optional)
**Why Markdown Editor?**
- High demand from developers
- Complements existing tools
- Unique value: conversion hub
- Great for SEO
- Relatively straightforward to implement
---
#### Diagram Tool 🎨
**Status:** Planned
**Timeline:** 1-2 weeks
**Impact:** MEDIUM-HIGH - Separate tool, different audience
**Decision: SEPARATE from Markdown Editor**
**Why Separate:**
- Different use cases (documents vs diagrams)
- Different UX needs (text-heavy vs visual-heavy)
- Better specialization
- More SEO entry points
- Can be more powerful than mermaid alone
**Core Features (MVP):**
- **Input Methods:**
- Create New (empty/sample diagrams)
- URL Import (fetch mermaid/PlantUML code)
- Paste (mermaid code, PlantUML, etc.)
- Open Files (.mmd, .mermaid, .txt)
- **Editor:**
- Text-based editor (Mermaid syntax)
- Live preview
- Syntax highlighting
- Error detection
- **Diagram Types (Mermaid-based):**
- Flowchart
- Sequence Diagram
- Class Diagram
- State Diagram
- Entity Relationship Diagram
- Gantt Chart
- Pie Chart
- Git Graph
- **Export:**
- PNG (.png)
- SVG (.svg)
- PDF (.pdf)
- Mermaid Code (.mmd)
**Advanced Features (Post-MVP):**
- Visual editor (drag-and-drop nodes)
- Excalidraw-style drawing
- Draw.io-style interface
- Import from PlantUML
- Custom themes
- Collaboration features
**Libraries:**
- `mermaid` - Diagram rendering
- `html2canvas` - PNG export
- `svg-to-pdf` - PDF export
**Why Separate Diagram Tool?**
- Mermaid is powerful but limited
- Users want visual editing
- Can compete with Lucidchart, draw.io
- Different target audience
- Can be monetized separately
---
### Phase 3: Monetization Backend (Week 7+)
#### Payment Integration 💳
**Status:** Future
**Timeline:** 1 week
**Impact:** MEDIUM - Enables ad-free revenue
**Prerequisites:**
- Significant traffic (>1000 daily users)
- Ad revenue validated
- User requests for ad-free option
**Features:**
- One-time payment (no subscription)
- Multiple duration options
- Stripe/PayPal integration
- Simple checkout flow
- Email receipt
**Pricing Strategy (Recommended):**
- 1 month: $2.99
- 3 months: $6.99 (save 22%)
- 6 months: $11.99 (save 33%)
- 12 months: $19.99 (save 44%)
**Implementation:**
- Backend: Node.js + Express (or serverless functions)
- Database: PostgreSQL or MongoDB
- Payment: Stripe API
- Email: SendGrid or similar
---
#### User Accounts (Simple) 👤
**Status:** Future
**Timeline:** 1 week
**Impact:** MEDIUM - Required for ad-free tracking
**Features:**
- Email + password authentication
- OAuth (Google, GitHub) - optional
- Ad-free status tracking
- Purchase history
- Simple profile page
**No Complex Features:**
- No social features
- No data sync (keep tools client-side)
- No collaboration
- Just auth + payment tracking
**Implementation:**
- Auth: Firebase Auth or Auth0
- Database: Store user ID + ad-free expiry date
- Frontend: Check ad-free status, hide ads
---
#### Ad-Free Experience 🚫
**Status:** Future
**Timeline:** 3 days
**Impact:** MEDIUM - Premium user experience
**Features:**
- Hide all ads for paid users
- "Remove Ads" badge in UI
- Expiry reminder (7 days before)
- Easy renewal process
**Implementation:**
```jsx
const { user, isAdFree } = useAuth();
return (
<div className="flex gap-5">
<main className="flex-1">
{/* Tool content */}
</main>
{!isAdFree && <AdColumn />}
</div>
);
```
---
## 🎯 Success Metrics
### Phase 1 (Foundation & Monetization)
- [ ] Advanced URL Fetch used by >50% of users
- [ ] Ad impressions: >10,000/day
- [ ] Ad revenue: >$50/month
- [ ] No significant performance degradation
### Phase 2 (Content Expansion)
- [ ] Markdown Editor: >1,000 users/week
- [ ] Diagram Tool: >500 users/week
- [ ] Total tools: 5 (Object, Table, Invoice, Markdown, Diagram)
- [ ] Ad revenue: >$200/month
### Phase 3 (Monetization Backend)
- [ ] Ad-free purchases: >10/month
- [ ] Ad-free revenue: >$50/month
- [ ] Total revenue: >$250/month
- [ ] User satisfaction: >4.5/5 stars
---
## 📈 Growth Strategy
### SEO Optimization
- [ ] Optimize page titles and meta descriptions
- [ ] Add structured data (Schema.org)
- [ ] Create sitemap.xml
- [ ] Submit to Google Search Console
- [ ] Create blog posts about tools
- [ ] Create tutorial videos
- [ ] Build backlinks
### Content Marketing
- [ ] Write "How to" guides
- [ ] Create comparison articles (vs competitors)
- [ ] Share on Reddit (r/webdev, r/programming)
- [ ] Share on Hacker News
- [ ] Share on Twitter/X
- [ ] Share on LinkedIn
### Community Building
- [ ] Create Discord server (optional)
- [ ] Add feedback form
- [ ] Respond to user feedback
- [ ] Add feature request voting
- [ ] Create changelog page
---
## 🛠️ Technical Debt & Improvements
### Performance
- [ ] Implement code splitting
- [ ] Lazy load heavy components
- [ ] Optimize bundle size
- [ ] Add service worker (PWA)
- [ ] Implement caching strategies
### Testing
- [ ] Add unit tests (Jest)
- [ ] Add integration tests (React Testing Library)
- [ ] Add E2E tests (Playwright)
- [ ] Set up CI/CD pipeline
### Accessibility
- [ ] Add ARIA labels
- [ ] Test with screen readers
- [ ] Ensure keyboard navigation
- [ ] Add focus indicators
- [ ] Test color contrast
### Documentation
- [ ] Create API documentation (if applicable)
- [ ] Create user guides
- [ ] Create video tutorials
- [ ] Create FAQ page
---
## 📝 Notes
### Decision Log
- **2025-10-14**: Decided to separate Diagram Tool from Markdown Editor for better specialization
- **2025-10-14**: Prioritized Advanced URL Fetch over Markdown Editor for immediate impact
- **2025-10-14**: Chose ad-first monetization strategy with optional ad-free experience
- **2025-10-14**: Decided on one-time payment (no subscription) for ad-free
### Risks & Mitigation
- **Risk**: Ad revenue too low
- **Mitigation**: Focus on traffic growth, optimize ad placement
- **Risk**: Users use ad blockers
- **Mitigation**: Polite message asking to whitelist, offer ad-free option
- **Risk**: Competition from existing tools
- **Mitigation**: Focus on unique features (conversion hub, privacy-first)
- **Risk**: Backend costs too high
- **Mitigation**: Start with serverless, scale as needed
### Future Ideas (Backlog)
- Code Formatter (Prettier-based)
- JSON Diff Tool
- Base64 Encoder/Decoder
- URL Encoder/Decoder
- Hash Generator (MD5, SHA256, etc.)
- QR Code Generator
- Color Picker & Converter
- Regex Tester
- Cron Expression Generator
- JWT Decoder
- API Testing Tool (Postman-like)
---
**End of Roadmap**

889
TODO.md Normal file
View File

@@ -0,0 +1,889 @@
# Developer Tools - To-Do List
**Last Updated:** October 14, 2025
---
## 📋 Phase 1: Foundation & Monetization
### ⭐ Priority 1: Advanced URL Fetch (1-2 days)
#### Design & Planning
- [ ] Design UI mockup for simple mode
- [ ] Design UI mockup for advanced mode
- [ ] Design toggle between modes
- [ ] Plan state management structure
- [ ] Plan localStorage schema for presets
#### Component Development
- [ ] Create `AdvancedURLFetch.jsx` component
- [ ] Create `HTTPMethodSelector.jsx` component
- [ ] Create `HeadersEditor.jsx` component (key-value pairs)
- [ ] Create `RequestBodyEditor.jsx` component (CodeMirror)
- [ ] Create `QueryParamsBuilder.jsx` component
- [ ] Create `AuthPresets.jsx` component
- [ ] Create `RequestPresetManager.jsx` component
#### Simple Mode
- [ ] Implement URL input field
- [ ] Implement GET request button
- [ ] Implement loading state
- [ ] Implement error handling
- [ ] Add helper text
#### Advanced Mode
- [ ] Implement mode toggle button
- [ ] Implement HTTP method selector (GET, POST, PUT, DELETE, PATCH)
- [ ] Implement custom headers input
- [ ] Add header button
- [ ] Remove header button
- [ ] Key-value pair inputs
- [ ] Validation
- [ ] Implement request body editor
- [ ] CodeMirror integration
- [ ] JSON syntax highlighting
- [ ] JSON validation
- [ ] Format button
- [ ] Implement query parameters builder
- [ ] Add param button
- [ ] Remove param button
- [ ] Key-value pair inputs
- [ ] Auto-append to URL
- [ ] Implement auth presets
- [ ] Bearer Token preset
- [ ] Basic Auth preset
- [ ] API Key preset
- [ ] Custom preset
#### Response Display
- [ ] Create response tabs (Body, Headers, Status)
- [ ] Implement response body display (formatted JSON)
- [ ] Implement response headers display
- [ ] Implement status code display with color coding
- [ ] Add response time display
- [ ] Add response size display
#### Presets Management
- [ ] Implement save preset functionality
- [ ] Implement load preset functionality
- [ ] Implement delete preset functionality
- [ ] Implement preset list UI
- [ ] Store presets in localStorage
- [ ] Add preset name input
- [ ] Add preset description (optional)
#### Integration
- [ ] Integrate into Object Editor
- [ ] Replace simple URL input
- [ ] Test with various APIs
- [ ] Update Usage Tips
- [ ] Integrate into Table Editor
- [ ] Replace simple URL input
- [ ] Test with various APIs
- [ ] Update Usage Tips
- [ ] Integrate into Invoice Editor
- [ ] Replace simple URL input
- [ ] Test with various APIs
- [ ] Update Usage Tips
#### Testing
- [ ] Test with GitHub API (requires auth)
- [ ] Test with JSONPlaceholder (public API)
- [ ] Test with Telegram Bot API
- [ ] Test with GraphQL endpoint
- [ ] Test with POST request + body
- [ ] Test with custom headers
- [ ] Test with query parameters
- [ ] Test error scenarios (404, 500, timeout)
- [ ] Test loading states
- [ ] Test preset save/load
- [ ] Test on mobile devices
- [ ] Test dark mode
#### Documentation
- [ ] Update EDITOR_TOOL_GUIDE.md with advanced URL pattern
- [ ] Add code examples
- [ ] Add screenshots
- [ ] Update EDITOR_CHECKLIST.md
---
### 💰 Priority 2: Ad Space Preparation (1 day)
#### Design
- [ ] Finalize desktop ad column layout
- [ ] Finalize mobile ad banner layout
- [ ] Design placeholder ads for testing
- [ ] Plan responsive breakpoints
#### Desktop Ad Column
- [ ] Create `AdColumn.jsx` component
- [ ] 300px fixed width
- [ ] Sticky scroll behavior
- [ ] Space for 3 ad blocks
- [ ] Proper spacing between ads
- [ ] Create `AdBlock.jsx` component
- [ ] Support 300x250 size
- [ ] Support 300x600 size
- [ ] Placeholder content for testing
- [ ] Loading state
- [ ] Error state (if ad fails to load)
- [ ] Implement responsive behavior
- [ ] Show on screens >= 1200px
- [ ] Hide on screens < 1200px
- [ ] Smooth transition
#### Mobile Ad Banner
- [ ] Create `MobileAdBanner.jsx` component
- [ ] 320x50 or 320x100 size
- [ ] Sticky bottom position
- [ ] Close button
- [ ] Slide-in animation (optional)
- [ ] Implement close functionality
- [ ] Hide banner on close
- [ ] Store preference in localStorage (optional)
- [ ] Respect user choice
- [ ] Add padding-bottom to content
- [ ] Ensure content not hidden behind banner
- [ ] Dynamic padding based on banner height
#### Layout Integration
- [ ] Update `ToolLayout.jsx` to include ad spaces
- [ ] Add ad column container
- [ ] Add mobile banner container
- [ ] Maintain proper spacing
- [ ] Update main content width
- [ ] Desktop: `calc(100% - 320px)`
- [ ] Mobile: `100%`
- [ ] Test layout on all tool pages
- [ ] Object Editor
- [ ] Table Editor
- [ ] Invoice Editor
#### Testing
- [ ] Test desktop layout (1200px+)
- [ ] Test tablet layout (768px - 1199px)
- [ ] Test mobile layout (<768px)
- [ ] Test sticky scroll behavior
- [ ] Test sticky bottom banner
- [ ] Test close button functionality
- [ ] Test with placeholder ads
- [ ] Test dark mode compatibility
- [ ] Test on various screen sizes
- [ ] Test on actual devices (iOS, Android)
#### Performance
- [ ] Ensure no layout shift
- [ ] Optimize rendering performance
- [ ] Test scroll performance
- [ ] Measure impact on page load time
---
### 💵 Priority 3: AdSense Integration (1 day)
#### AdSense Setup
- [ ] Apply for Google AdSense account
- [ ] Provide website URL
- [ ] Provide contact information
- [ ] Wait for approval (can take 1-3 days)
- [ ] Verify site ownership (add verification code)
#### Ad Units Creation
- [ ] Log in to AdSense dashboard
- [ ] Create ad unit: Desktop Sidebar 1 (300x250)
- [ ] Create ad unit: Desktop Sidebar 2 (300x250)
- [ ] Create ad unit: Desktop Sidebar 3 (300x600)
- [ ] Create ad unit: Mobile Bottom Banner (320x50)
- [ ] Copy ad unit codes
#### Implementation
- [ ] Add AdSense script to `public/index.html`
```html
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-XXXXXXXX"
crossorigin="anonymous"></script>
```
- [ ] Update `AdBlock.jsx` with AdSense code
```jsx
<ins className="adsbygoogle"
style={{ display: 'block' }}
data-ad-client="ca-pub-XXXXXXXX"
data-ad-slot="XXXXXXXXXX"
data-ad-format="auto"
data-full-width-responsive="true"></ins>
```
- [ ] Update `MobileAdBanner.jsx` with AdSense code
- [ ] Initialize ads: `(adsbygoogle = window.adsbygoogle || []).push({});`
#### Testing
- [ ] Test ad display on desktop
- [ ] Test ad display on mobile
- [ ] Verify ads load correctly
- [ ] Check for console errors
- [ ] Test with ad blocker (should show message)
- [ ] Test on different browsers (Chrome, Firefox, Safari)
- [ ] Test on different devices
#### Monitoring
- [ ] Set up AdSense reporting
- [ ] Monitor ad impressions
- [ ] Monitor ad clicks
- [ ] Monitor ad revenue
- [ ] Track CTR (Click-Through Rate)
- [ ] Identify best-performing ad units
#### Compliance
- [ ] Create Privacy Policy page
- [ ] Data collection disclosure
- [ ] Cookie usage disclosure
- [ ] Third-party services (AdSense)
- [ ] User rights (GDPR)
- [ ] Create Terms of Service page
- [ ] Acceptable use policy
- [ ] Disclaimer
- [ ] Limitation of liability
- [ ] Add cookie consent banner (if required)
- [ ] Show on first visit
- [ ] Allow accept/decline
- [ ] Store preference
- [ ] Add "About Ads" link in footer
- [ ] Add "Privacy Policy" link in footer
- [ ] Add "Terms of Service" link in footer
#### Optimization
- [ ] Test different ad placements
- [ ] Test different ad sizes
- [ ] Monitor ad viewability
- [ ] Optimize for higher CTR
- [ ] A/B test ad positions (optional)
---
## 📋 Phase 2: Content Expansion
### 📝 Markdown Editor - MVP (1-2 weeks)
#### Planning
- [ ] Finalize feature list for MVP
- [ ] Design UI mockup (split view)
- [ ] Plan component structure
- [ ] Choose markdown parser (marked vs markdown-it)
- [ ] Plan export formats
#### Project Setup
- [ ] Create `MarkdownEditor.jsx` page
- [ ] Set up routing (`/markdown-editor`)
- [ ] Add to navigation menu
- [ ] Add to homepage tools list
#### Input Section
- [ ] Implement Create New tab
- [ ] Start Empty button
- [ ] Load Sample button (with example markdown)
- [ ] Tip box
- [ ] Implement URL tab
- [ ] Use AdvancedURLFetch component
- [ ] Support GitHub raw URLs
- [ ] Support Gist URLs
- [ ] Test with various markdown sources
- [ ] Implement Paste tab
- [ ] CodeMirror editor
- [ ] Markdown syntax highlighting
- [ ] Auto-detect markdown
- [ ] Parse button
- [ ] Collapse after parse
- [ ] Implement Open tab
- [ ] Support .md files
- [ ] Support .txt files
- [ ] Support .html files (convert to markdown)
- [ ] Support .docx files (convert to markdown)
- [ ] Auto-load on file selection
#### Editor Section
- [ ] Set up CodeMirror for markdown
- [ ] Install @codemirror/lang-markdown
- [ ] Configure markdown mode
- [ ] Add syntax highlighting
- [ ] Add line numbers
- [ ] Add line wrapping
- [ ] Implement split view layout
- [ ] Editor pane (left)
- [ ] Preview pane (right)
- [ ] Resizable divider (optional)
- [ ] Implement view mode toggle
- [ ] Split view (default)
- [ ] Editor only
- [ ] Preview only
- [ ] Fullscreen mode
- [ ] Add markdown toolbar
- [ ] Bold button (Ctrl+B)
- [ ] Italic button (Ctrl+I)
- [ ] H1 button
- [ ] H2 button
- [ ] H3 button
- [ ] Link button (Ctrl+K)
- [ ] Image button
- [ ] Code button (Ctrl+`)
- [ ] Quote button
- [ ] Unordered list button
- [ ] Ordered list button
- [ ] Table button
- [ ] Add editor features
- [ ] Word count
- [ ] Character count
- [ ] Line count
- [ ] Reading time estimate
#### Preview Section
- [ ] Set up markdown parser (marked)
- [ ] Install marked
- [ ] Install DOMPurify
- [ ] Configure marked options
- [ ] Implement live preview
- [ ] Real-time rendering
- [ ] Debounce for performance
- [ ] Scroll sync (optional)
- [ ] Add syntax highlighting for code blocks
- [ ] Install highlight.js
- [ ] Configure languages
- [ ] Apply highlighting
- [ ] Add GitHub Flavored Markdown support
- [ ] Tables
- [ ] Strikethrough
- [ ] Task lists
- [ ] Autolinks
- [ ] Implement Table of Contents
- [ ] Auto-generate from headers
- [ ] Clickable links
- [ ] Collapsible (optional)
- [ ] Add mermaid diagram rendering
- [ ] Install mermaid
- [ ] Detect mermaid code blocks
- [ ] Render diagrams
- [ ] Error handling
#### Export Section
- [ ] Create collapsible export section
- [ ] Implement Markdown export
- [ ] Standard Markdown
- [ ] GitHub Flavored Markdown
- [ ] CommonMark
- [ ] Copy to clipboard
- [ ] Download as .md file
- [ ] Implement HTML export
- [ ] Standalone HTML with CSS
- [ ] Inline styles
- [ ] Include syntax highlighting CSS
- [ ] Copy to clipboard
- [ ] Download as .html file
- [ ] Implement Plain Text export
- [ ] Strip all formatting
- [ ] Copy to clipboard
- [ ] Download as .txt file
- [ ] Implement PDF export
- [ ] Install html2pdf.js
- [ ] Convert HTML to PDF
- [ ] Maintain formatting
- [ ] Download as .pdf file
- [ ] Implement DOCX export
- [ ] Install docx library
- [ ] Convert markdown to DOCX
- [ ] Maintain formatting
- [ ] Download as .docx file
#### Conversion Features
- [ ] HTML to Markdown conversion
- [ ] Install turndown
- [ ] Convert on paste (if HTML detected)
- [ ] Convert on file open (.html)
- [ ] DOCX to Markdown conversion
- [ ] Install mammoth.js
- [ ] Convert on file open (.docx)
- [ ] Extract text and formatting
#### Usage Tips
- [ ] Create collapsible Usage Tips section
- [ ] Add Input Methods tips
- [ ] Add Editor Features tips
- [ ] Add Markdown Syntax tips
- [ ] Add Export Options tips
- [ ] Add Data Privacy tips
#### Data Loss Prevention
- [ ] Implement hasUserData() function
- [ ] Implement hasModifiedData() function
- [ ] Add confirmation modal for tab changes
- [ ] Add confirmation for Create New buttons
#### Testing
- [ ] Test all input methods
- [ ] Test markdown rendering
- [ ] Test all export formats
- [ ] Test HTML to Markdown conversion
- [ ] Test DOCX import
- [ ] Test mermaid diagrams
- [ ] Test code syntax highlighting
- [ ] Test Table of Contents
- [ ] Test view mode toggle
- [ ] Test toolbar buttons
- [ ] Test keyboard shortcuts
- [ ] Test responsive design
- [ ] Test dark mode
- [ ] Test on mobile devices
#### Documentation
- [ ] Add to EDITOR_TOOL_GUIDE.md
- [ ] Create user guide
- [ ] Add screenshots
- [ ] Create tutorial video (optional)
---
### 📝 Markdown Editor - Post-MVP (Future)
#### Advanced Markdown Features
- [ ] Add table support (GitHub-style)
- [ ] Add task lists (checkboxes)
- [ ] Add footnotes support
- [ ] Add emoji support (:smile:)
- [ ] Add math equations (KaTeX)
- [ ] Install katex
- [ ] Detect math blocks
- [ ] Render equations
#### Templates
- [ ] Create README.md template
- [ ] Create Documentation template
- [ ] Create Blog post template
- [ ] Create Meeting notes template
- [ ] Create Project proposal template
- [ ] Add template selector UI
- [ ] Allow custom templates
#### Utilities
- [ ] Add markdown linter
- [ ] Check for common issues
- [ ] Suggest improvements
- [ ] Show warnings
- [ ] Add link checker
- [ ] Validate URLs
- [ ] Check for broken links
- [ ] Show status
- [ ] Add format beautifier
- [ ] Clean up markdown
- [ ] Consistent formatting
- [ ] Fix indentation
- [ ] Add image optimizer
- [ ] Compress images
- [ ] Convert to base64
- [ ] Optimize for web
#### Enhanced Features
- [ ] Add keyboard shortcuts
- [ ] Add auto-save (localStorage)
- [ ] Add export history
- [ ] Add version history
- [ ] Add collaborative editing (future)
---
### 🎨 Diagram Tool - MVP (1-2 weeks)
#### Planning
- [ ] Finalize feature list for MVP
- [ ] Design UI mockup
- [ ] Plan component structure
- [ ] Research mermaid.js capabilities
#### Project Setup
- [ ] Create `DiagramEditor.jsx` page
- [ ] Set up routing (`/diagram-editor`)
- [ ] Add to navigation menu
- [ ] Add to homepage tools list
#### Input Section
- [ ] Implement Create New tab
- [ ] Start Empty button
- [ ] Load Sample button (with example diagrams)
- [ ] Diagram type selector
- [ ] Implement URL tab
- [ ] Use AdvancedURLFetch component
- [ ] Support mermaid code URLs
- [ ] Support PlantUML URLs (future)
- [ ] Implement Paste tab
- [ ] CodeMirror editor
- [ ] Mermaid syntax highlighting
- [ ] Parse button
- [ ] Collapse after parse
- [ ] Implement Open tab
- [ ] Support .mmd files
- [ ] Support .mermaid files
- [ ] Support .txt files
- [ ] Auto-load on file selection
#### Editor Section
- [ ] Set up CodeMirror for mermaid
- [ ] Configure mermaid mode
- [ ] Add syntax highlighting
- [ ] Add line numbers
- [ ] Implement diagram type selector
- [ ] Flowchart
- [ ] Sequence Diagram
- [ ] Class Diagram
- [ ] State Diagram
- [ ] Entity Relationship Diagram
- [ ] Gantt Chart
- [ ] Pie Chart
- [ ] Git Graph
- [ ] Add code templates for each diagram type
- [ ] Add error detection
- [ ] Syntax errors
- [ ] Invalid diagram type
- [ ] Show error messages
#### Preview Section
- [ ] Set up mermaid.js
- [ ] Install mermaid
- [ ] Configure mermaid
- [ ] Initialize mermaid
- [ ] Implement live preview
- [ ] Real-time rendering
- [ ] Debounce for performance
- [ ] Error handling
- [ ] Add zoom controls
- [ ] Zoom in button
- [ ] Zoom out button
- [ ] Reset zoom button
- [ ] Fit to screen button
- [ ] Add pan controls
- [ ] Drag to pan
- [ ] Pan with mouse wheel
- [ ] Add fullscreen mode
#### Export Section
- [ ] Create collapsible export section
- [ ] Implement PNG export
- [ ] Install html2canvas
- [ ] Convert SVG to PNG
- [ ] Download as .png file
- [ ] Implement SVG export
- [ ] Extract SVG from preview
- [ ] Download as .svg file
- [ ] Implement PDF export
- [ ] Convert SVG to PDF
- [ ] Download as .pdf file
- [ ] Implement Mermaid Code export
- [ ] Copy to clipboard
- [ ] Download as .mmd file
#### Usage Tips
- [ ] Create collapsible Usage Tips section
- [ ] Add Input Methods tips
- [ ] Add Diagram Types tips
- [ ] Add Mermaid Syntax tips
- [ ] Add Export Options tips
- [ ] Add Data Privacy tips
#### Data Loss Prevention
- [ ] Implement hasUserData() function
- [ ] Implement hasModifiedData() function
- [ ] Add confirmation modal for tab changes
- [ ] Add confirmation for Create New buttons
#### Testing
- [ ] Test all input methods
- [ ] Test all diagram types
- [ ] Test diagram rendering
- [ ] Test all export formats
- [ ] Test zoom controls
- [ ] Test pan controls
- [ ] Test fullscreen mode
- [ ] Test error handling
- [ ] Test responsive design
- [ ] Test dark mode
- [ ] Test on mobile devices
#### Documentation
- [ ] Add to EDITOR_TOOL_GUIDE.md
- [ ] Create user guide
- [ ] Add screenshots
- [ ] Create tutorial video (optional)
---
### 🎨 Diagram Tool - Post-MVP (Future)
#### Visual Editor
- [ ] Design drag-and-drop interface
- [ ] Implement node library (shapes, icons)
- [ ] Implement connection tools
- [ ] Implement styling options (colors, fonts, borders)
- [ ] Add Excalidraw-style drawing
- [ ] Add auto-layout options
#### Advanced Features
- [ ] Add PlantUML import
- [ ] Add custom themes
- [ ] Add diagram templates
- [ ] Add collaboration features (future)
- [ ] Add version history
---
## 📋 Phase 3: Monetization Backend
### 💳 Payment Integration (1 week)
#### Planning
- [ ] Choose payment provider (Stripe recommended)
- [ ] Plan payment flow
- [ ] Plan database schema
- [ ] Plan backend architecture
#### Stripe Setup
- [ ] Create Stripe account
- [ ] Verify business information
- [ ] Set up pricing plans in Stripe
- [ ] 1 month: $2.99
- [ ] 3 months: $6.99
- [ ] 6 months: $11.99
- [ ] 12 months: $19.99
- [ ] Get API keys (test and live)
#### Backend Setup
- [ ] Choose backend (Node.js + Express or serverless)
- [ ] Set up backend project
- [ ] Set up database (PostgreSQL or MongoDB)
- [ ] Create database schema
- [ ] Users table
- [ ] Purchases table
- [ ] Ad-free status table
- [ ] Set up environment variables
#### API Development
- [ ] Create payment intent endpoint
- [ ] Create payment confirmation endpoint
- [ ] Create purchase history endpoint
- [ ] Create ad-free status endpoint
- [ ] Add error handling
- [ ] Add logging
#### Frontend Integration
- [ ] Design payment flow UI
- [ ] Create checkout page
- [ ] Integrate Stripe Elements
- [ ] Implement payment form
- [ ] Implement payment success handling
- [ ] Implement payment failure handling
- [ ] Add loading states
- [ ] Add error messages
#### Email Integration
- [ ] Choose email service (SendGrid recommended)
- [ ] Set up email service account
- [ ] Create email templates
- [ ] Purchase confirmation
- [ ] Receipt
- [ ] Expiry reminder
- [ ] Implement email sending
#### Testing
- [ ] Test payment flow (test mode)
- [ ] Test with test cards
- [ ] Test payment success
- [ ] Test payment failure
- [ ] Test email sending
- [ ] Test with real payment (small amount)
- [ ] Test on different browsers
- [ ] Test on mobile devices
#### Deployment
- [ ] Deploy backend
- [ ] Set up production database
- [ ] Configure environment variables
- [ ] Test in production
- [ ] Monitor transactions
#### Legal & Compliance
- [ ] Add refund policy
- [ ] Add customer support email
- [ ] Update Terms of Service
- [ ] Update Privacy Policy
---
### 👤 User Accounts (1 week)
#### Planning
- [ ] Choose auth provider (Firebase Auth or Auth0)
- [ ] Plan auth flow
- [ ] Plan user data structure
#### Auth Setup
- [ ] Create auth provider account
- [ ] Configure auth provider
- [ ] Get API keys
- [ ] Set up authentication methods
- [ ] Email + password
- [ ] Google OAuth (optional)
- [ ] GitHub OAuth (optional)
#### UI Development
- [ ] Design login page
- [ ] Design signup page
- [ ] Design profile page
- [ ] Create login form
- [ ] Create signup form
- [ ] Create profile page
- [ ] Add form validation
#### Auth Implementation
- [ ] Implement email + password auth
- [ ] Implement OAuth (optional)
- [ ] Implement logout functionality
- [ ] Implement password reset flow
- [ ] Implement email verification
- [ ] Add session management
#### User Profile
- [ ] Display user information
- [ ] Display ad-free status
- [ ] Display purchase history
- [ ] Add edit profile functionality
- [ ] Add change password functionality
#### Integration
- [ ] Connect auth to payment system
- [ ] Store user ID with purchases
- [ ] Track ad-free status
- [ ] Update UI based on auth state
#### Testing
- [ ] Test signup flow
- [ ] Test login flow
- [ ] Test logout flow
- [ ] Test password reset
- [ ] Test email verification
- [ ] Test on different browsers
- [ ] Test on mobile devices
#### Security
- [ ] Add rate limiting
- [ ] Add CSRF protection
- [ ] Add XSS protection
- [ ] Add SQL injection protection
- [ ] Implement secure session handling
---
### 🚫 Ad-Free Experience (3 days)
#### UI Development
- [ ] Design ad-free badge
- [ ] Design expiry reminder
- [ ] Design renewal flow
- [ ] Create ad-free badge component
- [ ] Create expiry reminder component
#### Implementation
- [ ] Check ad-free status on page load
- [ ] Hide ads for ad-free users
- [ ] Show ad-free badge in UI
- [ ] Implement expiry reminder (7 days before)
- [ ] Implement grace period (3 days after expiry)
- [ ] Add renewal flow
#### Testing
- [ ] Test ad-free status check
- [ ] Test ad hiding
- [ ] Test ad-free badge display
- [ ] Test expiry reminder
- [ ] Test grace period
- [ ] Test renewal flow
- [ ] Test on different browsers
- [ ] Test on mobile devices
#### Monitoring
- [ ] Track ad-free user count
- [ ] Monitor expiry dates
- [ ] Send expiry reminders
- [ ] Collect feedback from ad-free users
---
## 🎯 Quick Wins (Can Do Anytime)
### SEO & Marketing
- [ ] Optimize page titles
- [ ] Optimize meta descriptions
- [ ] Add Open Graph tags
- [ ] Add Twitter Card tags
- [ ] Create sitemap.xml
- [ ] Submit to Google Search Console
- [ ] Create robots.txt
### Content
- [ ] Write blog post: "How to use Object Editor"
- [ ] Write blog post: "How to use Table Editor"
- [ ] Write blog post: "How to use Invoice Editor"
- [ ] Create tutorial videos
- [ ] Share on Reddit
- [ ] Share on Hacker News
- [ ] Share on Twitter/X
### UX Improvements
- [ ] Add loading skeletons
- [ ] Add empty states
- [ ] Add success messages
- [ ] Add error messages
- [ ] Improve error handling
- [ ] Add tooltips
- [ ] Add keyboard shortcuts
### Performance
- [ ] Implement code splitting
- [ ] Lazy load components
- [ ] Optimize images
- [ ] Minify CSS/JS
- [ ] Add caching headers
- [ ] Implement service worker (PWA)
---
## 📊 Metrics to Track
### User Metrics
- [ ] Daily active users (DAU)
- [ ] Weekly active users (WAU)
- [ ] Monthly active users (MAU)
- [ ] New users per day
- [ ] Returning users
- [ ] User retention rate
### Tool Usage
- [ ] Object Editor usage
- [ ] Table Editor usage
- [ ] Invoice Editor usage
- [ ] Markdown Editor usage (when launched)
- [ ] Diagram Tool usage (when launched)
- [ ] Most popular features
### Monetization
- [ ] Ad impressions per day
- [ ] Ad clicks per day
- [ ] Ad CTR (Click-Through Rate)
- [ ] Ad revenue per day
- [ ] Ad-free purchases per month
- [ ] Ad-free revenue per month
- [ ] Total revenue per month
### Performance
- [ ] Page load time
- [ ] Time to interactive
- [ ] First contentful paint
- [ ] Largest contentful paint
- [ ] Cumulative layout shift
---
**End of To-Do List**

View File

@@ -1,5 +1,52 @@
{ {
"changelog": [ "changelog": [
{
"date": "2025-10-14",
"changes": [
{
"datetime": "2025-10-14T23:59:00+07:00",
"type": "enhancement",
"title": "Editor UX Refinement - Collapsible Sections & Usage Tips",
"description": "Enhanced all editor tools (Object, Table, Invoice) with collapsible export sections and comprehensive usage tips. Export sections now start collapsed to reduce page height, and usage tips are collapsible with eye-catching design. Added detailed documentation guides (EDITOR_TOOL_GUIDE.md, EDITOR_CHECKLIST.md) for consistent future development."
},
{
"datetime": "2025-10-14T22:30:00+07:00",
"type": "enhancement",
"title": "Consistent Input Methods Across All Editors",
"description": "Standardized file input and URL fetch UI across Object, Table, and Invoice editors. File inputs now use consistent 'tool-input' class and auto-load content immediately. URL inputs feature inline fetch buttons (not below) for better UX. Paste sections now collapse after successful parsing with data summaries."
},
{
"datetime": "2025-10-14T21:00:00+07:00",
"type": "enhancement",
"title": "Data Loss Prevention System",
"description": "Implemented comprehensive confirmation modals across all editors to prevent accidental data loss. When switching input methods or tabs with existing data, users now see a detailed warning with specific data summary and option to save before proceeding. Features amber-themed warning design with clear action buttons."
},
{
"datetime": "2025-10-14T20:00:00+07:00",
"type": "fix",
"title": "Table Editor Cell Overflow Fix",
"description": "Fixed text overflow issues in Table Editor cells. Long text now properly truncates with ellipsis instead of breaking layout. Added proper overflow handling with 'truncate' class and block-level spans for consistent cell rendering."
},
{
"datetime": "2025-10-14T19:30:00+07:00",
"type": "enhancement",
"title": "Object Editor Output Section Improvements",
"description": "Made Object Editor's output/export section collapsible to reduce page height when working with long data. Export section now starts collapsed with a clickable header showing data summary, matching the pattern used in other editors."
},
{
"datetime": "2025-10-14T19:00:00+07:00",
"type": "fix",
"title": "Object Editor File Input Improvements",
"description": "Fixed Object Editor's 'Open File' tab to auto-load content immediately upon file selection, matching Table and Invoice editor behavior. Removed unnecessary 'Parse Object' button and irrelevant CSV checkbox for cleaner, more intuitive UX."
},
{
"datetime": "2025-10-14T18:30:00+07:00",
"type": "enhancement",
"title": "Invoice Editor Export & URL Consistency",
"description": "Made Invoice Editor's export section collapsible to match other editors. Fixed URL form layout to use inline fetch button instead of below-input placement. Improved overall consistency across all editor input methods."
}
]
},
{ {
"date": "2025-09-28", "date": "2025-09-28",
"changes": [ "changes": [

View File

@@ -123,7 +123,7 @@ const CodeMirrorEditor = ({
<div className={`relative ${className}`}> <div className={`relative ${className}`}>
<div <div
ref={editorRef} ref={editorRef}
className={`dewedev-code-mirror border border-gray-300 dark:border-gray-600 rounded-md ${ className={`dewedev-code-mirror border border-gray-300 dark:border-gray-600 rounded-md overflow-hidden ${
isDark ? 'bg-gray-900' : 'bg-white' isDark ? 'bg-gray-900' : 'bg-white'
} ${isExpanded ? 'h-auto' : 'h-[350px]'}`} } ${isExpanded ? 'h-auto' : 'h-[350px]'}`}
/> />

View File

@@ -454,7 +454,7 @@ const MindmapView = React.memo(({ data }) => {
return ( return (
<div className={`w-full border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 ${ <div className={`w-full border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 ${
isFullscreen isFullscreen
? 'fixed inset-0 z-50 rounded-none' ? 'fixed inset-0 z-[99999] rounded-none'
: 'h-[600px]' : 'h-[600px]'
}`}> }`}>
<ReactFlow <ReactFlow

View File

@@ -1,11 +1,13 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { Plus, Minus, ChevronDown, ChevronRight, Type, Hash, ToggleLeft, List, Braces } from 'lucide-react'; import { Plus, Minus, ChevronDown, ChevronRight, Type, Hash, ToggleLeft, List, Braces, Edit3, X } from 'lucide-react';
const StructuredEditor = ({ onDataChange, initialData = {} }) => { const StructuredEditor = ({ onDataChange, initialData = {} }) => {
const [data, setData] = useState(initialData); const [data, setData] = useState(initialData);
const [expandedNodes, setExpandedNodes] = useState(new Set(['root'])); const [expandedNodes, setExpandedNodes] = useState(new Set(['root']));
const [fieldTypes, setFieldTypes] = useState({}); // Track intended types for fields const [fieldTypes, setFieldTypes] = useState({}); // Track intended types for fields
const isInternalUpdate = useRef(false); const isInternalUpdate = useRef(false);
const [nestedEditModal, setNestedEditModal] = useState(null); // { path, value, type: 'json' | 'serialized' }
const [nestedData, setNestedData] = useState(null);
// Update internal data when initialData prop changes (but not from internal updates) // Update internal data when initialData prop changes (but not from internal updates)
useEffect(() => { useEffect(() => {
@@ -28,6 +30,173 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
onDataChange(newData); onDataChange(newData);
}; };
// PHP serialize/unserialize functions
const phpSerialize = (data) => {
if (data === null) return 'N;';
if (typeof data === 'boolean') return data ? 'b:1;' : 'b:0;';
if (typeof data === 'number') {
return Number.isInteger(data) ? `i:${data};` : `d:${data};`;
}
if (typeof data === 'string') {
const escapedData = data.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
const byteLength = new TextEncoder().encode(escapedData).length;
return `s:${byteLength}:"${escapedData}";`;
}
if (Array.isArray(data)) {
let result = `a:${data.length}:{`;
data.forEach((item, index) => {
result += phpSerialize(index) + phpSerialize(item);
});
result += '}';
return result;
}
if (typeof data === 'object') {
const keys = Object.keys(data);
let result = `a:${keys.length}:{`;
keys.forEach(key => {
result += phpSerialize(key) + phpSerialize(data[key]);
});
result += '}';
return result;
}
return 'N;';
};
const phpUnserialize = (str) => {
let index = 0;
const parseValue = () => {
if (index >= str.length) throw new Error('Unexpected end of string');
const type = str[index];
if (type === 'N') {
index += 2;
return null;
}
if (str[index + 1] !== ':') throw new Error(`Expected ':' after type '${type}'`);
index += 2;
switch (type) {
case 'b':
const boolVal = str[index] === '1';
index += 2;
return boolVal;
case 'i':
let intStr = '';
while (index < str.length && str[index] !== ';') intStr += str[index++];
index++;
return parseInt(intStr);
case 'd':
let floatStr = '';
while (index < str.length && str[index] !== ';') floatStr += str[index++];
index++;
return parseFloat(floatStr);
case 's':
let lenStr = '';
while (index < str.length && str[index] !== ':') lenStr += str[index++];
index++;
if (str[index] !== '"') throw new Error('Expected opening quote');
index++;
const byteLength = parseInt(lenStr);
if (byteLength === 0) {
index += 2;
return '';
}
let endQuotePos = -1;
for (let i = index; i < str.length - 1; i++) {
if (str[i] === '"' && str[i + 1] === ';') {
endQuotePos = i;
break;
}
}
if (endQuotePos === -1) throw new Error('Could not find closing quote');
const strValue = str.substring(index, endQuotePos);
index = endQuotePos + 2;
return strValue.replace(/\\"/g, '"').replace(/\\\\/g, '\\');
case 'a':
let countStr = '';
while (index < str.length && str[index] !== ':') countStr += str[index++];
const count = parseInt(countStr);
index += 2;
const result = {};
let isArray = true;
for (let i = 0; i < count; i++) {
const key = parseValue();
const value = parseValue();
result[key] = value;
if (key !== i) isArray = false;
}
index++;
return isArray ? Object.values(result) : result;
default:
throw new Error(`Unknown type: ${type}`);
}
};
return parseValue();
};
// Detect if a string contains JSON or serialized data
const detectNestedData = (value) => {
if (typeof value !== 'string' || value.length < 5) return null;
// Try JSON first
try {
const parsed = JSON.parse(value);
if (typeof parsed === 'object' && parsed !== null) {
return { type: 'json', data: parsed };
}
} catch (e) {
// Not JSON, continue
}
// Try PHP serialized
try {
// Check if it looks like PHP serialized format
if (/^[abidsNO]:[^;]*;/.test(value) || /^a:\d+:\{/.test(value)) {
const parsed = phpUnserialize(value);
if (typeof parsed === 'object' && parsed !== null) {
return { type: 'serialized', data: parsed };
}
}
} catch (e) {
// Not serialized
}
return null;
};
// Open nested editor modal
const openNestedEditor = (value, path) => {
const detected = detectNestedData(value);
if (detected) {
setNestedEditModal({ path, value, type: detected.type });
setNestedData(detected.data);
}
};
// Save nested editor changes
const saveNestedEdit = () => {
if (!nestedEditModal || !nestedData) return;
// Convert back to string based on type
let stringValue;
if (nestedEditModal.type === 'json') {
stringValue = JSON.stringify(nestedData);
} else if (nestedEditModal.type === 'serialized') {
stringValue = phpSerialize(nestedData);
}
// Update the value in the main data
updateValue(stringValue, nestedEditModal.path);
// Close modal
setNestedEditModal(null);
setNestedData(null);
};
// Close nested editor modal
const closeNestedEditor = () => {
setNestedEditModal(null);
setNestedData(null);
};
const toggleNode = (path) => { const toggleNode = (path) => {
const newExpanded = new Set(expandedNodes); const newExpanded = new Set(expandedNodes);
if (newExpanded.has(path)) { if (newExpanded.has(path)) {
@@ -431,23 +600,34 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
</span> </span>
</div> </div>
) : ( ) : (
(fieldTypes[path] === 'longtext' || (typeof value === 'string' && value.includes('\n'))) ? ( <div className="flex-1 flex items-center gap-2">
<textarea {(fieldTypes[path] === 'longtext' || (typeof value === 'string' && value.includes('\n'))) ? (
value={getDisplayValue(value)} <textarea
onChange={(e) => updateValue(e.target.value, path)} value={getDisplayValue(value)}
className="flex-1 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 min-w-0 resize-y items" onChange={(e) => updateValue(e.target.value, path)}
placeholder="Long text value" className="flex-1 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 min-w-0 resize-y items"
rows={3} placeholder="Long text value"
/> rows={3}
) : ( />
<input ) : (
type="text" <input
value={getDisplayValue(value)} type="text"
onChange={(e) => updateValue(e.target.value, path)} value={getDisplayValue(value)}
className="flex-1 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 min-w-0" onChange={(e) => updateValue(e.target.value, path)}
placeholder="Value" className="flex-1 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 min-w-0"
/> placeholder="Value"
) />
)}
{typeof value === 'string' && detectNestedData(value) && (
<button
onClick={() => openNestedEditor(value, path)}
className="p-1 text-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900 rounded flex-shrink-0"
title={`Edit nested ${detectNestedData(value).type} data`}
>
<Edit3 className="h-4 w-4" />
</button>
)}
</div>
) )
) : ( ) : (
<span className="flex-1 text-sm text-gray-600 dark:text-gray-400"> <span className="flex-1 text-sm text-gray-600 dark:text-gray-400">
@@ -552,6 +732,55 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
<span>Add Property</span> <span>Add Property</span>
</button> </button>
</div> </div>
{/* Nested Data Editor Modal */}
{nestedEditModal && nestedData && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[99999] p-4">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col">
{/* Modal Header */}
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700 bg-blue-50 dark:bg-blue-900/20 flex items-center justify-between">
<div>
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">
Edit Nested {nestedEditModal.type === 'json' ? 'JSON' : 'Serialized'} Data
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
Changes will be saved back as a {nestedEditModal.type === 'json' ? 'JSON' : 'serialized'} string
</p>
</div>
<button
onClick={closeNestedEditor}
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
>
<X className="h-5 w-5" />
</button>
</div>
{/* Modal Body - Nested Editor */}
<div className="flex-1 overflow-auto p-6">
<StructuredEditor
initialData={nestedData}
onDataChange={setNestedData}
/>
</div>
{/* Modal Footer */}
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/20 flex justify-end gap-3">
<button
onClick={closeNestedEditor}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
>
Cancel
</button>
<button
onClick={saveNestedEdit}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 rounded-md transition-colors"
>
Save Changes
</button>
</div>
</div>
</div>
)}
</div> </div>
); );
}; };

View File

@@ -22,6 +22,10 @@ const InvoiceEditor = () => {
const [error, setError] = useState(''); const [error, setError] = useState('');
const fileInputRef = useRef(null); const fileInputRef = useRef(null);
const logoInputRef = 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); URL.revokeObjectURL(url);
}; };
const handleFileImport = (event) => { // Handle file import (same as Table Editor)
const handleFileSelect = (event) => {
const file = event.target.files[0]; const file = event.target.files[0];
if (file) { if (file) {
const reader = new FileReader(); const reader = new FileReader();
@@ -642,7 +647,6 @@ const InvoiceEditor = () => {
const importedData = JSON.parse(content); const importedData = JSON.parse(content);
setInvoiceData(importedData); setInvoiceData(importedData);
setCreateNewCompleted(true); setCreateNewCompleted(true);
setActiveTab('create');
setError(''); setError('');
} catch (err) { } catch (err) {
setError('Invalid JSON file format'); setError('Invalid JSON file format');
@@ -834,105 +838,109 @@ const InvoiceEditor = () => {
{activeTab === 'url' && ( {activeTab === 'url' && (
<div className="space-y-3"> <div className="space-y-3">
<div> <div className="flex gap-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> <div className="relative flex-1">
Invoice JSON URL <input
</label> type="url"
<input value={url}
type="url" onChange={(e) => setUrl(e.target.value)}
value={url} placeholder="https://api.telegram.org/bot<token>/getMe"
onChange={(e) => setUrl(e.target.value)} className="tool-input w-full"
placeholder="https://drive.google.com/file/d/... or any JSON URL" onKeyPress={(e) => e.key === 'Enter' && handleUrlFetch()}
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> </div>
<p className="text-xs text-blue-700 dark:text-blue-300"> <button
<strong>Google Drive:</strong> Use share links like "drive.google.com/file/d/..." - we'll convert them automatically. onClick={handleUrlFetch}
</p> 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> </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> </div>
)} )}
{activeTab === 'paste' && ( {activeTab === 'paste' && (
<div className="space-y-3"> pasteCollapsed ? (
<div> <div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> <div className="flex items-center justify-between">
Paste Invoice JSON Data <span className="text-sm text-green-700 dark:text-green-300">
</label> ✓ Invoice loaded: {pasteDataSummary.invoiceNumber || 'New Invoice'}
<CodeMirrorEditor </span>
value={inputText} <button
onChange={setInputText} onClick={() => setPasteCollapsed(false)}
placeholder="Paste your invoice JSON data here..." className="text-sm text-blue-600 hover:underline"
language="json" >
maxLines={12} Edit Input ▼
showToggle={true} </button>
className="w-full" </div>
/>
</div> </div>
<button ) : (
onClick={() => { <div className="space-y-3">
try { <div>
const parsed = JSON.parse(inputText); <CodeMirrorEditor
setInvoiceData(parsed); value={inputText}
setCreateNewCompleted(true); onChange={setInputText}
setError(''); placeholder="Paste your invoice JSON data here..."
} catch (err) { language="json"
setError('Invalid JSON format'); maxLines={12}
} showToggle={true}
}} className="w-full"
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" </div>
> {error && (
Load Invoice Data <div className="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
</button> <p className="text-sm text-red-600 dark:text-red-400">
</div> <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' && ( {activeTab === 'open' && (
<div className="space-y-3"> <div className="space-y-3">
<div> <input
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> ref={fileInputRef}
Choose File type="file"
</label> accept=".json"
<input onChange={handleFileSelect}
ref={fileInputRef} className="tool-input"
type="file" />
accept=".json" <div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-md p-3">
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>
<p className="text-xs text-green-700 dark:text-green-300"> <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> </p>
</div> </div>
</div> </div>
@@ -1955,14 +1963,21 @@ const InvoiceEditor = () => {
{/* Export Section */} {/* Export Section */}
{(activeTab !== 'create' || createNewCompleted) && createNewCompleted && ( {(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="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
<div className="flex items-center gap-3"> onClick={() => setExportExpanded(!exportExpanded)}
<Download className="h-5 w-5 text-green-600 dark:text-green-400" /> 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"
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Export Invoice</h2> >
<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> </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"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<button <button
onClick={handleGeneratePreview} onClick={handleGeneratePreview}
@@ -1993,9 +2008,75 @@ const InvoiceEditor = () => {
</p> </p>
</div> </div>
</div> </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 Display */}
{error && ( {error && (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4"> <div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">

View File

@@ -1,5 +1,5 @@
import React, { useState, useRef, useEffect, useCallback } from '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 { Plus, Upload, FileText, Globe, Edit3, Download, Workflow, Table, Braces, Code, AlertTriangle, ChevronUp, ChevronDown } from 'lucide-react';
import ToolLayout from '../components/ToolLayout'; import ToolLayout from '../components/ToolLayout';
import StructuredEditor from '../components/StructuredEditor'; import StructuredEditor from '../components/StructuredEditor';
import MindmapView from '../components/MindmapView'; import MindmapView from '../components/MindmapView';
@@ -56,6 +56,10 @@ const ObjectEditor = () => {
const [showInputChangeModal, setShowInputChangeModal] = useState(false); const [showInputChangeModal, setShowInputChangeModal] = useState(false);
const [pendingTabChange, setPendingTabChange] = useState(null); const [pendingTabChange, setPendingTabChange] = useState(null);
const fileInputRef = useRef(null); const fileInputRef = useRef(null);
const [pasteCollapsed, setPasteCollapsed] = useState(false);
const [pasteDataSummary, setPasteDataSummary] = useState(null);
const [outputExpanded, setOutputExpanded] = useState(false);
const [usageTipsExpanded, setUsageTipsExpanded] = useState(false);
// Helper function to check if user has data that would be lost // Helper function to check if user has data that would be lost
const hasUserData = () => { const hasUserData = () => {
@@ -427,7 +431,7 @@ const ObjectEditor = () => {
} }
}; };
// Handle input text change // Handle input text change (validation only, no auto-load)
const handleInputChange = (value) => { const handleInputChange = (value) => {
setInputText(value); setInputText(value);
const detection = detectInputFormat(value); const detection = detectInputFormat(value);
@@ -435,11 +439,8 @@ const ObjectEditor = () => {
setInputValid(detection.valid); setInputValid(detection.valid);
if (detection.valid) { if (detection.valid) {
setStructuredData(detection.data);
setError(''); setError('');
setCreateNewCompleted(true);
} else if (value.trim()) { } else if (value.trim()) {
// Use specific error message if available, otherwise generic message
const errorMessage = detection.error || 'Invalid format. Please enter valid JSON or PHP serialized data.'; const errorMessage = detection.error || 'Invalid format. Please enter valid JSON or PHP serialized data.';
setError(errorMessage); setError(errorMessage);
} else { } else {
@@ -447,6 +448,28 @@ const ObjectEditor = () => {
} }
}; };
// Handle Parse Object button click
const handleParseObject = () => {
const detection = detectInputFormat(inputText);
if (detection.valid) {
setStructuredData(detection.data);
setError('');
setCreateNewCompleted(true);
setPasteDataSummary({
format: detection.format,
size: inputText.length,
properties: Object.keys(detection.data).length
});
setPasteCollapsed(true);
} else {
// Show error, keep input expanded
const errorMessage = detection.error || 'Invalid format. Please enter valid JSON or PHP serialized data.';
setError(errorMessage);
setPasteCollapsed(false);
}
};
// Handle structured data change from visual editor // Handle structured data change from visual editor
const handleStructuredDataChange = (newData) => { const handleStructuredDataChange = (newData) => {
setStructuredData(newData); setStructuredData(newData);
@@ -523,16 +546,26 @@ const ObjectEditor = () => {
} }
}, [serializeToPhp]); }, [serializeToPhp]);
// Handle file import // Handle file import (auto-load, same as Table/Invoice Editor)
const handleFileImport = (event) => { const handleFileImport = (event) => {
const file = event.target.files[0]; const file = event.target.files[0];
if (file) { if (file) {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => { reader.onload = (e) => {
const content = e.target.result; try {
setInputText(content); const content = e.target.result;
handleInputChange(content); const detection = detectInputFormat(content);
setCreateNewCompleted(true);
if (detection.valid) {
setStructuredData(detection.data);
setCreateNewCompleted(true);
setError('');
} else {
setError(detection.error || 'Invalid format. Please enter valid JSON or PHP serialized data.');
}
} catch (err) {
setError('Failed to read file: ' + err.message);
}
}; };
reader.readAsText(file); reader.readAsText(file);
} }
@@ -774,33 +807,63 @@ const ObjectEditor = () => {
{/* Paste Tab Content */} {/* Paste Tab Content */}
{activeTab === 'paste' && ( {activeTab === 'paste' && (
<div className="space-y-3"> pasteCollapsed ? (
<div className="flex justify-end items-center"> <div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
{inputFormat && ( <div className="flex items-center justify-between">
<span className={`text-xs px-2 py-1 rounded ${ <span className="text-sm text-green-700 dark:text-green-300">
inputValid ✓ Object loaded: {pasteDataSummary.format} ({pasteDataSummary.size.toLocaleString()} chars, {pasteDataSummary.properties} {pasteDataSummary.properties === 1 ? 'property' : 'properties'})
? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300'
: 'bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-300'
}`}>
{inputFormat} {inputValid ? '' : ''}
</span> </span>
)} <button
onClick={() => setPasteCollapsed(false)}
className="text-sm text-blue-600 hover:underline"
>
Edit Input ▼
</button>
</div>
</div> </div>
<CodeMirrorEditor ) : (
value={inputText} <div className="space-y-3">
onChange={(value) => handleInputChange(value)} <div>
language={inputFormat === 'JSON' ? 'json' : 'javascript'} <CodeMirrorEditor
placeholder="Paste JSON or PHP serialized data here..." value={inputText}
maxLines={12} onChange={(value) => handleInputChange(value)}
showToggle={true} language={inputFormat === 'JSON' ? 'json' : 'javascript'}
className="w-full" placeholder="Paste JSON or PHP serialized data here..."
/> maxLines={12}
{error && ( showToggle={true}
<p className="text-sm text-red-600 dark:text-red-400"> className="w-full"
{error} />
</p> </div>
)} {error && (
</div> <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">
{inputFormat && (
<span className={`inline-flex items-center px-2 py-1 rounded text-xs font-medium ${
inputValid
? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300'
: 'bg-amber-100 text-amber-800 dark:bg-amber-900/20 dark:text-amber-300'
}`}>
{inputFormat} {inputValid ? ' Valid' : ' Invalid'}
</span>
)}
{!inputFormat && 'Auto-detects JSON and PHP serialized formats'}
</div>
<button
onClick={handleParseObject}
disabled={!inputValid || !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"
>
Parse Object
</button>
</div>
</div>
)
)} )}
{/* Open Tab Content */} {/* Open Tab Content */}
@@ -811,28 +874,11 @@ const ObjectEditor = () => {
type="file" type="file"
accept=".json,.txt" accept=".json,.txt"
onChange={handleFileImport} onChange={handleFileImport}
className="tool-input w-full" className="tool-input"
/> />
<div className="flex items-center"> <div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-md p-3">
<input
type="checkbox"
id="useFirstRowAsHeader"
checked={true}
readOnly
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mr-2"
/>
<label htmlFor="useFirstRowAsHeader" className="text-sm text-gray-700 dark:text-gray-300">
Use first row as column headers (for CSV/TSV)
</label>
</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>
<p className="text-xs text-green-700 dark:text-green-300"> <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 - just help you open, edit, and export your files locally. 🔒 <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> </p>
</div> </div>
</div> </div>
@@ -939,12 +985,16 @@ const ObjectEditor = () => {
{/* Export Section - Only show if createNewCompleted or not on create tab */} {/* Export Section - Only show if createNewCompleted or not on create tab */}
{(activeTab !== 'create' || createNewCompleted) && Object.keys(structuredData).length > 0 && ( {(activeTab !== 'create' || createNewCompleted) && Object.keys(structuredData).length > 0 && (
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mt-6"> <div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mt-6">
{/* Export Header */} {/* Export Header - Collapsible */}
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700"> <div
onClick={() => setOutputExpanded(!outputExpanded)}
className="px-4 py-3 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 justify-between">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2"> <h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Download className="h-5 w-5 text-green-600 dark:text-green-400" /> <Download className="h-5 w-5 text-green-600 dark:text-green-400" />
Export Results Export Results
{outputExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</h3> </h3>
<div className="text-sm text-gray-600 dark:text-gray-400"> <div className="text-sm text-gray-600 dark:text-gray-400">
<span>Object: {Object.keys(structuredData).length} properties</span> <span>Object: {Object.keys(structuredData).length} properties</span>
@@ -952,6 +1002,9 @@ const ObjectEditor = () => {
</div> </div>
</div> </div>
{/* Export Content - Collapsible */}
{outputExpanded && (
<div>
{/* Export Tabs */} {/* Export Tabs */}
<div className="flex border-b border-gray-200 dark:border-gray-700"> <div className="flex border-b border-gray-200 dark:border-gray-700">
<button <button
@@ -1082,6 +1135,8 @@ const ObjectEditor = () => {
</div> </div>
)} )}
</div> </div>
</div>
)}
</div> </div>
)} )}
@@ -1104,9 +1159,19 @@ const ObjectEditor = () => {
)} )}
{/* Usage Tips */} {/* Usage Tips */}
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md p-4 mt-6"> <div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md overflow-hidden mt-6">
<h4 className="text-blue-800 dark:text-blue-200 font-medium mb-3">Usage Tips</h4> <div
<div className="text-blue-700 dark:text-blue-300 text-sm space-y-2"> 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-2">
<div> <div>
<p className="font-medium mb-1">📝 Input Methods:</p> <p className="font-medium mb-1">📝 Input Methods:</p>
<ul className="list-disc list-inside space-y-1 ml-2"> <ul className="list-disc list-inside space-y-1 ml-2">
@@ -1144,6 +1209,7 @@ const ObjectEditor = () => {
</ul> </ul>
</div> </div>
</div> </div>
)}
</div> </div>
</ToolLayout> </ToolLayout>
); );

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; 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 { Plus, Upload, FileText, Globe, Download, X, Table, Trash2, Database, Braces, Code, Eye, Minimize2, Maximize2, Search, ArrowUpDown, AlertTriangle, Edit3, ChevronUp, ChevronDown } from 'lucide-react';
import ToolLayout from '../components/ToolLayout'; import ToolLayout from '../components/ToolLayout';
import CodeEditor from '../components/CodeEditor'; import CodeEditor from '../components/CodeEditor';
import CodeMirrorEditor from '../components/CodeMirrorEditor'; import CodeMirrorEditor from '../components/CodeMirrorEditor';
@@ -67,6 +67,10 @@ const TableEditor = () => {
const [showInputChangeModal, setShowInputChangeModal] = useState(false); // For input method change confirmation const [showInputChangeModal, setShowInputChangeModal] = useState(false); // For input method change confirmation
const [pendingTabChange, setPendingTabChange] = useState(null); // Store pending tab change const [pendingTabChange, setPendingTabChange] = useState(null); // Store pending tab change
const [createNewCompleted, setCreateNewCompleted] = useState(false); // Track if user completed Create New step const [createNewCompleted, setCreateNewCompleted] = useState(false); // Track if user completed Create New step
const [pasteCollapsed, setPasteCollapsed] = useState(false); // Track if paste input is collapsed
const [pasteDataSummary, setPasteDataSummary] = useState(null); // Summary of pasted data
const [exportExpanded, setExportExpanded] = useState(false); // Track if export section is expanded
const [usageTipsExpanded, setUsageTipsExpanded] = useState(false); // Track if usage tips is expanded
// SQL Export specific state // SQL Export specific state
const [sqlTableName, setSqlTableName] = useState(""); // Table name for SQL export const [sqlTableName, setSqlTableName] = useState(""); // Table name for SQL export
@@ -977,24 +981,50 @@ const TableEditor = () => {
const handleTextInput = () => { const handleTextInput = () => {
if (!inputText.trim()) { if (!inputText.trim()) {
setError("Please enter some data"); setError("Please enter some data");
setPasteCollapsed(false);
return; return;
} }
const trimmed = inputText.trim(); const trimmed = inputText.trim();
let format = '';
let success = false;
// Try to detect format try {
if (trimmed.startsWith("[") && trimmed.endsWith("]")) { // Try to detect format
// JSON array if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
parseJsonData(trimmed); // JSON array
} else if ( parseJsonData(trimmed);
trimmed.toLowerCase().includes("insert into") && format = 'JSON';
trimmed.toLowerCase().includes("values") success = true;
) { } else if (
// SQL INSERT statements trimmed.toLowerCase().includes("insert into") &&
parseSqlData(trimmed); trimmed.toLowerCase().includes("values")
} else { ) {
// CSV/TSV // SQL INSERT statements
parseData(trimmed, useFirstRowAsHeader); parseSqlData(trimmed);
format = 'SQL';
success = true;
} else {
// CSV/TSV
parseData(trimmed, useFirstRowAsHeader);
format = trimmed.includes('\t') ? 'TSV' : 'CSV';
success = true;
}
// If successful, collapse input and show summary
if (success && data.length > 0) {
setPasteDataSummary({
format: format,
size: inputText.length,
rows: data.length
});
setPasteCollapsed(true);
setError('');
}
} catch (err) {
// Keep input expanded on error
setPasteCollapsed(false);
setError(err.message || 'Failed to parse data');
} }
}; };
@@ -1793,7 +1823,7 @@ const TableEditor = () => {
icon={Table} icon={Table}
> >
{/* Input Section with Tabs */} {/* Input Section with Tabs */}
<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"> <div className="w-full bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
{/* Tabs */} {/* Tabs */}
<div className="flex border-b border-gray-200 dark:border-gray-700 overflow-x-auto scrollbar-hide"> <div className="flex border-b border-gray-200 dark:border-gray-700 overflow-x-auto scrollbar-hide">
<div className="flex min-w-max"> <div className="flex min-w-max">
@@ -2006,34 +2036,59 @@ const TableEditor = () => {
)} )}
{activeTab === "paste" && ( {activeTab === "paste" && (
<div className="space-y-3"> pasteCollapsed ? (
<CodeMirrorEditor <div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
value={inputText} <div className="flex items-center justify-between">
onChange={setInputText} <span className="text-sm text-green-700 dark:text-green-300">
language="json" ✓ Data loaded: {pasteDataSummary.format} ({pasteDataSummary.size.toLocaleString()} chars, {pasteDataSummary.rows} rows)
placeholder="Paste CSV, TSV, JSON, or SQL INSERT statements here..." </span>
maxLines={12} <button
showToggle={true} onClick={() => setPasteCollapsed(false)}
className="w-full" className="text-sm text-blue-600 hover:underline"
/> >
<div className="flex items-center justify-between"> Edit Input ▼
<label className="flex items-center text-sm text-gray-600 dark:text-gray-400"> </button>
<input </div>
type="checkbox"
checked={useFirstRowAsHeader}
onChange={(e) => setUseFirstRowAsHeader(e.target.checked)}
className="mr-2"
/>
Use first row as column headers (for CSV/TSV)
</label>
<button
onClick={handleTextInput}
className="bg-blue-600 hover:bg-blue-700 text-white font-medium px-4 py-2 rounded-md transition-colors"
>
Parse Data
</button>
</div> </div>
</div> ) : (
<div className="space-y-3">
<div>
<CodeMirrorEditor
value={inputText}
onChange={setInputText}
language="json"
placeholder="Paste CSV, TSV, JSON, or SQL INSERT statements here..."
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">
<label className="flex items-center text-sm text-gray-600 dark:text-gray-400">
<input
type="checkbox"
checked={useFirstRowAsHeader}
onChange={(e) => setUseFirstRowAsHeader(e.target.checked)}
className="mr-2"
/>
Use first row as column headers (for CSV/TSV)
</label>
<button
onClick={handleTextInput}
className="bg-blue-600 hover:bg-blue-700 text-white font-medium px-4 py-2 rounded-md transition-colors flex-shrink-0"
>
Parse Data
</button>
</div>
</div>
)
)} )}
{activeTab === "upload" && ( {activeTab === "upload" && (
@@ -2070,9 +2125,10 @@ const TableEditor = () => {
<div <div
className={`bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 min-w-0 ${ className={`bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 min-w-0 ${
isTableFullscreen isTableFullscreen
? "fixed inset-0 z-50 rounded-none border-0 shadow-none overflow-hidden" ? "fixed inset-0 z-[99999] rounded-none border-0 shadow-none overflow-hidden !m-0"
: "overflow-x-auto" : "overflow-x-auto mt-4 sm:mt-6"
}`} }`}
style={isTableFullscreen ? { marginTop: "0 !important" } : {}}
> >
{/* Header */} {/* Header */}
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700"> <div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
@@ -2398,7 +2454,7 @@ const TableEditor = () => {
return ( return (
<td <td
key={column.id} key={column.id}
className={`px-4 py-3 text-sm text-gray-900 dark:text-gray-100 border-r border-gray-200 dark:border-gray-600 break-words ${ className={`px-4 py-3 text-sm text-gray-900 dark:text-gray-100 border-r border-gray-200 dark:border-gray-600 break-words overflow-hidden ${
isFrozen isFrozen
? "sticky z-10 bg-blue-50 dark:!bg-blue-900" ? "sticky z-10 bg-blue-50 dark:!bg-blue-900"
: "" : ""
@@ -2500,7 +2556,7 @@ const TableEditor = () => {
// For regular text, show normal cell // For regular text, show normal cell
return ( return (
<div <div
className="cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600 p-2 rounded min-h-[32px] flex items-center" className="cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600 p-2 rounded min-h-[32px] flex items-center overflow-hidden"
onClick={() => onClick={() =>
setEditingCell({ setEditingCell({
rowId: row.id, rowId: row.id,
@@ -2511,11 +2567,13 @@ const TableEditor = () => {
isLongValue ? cellValue : undefined isLongValue ? cellValue : undefined
} }
> >
{cellValue || ( <span className="truncate block w-full">
<span className="text-gray-400 dark:text-gray-500 italic text-sm"> {cellValue || (
Click to edit <span className="text-gray-400 dark:text-gray-500 italic text-sm">
</span> Click to edit
)} </span>
)}
</span>
</div> </div>
); );
} }
@@ -2603,12 +2661,16 @@ const TableEditor = () => {
{/* Export Section */} {/* Export Section */}
{data.length > 0 && ( {data.length > 0 && (
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mt-6"> <div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mt-6">
{/* Export Header */} {/* Export Header - Collapsible */}
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700"> <div
onClick={() => setExportExpanded(!exportExpanded)}
className="px-4 py-3 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 justify-between">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2"> <h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Download className="h-5 w-5 text-green-600 dark:text-green-400" /> <Download className="h-5 w-5 text-green-600 dark:text-green-400" />
Export Results Export Results
{exportExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</h3> </h3>
<div className="text-sm text-gray-600 dark:text-gray-400"> <div className="text-sm text-gray-600 dark:text-gray-400">
{availableTables.length > 1 ? ( {availableTables.length > 1 ? (
@@ -2625,6 +2687,10 @@ const TableEditor = () => {
</div> </div>
</div> </div>
</div> </div>
{/* Export Content - Collapsible */}
{exportExpanded && (
<div>
{/* Export Tabs */} {/* Export Tabs */}
<div className="flex border-b border-gray-200 dark:border-gray-700"> <div className="flex border-b border-gray-200 dark:border-gray-700">
@@ -3001,15 +3067,25 @@ const TableEditor = () => {
</div> </div>
)} )}
</div> </div>
</div>
)}
</div> </div>
)} )}
{/* Usage Tips */} {/* Usage Tips */}
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md p-4 mt-6"> <div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md overflow-hidden mt-6">
<h4 className="text-blue-800 dark:text-blue-200 font-medium mb-3"> <div
Usage Tips onClick={() => setUsageTipsExpanded(!usageTipsExpanded)}
</h4> className="px-4 py-3 cursor-pointer hover:bg-blue-100 dark:hover:bg-blue-900/30 transition-colors flex items-center justify-between"
<div className="text-blue-700 dark:text-blue-300 text-sm space-y-2"> >
<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-2">
<div> <div>
<p className="font-medium mb-1">📝 Input Methods:</p> <p className="font-medium mb-1">📝 Input Methods:</p>
<ul className="list-disc list-inside space-y-1 ml-2"> <ul className="list-disc list-inside space-y-1 ml-2">
@@ -3097,6 +3173,7 @@ const TableEditor = () => {
</ul> </ul>
</div> </div>
</div> </div>
)}
</div> </div>
</ToolLayout> </ToolLayout>
); );
@@ -3197,6 +3274,77 @@ const ClearConfirmationModal = ({
// Object Editor Modal Component // Object Editor Modal Component
const ObjectEditorModal = ({ modal, onClose, onApply }) => { const ObjectEditorModal = ({ modal, onClose, onApply }) => {
// Initialize with parsed data immediately // Initialize with parsed data immediately
// PHP unserialize function (same as StructuredEditor)
const phpUnserialize = (str) => {
let index = 0;
const parseValue = () => {
if (index >= str.length) throw new Error('Unexpected end of string');
const type = str[index];
if (type === 'N') {
index += 2;
return null;
}
if (str[index + 1] !== ':') throw new Error(`Expected ':' after type '${type}'`);
index += 2;
switch (type) {
case 'b':
const boolVal = str[index] === '1';
index += 2;
return boolVal;
case 'i':
let intStr = '';
while (index < str.length && str[index] !== ';') intStr += str[index++];
index++;
return parseInt(intStr);
case 'd':
let floatStr = '';
while (index < str.length && str[index] !== ';') floatStr += str[index++];
index++;
return parseFloat(floatStr);
case 's':
let lenStr = '';
while (index < str.length && str[index] !== ':') lenStr += str[index++];
index++;
if (str[index] !== '"') throw new Error('Expected opening quote');
index++;
const byteLength = parseInt(lenStr);
if (byteLength === 0) {
index += 2;
return '';
}
let endQuotePos = -1;
for (let i = index; i < str.length - 1; i++) {
if (str[i] === '"' && str[i + 1] === ';') {
endQuotePos = i;
break;
}
}
if (endQuotePos === -1) throw new Error('Could not find closing quote');
const strValue = str.substring(index, endQuotePos);
index = endQuotePos + 2;
return strValue.replace(/\\"/g, '"').replace(/\\\\/g, '\\');
case 'a':
let countStr = '';
while (index < str.length && str[index] !== ':') countStr += str[index++];
const count = parseInt(countStr);
index += 2;
const result = {};
let isArray = true;
for (let i = 0; i < count; i++) {
const key = parseValue();
const value = parseValue();
result[key] = value;
if (key !== i) isArray = false;
}
index++;
return isArray ? Object.values(result) : result;
default:
throw new Error(`Unknown type: ${type}`);
}
};
return parseValue();
};
const initializeData = () => { const initializeData = () => {
try { try {
let data = modal.originalValue; let data = modal.originalValue;
@@ -3207,12 +3355,25 @@ const ObjectEditorModal = ({ modal, onClose, onApply }) => {
} }
if (modal.format.type === "php_serialized") { if (modal.format.type === "php_serialized") {
return { try {
structuredData: {}, console.log('Attempting to parse PHP serialized:', modal.originalValue);
currentValue: modal.originalValue, const parsed = phpUnserialize(modal.originalValue);
isValid: true, console.log('Parsed result:', parsed);
error: "", return {
}; structuredData: parsed,
currentValue: modal.originalValue,
isValid: true,
error: "",
};
} catch (err) {
console.error('PHP unserialize error:', err);
return {
structuredData: {},
currentValue: modal.originalValue,
isValid: false,
error: err.message,
};
}
} }
const parsed = JSON.parse(data); const parsed = JSON.parse(data);
@@ -3256,13 +3417,56 @@ const ObjectEditorModal = ({ modal, onClose, onApply }) => {
const [error, setError] = useState(initialState.error); const [error, setError] = useState(initialState.error);
// Debug log to see what we initialized with // Debug log to see what we initialized with
console.log('Modal initialized with:', {
structuredData,
isValid,
error,
format: modal.format.type
});
// PHP serialize function
const phpSerialize = (data) => {
if (data === null) return 'N;';
if (typeof data === 'boolean') return data ? 'b:1;' : 'b:0;';
if (typeof data === 'number') {
return Number.isInteger(data) ? `i:${data};` : `d:${data};`;
}
if (typeof data === 'string') {
const escapedData = data.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
const byteLength = new TextEncoder().encode(escapedData).length;
return `s:${byteLength}:"${escapedData}";`;
}
if (Array.isArray(data)) {
let result = `a:${data.length}:{`;
data.forEach((item, index) => {
result += phpSerialize(index) + phpSerialize(item);
});
result += '}';
return result;
}
if (typeof data === 'object') {
const keys = Object.keys(data);
let result = `a:${keys.length}:{`;
keys.forEach(key => {
result += phpSerialize(key) + phpSerialize(data[key]);
});
result += '}';
return result;
}
return 'N;';
};
// Update current value when structured data changes // Update current value when structured data changes
const handleStructuredDataChange = (newData) => { const handleStructuredDataChange = (newData) => {
setStructuredData(newData); setStructuredData(newData);
try { try {
const jsonString = JSON.stringify(newData, null, 2); if (modal.format.type === "php_serialized") {
setCurrentValue(jsonString); const serialized = phpSerialize(newData);
setCurrentValue(serialized);
} else {
const jsonString = JSON.stringify(newData, null, 2);
setCurrentValue(jsonString);
}
setIsValid(true); setIsValid(true);
setError(""); setError("");
} catch (err) { } catch (err) {
@@ -3283,8 +3487,15 @@ const ObjectEditorModal = ({ modal, onClose, onApply }) => {
} }
if (modal.format.type === "php_serialized") { if (modal.format.type === "php_serialized") {
setIsValid(true); try {
setError(""); const parsed = phpUnserialize(newValue);
setStructuredData(parsed);
setIsValid(true);
setError("");
} catch (err) {
setIsValid(false);
setError(err.message);
}
return; return;
} }
@@ -3331,16 +3542,6 @@ const ObjectEditorModal = ({ modal, onClose, onApply }) => {
); );
} }
if (modal.format.type === "php_serialized") {
return (
<div className="h-full flex items-center justify-center text-gray-500 dark:text-gray-400 p-6">
<div className="text-center">
<Code className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p>PHP Serialized data - use Raw Editor to modify</p>
</div>
</div>
);
}
return ( return (
<div className="h-full bg-white dark:bg-gray-800 p-6"> <div className="h-full bg-white dark:bg-gray-800 p-6">
@@ -3354,8 +3555,8 @@ const ObjectEditorModal = ({ modal, onClose, onApply }) => {
return ( return (
<div <div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[99999] p-4 !m-0"
style={{ minHeight: "100vh" }} style={{ minHeight: "100vh", marginTop: "0 !important" }}
> >
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-5xl w-full max-h-[90vh] overflow-hidden flex flex-col"> <div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-5xl w-full max-h-[90vh] overflow-hidden flex flex-col">
{/* Header */} {/* Header */}