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

257
package-lock.json generated
View File

@@ -16,13 +16,19 @@
"@codemirror/search": "^6.5.11",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.38.1",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@uiw/react-codemirror": "^4.25.1",
"codemirror": "^6.0.2",
"diff-match-patch": "^1.0.5",
"html2pdf.js": "^0.12.1",
"js-beautify": "^1.15.4",
"jspdf": "^3.0.3",
"jspdf-autotable": "^5.0.2",
"lucide-react": "^0.540.0",
"papaparse": "^5.5.3",
"react": "18.3.1",
@@ -2372,6 +2378,59 @@
"postcss-selector-parser": "^6.0.10"
}
},
"node_modules/@dnd-kit/accessibility": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/core": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@dnd-kit/sortable": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
"license": "MIT",
"dependencies": {
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.3.0",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.7.0",
"dev": true,
@@ -4092,6 +4151,12 @@
"@types/node": "*"
}
},
"node_modules/@types/pako": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
"license": "MIT"
},
"node_modules/@types/parse-json": {
"version": "4.0.2",
"dev": true,
@@ -4112,6 +4177,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/raf": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/range-parser": {
"version": "1.2.7",
"dev": true,
@@ -4177,7 +4249,7 @@
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/ws": {
@@ -5416,6 +5488,15 @@
"version": "1.0.2",
"license": "MIT"
},
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/batch": {
"version": "0.6.1",
"dev": true,
@@ -5717,6 +5798,26 @@
],
"license": "CC-BY-4.0"
},
"node_modules/canvg": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
"license": "MIT",
"optional": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/raf": "^3.4.0",
"core-js": "^3.8.3",
"raf": "^3.4.1",
"regenerator-runtime": "^0.13.7",
"rgbcolor": "^1.0.1",
"stackblur-canvas": "^2.0.0",
"svg-pathdata": "^6.0.3"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/case-sensitive-paths-webpack-plugin": {
"version": "2.4.0",
"dev": true,
@@ -6191,7 +6292,7 @@
},
"node_modules/core-js": {
"version": "3.45.1",
"dev": true,
"devOptional": true,
"hasInstallScript": true,
"license": "MIT",
"funding": {
@@ -6310,6 +6411,15 @@
"postcss": "^8.4"
}
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/css-loader": {
"version": "6.11.0",
"dev": true,
@@ -7074,6 +7184,16 @@
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/dompurify": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optional": true,
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/domutils": {
"version": "2.8.0",
"dev": true,
@@ -8291,6 +8411,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/fast-png": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
"license": "MIT",
"dependencies": {
"@types/pako": "^2.0.3",
"iobuffer": "^5.3.2",
"pako": "^2.1.0"
}
},
"node_modules/fast-uri": {
"version": "3.0.6",
"dev": true,
@@ -8333,6 +8464,12 @@
"bser": "2.1.1"
}
},
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"license": "MIT"
},
"node_modules/file-entry-cache": {
"version": "6.0.1",
"dev": true,
@@ -9287,6 +9424,29 @@
}
}
},
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/html2pdf.js": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/html2pdf.js/-/html2pdf.js-0.12.1.tgz",
"integrity": "sha512-3rBWQ96H5oOU9jtoz3MnE/epGi27ig9h8aonBk4JTpvUERM3lMRxhIRckhJZEi4wE0YfRINoYOIDY0hLY0CHgQ==",
"license": "MIT",
"dependencies": {
"html2canvas": "^1.0.0",
"jspdf": "^3.0.0"
}
},
"node_modules/htmlparser2": {
"version": "6.1.0",
"dev": true,
@@ -9541,6 +9701,12 @@
"node": ">= 0.4"
}
},
"node_modules/iobuffer": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
"license": "MIT"
},
"node_modules/ipaddr.js": {
"version": "2.2.0",
"dev": true,
@@ -11229,6 +11395,32 @@
"node": ">=0.10.0"
}
},
"node_modules/jspdf": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.3.tgz",
"integrity": "sha512-eURjAyz5iX1H8BOYAfzvdPfIKK53V7mCpBTe7Kb16PaM8JSXEcUQNBQaiWMI8wY5RvNOPj4GccMjTlfwRBd+oQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.9",
"fast-png": "^6.2.0",
"fflate": "^0.8.1"
},
"optionalDependencies": {
"canvg": "^3.0.11",
"core-js": "^3.6.0",
"dompurify": "^3.2.4",
"html2canvas": "^1.0.0-rc.5"
}
},
"node_modules/jspdf-autotable": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.2.tgz",
"integrity": "sha512-YNKeB7qmx3pxOLcNeoqAv3qTS7KuvVwkFe5AduCawpop3NOkBUtqDToxNc225MlNecxT4kP2Zy3z/y/yvGdXUQ==",
"license": "MIT",
"peerDependencies": {
"jspdf": "^2 || ^3"
}
},
"node_modules/jsx-ast-utils": {
"version": "3.3.5",
"dev": true,
@@ -12089,6 +12281,12 @@
"version": "1.0.1",
"license": "BlueOak-1.0.0"
},
"node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
"license": "(MIT AND Zlib)"
},
"node_modules/papaparse": {
"version": "5.5.3",
"license": "MIT"
@@ -12209,7 +12407,7 @@
},
"node_modules/performance-now": {
"version": "2.1.0",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/picocolors": {
@@ -13716,7 +13914,7 @@
},
"node_modules/raf": {
"version": "3.4.1",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"performance-now": "^2.1.0"
@@ -14172,7 +14370,7 @@
},
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/regex-parser": {
@@ -14418,6 +14616,16 @@
"node": ">=0.10.0"
}
},
"node_modules/rgbcolor": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
"optional": true,
"engines": {
"node": ">= 0.8.15"
}
},
"node_modules/rimraf": {
"version": "3.0.2",
"dev": true,
@@ -15191,6 +15399,16 @@
"node": ">=8"
}
},
"node_modules/stackblur-canvas": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.1.14"
}
},
"node_modules/stackframe": {
"version": "1.3.4",
"dev": true,
@@ -15681,6 +15899,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/svg-pathdata": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/svgo": {
"version": "1.3.2",
"dev": true,
@@ -16042,6 +16270,15 @@
"node": "*"
}
},
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/text-table": {
"version": "0.2.0",
"dev": true,
@@ -16175,7 +16412,6 @@
},
"node_modules/tslib": {
"version": "2.8.1",
"dev": true,
"license": "0BSD"
},
"node_modules/tsutils": {
@@ -16523,6 +16759,15 @@
"node": ">= 0.4.0"
}
},
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/uuid": {
"version": "8.3.2",
"dev": true,

View File

@@ -12,13 +12,19 @@
"@codemirror/search": "^6.5.11",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.38.1",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@uiw/react-codemirror": "^4.25.1",
"codemirror": "^6.0.2",
"diff-match-patch": "^1.0.5",
"html2pdf.js": "^0.12.1",
"js-beautify": "^1.15.4",
"jspdf": "^3.0.3",
"jspdf-autotable": "^5.0.2",
"lucide-react": "^0.540.0",
"papaparse": "^5.5.3",
"react": "18.3.1",

File diff suppressed because one or more lines are too long

View File

