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:
99
src/components/CodeEditor.js
Normal file
99
src/components/CodeEditor.js
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
115
src/components/NavigationConfirmModal.js
Normal file
115
src/components/NavigationConfirmModal.js
Normal 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;
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
628
src/components/invoice-templates/MinimalTemplate.js
Normal file
628
src/components/invoice-templates/MinimalTemplate.js
Normal 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;
|
||||
Reference in New Issue
Block a user