feat: Invoice Editor improvements and code cleanup

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

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

View File

@@ -0,0 +1,99 @@
import React from 'react';
import CodeMirror from '@uiw/react-codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { json } from '@codemirror/lang-json';
import { html } from '@codemirror/lang-html';
import { css } from '@codemirror/lang-css';
import { oneDark } from '@codemirror/theme-one-dark';
import { EditorView } from '@codemirror/view';
const CodeEditor = ({
value,
onChange,
language = 'json',
placeholder = '',
readOnly = false,
height = '300px',
className = '',
theme = 'light'
}) => {
// Language extensions mapping
const getLanguageExtension = (lang) => {
switch (lang.toLowerCase()) {
case 'javascript':
case 'js':
return [javascript()];
case 'json':
return [json()];
case 'html':
return [html()];
case 'css':
return [css()];
default:
return [json()]; // Default to JSON
}
};
// Theme configuration
const getTheme = () => {
if (theme === 'dark') {
return oneDark;
}
return undefined; // Use default light theme
};
// Extensions
const extensions = [
...getLanguageExtension(language),
EditorView.theme({
'&': {
fontSize: '14px',
},
'.cm-content': {
padding: '16px',
minHeight: height,
},
'.cm-focused': {
outline: 'none',
},
'.cm-editor': {
borderRadius: '8px',
},
'.cm-scroller': {
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace',
},
}),
EditorView.lineWrapping,
];
return (
<div className={`border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden ${className}`}>
<CodeMirror
value={value}
onChange={onChange}
extensions={extensions}
theme={getTheme()}
placeholder={placeholder}
readOnly={readOnly}
basicSetup={{
lineNumbers: true,
foldGutter: true,
dropCursor: false,
allowMultipleSelections: false,
indentOnInput: true,
bracketMatching: true,
closeBrackets: true,
autocompletion: true,
highlightSelectionMatches: false,
searchKeymap: true,
}}
style={{
fontSize: '14px',
minHeight: height,
}}
/>
</div>
);
};
export default CodeEditor;

View File

