squiggle with signature-pad and smoother lines + blossom #258
33
package-lock.json
generated
33
package-lock.json
generated
@ -47,7 +47,7 @@
|
|||||||
"react-singleton-hook": "^4.0.1",
|
"react-singleton-hook": "^4.0.1",
|
||||||
"react-toastify": "10.0.4",
|
"react-toastify": "10.0.4",
|
||||||
"redux": "5.0.1",
|
"redux": "5.0.1",
|
||||||
"svgo": "^3.3.2",
|
"signature_pad": "^5.0.4",
|
||||||
"tseep": "1.2.1"
|
"tseep": "1.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -2214,6 +2214,7 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
|
||||||
"integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==",
|
"integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
@ -2935,6 +2936,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
@ -3634,6 +3636,7 @@
|
|||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
|
||||||
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
|
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
|
||||||
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"boolbase": "^1.0.0",
|
"boolbase": "^1.0.0",
|
||||||
@ -3650,6 +3653,7 @@
|
|||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
|
||||||
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
|
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mdn-data": "2.0.30",
|
"mdn-data": "2.0.30",
|
||||||
@ -3663,6 +3667,7 @@
|
|||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
|
||||||
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
|
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
|
||||||
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
@ -3675,6 +3680,7 @@
|
|||||||
"version": "5.0.5",
|
"version": "5.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz",
|
||||||
"integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==",
|
"integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"css-tree": "~2.2.0"
|
"css-tree": "~2.2.0"
|
||||||
@ -3688,6 +3694,7 @@
|
|||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz",
|
||||||
"integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==",
|
"integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mdn-data": "2.0.28",
|
"mdn-data": "2.0.28",
|
||||||
@ -3702,6 +3709,7 @@
|
|||||||
"version": "2.0.28",
|
"version": "2.0.28",
|
||||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz",
|
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz",
|
||||||
"integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==",
|
"integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==",
|
||||||
|
"dev": true,
|
||||||
"license": "CC0-1.0"
|
"license": "CC0-1.0"
|
||||||
},
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
@ -3949,6 +3957,7 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"domelementtype": "^2.3.0",
|
"domelementtype": "^2.3.0",
|
||||||
@ -3976,6 +3985,7 @@
|
|||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||||
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@ -3988,6 +3998,7 @@
|
|||||||
"version": "5.0.3",
|
"version": "5.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||||
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"domelementtype": "^2.3.0"
|
"domelementtype": "^2.3.0"
|
||||||
@ -4003,6 +4014,7 @@
|
|||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
|
||||||
"integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
|
"integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
|
||||||
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dom-serializer": "^2.0.0",
|
"dom-serializer": "^2.0.0",
|
||||||
@ -4020,9 +4032,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/elliptic": {
|
"node_modules/elliptic": {
|
||||||
"version": "6.5.7",
|
"version": "6.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.0.tgz",
|
||||||
"integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==",
|
"integrity": "sha512-dpwoQcLc/2WLQvJvLRHKZ+f9FgOdjnq11rurqwekGQygGPsYSK29OMMD2WalatiqQ+XGFDglTNixpPfI+lpaAA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -4052,6 +4064,7 @@
|
|||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.12"
|
"node": ">=0.12"
|
||||||
@ -5979,6 +5992,7 @@
|
|||||||
"version": "2.0.30",
|
"version": "2.0.30",
|
||||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
||||||
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
|
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
|
||||||
|
"dev": true,
|
||||||
"license": "CC0-1.0"
|
"license": "CC0-1.0"
|
||||||
},
|
},
|
||||||
"node_modules/merge-stream": {
|
"node_modules/merge-stream": {
|
||||||
@ -6560,6 +6574,7 @@
|
|||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||||
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
||||||
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"boolbase": "^1.0.0"
|
"boolbase": "^1.0.0"
|
||||||
@ -6919,6 +6934,7 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
|
||||||
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
|
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
@ -7866,6 +7882,12 @@
|
|||||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/signature_pad": {
|
||||||
|
"version": "5.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/signature_pad/-/signature_pad-5.0.4.tgz",
|
||||||
|
"integrity": "sha512-nngOixbwLAUOuH3QnZwlgwmynQblxmo4iWacKFwfymJfiY+Qt+9icNtcIe/okqXKun4hJ5QTFmHyC7dmv6lf2w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/simple-concat": {
|
"node_modules/simple-concat": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||||
@ -7971,6 +7993,7 @@
|
|||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||||
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@ -8197,6 +8220,7 @@
|
|||||||
"version": "3.3.2",
|
"version": "3.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz",
|
||||||
"integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==",
|
"integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@trysound/sax": "0.2.0",
|
"@trysound/sax": "0.2.0",
|
||||||
@ -8222,6 +8246,7 @@
|
|||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||||
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
|
@ -57,7 +57,7 @@
|
|||||||
"react-singleton-hook": "^4.0.1",
|
"react-singleton-hook": "^4.0.1",
|
||||||
"react-toastify": "10.0.4",
|
"react-toastify": "10.0.4",
|
||||||
"redux": "5.0.1",
|
"redux": "5.0.1",
|
||||||
"svgo": "^3.3.2",
|
"signature_pad": "^5.0.4",
|
||||||
"tseep": "1.2.1"
|
"tseep": "1.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -3,14 +3,13 @@ import { useAppSelector } from './hooks/store'
|
|||||||
import { Navigate, Route, Routes } from 'react-router-dom'
|
import { Navigate, Route, Routes } from 'react-router-dom'
|
||||||
import { AuthController } from './controllers'
|
import { AuthController } from './controllers'
|
||||||
import { MainLayout } from './layouts/Main'
|
import { MainLayout } from './layouts/Main'
|
||||||
|
import { appPrivateRoutes, appPublicRoutes } from './routes'
|
||||||
|
import './App.scss'
|
||||||
import {
|
import {
|
||||||
appPrivateRoutes,
|
|
||||||
appPublicRoutes,
|
|
||||||
privateRoutes,
|
privateRoutes,
|
||||||
publicRoutes,
|
publicRoutes,
|
||||||
recursiveRouteRenderer
|
recursiveRouteRenderer
|
||||||
} from './routes'
|
} from './routes/util'
|
||||||
import './App.scss'
|
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const authState = useAppSelector((state) => state.auth)
|
const authState = useAppSelector((state) => state.auth)
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
isCurrentValueLast
|
isCurrentValueLast
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { MARK_TYPE_CONFIG } from '../getMarkComponents.tsx'
|
import { MarkInput } from '../MarkTypeStrategy/MarkInput.tsx'
|
||||||
|
|
||||||
interface MarkFormFieldProps {
|
interface MarkFormFieldProps {
|
||||||
currentUserMarks: CurrentUserMark[]
|
currentUserMarks: CurrentUserMark[]
|
||||||
@ -52,8 +52,7 @@ const MarkFormField = ({
|
|||||||
}
|
}
|
||||||
const toggleActions = () => setDisplayActions(!displayActions)
|
const toggleActions = () => setDisplayActions(!displayActions)
|
||||||
const markLabel = getToolboxLabelByMarkType(selectedMark.mark.type)
|
const markLabel = getToolboxLabelByMarkType(selectedMark.mark.type)
|
||||||
const { input: MarkInputComponent } =
|
|
||||||
MARK_TYPE_CONFIG[selectedMark.mark.type] || {}
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.trigger}>
|
<div className={styles.trigger}>
|
||||||
@ -84,14 +83,14 @@ const MarkFormField = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.inputWrapper}>
|
<div className={styles.inputWrapper}>
|
||||||
<form onSubmit={(e) => handleFormSubmit(e)}>
|
<form onSubmit={(e) => handleFormSubmit(e)}>
|
||||||
{typeof MarkInputComponent !== 'undefined' && (
|
<MarkInput
|
||||||
<MarkInputComponent
|
markType={selectedMark.mark.type}
|
||||||
value={selectedMarkValue}
|
key={selectedMark.id}
|
||||||
placeholder={markLabel}
|
value={selectedMarkValue}
|
||||||
handler={handleSelectedMarkValueChange}
|
placeholder={markLabel}
|
||||||
userMark={selectedMark}
|
handler={handleSelectedMarkValueChange}
|
||||||
/>
|
userMark={selectedMark}
|
||||||
)}
|
/>
|
||||||
<div className={styles.actionsBottom}>
|
<div className={styles.actionsBottom}>
|
||||||
<button type="submit" className={styles.submitButton}>
|
<button type="submit" className={styles.submitButton}>
|
||||||
NEXT
|
NEXT
|
||||||
@ -104,7 +103,7 @@ const MarkFormField = ({
|
|||||||
return (
|
return (
|
||||||
<div className={styles.pagination} key={index}>
|
<div className={styles.pagination} key={index}>
|
||||||
<button
|
<button
|
||||||
className={`${styles.paginationButton} ${isDone(mark) && styles.paginationButtonDone}`}
|
className={`${styles.paginationButton} ${isDone(mark) ? styles.paginationButtonDone : ''}`}
|
||||||
onClick={() => handleCurrentUserMarkChange(mark)}
|
onClick={() => handleCurrentUserMarkChange(mark)}
|
||||||
>
|
>
|
||||||
{mark.id}
|
{mark.id}
|
||||||
|
@ -1,101 +0,0 @@
|
|||||||
import { useRef, useState } from 'react'
|
|
||||||
import { MarkInputProps } from '../../types/mark'
|
|
||||||
import { getOptimizedPaths, optimizeSVG } from '../../utils'
|
|
||||||
import { faEraser } from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|
||||||
import styles from './Signature.module.scss'
|
|
||||||
import { MarkRenderSignature } from '../MarkRender/Signature'
|
|
||||||
|
|
||||||
export const MarkInputSignature = ({
|
|
||||||
value,
|
|
||||||
handler,
|
|
||||||
userMark
|
|
||||||
}: MarkInputProps) => {
|
|
||||||
const location = userMark?.mark.location
|
|
||||||
|
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
||||||
const [drawing, setDrawing] = useState(false)
|
|
||||||
const [paths, setPaths] = useState<string[]>(value ? JSON.parse(value) : [])
|
|
||||||
|
|
||||||
function update() {
|
|
||||||
if (location && paths) {
|
|
||||||
if (paths.length) {
|
|
||||||
const optimizedSvg = optimizeSVG(location, paths)
|
|
||||||
const extractedPaths = getOptimizedPaths(optimizedSvg)
|
|
||||||
handler(JSON.stringify(extractedPaths))
|
|
||||||
} else {
|
|
||||||
handler('')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePointerDown = (event: React.PointerEvent) => {
|
|
||||||
const rect = event.currentTarget.getBoundingClientRect()
|
|
||||||
const x = event.clientX - rect.left
|
|
||||||
const y = event.clientY - rect.top
|
|
||||||
|
|
||||||
const ctx = canvasRef.current?.getContext('2d')
|
|
||||||
ctx?.beginPath()
|
|
||||||
ctx?.moveTo(x, y)
|
|
||||||
setPaths([...paths, `M ${x} ${y}`])
|
|
||||||
setDrawing(true)
|
|
||||||
}
|
|
||||||
const handlePointerUp = () => {
|
|
||||||
setDrawing(false)
|
|
||||||
update()
|
|
||||||
const ctx = canvasRef.current?.getContext('2d')
|
|
||||||
ctx?.clearRect(0, 0, canvasRef.current!.width, canvasRef.current!.height)
|
|
||||||
}
|
|
||||||
const handlePointerMove = (event: React.PointerEvent) => {
|
|
||||||
if (!drawing) return
|
|
||||||
const ctx = canvasRef.current?.getContext('2d')
|
|
||||||
const rect = canvasRef.current?.getBoundingClientRect()
|
|
||||||
const x = event.clientX - rect!.left
|
|
||||||
const y = event.clientY - rect!.top
|
|
||||||
|
|
||||||
ctx?.lineTo(x, y)
|
|
||||||
ctx?.stroke()
|
|
||||||
|
|
||||||
// Collect the path data
|
|
||||||
setPaths((prevPaths) => {
|
|
||||||
const newPaths = [...prevPaths]
|
|
||||||
newPaths[newPaths.length - 1] += ` L ${x} ${y}`
|
|
||||||
return newPaths
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleReset = () => {
|
|
||||||
setPaths([])
|
|
||||||
setDrawing(false)
|
|
||||||
update()
|
|
||||||
const ctx = canvasRef.current?.getContext('2d')
|
|
||||||
ctx?.clearRect(0, 0, canvasRef.current!.width, canvasRef.current!.height)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={styles.wrapper}>
|
|
||||||
<div className={styles.relative}>
|
|
||||||
<canvas
|
|
||||||
height={location?.height}
|
|
||||||
width={location?.width}
|
|
||||||
ref={canvasRef}
|
|
||||||
className={styles.canvas}
|
|
||||||
onPointerDown={handlePointerDown}
|
|
||||||
onPointerUp={handlePointerUp}
|
|
||||||
onPointerMove={handlePointerMove}
|
|
||||||
onPointerOut={handlePointerUp}
|
|
||||||
></canvas>
|
|
||||||
{typeof userMark?.mark !== 'undefined' && (
|
|
||||||
<div className={styles.absolute}>
|
|
||||||
<MarkRenderSignature value={value} mark={userMark.mark} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={styles.reset}>
|
|
||||||
<FontAwesomeIcon size="sm" icon={faEraser} onClick={handleReset} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
import { MarkRenderProps } from '../../types/mark'
|
|
||||||
|
|
||||||
export const MarkRenderSignature = ({ value, mark }: MarkRenderProps) => {
|
|
||||||
const paths = value ? JSON.parse(value) : []
|
|
||||||
|
|
||||||
return (
|
|
||||||
<svg viewBox={`0 0 ${mark.location.width} ${mark.location.height}`}>
|
|
||||||
{paths.map((path: string) => (
|
|
||||||
<path d={path} stroke="black" fill="none" />
|
|
||||||
))}
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
}
|
|
16
src/components/MarkTypeStrategy/MarkInput.tsx
Normal file
16
src/components/MarkTypeStrategy/MarkInput.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { MarkType } from '../../types/drawing'
|
||||||
|
import { MARK_TYPE_CONFIG, MarkInputProps } from './MarkStrategy'
|
||||||
|
|
||||||
|
interface MarkInputComponentProps extends MarkInputProps {
|
||||||
|
markType: MarkType
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MarkInput = ({ markType, ...rest }: MarkInputComponentProps) => {
|
||||||
|
const { input: InputComponent } = MARK_TYPE_CONFIG[markType] || {}
|
||||||
|
|
||||||
|
if (typeof InputComponent !== 'undefined') {
|
||||||
|
return <InputComponent {...rest} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
20
src/components/MarkTypeStrategy/MarkRender.tsx
Normal file
20
src/components/MarkTypeStrategy/MarkRender.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { MarkType } from '../../types/drawing'
|
||||||
|
import { MARK_TYPE_CONFIG, MarkRenderProps } from './MarkStrategy'
|
||||||
|
|
||||||
|
interface MarkRenderComponentProps extends MarkRenderProps {
|
||||||
|
markType: MarkType
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MarkRender = ({ markType, ...rest }: MarkRenderComponentProps) => {
|
||||||
|
const { render: RenderComponent } = MARK_TYPE_CONFIG[markType] || {}
|
||||||
|
|
||||||
|
if (typeof RenderComponent !== 'undefined') {
|
||||||
|
return <RenderComponent {...rest} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <DefaultRenderComponent {...rest} />
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultRenderComponent = ({ value }: MarkRenderProps) => (
|
||||||
|
<span>{value}</span>
|
||||||
|
)
|
32
src/components/MarkTypeStrategy/MarkStrategy.tsx
Normal file
32
src/components/MarkTypeStrategy/MarkStrategy.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { MarkType } from '../../types/drawing'
|
||||||
|
import { CurrentUserMark, Mark } from '../../types/mark'
|
||||||
|
import { TextStrategy } from './Text'
|
||||||
|
import { SignatureStrategy } from './Signature'
|
||||||
|
|
||||||
|
export interface MarkInputProps {
|
||||||
|
value: string
|
||||||
|
handler: (value: string) => void
|
||||||
|
placeholder?: string
|
||||||
|
userMark?: CurrentUserMark
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarkRenderProps {
|
||||||
|
value?: string
|
||||||
|
mark: Mark
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarkStrategy {
|
||||||
|
input: React.FC<MarkInputProps>
|
||||||
|
render: React.FC<MarkRenderProps>
|
||||||
|
encryptAndUpload?: (value: string, key?: string) => Promise<string>
|
||||||
|
fetchAndDecrypt?: (value: string, key?: string) => Promise<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MarkStrategies = {
|
||||||
|
[key in MarkType]?: MarkStrategy
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MARK_TYPE_CONFIG: MarkStrategies = {
|
||||||
|
[MarkType.TEXT]: TextStrategy,
|
||||||
|
[MarkType.SIGNATURE]: SignatureStrategy
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
@import '../../styles/colors.scss';
|
@import '../../../styles/colors.scss';
|
||||||
|
|
||||||
$padding: 5px;
|
$padding: 5px;
|
||||||
|
|
||||||
@ -11,10 +11,10 @@ $padding: 5px;
|
|||||||
|
|
||||||
.relative {
|
.relative {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
outline: 1px solid black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvas {
|
.canvas {
|
||||||
outline: 1px solid black;
|
|
||||||
background-color: $body-background-color;
|
background-color: $body-background-color;
|
||||||
cursor: crosshair;
|
cursor: crosshair;
|
||||||
|
|
101
src/components/MarkTypeStrategy/Signature/Input.tsx
Normal file
101
src/components/MarkTypeStrategy/Signature/Input.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { useCallback, useEffect, useRef } from 'react'
|
||||||
|
import { faEraser } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { MarkRenderSignature } from './Render'
|
||||||
|
import SignaturePad from 'signature_pad'
|
||||||
|
import { SIGNATURE_PAD_OPTIONS, SIGNATURE_PAD_SIZE } from '../../../utils/const'
|
||||||
|
import { BasicPoint } from 'signature_pad/dist/types/point'
|
||||||
|
import { MarkInputProps } from '../MarkStrategy'
|
||||||
|
import styles from './Input.module.scss'
|
||||||
|
|
||||||
|
export const MarkInputSignature = ({
|
||||||
|
value,
|
||||||
|
handler,
|
||||||
|
userMark
|
||||||
|
}: MarkInputProps) => {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||||
|
const signaturePad = useRef<SignaturePad | null>(null)
|
||||||
|
|
||||||
|
const update = useCallback(() => {
|
||||||
|
const data = signaturePad.current?.toData()
|
||||||
|
const reduced = data?.map((pg) => pg.points)
|
||||||
|
const json = JSON.stringify(reduced)
|
||||||
|
|
||||||
|
if (signaturePad.current && !signaturePad.current?.isEmpty()) {
|
||||||
|
handler(json)
|
||||||
|
} else {
|
||||||
|
handler('')
|
||||||
|
}
|
||||||
|
}, [handler])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleEndStroke = () => {
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
if (canvasRef.current) {
|
||||||
|
if (signaturePad.current === null) {
|
||||||
|
signaturePad.current = new SignaturePad(
|
||||||
|
canvasRef.current,
|
||||||
|
SIGNATURE_PAD_OPTIONS
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
signaturePad.current.addEventListener('endStroke', handleEndStroke)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('endStroke', handleEndStroke)
|
||||||
|
}
|
||||||
|
}, [update])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (signaturePad.current) {
|
||||||
|
if (value) {
|
||||||
|
signaturePad.current.fromData(
|
||||||
|
JSON.parse(value).map((p: BasicPoint[]) => ({
|
||||||
|
points: p
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
signaturePad.current?.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
update()
|
||||||
|
}, [update, value])
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
signaturePad.current?.clear()
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.wrapper}>
|
||||||
|
<div
|
||||||
|
className={styles.relative}
|
||||||
|
style={{
|
||||||
|
width: SIGNATURE_PAD_SIZE.width,
|
||||||
|
height: SIGNATURE_PAD_SIZE.height
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<canvas
|
||||||
|
width={SIGNATURE_PAD_SIZE.width}
|
||||||
|
height={SIGNATURE_PAD_SIZE.height}
|
||||||
|
ref={canvasRef}
|
||||||
|
className={styles.canvas}
|
||||||
|
></canvas>
|
||||||
|
{typeof userMark?.mark !== 'undefined' && (
|
||||||
|
<div className={styles.absolute}>
|
||||||
|
<MarkRenderSignature
|
||||||
|
key={userMark.mark.value}
|
||||||
|
value={userMark.mark.value}
|
||||||
|
mark={userMark.mark}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.reset}>
|
||||||
|
<FontAwesomeIcon size="sm" icon={faEraser} onClick={handleReset} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
.img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
27
src/components/MarkTypeStrategy/Signature/Render.tsx
Normal file
27
src/components/MarkTypeStrategy/Signature/Render.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import SignaturePad from 'signature_pad'
|
||||||
|
import { SIGNATURE_PAD_OPTIONS, SIGNATURE_PAD_SIZE } from '../../../utils'
|
||||||
|
import { BasicPoint } from 'signature_pad/dist/types/point'
|
||||||
|
import { MarkRenderProps } from '../MarkStrategy'
|
||||||
|
import styles from './Render.module.scss'
|
||||||
|
|
||||||
|
export const MarkRenderSignature = ({ value }: MarkRenderProps) => {
|
||||||
|
const [dataUrl, setDataUrl] = useState<string | undefined>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (value) {
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = SIGNATURE_PAD_SIZE.width
|
||||||
|
canvas.height = SIGNATURE_PAD_SIZE.height
|
||||||
|
const pad = new SignaturePad(canvas, SIGNATURE_PAD_OPTIONS)
|
||||||
|
pad.fromData(
|
||||||
|
JSON.parse(value).map((p: BasicPoint[]) => ({
|
||||||
|
points: p
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
setDataUrl(canvas.toDataURL('image/webp'))
|
||||||
|
}
|
||||||
|
}, [value])
|
||||||
|
|
||||||
|
return dataUrl ? <img src={dataUrl} className={styles.img} alt="" /> : null
|
||||||
|
}
|
95
src/components/MarkTypeStrategy/Signature/index.tsx
Normal file
95
src/components/MarkTypeStrategy/Signature/index.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import {
|
||||||
|
decryptArrayBuffer,
|
||||||
|
encryptArrayBuffer,
|
||||||
|
getHash,
|
||||||
|
isOnline,
|
||||||
|
uploadToFileStorage
|
||||||
|
} from '../../../utils'
|
||||||
|
import { MarkStrategy } from '../MarkStrategy'
|
||||||
|
import { MarkInputSignature } from './Input'
|
||||||
|
import { MarkRenderSignature } from './Render'
|
||||||
|
|
||||||
|
export const SignatureStrategy: MarkStrategy = {
|
||||||
|
input: MarkInputSignature,
|
||||||
|
render: MarkRenderSignature,
|
||||||
|
encryptAndUpload: async (value, encryptionKey) => {
|
||||||
|
// Value is the stringified signature object
|
||||||
|
// Encode it to the arrayBuffer
|
||||||
|
const encoder = new TextEncoder()
|
||||||
|
const uint8Array = encoder.encode(value)
|
||||||
|
|
||||||
|
if (!encryptionKey) {
|
||||||
|
throw new Error('Signature requires an encryption key')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt the file contents with the same encryption key from the create signature
|
||||||
|
const encryptedArrayBuffer = await encryptArrayBuffer(
|
||||||
|
uint8Array,
|
||||||
|
encryptionKey
|
||||||
|
)
|
||||||
|
|
||||||
|
const hash = await getHash(encryptedArrayBuffer)
|
||||||
|
if (!hash) {
|
||||||
|
throw new Error("Can't get encrypted file hash.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the encrypted json file from array buffer and hash
|
||||||
|
const file = new File([encryptedArrayBuffer], `${hash}.json`)
|
||||||
|
|
||||||
|
if (await isOnline()) {
|
||||||
|
try {
|
||||||
|
const url = await uploadToFileStorage(file)
|
||||||
|
console.info(`${file.name} uploaded to file storage`)
|
||||||
|
return url
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error(
|
||||||
|
`Error occurred in uploading file ${file.name}`,
|
||||||
|
error.message
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TOOD: offline
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
},
|
||||||
|
fetchAndDecrypt: async (value, encryptionKey) => {
|
||||||
|
if (!encryptionKey) {
|
||||||
|
throw new Error('Signature requires an encryption key')
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptedArrayBuffer = await axios.get(value, {
|
||||||
|
responseType: 'arraybuffer'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Verify hash
|
||||||
|
const parts = value.split('/')
|
||||||
|
const urlHash = parts[parts.length - 1]
|
||||||
|
const hash = await getHash(encryptedArrayBuffer.data)
|
||||||
|
if (hash !== urlHash) {
|
||||||
|
// TODO: handle hash verification failing
|
||||||
|
throw new Error('Unable to verify signature')
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrayBuffer = await decryptArrayBuffer(
|
||||||
|
encryptedArrayBuffer.data,
|
||||||
|
encryptionKey
|
||||||
|
).catch((err) => {
|
||||||
|
console.log('err in decryption:>> ', err)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (arrayBuffer) {
|
||||||
|
// decode json
|
||||||
|
const decoder = new TextDecoder()
|
||||||
|
const json = decoder.decode(arrayBuffer)
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
|
// TOOD: offline
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import { MarkInputProps } from '../../types/mark'
|
import { MarkInputProps } from '../MarkStrategy'
|
||||||
import styles from '../MarkFormField/style.module.scss'
|
import styles from '../../MarkFormField/style.module.scss'
|
||||||
|
|
||||||
export const MarkInputText = ({
|
export const MarkInputText = ({
|
||||||
value,
|
value,
|
7
src/components/MarkTypeStrategy/Text/index.tsx
Normal file
7
src/components/MarkTypeStrategy/Text/index.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { MarkStrategy } from '../MarkStrategy'
|
||||||
|
import { MarkInputText } from './Input'
|
||||||
|
|
||||||
|
export const TextStrategy: MarkStrategy = {
|
||||||
|
input: MarkInputText,
|
||||||
|
render: ({ value }) => <>{value}</>
|
||||||
|
}
|
@ -4,7 +4,7 @@ import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts'
|
|||||||
import { useScale } from '../../hooks/useScale.tsx'
|
import { useScale } from '../../hooks/useScale.tsx'
|
||||||
import { forwardRef } from 'react'
|
import { forwardRef } from 'react'
|
||||||
import { npubToHex } from '../../utils/nostr.ts'
|
import { npubToHex } from '../../utils/nostr.ts'
|
||||||
import { MARK_TYPE_CONFIG } from '../getMarkComponents.tsx'
|
import { MarkRender } from '../MarkTypeStrategy/MarkRender.tsx'
|
||||||
|
|
||||||
interface PdfMarkItemProps {
|
interface PdfMarkItemProps {
|
||||||
userMark: CurrentUserMark
|
userMark: CurrentUserMark
|
||||||
@ -28,8 +28,6 @@ const PdfMarkItem = forwardRef<HTMLDivElement, PdfMarkItemProps>(
|
|||||||
const getMarkValue = () =>
|
const getMarkValue = () =>
|
||||||
isEdited() ? selectedMarkValue : userMark.currentValue
|
isEdited() ? selectedMarkValue : userMark.currentValue
|
||||||
const { from } = useScale()
|
const { from } = useScale()
|
||||||
const { render: MarkRenderComponent } =
|
|
||||||
MARK_TYPE_CONFIG[userMark.mark.type] || {}
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@ -50,9 +48,12 @@ const PdfMarkItem = forwardRef<HTMLDivElement, PdfMarkItemProps>(
|
|||||||
fontSize: inPx(from(pageWidth, FONT_SIZE))
|
fontSize: inPx(from(pageWidth, FONT_SIZE))
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{typeof MarkRenderComponent !== 'undefined' && (
|
<MarkRender
|
||||||
<MarkRenderComponent value={getMarkValue()} mark={userMark.mark} />
|
key={getMarkValue()}
|
||||||
)}
|
markType={userMark.mark.type}
|
||||||
|
value={getMarkValue()}
|
||||||
|
mark={userMark.mark}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -117,7 +117,9 @@ const PdfMarking = (props: PdfMarkingProps) => {
|
|||||||
// setCurrentUserMarks(updatedCurrentUserMarks)
|
// setCurrentUserMarks(updatedCurrentUserMarks)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
const handleChange = (value: string) => setSelectedMarkValue(value)
|
const handleChange = (value: string) => {
|
||||||
|
setSelectedMarkValue(value)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -6,6 +6,7 @@ import { useEffect, useRef } from 'react'
|
|||||||
import pdfViewStyles from './style.module.scss'
|
import pdfViewStyles from './style.module.scss'
|
||||||
import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts'
|
import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts'
|
||||||
import { useScale } from '../../hooks/useScale.tsx'
|
import { useScale } from '../../hooks/useScale.tsx'
|
||||||
|
import { MarkRender } from '../MarkTypeStrategy/MarkRender.tsx'
|
||||||
interface PdfPageProps {
|
interface PdfPageProps {
|
||||||
fileName: string
|
fileName: string
|
||||||
pageIndex: number
|
pageIndex: number
|
||||||
@ -73,7 +74,7 @@ const PdfPageItem = ({
|
|||||||
fontSize: inPx(from(page.width, FONT_SIZE))
|
fontSize: inPx(from(page.width, FONT_SIZE))
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{m.value}
|
<MarkRender value={m.value} mark={m} markType={m.type} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
import { MarkType } from '../types/drawing'
|
|
||||||
import { MarkConfigs } from '../types/mark'
|
|
||||||
import { MarkInputSignature } from './MarkInputs/Signature'
|
|
||||||
import { MarkInputText } from './MarkInputs/Text'
|
|
||||||
import { MarkRenderSignature } from './MarkRender/Signature'
|
|
||||||
|
|
||||||
export const MARK_TYPE_CONFIG: MarkConfigs = {
|
|
||||||
[MarkType.TEXT]: {
|
|
||||||
input: MarkInputText,
|
|
||||||
render: ({ value }) => <>{value}</>
|
|
||||||
},
|
|
||||||
[MarkType.SIGNATURE]: {
|
|
||||||
input: MarkInputSignature,
|
|
||||||
render: MarkRenderSignature
|
|
||||||
}
|
|
||||||
}
|
|
@ -21,6 +21,7 @@ import { Event } from 'nostr-tools'
|
|||||||
import store from '../store/store'
|
import store from '../store/store'
|
||||||
import { NostrController } from '../controllers'
|
import { NostrController } from '../controllers'
|
||||||
import { MetaParseError } from '../types/errors/MetaParseError'
|
import { MetaParseError } from '../types/errors/MetaParseError'
|
||||||
|
import { MARK_TYPE_CONFIG } from '../components/MarkTypeStrategy/MarkStrategy'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flattened interface that combines properties `Meta`, `CreateSignatureEventContent`,
|
* Flattened interface that combines properties `Meta`, `CreateSignatureEventContent`,
|
||||||
@ -142,6 +143,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
setMarkConfig(markConfig)
|
setMarkConfig(markConfig)
|
||||||
setZipUrl(zipUrl)
|
setZipUrl(zipUrl)
|
||||||
|
|
||||||
|
let encryptionKey: string | null = null
|
||||||
if (meta.keys) {
|
if (meta.keys) {
|
||||||
const { sender, keys } = meta.keys
|
const { sender, keys } = meta.keys
|
||||||
// Retrieve the user's public key from the state
|
// Retrieve the user's public key from the state
|
||||||
@ -162,6 +164,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
encryptionKey = decrypted
|
||||||
setEncryptionKey(decrypted)
|
setEncryptionKey(decrypted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -206,13 +209,40 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedSignatureEventsMap.forEach((event, npub) => {
|
for (const [npub, event] of parsedSignatureEventsMap) {
|
||||||
const isValidSignature = verifyEvent(event)
|
const isValidSignature = verifyEvent(event)
|
||||||
if (isValidSignature) {
|
if (isValidSignature) {
|
||||||
// get the signature of prev signer from the content of current signers signedEvent
|
// get the signature of prev signer from the content of current signers signedEvent
|
||||||
const prevSignersSig = getPrevSignerSig(npub)
|
const prevSignersSig = getPrevSignerSig(npub)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const obj: SignedEventContent = JSON.parse(event.content)
|
const obj: SignedEventContent = JSON.parse(event.content)
|
||||||
|
|
||||||
|
// Signature object can include values that need to be fetched and decrypted
|
||||||
|
for (let i = 0; i < obj.marks.length; i++) {
|
||||||
|
const m = obj.marks[i]
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { fetchAndDecrypt } = MARK_TYPE_CONFIG[m.type] || {}
|
||||||
|
if (
|
||||||
|
typeof fetchAndDecrypt === 'function' &&
|
||||||
|
m.value &&
|
||||||
|
encryptionKey
|
||||||
|
) {
|
||||||
|
const decrypted = await fetchAndDecrypt(
|
||||||
|
m.value,
|
||||||
|
encryptionKey
|
||||||
|
)
|
||||||
|
obj.marks[i].value = decrypted
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Error during mark fetchAndDecrypt phase`,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
parsedSignatureEventsMap.set(npub, {
|
parsedSignatureEventsMap.set(npub, {
|
||||||
...event,
|
...event,
|
||||||
parsedContent: obj
|
parsedContent: obj
|
||||||
@ -228,7 +258,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
|||||||
signerStatusMap.set(npub as `npub1${string}`, SignStatus.Invalid)
|
signerStatusMap.set(npub as `npub1${string}`, SignStatus.Invalid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
signers
|
signers
|
||||||
.filter((s) => !parsedSignatureEventsMap.has(s))
|
.filter((s) => !parsedSignatureEventsMap.has(s))
|
||||||
|
@ -33,7 +33,8 @@ import {
|
|||||||
signEventForMetaFile,
|
signEventForMetaFile,
|
||||||
updateUsersAppData,
|
updateUsersAppData,
|
||||||
findOtherUserMarks,
|
findOtherUserMarks,
|
||||||
timeout
|
timeout,
|
||||||
|
processMarks
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import { Container } from '../../components/Container'
|
import { Container } from '../../components/Container'
|
||||||
import { DisplayMeta } from './internal/displayMeta'
|
import { DisplayMeta } from './internal/displayMeta'
|
||||||
@ -54,6 +55,7 @@ import {
|
|||||||
} from '../../utils/file.ts'
|
} from '../../utils/file.ts'
|
||||||
import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.ts'
|
import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.ts'
|
||||||
import { generateTimestamp } from '../../utils/opentimestamps.ts'
|
import { generateTimestamp } from '../../utils/opentimestamps.ts'
|
||||||
|
import { MARK_TYPE_CONFIG } from '../../components/MarkTypeStrategy/MarkStrategy.tsx'
|
||||||
|
|
||||||
enum SignedStatus {
|
enum SignedStatus {
|
||||||
Fully_Signed,
|
Fully_Signed,
|
||||||
@ -237,6 +239,43 @@ export const SignPage = () => {
|
|||||||
const signedMarks = extractMarksFromSignedMeta(meta)
|
const signedMarks = extractMarksFromSignedMeta(meta)
|
||||||
const currentUserMarks = getCurrentUserMarks(metaMarks, signedMarks)
|
const currentUserMarks = getCurrentUserMarks(metaMarks, signedMarks)
|
||||||
const otherUserMarks = findOtherUserMarks(signedMarks, usersPubkey!)
|
const otherUserMarks = findOtherUserMarks(signedMarks, usersPubkey!)
|
||||||
|
|
||||||
|
if (meta.keys) {
|
||||||
|
for (let i = 0; i < otherUserMarks.length; i++) {
|
||||||
|
const m = otherUserMarks[i]
|
||||||
|
const { sender, keys } = meta.keys
|
||||||
|
const usersNpub = hexToNpub(usersPubkey)
|
||||||
|
if (usersNpub in keys) {
|
||||||
|
const encryptionKey = await nostrController
|
||||||
|
.nip04Decrypt(sender, keys[usersNpub])
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(
|
||||||
|
'An error occurred in decrypting encryption key',
|
||||||
|
err
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { fetchAndDecrypt } = MARK_TYPE_CONFIG[m.type] || {}
|
||||||
|
if (
|
||||||
|
typeof fetchAndDecrypt === 'function' &&
|
||||||
|
m.value &&
|
||||||
|
encryptionKey
|
||||||
|
) {
|
||||||
|
const decrypted = await fetchAndDecrypt(
|
||||||
|
m.value,
|
||||||
|
encryptionKey
|
||||||
|
)
|
||||||
|
otherUserMarks[i].value = decrypted
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error during mark fetchAndDecrypt phase`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setOtherUserMarks(otherUserMarks)
|
setOtherUserMarks(otherUserMarks)
|
||||||
setCurrentUserMarks(currentUserMarks)
|
setCurrentUserMarks(currentUserMarks)
|
||||||
setIsMarksCompleted(isCurrentUserMarksComplete(currentUserMarks))
|
setIsMarksCompleted(isCurrentUserMarksComplete(currentUserMarks))
|
||||||
@ -248,6 +287,7 @@ export const SignPage = () => {
|
|||||||
if (meta) {
|
if (meta) {
|
||||||
handleUpdatedMeta(meta)
|
handleUpdatedMeta(meta)
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [meta, usersPubkey])
|
}, [meta, usersPubkey])
|
||||||
|
|
||||||
const handleDownload = async () => {
|
const handleDownload = async () => {
|
||||||
@ -552,8 +592,8 @@ export const SignPage = () => {
|
|||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Signing nostr event')
|
setLoadingSpinnerDesc('Signing nostr event')
|
||||||
|
const usersNpub = hexToNpub(usersPubkey!)
|
||||||
const prevSig = getPrevSignersSig(hexToNpub(usersPubkey!))
|
const prevSig = getPrevSignersSig(usersNpub)
|
||||||
if (!prevSig) {
|
if (!prevSig) {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
toast.error('Previous signature is invalid')
|
toast.error('Previous signature is invalid')
|
||||||
@ -562,7 +602,26 @@ export const SignPage = () => {
|
|||||||
|
|
||||||
const marks = getSignerMarksForMeta() || []
|
const marks = getSignerMarksForMeta() || []
|
||||||
|
|
||||||
const signedEvent = await signEventForMeta({ prevSig, marks })
|
let encryptionKey: string | undefined
|
||||||
|
if (meta.keys) {
|
||||||
|
const { sender, keys } = meta.keys
|
||||||
|
encryptionKey = await nostrController
|
||||||
|
.nip04Decrypt(sender, keys[usersNpub])
|
||||||
|
.catch((err) => {
|
||||||
|
// Log and display an error message if decryption fails
|
||||||
|
console.log('An error occurred in decrypting encryption key', err)
|
||||||
|
toast.error('An error occurred in decrypting encryption key')
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const processedMarks = await processMarks(marks, encryptionKey)
|
||||||
|
|
||||||
|
const signedEvent = await signEventForMeta({
|
||||||
|
prevSig,
|
||||||
|
marks: processedMarks
|
||||||
|
})
|
||||||
|
|
||||||
if (!signedEvent) return
|
if (!signedEvent) return
|
||||||
|
|
||||||
const updatedMeta = updateMetaSignatures(meta, signedEvent)
|
const updatedMeta = updateMetaSignatures(meta, signedEvent)
|
||||||
|
@ -55,7 +55,7 @@ import {
|
|||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import { upgradeAndVerifyTimestamp } from '../../utils/opentimestamps.ts'
|
import { upgradeAndVerifyTimestamp } from '../../utils/opentimestamps.ts'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { MARK_TYPE_CONFIG } from '../../components/getMarkComponents.tsx'
|
import { MarkRender } from '../../components/MarkTypeStrategy/MarkRender.tsx'
|
||||||
|
|
||||||
interface PdfViewProps {
|
interface PdfViewProps {
|
||||||
files: CurrentUserFile[]
|
files: CurrentUserFile[]
|
||||||
@ -115,8 +115,6 @@ const SlimPdfView = ({
|
|||||||
alt={`page ${i} of ${file.name}`}
|
alt={`page ${i} of ${file.name}`}
|
||||||
/>
|
/>
|
||||||
{marks.map((m) => {
|
{marks.map((m) => {
|
||||||
const { render: MarkRenderComponent } =
|
|
||||||
MARK_TYPE_CONFIG[m.type] || {}
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`file-mark ${styles.mark}`}
|
className={`file-mark ${styles.mark}`}
|
||||||
@ -132,9 +130,11 @@ const SlimPdfView = ({
|
|||||||
fontSize: inPx(from(page.width, FONT_SIZE))
|
fontSize: inPx(from(page.width, FONT_SIZE))
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{typeof MarkRenderComponent !== 'undefined' && (
|
<MarkRender
|
||||||
<MarkRenderComponent value={m.value} mark={m} />
|
markType={m.type}
|
||||||
)}
|
value={m.value}
|
||||||
|
mark={m}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@ -367,82 +367,77 @@ export const VerifyPage = () => {
|
|||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Fetching file from file server')
|
setLoadingSpinnerDesc('Fetching file from file server')
|
||||||
axios
|
try {
|
||||||
.get(zipUrl, {
|
const res = await axios.get(zipUrl, {
|
||||||
responseType: 'arraybuffer'
|
responseType: 'arraybuffer'
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
|
||||||
const fileName = zipUrl.split('/').pop()
|
|
||||||
const file = new File([res.data], fileName!)
|
|
||||||
|
|
||||||
const encryptedArrayBuffer = await file.arrayBuffer()
|
const fileName = zipUrl.split('/').pop()
|
||||||
const arrayBuffer = await decryptArrayBuffer(
|
const file = new File([res.data], fileName!)
|
||||||
encryptedArrayBuffer,
|
|
||||||
encryptionKey
|
const encryptedArrayBuffer = await file.arrayBuffer()
|
||||||
).catch((err) => {
|
const arrayBuffer = await decryptArrayBuffer(
|
||||||
console.log('err in decryption:>> ', err)
|
encryptedArrayBuffer,
|
||||||
|
encryptionKey
|
||||||
|
).catch((err) => {
|
||||||
|
console.log('err in decryption:>> ', err)
|
||||||
|
toast.error(err.message || 'An error occurred in decrypting file.')
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (arrayBuffer) {
|
||||||
|
const zip = await JSZip.loadAsync(arrayBuffer).catch((err) => {
|
||||||
|
console.log('err in loading zip file :>> ', err)
|
||||||
toast.error(
|
toast.error(
|
||||||
err.message || 'An error occurred in decrypting file.'
|
err.message || 'An error occurred in loading zip file.'
|
||||||
)
|
)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
if (arrayBuffer) {
|
if (!zip) return
|
||||||
const zip = await JSZip.loadAsync(arrayBuffer).catch((err) => {
|
|
||||||
console.log('err in loading zip file :>> ', err)
|
|
||||||
toast.error(
|
|
||||||
err.message || 'An error occurred in loading zip file.'
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!zip) return
|
const files: { [fileName: string]: SigitFile } = {}
|
||||||
|
const fileHashes: { [key: string]: string | null } = {}
|
||||||
|
const fileNames = Object.values(zip.files).map(
|
||||||
|
(entry) => entry.name
|
||||||
|
)
|
||||||
|
|
||||||
const files: { [fileName: string]: SigitFile } = {}
|
// generate hashes for all entries in files folder of zipArchive
|
||||||
const fileHashes: { [key: string]: string | null } = {}
|
// these hashes can be used to verify the originality of files
|
||||||
const fileNames = Object.values(zip.files).map(
|
for (const fileName of fileNames) {
|
||||||
(entry) => entry.name
|
const arrayBuffer = await readContentOfZipEntry(
|
||||||
|
zip,
|
||||||
|
fileName,
|
||||||
|
'arraybuffer'
|
||||||
)
|
)
|
||||||
|
|
||||||
// generate hashes for all entries in files folder of zipArchive
|
if (arrayBuffer) {
|
||||||
// these hashes can be used to verify the originality of files
|
files[fileName] = await convertToSigitFile(
|
||||||
for (const fileName of fileNames) {
|
arrayBuffer,
|
||||||
const arrayBuffer = await readContentOfZipEntry(
|
fileName!
|
||||||
zip,
|
|
||||||
fileName,
|
|
||||||
'arraybuffer'
|
|
||||||
)
|
)
|
||||||
|
const hash = await getHash(arrayBuffer)
|
||||||
|
|
||||||
if (arrayBuffer) {
|
if (hash) {
|
||||||
files[fileName] = await convertToSigitFile(
|
fileHashes[fileName.replace(/^files\//, '')] = hash
|
||||||
arrayBuffer,
|
|
||||||
fileName!
|
|
||||||
)
|
|
||||||
const hash = await getHash(arrayBuffer)
|
|
||||||
|
|
||||||
if (hash) {
|
|
||||||
fileHashes[fileName.replace(/^files\//, '')] = hash
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fileHashes[fileName.replace(/^files\//, '')] = null
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
fileHashes[fileName.replace(/^files\//, '')] = null
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentFileHashes(fileHashes)
|
|
||||||
setFiles(files)
|
|
||||||
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch((err) => {
|
setCurrentFileHashes(fileHashes)
|
||||||
console.error(`error occurred in getting file from ${zipUrl}`, err)
|
setFiles(files)
|
||||||
toast.error(
|
|
||||||
err.message || `error occurred in getting file from ${zipUrl}`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
})
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const message = `error occurred in getting file from ${zipUrl}`
|
||||||
|
console.error(message, err)
|
||||||
|
if (err instanceof Error) toast.error(err.message)
|
||||||
|
else toast.error(message)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
processSigit()
|
processSigit()
|
||||||
|
@ -1,16 +1,4 @@
|
|||||||
import { CreatePage } from '../pages/create'
|
|
||||||
import { HomePage } from '../pages/home'
|
|
||||||
import { LandingPage } from '../pages/landing'
|
|
||||||
import { ProfilePage } from '../pages/profile'
|
|
||||||
import { SettingsPage } from '../pages/settings/Settings'
|
|
||||||
import { CacheSettingsPage } from '../pages/settings/cache'
|
|
||||||
import { NostrLoginPage } from '../pages/settings/nostrLogin'
|
|
||||||
import { ProfileSettingsPage } from '../pages/settings/profile'
|
|
||||||
import { RelaysPage } from '../pages/settings/relays'
|
|
||||||
import { SignPage } from '../pages/sign'
|
|
||||||
import { VerifyPage } from '../pages/verify'
|
|
||||||
import { hexToNpub } from '../utils'
|
import { hexToNpub } from '../utils'
|
||||||
import { Route, RouteProps } from 'react-router-dom'
|
|
||||||
|
|
||||||
export const appPrivateRoutes = {
|
export const appPrivateRoutes = {
|
||||||
homePage: '/',
|
homePage: '/',
|
||||||
@ -39,93 +27,3 @@ export const getProfileRoute = (hexKey: string) =>
|
|||||||
|
|
||||||
export const getProfileSettingsRoute = (hexKey: string) =>
|
export const getProfileSettingsRoute = (hexKey: string) =>
|
||||||
appPrivateRoutes.profileSettings.replace(':npub', hexToNpub(hexKey))
|
appPrivateRoutes.profileSettings.replace(':npub', hexToNpub(hexKey))
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper type allows for extending react-router-dom's **RouteProps** with generic type
|
|
||||||
*/
|
|
||||||
type CustomRouteProps<T> = T &
|
|
||||||
Omit<RouteProps, 'children'> & {
|
|
||||||
children?: Array<CustomRouteProps<T>>
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function maps over nested routes with optional condition for rendering
|
|
||||||
* @param {CustomRouteProps<T>[]} routes - routes list
|
|
||||||
* @param {RenderConditionCallbackFn} renderConditionCallbackFn - render condition callback (default true)
|
|
||||||
*/
|
|
||||||
export function recursiveRouteRenderer<T>(
|
|
||||||
routes?: CustomRouteProps<T>[],
|
|
||||||
renderConditionCallbackFn: (route: CustomRouteProps<T>) => boolean = () =>
|
|
||||||
true
|
|
||||||
) {
|
|
||||||
if (!routes) return null
|
|
||||||
|
|
||||||
// Callback allows us to pass arbitrary conditions for each route's rendering
|
|
||||||
// Skipping the callback will by default evaluate to true (show route)
|
|
||||||
return routes.map((route, index) =>
|
|
||||||
renderConditionCallbackFn(route) ? (
|
|
||||||
<Route
|
|
||||||
key={`${route.path}${index}`}
|
|
||||||
path={route.path}
|
|
||||||
element={route.element}
|
|
||||||
>
|
|
||||||
{recursiveRouteRenderer(route.children, renderConditionCallbackFn)}
|
|
||||||
</Route>
|
|
||||||
) : null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type PublicRouteProps = CustomRouteProps<{
|
|
||||||
hiddenWhenLoggedIn?: boolean
|
|
||||||
}>
|
|
||||||
|
|
||||||
export const publicRoutes: PublicRouteProps[] = [
|
|
||||||
{
|
|
||||||
path: appPublicRoutes.landingPage,
|
|
||||||
hiddenWhenLoggedIn: true,
|
|
||||||
element: <LandingPage />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: appPublicRoutes.profile,
|
|
||||||
element: <ProfilePage />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: appPublicRoutes.verify,
|
|
||||||
element: <VerifyPage />
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
export const privateRoutes = [
|
|
||||||
{
|
|
||||||
path: appPrivateRoutes.homePage,
|
|
||||||
element: <HomePage />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: appPrivateRoutes.create,
|
|
||||||
element: <CreatePage />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: `${appPrivateRoutes.sign}/:id?`,
|
|
||||||
element: <SignPage />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: appPrivateRoutes.settings,
|
|
||||||
element: <SettingsPage />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: appPrivateRoutes.profileSettings,
|
|
||||||
element: <ProfileSettingsPage />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: appPrivateRoutes.cacheSettings,
|
|
||||||
element: <CacheSettingsPage />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: appPrivateRoutes.relays,
|
|
||||||
element: <RelaysPage />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: appPrivateRoutes.nostrLogin,
|
|
||||||
element: <NostrLoginPage />
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
103
src/routes/util.tsx
Normal file
103
src/routes/util.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { Route, RouteProps } from 'react-router-dom'
|
||||||
|
import { appPrivateRoutes, appPublicRoutes } from '.'
|
||||||
|
import { CreatePage } from '../pages/create'
|
||||||
|
import { HomePage } from '../pages/home'
|
||||||
|
import { LandingPage } from '../pages/landing'
|
||||||
|
import { ProfilePage } from '../pages/profile'
|
||||||
|
import { CacheSettingsPage } from '../pages/settings/cache'
|
||||||
|
import { NostrLoginPage } from '../pages/settings/nostrLogin'
|
||||||
|
import { ProfileSettingsPage } from '../pages/settings/profile'
|
||||||
|
import { RelaysPage } from '../pages/settings/relays'
|
||||||
|
import { SettingsPage } from '../pages/settings/Settings'
|
||||||
|
import { SignPage } from '../pages/sign'
|
||||||
|
import { VerifyPage } from '../pages/verify'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper type allows for extending react-router-dom's **RouteProps** with generic type
|
||||||
|
*/
|
||||||
|
type CustomRouteProps<T> = T &
|
||||||
|
Omit<RouteProps, 'children'> & {
|
||||||
|
children?: Array<CustomRouteProps<T>>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function maps over nested routes with optional condition for rendering
|
||||||
|
* @param {CustomRouteProps<T>[]} routes - routes list
|
||||||
|
* @param {RenderConditionCallbackFn} renderConditionCallbackFn - render condition callback (default true)
|
||||||
|
*/
|
||||||
|
export function recursiveRouteRenderer<T>(
|
||||||
|
routes?: CustomRouteProps<T>[],
|
||||||
|
renderConditionCallbackFn: (route: CustomRouteProps<T>) => boolean = () =>
|
||||||
|
true
|
||||||
|
) {
|
||||||
|
if (!routes) return null
|
||||||
|
|
||||||
|
// Callback allows us to pass arbitrary conditions for each route's rendering
|
||||||
|
// Skipping the callback will by default evaluate to true (show route)
|
||||||
|
return routes.map((route, index) =>
|
||||||
|
renderConditionCallbackFn(route) ? (
|
||||||
|
<Route
|
||||||
|
key={`${route.path}${index}`}
|
||||||
|
path={route.path}
|
||||||
|
element={route.element}
|
||||||
|
>
|
||||||
|
{recursiveRouteRenderer(route.children, renderConditionCallbackFn)}
|
||||||
|
</Route>
|
||||||
|
) : null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type PublicRouteProps = CustomRouteProps<{
|
||||||
|
hiddenWhenLoggedIn?: boolean
|
||||||
|
}>
|
||||||
|
|
||||||
|
export const publicRoutes: PublicRouteProps[] = [
|
||||||
|
{
|
||||||
|
path: appPublicRoutes.landingPage,
|
||||||
|
hiddenWhenLoggedIn: true,
|
||||||
|
element: <LandingPage />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: appPublicRoutes.profile,
|
||||||
|
element: <ProfilePage />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: appPublicRoutes.verify,
|
||||||
|
element: <VerifyPage />
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export const privateRoutes = [
|
||||||
|
{
|
||||||
|
path: appPrivateRoutes.homePage,
|
||||||
|
element: <HomePage />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: appPrivateRoutes.create,
|
||||||
|
element: <CreatePage />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `${appPrivateRoutes.sign}/:id?`,
|
||||||
|
element: <SignPage />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: appPrivateRoutes.settings,
|
||||||
|
element: <SettingsPage />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: appPrivateRoutes.profileSettings,
|
||||||
|
element: <ProfileSettingsPage />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: appPrivateRoutes.cacheSettings,
|
||||||
|
element: <CacheSettingsPage />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: appPrivateRoutes.relays,
|
||||||
|
element: <RelaysPage />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: appPrivateRoutes.nostrLogin,
|
||||||
|
element: <NostrLoginPage />
|
||||||
|
}
|
||||||
|
]
|
@ -28,24 +28,3 @@ export interface MarkRect {
|
|||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MarkInputProps {
|
|
||||||
value: string
|
|
||||||
handler: (value: string) => void
|
|
||||||
placeholder?: string
|
|
||||||
userMark?: CurrentUserMark
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MarkRenderProps {
|
|
||||||
value?: string
|
|
||||||
mark: Mark
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MarkConfig {
|
|
||||||
input: React.FC<MarkInputProps>
|
|
||||||
render?: React.FC<MarkRenderProps>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MarkConfigs = {
|
|
||||||
[key in MarkType]?: MarkConfig
|
|
||||||
}
|
|
||||||
|
@ -112,3 +112,13 @@ export const MOST_COMMON_MEDIA_TYPES = new Map([
|
|||||||
['3g2', 'video/3gpp2'], // 3GPP2 audio/video container
|
['3g2', 'video/3gpp2'], // 3GPP2 audio/video container
|
||||||
['7z', 'application/x-7z-compressed'] // 7-zip archive
|
['7z', 'application/x-7z-compressed'] // 7-zip archive
|
||||||
])
|
])
|
||||||
|
|
||||||
|
export const SIGNATURE_PAD_OPTIONS = {
|
||||||
|
minWidth: 0.5,
|
||||||
|
maxWidth: 3
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const SIGNATURE_PAD_SIZE = {
|
||||||
|
width: 600,
|
||||||
|
height: 300
|
||||||
|
}
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
|
import { MARK_TYPE_CONFIG } from '../components/MarkTypeStrategy/MarkStrategy.tsx'
|
||||||
|
import { NostrController } from '../controllers/NostrController.ts'
|
||||||
|
import store from '../store/store.ts'
|
||||||
import { Meta } from '../types'
|
import { Meta } from '../types'
|
||||||
import { PdfPage } from '../types/drawing.ts'
|
import { PdfPage } from '../types/drawing.ts'
|
||||||
import { MOST_COMMON_MEDIA_TYPES } from './const.ts'
|
import { MOST_COMMON_MEDIA_TYPES } from './const.ts'
|
||||||
import { extractMarksFromSignedMeta } from './mark.ts'
|
import { extractMarksFromSignedMeta } from './mark.ts'
|
||||||
|
import { hexToNpub } from './nostr.ts'
|
||||||
import {
|
import {
|
||||||
addMarks,
|
addMarks,
|
||||||
groupMarksByFileNamePage,
|
groupMarksByFileNamePage,
|
||||||
@ -21,7 +25,49 @@ export const getZipWithFiles = async (
|
|||||||
for (const [fileName, file] of Object.entries(files)) {
|
for (const [fileName, file] of Object.entries(files)) {
|
||||||
// Handle PDF Files, add marks
|
// Handle PDF Files, add marks
|
||||||
if (file.isPdf && fileName in marksByFileNamePage) {
|
if (file.isPdf && fileName in marksByFileNamePage) {
|
||||||
const blob = await addMarks(file, marksByFileNamePage[fileName])
|
const marksToAdd = marksByFileNamePage[fileName]
|
||||||
|
if (meta.keys) {
|
||||||
|
for (let i = 0; i < marks.length; i++) {
|
||||||
|
const m = marks[i]
|
||||||
|
const { sender, keys } = meta.keys
|
||||||
|
const usersPubkey = store.getState().auth.usersPubkey!
|
||||||
|
const usersNpub = hexToNpub(usersPubkey)
|
||||||
|
if (usersNpub in keys) {
|
||||||
|
const encryptionKey = await NostrController.getInstance()
|
||||||
|
.nip04Decrypt(sender, keys[usersNpub])
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(
|
||||||
|
'An error occurred in decrypting encryption key',
|
||||||
|
err
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { fetchAndDecrypt } = MARK_TYPE_CONFIG[m.type] || {}
|
||||||
|
if (
|
||||||
|
typeof fetchAndDecrypt === 'function' &&
|
||||||
|
m.value &&
|
||||||
|
encryptionKey
|
||||||
|
) {
|
||||||
|
// Fetch and decrypt the original file
|
||||||
|
const link = m.value.split('/')
|
||||||
|
const decrypted = await fetchAndDecrypt(m.value, encryptionKey)
|
||||||
|
|
||||||
|
// Save decrypted
|
||||||
|
zip.file(
|
||||||
|
`signatures/${link[link.length - 1]}.json`,
|
||||||
|
new Blob([decrypted])
|
||||||
|
)
|
||||||
|
marks[i].value = decrypted
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error during mark fetchAndDecrypt phase`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const blob = await addMarks(file, marksToAdd)
|
||||||
zip.file(`marked/${fileName}`, blob)
|
zip.file(`marked/${fileName}`, blob)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,3 +11,4 @@ export * from './string'
|
|||||||
export * from './url'
|
export * from './url'
|
||||||
export * from './utils'
|
export * from './utils'
|
||||||
export * from './zip'
|
export * from './zip'
|
||||||
|
export * from './const'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { CurrentUserMark, Mark, MarkLocation } from '../types/mark.ts'
|
import { CurrentUserMark, Mark } from '../types/mark.ts'
|
||||||
import { hexToNpub } from './nostr.ts'
|
import { hexToNpub } from './nostr.ts'
|
||||||
import { Meta, SignedEventContent } from '../types'
|
import { Meta, SignedEventContent } from '../types'
|
||||||
import { Event } from 'nostr-tools'
|
import { Event } from 'nostr-tools'
|
||||||
@ -24,7 +24,7 @@ import {
|
|||||||
faStamp,
|
faStamp,
|
||||||
faTableCellsLarge
|
faTableCellsLarge
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import { Config, optimize } from 'svgo'
|
import { MARK_TYPE_CONFIG } from '../components/MarkTypeStrategy/MarkStrategy.tsx'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes in an array of Marks already filtered by User.
|
* Takes in an array of Marks already filtered by User.
|
||||||
@ -266,22 +266,38 @@ export const getToolboxLabelByMarkType = (markType: MarkType) => {
|
|||||||
return DEFAULT_TOOLBOX.find((t) => t.identifier === markType)?.label
|
return DEFAULT_TOOLBOX.find((t) => t.identifier === markType)?.label
|
||||||
}
|
}
|
||||||
|
|
||||||
export const optimizeSVG = (location: MarkLocation, paths: string[]) => {
|
export const getOptimizedPathsWithStrokeWidth = (svgString: string) => {
|
||||||
const svgContent = `<svg xmlns="http://www.w3.org/2000/svg" width="${location.width}" height="${location.height}">${paths.map((path) => `<path d="${path}" stroke="black" fill="none" />`).join('')}</svg>`
|
const parser = new DOMParser()
|
||||||
const optimizedSVG = optimize(svgContent, {
|
const xmlDoc = parser.parseFromString(svgString, 'image/svg+xml')
|
||||||
multipass: true, // Optimize multiple times if needed
|
const paths = xmlDoc.querySelectorAll('path')
|
||||||
floatPrecision: 2 // Adjust precision
|
const tuples: string[][] = []
|
||||||
} as Config)
|
paths.forEach((path) => {
|
||||||
|
const d = path.getAttribute('d') ?? ''
|
||||||
|
const strokeWidth = path.getAttribute('stroke-width') ?? ''
|
||||||
|
tuples.push([d, strokeWidth])
|
||||||
|
})
|
||||||
|
|
||||||
return optimizedSVG.data
|
return tuples
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getOptimizedPaths = (svgString: string) => {
|
export const processMarks = async (marks: Mark[], encryptionKey?: string) => {
|
||||||
const regex = / d="([^"]*)"/g
|
const _marks = [...marks]
|
||||||
const matches = [...svgString.matchAll(regex)]
|
for (let i = 0; i < _marks.length; i++) {
|
||||||
const pathValues = matches.map((match) => match[1])
|
const mark = _marks[i]
|
||||||
|
const hasProcess =
|
||||||
|
mark.type in MARK_TYPE_CONFIG &&
|
||||||
|
typeof MARK_TYPE_CONFIG[mark.type]?.encryptAndUpload === 'function'
|
||||||
|
|
||||||
return pathValues
|
if (hasProcess) {
|
||||||
|
const value = mark.value!
|
||||||
|
const processFn = MARK_TYPE_CONFIG[mark.type]?.encryptAndUpload
|
||||||
|
if (processFn) {
|
||||||
|
mark.value = await processFn(value, encryptionKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _marks
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -11,6 +11,9 @@ if (!PDFJS.GlobalWorkerOptions.workerPort) {
|
|||||||
|
|
||||||
import fontkit from '@pdf-lib/fontkit'
|
import fontkit from '@pdf-lib/fontkit'
|
||||||
import defaultFont from '../assets/fonts/roboto-regular.ttf'
|
import defaultFont from '../assets/fonts/roboto-regular.ttf'
|
||||||
|
import { BasicPoint } from 'signature_pad/dist/types/point'
|
||||||
|
import SignaturePad from 'signature_pad'
|
||||||
|
import { SIGNATURE_PAD_OPTIONS, SIGNATURE_PAD_SIZE } from './const.ts'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defined font size used when generating a PDF. Currently it is difficult to fully
|
* Defined font size used when generating a PDF. Currently it is difficult to fully
|
||||||
@ -132,17 +135,18 @@ export const addMarks = async (
|
|||||||
|
|
||||||
for (let i = 0; i < pages.length; i++) {
|
for (let i = 0; i < pages.length; i++) {
|
||||||
if (marksPerPage && Object.hasOwn(marksPerPage, i)) {
|
if (marksPerPage && Object.hasOwn(marksPerPage, i)) {
|
||||||
marksPerPage[i]?.forEach((mark) => {
|
for (let j = 0; j < marksPerPage[i].length; j++) {
|
||||||
|
const mark = marksPerPage[i][j]
|
||||||
switch (mark.type) {
|
switch (mark.type) {
|
||||||
case MarkType.SIGNATURE:
|
case MarkType.SIGNATURE:
|
||||||
drawSignatureText(mark, pages[i])
|
await embedSignaturePng(mark, pages[i], pdf)
|
||||||
break
|
break
|
||||||
|
|
||||||
default:
|
default:
|
||||||
drawMarkText(mark, pages[i], robotoFont)
|
drawMarkText(mark, pages[i], robotoFont)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,18 +258,41 @@ async function embedFont(pdf: PDFDocument) {
|
|||||||
return embeddedFont
|
return embeddedFont
|
||||||
}
|
}
|
||||||
|
|
||||||
const drawSignatureText = (mark: Mark, page: PDFPage) => {
|
const embedSignaturePng = async (
|
||||||
|
mark: Mark,
|
||||||
|
page: PDFPage,
|
||||||
|
pdf: PDFDocument
|
||||||
|
) => {
|
||||||
const { location } = mark
|
const { location } = mark
|
||||||
const { height } = page.getSize()
|
const { height } = page.getSize()
|
||||||
|
|
||||||
// Convert the mark location origin (top, left) to PDF origin (bottom, left)
|
|
||||||
const x = location.left
|
|
||||||
const y = height - location.top
|
|
||||||
|
|
||||||
if (hasValue(mark)) {
|
if (hasValue(mark)) {
|
||||||
const paths = JSON.parse(mark.value!)
|
const data = JSON.parse(mark.value!).map((p: BasicPoint[]) => ({
|
||||||
paths.forEach((d: string) => {
|
points: p
|
||||||
page.drawSvgPath(d, { x, y })
|
}))
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = SIGNATURE_PAD_SIZE.width
|
||||||
|
canvas.height = SIGNATURE_PAD_SIZE.height
|
||||||
|
const pad = new SignaturePad(canvas, SIGNATURE_PAD_OPTIONS)
|
||||||
|
pad.fromData(data)
|
||||||
|
const signatureImage = await pdf.embedPng(pad.toDataURL())
|
||||||
|
|
||||||
|
const scaled = signatureImage.scaleToFit(location.width, location.height)
|
||||||
|
|
||||||
|
// Convert the mark location origin (top, left) to PDF origin (bottom, left)
|
||||||
|
// and center the image
|
||||||
|
const x = location.left + (location.width - scaled.width) / 2
|
||||||
|
const y =
|
||||||
|
height -
|
||||||
|
location.top -
|
||||||
|
location.height +
|
||||||
|
(location.height - scaled.height) / 2
|
||||||
|
|
||||||
|
page.drawImage(signatureImage, {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width: scaled.width,
|
||||||
|
height: scaled.height
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user