@@ -13,6 +13,9 @@ import DiffTool from './pages/DiffTool';
import TextLengthTool from './pages/TextLengthTool';
import ObjectEditor from './pages/ObjectEditor';
import TableEditor from './pages/TableEditor';
import InvoiceEditor from './pages/InvoiceEditor';
import InvoicePreview from './pages/InvoicePreview';
import InvoicePreviewMinimal from './pages/InvoicePreviewMinimal';
import ReleaseNotes from './pages/ReleaseNotes';
import TermsOfService from './pages/TermsOfService';
import PrivacyPolicy from './pages/PrivacyPolicy';
@@ -42,6 +45,9 @@ function App() {
<Route path="/text-length" element={<TextLengthTool />} />
<Route path="/object-editor" element={<ObjectEditor />} />
<Route path="/table-editor" element={<TableEditor />} />
<Route path="/invoice-editor" element={<InvoiceEditor />} />
<Route path="/invoice-preview" element={<InvoicePreview />} />
<Route path="/invoice-preview-minimal" element={<InvoicePreviewMinimal />} />
<Route path="/release-notes" element={<ReleaseNotes />} />
<Route path="/privacy" element={<PrivacyPolicy />} />
<Route path="/terms" element={<TermsOfService />} />

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);
@@ -44,16 +47,19 @@ 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">
{/* SEO Head Management */}
<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,25 +2,25 @@ 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>

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;

View File