@@ -1,8 +1,10 @@
import React, { useState, useEffect, useRef } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Home, Menu, X, ChevronDown, Terminal, Sparkles } from 'lucide-react';
import ThemeToggle from './ThemeToggle';
import { useLocation } from 'react-router-dom';
import ToolSidebar from './ToolSidebar';
import NavigationConfirmModal from './NavigationConfirmModal';
import useNavigationGuard from '../hooks/useNavigationGuard';
import { Menu, X, ChevronDown, Terminal, Sparkles, Home } from 'lucide-react';
import ThemeToggle from './ThemeToggle';
import SEOHead from './SEOHead';
import ConsentBanner from './ConsentBanner';
import { NON_TOOLS, TOOLS, SITE_CONFIG, getCategoryConfig } from '../config/tools';
@@ -10,6 +12,7 @@ import { useAnalytics } from '../hooks/useAnalytics';
const Layout = ({ children }) => {
const location = useLocation();
const { showModal, pendingNavigation, handleConfirm, handleCancel, hasUnsavedData, navigateWithGuard } = useNavigationGuard();
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const dropdownRef = useRef(null);
@@ -43,6 +46,9 @@ const Layout = ({ children }) => {
// Check if we're on a tool page (not homepage)
const isToolPage = location.pathname !== '/';
// Check if we're on invoice preview page (no sidebar needed)
const isInvoicePreviewPage = location.pathname === '/invoice-preview';
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 dark:from-slate-900 dark:via-slate-800 dark:to-indigo-900 flex flex-col">
@@ -50,10 +56,10 @@ const Layout = ({ children }) => {
<SEOHead />
{/* Header */}
<header className="sticky top-0 z-50 bg-white/80 dark:bg-slate-800/80 backdrop-blur-md shadow-lg border-b border-slate-200/50 dark:border-slate-700/50 flex-shrink-0">
<header className="fixed top-0 left-0 right-0 z-50 bg-white/80 dark:bg-slate-800/80 backdrop-blur-md shadow-lg border-b border-slate-200/50 dark:border-slate-700/50 flex-shrink-0">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<Link to="/" className="flex items-center space-x-3 group">
<button onClick={() => navigateWithGuard('/')} className="flex items-center space-x-3 group">
<div className="relative">
<div className="absolute inset-0 bg-gradient-to-r from-blue-500 to-purple-500 rounded-lg blur opacity-20 group-hover:opacity-40 transition-opacity"></div>
<div className="relative bg-gradient-to-r from-blue-500 to-purple-500 p-2 rounded-lg">
@@ -63,23 +69,26 @@ const Layout = ({ children }) => {
<span className="text-xl font-bold bg-gradient-to-r from-blue-600 via-purple-600 to-indigo-600 bg-clip-text text-transparent">
{SITE_CONFIG.title}
</span>
</Link>
</button>
<div className="flex items-center space-x-4">
{/* Desktop Navigation - only show on homepage */}
{!isToolPage && (
<nav className="hidden md:flex items-center space-x-6">
<Link
to="/"
<button
onClick={() => {
setIsDropdownOpen(false);
navigateWithGuard('/');
}}
className={`flex items-center space-x-2 px-4 py-2 rounded-xl text-sm font-medium transition-all duration-300 ${
isActive('/')
? 'bg-gradient-to-r from-blue-500 to-purple-500 text-white shadow-lg'
: 'text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white hover:bg-white/50 dark:hover:bg-slate-700/50'
: 'text-slate-600 dark:text-slate-300 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-white/50 dark:hover:bg-slate-700/50'
}`}
>
<Home className="h-4 w-4" />
<span>Home</span>
</Link>
</button>
{/* Tools Dropdown */}
<div className="relative" ref={dropdownRef}>
@@ -104,11 +113,13 @@ const Layout = ({ children }) => {
const categoryConfig = getCategoryConfig(tool.category);
return (
<Link
<button
key={tool.path}
to={tool.path}
onClick={() => setIsDropdownOpen(false)}
className={`group flex items-center space-x-4 px-4 py-3 text-sm hover:bg-white/50 dark:hover:bg-slate-700/50 transition-all duration-300 ${
onClick={() => {
setIsDropdownOpen(false);
navigateWithGuard(tool.path);
}}
className={`group flex items-center space-x-4 px-4 py-3 text-sm hover:bg-white/50 dark:hover:bg-slate-700/50 transition-all duration-300 w-full text-left ${
isActive(tool.path)
? 'bg-gradient-to-r from-blue-50 to-purple-50 dark:from-slate-700 dark:to-slate-600 text-blue-700 dark:text-blue-300'
: 'text-slate-700 dark:text-slate-300'
@@ -124,7 +135,7 @@ const Layout = ({ children }) => {
<div className="opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<ChevronDown className="h-4 w-4 -rotate-90 text-slate-400" />
</div>
</Link>
</button>
);
})}
</div>
@@ -158,7 +169,7 @@ const Layout = ({ children }) => {
/>
{/* Menu */}
<div className="md:hidden fixed top-16 left-0 right-0 z-40 bg-white/95 dark:bg-slate-800/95 backdrop-blur-md border-b border-slate-200/50 dark:border-slate-700/50 shadow-lg max-h-[calc(100vh-4rem)] overflow-y-auto">
<div className="md:hidden fixed top-16 left-0 right-0 z-40 bg-white/95 dark:bg-slate-800/95 backdrop-blur-md border-b border-slate-200/50 dark:border-slate-700/50 shadow-lg max-h-[calc(100vh-4rem)]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="space-y-2">
{/* Non-Tools Section */}
@@ -166,11 +177,13 @@ const Layout = ({ children }) => {
const IconComponent = tool.icon;
return (
<Link
<button
key={tool.path}
to={tool.path}
onClick={() => setIsMobileMenuOpen(false)}
className={`flex items-center space-x-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300 ${
onClick={() => {
setIsMobileMenuOpen(false);
navigateWithGuard(tool.path);
}}
className={`flex items-center space-x-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300 w-full text-left ${
isActive(tool.path)
? 'bg-gradient-to-r from-indigo-500 to-purple-500 text-white shadow-lg'
: 'text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white hover:bg-white/50 dark:hover:bg-slate-700/50'
@@ -180,7 +193,7 @@ const Layout = ({ children }) => {
<IconComponent className={`h-4 w-4 ${isActive(tool.path) ? 'text-white' : 'text-white'}`} />
</div>
<span>{tool.name}</span>
</Link>
</button>
);
})}
@@ -194,11 +207,13 @@ const Layout = ({ children }) => {
const categoryConfig = getCategoryConfig(tool.category);
return (
<Link
<button
key={tool.path}
to={tool.path}
onClick={() => setIsMobileMenuOpen(false)}
className={`flex items-center space-x-4 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300 ${
onClick={() => {
setIsMobileMenuOpen(false);
navigateWithGuard(tool.path);
}}
className={`flex items-center space-x-4 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300 w-full text-left ${
isActive(tool.path)
? 'bg-gradient-to-r from-blue-50 to-purple-50 dark:from-slate-700 dark:to-slate-600 text-blue-700 dark:text-blue-300'
: 'text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white hover:bg-white/50 dark:hover:bg-slate-700/50'
@@ -211,7 +226,7 @@ const Layout = ({ children }) => {
<div className="font-medium">{tool.name}</div>
<div className="text-xs text-slate-500 dark:text-slate-400">{tool.description}</div>
</div>
</Link>
</button>
);
})}
</div>
@@ -222,99 +237,152 @@ const Layout = ({ children }) => {
)}
{/* Main Content */}
<div className="flex flex-1">
{/* Tool Sidebar - only show on tool pages */}
{isToolPage && (
<div className="hidden lg:block flex-shrink-0">
</div>
)}
<div className="flex flex-1 pt-16">
{/* Main Content Area */}
<main className="flex-1 flex">
{isToolPage ? (
<div className="flex flex-1">
<ToolSidebar />
<div className="flex-1 p-6">
<main className="flex-1 flex flex-col">
{isToolPage && !isInvoicePreviewPage ? (
<div className="block">
<div className="hidden lg:block fixed top-16 left-0 z-[9999]">
<ToolSidebar navigateWithGuard={navigateWithGuard} />
</div>
<div className="flex-1 flex flex-col min-h-0 pl-0 lg:pl-16">
<div className="flex-1 p-4 sm:p-6 w-full min-w-0 overflow-auto">
{children}
</div>
</div>
</div>
) : isInvoicePreviewPage ? (
<div className="flex-1 flex flex-col">
<div className="flex-1">
{children}
</div>
</div>
) : (
<div className="flex-1">
{children}
<div className="flex-1 flex flex-col">
<div className="flex-1">
{children}
</div>
{/* Global Footer for Homepage */}
<footer className="bg-white/30 dark:bg-slate-800/30 backdrop-blur-sm border-t border-slate-200/30 dark:border-slate-700/30 mt-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="text-center">
<div className="flex items-center justify-center gap-3 mb-4">
<div className="relative">
<div className="absolute inset-0 bg-gradient-to-r from-blue-500 to-purple-500 rounded-lg blur opacity-20"></div>
<div className="relative bg-gradient-to-r from-blue-500 to-purple-500 p-2 rounded-lg">
<Terminal className="h-5 w-5 text-white" />
</div>
</div>
<span className="text-lg font-bold bg-gradient-to-r from-blue-600 via-purple-600 to-indigo-600 bg-clip-text text-transparent">
{SITE_CONFIG.title}
</span>
</div>
<div className="flex items-center justify-center gap-2 mb-3">
<div className="w-2 h-2 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full"></div>
<span className="text-sm font-medium text-slate-600 dark:text-slate-400">
© {SITE_CONFIG.year} {SITE_CONFIG.title}
</span>
<div className="w-2 h-2 bg-gradient-to-r from-purple-500 to-indigo-500 rounded-full"></div>
</div>
<p className="text-sm text-slate-500 dark:text-slate-500 mb-4">
Built with for developers worldwide
</p>
<div className="flex flex-col items-center gap-4">
<div className="flex justify-center items-center gap-6 text-xs text-slate-400 dark:text-slate-500">
<div className="flex items-center gap-1">
<div className="w-1.5 h-1.5 bg-green-500 rounded-full animate-pulse"></div>
<span>100% Client-Side</span>
</div>
<div className="flex items-center gap-1">
<div className="w-1.5 h-1.5 bg-blue-500 rounded-full"></div>
<span>Privacy First</span>
</div>
<div className="flex items-center gap-1">
<div className="w-1.5 h-1.5 bg-purple-500 rounded-full"></div>
<span>Open Source</span>
</div>
</div>
<div className="flex items-center gap-4 text-xs">
<button
onClick={() => navigateWithGuard('/release-notes')}
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Release Notes
</button>
<span className="text-slate-300 dark:text-slate-600"></span>
<button
onClick={() => navigateWithGuard('/privacy')}
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Privacy Policy
</button>
<span className="text-slate-300 dark:text-slate-600"></span>
<button
onClick={() => navigateWithGuard('/terms')}
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Terms of Service
</button>
</div>
</div>
</div>
</div>
</footer>
</div>
)}
</main>
</div>
{/* Global Footer */}
<footer className="bg-white/30 dark:bg-slate-800/30 backdrop-blur-sm border-t border-slate-200/30 dark:border-slate-700/30 mt-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="text-center">
<div className="flex items-center justify-center gap-3 mb-4">
<div className="relative">
<div className="absolute inset-0 bg-gradient-to-r from-blue-500 to-purple-500 rounded-lg blur opacity-20"></div>
<div className="relative bg-gradient-to-r from-blue-500 to-purple-500 p-2 rounded-lg">
<Terminal className="h-5 w-5 text-white" />
</div>
{/* Footer for Tool Pages */}
{isToolPage && (
<footer className="bg-white/30 dark:bg-slate-800/30 backdrop-blur-sm border-t border-slate-200/30 dark:border-slate-700/30">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="text-center">
<div className="flex items-center justify-center gap-2 mb-2">
<div className="w-2 h-2 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full"></div>
<span className="text-xs font-medium text-slate-600 dark:text-slate-400">
© {SITE_CONFIG.year} {SITE_CONFIG.title}
</span>
<div className="w-2 h-2 bg-gradient-to-r from-purple-500 to-indigo-500 rounded-full"></div>
</div>
<span className="text-lg font-bold bg-gradient-to-r from-blue-600 via-purple-600 to-indigo-600 bg-clip-text text-transparent">
{SITE_CONFIG.title}
</span>
</div>
<div className="flex items-center justify-center gap-2 mb-3">
<div className="w-2 h-2 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full"></div>
<span className="text-sm font-medium text-slate-600 dark:text-slate-400">
© {SITE_CONFIG.year} {SITE_CONFIG.title}
</span>
<div className="w-2 h-2 bg-gradient-to-r from-purple-500 to-indigo-500 rounded-full"></div>
</div>
<p className="text-sm text-slate-500 dark:text-slate-500 mb-4">
Built with for developers worldwide
</p>
<div className="flex flex-col items-center gap-4">
<div className="flex justify-center items-center gap-6 text-xs text-slate-400 dark:text-slate-500">
<div className="flex items-center gap-1">
<div className="w-1.5 h-1.5 bg-green-500 rounded-full animate-pulse"></div>
<span>100% Client-Side</span>
</div>
<div className="flex items-center gap-1">
<div className="w-1.5 h-1.5 bg-blue-500 rounded-full"></div>
<span>Privacy First</span>
</div>
<div className="flex items-center gap-1">
<div className="w-1.5 h-1.5 bg-purple-500 rounded-full"></div>
<span>Open Source</span>
</div>
</div>
<div className="flex items-center gap-4 text-xs">
<Link
to="/release-notes"
<div className="flex items-center justify-center gap-4 text-xs">
<button
onClick={() => navigateWithGuard('/release-notes')}
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Release Notes
</Link>
</button>
<span className="text-slate-300 dark:text-slate-600"></span>
<Link
to="/privacy"
<button
onClick={() => navigateWithGuard('/privacy')}
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Privacy Policy
</Link>
</button>
<span className="text-slate-300 dark:text-slate-600"></span>
<Link
to="/terms"
<button
onClick={() => navigateWithGuard('/terms')}
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Terms of Service
</Link>
</button>
</div>
</div>
</div>
</div>
</footer>
</footer>
)}
{/* GDPR Consent Banner */}
<ConsentBanner />
{/* Navigation Confirmation Modal */}
<NavigationConfirmModal
isOpen={showModal}
onConfirm={handleConfirm}
onCancel={handleCancel}
targetPath={pendingNavigation?.to}
hasData={hasUnsavedData}
/>
</div>
);
};

View File

@@ -0,0 +1,115 @@
import React from 'react';
import { AlertTriangle } from 'lucide-react';
const NavigationConfirmModal = ({ isOpen, onConfirm, onCancel, targetPath, hasData }) => {
if (!isOpen) return null;
const getDataSummary = () => {
try {
const invoiceData = localStorage.getItem('currentInvoice');
const objectData = localStorage.getItem('objectEditorData');
const tableData = localStorage.getItem('tableEditorData');
const summary = [];
if (invoiceData) {
const parsed = JSON.parse(invoiceData);
if (parsed.invoiceNumber) summary.push(`Invoice #${parsed.invoiceNumber}`);
if (parsed.company?.name) summary.push(`Company information (${parsed.company.name})`);
if (parsed.client?.name) summary.push(`Client information (${parsed.client.name})`);
if (parsed.items?.length > 0) summary.push(`${parsed.items.length} line items`);
}
if (objectData) {
const parsed = JSON.parse(objectData);
if (parsed && Object.keys(parsed).length > 0) {
summary.push(`Object data with ${Object.keys(parsed).length} properties`);
}
}
if (tableData) {
const parsed = JSON.parse(tableData);
if (parsed && parsed.length > 0) {
summary.push(`Table data with ${parsed.length} rows`);
}
}
return summary;
} catch (error) {
return ['Unsaved data'];
}
};
const dataSummary = getDataSummary();
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 */}
<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-900 dark:text-amber-100">
Confirm Navigation
</h3>
<p className="text-sm text-amber-700 dark:text-amber-300">
You have unsaved data that will be lost
</p>
</div>
</div>
</div>
{/* Content */}
<div className="px-6 py-4">
<p className="text-gray-700 dark:text-gray-300 mb-4">
You currently have unsaved data that will be lost if you leave this page. Are you sure you want to continue?
</p>
{dataSummary.length > 0 && (
<div className="bg-gray-50 dark:bg-gray-700 rounded-md p-3 mb-4">
<p className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
You currently have:
</p>
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
{dataSummary.map((item, index) => (
<li key={index} className="flex items-center">
<span className="w-1.5 h-1.5 bg-amber-500 rounded-full mr-2 flex-shrink-0"></span>
{item}
</li>
))}
</ul>
</div>
)}
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md p-3">
<p className="text-blue-800 dark:text-blue-200 text-sm">
<strong>Tip:</strong> Consider saving or exporting your current work before proceeding.
</p>
</div>
</div>
{/* Footer */}
<div className="px-6 py-4 bg-gray-50 dark:bg-gray-700 border-t border-gray-200 dark:border-gray-600 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" />
Continue & Lose Data
</button>
</div>
</div>
</div>
);
};
export default NavigationConfirmModal;

View File

@@ -224,11 +224,20 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
const renderValue = (value) => {
const typeStyle = getTypeStyle(value);
const formattedValue = formatValue(value);
const hasHtml = isHtmlContent(value);
return (
<span className={`inline-flex items-center space-x-1 px-2 py-1 rounded text-xs font-medium ${typeStyle.color}`}>
{typeStyle.icon}
<span>{formattedValue}</span>
<span className="flex-shrink-0 w-3 h-3 flex items-center justify-center">
{typeStyle.icon}
</span>
<span>
{hasHtml && renderHtml ? (
<div dangerouslySetInnerHTML={{ __html: String(value) }} />
) : (
formattedValue
)}
</span>
</span>
);
};
@@ -241,8 +250,10 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
return (
<div className="relative">
<span className={`inline-flex items-start space-x-2 px-2 py-1 rounded text-xs font-medium ${typeStyle.color}`}>
<span className="flex-shrink-0 mt-0.5">{typeStyle.icon}</span>
<span className={`inline-flex items-center space-x-2 px-2 py-1 rounded text-xs font-medium ${typeStyle.color}`}>
<span className="flex-shrink-0 w-3 h-3 flex items-center justify-center">
{typeStyle.icon}
</span>
<span className="whitespace-pre-wrap break-words flex-1">
{hasHtml && renderHtml ? (
<div dangerouslySetInnerHTML={{ __html: String(value) }} />
@@ -252,33 +263,6 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
</span>
</span>
{/* HTML Toggle Buttons */}
{hasHtml && (
<div className="absolute -top-1 -right-1 flex">
<button
onClick={() => setRenderHtml(true)}
className={`px-1.5 py-0.5 text-xs rounded-l ${
renderHtml
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-600 hover:bg-gray-300 dark:bg-gray-600 dark:text-gray-300 dark:hover:bg-gray-500'
}`}
title="Render HTML"
>
<Eye className="h-3 w-3" />
</button>
<button
onClick={() => setRenderHtml(false)}
className={`px-1.5 py-0.5 text-xs rounded-r ${
!renderHtml
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-600 hover:bg-gray-300 dark:bg-gray-600 dark:text-gray-300 dark:hover:bg-gray-500'
}`}
title="Show Raw HTML"
>
<Code className="h-3 w-3" />
</button>
</div>
)}
</div>
);
};
@@ -324,15 +308,46 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
))}
</div>
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{isArrayView && `${currentData.length} items`}
{isObjectView && `${Object.keys(currentData).length} properties`}
<div className="flex items-center justify-between">
<div className="text-sm text-gray-500 dark:text-gray-400">
{isArrayView && `${currentData.length} items`}
{isObjectView && `${Object.keys(currentData).length} properties`}
</div>
{/* Global HTML/Raw Toggle */}
<div className="flex items-center space-x-2">
<span className="text-xs text-gray-500 dark:text-gray-400">Text Display:</span>
<div className="flex rounded-md overflow-hidden border border-gray-300 dark:border-gray-600">
<button
onClick={() => setRenderHtml(true)}
className={`px-2 py-1 text-xs transition-colors ${
renderHtml
? 'bg-blue-500 text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-gray-600 dark:text-gray-300 dark:hover:bg-gray-500'
}`}
title="Render HTML"
>
<Eye className="h-3 w-3" />
</button>
<button
onClick={() => setRenderHtml(false)}
className={`px-2 py-1 text-xs transition-colors ${
!renderHtml
? 'bg-blue-500 text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-gray-600 dark:text-gray-300 dark:hover:bg-gray-500'
}`}
title="Show Raw Text"
>
<Code className="h-3 w-3" />
</button>
</div>
</div>
</div>
</div>
</div>
{/* Content */}
<div className="overflow-auto max-h-96">
<div className="overflow-auto">
{isArrayView ? (
// Horizontal table for arrays
<table className="w-full">
@@ -439,9 +454,6 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
<div className="text-lg font-mono text-gray-900 dark:text-gray-100 whitespace-pre-wrap break-words">
{formatFullValue(currentData)}
</div>
<div className="text-sm mt-2">
Type: {getValueType(currentData)}
</div>
</div>
</div>
)}

View File

@@ -2,29 +2,29 @@ import React from 'react';
const ToolLayout = ({ title, description, children, icon: Icon }) => {
return (
<div className="max-w-6xl mx-auto">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 w-full" style={{ maxWidth: 'min(80rem, calc(100vw - 2rem))' }}>
{/* Header */}
<div className="mb-8">
<div className="flex items-center space-x-3 mb-2">
{Icon && <Icon className="h-8 w-8 text-primary-600" />}
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
<div className="mb-6 sm:mb-8">
<div className="flex items-center space-x-2 sm:space-x-3 mb-2">
{Icon && <Icon className="h-6 w-6 sm:h-8 sm:w-8 text-primary-600 flex-shrink-0" />}
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-white truncate">
{title}
</h1>
</div>
{description && (
<p className="text-gray-600 dark:text-gray-300 text-lg">
<p className="text-gray-600 dark:text-gray-300 text-base sm:text-lg">
{description}
</p>
)}
</div>
{/* Tool Content */}
<div className="space-y-6">
<div className="space-y-4 sm:space-y-6 w-full">
{children}
</div>
</div>
);
};
export default ToolLayout;
export default ToolLayout;

View File

@@ -1,12 +1,25 @@
import React, { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Search, ChevronLeft, ChevronRight, Sparkles } from 'lucide-react';
import { NON_TOOLS, TOOLS, SITE_CONFIG } from '../config/tools';
import React, { useState, useRef } from 'react';
import { useLocation } from 'react-router-dom';
import { Search, ChevronLeft, ChevronRight, Sparkles, ChevronDown, ChevronUp } from 'lucide-react';
import { NON_TOOLS, TOOLS, SITE_CONFIG, getCategoryConfig } from '../config/tools';
import useNavigationGuard from '../hooks/useNavigationGuard';
const ToolSidebar = () => {
const ToolSidebar = ({ navigateWithGuard: propNavigateWithGuard }) => {
const location = useLocation();
const { navigateWithGuard: hookNavigateWithGuard } = useNavigationGuard();
// Use prop navigation guard if provided, otherwise use hook
const navigateWithGuard = propNavigateWithGuard || hookNavigateWithGuard;
const [isCollapsed, setIsCollapsed] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [expandedCategories, setExpandedCategories] = useState({
editor: false,
encoder: false,
formatter: false,
analyzer: false
});
const [hoveredTooltip, setHoveredTooltip] = useState(null);
const tooltipTimeoutRef = useRef(null);
// Filter non-tools and tools separately
const filteredNonTools = NON_TOOLS.filter(tool =>
@@ -21,10 +34,64 @@ const ToolSidebar = () => {
const isActive = (path) => location.pathname === path;
// Toggle category expansion - close others when opening one
const toggleCategory = (categoryKey) => {
setExpandedCategories(prev => {
const isCurrentlyExpanded = prev[categoryKey];
if (isCurrentlyExpanded) {
// If currently expanded, just close it
return {
...prev,
[categoryKey]: false
};
} else {
// If currently closed, close all others and open this one
const newState = {
editor: false,
encoder: false,
formatter: false,
analyzer: false
};
newState[categoryKey] = true;
return newState;
}
});
};
// Tooltip hover handlers
const handleTooltipMouseEnter = (categoryKey) => {
if (tooltipTimeoutRef.current) {
clearTimeout(tooltipTimeoutRef.current);
}
setHoveredTooltip(categoryKey);
};
const handleTooltipMouseLeave = () => {
tooltipTimeoutRef.current = setTimeout(() => {
setHoveredTooltip(null);
}, 300); // 300ms delay before hiding
};
// Handle navigation with data validation
const handleNavigation = (path, event) => {
event.preventDefault();
navigateWithGuard(path);
};
// Group tools by category
const toolsByCategory = {};
filteredTools.forEach(tool => {
if (!toolsByCategory[tool.category]) {
toolsByCategory[tool.category] = [];
}
toolsByCategory[tool.category].push(tool);
});
return (
<div className={`bg-white/70 dark:bg-slate-800/70 backdrop-blur-sm border-r border-slate-200/50 dark:border-slate-700/50 transition-all duration-300 sticky top-16 ${
<div className={`bg-white/70 dark:bg-slate-800/70 backdrop-blur-sm border-r border-slate-200/50 dark:border-slate-700/50 transition-all duration-300 ${
isCollapsed ? 'w-16' : 'w-64'
}`} style={{ height: 'calc(100vh - 4rem)' }}>
} sticky top-16 ${isCollapsed ? 'overflow-visible' : 'overflow-hidden'}`} style={{ height: 'calc(100vh - 4rem)' }}>
<div className="h-full flex flex-col">
{/* Sidebar Header */}
<div className="p-4 border-b border-slate-200/50 dark:border-slate-700/50">
@@ -68,60 +135,83 @@ const ToolSidebar = () => {
</div>
{/* Tools List */}
<div className="flex-1 overflow-y-auto py-3">
<div className={`flex-1 py-3 ${isCollapsed ? 'overflow-visible' : 'overflow-y-auto'}`}>
<nav className="space-y-2 px-3">
{/* Render Non-Tools (Home, What's New) */}
{/* Render Non-Tools (Home, What's New) with special styling */}
{filteredNonTools.map((tool) => {
const IconComponent = tool.icon;
const isActiveItem = isActive(tool.path);
const isWhatsNew = tool.path === '/release-notes';
return (
<Link
<a
key={tool.path}
to={tool.path}
className={`group flex items-center text-sm font-medium rounded-xl transition-all duration-300 ${
href={tool.path}
onClick={(e) => handleNavigation(tool.path, e)}
className={`group flex items-center text-sm font-medium rounded-xl transition-all duration-300 cursor-pointer ${
isActiveItem
? isCollapsed
? ' justify-center py-3' // Center for folded
: 'bg-gradient-to-r from-indigo-50 to-indigo-100 dark:from-indigo-900/30 dark:to-indigo-800/30 shadow-lg px-3 py-3'
? ' justify-center py-3'
: isWhatsNew
? 'bg-gradient-to-r from-amber-50 to-yellow-50 dark:from-amber-900/30 dark:to-yellow-800/30 shadow-lg px-3 py-3 border-2 border-amber-200 dark:border-amber-700'
: 'bg-gradient-to-r from-indigo-50 to-indigo-100 dark:from-indigo-900/30 dark:to-indigo-800/30 shadow-lg px-3 py-3'
: isCollapsed
? ' justify-center py-3' // Center for folded
: 'hover:bg-white/50 dark:hover:bg-slate-700/50 px-3 py-3'
? ' justify-center py-3'
: isWhatsNew
? 'hover:bg-gradient-to-r hover:from-amber-50 hover:to-yellow-50 dark:hover:from-amber-900/20 dark:hover:to-yellow-800/20 px-3 py-3 border border-amber-200/50 dark:border-amber-700/50'
: 'hover:bg-white/50 dark:hover:bg-slate-700/50 px-3 py-3'
}`}
title={isCollapsed ? tool.name : ''}
>
{isCollapsed ? (
// Folded sidebar - clean icon squares only, centered
<div className={`rounded-lg shadow-sm group-hover:scale-110 transition-transform duration-300 ${
isActiveItem
? 'bg-gradient-to-br from-indigo-500 to-purple-500 p-3' // Active: bigger padding (no border)
: 'border-2 border-indigo-300 dark:border-indigo-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-indigo-500 group-hover:to-purple-500 p-2' // Inactive: normal padding (has border)
? isWhatsNew
? 'bg-gradient-to-br from-amber-500 to-yellow-500 p-3'
: 'bg-gradient-to-br from-indigo-500 to-purple-500 p-3'
: isWhatsNew
? 'border-2 border-amber-300 dark:border-amber-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-amber-500 group-hover:to-yellow-500 p-2'
: 'border-2 border-indigo-300 dark:border-indigo-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-indigo-500 group-hover:to-purple-500 p-2'
}`}>
<IconComponent className={`${
isActiveItem
? 'h-5 w-5 text-white' // Active: bigger icon, white
: 'h-4 w-4 text-slate-500 dark:text-slate-400 group-hover:text-white' // Inactive: normal size, grayscale/hover
? 'h-5 w-5 text-white'
: 'h-4 w-4 text-slate-500 dark:text-slate-400 group-hover:text-white'
}`} />
</div>
) : (
// Expanded sidebar
<>
<div className={`p-2 rounded-lg shadow-sm group-hover:scale-110 transition-transform duration-300 mr-3 flex-shrink-0 ${
isActiveItem
? 'bg-gradient-to-br from-indigo-500 to-purple-500' // Active: colored background
: 'border-2 border-indigo-300 dark:border-indigo-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-indigo-500 group-hover:to-purple-500' // Inactive: transparent with colored border
? isWhatsNew
? 'bg-gradient-to-br from-amber-500 to-yellow-500'
: 'bg-gradient-to-br from-indigo-500 to-purple-500'
: isWhatsNew
? 'border-2 border-amber-300 dark:border-amber-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-amber-500 group-hover:to-yellow-500'
: 'border-2 border-indigo-300 dark:border-indigo-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-indigo-500 group-hover:to-purple-500'
}`}>
<IconComponent className={`h-4 w-4 ${
isActiveItem
? 'text-white' // Active: white icon
: 'text-slate-500 dark:text-slate-400 group-hover:text-white' // Inactive: grayscale icon
? 'text-white'
: 'text-slate-500 dark:text-slate-400 group-hover:text-white'
}`} />
</div>
<div className="flex-1 min-w-0">
<div className={`font-medium truncate ${
isActiveItem ? 'text-indigo-700 dark:text-indigo-300' : 'text-slate-500 dark:text-slate-400 group-hover:text-indigo-600 dark:group-hover:text-indigo-400'
isActiveItem
? isWhatsNew
? 'text-amber-700 dark:text-amber-300'
: 'text-indigo-700 dark:text-indigo-300'
: isWhatsNew
? 'text-slate-500 dark:text-slate-400 group-hover:text-amber-600 dark:group-hover:text-amber-400'
: 'text-slate-500 dark:text-slate-400 group-hover:text-indigo-600 dark:group-hover:text-indigo-400'
}`}>
{tool.name}
{isWhatsNew && !isCollapsed && (
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300">
New
</span>
)}
</div>
<div className="text-xs text-slate-500 dark:text-slate-400 truncate">
{tool.description}
@@ -129,199 +219,191 @@ const ToolSidebar = () => {
</div>
</>
)}
</Link>
</a>
);
})}
{/* Separator between non-tools and tools */}
{!isCollapsed && filteredNonTools.length > 0 && filteredTools.length > 0 && (
<div className="border-t border-slate-200/50 dark:border-slate-700/50 my-3"></div>
{!isCollapsed && filteredNonTools.length > 0 && Object.keys(toolsByCategory).length > 0 && (
<div className="border-t border-slate-200/50 dark:border-slate-700/50 my-4"></div>
)}
{/* Render Tools */}
{filteredTools.map((tool) => {
const IconComponent = tool.icon;
const isActiveItem = isActive(tool.path);
const isHome = tool.path === '/';
// Get category-specific colors for active states
const getActiveClasses = (category, isHome) => {
if (isHome) {
return {
collapsed: '', // No background for folded active items
expanded: 'bg-gradient-to-r from-slate-50 to-slate-100 dark:from-slate-700 dark:to-slate-600',
titleColor: 'text-slate-700 dark:text-slate-300',
iconBg: 'bg-gradient-to-br from-slate-500 to-slate-600' // Active icon has colored background
};
}
switch (category) {
case 'editor':
return {
collapsed: '', // No background for folded active items
expanded: 'bg-gradient-to-r from-blue-50 to-blue-100 dark:from-blue-900/30 dark:to-blue-800/30',
titleColor: 'text-blue-700 dark:text-blue-300',
iconBg: 'bg-gradient-to-br from-blue-500 to-cyan-500'
};
case 'encoder':
return {
collapsed: '', // No background for folded active items
expanded: 'bg-gradient-to-r from-purple-50 to-purple-100 dark:from-purple-900/30 dark:to-purple-800/30',
titleColor: 'text-purple-700 dark:text-purple-300',
iconBg: 'bg-gradient-to-br from-purple-500 to-pink-500'
};
case 'formatter':
return {
collapsed: '', // No background for folded active items
expanded: 'bg-gradient-to-r from-green-50 to-green-100 dark:from-green-900/30 dark:to-green-800/30',
titleColor: 'text-green-700 dark:text-green-300',
iconBg: 'bg-gradient-to-br from-green-500 to-emerald-500'
};
case 'analyzer':
return {
collapsed: '', // No background for folded active items
expanded: 'bg-gradient-to-r from-orange-50 to-orange-100 dark:from-orange-900/30 dark:to-orange-800/30',
titleColor: 'text-orange-700 dark:text-orange-300',
iconBg: 'bg-gradient-to-br from-orange-500 to-red-500'
};
case 'non_tools':
return {
collapsed: '', // No background for folded active items
expanded: 'bg-gradient-to-r from-indigo-50 to-indigo-100 dark:from-indigo-900/30 dark:to-indigo-800/30',
titleColor: 'text-indigo-700 dark:text-indigo-300',
iconBg: 'bg-gradient-to-br from-indigo-500 to-purple-500'
};
default:
return {
collapsed: '', // No background for folded active items
expanded: 'bg-gradient-to-r from-slate-50 to-slate-100 dark:from-slate-700 dark:to-slate-600',
titleColor: 'text-slate-700 dark:text-slate-300',
iconBg: 'bg-gradient-to-br from-slate-500 to-slate-600'
};
}
};
const getInactiveClasses = (category, isHome) => {
if (isHome) {
return {
collapsed: '', // No background for folded inactive items
expanded: 'hover:bg-white/50 dark:hover:bg-slate-700/50',
titleColor: 'text-slate-500 dark:text-slate-400 group-hover:text-slate-700 dark:group-hover:text-slate-300',
iconBorder: 'border-2 border-slate-300 dark:border-slate-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-slate-500 group-hover:to-slate-600', // Hover: colored background
iconColor: 'text-slate-500 dark:text-slate-400 group-hover:text-white' // Hover: white icon
};
}
switch (category) {
case 'editor':
return {
collapsed: '', // No background for folded inactive items
expanded: 'hover:bg-white/50 dark:hover:bg-slate-700/50',
titleColor: 'text-slate-500 dark:text-slate-400 group-hover:text-blue-600 dark:group-hover:text-blue-400',
iconBorder: 'border-2 border-blue-300 dark:border-blue-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-blue-500 group-hover:to-cyan-500',
iconColor: 'text-slate-500 dark:text-slate-400 group-hover:text-white'
};
case 'encoder':
return {
collapsed: '', // No background for folded inactive items
expanded: 'hover:bg-white/50 dark:hover:bg-slate-700/50',
titleColor: 'text-slate-500 dark:text-slate-400 group-hover:text-purple-600 dark:group-hover:text-purple-400',
iconBorder: 'border-2 border-purple-300 dark:border-purple-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-purple-500 group-hover:to-pink-500',
iconColor: 'text-slate-500 dark:text-slate-400 group-hover:text-white'
};
case 'formatter':
return {
collapsed: '', // No background for folded inactive items
expanded: 'hover:bg-white/50 dark:hover:bg-slate-700/50',
titleColor: 'text-slate-500 dark:text-slate-400 group-hover:text-green-600 dark:group-hover:text-green-400',
iconBorder: 'border-2 border-green-300 dark:border-green-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-green-500 group-hover:to-emerald-500',
iconColor: 'text-slate-500 dark:text-slate-400 group-hover:text-white'
};
case 'analyzer':
return {
collapsed: '', // No background for folded inactive items
expanded: 'hover:bg-white/50 dark:hover:bg-slate-700/50',
titleColor: 'text-slate-500 dark:text-slate-400 group-hover:text-orange-600 dark:group-hover:text-orange-400',
iconBorder: 'border-2 border-orange-300 dark:border-orange-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-orange-500 group-hover:to-red-500',
iconColor: 'text-slate-500 dark:text-slate-400 group-hover:text-white'
};
case 'non_tools':
return {
collapsed: '', // No background for folded inactive items
expanded: 'hover:bg-white/50 dark:hover:bg-slate-700/50',
titleColor: 'text-slate-500 dark:text-slate-400 group-hover:text-indigo-600 dark:group-hover:text-indigo-400',
iconBorder: 'border-2 border-indigo-300 dark:border-indigo-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-indigo-500 group-hover:to-purple-500',
iconColor: 'text-slate-500 dark:text-slate-400 group-hover:text-white'
};
default:
return {
collapsed: '', // No background for folded inactive items
expanded: 'hover:bg-white/50 dark:hover:bg-slate-700/50',
titleColor: 'text-slate-500 dark:text-slate-400 group-hover:text-slate-700 dark:group-hover:text-slate-300',
iconBorder: 'border-2 border-slate-300 dark:border-slate-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-slate-500 group-hover:to-slate-600',
iconColor: 'text-slate-500 dark:text-slate-400 group-hover:text-white'
};
}
};
const activeClasses = getActiveClasses(tool.category, isHome);
const inactiveClasses = getInactiveClasses(tool.category, isHome);
{/* Render Tools by Category */}
{!isCollapsed && Object.entries(toolsByCategory).map(([categoryKey, tools]) => {
const categoryConfig = getCategoryConfig(categoryKey);
const isExpanded = expandedCategories[categoryKey];
return (
<Link
key={tool.path}
to={tool.path}
className={`group flex items-center text-sm font-medium rounded-xl transition-all duration-300 ${
isActiveItem
? isCollapsed
? activeClasses.collapsed + ' justify-center py-3' // Center for folded
: activeClasses.expanded + ' shadow-lg px-3 py-3'
: isCollapsed
? inactiveClasses.collapsed + ' justify-center py-3' // Center for folded
: inactiveClasses.expanded + ' px-3 py-3'
}`}
title={isCollapsed ? tool.name : ''}
>
{isCollapsed ? (
// Folded sidebar - clean icon squares only, centered
<div className={`rounded-lg shadow-sm group-hover:scale-110 transition-transform duration-300 ${
isActiveItem
? activeClasses.iconBg + ' p-3' // Active: bigger padding (no border)
: inactiveClasses.iconBorder + ' p-2' // Inactive: normal padding (has border)
}`}>
<IconComponent className={`${
isActiveItem
? 'h-5 w-5 text-white' // Active: bigger icon, white
: 'h-4 w-4 ' + inactiveClasses.iconColor // Inactive: normal size, grayscale/hover
}`} />
<div key={categoryKey} className="mb-2">
{/* Category Header */}
<button
onClick={() => toggleCategory(categoryKey)}
className="w-full flex items-center justify-between px-3 py-2 text-xs font-semibold text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 transition-colors rounded-lg hover:bg-white/50 dark:hover:bg-slate-700/50"
>
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full bg-gradient-to-r ${categoryConfig.color}`}></div>
<span className="uppercase tracking-wider">{categoryConfig.name}</span>
<span className="text-slate-400 dark:text-slate-500">({tools.length})</span>
</div>
{isExpanded ? (
<ChevronUp className="h-3 w-3" />
) : (
<ChevronDown className="h-3 w-3" />
)}
</button>
{/* Category Tools */}
{isExpanded && (
<div className="ml-4 space-y-1 mt-1">
{tools.map((tool) => {
const IconComponent = tool.icon;
const isActiveItem = isActive(tool.path);
const getActiveClasses = (category) => {
switch (category) {
case 'editor':
return {
expanded: 'bg-gradient-to-r from-blue-50 to-blue-100 dark:from-blue-900/30 dark:to-blue-800/30',
titleColor: 'text-blue-700 dark:text-blue-300',
iconBg: 'bg-gradient-to-br from-blue-500 to-cyan-500'
};
case 'encoder':
return {
expanded: 'bg-gradient-to-r from-purple-50 to-purple-100 dark:from-purple-900/30 dark:to-purple-800/30',
titleColor: 'text-purple-700 dark:text-purple-300',
iconBg: 'bg-gradient-to-br from-purple-500 to-pink-500'
};
case 'formatter':
return {
expanded: 'bg-gradient-to-r from-green-50 to-green-100 dark:from-green-900/30 dark:to-green-800/30',
titleColor: 'text-green-700 dark:text-green-300',
iconBg: 'bg-gradient-to-br from-green-500 to-emerald-500'
};
case 'analyzer':
return {
expanded: 'bg-gradient-to-r from-orange-50 to-orange-100 dark:from-orange-900/30 dark:to-orange-800/30',
titleColor: 'text-orange-700 dark:text-orange-300',
iconBg: 'bg-gradient-to-br from-orange-500 to-red-500'
};
default:
return {
expanded: 'bg-gradient-to-r from-slate-50 to-slate-100 dark:from-slate-700 dark:to-slate-600',
titleColor: 'text-slate-700 dark:text-slate-300',
iconBg: 'bg-gradient-to-br from-slate-500 to-slate-600'
};
}
};
const activeClasses = getActiveClasses(tool.category);
return (
<a
key={tool.path}
href={tool.path}
onClick={(e) => handleNavigation(tool.path, e)}
className={`group flex items-center text-sm font-medium rounded-lg transition-all duration-300 px-3 py-2 cursor-pointer ${
isActiveItem
? activeClasses.expanded + ' shadow-md'
: 'hover:bg-white/50 dark:hover:bg-slate-700/50'
}`}
>
<div className={`p-1.5 rounded-md shadow-sm group-hover:scale-110 transition-transform duration-300 mr-3 flex-shrink-0 ${
isActiveItem
? activeClasses.iconBg
: `border border-${categoryConfig.color.split('-')[1]}-300 dark:border-${categoryConfig.color.split('-')[1]}-600 bg-transparent group-hover:bg-gradient-to-br group-hover:${categoryConfig.color}`
}`}>
<IconComponent className={`h-3.5 w-3.5 ${
isActiveItem
? 'text-white'
: 'text-slate-500 dark:text-slate-400 group-hover:text-white'
}`} />
</div>
<div className="flex-1 min-w-0">
<div className={`font-medium truncate text-sm ${
isActiveItem ? activeClasses.titleColor : 'text-slate-600 dark:text-slate-400 group-hover:text-slate-800 dark:group-hover:text-slate-200'
}`}>
{tool.name}
</div>
<div className="text-xs text-slate-500 dark:text-slate-500 truncate">
{tool.description}
</div>
</div>
</a>
);
})}
</div>
) : (
// Expanded sidebar
<>
<div className={`p-2 rounded-lg shadow-sm group-hover:scale-110 transition-transform duration-300 mr-3 flex-shrink-0 ${
isActiveItem
? activeClasses.iconBg // Active: colored background
: inactiveClasses.iconBorder // Inactive: transparent with colored border
}`}>
<IconComponent className={`h-4 w-4 ${
isActiveItem
? 'text-white' // Active: white icon
: inactiveClasses.iconColor // Inactive: grayscale icon
}`} />
</div>
<div className="flex-1 min-w-0">
<div className={`font-medium truncate ${
isActiveItem ? activeClasses.titleColor : inactiveClasses.titleColor
}`}>
{tool.name}
</div>
<div className="text-xs text-slate-500 dark:text-slate-400 truncate">
{tool.description}
</div>
</div>
</>
)}
</Link>
</div>
);
})}
{/* Collapsed view - show categories with tooltip submenus */}
{isCollapsed && Object.entries(toolsByCategory).map(([categoryKey, tools]) => {
const categoryConfig = getCategoryConfig(categoryKey);
const isTooltipVisible = hoveredTooltip === categoryKey;
return (
<div
key={categoryKey}
className="relative"
onMouseEnter={() => handleTooltipMouseEnter(categoryKey)}
onMouseLeave={handleTooltipMouseLeave}
>
<div className="flex items-center justify-center py-3 rounded-xl transition-all duration-300 cursor-pointer">
<div className={`rounded-lg shadow-sm hover:scale-110 transition-transform duration-300 p-2 bg-gradient-to-br ${categoryConfig.color} ${isTooltipVisible ? 'opacity-100 scale-110' : 'opacity-80 hover:opacity-100'}`}>
<div className="h-4 w-4 bg-white rounded-sm flex items-center justify-center">
<span className="text-xs font-bold text-gray-700">{tools.length}</span>
</div>
</div>
</div>
{/* Tooltip Submenu */}
<div className={`absolute left-full ml-2 top-0 w-64 bg-white dark:bg-slate-800 rounded-lg shadow-xl border border-gray-200 dark:border-slate-700 z-[9999] transition-all duration-200 transform ${
isTooltipVisible
? 'opacity-100 visible translate-x-0 pointer-events-auto'
: 'opacity-0 invisible translate-x-2 pointer-events-none'
}`}>
<div className="p-3">
<div className="flex items-center gap-2 mb-3 pb-2 border-b border-gray-200 dark:border-slate-700">
<div className={`w-3 h-3 rounded-full bg-gradient-to-r ${categoryConfig.color}`}></div>
<span className="text-sm font-semibold text-gray-900 dark:text-gray-100 uppercase tracking-wider">{categoryConfig.name}</span>
</div>
<div className="space-y-1">
{tools.map((tool) => {
const IconComponent = tool.icon;
const isActiveItem = isActive(tool.path);
return (
<a
key={tool.path}
href={tool.path}
onClick={(e) => handleNavigation(tool.path, e)}
className={`flex items-center gap-3 p-2 rounded-lg transition-colors cursor-pointer ${
isActiveItem
? `bg-gradient-to-r ${categoryConfig.color.replace('from-', 'from-').replace('to-', 'to-')}20 text-gray-900 dark:text-gray-100`
: 'hover:bg-gray-50 dark:hover:bg-slate-700 text-gray-700 dark:text-gray-300'
}`}
>
<div className={`p-1.5 rounded-md ${
isActiveItem
? `bg-gradient-to-br ${categoryConfig.color}`
: `border border-gray-300 dark:border-slate-600 bg-transparent`
}`}>
<IconComponent className={`h-3.5 w-3.5 ${
isActiveItem ? 'text-white' : 'text-gray-500 dark:text-gray-400'
}`} />
</div>
<div className="flex-1 min-w-0">
<div className={`font-medium text-sm truncate ${isActiveItem ? `text-white` : `text-gray-500 dark:text-gray-400`}`}>{tool.name}</div>
<div className={`text-xs ${isActiveItem ? `text-white` : `text-gray-500 dark:text-gray-400`} truncate`}>{tool.description}</div>
</div>
</a>
);
})}
</div>
</div>
</div>
</div>
);
})}
</nav>

View File

@@ -0,0 +1,628 @@
import React from 'react';
const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
// Get the chosen color scheme or default to golden
const accentColor = invoiceData.settings?.colorScheme || '#D4AF37';
// Get layout settings
const sectionSpacing = invoiceData.settings?.sectionSpacing || 'normal';
const pageBreaks = invoiceData.settings?.pageBreaks || {};
// Section spacing values
const spacingMap = {
compact: '15px',
normal: '25px',
spacious: '40px'
};
return (
<div style={{
padding: '10px', // Further reduced for better print layout
fontSize: '13px',
lineHeight: '1.4',
fontFamily: 'system-ui, -apple-system, sans-serif',
color: '#000000'
}}>
{/* Header Section - 2 Column Layout */}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '30px',
padding: '20px',
background: `linear-gradient(135deg, ${accentColor}, ${accentColor}dd)`,
borderRadius: '4px',
color: 'white'
}}>
{/* Left: Logo + INVOICE + Company Name */}
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
{/* Company Logo */}
{invoiceData.company.logo ? (
<img
src={invoiceData.company.logo}
alt="Company Logo"
style={{
width: '60px',
height: '60px',
objectFit: 'contain',
borderRadius: '8px',
background: 'white',
padding: '4px'
}}
/>
) : (
<div style={{
width: '60px',
height: '60px',
background: 'rgba(255,255,255,0.2)',
borderRadius: '12px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '24px',
color: 'white',
fontWeight: 'bold'
}}>
</div>
)}
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
<h2 style={{
fontSize: '32px',
fontWeight: 'bold',
color: 'white',
margin: '0',
lineHeight: '1'
}}>
INVOICE
</h2>
<div style={{
fontSize: '16px',
fontWeight: '600',
color: 'rgba(255,255,255,0.9)',
margin: '4px 0 0 0'
}}>
{invoiceData.company.name || 'DevTools Inc.'}
</div>
</div>
</div>
{/* Right: Invoice Details */}
<div style={{ textAlign: 'right' }}>
<div style={{ marginBottom: '8px', display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: '12px' }}>
<span style={{ fontSize: '16px', fontWeight: '600', color: 'rgba(255,255,255,0.8)' }}>#</span>
<span style={{ fontSize: '18px', fontWeight: 'bold', color: 'white' }}>
{invoiceData.invoiceNumber || 'INV-2024-001'}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: '12px' }}>
<span style={{ fontSize: '16px', fontWeight: '600', color: 'rgba(255,255,255,0.8)' }}>Date:</span>
<span style={{ fontSize: '16px', fontWeight: '600', color: 'white' }}>
{invoiceData.date || '15/01/2024'}
</span>
</div>
{invoiceData.dueDate && (
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: '12px', marginTop: '4px' }}>
<span style={{ fontSize: '16px', fontWeight: '600', color: 'rgba(255,255,255,0.8)' }}>Due:</span>
<span style={{ fontSize: '16px', fontWeight: '600', color: 'white' }}>
{invoiceData.dueDate}
</span>
</div>
)}
</div>
</div>
{/* Subheader Section - 2 Column Layout: FROM and TO */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '30px', marginBottom: '25px' }}>
{/* FROM Section */}
{(invoiceData.settings?.showFromSection ?? true) && (
<div style={{
padding: '20px',
background: `${accentColor}08`,
borderRadius: '4px',
border: '1px solid #e9ecef'
}}>
<h3 style={{
fontSize: '14px',
fontWeight: 'bold',
color: accentColor,
marginBottom: '12px',
textTransform: 'uppercase',
letterSpacing: '0.5px'
}}>
FROM
</h3>
<div style={{ fontSize: '14px', color: '#000000', lineHeight: '1.6' }}>
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>
{invoiceData.company.name || 'DevTools Inc.'}
</div>
{invoiceData.company.address && <div>{invoiceData.company.address}</div>}
{invoiceData.company.city && <div>{invoiceData.company.city}</div>}
{invoiceData.company.phone && <div>{invoiceData.company.phone}</div>}
{invoiceData.company.email && <div>{invoiceData.company.email}</div>}
</div>
</div>
)}
{/* TO Section */}
<div style={{
padding: '20px',
background: `${accentColor}08`,
borderRadius: '4px',
border: '1px solid #e9ecef'
}}>
<h3 style={{
fontSize: '14px',
fontWeight: 'bold',
color: accentColor,
marginBottom: '12px',
textTransform: 'uppercase',
letterSpacing: '0.5px'
}}>
TO
</h3>
<div style={{ fontSize: '14px', color: '#000000', lineHeight: '1.6' }}>
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>
{invoiceData.client.name || 'Acme Corporation'}
</div>
<div>{invoiceData.client.address || '456 Business Ave'}</div>
<div>{invoiceData.client.city || 'New York, NY 10001'}</div>
{invoiceData.client.phone && <div>{invoiceData.client.phone}</div>}
{invoiceData.client.email && <div>{invoiceData.client.email}</div>}
</div>
</div>
</div>
{/* Payment Terms Section */}
{invoiceData.paymentTerms?.type !== 'full' && (
<div
className={pageBreaks.beforePaymentSchedule ? 'page-break-before' : ''}
style={{ marginBottom: '20px', marginTop: spacingMap[sectionSpacing] }}
>
<h3 style={{
fontSize: '16px',
fontWeight: 'bold',
color: accentColor,
marginBottom: '16px',
textTransform: 'uppercase',
letterSpacing: '0.5px'
}}>
Payment Schedule
</h3>
<div style={{
background: `${accentColor}08`,
borderRadius: '4px',
border: '1px solid #e9ecef',
padding: '16px'
}}>
{/* Down Payment */}
{invoiceData.paymentTerms?.type === 'downpayment' && invoiceData.paymentTerms?.downPayment?.amount > 0 && (
<div style={{ marginBottom: '12px', paddingBottom: '12px', borderBottom: '1px solid #e9ecef' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<div>
<div style={{ fontSize: '14px', fontWeight: '600', color: '#000000', marginBottom: '4px' }}>
Down Payment ({invoiceData.paymentTerms.downPayment.percentage?.toFixed(1)}%)
</div>
<div style={{ fontSize: '12px', marginTop: '2px' }}>
{invoiceData.paymentTerms.downPayment.status === 'overdue' && invoiceData.paymentTerms.downPayment.dueDate && (
<span style={{ color: '#EF4444' }}>
Overdue - Due: {new Date(invoiceData.paymentTerms.downPayment.dueDate).toLocaleDateString()}
</span>
)}
{invoiceData.paymentTerms.downPayment.status === 'current' && invoiceData.paymentTerms.downPayment.dueDate && (
<span style={{ color: accentColor }}>
Current - <span style={{ color: '#666666' }}>Due: {new Date(invoiceData.paymentTerms.downPayment.dueDate).toLocaleDateString()}</span>
</span>
)}
{invoiceData.paymentTerms.downPayment.status === 'paid' && (
<span style={{ color: '#666666' }}>Paid</span>
)}
{(!invoiceData.paymentTerms.downPayment.status || invoiceData.paymentTerms.downPayment.status === 'pending') && (
<span style={{ color: '#666666' }}>Pending</span>
)}
</div>
</div>
</div>
<span style={{
fontSize: '14px',
fontWeight: 'bold',
color: invoiceData.paymentTerms.downPayment.status === 'current' ? accentColor : '#666666'
}}>
{new Intl.NumberFormat('en-US', {
style: 'currency',
currency: invoiceData.settings?.currency?.code || 'USD'
}).format(invoiceData.paymentTerms.downPayment.amount)}
</span>
</div>
</div>
)}
{/* Installments */}
{invoiceData.paymentTerms?.installments && invoiceData.paymentTerms.installments.length > 0 && (
<div>
{invoiceData.paymentTerms?.type === 'downpayment' && (
<div style={{ fontSize: '12px', color: '#666666', marginBottom: '8px' }}>
Remaining Balance: {new Intl.NumberFormat('en-US', {
style: 'currency',
currency: invoiceData.settings?.currency?.code || 'USD'
}).format(invoiceData.total - (invoiceData.paymentTerms?.downPayment?.amount || 0))}
</div>
)}
{invoiceData.paymentTerms.installments.map((installment, index) => (
<div key={installment.id} style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '8px',
padding: '8px',
background: 'rgba(255,255,255,0.5)',
borderRadius: '4px'
}}>
<div>
<div>
<div style={{ fontSize: '14px', fontWeight: '500', color: '#000000', marginBottom: '4px' }}>
{installment.description || `Installment ${index + 1}`}
</div>
<div style={{ fontSize: '12px', marginTop: '2px' }}>
{installment.status === 'overdue' && installment.dueDate && (
<span style={{ color: '#EF4444' }}>
Overdue - Due: {new Date(installment.dueDate).toLocaleDateString()}
</span>
)}
{installment.status === 'current' && installment.dueDate && (
<span style={{ color: accentColor }}>
Current - <span style={{ color: '#666666' }}>Due: {new Date(installment.dueDate).toLocaleDateString()}</span>
</span>
)}
{installment.status === 'paid' && (
<span style={{ color: '#666666' }}>Paid</span>
)}
{(!installment.status || installment.status === 'pending') && (
<span style={{ color: '#666666' }}>Pending</span>
)}
</div>
</div>
</div>
<span style={{
fontSize: '14px',
fontWeight: 'bold',
color: installment.status === 'current' ? accentColor : '#666666'
}}>
{new Intl.NumberFormat('en-US', {
style: 'currency',
currency: invoiceData.settings?.currency?.code || 'USD'
}).format(installment.amount || 0)}
</span>
</div>
))}
</div>
)}
</div>
</div>
)}
{/* Items Table */}
<div
className={pageBreaks.beforeItemsTable ? 'page-break-before' : ''}
style={{ marginBottom: '20px', marginTop: spacingMap[sectionSpacing] }}
>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
{/* Table Header */}
<thead>
<tr style={{
borderBottom: `2px solid ${accentColor}`,
backgroundColor: `${accentColor}15` // 15 = ~8% opacity
}}>
<th style={{
padding: '12px 0 12px 16px',
textAlign: 'left',
fontSize: '16px',
fontWeight: 'bold',
color: accentColor,
verticalAlign: 'middle'
}}>Item</th>
<th style={{
padding: '12px 0',
textAlign: 'center',
fontSize: '16px',
fontWeight: 'bold',
color: accentColor,
verticalAlign: 'middle'
}}>Quantity</th>
<th style={{
padding: '12px 0',
textAlign: 'center',
fontSize: '16px',
fontWeight: 'bold',
color: accentColor,
verticalAlign: 'middle'
}}>Unit Price</th>
<th style={{
padding: '12px 16px 12px 0',
textAlign: 'right',
fontSize: '16px',
fontWeight: 'bold',
color: accentColor,
verticalAlign: 'middle'
}}>Total</th>
</tr>
</thead>
{/* Table Body */}
<tbody>
{invoiceData.items.map((item, index) => (
<tr key={item.id} style={{
borderBottom: '1px solid #E5E5E5'
}}>
<td style={{
padding: '16px 0 16px 16px',
fontSize: '14px',
color: '#000000',
verticalAlign: 'middle'
}}>
{item.description}
</td>
<td style={{
padding: '16px 0',
textAlign: 'center',
fontSize: '14px',
color: '#000000',
verticalAlign: 'middle'
}}>
{item.quantity}
</td>
<td style={{
padding: '16px 0',
textAlign: 'center',
fontSize: '14px',
color: '#000000',
verticalAlign: 'middle'
}}>
{formatCurrency(item.rate, true)}
</td>
<td style={{
padding: '16px 16px 16px 0',
textAlign: 'right',
fontSize: '14px',
color: '#000000',
fontWeight: 'bold',
verticalAlign: 'middle'
}}>
{formatCurrency(item.amount, true)}
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Payment Method and Totals */}
<div
className={pageBreaks.beforePaymentMethod ? 'page-break-before' : ''}
style={{ display: 'flex', justifyContent: 'space-between', gap: '40px', marginBottom: '40px', marginTop: spacingMap[sectionSpacing] }}
>
{/* Payment Method */}
{invoiceData.paymentMethod?.type !== 'none' && (
<div style={{ flex: 1 }}>
<h3 style={{
fontSize: '16px',
fontWeight: 'bold',
color: '#000000',
marginBottom: '12px'
}}>
Payment Method
</h3>
{/* Bank Details */}
{invoiceData.paymentMethod?.type === 'bank' && (
<div style={{ fontSize: '14px', color: '#000000', lineHeight: '1.6' }}>
{invoiceData.paymentMethod.bankDetails?.bankName && (
<div style={{ marginBottom: '4px' }}>{invoiceData.paymentMethod.bankDetails.bankName}</div>
)}
{invoiceData.paymentMethod.bankDetails?.accountName && (
<div style={{ marginBottom: '4px' }}>Account Name: {invoiceData.paymentMethod.bankDetails.accountName}</div>
)}
{invoiceData.paymentMethod.bankDetails?.accountNumber && (
<div style={{ marginBottom: '4px' }}>Account No.: {invoiceData.paymentMethod.bankDetails.accountNumber}</div>
)}
{invoiceData.paymentMethod.bankDetails?.routingNumber && (
<div style={{ marginBottom: '4px' }}>Routing: {invoiceData.paymentMethod.bankDetails.routingNumber}</div>
)}
{invoiceData.paymentMethod.bankDetails?.swiftCode && (
<div style={{ marginBottom: '4px' }}>SWIFT: {invoiceData.paymentMethod.bankDetails.swiftCode}</div>
)}
{invoiceData.paymentMethod.bankDetails?.iban && (
<div style={{ marginBottom: '4px' }}>IBAN: {invoiceData.paymentMethod.bankDetails.iban}</div>
)}
{invoiceData.dueDate && <div>Pay by: {invoiceData.dueDate}</div>}
</div>
)}
{/* Payment Link */}
{invoiceData.paymentMethod?.type === 'link' && invoiceData.paymentMethod.paymentLink?.url && (
<div style={{ fontSize: '14px', color: '#000000', lineHeight: '1.6' }}>
<a
href={invoiceData.paymentMethod.paymentLink.url}
target="_blank"
rel="noopener noreferrer"
style={{
display: 'inline-block',
padding: '8px 16px',
background: accentColor,
color: 'white',
textDecoration: 'none',
borderRadius: '4px',
fontSize: '14px',
fontWeight: '600'
}}
>
{invoiceData.paymentMethod.paymentLink.label || 'Pay Online'}
</a>
{invoiceData.dueDate && <div style={{ marginTop: '8px' }}>Pay by: {invoiceData.dueDate}</div>}
</div>
)}
{/* QR Code */}
{invoiceData.paymentMethod?.type === 'qr' && (invoiceData.paymentMethod.qrCode?.url || invoiceData.paymentMethod.qrCode?.customImage) && (
<div style={{ fontSize: '14px', color: '#000000', lineHeight: '1.6' }}>
<div style={{ marginBottom: '8px' }}>
<img
src={
invoiceData.paymentMethod.qrCode.customImage
? invoiceData.paymentMethod.qrCode.customImage
: `https://api.qrserver.com/v1/create-qr-code/?size=80x80&data=${encodeURIComponent(invoiceData.paymentMethod.qrCode.url)}`
}
alt="Payment QR Code"
style={{ width: '80px', height: '80px', border: '1px solid #e9ecef' }}
/>
</div>
<div style={{ fontSize: '12px', color: '#666666', marginBottom: '4px' }}>
{invoiceData.paymentMethod.qrCode.label || 'Scan to Pay'}
</div>
{invoiceData.dueDate && <div>Pay by: {invoiceData.dueDate}</div>}
</div>
)}
</div>
)}
{/* Totals */}
<div style={{ textAlign: 'right', minWidth: '200px' }}>
<div style={{ marginBottom: '12px' }}>
<span style={{ fontSize: '14px', color: '#000000' }}>Subtotal</span>
<span style={{ fontSize: '14px', color: '#000000', marginLeft: '40px' }}>
{formatCurrency(invoiceData.subtotal, true)}
</span>
</div>
{/* Dynamic Fees */}
{invoiceData.fees && invoiceData.fees.map((fee) => (
<div key={fee.id} style={{ marginBottom: '12px' }}>
<span style={{ fontSize: '14px', color: '#000000' }}>
{fee.label || 'Fee'} {fee.type === 'percentage' ? `(${fee.value}%)` : ''}
</span>
<span style={{ fontSize: '14px', color: '#000000', marginLeft: '40px' }}>
+{formatCurrency(fee.type === 'percentage' ? (invoiceData.subtotal * fee.value) / 100 : fee.value, true)}
</span>
</div>
))}
{/* Dynamic Discounts */}
{invoiceData.discounts && invoiceData.discounts.map((discount) => (
<div key={discount.id} style={{ marginBottom: '12px' }}>
<span style={{ fontSize: '14px', color: '#000000' }}>
{discount.label || 'Discount'} {discount.type === 'percentage' ? `(${discount.value}%)` : ''}
</span>
<span style={{ fontSize: '14px', color: '#000000', marginLeft: '40px' }}>
-{formatCurrency(discount.type === 'percentage' ? (invoiceData.subtotal * discount.value) / 100 : discount.value, true)}
</span>
</div>
))}
{/* Legacy Discount */}
{invoiceData.discount > 0 && (
<div style={{ marginBottom: '12px' }}>
<span style={{ fontSize: '14px', color: '#000000' }}>Discount</span>
<span style={{ fontSize: '14px', color: '#000000', marginLeft: '40px' }}>
-{formatCurrency(invoiceData.discount, true)}
</span>
</div>
)}
<div style={{
padding: '12px 16px 12px 16px', // Match table head padding
marginTop: '16px',
backgroundColor: `${accentColor}10`, // 10 = ~6% opacity
borderTop: `2px solid ${accentColor}`,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<span style={{ fontSize: '18px', fontWeight: 'bold', color: accentColor }}>Total</span>
<span style={{
fontSize: '18px',
fontWeight: 'bold',
color: accentColor
}}>
{formatCurrency(invoiceData.total, true)}
</span>
</div>
</div>
</div>
{/* Notes Section */}
{invoiceData.notes && (
<div style={{ marginBottom: '30px' }}>
<h3 style={{
fontSize: '16px',
fontWeight: 'bold',
color: '#000000',
marginBottom: '8px'
}}>
Notes
</h3>
<p style={{
fontSize: '14px',
color: '#000000',
margin: '0',
lineHeight: '1.5'
}}>
{invoiceData.notes}
</p>
</div>
)}
{/* Footer */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', marginTop: '40px' }}>
{/* Thank you message */}
<div>
<p style={{ fontSize: '16px', color: '#000000', margin: '0' }}>
{invoiceData.thankYouMessage || 'Thank you for your business!'}
</p>
</div>
{/* Signature Line */}
<div style={{ textAlign: 'center' }}>
{/* Digital Signature */}
{invoiceData.digitalSignature ? (
<div style={{ marginBottom: '8px' }}>
<img
src={invoiceData.digitalSignature}
alt="Digital Signature"
style={{
maxWidth: '200px',
maxHeight: '200px',
objectFit: 'contain'
}}
/>
</div>
) : (
// More space for physical signature when no digital signature
<div style={{
height: '60px',
marginBottom: '8px',
display: 'flex',
alignItems: 'flex-end'
}}></div>
)}
<div style={{
width: '200px',
borderBottom: `2px solid ${accentColor}`,
marginBottom: '8px'
}}></div>
<p style={{ fontSize: '12px', color: '#666666', margin: '0' }}>
{invoiceData.authorizedSignedText || 'Authorized Signed'}
</p>
</div>
</div>
</div>
);
};
export default MinimalTemplate;