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:
257
package-lock.json
generated
257
package-lock.json
generated
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
1
public/utils/currencies.json
Normal file
1
public/utils/currencies.json
Normal file
File diff suppressed because one or more lines are too long
@@ -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 />} />
|
||||
|
||||
99
src/components/CodeEditor.js
Normal file
99
src/components/CodeEditor.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import React from 'react';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { json } from '@codemirror/lang-json';
|
||||
import { html } from '@codemirror/lang-html';
|
||||
import { css } from '@codemirror/lang-css';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
|
||||
const CodeEditor = ({
|
||||
value,
|
||||
onChange,
|
||||
language = 'json',
|
||||
placeholder = '',
|
||||
readOnly = false,
|
||||
height = '300px',
|
||||
className = '',
|
||||
theme = 'light'
|
||||
}) => {
|
||||
// Language extensions mapping
|
||||
const getLanguageExtension = (lang) => {
|
||||
switch (lang.toLowerCase()) {
|
||||
case 'javascript':
|
||||
case 'js':
|
||||
return [javascript()];
|
||||
case 'json':
|
||||
return [json()];
|
||||
case 'html':
|
||||
return [html()];
|
||||
case 'css':
|
||||
return [css()];
|
||||
default:
|
||||
return [json()]; // Default to JSON
|
||||
}
|
||||
};
|
||||
|
||||
// Theme configuration
|
||||
const getTheme = () => {
|
||||
if (theme === 'dark') {
|
||||
return oneDark;
|
||||
}
|
||||
return undefined; // Use default light theme
|
||||
};
|
||||
|
||||
// Extensions
|
||||
const extensions = [
|
||||
...getLanguageExtension(language),
|
||||
EditorView.theme({
|
||||
'&': {
|
||||
fontSize: '14px',
|
||||
},
|
||||
'.cm-content': {
|
||||
padding: '16px',
|
||||
minHeight: height,
|
||||
},
|
||||
'.cm-focused': {
|
||||
outline: 'none',
|
||||
},
|
||||
'.cm-editor': {
|
||||
borderRadius: '8px',
|
||||
},
|
||||
'.cm-scroller': {
|
||||
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace',
|
||||
},
|
||||
}),
|
||||
EditorView.lineWrapping,
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={`border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden ${className}`}>
|
||||
<CodeMirror
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
extensions={extensions}
|
||||
theme={getTheme()}
|
||||
placeholder={placeholder}
|
||||
readOnly={readOnly}
|
||||
basicSetup={{
|
||||
lineNumbers: true,
|
||||
foldGutter: true,
|
||||
dropCursor: false,
|
||||
allowMultipleSelections: false,
|
||||
indentOnInput: true,
|
||||
bracketMatching: true,
|
||||
closeBrackets: true,
|
||||
autocompletion: true,
|
||||
highlightSelectionMatches: false,
|
||||
searchKeymap: true,
|
||||
}}
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
minHeight: height,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeEditor;
|
||||
@@ -1,8 +1,10 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { Home, Menu, X, ChevronDown, Terminal, Sparkles } from 'lucide-react';
|
||||
import ThemeToggle from './ThemeToggle';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import ToolSidebar from './ToolSidebar';
|
||||
import NavigationConfirmModal from './NavigationConfirmModal';
|
||||
import useNavigationGuard from '../hooks/useNavigationGuard';
|
||||
import { Menu, X, ChevronDown, Terminal, Sparkles, Home } from 'lucide-react';
|
||||
import ThemeToggle from './ThemeToggle';
|
||||
import SEOHead from './SEOHead';
|
||||
import ConsentBanner from './ConsentBanner';
|
||||
import { NON_TOOLS, TOOLS, SITE_CONFIG, getCategoryConfig } from '../config/tools';
|
||||
@@ -10,6 +12,7 @@ import { useAnalytics } from '../hooks/useAnalytics';
|
||||
|
||||
const Layout = ({ children }) => {
|
||||
const location = useLocation();
|
||||
const { showModal, pendingNavigation, handleConfirm, handleCancel, hasUnsavedData, navigateWithGuard } = useNavigationGuard();
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
const dropdownRef = useRef(null);
|
||||
@@ -43,6 +46,9 @@ const Layout = ({ children }) => {
|
||||
|
||||
// Check if we're on a tool page (not homepage)
|
||||
const isToolPage = location.pathname !== '/';
|
||||
|
||||
// Check if we're on invoice preview page (no sidebar needed)
|
||||
const isInvoicePreviewPage = location.pathname === '/invoice-preview';
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 dark:from-slate-900 dark:via-slate-800 dark:to-indigo-900 flex flex-col">
|
||||
@@ -50,10 +56,10 @@ const Layout = ({ children }) => {
|
||||
<SEOHead />
|
||||
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-50 bg-white/80 dark:bg-slate-800/80 backdrop-blur-md shadow-lg border-b border-slate-200/50 dark:border-slate-700/50 flex-shrink-0">
|
||||
<header className="fixed top-0 left-0 right-0 z-50 bg-white/80 dark:bg-slate-800/80 backdrop-blur-md shadow-lg border-b border-slate-200/50 dark:border-slate-700/50 flex-shrink-0">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
<Link to="/" className="flex items-center space-x-3 group">
|
||||
<button onClick={() => navigateWithGuard('/')} className="flex items-center space-x-3 group">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-blue-500 to-purple-500 rounded-lg blur opacity-20 group-hover:opacity-40 transition-opacity"></div>
|
||||
<div className="relative bg-gradient-to-r from-blue-500 to-purple-500 p-2 rounded-lg">
|
||||
@@ -63,23 +69,26 @@ const Layout = ({ children }) => {
|
||||
<span className="text-xl font-bold bg-gradient-to-r from-blue-600 via-purple-600 to-indigo-600 bg-clip-text text-transparent">
|
||||
{SITE_CONFIG.title}
|
||||
</span>
|
||||
</Link>
|
||||
</button>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
{/* Desktop Navigation - only show on homepage */}
|
||||
{!isToolPage && (
|
||||
<nav className="hidden md:flex items-center space-x-6">
|
||||
<Link
|
||||
to="/"
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDropdownOpen(false);
|
||||
navigateWithGuard('/');
|
||||
}}
|
||||
className={`flex items-center space-x-2 px-4 py-2 rounded-xl text-sm font-medium transition-all duration-300 ${
|
||||
isActive('/')
|
||||
? 'bg-gradient-to-r from-blue-500 to-purple-500 text-white shadow-lg'
|
||||
: 'text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white hover:bg-white/50 dark:hover:bg-slate-700/50'
|
||||
: 'text-slate-600 dark:text-slate-300 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-white/50 dark:hover:bg-slate-700/50'
|
||||
}`}
|
||||
>
|
||||
<Home className="h-4 w-4" />
|
||||
<span>Home</span>
|
||||
</Link>
|
||||
</button>
|
||||
|
||||
{/* Tools Dropdown */}
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
@@ -104,11 +113,13 @@ const Layout = ({ children }) => {
|
||||
const categoryConfig = getCategoryConfig(tool.category);
|
||||
|
||||
return (
|
||||
<Link
|
||||
<button
|
||||
key={tool.path}
|
||||
to={tool.path}
|
||||
onClick={() => setIsDropdownOpen(false)}
|
||||
className={`group flex items-center space-x-4 px-4 py-3 text-sm hover:bg-white/50 dark:hover:bg-slate-700/50 transition-all duration-300 ${
|
||||
onClick={() => {
|
||||
setIsDropdownOpen(false);
|
||||
navigateWithGuard(tool.path);
|
||||
}}
|
||||
className={`group flex items-center space-x-4 px-4 py-3 text-sm hover:bg-white/50 dark:hover:bg-slate-700/50 transition-all duration-300 w-full text-left ${
|
||||
isActive(tool.path)
|
||||
? 'bg-gradient-to-r from-blue-50 to-purple-50 dark:from-slate-700 dark:to-slate-600 text-blue-700 dark:text-blue-300'
|
||||
: 'text-slate-700 dark:text-slate-300'
|
||||
@@ -124,7 +135,7 @@ const Layout = ({ children }) => {
|
||||
<div className="opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<ChevronDown className="h-4 w-4 -rotate-90 text-slate-400" />
|
||||
</div>
|
||||
</Link>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -158,7 +169,7 @@ const Layout = ({ children }) => {
|
||||
/>
|
||||
|
||||
{/* Menu */}
|
||||
<div className="md:hidden fixed top-16 left-0 right-0 z-40 bg-white/95 dark:bg-slate-800/95 backdrop-blur-md border-b border-slate-200/50 dark:border-slate-700/50 shadow-lg max-h-[calc(100vh-4rem)] overflow-y-auto">
|
||||
<div className="md:hidden fixed top-16 left-0 right-0 z-40 bg-white/95 dark:bg-slate-800/95 backdrop-blur-md border-b border-slate-200/50 dark:border-slate-700/50 shadow-lg max-h-[calc(100vh-4rem)]">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div className="space-y-2">
|
||||
{/* Non-Tools Section */}
|
||||
@@ -166,11 +177,13 @@ const Layout = ({ children }) => {
|
||||
const IconComponent = tool.icon;
|
||||
|
||||
return (
|
||||
<Link
|
||||
<button
|
||||
key={tool.path}
|
||||
to={tool.path}
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
className={`flex items-center space-x-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300 ${
|
||||
onClick={() => {
|
||||
setIsMobileMenuOpen(false);
|
||||
navigateWithGuard(tool.path);
|
||||
}}
|
||||
className={`flex items-center space-x-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300 w-full text-left ${
|
||||
isActive(tool.path)
|
||||
? 'bg-gradient-to-r from-indigo-500 to-purple-500 text-white shadow-lg'
|
||||
: 'text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white hover:bg-white/50 dark:hover:bg-slate-700/50'
|
||||
@@ -180,7 +193,7 @@ const Layout = ({ children }) => {
|
||||
<IconComponent className={`h-4 w-4 ${isActive(tool.path) ? 'text-white' : 'text-white'}`} />
|
||||
</div>
|
||||
<span>{tool.name}</span>
|
||||
</Link>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -194,11 +207,13 @@ const Layout = ({ children }) => {
|
||||
const categoryConfig = getCategoryConfig(tool.category);
|
||||
|
||||
return (
|
||||
<Link
|
||||
<button
|
||||
key={tool.path}
|
||||
to={tool.path}
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
className={`flex items-center space-x-4 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300 ${
|
||||
onClick={() => {
|
||||
setIsMobileMenuOpen(false);
|
||||
navigateWithGuard(tool.path);
|
||||
}}
|
||||
className={`flex items-center space-x-4 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300 w-full text-left ${
|
||||
isActive(tool.path)
|
||||
? 'bg-gradient-to-r from-blue-50 to-purple-50 dark:from-slate-700 dark:to-slate-600 text-blue-700 dark:text-blue-300'
|
||||
: 'text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white hover:bg-white/50 dark:hover:bg-slate-700/50'
|
||||
@@ -211,7 +226,7 @@ const Layout = ({ children }) => {
|
||||
<div className="font-medium">{tool.name}</div>
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400">{tool.description}</div>
|
||||
</div>
|
||||
</Link>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -222,99 +237,152 @@ const Layout = ({ children }) => {
|
||||
)}
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex flex-1">
|
||||
{/* Tool Sidebar - only show on tool pages */}
|
||||
{isToolPage && (
|
||||
<div className="hidden lg:block flex-shrink-0">
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-1 pt-16">
|
||||
{/* Main Content Area */}
|
||||
<main className="flex-1 flex">
|
||||
{isToolPage ? (
|
||||
<div className="flex flex-1">
|
||||
<ToolSidebar />
|
||||
<div className="flex-1 p-6">
|
||||
<main className="flex-1 flex flex-col">
|
||||
{isToolPage && !isInvoicePreviewPage ? (
|
||||
<div className="block">
|
||||
<div className="hidden lg:block fixed top-16 left-0 z-[9999]">
|
||||
<ToolSidebar navigateWithGuard={navigateWithGuard} />
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col min-h-0 pl-0 lg:pl-16">
|
||||
<div className="flex-1 p-4 sm:p-6 w-full min-w-0 overflow-auto">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : isInvoicePreviewPage ? (
|
||||
<div className="flex-1 flex flex-col">
|
||||
<div className="flex-1">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1">
|
||||
{children}
|
||||
<div className="flex-1 flex flex-col">
|
||||
<div className="flex-1">
|
||||
{children}
|
||||
</div>
|
||||
{/* Global Footer for Homepage */}
|
||||
<footer className="bg-white/30 dark:bg-slate-800/30 backdrop-blur-sm border-t border-slate-200/30 dark:border-slate-700/30 mt-20">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="text-center">
|
||||
<div className="flex items-center justify-center gap-3 mb-4">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-blue-500 to-purple-500 rounded-lg blur opacity-20"></div>
|
||||
<div className="relative bg-gradient-to-r from-blue-500 to-purple-500 p-2 rounded-lg">
|
||||
<Terminal className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-lg font-bold bg-gradient-to-r from-blue-600 via-purple-600 to-indigo-600 bg-clip-text text-transparent">
|
||||
{SITE_CONFIG.title}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-2 mb-3">
|
||||
<div className="w-2 h-2 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full"></div>
|
||||
<span className="text-sm font-medium text-slate-600 dark:text-slate-400">
|
||||
© {SITE_CONFIG.year} {SITE_CONFIG.title}
|
||||
</span>
|
||||
<div className="w-2 h-2 bg-gradient-to-r from-purple-500 to-indigo-500 rounded-full"></div>
|
||||
</div>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-500 mb-4">
|
||||
Built with ❤️ for developers worldwide
|
||||
</p>
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="flex justify-center items-center gap-6 text-xs text-slate-400 dark:text-slate-500">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-1.5 h-1.5 bg-green-500 rounded-full animate-pulse"></div>
|
||||
<span>100% Client-Side</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-1.5 h-1.5 bg-blue-500 rounded-full"></div>
|
||||
<span>Privacy First</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-1.5 h-1.5 bg-purple-500 rounded-full"></div>
|
||||
<span>Open Source</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-xs">
|
||||
<button
|
||||
onClick={() => navigateWithGuard('/release-notes')}
|
||||
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
|
||||
>
|
||||
Release Notes
|
||||
</button>
|
||||
<span className="text-slate-300 dark:text-slate-600">•</span>
|
||||
<button
|
||||
onClick={() => navigateWithGuard('/privacy')}
|
||||
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
|
||||
>
|
||||
Privacy Policy
|
||||
</button>
|
||||
<span className="text-slate-300 dark:text-slate-600">•</span>
|
||||
<button
|
||||
onClick={() => navigateWithGuard('/terms')}
|
||||
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
|
||||
>
|
||||
Terms of Service
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Global Footer */}
|
||||
<footer className="bg-white/30 dark:bg-slate-800/30 backdrop-blur-sm border-t border-slate-200/30 dark:border-slate-700/30 mt-20">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="text-center">
|
||||
<div className="flex items-center justify-center gap-3 mb-4">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-blue-500 to-purple-500 rounded-lg blur opacity-20"></div>
|
||||
<div className="relative bg-gradient-to-r from-blue-500 to-purple-500 p-2 rounded-lg">
|
||||
<Terminal className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
|
||||
{/* Footer for Tool Pages */}
|
||||
{isToolPage && (
|
||||
<footer className="bg-white/30 dark:bg-slate-800/30 backdrop-blur-sm border-t border-slate-200/30 dark:border-slate-700/30">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div className="text-center">
|
||||
<div className="flex items-center justify-center gap-2 mb-2">
|
||||
<div className="w-2 h-2 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full"></div>
|
||||
<span className="text-xs font-medium text-slate-600 dark:text-slate-400">
|
||||
© {SITE_CONFIG.year} {SITE_CONFIG.title}
|
||||
</span>
|
||||
<div className="w-2 h-2 bg-gradient-to-r from-purple-500 to-indigo-500 rounded-full"></div>
|
||||
</div>
|
||||
<span className="text-lg font-bold bg-gradient-to-r from-blue-600 via-purple-600 to-indigo-600 bg-clip-text text-transparent">
|
||||
{SITE_CONFIG.title}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-2 mb-3">
|
||||
<div className="w-2 h-2 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full"></div>
|
||||
<span className="text-sm font-medium text-slate-600 dark:text-slate-400">
|
||||
© {SITE_CONFIG.year} {SITE_CONFIG.title}
|
||||
</span>
|
||||
<div className="w-2 h-2 bg-gradient-to-r from-purple-500 to-indigo-500 rounded-full"></div>
|
||||
</div>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-500 mb-4">
|
||||
Built with ❤️ for developers worldwide
|
||||
</p>
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="flex justify-center items-center gap-6 text-xs text-slate-400 dark:text-slate-500">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-1.5 h-1.5 bg-green-500 rounded-full animate-pulse"></div>
|
||||
<span>100% Client-Side</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-1.5 h-1.5 bg-blue-500 rounded-full"></div>
|
||||
<span>Privacy First</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-1.5 h-1.5 bg-purple-500 rounded-full"></div>
|
||||
<span>Open Source</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-xs">
|
||||
<Link
|
||||
to="/release-notes"
|
||||
<div className="flex items-center justify-center gap-4 text-xs">
|
||||
<button
|
||||
onClick={() => navigateWithGuard('/release-notes')}
|
||||
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
|
||||
>
|
||||
Release Notes
|
||||
</Link>
|
||||
</button>
|
||||
<span className="text-slate-300 dark:text-slate-600">•</span>
|
||||
<Link
|
||||
to="/privacy"
|
||||
<button
|
||||
onClick={() => navigateWithGuard('/privacy')}
|
||||
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</button>
|
||||
<span className="text-slate-300 dark:text-slate-600">•</span>
|
||||
<Link
|
||||
to="/terms"
|
||||
<button
|
||||
onClick={() => navigateWithGuard('/terms')}
|
||||
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
|
||||
>
|
||||
Terms of Service
|
||||
</Link>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</footer>
|
||||
)}
|
||||
|
||||
{/* GDPR Consent Banner */}
|
||||
<ConsentBanner />
|
||||
|
||||
{/* Navigation Confirmation Modal */}
|
||||
<NavigationConfirmModal
|
||||
isOpen={showModal}
|
||||
onConfirm={handleConfirm}
|
||||
onCancel={handleCancel}
|
||||
targetPath={pendingNavigation?.to}
|
||||
hasData={hasUnsavedData}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
115
src/components/NavigationConfirmModal.js
Normal file
115
src/components/NavigationConfirmModal.js
Normal file
@@ -0,0 +1,115 @@
|
||||
import React from 'react';
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
|
||||
const NavigationConfirmModal = ({ isOpen, onConfirm, onCancel, targetPath, hasData }) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
const getDataSummary = () => {
|
||||
try {
|
||||
const invoiceData = localStorage.getItem('currentInvoice');
|
||||
const objectData = localStorage.getItem('objectEditorData');
|
||||
const tableData = localStorage.getItem('tableEditorData');
|
||||
|
||||
const summary = [];
|
||||
|
||||
if (invoiceData) {
|
||||
const parsed = JSON.parse(invoiceData);
|
||||
if (parsed.invoiceNumber) summary.push(`Invoice #${parsed.invoiceNumber}`);
|
||||
if (parsed.company?.name) summary.push(`Company information (${parsed.company.name})`);
|
||||
if (parsed.client?.name) summary.push(`Client information (${parsed.client.name})`);
|
||||
if (parsed.items?.length > 0) summary.push(`${parsed.items.length} line items`);
|
||||
}
|
||||
|
||||
if (objectData) {
|
||||
const parsed = JSON.parse(objectData);
|
||||
if (parsed && Object.keys(parsed).length > 0) {
|
||||
summary.push(`Object data with ${Object.keys(parsed).length} properties`);
|
||||
}
|
||||
}
|
||||
|
||||
if (tableData) {
|
||||
const parsed = JSON.parse(tableData);
|
||||
if (parsed && parsed.length > 0) {
|
||||
summary.push(`Table data with ${parsed.length} rows`);
|
||||
}
|
||||
}
|
||||
|
||||
return summary;
|
||||
} catch (error) {
|
||||
return ['Unsaved data'];
|
||||
}
|
||||
};
|
||||
|
||||
const dataSummary = getDataSummary();
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700 bg-amber-50 dark:bg-amber-900/20">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-shrink-0">
|
||||
<AlertTriangle className="h-6 w-6 text-amber-600 dark:text-amber-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-amber-900 dark:text-amber-100">
|
||||
Confirm Navigation
|
||||
</h3>
|
||||
<p className="text-sm text-amber-700 dark:text-amber-300">
|
||||
You have unsaved data that will be lost
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="px-6 py-4">
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
You currently have unsaved data that will be lost if you leave this page. Are you sure you want to continue?
|
||||
</p>
|
||||
|
||||
{dataSummary.length > 0 && (
|
||||
<div className="bg-gray-50 dark:bg-gray-700 rounded-md p-3 mb-4">
|
||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
You currently have:
|
||||
</p>
|
||||
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
|
||||
{dataSummary.map((item, index) => (
|
||||
<li key={index} className="flex items-center">
|
||||
<span className="w-1.5 h-1.5 bg-amber-500 rounded-full mr-2 flex-shrink-0"></span>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md p-3">
|
||||
<p className="text-blue-800 dark:text-blue-200 text-sm">
|
||||
<strong>Tip:</strong> Consider saving or exporting your current work before proceeding.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-6 py-4 bg-gray-50 dark:bg-gray-700 border-t border-gray-200 dark:border-gray-600 flex justify-end gap-3">
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-md hover:bg-gray-50 dark:hover:bg-gray-500 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={onConfirm}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-amber-600 hover:bg-amber-700 rounded-md transition-colors flex items-center gap-2"
|
||||
>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
Continue & Lose Data
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavigationConfirmModal;
|
||||
@@ -224,11 +224,20 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
|
||||
const renderValue = (value) => {
|
||||
const typeStyle = getTypeStyle(value);
|
||||
const formattedValue = formatValue(value);
|
||||
const hasHtml = isHtmlContent(value);
|
||||
|
||||
return (
|
||||
<span className={`inline-flex items-center space-x-1 px-2 py-1 rounded text-xs font-medium ${typeStyle.color}`}>
|
||||
{typeStyle.icon}
|
||||
<span>{formattedValue}</span>
|
||||
<span className="flex-shrink-0 w-3 h-3 flex items-center justify-center">
|
||||
{typeStyle.icon}
|
||||
</span>
|
||||
<span>
|
||||
{hasHtml && renderHtml ? (
|
||||
<div dangerouslySetInnerHTML={{ __html: String(value) }} />
|
||||
) : (
|
||||
formattedValue
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -241,8 +250,10 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<span className={`inline-flex items-start space-x-2 px-2 py-1 rounded text-xs font-medium ${typeStyle.color}`}>
|
||||
<span className="flex-shrink-0 mt-0.5">{typeStyle.icon}</span>
|
||||
<span className={`inline-flex items-center space-x-2 px-2 py-1 rounded text-xs font-medium ${typeStyle.color}`}>
|
||||
<span className="flex-shrink-0 w-3 h-3 flex items-center justify-center">
|
||||
{typeStyle.icon}
|
||||
</span>
|
||||
<span className="whitespace-pre-wrap break-words flex-1">
|
||||
{hasHtml && renderHtml ? (
|
||||
<div dangerouslySetInnerHTML={{ __html: String(value) }} />
|
||||
@@ -252,33 +263,6 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{/* HTML Toggle Buttons */}
|
||||
{hasHtml && (
|
||||
<div className="absolute -top-1 -right-1 flex">
|
||||
<button
|
||||
onClick={() => setRenderHtml(true)}
|
||||
className={`px-1.5 py-0.5 text-xs rounded-l ${
|
||||
renderHtml
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-200 text-gray-600 hover:bg-gray-300 dark:bg-gray-600 dark:text-gray-300 dark:hover:bg-gray-500'
|
||||
}`}
|
||||
title="Render HTML"
|
||||
>
|
||||
<Eye className="h-3 w-3" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setRenderHtml(false)}
|
||||
className={`px-1.5 py-0.5 text-xs rounded-r ${
|
||||
!renderHtml
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-200 text-gray-600 hover:bg-gray-300 dark:bg-gray-600 dark:text-gray-300 dark:hover:bg-gray-500'
|
||||
}`}
|
||||
title="Show Raw HTML"
|
||||
>
|
||||
<Code className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -324,15 +308,46 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{isArrayView && `${currentData.length} items`}
|
||||
{isObjectView && `${Object.keys(currentData).length} properties`}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{isArrayView && `${currentData.length} items`}
|
||||
{isObjectView && `${Object.keys(currentData).length} properties`}
|
||||
</div>
|
||||
|
||||
{/* Global HTML/Raw Toggle */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">Text Display:</span>
|
||||
<div className="flex rounded-md overflow-hidden border border-gray-300 dark:border-gray-600">
|
||||
<button
|
||||
onClick={() => setRenderHtml(true)}
|
||||
className={`px-2 py-1 text-xs transition-colors ${
|
||||
renderHtml
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-gray-600 dark:text-gray-300 dark:hover:bg-gray-500'
|
||||
}`}
|
||||
title="Render HTML"
|
||||
>
|
||||
<Eye className="h-3 w-3" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setRenderHtml(false)}
|
||||
className={`px-2 py-1 text-xs transition-colors ${
|
||||
!renderHtml
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-gray-600 dark:text-gray-300 dark:hover:bg-gray-500'
|
||||
}`}
|
||||
title="Show Raw Text"
|
||||
>
|
||||
<Code className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="overflow-auto max-h-96">
|
||||
<div className="overflow-auto">
|
||||
{isArrayView ? (
|
||||
// Horizontal table for arrays
|
||||
<table className="w-full">
|
||||
@@ -439,9 +454,6 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
|
||||
<div className="text-lg font-mono text-gray-900 dark:text-gray-100 whitespace-pre-wrap break-words">
|
||||
{formatFullValue(currentData)}
|
||||
</div>
|
||||
<div className="text-sm mt-2">
|
||||
Type: {getValueType(currentData)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2,29 +2,29 @@ import React from 'react';
|
||||
|
||||
const ToolLayout = ({ title, description, children, icon: Icon }) => {
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 w-full" style={{ maxWidth: 'min(80rem, calc(100vw - 2rem))' }}>
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center space-x-3 mb-2">
|
||||
{Icon && <Icon className="h-8 w-8 text-primary-600" />}
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
<div className="mb-6 sm:mb-8">
|
||||
<div className="flex items-center space-x-2 sm:space-x-3 mb-2">
|
||||
{Icon && <Icon className="h-6 w-6 sm:h-8 sm:w-8 text-primary-600 flex-shrink-0" />}
|
||||
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-white truncate">
|
||||
{title}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{description && (
|
||||
<p className="text-gray-600 dark:text-gray-300 text-lg">
|
||||
<p className="text-gray-600 dark:text-gray-300 text-base sm:text-lg">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tool Content */}
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4 sm:space-y-6 w-full">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolLayout;
|
||||
export default ToolLayout;
|
||||
@@ -1,12 +1,25 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { Search, ChevronLeft, ChevronRight, Sparkles } from 'lucide-react';
|
||||
import { NON_TOOLS, TOOLS, SITE_CONFIG } from '../config/tools';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Search, ChevronLeft, ChevronRight, Sparkles, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { NON_TOOLS, TOOLS, SITE_CONFIG, getCategoryConfig } from '../config/tools';
|
||||
import useNavigationGuard from '../hooks/useNavigationGuard';
|
||||
|
||||
const ToolSidebar = () => {
|
||||
const ToolSidebar = ({ navigateWithGuard: propNavigateWithGuard }) => {
|
||||
const location = useLocation();
|
||||
const { navigateWithGuard: hookNavigateWithGuard } = useNavigationGuard();
|
||||
|
||||
// Use prop navigation guard if provided, otherwise use hook
|
||||
const navigateWithGuard = propNavigateWithGuard || hookNavigateWithGuard;
|
||||
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [expandedCategories, setExpandedCategories] = useState({
|
||||
editor: false,
|
||||
encoder: false,
|
||||
formatter: false,
|
||||
analyzer: false
|
||||
});
|
||||
const [hoveredTooltip, setHoveredTooltip] = useState(null);
|
||||
const tooltipTimeoutRef = useRef(null);
|
||||
|
||||
// Filter non-tools and tools separately
|
||||
const filteredNonTools = NON_TOOLS.filter(tool =>
|
||||
@@ -21,10 +34,64 @@ const ToolSidebar = () => {
|
||||
|
||||
const isActive = (path) => location.pathname === path;
|
||||
|
||||
// Toggle category expansion - close others when opening one
|
||||
const toggleCategory = (categoryKey) => {
|
||||
setExpandedCategories(prev => {
|
||||
const isCurrentlyExpanded = prev[categoryKey];
|
||||
|
||||
if (isCurrentlyExpanded) {
|
||||
// If currently expanded, just close it
|
||||
return {
|
||||
...prev,
|
||||
[categoryKey]: false
|
||||
};
|
||||
} else {
|
||||
// If currently closed, close all others and open this one
|
||||
const newState = {
|
||||
editor: false,
|
||||
encoder: false,
|
||||
formatter: false,
|
||||
analyzer: false
|
||||
};
|
||||
newState[categoryKey] = true;
|
||||
return newState;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Tooltip hover handlers
|
||||
const handleTooltipMouseEnter = (categoryKey) => {
|
||||
if (tooltipTimeoutRef.current) {
|
||||
clearTimeout(tooltipTimeoutRef.current);
|
||||
}
|
||||
setHoveredTooltip(categoryKey);
|
||||
};
|
||||
|
||||
const handleTooltipMouseLeave = () => {
|
||||
tooltipTimeoutRef.current = setTimeout(() => {
|
||||
setHoveredTooltip(null);
|
||||
}, 300); // 300ms delay before hiding
|
||||
};
|
||||
|
||||
// Handle navigation with data validation
|
||||
const handleNavigation = (path, event) => {
|
||||
event.preventDefault();
|
||||
navigateWithGuard(path);
|
||||
};
|
||||
|
||||
// Group tools by category
|
||||
const toolsByCategory = {};
|
||||
filteredTools.forEach(tool => {
|
||||
if (!toolsByCategory[tool.category]) {
|
||||
toolsByCategory[tool.category] = [];
|
||||
}
|
||||
toolsByCategory[tool.category].push(tool);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={`bg-white/70 dark:bg-slate-800/70 backdrop-blur-sm border-r border-slate-200/50 dark:border-slate-700/50 transition-all duration-300 sticky top-16 ${
|
||||
<div className={`bg-white/70 dark:bg-slate-800/70 backdrop-blur-sm border-r border-slate-200/50 dark:border-slate-700/50 transition-all duration-300 ${
|
||||
isCollapsed ? 'w-16' : 'w-64'
|
||||
}`} style={{ height: 'calc(100vh - 4rem)' }}>
|
||||
} sticky top-16 ${isCollapsed ? 'overflow-visible' : 'overflow-hidden'}`} style={{ height: 'calc(100vh - 4rem)' }}>
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Sidebar Header */}
|
||||
<div className="p-4 border-b border-slate-200/50 dark:border-slate-700/50">
|
||||
@@ -68,60 +135,83 @@ const ToolSidebar = () => {
|
||||
</div>
|
||||
|
||||
{/* Tools List */}
|
||||
<div className="flex-1 overflow-y-auto py-3">
|
||||
<div className={`flex-1 py-3 ${isCollapsed ? 'overflow-visible' : 'overflow-y-auto'}`}>
|
||||
<nav className="space-y-2 px-3">
|
||||
{/* Render Non-Tools (Home, What's New) */}
|
||||
{/* Render Non-Tools (Home, What's New) with special styling */}
|
||||
{filteredNonTools.map((tool) => {
|
||||
const IconComponent = tool.icon;
|
||||
const isActiveItem = isActive(tool.path);
|
||||
const isWhatsNew = tool.path === '/release-notes';
|
||||
|
||||
return (
|
||||
<Link
|
||||
<a
|
||||
key={tool.path}
|
||||
to={tool.path}
|
||||
className={`group flex items-center text-sm font-medium rounded-xl transition-all duration-300 ${
|
||||
href={tool.path}
|
||||
onClick={(e) => handleNavigation(tool.path, e)}
|
||||
className={`group flex items-center text-sm font-medium rounded-xl transition-all duration-300 cursor-pointer ${
|
||||
isActiveItem
|
||||
? isCollapsed
|
||||
? ' justify-center py-3' // Center for folded
|
||||
: 'bg-gradient-to-r from-indigo-50 to-indigo-100 dark:from-indigo-900/30 dark:to-indigo-800/30 shadow-lg px-3 py-3'
|
||||
? ' justify-center py-3'
|
||||
: isWhatsNew
|
||||
? 'bg-gradient-to-r from-amber-50 to-yellow-50 dark:from-amber-900/30 dark:to-yellow-800/30 shadow-lg px-3 py-3 border-2 border-amber-200 dark:border-amber-700'
|
||||
: 'bg-gradient-to-r from-indigo-50 to-indigo-100 dark:from-indigo-900/30 dark:to-indigo-800/30 shadow-lg px-3 py-3'
|
||||
: isCollapsed
|
||||
? ' justify-center py-3' // Center for folded
|
||||
: 'hover:bg-white/50 dark:hover:bg-slate-700/50 px-3 py-3'
|
||||
? ' justify-center py-3'
|
||||
: isWhatsNew
|
||||
? 'hover:bg-gradient-to-r hover:from-amber-50 hover:to-yellow-50 dark:hover:from-amber-900/20 dark:hover:to-yellow-800/20 px-3 py-3 border border-amber-200/50 dark:border-amber-700/50'
|
||||
: 'hover:bg-white/50 dark:hover:bg-slate-700/50 px-3 py-3'
|
||||
}`}
|
||||
title={isCollapsed ? tool.name : ''}
|
||||
>
|
||||
{isCollapsed ? (
|
||||
// Folded sidebar - clean icon squares only, centered
|
||||
<div className={`rounded-lg shadow-sm group-hover:scale-110 transition-transform duration-300 ${
|
||||
isActiveItem
|
||||
? 'bg-gradient-to-br from-indigo-500 to-purple-500 p-3' // Active: bigger padding (no border)
|
||||
: 'border-2 border-indigo-300 dark:border-indigo-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-indigo-500 group-hover:to-purple-500 p-2' // Inactive: normal padding (has border)
|
||||
? isWhatsNew
|
||||
? 'bg-gradient-to-br from-amber-500 to-yellow-500 p-3'
|
||||
: 'bg-gradient-to-br from-indigo-500 to-purple-500 p-3'
|
||||
: isWhatsNew
|
||||
? 'border-2 border-amber-300 dark:border-amber-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-amber-500 group-hover:to-yellow-500 p-2'
|
||||
: 'border-2 border-indigo-300 dark:border-indigo-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-indigo-500 group-hover:to-purple-500 p-2'
|
||||
}`}>
|
||||
<IconComponent className={`${
|
||||
isActiveItem
|
||||
? 'h-5 w-5 text-white' // Active: bigger icon, white
|
||||
: 'h-4 w-4 text-slate-500 dark:text-slate-400 group-hover:text-white' // Inactive: normal size, grayscale/hover
|
||||
? 'h-5 w-5 text-white'
|
||||
: 'h-4 w-4 text-slate-500 dark:text-slate-400 group-hover:text-white'
|
||||
}`} />
|
||||
</div>
|
||||
) : (
|
||||
// Expanded sidebar
|
||||
<>
|
||||
<div className={`p-2 rounded-lg shadow-sm group-hover:scale-110 transition-transform duration-300 mr-3 flex-shrink-0 ${
|
||||
isActiveItem
|
||||
? 'bg-gradient-to-br from-indigo-500 to-purple-500' // Active: colored background
|
||||
: 'border-2 border-indigo-300 dark:border-indigo-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-indigo-500 group-hover:to-purple-500' // Inactive: transparent with colored border
|
||||
? isWhatsNew
|
||||
? 'bg-gradient-to-br from-amber-500 to-yellow-500'
|
||||
: 'bg-gradient-to-br from-indigo-500 to-purple-500'
|
||||
: isWhatsNew
|
||||
? 'border-2 border-amber-300 dark:border-amber-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-amber-500 group-hover:to-yellow-500'
|
||||
: 'border-2 border-indigo-300 dark:border-indigo-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-indigo-500 group-hover:to-purple-500'
|
||||
}`}>
|
||||
<IconComponent className={`h-4 w-4 ${
|
||||
isActiveItem
|
||||
? 'text-white' // Active: white icon
|
||||
: 'text-slate-500 dark:text-slate-400 group-hover:text-white' // Inactive: grayscale icon
|
||||
? 'text-white'
|
||||
: 'text-slate-500 dark:text-slate-400 group-hover:text-white'
|
||||
}`} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className={`font-medium truncate ${
|
||||
isActiveItem ? 'text-indigo-700 dark:text-indigo-300' : 'text-slate-500 dark:text-slate-400 group-hover:text-indigo-600 dark:group-hover:text-indigo-400'
|
||||
isActiveItem
|
||||
? isWhatsNew
|
||||
? 'text-amber-700 dark:text-amber-300'
|
||||
: 'text-indigo-700 dark:text-indigo-300'
|
||||
: isWhatsNew
|
||||
? 'text-slate-500 dark:text-slate-400 group-hover:text-amber-600 dark:group-hover:text-amber-400'
|
||||
: 'text-slate-500 dark:text-slate-400 group-hover:text-indigo-600 dark:group-hover:text-indigo-400'
|
||||
}`}>
|
||||
{tool.name}
|
||||
{isWhatsNew && !isCollapsed && (
|
||||
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300">
|
||||
New
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400 truncate">
|
||||
{tool.description}
|
||||
@@ -129,199 +219,191 @@ const ToolSidebar = () => {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Link>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Separator between non-tools and tools */}
|
||||
{!isCollapsed && filteredNonTools.length > 0 && filteredTools.length > 0 && (
|
||||
<div className="border-t border-slate-200/50 dark:border-slate-700/50 my-3"></div>
|
||||
{!isCollapsed && filteredNonTools.length > 0 && Object.keys(toolsByCategory).length > 0 && (
|
||||
<div className="border-t border-slate-200/50 dark:border-slate-700/50 my-4"></div>
|
||||
)}
|
||||
|
||||
{/* Render Tools */}
|
||||
{filteredTools.map((tool) => {
|
||||
const IconComponent = tool.icon;
|
||||
const isActiveItem = isActive(tool.path);
|
||||
const isHome = tool.path === '/';
|
||||
|
||||
// Get category-specific colors for active states
|
||||
const getActiveClasses = (category, isHome) => {
|
||||
if (isHome) {
|
||||
return {
|
||||
collapsed: '', // No background for folded active items
|
||||
expanded: 'bg-gradient-to-r from-slate-50 to-slate-100 dark:from-slate-700 dark:to-slate-600',
|
||||
titleColor: 'text-slate-700 dark:text-slate-300',
|
||||
iconBg: 'bg-gradient-to-br from-slate-500 to-slate-600' // Active icon has colored background
|
||||
};
|
||||
}
|
||||
|
||||
switch (category) {
|
||||
case 'editor':
|
||||
return {
|
||||
collapsed: '', // No background for folded active items
|
||||
expanded: 'bg-gradient-to-r from-blue-50 to-blue-100 dark:from-blue-900/30 dark:to-blue-800/30',
|
||||
titleColor: 'text-blue-700 dark:text-blue-300',
|
||||
iconBg: 'bg-gradient-to-br from-blue-500 to-cyan-500'
|
||||
};
|
||||
case 'encoder':
|
||||
return {
|
||||
collapsed: '', // No background for folded active items
|
||||
expanded: 'bg-gradient-to-r from-purple-50 to-purple-100 dark:from-purple-900/30 dark:to-purple-800/30',
|
||||
titleColor: 'text-purple-700 dark:text-purple-300',
|
||||
iconBg: 'bg-gradient-to-br from-purple-500 to-pink-500'
|
||||
};
|
||||
case 'formatter':
|
||||
return {
|
||||
collapsed: '', // No background for folded active items
|
||||
expanded: 'bg-gradient-to-r from-green-50 to-green-100 dark:from-green-900/30 dark:to-green-800/30',
|
||||
titleColor: 'text-green-700 dark:text-green-300',
|
||||
iconBg: 'bg-gradient-to-br from-green-500 to-emerald-500'
|
||||
};
|
||||
case 'analyzer':
|
||||
return {
|
||||
collapsed: '', // No background for folded active items
|
||||
expanded: 'bg-gradient-to-r from-orange-50 to-orange-100 dark:from-orange-900/30 dark:to-orange-800/30',
|
||||
titleColor: 'text-orange-700 dark:text-orange-300',
|
||||
iconBg: 'bg-gradient-to-br from-orange-500 to-red-500'
|
||||
};
|
||||
case 'non_tools':
|
||||
return {
|
||||
collapsed: '', // No background for folded active items
|
||||
expanded: 'bg-gradient-to-r from-indigo-50 to-indigo-100 dark:from-indigo-900/30 dark:to-indigo-800/30',
|
||||
titleColor: 'text-indigo-700 dark:text-indigo-300',
|
||||
iconBg: 'bg-gradient-to-br from-indigo-500 to-purple-500'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
collapsed: '', // No background for folded active items
|
||||
expanded: 'bg-gradient-to-r from-slate-50 to-slate-100 dark:from-slate-700 dark:to-slate-600',
|
||||
titleColor: 'text-slate-700 dark:text-slate-300',
|
||||
iconBg: 'bg-gradient-to-br from-slate-500 to-slate-600'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getInactiveClasses = (category, isHome) => {
|
||||
if (isHome) {
|
||||
return {
|
||||
collapsed: '', // No background for folded inactive items
|
||||
expanded: 'hover:bg-white/50 dark:hover:bg-slate-700/50',
|
||||
titleColor: 'text-slate-500 dark:text-slate-400 group-hover:text-slate-700 dark:group-hover:text-slate-300',
|
||||
iconBorder: 'border-2 border-slate-300 dark:border-slate-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-slate-500 group-hover:to-slate-600', // Hover: colored background
|
||||
iconColor: 'text-slate-500 dark:text-slate-400 group-hover:text-white' // Hover: white icon
|
||||
};
|
||||
}
|
||||
|
||||
switch (category) {
|
||||
case 'editor':
|
||||
return {
|
||||
collapsed: '', // No background for folded inactive items
|
||||
expanded: 'hover:bg-white/50 dark:hover:bg-slate-700/50',
|
||||
titleColor: 'text-slate-500 dark:text-slate-400 group-hover:text-blue-600 dark:group-hover:text-blue-400',
|
||||
iconBorder: 'border-2 border-blue-300 dark:border-blue-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-blue-500 group-hover:to-cyan-500',
|
||||
iconColor: 'text-slate-500 dark:text-slate-400 group-hover:text-white'
|
||||
};
|
||||
case 'encoder':
|
||||
return {
|
||||
collapsed: '', // No background for folded inactive items
|
||||
expanded: 'hover:bg-white/50 dark:hover:bg-slate-700/50',
|
||||
titleColor: 'text-slate-500 dark:text-slate-400 group-hover:text-purple-600 dark:group-hover:text-purple-400',
|
||||
iconBorder: 'border-2 border-purple-300 dark:border-purple-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-purple-500 group-hover:to-pink-500',
|
||||
iconColor: 'text-slate-500 dark:text-slate-400 group-hover:text-white'
|
||||
};
|
||||
case 'formatter':
|
||||
return {
|
||||
collapsed: '', // No background for folded inactive items
|
||||
expanded: 'hover:bg-white/50 dark:hover:bg-slate-700/50',
|
||||
titleColor: 'text-slate-500 dark:text-slate-400 group-hover:text-green-600 dark:group-hover:text-green-400',
|
||||
iconBorder: 'border-2 border-green-300 dark:border-green-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-green-500 group-hover:to-emerald-500',
|
||||
iconColor: 'text-slate-500 dark:text-slate-400 group-hover:text-white'
|
||||
};
|
||||
case 'analyzer':
|
||||
return {
|
||||
collapsed: '', // No background for folded inactive items
|
||||
expanded: 'hover:bg-white/50 dark:hover:bg-slate-700/50',
|
||||
titleColor: 'text-slate-500 dark:text-slate-400 group-hover:text-orange-600 dark:group-hover:text-orange-400',
|
||||
iconBorder: 'border-2 border-orange-300 dark:border-orange-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-orange-500 group-hover:to-red-500',
|
||||
iconColor: 'text-slate-500 dark:text-slate-400 group-hover:text-white'
|
||||
};
|
||||
case 'non_tools':
|
||||
return {
|
||||
collapsed: '', // No background for folded inactive items
|
||||
expanded: 'hover:bg-white/50 dark:hover:bg-slate-700/50',
|
||||
titleColor: 'text-slate-500 dark:text-slate-400 group-hover:text-indigo-600 dark:group-hover:text-indigo-400',
|
||||
iconBorder: 'border-2 border-indigo-300 dark:border-indigo-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-indigo-500 group-hover:to-purple-500',
|
||||
iconColor: 'text-slate-500 dark:text-slate-400 group-hover:text-white'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
collapsed: '', // No background for folded inactive items
|
||||
expanded: 'hover:bg-white/50 dark:hover:bg-slate-700/50',
|
||||
titleColor: 'text-slate-500 dark:text-slate-400 group-hover:text-slate-700 dark:group-hover:text-slate-300',
|
||||
iconBorder: 'border-2 border-slate-300 dark:border-slate-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-slate-500 group-hover:to-slate-600',
|
||||
iconColor: 'text-slate-500 dark:text-slate-400 group-hover:text-white'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const activeClasses = getActiveClasses(tool.category, isHome);
|
||||
const inactiveClasses = getInactiveClasses(tool.category, isHome);
|
||||
{/* Render Tools by Category */}
|
||||
{!isCollapsed && Object.entries(toolsByCategory).map(([categoryKey, tools]) => {
|
||||
const categoryConfig = getCategoryConfig(categoryKey);
|
||||
const isExpanded = expandedCategories[categoryKey];
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={tool.path}
|
||||
to={tool.path}
|
||||
className={`group flex items-center text-sm font-medium rounded-xl transition-all duration-300 ${
|
||||
isActiveItem
|
||||
? isCollapsed
|
||||
? activeClasses.collapsed + ' justify-center py-3' // Center for folded
|
||||
: activeClasses.expanded + ' shadow-lg px-3 py-3'
|
||||
: isCollapsed
|
||||
? inactiveClasses.collapsed + ' justify-center py-3' // Center for folded
|
||||
: inactiveClasses.expanded + ' px-3 py-3'
|
||||
}`}
|
||||
title={isCollapsed ? tool.name : ''}
|
||||
>
|
||||
{isCollapsed ? (
|
||||
// Folded sidebar - clean icon squares only, centered
|
||||
<div className={`rounded-lg shadow-sm group-hover:scale-110 transition-transform duration-300 ${
|
||||
isActiveItem
|
||||
? activeClasses.iconBg + ' p-3' // Active: bigger padding (no border)
|
||||
: inactiveClasses.iconBorder + ' p-2' // Inactive: normal padding (has border)
|
||||
}`}>
|
||||
<IconComponent className={`${
|
||||
isActiveItem
|
||||
? 'h-5 w-5 text-white' // Active: bigger icon, white
|
||||
: 'h-4 w-4 ' + inactiveClasses.iconColor // Inactive: normal size, grayscale/hover
|
||||
}`} />
|
||||
<div key={categoryKey} className="mb-2">
|
||||
{/* Category Header */}
|
||||
<button
|
||||
onClick={() => toggleCategory(categoryKey)}
|
||||
className="w-full flex items-center justify-between px-3 py-2 text-xs font-semibold text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 transition-colors rounded-lg hover:bg-white/50 dark:hover:bg-slate-700/50"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-2 h-2 rounded-full bg-gradient-to-r ${categoryConfig.color}`}></div>
|
||||
<span className="uppercase tracking-wider">{categoryConfig.name}</span>
|
||||
<span className="text-slate-400 dark:text-slate-500">({tools.length})</span>
|
||||
</div>
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="h-3 w-3" />
|
||||
) : (
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Category Tools */}
|
||||
{isExpanded && (
|
||||
<div className="ml-4 space-y-1 mt-1">
|
||||
{tools.map((tool) => {
|
||||
const IconComponent = tool.icon;
|
||||
const isActiveItem = isActive(tool.path);
|
||||
|
||||
const getActiveClasses = (category) => {
|
||||
switch (category) {
|
||||
case 'editor':
|
||||
return {
|
||||
expanded: 'bg-gradient-to-r from-blue-50 to-blue-100 dark:from-blue-900/30 dark:to-blue-800/30',
|
||||
titleColor: 'text-blue-700 dark:text-blue-300',
|
||||
iconBg: 'bg-gradient-to-br from-blue-500 to-cyan-500'
|
||||
};
|
||||
case 'encoder':
|
||||
return {
|
||||
expanded: 'bg-gradient-to-r from-purple-50 to-purple-100 dark:from-purple-900/30 dark:to-purple-800/30',
|
||||
titleColor: 'text-purple-700 dark:text-purple-300',
|
||||
iconBg: 'bg-gradient-to-br from-purple-500 to-pink-500'
|
||||
};
|
||||
case 'formatter':
|
||||
return {
|
||||
expanded: 'bg-gradient-to-r from-green-50 to-green-100 dark:from-green-900/30 dark:to-green-800/30',
|
||||
titleColor: 'text-green-700 dark:text-green-300',
|
||||
iconBg: 'bg-gradient-to-br from-green-500 to-emerald-500'
|
||||
};
|
||||
case 'analyzer':
|
||||
return {
|
||||
expanded: 'bg-gradient-to-r from-orange-50 to-orange-100 dark:from-orange-900/30 dark:to-orange-800/30',
|
||||
titleColor: 'text-orange-700 dark:text-orange-300',
|
||||
iconBg: 'bg-gradient-to-br from-orange-500 to-red-500'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
expanded: 'bg-gradient-to-r from-slate-50 to-slate-100 dark:from-slate-700 dark:to-slate-600',
|
||||
titleColor: 'text-slate-700 dark:text-slate-300',
|
||||
iconBg: 'bg-gradient-to-br from-slate-500 to-slate-600'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const activeClasses = getActiveClasses(tool.category);
|
||||
|
||||
return (
|
||||
<a
|
||||
key={tool.path}
|
||||
href={tool.path}
|
||||
onClick={(e) => handleNavigation(tool.path, e)}
|
||||
className={`group flex items-center text-sm font-medium rounded-lg transition-all duration-300 px-3 py-2 cursor-pointer ${
|
||||
isActiveItem
|
||||
? activeClasses.expanded + ' shadow-md'
|
||||
: 'hover:bg-white/50 dark:hover:bg-slate-700/50'
|
||||
}`}
|
||||
>
|
||||
<div className={`p-1.5 rounded-md shadow-sm group-hover:scale-110 transition-transform duration-300 mr-3 flex-shrink-0 ${
|
||||
isActiveItem
|
||||
? activeClasses.iconBg
|
||||
: `border border-${categoryConfig.color.split('-')[1]}-300 dark:border-${categoryConfig.color.split('-')[1]}-600 bg-transparent group-hover:bg-gradient-to-br group-hover:${categoryConfig.color}`
|
||||
}`}>
|
||||
<IconComponent className={`h-3.5 w-3.5 ${
|
||||
isActiveItem
|
||||
? 'text-white'
|
||||
: 'text-slate-500 dark:text-slate-400 group-hover:text-white'
|
||||
}`} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className={`font-medium truncate text-sm ${
|
||||
isActiveItem ? activeClasses.titleColor : 'text-slate-600 dark:text-slate-400 group-hover:text-slate-800 dark:group-hover:text-slate-200'
|
||||
}`}>
|
||||
{tool.name}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 dark:text-slate-500 truncate">
|
||||
{tool.description}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
// Expanded sidebar
|
||||
<>
|
||||
<div className={`p-2 rounded-lg shadow-sm group-hover:scale-110 transition-transform duration-300 mr-3 flex-shrink-0 ${
|
||||
isActiveItem
|
||||
? activeClasses.iconBg // Active: colored background
|
||||
: inactiveClasses.iconBorder // Inactive: transparent with colored border
|
||||
}`}>
|
||||
<IconComponent className={`h-4 w-4 ${
|
||||
isActiveItem
|
||||
? 'text-white' // Active: white icon
|
||||
: inactiveClasses.iconColor // Inactive: grayscale icon
|
||||
}`} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className={`font-medium truncate ${
|
||||
isActiveItem ? activeClasses.titleColor : inactiveClasses.titleColor
|
||||
}`}>
|
||||
{tool.name}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400 truncate">
|
||||
{tool.description}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Collapsed view - show categories with tooltip submenus */}
|
||||
{isCollapsed && Object.entries(toolsByCategory).map(([categoryKey, tools]) => {
|
||||
const categoryConfig = getCategoryConfig(categoryKey);
|
||||
const isTooltipVisible = hoveredTooltip === categoryKey;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={categoryKey}
|
||||
className="relative"
|
||||
onMouseEnter={() => handleTooltipMouseEnter(categoryKey)}
|
||||
onMouseLeave={handleTooltipMouseLeave}
|
||||
>
|
||||
<div className="flex items-center justify-center py-3 rounded-xl transition-all duration-300 cursor-pointer">
|
||||
<div className={`rounded-lg shadow-sm hover:scale-110 transition-transform duration-300 p-2 bg-gradient-to-br ${categoryConfig.color} ${isTooltipVisible ? 'opacity-100 scale-110' : 'opacity-80 hover:opacity-100'}`}>
|
||||
<div className="h-4 w-4 bg-white rounded-sm flex items-center justify-center">
|
||||
<span className="text-xs font-bold text-gray-700">{tools.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tooltip Submenu */}
|
||||
<div className={`absolute left-full ml-2 top-0 w-64 bg-white dark:bg-slate-800 rounded-lg shadow-xl border border-gray-200 dark:border-slate-700 z-[9999] transition-all duration-200 transform ${
|
||||
isTooltipVisible
|
||||
? 'opacity-100 visible translate-x-0 pointer-events-auto'
|
||||
: 'opacity-0 invisible translate-x-2 pointer-events-none'
|
||||
}`}>
|
||||
<div className="p-3">
|
||||
<div className="flex items-center gap-2 mb-3 pb-2 border-b border-gray-200 dark:border-slate-700">
|
||||
<div className={`w-3 h-3 rounded-full bg-gradient-to-r ${categoryConfig.color}`}></div>
|
||||
<span className="text-sm font-semibold text-gray-900 dark:text-gray-100 uppercase tracking-wider">{categoryConfig.name}</span>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{tools.map((tool) => {
|
||||
const IconComponent = tool.icon;
|
||||
const isActiveItem = isActive(tool.path);
|
||||
|
||||
return (
|
||||
<a
|
||||
key={tool.path}
|
||||
href={tool.path}
|
||||
onClick={(e) => handleNavigation(tool.path, e)}
|
||||
className={`flex items-center gap-3 p-2 rounded-lg transition-colors cursor-pointer ${
|
||||
isActiveItem
|
||||
? `bg-gradient-to-r ${categoryConfig.color.replace('from-', 'from-').replace('to-', 'to-')}20 text-gray-900 dark:text-gray-100`
|
||||
: 'hover:bg-gray-50 dark:hover:bg-slate-700 text-gray-700 dark:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
<div className={`p-1.5 rounded-md ${
|
||||
isActiveItem
|
||||
? `bg-gradient-to-br ${categoryConfig.color}`
|
||||
: `border border-gray-300 dark:border-slate-600 bg-transparent`
|
||||
}`}>
|
||||
<IconComponent className={`h-3.5 w-3.5 ${
|
||||
isActiveItem ? 'text-white' : 'text-gray-500 dark:text-gray-400'
|
||||
}`} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className={`font-medium text-sm truncate ${isActiveItem ? `text-white` : `text-gray-500 dark:text-gray-400`}`}>{tool.name}</div>
|
||||
<div className={`text-xs ${isActiveItem ? `text-white` : `text-gray-500 dark:text-gray-400`} truncate`}>{tool.description}</div>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
628
src/components/invoice-templates/MinimalTemplate.js
Normal file
628
src/components/invoice-templates/MinimalTemplate.js
Normal file
@@ -0,0 +1,628 @@
|
||||
import React from 'react';
|
||||
|
||||
const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
|
||||
// Get the chosen color scheme or default to golden
|
||||
const accentColor = invoiceData.settings?.colorScheme || '#D4AF37';
|
||||
|
||||
// Get layout settings
|
||||
const sectionSpacing = invoiceData.settings?.sectionSpacing || 'normal';
|
||||
const pageBreaks = invoiceData.settings?.pageBreaks || {};
|
||||
|
||||
// Section spacing values
|
||||
const spacingMap = {
|
||||
compact: '15px',
|
||||
normal: '25px',
|
||||
spacious: '40px'
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: '10px', // Further reduced for better print layout
|
||||
fontSize: '13px',
|
||||
lineHeight: '1.4',
|
||||
fontFamily: 'system-ui, -apple-system, sans-serif',
|
||||
color: '#000000'
|
||||
}}>
|
||||
{/* Header Section - 2 Column Layout */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '30px',
|
||||
padding: '20px',
|
||||
background: `linear-gradient(135deg, ${accentColor}, ${accentColor}dd)`,
|
||||
borderRadius: '4px',
|
||||
color: 'white'
|
||||
}}>
|
||||
{/* Left: Logo + INVOICE + Company Name */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
||||
{/* Company Logo */}
|
||||
{invoiceData.company.logo ? (
|
||||
<img
|
||||
src={invoiceData.company.logo}
|
||||
alt="Company Logo"
|
||||
style={{
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
objectFit: 'contain',
|
||||
borderRadius: '8px',
|
||||
background: 'white',
|
||||
padding: '4px'
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div style={{
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
background: 'rgba(255,255,255,0.2)',
|
||||
borderRadius: '12px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '24px',
|
||||
color: 'white',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
✦
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
|
||||
<h2 style={{
|
||||
fontSize: '32px',
|
||||
fontWeight: 'bold',
|
||||
color: 'white',
|
||||
margin: '0',
|
||||
lineHeight: '1'
|
||||
}}>
|
||||
INVOICE
|
||||
</h2>
|
||||
<div style={{
|
||||
fontSize: '16px',
|
||||
fontWeight: '600',
|
||||
color: 'rgba(255,255,255,0.9)',
|
||||
margin: '4px 0 0 0'
|
||||
}}>
|
||||
{invoiceData.company.name || 'DevTools Inc.'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Invoice Details */}
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div style={{ marginBottom: '8px', display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: '12px' }}>
|
||||
<span style={{ fontSize: '16px', fontWeight: '600', color: 'rgba(255,255,255,0.8)' }}>#</span>
|
||||
<span style={{ fontSize: '18px', fontWeight: 'bold', color: 'white' }}>
|
||||
{invoiceData.invoiceNumber || 'INV-2024-001'}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: '12px' }}>
|
||||
<span style={{ fontSize: '16px', fontWeight: '600', color: 'rgba(255,255,255,0.8)' }}>Date:</span>
|
||||
<span style={{ fontSize: '16px', fontWeight: '600', color: 'white' }}>
|
||||
{invoiceData.date || '15/01/2024'}
|
||||
</span>
|
||||
</div>
|
||||
{invoiceData.dueDate && (
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: '12px', marginTop: '4px' }}>
|
||||
<span style={{ fontSize: '16px', fontWeight: '600', color: 'rgba(255,255,255,0.8)' }}>Due:</span>
|
||||
<span style={{ fontSize: '16px', fontWeight: '600', color: 'white' }}>
|
||||
{invoiceData.dueDate}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subheader Section - 2 Column Layout: FROM and TO */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '30px', marginBottom: '25px' }}>
|
||||
|
||||
{/* FROM Section */}
|
||||
{(invoiceData.settings?.showFromSection ?? true) && (
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
background: `${accentColor}08`,
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #e9ecef'
|
||||
}}>
|
||||
<h3 style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
color: accentColor,
|
||||
marginBottom: '12px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.5px'
|
||||
}}>
|
||||
FROM
|
||||
</h3>
|
||||
<div style={{ fontSize: '14px', color: '#000000', lineHeight: '1.6' }}>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>
|
||||
{invoiceData.company.name || 'DevTools Inc.'}
|
||||
</div>
|
||||
{invoiceData.company.address && <div>{invoiceData.company.address}</div>}
|
||||
{invoiceData.company.city && <div>{invoiceData.company.city}</div>}
|
||||
{invoiceData.company.phone && <div>{invoiceData.company.phone}</div>}
|
||||
{invoiceData.company.email && <div>{invoiceData.company.email}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TO Section */}
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
background: `${accentColor}08`,
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #e9ecef'
|
||||
}}>
|
||||
<h3 style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
color: accentColor,
|
||||
marginBottom: '12px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.5px'
|
||||
}}>
|
||||
TO
|
||||
</h3>
|
||||
<div style={{ fontSize: '14px', color: '#000000', lineHeight: '1.6' }}>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>
|
||||
{invoiceData.client.name || 'Acme Corporation'}
|
||||
</div>
|
||||
<div>{invoiceData.client.address || '456 Business Ave'}</div>
|
||||
<div>{invoiceData.client.city || 'New York, NY 10001'}</div>
|
||||
{invoiceData.client.phone && <div>{invoiceData.client.phone}</div>}
|
||||
{invoiceData.client.email && <div>{invoiceData.client.email}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Payment Terms Section */}
|
||||
{invoiceData.paymentTerms?.type !== 'full' && (
|
||||
<div
|
||||
className={pageBreaks.beforePaymentSchedule ? 'page-break-before' : ''}
|
||||
style={{ marginBottom: '20px', marginTop: spacingMap[sectionSpacing] }}
|
||||
>
|
||||
<h3 style={{
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
color: accentColor,
|
||||
marginBottom: '16px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.5px'
|
||||
}}>
|
||||
Payment Schedule
|
||||
</h3>
|
||||
|
||||
<div style={{
|
||||
background: `${accentColor}08`,
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #e9ecef',
|
||||
padding: '16px'
|
||||
}}>
|
||||
{/* Down Payment */}
|
||||
{invoiceData.paymentTerms?.type === 'downpayment' && invoiceData.paymentTerms?.downPayment?.amount > 0 && (
|
||||
<div style={{ marginBottom: '12px', paddingBottom: '12px', borderBottom: '1px solid #e9ecef' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<div>
|
||||
<div style={{ fontSize: '14px', fontWeight: '600', color: '#000000', marginBottom: '4px' }}>
|
||||
Down Payment ({invoiceData.paymentTerms.downPayment.percentage?.toFixed(1)}%)
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', marginTop: '2px' }}>
|
||||
{invoiceData.paymentTerms.downPayment.status === 'overdue' && invoiceData.paymentTerms.downPayment.dueDate && (
|
||||
<span style={{ color: '#EF4444' }}>
|
||||
Overdue - Due: {new Date(invoiceData.paymentTerms.downPayment.dueDate).toLocaleDateString()}
|
||||
</span>
|
||||
)}
|
||||
{invoiceData.paymentTerms.downPayment.status === 'current' && invoiceData.paymentTerms.downPayment.dueDate && (
|
||||
<span style={{ color: accentColor }}>
|
||||
Current - <span style={{ color: '#666666' }}>Due: {new Date(invoiceData.paymentTerms.downPayment.dueDate).toLocaleDateString()}</span>
|
||||
</span>
|
||||
)}
|
||||
{invoiceData.paymentTerms.downPayment.status === 'paid' && (
|
||||
<span style={{ color: '#666666' }}>Paid</span>
|
||||
)}
|
||||
{(!invoiceData.paymentTerms.downPayment.status || invoiceData.paymentTerms.downPayment.status === 'pending') && (
|
||||
<span style={{ color: '#666666' }}>Pending</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
color: invoiceData.paymentTerms.downPayment.status === 'current' ? accentColor : '#666666'
|
||||
}}>
|
||||
{new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: invoiceData.settings?.currency?.code || 'USD'
|
||||
}).format(invoiceData.paymentTerms.downPayment.amount)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Installments */}
|
||||
{invoiceData.paymentTerms?.installments && invoiceData.paymentTerms.installments.length > 0 && (
|
||||
<div>
|
||||
{invoiceData.paymentTerms?.type === 'downpayment' && (
|
||||
<div style={{ fontSize: '12px', color: '#666666', marginBottom: '8px' }}>
|
||||
Remaining Balance: {new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: invoiceData.settings?.currency?.code || 'USD'
|
||||
}).format(invoiceData.total - (invoiceData.paymentTerms?.downPayment?.amount || 0))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{invoiceData.paymentTerms.installments.map((installment, index) => (
|
||||
<div key={installment.id} style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '8px',
|
||||
padding: '8px',
|
||||
background: 'rgba(255,255,255,0.5)',
|
||||
borderRadius: '4px'
|
||||
}}>
|
||||
<div>
|
||||
<div>
|
||||
<div style={{ fontSize: '14px', fontWeight: '500', color: '#000000', marginBottom: '4px' }}>
|
||||
{installment.description || `Installment ${index + 1}`}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', marginTop: '2px' }}>
|
||||
{installment.status === 'overdue' && installment.dueDate && (
|
||||
<span style={{ color: '#EF4444' }}>
|
||||
Overdue - Due: {new Date(installment.dueDate).toLocaleDateString()}
|
||||
</span>
|
||||
)}
|
||||
{installment.status === 'current' && installment.dueDate && (
|
||||
<span style={{ color: accentColor }}>
|
||||
Current - <span style={{ color: '#666666' }}>Due: {new Date(installment.dueDate).toLocaleDateString()}</span>
|
||||
</span>
|
||||
)}
|
||||
{installment.status === 'paid' && (
|
||||
<span style={{ color: '#666666' }}>Paid</span>
|
||||
)}
|
||||
{(!installment.status || installment.status === 'pending') && (
|
||||
<span style={{ color: '#666666' }}>Pending</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
color: installment.status === 'current' ? accentColor : '#666666'
|
||||
}}>
|
||||
{new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: invoiceData.settings?.currency?.code || 'USD'
|
||||
}).format(installment.amount || 0)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Items Table */}
|
||||
<div
|
||||
className={pageBreaks.beforeItemsTable ? 'page-break-before' : ''}
|
||||
style={{ marginBottom: '20px', marginTop: spacingMap[sectionSpacing] }}
|
||||
>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
{/* Table Header */}
|
||||
<thead>
|
||||
<tr style={{
|
||||
borderBottom: `2px solid ${accentColor}`,
|
||||
backgroundColor: `${accentColor}15` // 15 = ~8% opacity
|
||||
}}>
|
||||
<th style={{
|
||||
padding: '12px 0 12px 16px',
|
||||
textAlign: 'left',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
color: accentColor,
|
||||
verticalAlign: 'middle'
|
||||
}}>Item</th>
|
||||
<th style={{
|
||||
padding: '12px 0',
|
||||
textAlign: 'center',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
color: accentColor,
|
||||
verticalAlign: 'middle'
|
||||
}}>Quantity</th>
|
||||
<th style={{
|
||||
padding: '12px 0',
|
||||
textAlign: 'center',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
color: accentColor,
|
||||
verticalAlign: 'middle'
|
||||
}}>Unit Price</th>
|
||||
<th style={{
|
||||
padding: '12px 16px 12px 0',
|
||||
textAlign: 'right',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
color: accentColor,
|
||||
verticalAlign: 'middle'
|
||||
}}>Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{/* Table Body */}
|
||||
<tbody>
|
||||
{invoiceData.items.map((item, index) => (
|
||||
<tr key={item.id} style={{
|
||||
borderBottom: '1px solid #E5E5E5'
|
||||
}}>
|
||||
<td style={{
|
||||
padding: '16px 0 16px 16px',
|
||||
fontSize: '14px',
|
||||
color: '#000000',
|
||||
verticalAlign: 'middle'
|
||||
}}>
|
||||
{item.description}
|
||||
</td>
|
||||
<td style={{
|
||||
padding: '16px 0',
|
||||
textAlign: 'center',
|
||||
fontSize: '14px',
|
||||
color: '#000000',
|
||||
verticalAlign: 'middle'
|
||||
}}>
|
||||
{item.quantity}
|
||||
</td>
|
||||
<td style={{
|
||||
padding: '16px 0',
|
||||
textAlign: 'center',
|
||||
fontSize: '14px',
|
||||
color: '#000000',
|
||||
verticalAlign: 'middle'
|
||||
}}>
|
||||
{formatCurrency(item.rate, true)}
|
||||
</td>
|
||||
<td style={{
|
||||
padding: '16px 16px 16px 0',
|
||||
textAlign: 'right',
|
||||
fontSize: '14px',
|
||||
color: '#000000',
|
||||
fontWeight: 'bold',
|
||||
verticalAlign: 'middle'
|
||||
}}>
|
||||
{formatCurrency(item.amount, true)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Payment Method and Totals */}
|
||||
<div
|
||||
className={pageBreaks.beforePaymentMethod ? 'page-break-before' : ''}
|
||||
style={{ display: 'flex', justifyContent: 'space-between', gap: '40px', marginBottom: '40px', marginTop: spacingMap[sectionSpacing] }}
|
||||
>
|
||||
{/* Payment Method */}
|
||||
{invoiceData.paymentMethod?.type !== 'none' && (
|
||||
<div style={{ flex: 1 }}>
|
||||
<h3 style={{
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
color: '#000000',
|
||||
marginBottom: '12px'
|
||||
}}>
|
||||
Payment Method
|
||||
</h3>
|
||||
|
||||
{/* Bank Details */}
|
||||
{invoiceData.paymentMethod?.type === 'bank' && (
|
||||
<div style={{ fontSize: '14px', color: '#000000', lineHeight: '1.6' }}>
|
||||
{invoiceData.paymentMethod.bankDetails?.bankName && (
|
||||
<div style={{ marginBottom: '4px' }}>{invoiceData.paymentMethod.bankDetails.bankName}</div>
|
||||
)}
|
||||
{invoiceData.paymentMethod.bankDetails?.accountName && (
|
||||
<div style={{ marginBottom: '4px' }}>Account Name: {invoiceData.paymentMethod.bankDetails.accountName}</div>
|
||||
)}
|
||||
{invoiceData.paymentMethod.bankDetails?.accountNumber && (
|
||||
<div style={{ marginBottom: '4px' }}>Account No.: {invoiceData.paymentMethod.bankDetails.accountNumber}</div>
|
||||
)}
|
||||
{invoiceData.paymentMethod.bankDetails?.routingNumber && (
|
||||
<div style={{ marginBottom: '4px' }}>Routing: {invoiceData.paymentMethod.bankDetails.routingNumber}</div>
|
||||
)}
|
||||
{invoiceData.paymentMethod.bankDetails?.swiftCode && (
|
||||
<div style={{ marginBottom: '4px' }}>SWIFT: {invoiceData.paymentMethod.bankDetails.swiftCode}</div>
|
||||
)}
|
||||
{invoiceData.paymentMethod.bankDetails?.iban && (
|
||||
<div style={{ marginBottom: '4px' }}>IBAN: {invoiceData.paymentMethod.bankDetails.iban}</div>
|
||||
)}
|
||||
{invoiceData.dueDate && <div>Pay by: {invoiceData.dueDate}</div>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Payment Link */}
|
||||
{invoiceData.paymentMethod?.type === 'link' && invoiceData.paymentMethod.paymentLink?.url && (
|
||||
<div style={{ fontSize: '14px', color: '#000000', lineHeight: '1.6' }}>
|
||||
<a
|
||||
href={invoiceData.paymentMethod.paymentLink.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
padding: '8px 16px',
|
||||
background: accentColor,
|
||||
color: 'white',
|
||||
textDecoration: 'none',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '600'
|
||||
}}
|
||||
>
|
||||
{invoiceData.paymentMethod.paymentLink.label || 'Pay Online'}
|
||||
</a>
|
||||
{invoiceData.dueDate && <div style={{ marginTop: '8px' }}>Pay by: {invoiceData.dueDate}</div>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* QR Code */}
|
||||
{invoiceData.paymentMethod?.type === 'qr' && (invoiceData.paymentMethod.qrCode?.url || invoiceData.paymentMethod.qrCode?.customImage) && (
|
||||
<div style={{ fontSize: '14px', color: '#000000', lineHeight: '1.6' }}>
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<img
|
||||
src={
|
||||
invoiceData.paymentMethod.qrCode.customImage
|
||||
? invoiceData.paymentMethod.qrCode.customImage
|
||||
: `https://api.qrserver.com/v1/create-qr-code/?size=80x80&data=${encodeURIComponent(invoiceData.paymentMethod.qrCode.url)}`
|
||||
}
|
||||
alt="Payment QR Code"
|
||||
style={{ width: '80px', height: '80px', border: '1px solid #e9ecef' }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#666666', marginBottom: '4px' }}>
|
||||
{invoiceData.paymentMethod.qrCode.label || 'Scan to Pay'}
|
||||
</div>
|
||||
{invoiceData.dueDate && <div>Pay by: {invoiceData.dueDate}</div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Totals */}
|
||||
<div style={{ textAlign: 'right', minWidth: '200px' }}>
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<span style={{ fontSize: '14px', color: '#000000' }}>Subtotal</span>
|
||||
<span style={{ fontSize: '14px', color: '#000000', marginLeft: '40px' }}>
|
||||
{formatCurrency(invoiceData.subtotal, true)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Dynamic Fees */}
|
||||
{invoiceData.fees && invoiceData.fees.map((fee) => (
|
||||
<div key={fee.id} style={{ marginBottom: '12px' }}>
|
||||
<span style={{ fontSize: '14px', color: '#000000' }}>
|
||||
{fee.label || 'Fee'} {fee.type === 'percentage' ? `(${fee.value}%)` : ''}
|
||||
</span>
|
||||
<span style={{ fontSize: '14px', color: '#000000', marginLeft: '40px' }}>
|
||||
+{formatCurrency(fee.type === 'percentage' ? (invoiceData.subtotal * fee.value) / 100 : fee.value, true)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Dynamic Discounts */}
|
||||
{invoiceData.discounts && invoiceData.discounts.map((discount) => (
|
||||
<div key={discount.id} style={{ marginBottom: '12px' }}>
|
||||
<span style={{ fontSize: '14px', color: '#000000' }}>
|
||||
{discount.label || 'Discount'} {discount.type === 'percentage' ? `(${discount.value}%)` : ''}
|
||||
</span>
|
||||
<span style={{ fontSize: '14px', color: '#000000', marginLeft: '40px' }}>
|
||||
-{formatCurrency(discount.type === 'percentage' ? (invoiceData.subtotal * discount.value) / 100 : discount.value, true)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Legacy Discount */}
|
||||
{invoiceData.discount > 0 && (
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<span style={{ fontSize: '14px', color: '#000000' }}>Discount</span>
|
||||
<span style={{ fontSize: '14px', color: '#000000', marginLeft: '40px' }}>
|
||||
-{formatCurrency(invoiceData.discount, true)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{
|
||||
padding: '12px 16px 12px 16px', // Match table head padding
|
||||
marginTop: '16px',
|
||||
backgroundColor: `${accentColor}10`, // 10 = ~6% opacity
|
||||
borderTop: `2px solid ${accentColor}`,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<span style={{ fontSize: '18px', fontWeight: 'bold', color: accentColor }}>Total</span>
|
||||
<span style={{
|
||||
fontSize: '18px',
|
||||
fontWeight: 'bold',
|
||||
color: accentColor
|
||||
}}>
|
||||
{formatCurrency(invoiceData.total, true)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notes Section */}
|
||||
{invoiceData.notes && (
|
||||
<div style={{ marginBottom: '30px' }}>
|
||||
<h3 style={{
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
color: '#000000',
|
||||
marginBottom: '8px'
|
||||
}}>
|
||||
Notes
|
||||
</h3>
|
||||
<p style={{
|
||||
fontSize: '14px',
|
||||
color: '#000000',
|
||||
margin: '0',
|
||||
lineHeight: '1.5'
|
||||
}}>
|
||||
{invoiceData.notes}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', marginTop: '40px' }}>
|
||||
{/* Thank you message */}
|
||||
<div>
|
||||
<p style={{ fontSize: '16px', color: '#000000', margin: '0' }}>
|
||||
{invoiceData.thankYouMessage || 'Thank you for your business!'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Signature Line */}
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
{/* Digital Signature */}
|
||||
{invoiceData.digitalSignature ? (
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<img
|
||||
src={invoiceData.digitalSignature}
|
||||
alt="Digital Signature"
|
||||
style={{
|
||||
maxWidth: '200px',
|
||||
maxHeight: '200px',
|
||||
objectFit: 'contain'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
// More space for physical signature when no digital signature
|
||||
<div style={{
|
||||
height: '60px',
|
||||
marginBottom: '8px',
|
||||
display: 'flex',
|
||||
alignItems: 'flex-end'
|
||||
}}></div>
|
||||
)}
|
||||
<div style={{
|
||||
width: '200px',
|
||||
borderBottom: `2px solid ${accentColor}`,
|
||||
marginBottom: '8px'
|
||||
}}></div>
|
||||
<p style={{ fontSize: '12px', color: '#666666', margin: '0' }}>
|
||||
{invoiceData.authorizedSignedText || 'Authorized Signed'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MinimalTemplate;
|
||||
@@ -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',
|
||||
|
||||
181
src/hooks/useNavigationGuard.js
Normal file
181
src/hooks/useNavigationGuard.js
Normal 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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
2733
src/pages/InvoiceEditor.js
Normal file
File diff suppressed because it is too large
Load Diff
242
src/pages/InvoicePreview.js
Normal file
242
src/pages/InvoicePreview.js
Normal 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;
|
||||
461
src/pages/InvoicePreviewMinimal.js
Normal file
461
src/pages/InvoicePreviewMinimal.js
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
1
src/utils/currencies.json
Normal file
1
src/utils/currencies.json
Normal file
File diff suppressed because one or more lines are too long
@@ -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();
|
||||
|
||||
@@ -101,10 +101,7 @@ export const buildSitemap = () => {
|
||||
// Generate and write robots.txt
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user