@@ -1,4 +1,4 @@
import { Edit3, Table, LinkIcon, Hash, Wand2, GitCompare, Type, Home, Zap } from 'lucide-react';
import { Edit3, Table, LinkIcon, Hash, Wand2, GitCompare, Type, Home, Zap, FileText } from 'lucide-react';
// Master tools configuration - single source of truth
export const TOOL_CATEGORIES = {
@@ -63,6 +63,14 @@ export const TOOLS = [
tags: ['Table', 'CSV', 'JSON', 'Data', 'Editor'],
category: 'editor'
},
{
path: '/invoice-editor',
name: 'Invoice Editor',
icon: FileText,
description: 'Create, edit, and export professional invoices with PDF generation',
tags: ['Invoice', 'PDF', 'Business', 'Billing', 'Export'],
category: 'editor'
},
{
path: '/url',
name: 'URL Encoder/Decoder',

View File

@@ -0,0 +1,181 @@
import { useCallback, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
const useNavigationGuard = () => {
const location = useLocation();
const navigate = useNavigate();
const [showModal, setShowModal] = useState(false);
const [pendingNavigation, setPendingNavigation] = useState(null);
// Check if we're on a page that might have user data
const isDataPage = useCallback(() => {
const dataPages = ['/invoice-editor', '/object-editor', '/table-editor'];
return dataPages.some(page => location.pathname.startsWith(page));
}, [location.pathname]);
// Check if there's modified data (not sample data) for the CURRENT editor only
const hasUnsavedData = useCallback(() => {
try {
// Only check data for the current editor
if (location.pathname.startsWith('/invoice-editor')) {
const invoiceData = localStorage.getItem('currentInvoice');
if (invoiceData) {
const parsed = JSON.parse(invoiceData);
// Check if has user data (same logic as InvoiceEditor hasUserData)
const hasUserData = parsed.invoiceNumber ||
parsed.company?.name ||
parsed.client?.name ||
(parsed.items && parsed.items.length > 0);
if (!hasUserData) return false;
// Check if it's not sample data (exact same logic as InvoiceEditor hasModifiedData)
const sampleInvoiceData = {
invoiceNumber: 'INV-2024-001',
date: '2024-01-15',
dueDate: '2024-02-15',
company: {
name: 'DevTools Inc.',
address: '123 Tech Street',
city: 'San Francisco, CA 94105',
phone: '+1 (555) 123-4567',
email: 'billing@devtools.com',
logo: null,
bankName: 'Chase Bank',
accountName: 'DevTools Inc.',
accountNumber: '1234567890'
},
client: {
name: 'Acme Corporation',
address: '456 Business Ave',
city: 'New York, NY 10001',
phone: '+1 (555) 987-6543',
email: 'accounts@acme.com'
},
items: [
{ id: 1, description: 'Web Development Services', quantity: 40, rate: 125, amount: 5000 },
{ id: 2, description: 'UI/UX Design', quantity: 20, rate: 100, amount: 2000 },
{ id: 3, description: 'Project Management', quantity: 10, rate: 150, amount: 1500 }
],
fees: [
{ id: 1, label: 'Processing Fee', type: 'fixed', value: 50, amount: 50 }
],
discounts: [
{ id: 1, label: 'Early Payment Discount', type: 'percentage', value: 5, amount: 425 }
],
subtotal: 8500,
discount: 0,
total: 8125,
notes: 'Payment due within 30 days.',
thankYouMessage: 'Thank you for your business!',
authorizedSignedText: 'Authorized Signed',
digitalSignature: null,
settings: {
colorScheme: '#3B82F6',
currency: { code: 'USD', symbol: '$' },
thousandSeparator: true
}
};
const isSampleData = JSON.stringify(parsed) === JSON.stringify(sampleInvoiceData);
return !isSampleData;
}
}
if (location.pathname.startsWith('/object-editor')) {
const objectData = localStorage.getItem('objectEditorData');
if (objectData) {
const parsed = JSON.parse(objectData);
// Check if has user data
const hasUserData = parsed && Object.keys(parsed).length > 0;
if (!hasUserData) return false;
// Check if it's not sample data (same logic as ObjectEditor)
const sampleObjectData = {
name: "John Doe",
age: 30,
email: "john@example.com",
address: {
street: "123 Main St",
city: "New York",
zipCode: "10001"
},
hobbies: ["reading", "coding", "traveling"]
};
const isSampleData = JSON.stringify(parsed) === JSON.stringify(sampleObjectData);
return !isSampleData;
}
}
if (location.pathname.startsWith('/table-editor')) {
const tableData = localStorage.getItem('tableEditorData');
if (tableData) {
const parsed = JSON.parse(tableData);
// Check if has user data
const hasUserData = parsed && parsed.length > 0;
if (!hasUserData) return false;
// Check if it's not sample data (same logic as TableEditor)
const sampleTableData = [
{ id: "row_0", col_0: 1, col_1: "John Doe", col_2: "john@example.com", col_3: 25, col_4: "New York" },
{ id: "row_1", col_0: 2, col_1: "Jane Smith", col_2: "jane@example.com", col_3: 30, col_4: "Los Angeles" },
{ id: "row_2", col_0: 3, col_1: "Bob Johnson", col_2: "bob@example.com", col_3: 35, col_4: "Chicago" },
{ id: "row_3", col_0: 4, col_1: "Alice Brown", col_2: "alice@example.com", col_3: 28, col_4: "Houston" },
{ id: "row_4", col_0: 5, col_1: "Charlie Wilson", col_2: "charlie@example.com", col_3: 32, col_4: "Phoenix" }
];
const isSampleData = JSON.stringify(parsed) === JSON.stringify(sampleTableData);
return !isSampleData;
}
}
return false;
} catch (error) {
return false;
}
}, [location.pathname]);
// Safe navigation function
const navigateWithGuard = useCallback((to, options = {}) => {
// If we're not on a data page or there's no unsaved data, navigate normally
if (!isDataPage() || !hasUnsavedData()) {
navigate(to, options);
return;
}
// Show modal and store pending navigation
setPendingNavigation({ to, options });
setShowModal(true);
}, [isDataPage, hasUnsavedData, navigate]);
// Handle modal confirmation
const handleConfirm = useCallback(() => {
if (pendingNavigation) {
navigate(pendingNavigation.to, pendingNavigation.options);
}
setShowModal(false);
setPendingNavigation(null);
}, [navigate, pendingNavigation]);
// Handle modal cancellation
const handleCancel = useCallback(() => {
setShowModal(false);
setPendingNavigation(null);
}, []);
return {
navigateWithGuard,
hasUnsavedData: hasUnsavedData(),
isDataPage: isDataPage(),
showModal,
pendingNavigation,
handleConfirm,
handleCancel
};
};
export default useNavigationGuard;

View File

@@ -7,6 +7,13 @@
@layer base {
html {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
overflow-x: hidden;
}
body {
overflow-x: hidden;
width: 100%;
max-width: 100vw;
}
code, pre {

View File

@@ -6,7 +6,6 @@ import { TOOLS, SITE_CONFIG } from '../config/tools';
import { useAnalytics } from '../hooks/useAnalytics';
const Home = () => {
console.log('🏠 NEW Home component loaded - Object Editor should be visible!');
const [searchTerm, setSearchTerm] = useState('');
const [mounted, setMounted] = useState(false);
const { trackSearch } = useAnalytics();

2733
src/pages/InvoiceEditor.js Normal file

File diff suppressed because it is too large Load Diff

242
src/pages/InvoicePreview.js Normal file
View File

@@ -0,0 +1,242 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { ArrowLeft, Download, FileText } from 'lucide-react';
import html2pdf from 'html2pdf.js';
import MinimalTemplate from '../components/invoice-templates/MinimalTemplate';
// Available templates
const templates = {
minimal: {
name: 'Minimal',
description: 'Simple, professional layout',
component: MinimalTemplate
}
};
const InvoicePreview = () => {
const navigate = useNavigate();
const [invoiceData, setInvoiceData] = useState(null);
const [pdfPageSize, setPdfPageSize] = useState('A4');
const [isGenerating, setIsGenerating] = useState(false);
const [selectedTemplate] = useState('minimal');
// Load invoice data from localStorage
useEffect(() => {
try {
const savedInvoice = localStorage.getItem('currentInvoice');
const savedPageSize = localStorage.getItem('pdfPageSize');
if (savedInvoice) {
const parsedInvoice = JSON.parse(savedInvoice);
setInvoiceData(parsedInvoice);
// Set page title with invoice number
document.title = `Invoice Preview - ${parsedInvoice.invoiceNumber || 'Draft'} | DevTools`;
} else {
// No invoice data, redirect back to editor
navigate('/invoice-editor');
}
if (savedPageSize) {
setPdfPageSize(savedPageSize);
}
} catch (error) {
console.error('Failed to load invoice data:', error);
navigate('/invoice-editor', { replace: true });
}
}, [navigate]);
// Format number with thousand separator
const formatNumber = (num) => {
if (!invoiceData?.settings?.thousandSeparator) return num.toString();
return num.toLocaleString('en-US', { minimumFractionDigits: 2 });
};
// Format currency
const formatCurrency = (amount, useThousandSeparator = false) => {
const symbol = invoiceData?.settings?.currency?.symbol || '$';
const formattedAmount = useThousandSeparator ? formatNumber(amount) : amount.toFixed(2);
return `${symbol} ${formattedAmount}`;
};
// Generate PDF from the visible invoice
const handleDownloadPDF = async () => {
if (!invoiceData) return;
setIsGenerating(true);
try {
const element = document.getElementById('invoice-content');
if (!element) {
throw new Error('Invoice content not found');
}
const opt = {
margin: [0.2, 0.4, 0.5, 0.4], // top, left, bottom, right margins in inches - reduced top margin for first page
filename: `invoice-${invoiceData.invoiceNumber || 'draft'}.pdf`,
image: { type: 'png', quality: 0.98 },
html2canvas: {
useCORS: true,
backgroundColor: '#ffffff',
letterRendering: true
},
jsPDF: {
unit: 'in',
format: pdfPageSize === 'F4' ? [8.27, 13] : pdfPageSize.toLowerCase(), // F4 dimensions in inches
orientation: 'portrait'
},
pagebreak: {
mode: ['avoid-all', 'css', 'legacy'],
before: '.page-break-before',
after: '.page-break-after',
avoid: '.page-break-avoid'
}
};
await html2pdf().set(opt).from(element).save();
} catch (error) {
console.error('PDF generation failed:', error);
alert('Failed to generate PDF. Please try again.');
} finally {
setIsGenerating(false);
}
};
// Navigate back to editor
const handleEditInvoice = () => {
// Ensure current invoice data is saved before navigating
if (invoiceData) {
try {
localStorage.setItem('currentInvoice', JSON.stringify(invoiceData));
} catch (error) {
console.error('Failed to save invoice data before edit:', error);
}
}
// Add a parameter to indicate we're editing existing data
navigate('/invoice-editor?mode=edit');
};
if (!invoiceData) {
return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">Loading invoice...</p>
</div>
</div>
);
}
// Get selected template component
const SelectedTemplateComponent = templates[selectedTemplate].component;
return (
<div className="min-h-screen bg-gray-100 dark:bg-slate-900">
{/* Mobile Notice */}
<div className="lg:hidden bg-amber-50 dark:bg-amber-900/20 border-b border-amber-200 dark:border-amber-800 p-4">
<div className="flex items-center gap-3">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-amber-600 dark:text-amber-400" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
</div>
<div className="flex-1">
<h3 className="text-sm font-medium text-amber-800 dark:text-amber-200">
Desktop Mode Recommended
</h3>
<p className="text-sm text-amber-700 dark:text-amber-300 mt-1">
For the best preview experience and accurate PDF generation, please use desktop mode or a larger screen. The invoice preview is optimized for desktop viewing.
</p>
</div>
</div>
</div>
{/* Invoice Preview */}
<div className="max-w-5xl mx-auto p-4 sm:p-6" style={{ maxWidth: 'min(60rem, calc(100vw - 2rem))' }}>
<div className="bg-white dark:bg-slate-800 rounded-lg shadow-sm border border-gray-200 dark:border-slate-700 overflow-hidden">
{(
<div className="px-4 sm:px-6 py-4 border-b border-gray-200 dark:border-slate-700">
<div className="flex items-center justify-between flex-col sm:flex-row gap-4">
<div className="flex items-center gap-3">
<FileText className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Invoice Preview</h2>
<div className="hidden sm:flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
<span></span>
<span>{pdfPageSize} Format</span>
</div>
</div>
<div className="flex items-center gap-2">
{/* Back Button */}
<button
onClick={handleEditInvoice}
className="flex items-center gap-2 px-3 py-2 text-gray-600 dark:text-gray-300 hover:text-gray-800 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-slate-700 rounded-lg transition-colors"
>
<ArrowLeft className="h-4 w-4" />
<span className="hidden sm:inline">Back</span>
</button>
{/* Paper Size Selector */}
<div className="flex items-center gap-2">
<label className="text-sm text-gray-600 dark:text-gray-300">Size:</label>
<select
value={pdfPageSize}
onChange={(e) => {
setPdfPageSize(e.target.value);
localStorage.setItem('pdfPageSize', e.target.value);
}}
className="px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-slate-700 text-gray-900 dark:text-gray-100"
>
<option value="A4">A4</option>
<option value="F4">F4</option>
</select>
</div>
{/* Download PDF Button */}
<button
onClick={handleDownloadPDF}
disabled={isGenerating}
className="flex items-center gap-2 px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors disabled:opacity-50"
>
<Download className="h-4 w-4" />
<span className="hidden sm:inline">{isGenerating ? 'Generating...' : 'Download PDF'}</span>
</button>
</div>
</div>
</div>
)}
<div className="p-4 sm:p-6">
<div className="bg-white rounded-lg shadow-lg overflow-x-auto">
{/* PDF-Ready Invoice Content */}
<div
id="invoice-content"
className="bg-white"
style={{
maxWidth: pdfPageSize === 'A4' ? '720px' : '750px',
width: '100%',
minHeight: pdfPageSize === 'A4' ? '720px' : '750px',
margin: '0 auto',
position: 'relative',
}}
>
<SelectedTemplateComponent
invoiceData={invoiceData}
formatNumber={formatNumber}
formatCurrency={formatCurrency}
/>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default InvoicePreview;

View File

@@ -0,0 +1,461 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { ArrowLeft, Download, FileText, Plus } from 'lucide-react';
import html2pdf from 'html2pdf.js';
const InvoicePreviewMinimal = () => {
const navigate = useNavigate();
const [invoiceData, setInvoiceData] = useState(null);
const [pdfPageSize, setPdfPageSize] = useState('A4');
const [isGenerating, setIsGenerating] = useState(false);
// Load invoice data from localStorage
useEffect(() => {
try {
const savedInvoice = localStorage.getItem('currentInvoice');
const savedPageSize = localStorage.getItem('pdfPageSize');
if (savedInvoice) {
const parsedInvoice = JSON.parse(savedInvoice);
setInvoiceData(parsedInvoice);
// Set page title with invoice number
document.title = `Invoice Preview - ${parsedInvoice.invoiceNumber || 'Draft'} | DevTools`;
} else {
// No invoice data, redirect back to editor
navigate('/invoice-editor');
}
if (savedPageSize) {
setPdfPageSize(savedPageSize);
}
} catch (error) {
console.error('Failed to load invoice data:', error);
navigate('/invoice-editor');
}
}, [navigate]);
// Format number with thousand separator
const formatNumber = (num) => {
if (!invoiceData?.settings?.thousandSeparator) return num.toString();
return num.toLocaleString();
};
// Format currency
const formatCurrency = (amount, useThousandSeparator = false) => {
const symbol = invoiceData?.settings?.currency?.symbol || '$';
const formattedAmount = useThousandSeparator ? formatNumber(amount) : amount.toFixed(2);
return `${symbol}${formattedAmount}`;
};
// Generate PDF from the visible invoice
const handleDownloadPDF = async () => {
if (!invoiceData) return;
setIsGenerating(true);
try {
const element = document.getElementById('minimal-invoice-content');
if (!element) {
throw new Error('Invoice content not found');
}
const opt = {
margin: [15, 15, 15, 15],
filename: `Invoice-${invoiceData.invoiceNumber || new Date().toISOString().split('T')[0]}.pdf`,
image: { type: 'jpeg', quality: 0.98 },
html2canvas: {
scale: 2,
useCORS: true,
letterRendering: true,
allowTaint: true,
backgroundColor: '#ffffff',
width: pdfPageSize === 'A4' ? 794 : 816,
height: pdfPageSize === 'A4' ? 1123 : 1248
},
jsPDF: {
unit: 'px',
format: pdfPageSize === 'A4' ? [794, 1123] : [816, 1248],
orientation: 'portrait'
}
};
await html2pdf().set(opt).from(element).save();
} catch (error) {
console.error('PDF generation failed:', error);
alert('Failed to generate PDF. Please try again.');
} finally {
setIsGenerating(false);
}
};
// Navigate back to editor
const handleEditInvoice = () => {
navigate('/invoice-editor');
};
// Create new invoice
const handleNewInvoice = () => {
localStorage.removeItem('currentInvoice');
navigate('/invoice-editor');
};
if (!invoiceData) {
return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">Loading invoice...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-100">
{/* Top Action Bar */}
<div className="bg-white shadow-sm border-b sticky top-0 z-10">
<div className="max-w-7xl mx-auto px-4 py-3">
<div className="flex items-center justify-between">
{/* Left Actions */}
<div className="flex items-center gap-4">
<button
onClick={handleEditInvoice}
className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition-colors"
>
<ArrowLeft className="h-4 w-4" />
Edit Invoice
</button>
<div className="h-6 w-px bg-gray-300"></div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<FileText className="h-4 w-4" />
<span className="font-medium">Minimal Invoice Preview</span>
<span className="text-gray-400"></span>
<span>{pdfPageSize} Format</span>
</div>
</div>
{/* Right Actions */}
<div className="flex items-center gap-3">
<button
onClick={handleNewInvoice}
className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition-colors"
>
<Plus className="h-4 w-4" />
New Invoice
</button>
<button
onClick={handleDownloadPDF}
disabled={isGenerating}
className="flex items-center gap-2 px-6 py-2 bg-amber-500 hover:bg-amber-600 disabled:bg-amber-400 text-white rounded-lg transition-colors font-medium"
>
<Download className="h-4 w-4" />
{isGenerating ? 'Generating...' : 'Download PDF'}
</button>
</div>
</div>
</div>
</div>
{/* Invoice Preview */}
<div className="max-w-4xl mx-auto p-6">
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
{/* Minimal Invoice Content - Recreating the exact design from image */}
<div
id="minimal-invoice-content"
className="bg-white"
style={{
width: pdfPageSize === 'A4' ? '794px' : '816px',
minHeight: pdfPageSize === 'A4' ? '1123px' : '1248px',
margin: '0 auto',
padding: '60px',
fontSize: '14px',
lineHeight: '1.5',
fontFamily: 'system-ui, -apple-system, sans-serif',
color: '#000000'
}}
>
{/* Header Section */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '80px' }}>
{/* Company Logo & Name */}
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
{/* Golden Star Logo */}
<div style={{
width: '40px',
height: '40px',
background: '#D4AF37',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '20px',
color: 'white',
fontWeight: 'bold'
}}>
</div>
<div>
<h1 style={{
fontSize: '24px',
fontWeight: 'bold',
color: '#000000',
margin: '0',
lineHeight: '1.2'
}}>
{invoiceData.company.name || 'Borcelle'}
</h1>
<p style={{
fontSize: '14px',
color: '#666666',
margin: '2px 0 0 0',
fontWeight: '400'
}}>
Meet All Your Needs
</p>
</div>
</div>
{/* Invoice Title */}
<h2 style={{
fontSize: '48px',
fontWeight: 'bold',
color: '#000000',
margin: '0',
letterSpacing: '2px'
}}>
INVOICE
</h2>
</div>
{/* Invoice Details Section */}
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '60px' }}>
{/* Invoice To */}
<div style={{ flex: 1 }}>
<h3 style={{
fontSize: '16px',
fontWeight: 'bold',
color: '#000000',
marginBottom: '12px'
}}>
Invoice to:
</h3>
<div style={{ fontSize: '16px', fontWeight: 'bold', color: '#000000', marginBottom: '8px' }}>
{invoiceData.client.name || 'Daniel Gallego'}
</div>
<div style={{ fontSize: '14px', color: '#000000', lineHeight: '1.6' }}>
<div>{invoiceData.client.address || '123 Anywhere St.,'}</div>
<div>{invoiceData.client.city || 'Any City, ST 12345'}</div>
</div>
</div>
{/* Invoice Number & Date */}
<div style={{ textAlign: 'right', minWidth: '200px' }}>
<div style={{ marginBottom: '12px' }}>
<span style={{ fontSize: '16px', fontWeight: 'bold', color: '#000000' }}>Invoice# </span>
<span style={{ fontSize: '16px', color: '#000000', marginLeft: '20px' }}>
{invoiceData.invoiceNumber || '52131'}
</span>
</div>
<div>
<span style={{ fontSize: '16px', fontWeight: 'bold', color: '#000000' }}>Date </span>
<span style={{ fontSize: '16px', color: '#000000', marginLeft: '20px' }}>
{invoiceData.date || '01/02/2023'}
</span>
</div>
</div>
</div>
{/* Items Table */}
<div style={{ marginBottom: '40px' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
{/* Table Header */}
<thead>
<tr style={{ borderBottom: '2px solid #000000' }}>
<th style={{
padding: '12px 0',
textAlign: 'left',
fontSize: '16px',
fontWeight: 'bold',
color: '#000000'
}}>Item</th>
<th style={{
padding: '12px 0',
textAlign: 'center',
fontSize: '16px',
fontWeight: 'bold',
color: '#000000'
}}>Quantity</th>
<th style={{
padding: '12px 0',
textAlign: 'center',
fontSize: '16px',
fontWeight: 'bold',
color: '#000000'
}}>Unit Price</th>
<th style={{
padding: '12px 0',
textAlign: 'right',
fontSize: '16px',
fontWeight: 'bold',
color: '#000000'
}}>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',
fontSize: '14px',
color: '#000000'
}}>
{item.description}
</td>
<td style={{
padding: '16px 0',
textAlign: 'center',
fontSize: '14px',
color: '#000000'
}}>
{formatNumber(item.quantity)}
</td>
<td style={{
padding: '16px 0',
textAlign: 'center',
fontSize: '14px',
color: '#000000'
}}>
{formatCurrency(item.rate, true)}
</td>
<td style={{
padding: '16px 0',
textAlign: 'right',
fontSize: '14px',
color: '#000000'
}}>
{formatCurrency(item.amount, true)}
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Payment Method & Totals Section */}
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '80px' }}>
{/* Payment Method */}
<div style={{ flex: 1, maxWidth: '300px' }}>
<h3 style={{
fontSize: '16px',
fontWeight: 'bold',
color: '#000000',
marginBottom: '16px'
}}>
PAYMENT METHOD
</h3>
<div style={{ fontSize: '14px', color: '#000000', lineHeight: '1.6' }}>
<div style={{ marginBottom: '4px' }}>Rimberio Bank</div>
<div style={{ marginBottom: '4px' }}>Account Name: Alfredo Torres</div>
<div style={{ marginBottom: '4px' }}>Account No.: 0123 4567 8901</div>
<div>Pay by: 23 June 2023</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>
{invoiceData.taxRate > 0 && (
<div style={{ marginBottom: '12px' }}>
<span style={{ fontSize: '14px', color: '#000000' }}>
Tax ({formatNumber(invoiceData.taxRate)}%)
</span>
<span style={{ fontSize: '14px', color: '#000000', marginLeft: '40px' }}>
{formatCurrency(invoiceData.taxAmount, true)}
</span>
</div>
)}
<div style={{
borderTop: '1px solid #000000',
paddingTop: '12px',
marginTop: '16px'
}}>
<span style={{ fontSize: '18px', fontWeight: 'bold', color: '#000000' }}>Total</span>
<span style={{
fontSize: '18px',
fontWeight: 'bold',
color: '#000000',
marginLeft: '40px'
}}>
{formatCurrency(invoiceData.total, true)}
</span>
</div>
</div>
</div>
{/* Footer Section */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', marginTop: '120px' }}>
{/* Thank You Message */}
<div>
<p style={{ fontSize: '16px', color: '#000000', margin: '0' }}>
Thank you for your business!
</p>
</div>
{/* Signature Line */}
<div style={{ textAlign: 'center' }}>
<div style={{
width: '200px',
borderBottom: '2px solid #D4AF37',
marginBottom: '8px'
}}></div>
<p style={{ fontSize: '12px', color: '#666666', margin: '0' }}>
Authorized Signed
</p>
</div>
</div>
{/* Bottom Golden Bar */}
<div style={{
position: 'absolute',
bottom: '0',
left: '0',
right: '0',
height: '60px',
background: '#D4AF37',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '40px',
color: 'white',
fontSize: '14px'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span>📞</span>
<span>123-456-7890</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span>📍</span>
<span>123 Anywhere St., Any City</span>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default InvoicePreviewMinimal;

View File

@@ -3,6 +3,7 @@ import { Code, AlertCircle, CheckCircle, Edit3 } from 'lucide-react';
import ToolLayout from '../components/ToolLayout';
import CopyButton from '../components/CopyButton';
import StructuredEditor from '../components/StructuredEditor';
import CodeEditor from '../components/CodeEditor';
const JsonTool = () => {
const [input, setInput] = useState('');
@@ -196,11 +197,13 @@ const JsonTool = () => {
</label>
<div className="relative">
{editorMode === 'text' ? (
<textarea
<CodeEditor
value={input}
onChange={(e) => setInput(e.target.value)}
onChange={(value) => setInput(value)}
language="json"
placeholder="Paste your JSON here..."
className="tool-input h-96"
height="400px"
className="w-full"
/>
) : (
<div className="min-h-96">
@@ -219,13 +222,17 @@ const JsonTool = () => {
Output
</label>
<div className="relative">
<textarea
<CodeEditor
value={output}
readOnly
language="json"
readOnly={true}
placeholder="Formatted JSON will appear here..."
className="tool-input h-96 bg-gray-50 dark:bg-gray-800"
height="400px"
className="w-full"
/>
{output && <CopyButton text={output} />}
<div className="absolute top-2 right-2">
<CopyButton text={output} />
</div>
</div>
</div>
</div>

View File

@@ -1,13 +1,41 @@
import React, { useState, useRef, useCallback } from 'react';
import { Upload, FileText, Workflow, Table, Globe, Plus, AlertTriangle, BrushCleaning, Code, Braces, Download, Edit3 } from 'lucide-react';
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { Plus, Upload, FileText, Globe, Edit3, Download, Workflow, Table, Braces, Code, AlertTriangle } from 'lucide-react';
import ToolLayout from '../components/ToolLayout';
import StructuredEditor from '../components/StructuredEditor';
import MindmapView from '../components/MindmapView';
import PostmanTable from '../components/PostmanTable';
import CodeEditor from '../components/CodeEditor';
// Hook to detect dark mode
const useDarkMode = () => {
const [isDark, setIsDark] = useState(() => {
return document.documentElement.classList.contains('dark');
});
useEffect(() => {
const observer = new MutationObserver(() => {
setIsDark(document.documentElement.classList.contains('dark'));
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
return () => observer.disconnect();
}, []);
return isDark;
};
const ObjectEditor = () => {
console.log(' ObjectEditor component loaded successfully!');
const isDark = useDarkMode();
const [structuredData, setStructuredData] = useState({});
// Sync structured data to localStorage for navigation guard
useEffect(() => {
localStorage.setItem('objectEditorData', JSON.stringify(structuredData));
}, [structuredData]);
const [activeTab, setActiveTab] = useState('create');
const [inputText, setInputText] = useState('');
const [inputFormat, setInputFormat] = useState('');
@@ -581,52 +609,53 @@ const ObjectEditor = () => {
icon={Edit3}
>
{/* Input Section with Tabs */}
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mb-6">
<div className="w-full bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mb-4 sm:mb-6">
{/* Tabs */}
<div className="flex border-b border-gray-200 dark:border-gray-700 overflow-x-auto scrollbar-hide">
<div className="flex min-w-max">
<button
onClick={() => handleTabChange('create')}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === 'create'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
<Plus className="h-4 w-4" />
Create New
<Plus className="h-4 w-4 flex-shrink-0" />
<span className="hidden sm:inline">Create New</span>
<span className="sm:hidden">New</span>
</button>
<button
onClick={() => handleTabChange('url')}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === 'url'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
<Globe className="h-4 w-4" />
<Globe className="h-4 w-4 flex-shrink-0" />
URL
</button>
<button
onClick={() => handleTabChange('paste')}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === 'paste'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
<FileText className="h-4 w-4" />
<FileText className="h-4 w-4 flex-shrink-0" />
Paste
</button>
<button
onClick={() => handleTabChange('open')}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === 'open'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
<Upload className="h-4 w-4" />
<Upload className="h-4 w-4 flex-shrink-0" />
Open
</button>
</div>
@@ -634,7 +663,7 @@ const ObjectEditor = () => {
{/* Tab Content */}
{(activeTab !== 'create' || !createNewCompleted) && (
<div className="p-4">
<div className="p-3 sm:p-4">
{/* Create New Tab Content */}
{activeTab === 'create' && !createNewCompleted && (
<div className="space-y-4">
@@ -756,11 +785,14 @@ const ObjectEditor = () => {
</span>
)}
</div>
<textarea
<CodeEditor
value={inputText}
onChange={(e) => handleInputChange(e.target.value)}
onChange={(value) => handleInputChange(value)}
language={inputFormat === 'JSON' ? 'json' : 'javascript'}
placeholder="Paste JSON or PHP serialized data here..."
className="tool-input h-32 resize-none"
height="200px"
className="w-full"
theme={isDark ? 'dark' : 'light'}
/>
{error && (
<p className="text-sm text-red-600 dark:text-red-400">
@@ -875,12 +907,14 @@ const ObjectEditor = () => {
) : (
<>
{viewMode === 'visual' && (
<div className="min-h-96 overflow-x-auto p-4">
<div className="min-w-max">
<StructuredEditor
initialData={structuredData}
onDataChange={handleStructuredDataChange}
/>
<div className="w-full overflow-hidden">
<div className="w-full overflow-x-auto p-4">
<div className="min-w-max">
<StructuredEditor
initialData={structuredData}
onDataChange={handleStructuredDataChange}
/>
</div>
</div>
</div>
)}
@@ -947,10 +981,13 @@ const ObjectEditor = () => {
<div className="p-4">
{activeExportTab === 'json' && (
<div className="space-y-3">
<textarea
<CodeEditor
value={jsonFormat === 'pretty' ? (outputs.jsonPretty || '{}') : (outputs.jsonMinified || '{}')}
readOnly
className="w-full h-64 font-mono text-sm border border-gray-300 dark:border-gray-600 rounded-md p-3 bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100"
language="json"
readOnly={true}
height="300px"
className="w-full"
theme={isDark ? 'dark' : 'light'}
/>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
@@ -1007,10 +1044,13 @@ const ObjectEditor = () => {
{activeExportTab === 'php' && (
<div className="space-y-3">
<textarea
<CodeEditor
value={outputs.serialized || 'a:0:{}'}
readOnly
className="w-full h-64 font-mono text-sm border border-gray-300 dark:border-gray-600 rounded-md p-3 bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100"
language="javascript"
readOnly={true}
height="300px"
className="w-full"
theme={isDark ? 'dark' : 'light'}
/>
<div className="flex justify-end gap-2">
<button
@@ -1191,7 +1231,7 @@ const InputChangeConfirmationModal = ({ objectData, currentMethod, newMethod, on
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"
>
<BrushCleaning className="h-4 w-4" />
<AlertTriangle className="h-4 w-4" />
Switch & Clear Data
</button>
</div>

View File

@@ -26,6 +26,12 @@ const ReleaseNotes = () => {
// Transform commit messages to user-friendly descriptions
const transformations = [
{
pattern: /feat.*invoice.*editor.*improvements/i,
type: 'feature',
title: 'Invoice Editor Major Update',
description: 'Complete overhaul of Invoice Editor with currency system, PDF generation fixes, improved UI/UX, removed print functionality (use PDF download instead), streamlined preview toolbar, and comprehensive bug fixes'
},
{
pattern: /feat.*enhanced.*what.*new.*feature.*non_tools.*category.*global.*footer/i,
type: 'feature',
@@ -269,8 +275,8 @@ const ReleaseNotes = () => {
return (
<ToolLayout
title="Release Notes"
description="Stay updated with the latest features, improvements, and fixes"
title=""
description=""
>
<div className="max-w-4xl mx-auto">
{/* Header */}
@@ -346,18 +352,21 @@ const ReleaseNotes = () => {
return (
<div key={release.hash} className={`p-6 ${index !== dayReleases.length - 1 ? 'border-b border-gray-100 dark:border-gray-700' : ''}`}>
<div className="flex items-start space-x-4">
<div className={`flex-shrink-0 p-2 rounded-lg ${typeConfig.bgColor}`}>
<div className="flex flex-col md:flex-row items-start md:space-x-4">
<div className={`flex-shrink-0 p-2 rounded-lg ${typeConfig.bgColor} flex items-center space-x-2 mb-2`}>
<div className={typeConfig.color}>
{typeConfig.icon}
</div>
<span className={`block md:hidden px-2 py-1 text-xs font-medium rounded-full ${typeConfig.bgColor} ${typeConfig.color}`}>
{typeConfig.label}
</span>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2 mb-2">
<h4 className="font-semibold text-gray-900 dark:text-gray-100">
{release.title}
</h4>
<span className={`px-2 py-1 text-xs font-medium rounded-full ${typeConfig.bgColor} ${typeConfig.color}`}>
<span className={`hidden md:block px-2 py-1 text-xs font-medium rounded-full ${typeConfig.bgColor} ${typeConfig.color}`}>
{typeConfig.label}
</span>
</div>

View File

@@ -117,7 +117,6 @@ const SerializeTool = () => {
index++; // Skip opening '"'
const byteLength = parseInt(lenStr);
console.log(`Parsing string with declared length: ${byteLength}, starting at position: ${index}`);
if (isNaN(byteLength) || byteLength < 0) {
throw new Error(`Invalid string length: ${lenStr}`);
@@ -153,14 +152,9 @@ const SerializeTool = () => {
const stringVal = str.substring(startIndex, endQuotePos);
const actualByteLength = new TextEncoder().encode(stringVal).length;
console.log(`String parsing: declared ${byteLength} bytes, actual ${actualByteLength} bytes, content length ${stringVal.length} chars`);
console.log(`Extracted string: "${stringVal.substring(0, 50)}${stringVal.length > 50 ? '...' : ''}"`);
// Move index to after the closing '";'
index = endQuotePos + 2;
console.log(`After string parsing, index is at: ${index}, next chars: "${str.substring(index, index + 5)}"`);
// Warn about byte length mismatch but continue parsing
if (actualByteLength !== byteLength) {
console.warn(`Warning: String byte length mismatch - declared ${byteLength}, actual ${actualByteLength}`);

View File

@@ -1,32 +1,41 @@
import React, { useState } from "react";
import {
Table,
Database,
Download,
Upload,
FileText,
Search,
Plus,
X,
Braces,
Code,
Eye,
Trash2,
ArrowUpDown,
Edit3,
Globe,
Maximize2,
Minimize2,
BrushCleaning,
AlertTriangle,
} from "lucide-react";
import ToolLayout from "../components/ToolLayout";
import React, { useState, useEffect } from 'react';
import { Plus, Upload, FileText, Globe, Download, X, Table, Trash2, Database, Braces, Code, Eye, Minimize2, Maximize2, Search, ArrowUpDown, AlertTriangle, Edit3 } from 'lucide-react';
import ToolLayout from '../components/ToolLayout';
import CodeEditor from '../components/CodeEditor';
import StructuredEditor from "../components/StructuredEditor";
import Papa from "papaparse";
// Hook to detect dark mode
const useDarkMode = () => {
const [isDark, setIsDark] = useState(() => {
return document.documentElement.classList.contains('dark');
});
useEffect(() => {
const observer = new MutationObserver(() => {
setIsDark(document.documentElement.classList.contains('dark'));
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
return () => observer.disconnect();
}, []);
return isDark;
};
const TableEditor = () => {
const isDark = useDarkMode();
const [data, setData] = useState([]);
const [columns, setColumns] = useState([]);
// Sync table data to localStorage for navigation guard
useEffect(() => {
localStorage.setItem('tableEditorData', JSON.stringify(data));
}, [data]);
const [inputText, setInputText] = useState("");
const [url, setUrl] = useState("");
const [isLoading, setIsLoading] = useState(false);
@@ -498,7 +507,7 @@ const TableEditor = () => {
const processedValues = values.map((val) => {
val = val.trim();
val = String(val).trim();
// Remove quotes and handle NULL
if (val === "NULL") return "";
if (val.startsWith("'") && val.endsWith("'")) {
@@ -1608,7 +1617,7 @@ const TableEditor = () => {
};
}
if (hasNumber && values.every((val) => !isNaN(val) && val.trim() !== "")) {
if (hasNumber && values.every((val) => !isNaN(val) && String(val).trim() !== "")) {
if (allIntegers) {
const maxVal = Math.max(...values.map((v) => Math.abs(Number(v))));
if (maxVal < 128)
@@ -1783,13 +1792,13 @@ const TableEditor = () => {
icon={Table}
>
{/* Input Section with Tabs */}
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mb-6">
<div className="w-full bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mb-4 sm:mb-6">
{/* Tabs */}
<div className="flex border-b border-gray-200 dark:border-gray-700 overflow-x-auto scrollbar-hide">
<div className="flex min-w-max">
<button
onClick={() => handleTabChange("create")}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === "create"
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
@@ -1800,7 +1809,7 @@ const TableEditor = () => {
</button>
<button
onClick={() => handleTabChange("url")}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === "url"
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
@@ -1811,7 +1820,7 @@ const TableEditor = () => {
</button>
<button
onClick={() => handleTabChange("paste")}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === "paste"
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
@@ -1822,7 +1831,7 @@ const TableEditor = () => {
</button>
<button
onClick={() => handleTabChange("upload")}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === "upload"
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
@@ -1997,11 +2006,13 @@ const TableEditor = () => {
{activeTab === "paste" && (
<div className="space-y-3">
<textarea
<CodeEditor
value={inputText}
onChange={(e) => setInputText(e.target.value)}
onChange={(value) => setInputText(value)}
language="javascript"
placeholder="Paste CSV, TSV, JSON, or SQL INSERT statements here..."
className="tool-input h-32 resize-none"
height="128px"
theme={isDark ? 'dark' : 'light'}
/>
<div className="flex items-center justify-between">
<label className="flex items-center text-sm text-gray-600 dark:text-gray-400">
@@ -2055,10 +2066,10 @@ const TableEditor = () => {
{data.length > 0 && (
<div
className={`bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden ${
className={`bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 min-w-0 ${
isTableFullscreen
? "fixed inset-0 z-50 rounded-none border-0 shadow-none"
: ""
? "fixed inset-0 z-50 rounded-none border-0 shadow-none overflow-hidden"
: "overflow-x-auto"
}`}
>
{/* Header */}
@@ -2103,7 +2114,7 @@ const TableEditor = () => {
onClick={clearData}
className="flex items-center gap-2 px-3 py-2 text-sm font-medium transition-colors border-l border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700"
>
<BrushCleaning className="h-4 w-4" />
<AlertTriangle className="h-4 w-4" />
<span className="hidden sm:inline">Clear All</span>
</button>
</div>
@@ -2141,8 +2152,8 @@ const TableEditor = () => {
</div>
)}
{/* Table Body - Edge to Edge */}
<div className="flex flex-col h-full">
{/* Table Body - edge to edge */}
<div className="flex flex-col h-full min-w-0">
{/* Controls */}
<div className="px-4 py-3 flex flex-col sm:flex-row items-start sm:items-center gap-3 sm:gap-4 border-b border-gray-200 dark:border-gray-700">
{/* Search Bar */}
@@ -2215,7 +2226,8 @@ const TableEditor = () => {
{/* Table */}
<div
className={`overflow-auto ${isTableFullscreen ? "max-h-[calc(100vh-200px)]" : "max-h-[500px]"}`}
className={`overflow-auto w-full ${isTableFullscreen ? "max-h-[calc(100vh-200px)]" : "max-h-[500px]"}`}
style={{ maxWidth: '100%' }}
>
<table className="w-full">
<thead className="bg-gray-50 dark:bg-gray-700 sticky top-0 z-10">
@@ -2616,7 +2628,7 @@ const TableEditor = () => {
<div className="flex border-b border-gray-200 dark:border-gray-700">
<button
onClick={() => setExportTab("json")}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
exportTab === "json"
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
@@ -2627,7 +2639,7 @@ const TableEditor = () => {
</button>
<button
onClick={() => setExportTab("csv")}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
exportTab === "csv"
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
@@ -2638,7 +2650,7 @@ const TableEditor = () => {
</button>
<button
onClick={() => setExportTab("tsv")}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
exportTab === "tsv"
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
@@ -2649,7 +2661,7 @@ const TableEditor = () => {
</button>
<button
onClick={() => setExportTab("sql")}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
exportTab === "sql"
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
@@ -2664,10 +2676,12 @@ const TableEditor = () => {
<div className="p-4">
{exportTab === "json" && (
<div className="space-y-3">
<textarea
<CodeEditor
value={getExportData("json")}
readOnly
className="w-full h-64 font-mono text-sm border border-gray-300 dark:border-gray-600 rounded-md p-3 bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100"
language="json"
readOnly={true}
height="256px"
theme={isDark ? 'dark' : 'light'}
/>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
@@ -2718,10 +2732,12 @@ const TableEditor = () => {
{exportTab === "csv" && (
<div className="space-y-3">
<textarea
<CodeEditor
value={getExportData("csv")}
readOnly
className="w-full h-64 font-mono text-sm border border-gray-300 dark:border-gray-600 rounded-md p-3 bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100"
language="javascript"
readOnly={true}
height="256px"
theme={isDark ? 'dark' : 'light'}
/>
<div className="flex justify-end gap-2">
<button
@@ -2748,10 +2764,12 @@ const TableEditor = () => {
{exportTab === "tsv" && (
<div className="space-y-3">
<textarea
<CodeEditor
value={getExportData("tsv")}
readOnly
className="w-full h-64 font-mono text-sm border border-gray-300 dark:border-gray-600 rounded-md p-3 bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100"
language="javascript"
readOnly={true}
height="256px"
theme={isDark ? 'dark' : 'light'}
/>
<div className="flex justify-end gap-2">
<button
@@ -2821,10 +2839,12 @@ const TableEditor = () => {
</div>
</div>
<textarea
<CodeEditor
value={getExportData("sql")}
readOnly
className="w-full h-64 font-mono text-sm border border-gray-300 dark:border-gray-600 rounded-md p-3 bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100"
language="javascript"
readOnly={true}
height="256px"
theme={isDark ? 'dark' : 'light'}
/>
{/* Intelligent Schema Analysis */}
@@ -3162,7 +3182,7 @@ const ClearConfirmationModal = ({
onClick={onConfirm}
className="px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-md transition-colors flex items-center gap-2"
>
<BrushCleaning className="h-4 w-4" />
<AlertTriangle className="h-4 w-4" />
Clear All Data
</button>
</div>
@@ -3559,7 +3579,7 @@ const InputChangeConfirmationModal = ({
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"
>
<BrushCleaning className="h-4 w-4" />
<AlertTriangle className="h-4 w-4" />
Switch & Clear Data
</button>
</div>

View File

@@ -8,15 +8,11 @@ const GA_MEASUREMENT_ID = 'G-S3K5P2PWV6';
export const initGA = () => {
// Don't initialize if already loaded
if (window.gtag) {
console.log('🔍 Google Analytics already initialized');
return;
}
// Show different behavior in development vs production
const isDevelopment = process.env.NODE_ENV !== 'production';
if (isDevelopment) {
console.log('🔍 [DEV] Initializing Google Analytics in development mode');
}
// Initialize gtag function first (required for Consent Mode)
window.dataLayer = window.dataLayer || [];
@@ -51,8 +47,6 @@ export const initGA = () => {
// Apply any stored consent preferences
applyStoredConsent();
const mode = isDevelopment ? '[DEV]' : '[PROD]';
console.log(`🔍 ${mode} Google Analytics initialized with Consent Mode v2`);
};
};
@@ -61,7 +55,6 @@ export const trackPageView = (path, title) => {
const isDevelopment = process.env.NODE_ENV !== 'production';
if (!window.gtag) {
console.log(`📊 [DEV] Page view: ${path} - ${title} (gtag not loaded)`);
return;
}
@@ -71,7 +64,6 @@ export const trackPageView = (path, title) => {
});
const mode = isDevelopment ? '[DEV]' : '[PROD]';
console.log(`📊 ${mode} Page view tracked: ${path}`);
};
// Track custom events
@@ -79,7 +71,6 @@ export const trackEvent = (eventName, parameters = {}) => {
const isDevelopment = process.env.NODE_ENV !== 'production';
if (!window.gtag) {
console.log(`📊 [DEV] Event: ${eventName}`, parameters, '(gtag not loaded)');
return;
}
@@ -88,9 +79,6 @@ export const trackEvent = (eventName, parameters = {}) => {
// Add privacy-friendly defaults
anonymize_ip: true,
});
const mode = isDevelopment ? '[DEV]' : '[PROD]';
console.log(`📊 ${mode} Event tracked: ${eventName}`);
};
// Predefined events for common actions

View File

@@ -117,9 +117,6 @@ export const addCompatibilityFixes = () => {
export const initBrowserCompat = () => {
const browserInfo = getBrowserInfo();
// Log browser info for debugging
console.log('Browser Info:', browserInfo);
// Add compatibility fixes
addCompatibilityFixes();

View File

@@ -161,7 +161,6 @@ export const applyStoredConsent = () => {
if (stored && window.gtag) {
const { timestamp, version, ...consentChoices } = stored;
window.gtag('consent', 'update', consentChoices);
console.log('🍪 Applied stored consent:', consentChoices);
}
};

File diff suppressed because one or more lines are too long

View File

@@ -83,6 +83,50 @@ export const generateSEOData = (path) => {
}
};
case '/release-notes':
return {
title: `Release Notes - ${SITE_CONFIG.title}`,
description: 'Latest updates, features, and improvements to our developer tools. Stay up-to-date with new releases and enhancements.',
keywords: 'release notes, updates, changelog, new features, developer tools updates',
canonical: `${baseUrl}/release-notes`,
ogType: 'article',
noindex: false,
structuredData: {
'@context': 'https://schema.org',
'@type': 'WebPage',
name: 'Release Notes',
description: 'Latest updates and release notes for Dewe.Dev developer tools',
url: `${baseUrl}/release-notes`,
isPartOf: {
'@type': 'WebSite',
name: SITE_CONFIG.title,
url: baseUrl
}
}
};
case '/invoice-preview':
return {
title: `Invoice Preview - ${SITE_CONFIG.title}`,
description: 'Preview and download your professional invoice with customizable templates.',
keywords: 'invoice preview, pdf generation, invoice templates, professional invoices',
canonical: `${baseUrl}/invoice-preview`,
ogType: 'website',
noindex: true, // Don't index preview pages
structuredData: {
'@context': 'https://schema.org',
'@type': 'WebPage',
name: 'Invoice Preview',
description: 'Invoice preview and PDF generation tool',
url: `${baseUrl}/invoice-preview`,
isPartOf: {
'@type': 'WebSite',
name: SITE_CONFIG.title,
url: baseUrl
}
}
};
default:
if (tool) {
const toolKeywords = tool.tags.join(', ').toLowerCase();

View File

@@ -102,9 +102,6 @@ export const buildSitemap = () => {
const robotsContent = generateRobotsTxt();
fs.writeFileSync(path.join(publicDir, 'robots.txt'), robotsContent, 'utf8');
console.log('✅ Sitemap and robots.txt generated successfully!');
console.log(`📍 Sitemap: ${SITE_CONFIG.domain}/sitemap.xml`);
console.log(`🤖 Robots: ${SITE_CONFIG.domain}/robots.txt`);
};
// Runtime sitemap data for dynamic generation